Power ShellでRedmineをバックアップする。

# 概要
チケット管理システム「Redimine」のデータをバックアップするPower Shellスクリプトを作成します。
データの圧縮には、フリーの圧縮ソフト「7-zip」を使います。

# 実装する機能
1. 圧縮保管
   Redmineのバックアップに必要な以下のデータを圧縮して保管します。
 - 画像ファイルが入っている「files」フォルダ
 - その他の全てのデータが格納されているMySQLのDBデータ

2. 世代管理
   3世代まで保持します。4世代以上過去のデータは削除します。
3. ミラーリング
   外付けHDDとミラーリングし、データを2箇所で保持します。

# 実装方法
## ■ 画像フォルダのバックアップ
Power ShellのCopy-Item関数でバックアップします。

```ps1:PowerShell
Copy-Item -Path （画像フォルダ） -Destination （バックアップ先） -Recurse
```
「-Recurse」を指定すると、フォルダ階層が深くても、再帰的にコピーしてくれます。
## ■ DBデータのバックアップ
MySQLに付属しているコマンド「mysqldump.exe」を使います。

```bat:コンソール
mysqldump --defaults-file=(オプションファイル) --result-file=(バックアップデータ出力先) --log-error=(エラーログ出力先) DB名
```
引数で指定しているオプションファイルには、ログイン情報を指定します。

```ini:mysqldump-options.ini
[mysqldump]
user=xxx
password=xxx
host=xx.xx.xx.xx
port=xxx
```
## ■ バックアップデータの圧縮
フリーの圧縮解凍ソフト「[7-zip] (https://sevenzip.osdn.jp/)」に付属しているコマンドラインツールを使って圧縮します。[^1]
圧縮形式は7zを使用します。

```bat:コンソール
7z a -sdel (圧縮先の書庫) (圧縮元の画像フォルダ) (圧縮元のダンプファイル)
```
引数の意味は以下です。

- a : addの略。ファイルを圧縮先の書庫に入れる。
- -sdel : 圧縮処理の後、圧縮元ファイルを削除する。

## ■ バックアップデータのローテート
ファイル作成日付でソートし、作成日付の新しい3ファイルを残して、古いデータを削除します。

```ps1:PowerShell
Get-ChildItem (バックアップフォルダ) |
Sort-Object CreationTime -Descending |
Select-Object -Skip 3 |
foreach{Remove-Item -Path $_.FullName}
```
## ■ バックアップデータのミラーリング
Windowsの付属コマンド「RoboCopy」を使います。

```bat:コンソール
ROBOCOPY (コピー元のバックアップフォルダ) （コピー先のミラーフォルダ）/MIR
```
引数の「/MIR」はミラーリングするという意味です。
「/MIR」を指定すると、コピー元にだけあるファイルはコピー先に作成され、コピー元にないファイルは、コピー先から削除されます。

# プログラム全体の構成
PowerShellで上記の処理を順番に呼び出します。
処理に必要なパスなどの情報は、設定ファイルに記述し、実行時に読み込む事にします。
処理に必要なフォルダは、初回実行時に作成します。

# フォルダ構成

以下にフォルダ構成の例を示します。
内蔵HDD内で一度バックアップし、外付けHDDにミラーリングする、という流れです。

```text:フォルダ構成
【内蔵HDD】
C:
└─Bitnami
    └─redmineBackup
        ├─backup_files
        │      redmineBackup_YYYY-MMDD-HHMMSS.7z
        │      redmineBackup_YYYY-MMDD-HHMMSS.7z
        │      redmineBackup_YYYY-MMDD-HHMMSS.7z
        │
        ├─config
        │      settings.ini
        │      mysqldump-options.ini
        │
        ├─log
        │      dumpMySqlDataError.log
        │      redmineBackup.log
        │      
        ├─scripts
        │      redmine_backup.ps1
        │          
        └─work

【外付けHDD】
E:
└─Bitnami
    └─redmine_data_backup
            redmineBackup_YYYY-MMDD-HHMMSS.7z
            redmineBackup_YYYY-MMDD-HHMMSS.7z
            redmineBackup_YYYY-MMDD-HHMMSS.7z
```



# ソースコード
```ps1:PowerShell
# ---  ヘルパー関数  ---

function initializeFolders ($bkroot,$settings) {
    createFolderIfNotExists (Join-Path $bkroot work)
    createFolderIfNotExists (Join-Path $bkroot log)
    createFolderIfNotExists (Join-Path $bkroot backup_files)
    createFolderIfNotExists $settings.mir
}

function createFolderIfNotExists($path){
    if(-not (Test-Path $path)){
        New-Item $path -ItemType Directory
    }
}

function deleteFileIfExists($path){
    if(Test-Path $path){
        Remove-Item $path
    }
}

function getBackUpFileName($filenameExtension){
    return "redmineBackup_" + (Get-Date -Format "yyyy-MMdd-HHmmss") + "." + $filenameExtension
}

function log($message, $bkroot){
    Write-Output $message
    Write-Output ((Get-Date -Format "yyyy-MM-dd HH:mm:ss") + "  " + $message) | Out-File (Join-Path $bkRoot log\redmineBackup.log) -Encoding utf8 -Append
}

# ---  初期化  ---

# プログラムのルートフォルダを取得します。
$bkRoot = Split-Path $MyInvocation.MyCommand.Path -Parent | Split-Path -Parent

# アクセスするリソースのパスをINIファイルから読み込みます。
$s = Get-Content (Join-Path $bkRoot config\settings.ini) | ConvertFrom-StringData

# 処理対象となるフォルダを作成します。
initializeFolders $bkRoot $s

# 古いログファイルがあれば削除します。
deleteFileIfExists (Join-Path $bkRoot log\redmineBackup.log)

# ---  バックアップ  ---

log "添付ファイルが格納されているfilesフォルダをworkフォルダにコピーします。" $bkRoot
Copy-Item -Path $s.files -Destination (Join-Path $bkRoot work) -Recurse

log "mySqlに格納されているデータをworkフォルダに取得します。" $bkRoot
Start-Process -NoNewWindow `
              -FilePath $s.mysqldump `
              -ArgumentList ("--defaults-file=" + (Join-Path $bkRoot config\mysqldump-options.ini)) , `
                            ("--result-file=" + (Join-Path $bkRoot work\databaseBackUp.sql)), `
                            ("--log-error=" + (Join-Path $bkRoot log\dumpMySqlDataError.log)), `
                            $s.dbname `
              -Wait

log "取得したバックアップデータを圧縮します。" $bkRoot
$backUpFile = getBackUpFileName "7z"
Start-Process -NoNewWindow `
              -FilePath $s._7zip `
              -ArgumentList a, `
                            -sdel, `
                            (Join-Path $bkRoot work | Join-Path -ChildPath $backUpFile), `
                            (Join-Path $bkRoot work\files), `
                            (Join-Path $bkRoot work\databaseBackUp.sql) `
              -Wait


log "バックアップデータを保管用フォルダに移動します。" $bkRoot
Move-Item -Path (Join-Path $bkRoot work | Join-Path -ChildPath $backUpFile) -Destination (Join-Path $bkRoot backup_files)

log "4世代以前のバックアップファイルを削除します。" $bkRoot
Get-ChildItem (Join-Path $bkRoot backup_files) |
Sort-Object CreationTime -Descending |
Select-Object -Skip 3 |
foreach{Remove-Item -Path $_.FullName}


# ---  ミラーリング  ---


log "バックアップファイルを外付けHDDとミラーリングします。" $bkRoot
if((Join-Path $bkRoot backup_files | Get-ChildItem | Measure-Object).Count -ne 0){ # 万が一、コピー元が空で同期してしまうと、コピー先のファイルが全部消えるので。
    ROBOCOPY (Join-Path $bkRoot backup_files) $s.mir /MIR
}
```
# 設定ファイル

```ini:settings.ini
\# redmineの画像データ「files」フォルダのパス
files=X:\\Bitnami\\xxx\\apps\\redmine\\htdocs\\files

\# MySQLからデータをダンプするツール「mysqldump」
mysqldump=X:\\xxx\\xxx\\bin\\mysqldump.exe

\# 「mysqldump」コマンドを用いてダンプするデータベース・インスタンス
dbname=xxx

\# 7-zipのコマンドラインツール「7z」
_7zip=X:\\xxx\\7-Zip\\7z.exe

\# ミラーリングする外部ストレージのフォルダパス
mir=Y:\\xxx\\xxx
```


# 環境
Power Shell 5.1
7-zip 16.04
Bitnami Redmine Stack 3.3.2-2
&nbsp;&nbsp;&nbsp;・Redmine 3.3.2
&nbsp;&nbsp;&nbsp;・MySQL 5.6.35
windows 10 Home

# github
ここで作成したプログラムは、以下に保存されています。
https://github.com/nogitsune413/redmineBackup

# 注釈
[^1]: PowerShellにも標準で圧縮用の関数「Compress-Archive」が入っているのですが、Compress-Archive関数のバグを報告するWebサイトの記事をいくつか見かけたので、今回は7-zipを使って実装しました。 簡単なプログラムですので、標準関数であるCompress-Archiveを使いたい方は、Power Shell内に書かれた、圧縮処理のコードを書き換えてください。<br /><br />【バグを報告しているサイト】<br />[powershellのcompress-archiveコマンドで作成したzipに潜むちょっとした罠](http://qiita.com/noranuk0/items/cb9de67bfc269391bf6e)<br />[これで解消！「KB2704299」でCompress-Archiveの文字化け対処](https://cheshire-wara.com/powershell/ps-column/compress-archive-resolved/)