PowerShellで苦労して作るシンボリックリンク

# そうだ、シンボリックリンクを作ろう
## シンボリックリンクを作る目的
### ある日の出来事
ある日、HDDの容量が一杯になった。
撮りためていた動画ファイルで容量がいっぱいになったのだ。
保存されているファイルはどれも「必要だけど移動させる予定はない」。
リードオンリーで使用するファイルばっかりだ。
でも、それらのファイルは「フォルダで分けて分類してある」。
新しくHDDを買って、その新しいHDDに分類していくのもいいが、
「これまで分類してきたファイルと同階層に置きたい」。

### どうしようかな？
記憶域やRAIDを利用した大規模容量を用意し、録画ファイルをそこに置くことにするのが根本解決になるだろう。
とはいえ、下記のような問題点があった。

* 大容量HDDの購入費用が大きい。  
  今現在使用しているものはもうすでにいっぱいなので使えない。  
  記憶域やRAIDを構成するなら、構成する際に空っぽにしなければならない都合上、  
  手持ち品は使えないので最低でもHDDを2台ほど用意しなければならないだろう。  
  お小遣い制度を導入している場合、急な出費は厳しいのでできるだけ小さくしたい。
* ファイルの移動時間がかかる。  
  これはもっと大きな問題だ。
  3TBにも膨れ上がった録画ファイルを移動するのは何日かかるだろうか。  
  今こうしている最中も録画ファイルは溜まっていく予定なのに！

### シンボリックリンクを使おう！
これらの問題(特に金銭)を解決する手段として、シンボリックリンクを使うことにした。
新規のHDDを1台購入し、そこに対して既存のHDDに入っているファイルをすべてシンボリックリンク扱いでコピーする。
拙い図で表すとこんな感じだ。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/196422/a0d85cf9-c619-e91f-f016-65fe97a82af9.png)

## 実際にスクリプトを書こう！
### テスト環境を作成する
実際にコマンドを適用する前に、テストをしたい。
テスト環境を以下のように作成した。

```
F:\record
├─テスト用from
│  ├─カードキャプターさくら
│  │      1話「さくらと透明なカード」[疾風 -GALE-].txt
│  │      2話「さくらと出口のない部屋」[包囲 -SIEGE-].txt
│  │
│  └─魔法少女まどか☆マギカ
│          1話「夢の中で逢った、ような・・・・・」.txt
│          2話「それはとっても嬉しいなって」.txt
│
└─テスト用to
```

ここでは、テスト用fromは既存HDD、テスト用toは新規HDDに当たるものとして扱うことにした。
またそれぞれのファイルの内容は問題ではないため、txtファイルとした。

### ディレクトリ構造をコピーする
とりあえず、最初にディレクトリ構造だけをコピーしたい。
これは単純にxcopyを使用すれば良い。(またはrobocopyでも良い。ここではxcopyとした)

```powershell
# 既存HDDと新規HDDとに当たるPathを変数として宣言しておく。
$fromPath = 'F:\record\テスト用from'
$toPath = 'F:\record\テスト用to'

# まず、フォルダ階層をコピーする
XCOPY $fromPath $toPath /T /B
```

ここまでを実行すると以下のようになった。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/196422/fc75f0df-ef82-61b4-badb-b1e19bac0e68.png)
後はシンボリックリンクを配置するだけだ。

### シンボリックリンクの作成
シンボリックリンクは下記の構文で作成できる。

```powershell
New-Item -Value 'シンボリックリンクのリンク先パス' -Path 'シンボリックリンクの作成先ディレクトリ' -Name 'シンボリックリンクのファイル名' -ItemType SymbolicLink
```

ここで用意しなければならない値は、リンク対象ファイルのパスとリンクの作成場所、及びリンク自体の名前だ。
ここでは下記の通りとした。

* シンボリックリンクのリンク先パス:既存HDD内のファイル
* シンボリックリンクの作成先ディレクトリ:新規HDD内の同階層にあるディレクトリ
* シンボリックリンクのファイル名：既存HDD内の拡張子を含めたファイル名+".symlink"

試しに、テスト用ファイルの一つに対してシンボリックリンクを作成してみる。

```powershell
New-Item -Value 'F:\record\テスト用from\魔法少女まどか☆マギカ\1話「夢の中で逢った、ような・・・・・」.txt' -Path 'F:\record\テスト用to\魔法少女まどか☆マギカ' -Name '1話「夢の中で逢った、ような・・・・・」.txt.symlink' -ItemType SymbolicLink
```
下記のように作成できた。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/196422/e8cd0349-0016-98ac-ff30-b1d6567bad11.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/196422/363fd265-1d05-2a87-0ea3-5ca9742d10c2.png)

### すべてのファイルに対してリンクを作成する
シンボリックリンクを作成することはできたが、いちいち手動でパスを設定しているのでは、とても面倒だ！
まずは、設定したいパラメータを取得するための、ファイルオブジェクトを取得することから始めよう。

```powershell
# -Recurseでサブフォルダまで検索、Where-Objectでディレクトリを対象外とする
Get-ChildItem $fromPath -Recurse | Where-Object {$_.PsIsContainer -eq $False}
```

下記のようにオブジェクトが取得できた。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/196422/c0a60692-4075-65cc-9506-e0ff64219f07.png)

次に、それぞれの設定したいパラメータを出力してみる。

```powershell
Get-ChildItem $fromPath -Recurse | Where-Object {$_.PsIsContainer -eq $False} | ForEach-Object {

    # シンボリックリンクのリンク先パス
    Write-Output $_.FullName

    # シンボリックリンクの作成先ディレクトリ
    # fromPath部分をtoPathに差し替え、ファイル名部分を削除する
    $tempPath = $toPath + $_.FullName.Substring($fromPath.Length)
    Write-Output $tempPath.Substring(0, $tempPath.Length - $_.Name.Length)

    # シンボリックリンクのファイル名
    Write-Output ($_.Name + '.symlink')

}
```

実行してみると下記のように表示することができた。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/196422/a8294f1d-c6e9-06f0-9d1e-ec110ccfd706.png)

### パラメータを設定してみる
実際にパラメータを設定してシンボリックリンクを作成する。

```powershell
Get-ChildItem $fromPath -Recurse | Where-Object {$_.PsIsContainer -eq $False} | ForEach-Object {

    # シンボリックリンクのリンク先パス
    $targetPath = $_.FullName

    # シンボリックリンクの作成先ディレクトリ
    # fromPath部分をtoPathに差し替え、ファイル名部分を削除する
    $tempPath = $toPath + $_.FullName.Substring($fromPath.Length)
    $filePath = $tempPath.Substring(0, $tempPath.Length - ($_.Name.Length +1))

    # シンボリックリンクのファイル名
    $fileName = ($_.Name + '.symlink')

    New-Item -Path $filePath -Value $targetPath -Name $fileName -ItemType SymbolicLink
}
```

このコードは一見正しいように見える。
が、実際に実行してみると半分はエラーが出る。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/196422/4e88e95c-0775-e502-b500-74d8af41d990.png)

## エラーの謎
### 犯人は誰か？
半分はシンボリックリンクの作成に成功し、残りの半分はエラーが起きた。
これは、対象ファイルパス、もしくは対象ファイル名になにか問題がある、ということだろう。
エラーが起きるパスを直接指定してコマンドを実行してみる。

```powershell
New-Item -Path 'F:\record\テスト用to\カードキャプターさくら\' -Value 'F:\record\テスト用from\カードキャプターさくら\1話「さくらと透明なカード」[疾風 -GALE-].txt' -Name '1話「さくらと透明なカード」[疾風 -GALE-].txt.symlink' -ItemType SymbolicLink
```

結果はエラーだ。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/196422/29436b33-97e3-520d-b6a1-cbd0427bcd9d.png)

メッセージを見ると、「パス 'F:\record\テスト用from\カードキャプターさくら\1話「さくらと透明なカード」[疾風 -GALE-].txt' が存在しないため検出できません。」と記述されている。
これは、Valueに設定した値だ。

ここで、PowerShellの補完機能を使ってみる。
すると、下記のように補完され、コマンドを実行することができた。
何が違うかわかるだろうか？
![image.png](https://qiita-image-store.s3.amazonaws.com/0/196422/35e90f9a-c646-1315-07bc-40f33930e752.png)

並べてみるとわかりやすい。

```powershell
New-Item -Path 'F:\record\テスト用to\カードキャプターさくら\' -Value 'F:\record\テスト用from\カードキャプターさくら\1話「さくらと透明なカード」[疾風 -GALE-].txt' -Name '1話「さくらと透明なカード」[疾風 -GALE-].txt.symlink' -ItemType SymbolicLink
New-Item -Path 'F:\record\テスト用to\カードキャプターさくら\' -Value 'F:\record\テスト用from\カードキャプターさくら\1話「さくらと透明なカード」`[疾風 -GALE-`].txt' -Name '1話「さくらと透明なカード」[疾風 -GALE-].txt.symlink' -ItemType SymbolicLink
```

そう、Valueに設定する値だけはエスケープしなければならないのだ。

### コードを書き直す
この結果に対して素直にコードを書き直すと下記のようになる。

```powershell
Get-ChildItem $fromPath -Recurse | Where-Object {$_.PsIsContainer -eq $False} | ForEach-Object {

    # シンボリックリンクのリンク先パス
    $targetPath = (($_.FullName -replace '\[', '`[') -replace '\]', '`]')

    # シンボリックリンクの作成先ディレクトリ
    # fromPath部分をtoPathに差し替え、ファイル名部分を削除する
    $tempPath = $toPath + $_.FullName.Substring($fromPath.Length)
    $filePath = $tempPath.Substring(0, $tempPath.Length - ($_.Name.Length +1))

    # シンボリックリンクのファイル名
    $fileName = ($_.Name + '.symlink')

    New-Item -Path $filePath -Value $targetPath -Name $fileName -ItemType SymbolicLink
}

```

無事にすべてのファイルが作成された。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/196422/c7a0f498-77c6-4383-0936-ef564f6a95c5.png)