ファイルシステム修正をDBで追跡する

世の中にはログファイルシステム(LFS)みたいなシステムもありますが、これを DB で実現しようとすると意外に難しかった。

まず、DB として、以下のようなテーブルを用意します。

CREATE TABLE node (
  id BIG INTEGER UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(1024) NOT NULL INDEX,
  flg_dir INTEGER DEFAULT(0) NOT NULL,
  parent_id BIG INTEGER UNSIGNED INDEX,
  deleted_at DATETIME
);
CREATE TABLE node_log (
  id BIG INTEGER UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  opcode INTEGER DEFAULT(1) NOT NULL,
  node_id BIG INTEGER UNSIGNED NOT NULL INDEX,
  old_name VARCHAR(1024),
  new_name VARCHAR(1024),
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

テーブル node はディレクトリ/ファイルを記録するもので、テーブル node_log は変更イベントを記録するものとなります。

1つのディレクトリ階層が node の1レコードとなります。末端はディレクトリかファイルとなりますが、flg_dir が 0(ファイル), 1(ディレクトリ) で区別することにします。

ファイル作成/更新時の動作

ファイル作成時は node_log に作成を記録するとともに、作成時は node に登録されていないため、登録処理を行います。

use App¥Models¥Node;
use App¥Models¥NodeLog;

function create_node(Request $request) {
    $names = explode('/', dirname($request->file_name));
    $fname = basename($request->file_name);
    $flg_dir = $request->flg_dir;
    $prev = null;
    $node = null;
    foreach ($names as $name) {
        if ($prev) {
            $node = Node::where('name', $name)->whereNull('deleted_at')
                ->where('parent_id', $prev->id)->first();
        } else {
            $node = Node::where('name', $name)->whereNull('deleted_at')
                ->whereNull('parent_id')->first();
        }
        if (!$node) {
            $node = new Node;
            $node->name = $name;
            $node->flg_dir = 1;
            $node->parent_id = $prev ? $prev->id : null;
            $node->save();
        }
        $prev = $node;
    }
    if ($prev) {
        $node = Node::where('name', $fname)->whereNull('deleted_at')
            ->where('parent_id', $prev->id)->first();
    } else {
        $node = Node::where('name', $fname)->whereNull('deleted_at')
            ->whereNull('parent_id')->first();
    }
    if ($node) {
        if ($node->flg_dir != $flg_dir) {
          throw new ¥Exeption("Node already created");
        }
    } else {
        $node = new Node;
        $node->name = $name;
        $node->flg_dir = $flg_dir;
        $node->parent_id = $prev ? $prev->id : null;
        $node->save();
    }
    $nlog = new NodeLog;
    $nlog->opcode = 1; // UPDATE
    $nlog->node_id = $node->id;
    $nlog->save();
}

ファイル削除時の動作

UNIX 系 OS ではディレクトリ内にファイルが残っているときはディレクトリを削除できません。この動作も入れ込んでみます。

use Carbon¥Carbon;

function remove_node(Request $request) {
    $names = explode('/', $request->file_name);
    $prev = null;
    $node = null;
    foreach ($names as $name) {
        if ($prev) {
            $node = Node::where('name', $name)->whereNull('deleted_at')
                ->where('parent_id', $prev->id)->first();
        } else {
            $node = Node::where('name', $name)->whereNull('deleted_at')
                ->whereNull('parent_id')->first();
        }
        if (!$node) {
            throw new ¥Exception("No node $name");
        }
        $prev = $node;
    }
    if (!$node) {
        throw new ¥Exception("Node not defined");
    }
    if ($node->flg_dir === 1
        && Node::where('parent_id', $node->id)->whereNull('deleted_at')->exists()) {
        throw new ¥Exception("Node not empty");
    }
    $node->deleted_at = Carbon::now();
    $node->save();

    $nlog = new NodeLog;
    $nlog->node_id = $node->id;
    $nlog->opcode = 3;  // REMOVE
    $nlog->save();
}

ファイル名変更時の動作

これが一番考えました。変更前と変更後の名前は node_log に残しておくこととし、node には最新の名称を入れておきます。

function rename_node(Request $request) {
    $names = explode('/', $request->old_file_name);
    $prev = null;
    $node = null;
    foreach ($names as $name) {
        if ($prev) {
            $node = Node::where('name', $name)->whereNull('deleted_at')
                ->where('parent_id', $prev->id)->first();
        } else {
            $node = Node::where('name', $name)->whereNull('deleted_at')
                ->whereNull('parent_id')->first();
        }
        if (!$node) {
            throw new ¥Exception("No node $name");
        }
        $prev = $node;
    }
    if (!$node) {
        throw new ¥Exception("No node found");
    }
    $oldname = $node->name;
    $node->name = $request->new_file_name;
    
    $nlog = new NodeLog;
    $nlog->node_id = $node->id;
    $nlog->opcode = 2; // RENAME
    $nlog->old_name = $oldname;
    $nlog->new_name = $node->name;
    $nlog->save();
}

過去のある時点でのファイル構成の取得

変更履歴(opcode=1)のあるファイルのうち、$when 時点で削除されていない(opcode=3)ノードを探します。flg_dir=0を指定して末端のファイルノードのみ探すようにしています。

こうして探し出したファイルノードは現在の名称と異なる可能性があるため、$when 以降最初に変更したログを調べます。もしログがあれば、ログのold_name がその時の名前となります。なければファイル名変更はなかったことになるので、ノードのnameを $when 時点での名称として採用します。

プログラムにすると、以下のようになります。ノード名決定ロジックは中間ノードでも必要であるため、可読性を上げるために関数を分けました。

use Illuminate¥Support¥Facades¥DB;

function get_snapshot($when) {
    $node_log_sums = NodeLog::select('node_id', DB::raw('max(node.id) as id'))
        ->join('node', 'node_id', 'node.id')
        ->where('flg_dir', 0)
        ->where('opcode', 1)
        ->where('node.created_at', '<=', $when)
        ->whereNotIn('node.id', function($q) use ($when) {
            $q->select('id')->from('node_logs')
              ->where('opcode', 3)->where('created_at', '<=', $when);
        })
        ->groupBy('node_id')
        ->get();
    $snapshot = [];
    foreach ($node_log_sums as $node_log_sum) {
        $node = Node::find($node_log_sum->node_id);
        $minid = NodeLog::where('node_id', $node->id)->where('created_at', '>', $when)
            ->where('opcode', 2)->min('id');
        if ($minid === null) {
            $fname = $node->name;
        } else {
            $nlog = NodeLog::find($minid);
            $fname = $nlog->old_name;
        }
        $snapshot[] = get_filename($node, $fname, $when)
    }
}

function get_filename($node, $fname, $when) {
    $ar = [$fname];
    while ($node->parent_id !== null) {
        $node = Node::find($node->parent_id);
        $minid = NodeLog::where('node_id', $node->id)->where('created_at', '>', $when)
            ->where('opcode', 2)->min('id');
        if ($minid === null) {
            $ar[] = $node->name;
        } else {
            $nlog = NodeLog::find($minid);
            $ar[] = $nlog->old_name;
        }
    }
    $ar = array_reverse($ar);
    return implode('/', $ar);
}

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です