VS Codeで保存せず消失したファイル(Untitled)をTime Machineから復元する

(最終更新: )

VS Codeで名前を付けずに書いていたメモが、ある日突然消えました。Untitledのまま数カ月使い、MacやVS Codeを再起動しても復元されていたため、通常のファイルに近い感覚で扱っていました。

今回、現在のVS Code保存領域には本文が残っていませんでしたが、Time Machineの過去バックアップから176行、16,470バイトのメモを復元できました。この記事では、macOS版VS Codeの未保存バッファを探し、元の管理領域を上書きせずに救出するまでの手順を書いておきます。

VS Codeは未保存の変更をBackupsへ退避している

VS Codeには、終了時に未保存の変更を記憶するHot Exitがあります。公式ドキュメントによると、macOSの標準インストールではバックアップが次の場所に保存されます。

$HOME/Library/Application Support/Code/Backups

Untitledの内容は、概ね次のような階層に置かれます。

Backups/
└── <workspace-id>/
    └── untitled/
        └── <hash>

ファイル名は元のメモ名ではなくハッシュです。VS Codeの公式実装を見ると、バックアップファイルは1行目にリソースURIとメタデータを置き、改行以降に本文を保存しています。

Hot Exitは便利ですが、未保存バッファを長期保管するための仕組みではありません。VS Codeがバックアップを破棄した時点で、現在のBackupsからは本文が消えます。

最初に現在のバックアップ領域を確認する

まず、VS Codeのバックアップ領域をFinderで開きます。

open "$HOME/Library/Application Support/Code/Backups"

対象ワークスペースのIDが分からない場合は、VS Codeのウインドウ状態に記録されたbackupPathを確認します。

code_root="$HOME/Library/Application Support/Code"

rg -n -C 3 'backupPath|folder' \
  "$code_root/User/globalStorage/storage.json"

対象フォルダのURIと同じまとまりにあるbackupPathが、そのワークスペースのバックアップ先です。たとえば次のようなパスが見つかります。

.../Code/Backups/<workspace-id>

その配下のuntitledにファイルが残っていれば、先に別の場所へコピーします。今回はディレクトリ自体はありましたが、中は空でした。

VS Codeの内部ファイルはバージョンによって変わる可能性があります。この段階では状態DBやBackupsを書き換えず、参照だけに留めました。

FinderのTime Machineから探す

Appleの案内では、復元したい項目があったFinderウインドウを開いてからTime Machineを起動し、過去のバックアップへ移動します。

今回の対象は次のディレクトリです。

~/Library/Application Support/Code/Backups/<workspace-id>/untitled

Finderで「移動」から「フォルダへ移動」を選び、このパスを開きます。その状態でTime Machineを起動し、メモが存在していた時期まで戻ります。

ここで注意したいのは、VS Codeの管理領域へいきなり「復元」しないことです。VS Codeが起動中の場合や、現在のバックアップ状態と衝突する場合を考え、まず内容を別名で救出する方が扱いやすくなります。

ターミナルからバックアップ世代を特定する

Finderで見つけにくい場合は、tmutilでバックアップ世代を確認できます。tmutilのman pageでは、listbackupsが完了済みバックアップを列挙し、-mを付けるとバックアップのマウントを試みると説明されています。

tmutil listbackups

候補の日付を決めたら、-mを付けて実際のマウント先を取得します。

tmutil listbackups -m | grep 'YYYY-MM-DD-HHMMSS'

環境によっては、ターミナルにフルディスクアクセスが必要です。macOSの「システム設定」から「プライバシーとセキュリティ」→「フルディスクアクセス」を開き、使用しているターミナルを許可します。

表示されたパスをbackupへ入れ、対象のuntitledだけを確認します。

backup='/Volumes/.timemachine/.../YYYY-MM-DD-HHMMSS.backup'
workspace_id='<workspace-id>'

target="$backup/Data/Users/$USER/Library/Application Support/Code/Backups/$workspace_id/untitled"

find "$target" -mindepth 1 -maxdepth 1 -type f \
  -exec stat -f '%Sp %z %Sm %N' -t '%Y-%m-%d %H:%M:%S' {} \;

Time Machineの表示パスには同じ日時名が二重に現れる場合があります。パスを手入力で組み立てず、tmutil listbackups -mが返した値をそのまま使う方が安全です。

今回見つかったファイルは、名前こそハッシュでしたが、更新日時と16,470バイトというサイズから候補を絞れました。

見つけたバックアップを別名でコピーする

候補ファイルを、VS Codeの管理外へコピーします。次の例ではデスクトップへ退避しています。

cp -p \
  "$target/<hash>" \
  "$HOME/Desktop/Recovered-Untitled.raw"

元のTime Machineバックアップは読み取り元として扱い、直接編集しません。また、現在のCode/Backupsへ戻すのではなく、独立したファイルとして保存します。

先頭のpreambleを除いて本文を取り出す

VS Codeのバックアップファイルは、先頭1行が管理情報、その後がバッファ本文です。公式ソースでも、最初の改行までをpreambleとして読み、その次のバイトから本文ストリームを構築しています。

そのため、テキストファイルであれば次のように2行目以降を取り出せます。

tail -n +2 \
  "$HOME/Desktop/Recovered-Untitled.raw" \
  > "$HOME/Desktop/Recovered-Untitled.md"

復元後は行数を確認します。

wc -l "$HOME/Desktop/Recovered-Untitled.md"
open -a 'Visual Studio Code' "$HOME/Desktop/Recovered-Untitled.md"

今回は、状態DBに残っていた「Untitled-1、Markdown、176行」という手掛かりと、復元した本文の行数が一致しました。内容も確認できたため、通常のMarkdownファイルとして保存しました。

バイナリや文字コードが不明なバックアップでは、機械的にtailを適用する前にfileコマンドなどで形式を確認した方が安全です。

確認後はバックアップ世代をアンマウントする

tmutil listbackups -mでマウントした世代は、確認後に解除します。返されたパスが日時ディレクトリを二重に含む形式なら、その親がマウントポイントです。

diskutil unmount "$(dirname "$backup")"

Unmount successfulと表示されれば完了です。通常はforceを付ける必要はありません。使用中と表示された場合は、そのバックアップを開いているFinderやターミナルを閉じてから再実行します。

Untitledは一時置き場として使う

VS Codeは未保存の変更を自動的にバックアップし、通常の終了や再起動をまたいで復元してくれます。ただし、Hot Exitのバックアップは通常ファイルの代わりにはなりません。

今回のメモは何度も再起動を越えて残っていたため、永続的に保存されているように感じていました。実際には現在のバックアップから消えており、Time Machineがなければ本文まで戻せなかった可能性があります。

短い作業メモならUntitledのままでも困りませんが、後で使う可能性がある内容は早めに名前を付けて保存する。今回の復旧で得た判断は、そのシンプルな運用でした。

参考資料

Related Posts