ProFTPD の HiddenStores で atomic なアップロード


TL;DR

  • HiddenStores でアップロードを2段階にできる(atomic なアップロード)
    1. アップロード開始時に .in.filename. があればエラー応答で中断
    2. アップロード中は .in.filename. に書き込む
    3. アップロード完了後に filename にリネームする
  • HiddenStores は真偽値だけでなく prefix/suffix も指定できる
  • DeleteAbortedStores で中断時にファイルを削除できる
    • HiddenStores を使っている場合 DeleteAbortedStores off としない限り削除される
    • HiddenStores を使っていない場合 DeleteAbortedStores on とした場合のみ削除される

環境

  • CentOS 7.1
  • ProFTPD 1.3.5a (yum epel)

同時アップロード

ProFTPD のデフォルト設定だと、異なる接続から異なるファイルを同一のパスに同時にアップロードした場合、場合によっては内容が混ざってしまう事がある。

put a.txt |-------->| data.txt

put b.txt   |--->| data.txt

上記の様なタイミングでアップロードを実行した場合、ファイルサイズが a.txt と同じで、 a.txtb.txt の内容を含んだ妙なファイルができあがる。
アップロード処理の実装は読んでいないので、理屈については割愛。

HiddenStores

ProFTPDmod_xfer には HiddenStores というディレクティブがある。
これを有効にするとアップロードが2ステップになり、内容の混在やアップロード途中のファイルが参照されてしまう様な事態を防ぐことができる。

/etc/proftpd.conf
HiddenStores On

アップロード中は .in.filename. という中間ファイルに書き込まれる仕組みになっており、アップロード(STOR)開始時に該当ファイルが存在する場合は 550 エラー応答が返ってくる。

ftp> put hoge.dat
local: hoge.dat remote: hoge.dat
229 Entering Extended Passive Mode (|||9147|)
550 hoge.dat: Temporary hidden file /.in.hoge.dat. already exists

中間ファイルの名前を変える

HiddenStores ディレクティブは prefix と suffix を指定する事もできる。
以下の様に設定すると、中間ファイル名は .pre.filename.suf. の形式になる。

/etc/proftpd.conf
HiddenStores .pre. .suf.

アップロードをやめた場合

ftp シェルと C-c

Mac OSX 付属の ftp コマンドの対話的シェルを利用してアップロードを行い、アップロード中に C-c で中断した場合の挙動を確認。

  • put hoge.dat を実行
  • リモートの .in.hoge.dat. に書き込まれていく
  • C-c で中断
  • リモートの .in.hoge.dat.hoge.dat にリネームされた
    • HiddenStore: complex path, will rename /.in.hoge.dat. to /hoge.dat

ftp シェルと SIGTERM

今度は、同様の手順でアップロード中に、そのプロセスに対して SIGTERM を送った場合の挙動を確認。

  • put hoge.dat を実行(プロセスA)
  • リモートの .in.hoge.dat. に書き込まれていく
  • 別の zsh シェルで pkill -TERM ftp を実行
  • リモートの .in.hoge.dat. はリネームされず削除された
    • HiddenStore: complex path, will rename /.in.hoge.dat. to /hoge.dat
    • removing aborted HiddenStores file '/.in.hoge.dat.'

DeleteAbortedStores ディレクティブとは?

mod_xfer には DeleteAbortedStores というディレクティブもあり、これは部分的にアップロードされた HiddenStores ファイルを自動で削除する機能を有効にするものと説明されている。

ただし、対象となるのは ABOR コマンドによって中断された転送のみで、接続の失敗によるものは対象としないとのこと。

SIGTERM を送った場合に記録されたログメッセージ removing aborted HiddenStores file '/.in.hoge.dat.' から、ソースコードの該当行を確認すると DeleteAbortedStores ディレクティブによる挙動に見える。

mod_xfer.c
  if (session.xfer.xfer_type == STOR_HIDDEN) {
    delete_stores = get_param_ptr(CURRENT_CONF, "DeleteAbortedStores", FALSE);
    if (delete_stores == NULL ||
        *delete_stores == TRUE) {
      /* If a hidden store was aborted, remove only hidden file, not real
       * one.
       */
      if (session.xfer.path_hidden) {
        pr_log_debug(DEBUG5, "removing aborted HiddenStores file '%s'",
          session.xfer.path_hidden);
        pr_fsio_unlink(session.xfer.path_hidden);
      }
    }

設定ファイルにディレクティブを書いていない状態なので NULL となっていて、ファイルを unlink する条件「 NULL TRUE 」を満たしたのではないか。

DeleteAbortedStores off

DeleteAbortedStores off とした場合の C-c による中断と SIGTERM による中断について挙動を確認。

C-c

  • put hoge.dat を実行
  • リモートの .in.hoge.dat. に書き込まれていく
  • C-c
  • リモートの .in.hoge.dat.hoge.dat にリネームされた
    • HiddenStore: complex path, will rename /.in.hoge.dat. to /hoge.dat

SIGTERM

  • put hoge.dat を実行(プロセスA)
  • リモートの .in.hoge.dat. に書き込まれていく
  • 別の zsh シェルで pkill -TERM ftp を実行
  • リモートの .in.hoge.dat.hoge.dat にリネームされた
    • HiddenStore: complex path, will rename /.in.hoge.dat. to /hoge.dat

DeleteAbortedStores on

C-C で中断

  • put hoge.dat を実行
  • リモートの .in.hoge.dat. に書き込まれていく
  • C-c
  • リモートの .in.hoge.dat.hoge.dat にリネームされた
    • HiddenStore: complex path, will rename /.in.hoge.dat. to /hoge.dat

SIGTERM

  • put hoge.dat を実行(プロセスA)
  • リモートの .in.hoge.dat. に書き込まれていく
  • 別の zsh シェルで pkill -TERM ftp を実行
  • リモートの .in.hoge.dat. はリネームされず削除された
    • HiddenStore: complex path, will rename /.in.hoge.dat. to /hoge.dat
    • removing aborted HiddenStores file '/.in.hoge.dat.'

HiddenStores off と DeleteAbortedStores

DeleteAbortedStores なし

C-c による中断および SIGTERM による中断でアップロード途中のファイルは残る。

DeleteAbortedStores on

C-c による中断でアップロード途中のファイルが残り、 SIGTERM による中断でアップロード途中のファイルは消される。

DeleteAbortedStores off

C-c による中断および SIGTERM による中断でアップロード途中のファイルは残る。

ソースコードで確認

HiddenStores が無効である場合、ファイルを unlink する条件が「 NULL ではなく TRUE である場合」となっている。
HiddenStores が有効か無効かで unlink する条件が異なる模様。

mod_xfer.c
  if (session.xfer.xfer_type == STOR_HIDDEN) {
    ...
  } else if (session.xfer.path) {
    delete_stores = get_param_ptr(CURRENT_CONF, "DeleteAbortedStores", FALSE);
    if (delete_stores != NULL &&
        *delete_stores == TRUE) {
      pr_log_debug(DEBUG5, "removing aborted file '%s'", session.xfer.path);
      pr_fsio_unlink(session.xfer.path);
    }
  }

HiddenStores とファイルモード

HiddenStores on としている時の STOR は、ファイルをモード O_WRONLY | O_CREAT | O_EXCL でオープンする。
O_EXCL が指定されているため、すでに中間ファイルが存在している場合はオープンに失敗する。

※ ただし、NFS を使っている場合は O_EXCL を使わない。