サーバ管理には付き物のバックアップとその世代管理。ルータやNASの設定バックアップファイルは小さいのでついつい生成し放題にしがちです。ログファイル管理のlogrotateのような直近n個を維持して古いのを削除するfilerotateを各環境別に組んでみます。
1. Shell Script
まずはLinux系のシェルスクリプトで考えてみます。バックアップストレージに使われるOSがLinux系であることがほとんどなので、これを作ってしまえば多くの用途を網羅することが出来ます。これがもし、n日より古いファイルは削除、という命題であれば、Linuxではとても簡単に実現出来ます。しかし、不定期にとったバックアップをいくつも保持している場合など、古いバックアップファイルを削除する目的であるストレージ容量の確保と言う観点からも、あまり相応しいとは思えません。引き続き検索を続けていると、とある記事にめぐり逢いました、Great Tnx!!
日本よりも海外で少ないながらも昔からニーズのあるバッチ処理なのかもしれません。早速これをたたき台として、自分仕様に組んでみます。前提条件は次のように。
- cdで対象フォルダに直接移動しない(バッチ終了後に面倒)。
- 対象フォルダにサブフォルダのある可能性を考慮し、ファイルのみを対象とする。
- 失敗したバックアップファイルなど、ゼロバイトファイルは対象外。
- パスやファイル名に空白が含まれる可能性が有る。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
#! /bin/sh # filerotate # Delete older files in particular folder path, keep 'n' newest files. # Usage: filerotate.sh <numfiles> <path> # PARAMETER VALIDITY CHECK echo "[`date +"%Y/%m/%d(%a) %k:%M:%S"`] Batch Start" if [ $# -lt 2 ]; then echo "[`date +"%Y/%m/%d(%a) %k:%M:%S"`] Arguments must be provided. Batch Aborted." exit 1 else # NUMERIC CHECK expr "$1" + 1 >/dev/null 2>&1 if [ $? -lt 2 ]; then keepFiles=$1 else echo "[`date +"%Y/%m/%d(%a) %k:%M:%S"`] First Argument is NOT Numeric. Batch Aborted." exit 1 fi # PATH EXISTS if [ ! -d "$2" ]; then echo "[`date +"%Y/%m/%d(%a) %k:%M:%S"`] Target Path Does Not Exist, Batch Aborted." exit 1 else strPath=$2 fi fi # COUNT FILES AND COMPARE numFiles=`find $strPath -maxdepth 1 -type f -size +0c | wc -l` if [ $numFiles -le $keepFiles ]; then echo "[`date +"%Y/%m/%d(%a) %k:%M:%S"`] No Need to delete any files, Batch end." exit 1 fi # FILE DELETION delFiles=`expr $numFiles - $keepFiles` # GNU #find $strPath -maxdepth 1 -type f -size +0c -print0 | xargs -r0 ls -1t --quoting-style=shell-always | tail -n $delFiles #DRY-RUN find $strPath -maxdepth 1 -type f -size +0c -print0 | xargs -r0 ls -1t --quoting-style=shell-always | tail -n $delFiles | xargs rm # BSD / Alt-F(BusyBox) #find $strPath -maxdepth 1 -type f -size +0c -print0 | xargs -r0 ls -1t | tail -n $delFiles | sed -e 's/ /\\ /g' #DRY-RUN #find $strPath -maxdepth 1 -type f -size +0c -print0 | xargs -r0 ls -1t | tail -n $delFiles | sed -e 's/ /\\ /g' | xargs rm if [ $? -ne 0 ]; then echo "[`date +"%Y/%m/%d(%a) %k:%M:%S"`] An error ocurred deleting the files, Batch abnormal end." exit 1 else echo "[`date +"%Y/%m/%d(%a) %k:%M:%S"`] $delFiles file(s) Deleted." fi |
最後の前提条件、空白対策が面倒なところで、xargsで渡す前に対策したり、lsコマンドに用意されている「–quoting-style=shell-always」で常にシングルクォート囲みとしたり。ところがこのオプションが実はBSD系など一部では対応しておらず、結局、空白をsedでバックスラッシュ掛けるようにしています。先人さまに感謝。
動作確認は、以下のシステムで行いました。巷ではlsの結果をtailに渡してという処理が期待通りに行われない、というケースも一部あるとの記述を目にします。導入の際はこの辺りの挙動をまず単発で試してみるのが確実です。
- Ubuntu 18.04LTS
- OpenWRT 18.06
- FreeNAS 11.3 (注意:BSD系のlsにつき、「–quoting-style」オプション無)
- Synology DSM 6.2.3
- D-Link DNS-323 Alt-F 0.1RC4.1 (注意:Busybox内のlsに「–quoting-style」オプション無)
と、自作スクリプトが出来上がった頃に、ファイル名のワイルドカード指定まで綺麗にオプション指定出来る素晴らしい製作物を見つけてしまいました。Great Works!
2. PowerShell
次にWindows系です。実は十年以上前にVB Scriptで自作したこのfilerotate.vbsを永らく愛用していました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
'================================================================= '### DELETE OLD FILES TO KEEP NBR OF FILES '### '================================================================= 'TARGET FULL PATH Set args = WScript.Arguments Dim targetPath targetPath = args.item(0) 'MAX FILES IN TARGET Dim maxFiles maxFiles = CInt(args.item(1)) Set args = Nothing Dim r Const adInteger = 3 Const adDate = 7 Const adFldIsNullable = 32 Const adVarChar = 200 Dim fs Set fs = CreateObject("Scripting.FileSystemObject") Dim fo Set fo = fs.GetFolder(targetPath) Dim fc Set fc = fo.Files Dim intFiles intFiles = fc.count If intFiles > maxFiles Then '# Set Files Collection into Recordset Call initAdo() Dim f For Each f In fc Call setAdo(f.Name, f.DateLastModified, f.Size) Next Set f = Nothing '# Sort Recordset r.MoveFirst r.sort = "fdate" r.MoveFirst 'MsgBox r.GetString() '# Delete Old Files ( Upper Rows ) Dim fss Set fss = CreateObject("Scripting.FileSystemObject") Dim i For i = maxFiles + 1 to intFiles fss.DeleteFile(targetPath + r("fname").value) r.MoveNext Next Set fss = Nothing Call r.Close() Set r = Nothing End If Set fc = Nothing Set fo = Nothing Set fs = Nothing Sub initAdo() Set r = CreateObject("ADOR.Recordset") Set fs = r.Fields Call fs.Append("fname", adVarChar, 300, adFldIsNullable) Call fs.Append("fdate", adDate, , adFldIsNullable) Call fs.Append("fsize", adInteger, , adFldIsNullable) Set fs = Nothing Call r.Open() End Sub Sub setAdo(ByVal fname, ByVal fdate, ByVal fsize) Call r.AddNew() Set fs = r.Fields fs(0).Value = fname fs(1).Value = fdate fs(2).Value = fsize Set fs = Nothing Call r.Update() End Sub |
しかしながら今ではvbsが時代遅れで不安全という傾向にあるので、PowerShell版を組んでみることにしたのですが、こちらも、n日より古いファイルを削除するのは、という命題の方が多いのが実状です。そこでPowerShellプログラミング基礎をつまみ食いしながら、つぎはぎで組んでみました。さすがにシェルとは違ってオブジェクト的で、凝った作りにせずとも実現することが出来ました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
#***************************************************************** # Script Name: filerotate.ps1 # Description: Delete Older Files in Directory # Usage : .\filerotate.ps1 <NUMFILES> <PATH> #***************************************************************** # PARAMETER VALIDITY CHECK # TWO PARAM REQUIRED if ($Args.length -ne 2) { echo "No Enough Parameters, Aborted." exit } # 1ST PARAM: IS NUMERIC/INT if (-not([int]::TryParse($Args[0],[ref]$null))) { echo "1st Parameter: is NOT Numeric, Aborted. --> $($Args[0])" exit }else{ $keepFiles = $Args[0] } # 2ND PARAM: TEST PATH if (-not(Test-Path $Args[1])) { echo "2nd Parameter: Target Path Incorrect or Negative, Aborted. --> $($Args[1])" exit }else{ $strPath = $Args[1] } # GET FILES COLLECTION $colFiles = Get-ChildItem $strPath -File | Where {$_.Length -gt 0} | Sort-Object -Property LastWriteTime # COUNT FILES AND COMPARE $numFiles = $colFiles.count if ($numFiles -le $keepFiles) { echo "No Need to delete any files, Batch end." exit } # FILE DELETION $delFiles = $numFiles - $keepFiles echo "$delFiles need to be deleted" $colDelFiles = $colFiles | Select-Object -first $delFiles $colDelFiles | foreach { $_.Delete()} |
3. Python
Shell Script版がAlt-F内のBusyBoxが古くて一部不具合となることから、もしもの時のためにPython版を組んでみました。古いシステム向けの互換性を考え、ver.2.7系で動くことを前提とします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
#!/usr/bin/env python # -*- coding: utf-8 -*- # # filerotate # Delete older files in particular folder path, keep 'n' newest files. # Usage: filerotate.py <numfiles> <path> # import glob, os, sys # PARAMETER VALIDITY CHECK if (len(sys.argv) != 3): print('Usage: python %s <numfiles> <path>') quit() else: # NUMERIC CHECK (ALL ARGS MUST BE STRING, CHECK DIGIT ONLY) if not sys.argv[1].isdigit(): print('1st Param is NOT Numeric, Aborted.') quit() else: keepFiles = int(sys.argv[1]) # PATH EXIST CHECK if (os.path.isdir(sys.argv[2])): strPath = sys.argv[2] else: print('2st Param Path Does NOT Exist, Aborted.') quit() # GET FILES COLLECTION colFiles = sorted([p for p in glob.glob(strPath+'*') if os.path.isfile(p) and os.path.getsize(p)>0], key=lambda f: os.stat(f).st_mtime, reverse=True) #for p in colFiles:print(p) # COUNT FILES AND COMPARE numFiles = len(colFiles) if (numFiles <= keepFiles): print('No Need to delete any files, Batch end.') quit() # FILE DELETION delFiles = numFiles - keepFiles print(str(delFiles)+' file(s) need to be deleted') del colFiles[0:keepFiles] for p in colFiles: #print(p) try: os.remove(p) except: print('Error occured while deletion, Aborted.') print('Batch End Properly.') |
globを使ったこのアプローチは、こちらの記事を参考にさせて頂きました、Great Tnx!!
リスト内包表記やlambdaは初めて使いますが、ほぼコピペです、理解して使えるようになるにはまだまだ。他にもpathlibを使った手法など、いくつか別の手段がありますが、中にはPython3系限定の物があるので、もしそうした手法を使う際には少し注意が必要でしょう。
尚、上述のShellScript版では空白避けの「–quoting-style」が使えなかったAlt-Fでも、Pythonパッケージを入れて問題無く動くことを確認しました(ver.2.7.2)。