パソコン使いだして長いけれど、思えば、バックアップってやったことなかったわけよ。幸運にも俺の使っているHDDくんはタフらしく、7歳になってもぎゅんぎゅん動いて一度もぶっ飛んだことがない。でもHDD壊れてスクリーンショット飛んだ奴も身近にいるし(おう、二三度飛ばしたきみのことだぞ)、ちょっとバックアッププログラムを考えてみようってわけ。バッチファイルを触るの初めてだけどがんばったぜ。

やりたいことは以下。

  • Cドライブから、外付けHDDであるDドライブへコピー。
  • 0時に自動で実行してほしい。
  • 0時にPCがスリープ状態でも実行してほしい。
  • 実行したらPCをシャットダウンしてほしい。でないと二日間放置したとき、変更がないのにバックアップしちゃってムダだから。
  • PCまるごとバックアップじゃなくて、フォルダ単位で指定したい。
  • せっかくなので5世代ほどバックアップしたい。
  • Dドライブはこういう構成にする。
Dドライブ
│
├─ ★バックアップ用のHDDです★(バックアップ用ドライブの存在フラグ)
│
├─ 01_Backup(バックアップフォルダ)
│    │
│    ├─ 2016.01.01._00.00.00
│    │    ├─ フォルダ単位のバックアップ
│    │    └─ フォルダ単位のバックアップ
│    │
│    ├─ 2016.01.02._00.00.00
│    │    ├─ フォルダ単位のバックアップ
│    │    └─ フォルダ単位のバックアップ
│    │
│    └─ 以下、5つまで続く。
│
└─ 02_BackupLog(ログフォルダ)
     │
     ├─ 2016.01.01._00.00.00
     │    ├─ フォルダ単位のバックアップログ
     │    └─ フォルダ単位のバックアップログ
     │
     ├─ 2016.01.02._00.00.00
     │    ├─ フォルダ単位のバックアップログ
     │    └─ フォルダ単位のバックアップログ
     │
     └─ ログはこれまでのを全部残す。

ほんで自分なりに書いてみたのがこれ。

: 「おまじない」。batファイルの最初にはこれを書くといいらしいぞ。*1
@echo off
cd /d %~dp0
setlocal enabledelayedexpansion

: バックアップフォルダのパス、ログフォルダのパス、保持する世代数を書く。
set BKFOLDER=D:\01_Backup
set LOGFOLDER=D:\02_BackupLogFolder
set GENERATION=5

: このバッチ実行中にスリープしないようにする。*6-1
powercfg -x -standby-timeout-ac 0
powercfg -x -standby-timeout-dc 0

: そもそも外付けHDDのがなかったら終了するぞ。*2
if not exist "D:\★バックアップ用のHDDです★" ( exit )

: バックアップフォルダの名前を作る(上述した2016.01.01._00.00.00みたいなやつ)。*3
set T=%time: =0%
set NOW=%date:~0,4%.%date:~5,2%.%date:~8,2%_%T:~0,2%.%T:~3,2%.%T:~6,2%.

: 最古のバックアップの名前。
set OLDEST=
for /f "usebackq delims=" %%i in (`dir D:\01_Backup /ad /o-d /b`) do ( set OLDEST=%%i )

: 現存するバックアップの数。
set COUNT=
for /f "usebackq" %%i in (`dir /ad /b D:\01_Backup ^| find /c /v ""`) do ( set COUNT=%%i )
: 値の前後のスペースを削除する。*5
call :Foo !COUNT!

: 現存数 < 欲しい世代数 だったら、新しくフォルダを作る。
if !COUNT! lss %GENERATION% ( mkdir "%BKFOLDER%\%NOW%" )

: 現存数 >= 欲しい世代数 だったら、最古のバックアップの名前を変更する。*4
if !COUNT! geq %GENERATION% (
    move "%BKFOLDER%\%OLDEST%" "%BKFOLDER%\%NOW%"
    : フォルダの更新日時だけ更新したいんだが、
    : 方法がわからんかったので中に適当なファイルを作成即削除して更新する
    echo aaa > "%BKFOLDER%\%NOW%\aaa.txt"
    del "%BKFOLDER%\%NOW%\aaa.txt"
)

: ログフォルダを作る。
mkdir "%LOGFOLDER%\%NOW%"

: ここが本題、バックアップする。この例ではデスクトップをまるごとバックアップする。
: in ("Desktop") のDesktopの部分に書いた名前でバックアップされる。
: "C:\Users\Username\Desktop" の部分がコピー元のパス。
set NAME=
for /f "delims=" %%i in ("Desktop") do ( set NAME=%%i )
call :Foo !NAME!
robocopy ^
    "C:\Users\Username\Desktop" ^
    "%BKFOLDER%\%NOW%\!NAME!" ^
    /LOG:"%LOGFOLDER%\%NOW%\!NAME!.txt" ^
    /MIR /R:0 /W:0 /NP /NDL /TEE /XJD /XJF /FFT
: 以下、バックアップしたいフォルダの数だけこれをコピーしてフォルダ名とかパスを書く。

: スリープの設定をもとに戻す。俺は普段60分設定にしてるので60。*6-2
powercfg -x -standby-timeout-ac 60
powercfg -x -standby-timeout-dc 60
endlocal

: バックアップ終わったらシャットダウンする。
shutdown.exe -s -t 60
exit

: 変数から前後のスペースを取り除く関数 ……関数って呼んでいいのかこれ? *5に関連。
: 使いたいところで call :Foo !A! って感じで呼ぶ。
:Foo
set COUNT=%*
*1 このおまじないなんなん?
「ふつーにbat実行すると書いたことが全部cmdに表示されちゃうけどそれ無意味だから表示されないようにする」、「このbatの場所をカレントディレクトリにする」、「batってデフォルトだとひとつの変数につき一回しか定義できないんだけどそれを何度も定義しなおせるようにする」。
*2 なんでHDD存在チェックしてんの?
このファイル名変えさえすれば、HDD繋ぎっぱなしでもバックアップを中止、再開できるようにしたら便利じゃないかなって思ったから。
*3 なんで現在時刻をいったんTに格納してんの?
ゼロ埋めのため。
*4 なんで新しいバックアップフォルダ作らないで最古のデータに上書きするって方式とってんの?
コピーに使っているrobocopyコマンドは差分コピーでバックアップデータからの変更点だけを上書きしてくれる。そのほうがあらたにイチからバックアップを作るより早く済み、PCくんの負担も少ないと思ったから。
最古のフォルダ名の取り方、および変数への格納について
for /f "usebackq delims=" %%i in (`dir D:\01_Backup /ad /o-d /b`) do ( set OLDEST=%%i )
「D:\01_Backupの中身を新しい順に並べ、順繰りに変数iへ格納し、最後に格納されたものが最古のフォルダ名」っていう内容。大事なのがオプションの delims= で、これがないとスペースの入ったフォルダ名に対応できない。「新しいフォルダ (1)」とかね(それでちょっと詰まった)。

んでそのバッチファイルを定時実行するのには、Windowsのタスクスケジューラを使う。

  • 全般タブ
    • ユーザがログオンしているかどうかにかかわらず実行する、にチェック。
    • パスワードを保存しない、にチェック。
    • 最上位の特権で実行する、にチェック。
  • トリガータブ
    • 実行間隔と時刻を記述。
    • 有効、にチェック。
  • 操作タブ
    • プログラム/スクリプトにいま書いたスクリプトの場所を設定。例、"C:\robocopy_backup.bat"
    • 開始ってところに上記スクリプトのディレクトリ名をダブルクォーテーションなしで記述。例、C:\
    • このパスにスペースが含まれていたり、カッコが含まれているとうまくいかないっぽい。具体的には、実行はできるんだが、タスクスケジューラの実行結果が0x1になっちゃうことがあった。
  • 条件タブ
    • タスクを実行するためにスリープを解除する、にチェック。

問題点としては、AppDataのバックアップはこのスクリプトで出来ないこと。バックアップを取りたかったフォルダのひとつに、ユーザフォルダ内のAppDataがあったんだが、これはうまくいかんかった。なんか権限がどうとか色々言われてさ。仕方ないからこれに関しては思い出したときに手動で行うこととした。

こんなところでうまく完了した。だが苦労したぜ、バッチファイルを甘くみていた。だってPythonを書けるんだから、楽勝だと思っていたのだ。蓋を開けたらPythonと全然違うじゃねーか! そもそもなんだ、指定ディレクトリ内のファイル数をカウントするためだけに3時間もググらせんじゃねーよ。ようやく方法を見つけたと思ったら、 for /f "usebackq" %%i in (`dir /ad /b D:\01_Backup ^| find /c /v ""`) do ( set COUNT=%%i ) とかお前、「 dir /ad /b D:\01_Backup コマンドでディレクトリの一覧を出し、その結果を find /c /v "" に放ってその行数を出し、その結果をfor文で回して変数に格納」するってどんな発想だよ。ほんとびっくりした。ってここまで書いて気付いたんだけど最初からPythonでやればよかったんじゃね? い、いやなんかバックアップのやり方とかググっていたらWindowsのデフォルト機能でシステムバックアップ、とかたくさん出てきたから、Windowsの機能であるバッチしか思い浮かばなくなっちゃったみたいだ…。まあでもひょっとしたら、Pythonの旦那も裏ではこういうコマンドを地道に打ってくれてるのかもしれないしな。ここは世界が広がったと満足しておこう。

(2016.11.25.)自分で使っていたところ、不具合が発生してたんで追記、修正。

*5に関連した処理を追加
なんかねえ、たとえば"2"って数値が入っていてほしい変数に"2 "って文字列が入っちゃってる部分が発生してた(末尾にスペースが入っちゃってる)せいでif文の分岐が狂っちゃっていた。不要なスペースを削除するための処理は上記スクリプト内で*5をつけて追記しといた。なお、変数セットするときに ( set COUNT=%%i ) って書き方してる部分を ( set COUNT=%%i) って書くようにすりゃあ(閉じカッコ前のスペースを削除)このトラブルはぶっちゃけ根治する。だが俺は新しく覚えたものを使いたがる病なので既存の書き方はそのままに、対応した。
*6-1 *6-2
バッチ実行中にPCがスリープする事件が起きやがったので、それに対応。Windowsの旦那はどうしてこうところどころ気が利かねーの? Macに浮気するぞこの野郎。
その他こまかいところを修正
バックアップフォルダの場所は直書きじゃなくてスクリプト冒頭で設定するような書き方にしたり、途中不等号の種類間違えてたりしたんで修正しといた。マヌケー。

(2016.12.21.)二周目のバックアップがとっても早くなるってのがコレの売りだったはずだが、はたしてどんくらい早くなるのか?

一周目(全部バックアップ)がこれくらい。0時に開始して、全部終わるのに3時間以上かかっている。なおこの日のバックアップサイズは全部で18GBくらいでした。

ほんで二周目(差分バックアップ)がこれくらい。0時に開始して、30分かからず終了している。

早くなったね。やったね! 寝てる間だから長かろうがなんだろうがどうでもいいのだけど、PCくんの負担が軽くなるはずだからいいことだよねきっと。

以下の記事からリンクされています