Debian(Linux)でファイルの変更監視
FTPサーバーの運用においてのトラブルのひとつに、ファイルが存在したらアペンドする運用のはずなのに上書きされてしまうというものがあります。これは他力本願なので間違いはおきえます(自力でも起きるのですが...)。
そこで、今回はLinuxのファイルやディレクトリの変更監視ツールであるinotify APIを使って、FTPサーバーへのアップロード(=ディレクトリ内のファイル変更)を検知して、ファイルを自動的にFTPサーバーの管理外へ移動させるようにしたいと思います。
inotifyやinotify-toolsについてはQuiita「inotify-toolsでファイルやディレクトリを監視する」を参考にさせていただきました。
inotify-tools
inotify API自体は標準的なインストールでDebian(Linux)に組み込まれると思います。
このAPIを利用するためのインターフェースにinotify-toolsがあり、今回はこれを利用します。他にはcronと似た設定方法で利用できるincronもあったのですがDebian11でパッケージリストから外れてしまったようです。
...
# apt update
...
# apt install inotify-tools
...
inotifywait
inotifywaitの基本的な使い方の例として、/home/user/sample.txtの監視をしてみます。ファイルの監視は既存のファイルがないとエラーになるので、最初にtouchで作成しています。
$ touch /home/user/sample.txt $ inofifywait /home/user/sample.txt ... Setting up watches. Watches established.
通常実行時は、inotifywaitはこの状態で戻りを待ちます。別のターミナルからこのファイルを編集をします。
$ nano /home/user/sample.txt
編集をしようとファイルを別のターミナルでオープンすると、元のターミナルのコンソールが次のようなメッセージとともに戻ってきます。
/home/user/sample.txt OPEN $
これはファイルのオープンイベントを検知してinotifywait処理が終わったことを意味します。
この後inotifywaitを再度起動すればこの後のファイルの変更のイベントも取得できますが、オープンは検知不要で変更だけを検知したい、といった場合には-eオプションを使います。-eオプションにmodifyを設定すると変更時のみ検知します。
$ inotifywait -e modify /home/user/sample.txt Setting up watches. Watches established. /home/user/sample.txt MODIFY $
今度はエディタで保存作業を行った際にinotifywait処理が終わります。-eオプションに渡せるイベント名は他に、open(オープン時)、access(読み込み時)、create(作成時)、delete(削除時)などがあります。複数のイベントを指定したい場合は,(半角カンマ)でつなげます。他にも設定できるイベントはありますのでman inotifywaitもしくはをinotifywait -hで表示されるマニュアルを参照してください。空のファイルは読みだしてもaccessを検知しなかったり癖があるので事前に意図した検知ができるかどうかの確認は必要だと思います。
他、主なオプションには次のようなものがあります。
- -t
タイムアウトの時間を秒数で指定します。
- -m
イベントを検知しても、終了せずに待機し続けます。-tオプションとは共存できません。
-
-d
デーモンとして起動します。挙動は-mオプションと似たような感じになります。
- -o
結果を指定したファイルに出力します。
- -s
結果をsyslogに出力します。
- -r
ディレクトリ監視を指定した際に子孫ディレクトリまで監視対象にします。
- -q
メッセージを少なくします。qqとすると、エラー以外でなくなります。
- -c
結果をCSVにします。"で囲んでくれますが、カンマを含んだ列が出力されるので利用する際にはひと手間かかります。今回は利用しませんでした。
イベント検知時にスクリプトを実行する
イベントを検知した際に別のスクリプトを実行したい場合は、-mや-dオプションで待ち受けることができなさそうです。inotifywaitが終了を待ち受けることを利用してbashスクリプトを組みます。
RESULTの中に「/home/user/sample.txt MODIFY」といったように結果が入りますので、その文字列を利用して処理を行います。
起動時のメッセージを出さないようにするには-qオプションを使います。
2>&1とすることで、エラー時のメッセージもRESULTに含めるようにしています。
sample.sh
while true
do
RESULT=$(inotifywait -e modify -q /home/user/ 2>&1)
echo $RESULT
# なにかしらの処理
done
仕様かバグかはわかりませんが、筆者の環境では監視対象をファイルにし-eオプションを使って個別にイベントを指定した場合には検知した際のログが表示されませんでした。コンソールが戻ってきたら$?に0が入っている事(結果コード)を確認することで、イベントを検知したことは判別はできますが、複数イベントを指定した際にはどのイベントだったか知りたいケースには対応できません。
vsftp連携
筆者がvsftpサーバで実際に運用しているスクリプトは次のようなものです。bashが苦手なのでpythonで書いています。
また、対象となるイベントを「close_write」というファイルに書込まれて閉じられた際に検知するものにしています。
sample.py
#!/usr/bin/env python3
import subprocess
import os
from datetime import datetime as dt
import syslog
# データ退避ルート
escdir="/var/ftpbackup"
# 監視ディレクトリ
obsdir="/ftproot"
# 無限ループ
while True:
#stdout=subprocess.PIPEで文字列として結果を取得しています
proc = subprocess.run(["inotifywait","-r","-q","-e","close_write",obsdir],stdout=subprocess.PIPE)
values = proc.stdout.decode("utf-8").split() # 出力はバイナリなのでデコード
# splitを引数なしに実行することで末尾の改行も消えます。
# 1列目がディレクトリ、2列目がイベント名、3列目がファイル名となります
# データ退避ディレクトリの作成
index=0
while True:
subdir = (dt.now()).strftime('%Y%m%d%H%M%S')
index += 1
strpath = escdir+'/'+subdir+'-'+str(index)
if not os.path.exists(strpath):
os.mkdir(strpath)
break
if 1000 < index:
# 想定外
exit()
# ファイル移動
proc = subprocess.run(["mv",values[0]+"/"+values[2],strpath+"/"],stdout=subprocess.PIPE,stderr=subprocess.PIPE)
# syslogに結果を出力
if proc.returncode == 0:
syslog.syslog(values[0]+"/"+values[2]+"を"+strpath+"に移動しました")
else:
syslog.syslog(values[0]+"/"+values[2]+"の移動に失敗しました")
syslog.syslog(proc.stderr.decode("utf-8"))
pythonでの標準出力取得の方法は「Python: subprocessでOSコマンドを実行する」を参考にさせていただきました。
アップロードファイル数が多い時、処理に抜けが発生する
先のコードでしばらく運用してみて、一度に多くのファイルをアップロードされると一旦終了したinotifywaitコマンドが再び待機するまでの間の変更を検知できなく、処理に抜けが発生することがわかりました。
そこで、-mオプションで待機してログを出力するプロセスと、そのログを監視してファイル移動処理をするプロセスの二段構えにしました。
sample2.py
#!/usr/bin/env python3
import subprocess
import os
from datetime import datetime as dt
import syslog
import sys
# データ退避ルート
escdir="/var/ftpbackup"
# 監視ディレクトリ
obsdir="/var/ftproot"
# メインの監視ログ
logpath="/var/inotifylog"
# ログクリア
if os.path.exists(logpath):
os.remove(logpath)
def main():
rowIndex = 0
# メインの監視 停止時はps -x等で見つけてkillの必要あり
subprocess.Popen(["touch",logpath])
subprocess.Popen(["inotifywait","-m","-q","-e","close_write","-o",logpath,obsdir])
syslog.syslog("FTPサーバーファイル監視を開始しました")
# サブの監視
while True:
subprocess.run(["inotifywait","-q","-e","modify",logpath])
# 変更を検知したらファイルオープン
f = open(logpath, 'r', encoding='UTF-8')
lines = f.readlines()
currentRowIndex = -1
for line in lines:
currentRowIndex +=1
# 処理済みの行は対象にしない
if currentRowIndex <= rowIndex :
continue
# ファイル移動処理実行
task(line)
# ファイルクローズ
f.close()
# 処理済みのインデックスを更新
rowIndex=currentRowIndex
def task(str):
...以下省略
main()
参考にさせていただきましたサイトの皆様、ありがとうございました。