PowerShellディレクトリ(フォルダ)の差分バックアップスクリプトを作る

powershell コンピュータ
powershell
ファイルが日々更新される作業ディレクトリを差分バックアップするスクリプトを作りたいと思います。

バックアップログ

バックアップログテーブルのレイアウト

src_dir

文字列,キー,バックアップ元のディレクトリ
path

文字列,キー,ルートからの相対パス
last_update

数値,キー,ファイルスタンプの更新日
size

数値,ファイルサイズ
dst_dir
文字列,バックアップ先のディレクトリ
backup_date
文字列,サブディレクトリ
ソースファイルパス:「ソースルート」+「パス」
バックアップファイルパス:「バックアップルート」+「バックアップ日付」+「パス」

日時型

SQLiteには日時型が無かったような気がします。PowerShell側で保存に都合の良い型に変換します。

日時を文字列(yyyyMMddHHmmss形式)に変換

バックアップ日時用

Get-Date -Format "yyyyMMddHHmmss"

ファイルの更新日時を数値に変換

Get-Item ファイルのパス | % { $_.LastWriteTime.Ticks }

再帰的にファイルを検索

サブフォルダも検索対象にします。

ls -Recurse | % { $_.FullName }

相対パスを取得

検索した絶対パスから相対パスを取得。
カレントディレクトリから見た相対パスのようなので、カレントディレクトリを移動する必要がありそうです。

Resolve-Path ファイルのパス -Relative

親ディレクトリのパス文字列を取得

バックアップ先のディレクトリ作成用

[System.IO.Path]::GetDirectoryName("ファイルのパス")

フォルダが存在しない場合フォルダを作成

mkdirで中間のサブフォルダもまとめて作成してくれました。

$a =".\a\b\cc"; if ((Test-Path $a) -eq $False) { mkdir $a }

スクリプトソース

using namespace System.Data.SQLite

# 
# ディレクトリの差分バックアップ
# 


Param(
    [String]$src_dir="D:\",
    [String]$dst_dir="G:\backup"
)


Set-StrictMode -Version Latest
$ErrorActionPreference = "STOP"

# モジュールのインポート
Import-Module SQLite

# データベース作成
$current_dir = Split-Path $MyInvocation.MyCommand.Path
$db_path = Join-Path $current_dir "Backup-Folder.db"

# コネクションオブジェクトの生成
$con = [SQLiteConnection]::new() | % {
    $_.ConnectionString = ("Data Source = {0}"-f $db_path)
    $_.Open()
    $_
}

# テーブル作成
$cmd = [SQLiteCommand]::new()
$cmd.Connection = $con
$cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS sample (
    src_dir text,
    path text,
    last_update int,
    size int,
    dst_dir text,
    backup_date text,
    primary key(src_dir, path, last_update)
)
"@
$cmd.ExecuteNonQuery() | Out-Null

# ディレクトリの退避
$bak_dir = (Get-Location).Path

# ディレクトリの移動
Set-Location -LiteralPath $src_dir
#echo (Get-Location).Path

# バックアップ日時の取得
$backup_date = Get-Date -Format "yyyyMMddHHmmss"

ls -LiteralPath $src_dir -Recurse | ? {
    -not $_.PSIsContainer
} | % {
    
    # 相対パスの取得
    $path = Resolve-Path -LiteralPath $_.FullName -Relative    
    # 最終更新日時
    $last_update = $_.LastWriteTime.Ticks
    # サイズ
    $size = $_.Length
    
    $cmd.CommandText = "SELECT path FROM sample WHERE src_dir = '{0}' AND path ='{1}' AND last_update = {2}" -f $src_dir, $path, $last_update
    #echo $cmd.CommandText
    $rec = $cmd.ExecuteReader()
    $b = $rec.HasRows
    $rec.Close()

    if ($b -eq $False)
    {
        # コピー先パス
        $dst_file = Join-Path (Join-Path $dst_dir $backup_date) $path

        # コピー先のディレクトリ作成
        $d = [System.IO.Path]::GetDirectoryName($dst_file)
        if ( -not (Test-Path $d) ) {
            mkdir -Path $d -Force | Out-Null
        }
        
        # コピー
        Copy-Item -LiteralPath $_.FullName -Destination $dst_file
        Write-Host $path

        # レコードの追加
        $cmd.CommandText = "INSERT INTO sample (src_dir,path,last_update,size,dst_dir,backup_date) values ('{0}', '{1}', {2}, {3},'{4}','{5}')" -f $src_dir, $path, $last_update, $size, $dst_dir, $backup_date
        #Write-Host $cmd.CommandText
        $cmd.ExecuteNonQuery()
    }
}

# ディレクトリの移動
Set-Location -LiteralPath $bak_dir
バックアップ先のディレクトリに名称がバックアップ日時のサブディレクトリが出来上がり、そこをルートにファイルがコピーされていました。初回はフルバックアップ2回目以降が差分バックアップになります。

作っていてハマったのは、ファイル名に”[]”が含まれるとコマンドレットがワイルドカードと認識してしまいうまく動いてくれないことがありました。コマンドレットに引数としてパスを引き渡す場合、-Pathではなく-LiteralPathを指定することで解決します。
また、パス文字列を操作する場合、PowerShellのコマンドレットの動作が怪しい場合System.IO.Pathのメソッドを使うことで動作が安定します。

とりあえず動作を確認しましたが、ある程度テスト実行してみて問題点の洗い出し→修正を繰り返すことにします。
今後出来たらですが、指定日の状態にバックアップからリストアするスクリプトと、過去のバックアップ履歴をブラウジングするGUIアプリケーションを作成が出来れば良いなと思います。

コメント