100日チャレンジ day36 (FUSE+SQLiteの独自ファイルシステム)

昨日
100日チャレンジに感化されたので、アレンジして自分でもやってみます。
やりたいこと
- 世の中のさまざまなドメインの簡易実装をつくり、バックエンドの実装に慣れる(dbスキーマ設計や、関数の分割、使いやすいインターフェイスの切り方に慣れる
- 設計力(これはシステムのオーバービューを先に自分で作ってaiに依頼できるようにする
- 生成aiをつかったバイブコーティングになれる
- 実際にやったことはzennのスクラップにまとめ、成果はzennのブログにまとめる(アプリ自体の公開は必須ではないかコードはgithubにおく)
できたもの

FUSEを用いた独自ファイルシステム を作る

なるほど!FUSE (Filesystem in Userspace) を使いたいんですね。承知しました。これは非常に面白い挑戦です!SQLiteをバックエンドにして、Go言語でユーザー空間ファイルシステムを実装しましょう。OSのファイルシステムコールをフックして、SQLiteへの読み書きに変換する形になりますね。
修正後の提案:
-
目的: Go言語とFUSEライブラリ (
bazil.org/fuse
) を使い、SQLiteデータベースを永続化ストレージとするユーザー空間ファイルシステムを実装する。 -
主要機能:
- OSからの標準的なファイルシステム操作(ディレクトリ作成・一覧表示・削除、ファイル作成・読み込み・書き込み・削除、属性取得など)に対応する。
- ファイルとディレクトリの構造、メタデータ、内容をSQLiteデータベース (
db/dev.db
) に保存する。 - 指定したディレクトリ(マウントポイント)にこのカスタムファイルシステムをマウントできるようにする。
-
使用ライブラリ:
-
bazil.org/fuse
およびそのサブパッケージ (fuse/fs
) database/sql
github.com/mattn/go-sqlite3
-
-
UI: なし。OS標準のコマンドラインツール (
ls
,mkdir
,cat
,echo
,rm
など) やファイルエクスプローラー (Finder, Explorer等) からマウントポイントを通じて操作する。 - ユーザー分離: 今回は実装スコープ外とし、単一のファイルシステム空間とします。
作業順序 (FUSE版):
-
プロジェクト初期化:
-
day36_fuse_sqlite_fs_go
ディレクトリを作成します。 -
go mod init github.com/lirlia/100day_challenge_backend/day36_fuse_sqlite_fs_go
を実行します。 -
go get bazil.org/fuse
とgo get github.com/mattn/go-sqlite3
を実行します。 -
main.go
(マウント処理)、fs.go
(FUSEインターフェース実装)、store.go
(SQLite操作)、db/schema.sql
(DBスキーマ) のようなファイル構造を作成します。 -
README.md
にアプリ概要を記述します。
-
-
データモデリングとDB設定:
-
db/schema.sql
に、ファイルとディレクトリの情報を格納するテーブルスキーマを定義します。inode、名前、親inode、種類(ファイル/ディレクトリ)、サイズ、パーミッション、タイムスタンプ、ファイル内容(BLOBまたは別テーブル)などを考慮します。 -
store.go
に、DB接続・初期化処理と、FUSE操作に対応するSQLite操作関数(例:CreateNode
,ReadNode
,WriteNodeData
,ListDir
,GetNodeAttributes
)を実装します。
-
-
FUSE実装 (
fs.go
):-
fuse/fs
パッケージの主要なインターフェース (FS
,Node
,Handle
など) を実装するGoの構造体 (FS
,Dir
,File
など) を定義します。 - OSからの各ファイルシステム操作に対応するメソッド(
Root
,Lookup
,Mkdir
,Create
,Read
,Write
,Readdir
,Getattr
,Setattr
,Remove
など)を実装します。これらのメソッド内でstore.go
の関数を呼び出し、SQLiteデータベースを操作します。ノード(inode)の管理が重要になります。
-
-
マウント処理 (
main.go
):- コマンドライン引数からマウントポイントのパスを受け取ります。
- SQLiteデータベースを初期化します(必要ならスキーマ適用も)。
-
fuse.Mount
を呼び出して、実装したファイルシステム (fs.go
のFS
構造体) を指定されたマウントポイントに接続します。 - 適切にアンマウントできるようにシグナルハンドリング(Ctrl+Cなど)を実装します。
-
ビルドとテスト:
-
go build
で実行ファイルを生成します。 - マウントポイント用のディレクトリを作成します (例:
mkdir ./mnt
)。 - ファイルシステムをマウントします (例:
sudo ./day36_fuse_sqlite_fs_go ./mnt
※FUSE操作には通常root権限か特定グループへの所属が必要です)。 - 別のターミナルを開き、マウントポイント (
./mnt
) に対して標準的なファイル操作コマンド (ls -la
,mkdir hello
,echo 'world' > hello/world.txt
,cat hello/world.txt
,rm hello/world.txt
,rmdir hello
) を実行し、期待通りに動作するか、SQLiteデータベースの内容が更新されるかを確認します。
-
-
ドキュメント作成:
README.md
に使い方や実装の詳細を記述し、knowledge.mdc
を更新します。
このFUSEを使った実装方針で進めましょう。準備ができましたら、まずプロジェクトの初期化から始めます。

作業手順
承知しました。複雑なプロジェクトになるため、詳細な計画とステップごとのテストは非常に重要ですね。以下にディレクトリ構造、ファイル一覧、そしてテストを組み込んだ具体的な作業ステップを提案します。
ディレクトリ構造案:
/day36_fuse_sqlite_fs_go/
├── main.go # エントリーポイント、マウント処理、コマンドライン引数処理
├── fusefs/ # FUSEファイルシステムロジック
│ ├── fs.go # FSインターフェース実装 (ルートノード提供)
│ ├── node.go # Nodeインターフェースの基本実装や共通処理 (Attrなど)
│ ├── dir.go # Dirノード (ディレクトリ操作: Lookup, Mkdir, Readdir, Remove)
│ ├── file.go # Fileノード (ファイル操作: Open, Create)
│ └── handle.go # Handleインターフェース実装 (Read, Write)
├── store/ # SQLiteストア関連
│ ├── store.go # DB操作インターフェース定義
│ ├── db.go # SQLite接続、初期化、スキーマ適用
│ ├── node_store.go # ノード(inode, メタデータ)関連のDB操作実装
│ ├── data_store.go # ファイルデータ関連のDB操作実装
│ └── schema.sql # DBスキーマ定義
├── models/ # データモデル
│ └── node.go # ノード情報 (ファイル/ディレクトリ) の構造体定義
├── tests/ # ユニットテスト用
│ ├── store_test.go # storeパッケージのユニットテスト
│ └── helpers_test.go # テスト用のヘルパー関数 (一時DB作成など)
├── .gitignore
├── go.mod
├── go.sum
└── README.md
作業ステップ:
Phase 1: 基盤構築 (DB とストア)
-
プロジェクト初期化と依存関係:
-
実装:
-
day36_fuse_sqlite_fs_go
ディレクトリ作成。 -
go mod init github.com/lirlia/100day_challenge_backend/day36_fuse_sqlite_fs_go
実行。 -
go get bazil.org/fuse
とgo get github.com/mattn/go-sqlite3
実行。 - 上記ディレクトリ構造の空ファイルとディレクトリを作成 (
.gitignore
,README.md
も)。
-
- テスト: なし (初期設定)。
-
実装:
-
DBスキーマ定義 (
store/schema.sql
):-
実装:
inodes
テーブル(id, parent_id, name, is_dir, mode, size, atime, mtime, ctime)とfile_data
テーブル(inode_id, offset, data)のCREATE TABLE
文を記述。ルートディレクトリ用の初期レコードも定義(例: id=1, parent_id=1, name='', is_dir=true)。 - テスト: スキーマの妥当性を目視確認。
-
実装:
-
DB接続と初期化 (
store/db.go
):-
実装:
NewDBConnection(dataSourceName string)
関数でDB接続 (sql.Open
) とPingを実行。InitializeSchema(db *sql.DB)
関数でschema.sql
を読み込み、Exec()
で実行(エラーハンドリング含む)。 -
テスト:
-
tests/helpers_test.go
: テスト用のインメモリDBまたは一時ファイルDBを作成/削除するヘルパー関数を実装。 -
tests/store_test.go
: ヘルパーを使いNewDBConnection
とInitializeSchema
がエラーなく実行できるかテスト。
-
-
実装:
-
データモデル定義 (
models/node.go
):-
実装:
inodes
テーブルに対応するNode
構造体を定義(Goの型で)。タイムスタンプはtime.Time
。 - テスト: なし (構造体定義のみ)。
-
実装:
-
ノードストア実装 (
store/node_store.go
とstore/store.go
):-
実装:
-
store/store.go
:NodeStore
インターフェースを定義 (GetNode, GetChildNode, ListChildren, CreateNode, UpdateNode, DeleteNode)。 -
store/node_store.go
:NodeStore
インターフェースを実装する構造体とメソッドを定義。SQL文を記述し、*sql.DB
を使ってDB操作。ルートノード(id=1)が存在しない場合の初期作成処理も考慮。
-
-
テスト:
tests/store_test.go
でNodeStore
の各メソッドに対するユニットテストを作成 (CRUD操作、親子関係、存在しない場合の挙動など)。
-
実装:
-
データストア実装 (
store/data_store.go
とstore/store.go
):-
実装:
-
store/store.go
:DataStore
インターフェースを定義 (ReadData, WriteData, DeleteData)。 -
store/data_store.go
:DataStore
インターフェースを実装する構造体とメソッドを定義。ReadData
は指定オフセット/サイズでfile_data
からSELECT
。WriteData
はREPLACE INTO
またはINSERT OR REPLACE
でデータを書き込み、関連するinodes
テーブルのsize
とmtime
も更新 (トランザクション考慮)。DeleteData
はDELETE
。
-
-
テスト:
tests/store_test.go
でDataStore
の各メソッドに対するユニットテストを作成 (読み書き、オフセット、サイズ更新、データ削除など)。
-
実装:
Phase 2: FUSE 実装
-
FUSE FS実装 (
fusefs/fs.go
):-
実装:
-
FileSystem
構造体を定義 (NodeStore
,DataStore
を保持)。 -
fuse/fs.FS
インターフェースのRoot()
メソッドを実装。NodeStore.GetNode(1)
でルートノードを取得し、fusefs.Dir
(次のステップで作成) として返す。
-
- テスト: この時点では統合テストのみ可能。
-
実装:
-
FUSE Node基本実装 (
fusefs/node.go
):-
実装:
-
Node
構造体を定義 (models.Node
,NodeStore
,DataStore
を保持)。 -
fuse/fs.Node
インターフェースのAttr(ctx context.Context, a *fuse.Attr)
メソッドを実装。保持しているmodels.Node
の情報 (mode, size, times) をfuse.Attr
に設定。パーミッション (mode) は適切に設定。Inode番号はDBのIDを使用。
-
- テスト: 統合テストでの確認が主。
-
実装:
-
FUSE Dir実装 (
fusefs/dir.go
):-
実装:
-
Dir
構造体を定義 (fusefs.Node
を埋め込み)。 -
fuse/fs.NodeLookuper
:Lookup(ctx, name)
メソッド実装 (NodeStore.GetChildNode
を使用)。見つかったらfusefs.Dir
またはfusefs.File
を返す。 -
fuse/fs.NodeMkdirer
:Mkdir(ctx, req)
メソッド実装 (NodeStore.CreateNode
でディレクトリ作成)。 -
fuse/fs.HandleReadDirAller
:ReadDirAll(ctx)
メソッド実装 (NodeStore.ListChildren
で子ノード一覧取得しfuse.Dirent
スライスに変換)。 -
fuse/fs.NodeRemover
:Remove(ctx, req)
メソッド実装 (NodeStore.DeleteNode
を使用。ファイル削除と空ディレクトリ削除に対応)。
-
- テスト: 統合テストでの確認が主。
-
実装:
-
FUSE File実装 (
fusefs/file.go
):-
実装:
-
File
構造体を定義 (fusefs.Node
を埋め込み)。 -
fuse/fs.NodeOpener
:Open(ctx, req, resp)
メソッド実装。FileHandle
(次のステップ) を作成しresp.Handle
に設定。フラグ (req.Flags
) に応じたパーミッションチェックは簡易的に。 -
fuse/fs.NodeCreater
:Create(ctx, req, resp)
メソッド実装 (NodeStore.CreateNode
でファイル作成、FileHandle
を作成して返す)。
-
- テスト: 統合テストでの確認が主。
-
実装:
-
FUSE Handle実装 (
fusefs/handle.go
):-
実装:
-
FileHandle
構造体を定義 (models.Node
,NodeStore
,DataStore
を保持)。 -
fuse/fs.HandleReader
:Read(ctx, req, resp)
メソッド実装 (DataStore.ReadData
を使用。req.Offset
,req.Size
に対応)。 -
fuse/fs.HandleWriter
:Write(ctx, req, resp)
メソッド実装 (DataStore.WriteData
を使用。req.Offset
,req.Data
を渡し、書き込みバイト数をresp.Size
に設定)。
-
- テスト: 統合テストでの確認が主。
-
実装:
Phase 3: 結合とテスト
-
メイン処理とマウント (
main.go
):-
実装:
-
flag
パッケージ等でコマンドライン引数(マウントポイント、DBファイルパス)を処理。 -
store.NewDBConnection
,store.InitializeSchema
を呼び出しDB準備。 -
NodeStore
,DataStore
のインスタンスを作成。 -
fusefs.NewFileSystem
(コンストラクタ) でファイルシステムインスタンス作成。 -
fuse.Mount
を呼び出しマウント実行。エラーハンドリング。 -
signal.Notify
でSIGINT
,SIGTERM
を捕捉し、fuse.Unmount
を呼び出す後処理を追加。
-
-
テスト (結合テスト):
-
go build
で実行ファイル作成。 - マウントポイント作成 (
mkdir ./mnt
)。 - マウント (
sudo ./day36_fuse_sqlite_fs_go -db ./db/dev.db ./mnt
または FUSEの設定によってはsudo
不要)。 -
別ターミナルでマウントポイント (
./mnt
) に対して以下のコマンドを実行し、エラーなく動作すること、DBの内容が意図通り変更されることを確認:ls -la ./mnt
mkdir ./mnt/testdir
ls -l ./mnt
echo "hello" > ./mnt/testfile.txt
-
cat ./mnt/testfile.txt
(内容が "hello" であること) -
ls -l ./mnt
(testfile.txt のサイズが正しいこと) echo " world" >> ./mnt/testfile.txt
-
cat ./mnt/testfile.txt
(内容が "hello world" であること) rm ./mnt/testfile.txt
rmdir ./mnt/testdir
-
ls -la ./mnt
(空になっていること)
- Ctrl+Cでプロセスを停止し、正常にアンマウントされることを確認。
-
-
実装:
-
ドキュメントと最終化:
-
実装:
README.md
にビルド方法、マウント方法、使い方、制限事項(未実装機能など)を記述。knowledge.mdc
を更新。コード内のコメントを整備。 - テスト: 全体の動作を再確認。
-
実装:
このステップで進めていきましょう。まずは Phase 1 のステップ1から始めます。準備ができたら指示してください。

https://bazil.org/fuse/ が古くもう対応していないので
https://github.com/hanwen/go-fuse を使う

ビルドするのにめっちゃ苦労してる

きたか・・・?

❯ mkdir mnt/testdir
ls -la mnt
echo "hello fuse" > mnt/hello.txt
cat mnt/hello.txt
ls -la mnt
touch mnt/testdir/emptyfile
ls -la mnt/testdir
rm mnt/hello.txt
rmdir mnt/testdir
ls -la mnt
mnt/testdir
total 0
drwxr-xr-x 3 noname staff 96 5 6 13:47 .
drwxr-xr-x 14 noname staff 448 5 6 13:45 ..
drwxr-xr-x 2 noname staff 64 5 6 13:47 testdir
hello fuse
total 8
drwxr-xr-x 4 noname staff 128 5 6 13:47 .
drwxr-xr-x 14 noname staff 448 5 6 13:45 ..
-rw-r--r-- 1 noname staff 11 5 6 13:47 hello.txt
drwxr-xr-x 2 noname staff 64 5 6 13:47 testdir
total 0
drwxr-xr-x 3 noname staff 96 5 6 13:47 .
drwxr-xr-x 4 noname staff 128 5 6 13:47 ..
-rw-r--r-- 1 noname staff 0 5 6 13:47 emptyfile
rmdir: mnt/testdir: Directory not empty
total 0
drwxr-xr-x 3 noname staff 96 5 6 13:47 .
drwxr-xr-x 14 noname staff 448 5 6 13:45 ..
drwxr-xr-x 3 noname staff 96 5 6 13:47 testdir

macFUSE をそもそも入れないとダメだった
そしてこれを入れるには mac のセキュリティレベルを下げないとダメ

main !?⇡ 🇬️ default v1.24.2
❯ ./sqlitefs -debug -mountpoint mnt
2025/05/06 14:52:34 Using database at: ./db/dev.db
2025/05/06 14:52:34 Root node already exists.
2025/05/06 14:52:34 Database schema initialized successfully.
2025/05/06 14:52:34 Root -> OnAdd() called
2025/05/06 14:52:34 Root -> OnAdd() loaded root model: &{ID:1 ParentID:0 Name: IsDir:true Mode:drwxr-xr-x Size:0 Atime:2025-05-06 14:00:13 +0900 JST Mtime:2025-05-06 14:00:13 +0900 JST Ctime:2025-05-06 14:00:13 +0900 JST UID:0 GID:0}
2025/05/06 14:52:34 rx 2: INIT n0 {7.19 Ra 2097152 ATOMIC_O_TRUNC,CACHE_SYMLINKS,NODE_RWLOCK,RENAME_SWAP,RENAME_EXCL,ALLOCATE,CASE_INSENSITIVE,VOL_RENAME,XTIMES} p4471
2025/05/06 14:52:34 tx 2: OK, {7.19 Ra 2097152 RENAME_SWAP 0/0 Wr 131072 Tg 0 MaxPages 8 MaxStack 1}
2025/05/06 14:52:34 rx 2: STATFS n1 p4471
2025/05/06 14:52:34 tx 2: OK, {blocks (0,0)/0 files 0/0 bs0 nl0 frs0}
2025/05/06 14:52:34 rx 2: STATFS n1 p339
2025/05/06 14:52:34 tx 2: OK, {blocks (0,0)/0 files 0/0 bs0 nl0 frs0}
2025/05/06 14:52:34 rx 2: ACCESS n1 {u=501 g=20 x} p4470
2025/05/06 14:52:34 Root -> Getattr() called
2025/05/06 14:52:34 Node -> Getattr() called for ID: 0
2025/05/06 14:52:34 Node -> Getattr() success for ID: 1, Size
なんか成功した

できた〜〜
macFuse 経由でマウントとして sqlite をバックエンドにしたファイルシステム!

今回やったこと
はい、承知いたしました。FUSE ファイルシステム、特に今回の Go と SQLite を使った実装がどのように動いているのか、基本的な仕組みと使用したツール(ライブラリ)の役割を説明しますね。
1. FUSE とは? - ファイルシステムを「自作」する仕組み
- FUSE (Filesystem in Userspace): 通常、ファイルシステム(データを整理し、ファイルとしてアクセス可能にする仕組み)は OS のカーネルというコア部分で動作します。FUSE は、このファイルシステムのロジックを カーネルの外 、つまり通常のアプリケーションと同じ「ユーザースペース」で開発・実行できるようにする仕組みです。
-
仲介役: OS カーネルには FUSE をサポートするためのモジュール (macOS なら
macfuse
、Linux ならfuse
) があり、これがユーザースペースで作られたファイルシステムプロセス (今回はsqlitefs
) とカーネルの間の通信を仲介します。
2. どうやって動いているの? - OS と自作プログラムの連携プレー
ユーザーがマウントポイント (今回は mnt
ディレクトリ) に対してファイル操作を行うと、以下のような流れで処理が進みます。
-
ユーザー操作: あなたがターミナルで
ls mnt/
やecho "hello" > mnt/file.txt
などのコマンドを実行します。 -
カーネルが検知: OS カーネルが、
/path/to/mnt
ディレクトリへの操作要求を検知します。 -
FUSE モジュールへ: カーネルは、
mnt
が FUSE によってマウントされていることを知っているので、その操作要求を FUSE カーネルモジュールに渡します。 -
sqlitefs
へリクエスト: FUSE カーネルモジュールは、マウント時に指定された我々のプログラムsqlitefs
(ユーザースペースで動いているプロセス) に対して、「mnt
ディレクトリの内容を教えて (Readdir)」や「mnt/file.txt
を作成して (Create)」といった具体的なリクエストを送ります。 -
sqlitefs
の処理:-
sqlitefs
プログラムは FUSE モジュールからのリクエストを受け取ります。 - リクエスト内容に応じて、実装した処理を実行します。例えば、「
mnt
ディレクトリの内容を教えて」なら、SQLite データベース (db/dev.db
) のnodes
テーブルを検索して、該当するファイルやディレクトリのリストを取得します。 - 「
mnt/file.txt
を作成して」なら、nodes
テーブルに新しいファイルのエントリを追加し、file_data
テーブルにも(最初は空の)データエントリを追加します。
-
-
sqlitefs
から応答: 処理結果(成功したか、エラーか、要求されたデータは何かなど)を FUSE カーネルモジュールに返します。 -
カーネルへ応答: FUSE カーネルモジュールは、
sqlitefs
からの応答を OS カーネルに伝えます。 -
ユーザーへ反映: OS カーネルは、受け取った結果を最終的にユーザーが実行したコマンド(
ls
やecho
)に返します。これにより、ls
ならファイルリストが表示され、echo
なら成功するかエラーメッセージが表示されます。
hanwen/go-fuse/v2
ライブラリは何をしている? - FUSE 通信のお手伝い
3. -
通信の抽象化: 上記の「FUSE カーネルモジュールとのリクエスト/応答のやり取り」は、実際には低レベルな通信プロトコルに基づいています。
hanwen/go-fuse/v2
は、この複雑な通信部分を隠蔽し、Go のインターフェースとして扱えるようにしてくれるライブラリです。 -
インターフェース実装: このライブラリは
fs.NodeLookuper
(Lookup 操作用)、fs.NodeGetattrer
(Getattr 操作用)、fs.NodeReaddirer
(Readdir 操作用)、fs.NodeCreater
(Create 操作用)、fs.FileWriter
(Write 操作用) といったインターフェースを提供します。 -
開発者の役割: 私たちは、これらのインターフェースを満たす Go の構造体 (
sqliteNode
,sqliteRoot
,sqliteDir
,sqliteFile
,sqliteHandle
など) を実装するだけで済みます。各メソッド内で、対応するファイルシステム操作のロジック(主にstore
パッケージを呼び出す部分)を記述します。 -
サーバー起動:
fs.Mount()
関数を呼び出すことで、実装したファイルシステムロジック (sqliteRoot
から始まるツリー) を指定したマウントポイントに接続し、FUSE カーネルモジュールからのリクエストを待ち受けるサーバープロセスを開始します。
store
パッケージ) は何をしている? - データの保存場所
4. SQLite バックエンド (-
データ永続化: FUSE ファイルシステムはメモリ上だけで完結することもできますが、今回はデータを永続化(プログラムを終了しても消えないように)するために SQLite を使っています。
store
パッケージがデータベースとのやり取りを担当します。 -
nodes
テーブル: ファイルシステムの「骨格」となる情報を保存します。- 各行がファイルまたはディレクトリ (inode に相当) を表します。
-
id
(一意な識別子)、parent_id
(親ディレクトリのID)、name
(ファイル/ディレクトリ名)、is_dir
(ディレクトリかどうかのフラグ)、mode
(パーミッション)、size
(ファイルサイズ)、uid
,gid
(所有者情報)、atime
,mtime
,ctime
(タイムスタンプ) などのカラムを持ちます。 - ディレクトリ構造は
parent_id
を辿ることで再現できます。
-
file_data
テーブル: 実際のファイルの内容を保存します。-
node_id
カラムでnodes
テーブルのファイルエントリと紐づきます。 -
data
カラムにファイルの内容がバイナリデータ (BLOB) として格納されます。
-
-
役割分担:
fusefs
パッケージ(FUSE のインターフェース実装)は、「どんな操作が要求されたか」に応じてstore
パッケージの関数 (GetNode
,ListChildren
,CreateNode
,WriteData
など) を呼び出します。store
パッケージが実際に SQL を実行して、データベースの読み書きを行います。
echo "hi" > mnt/hello.txt
5. 具体的な処理の流れの例: - シェルが
mnt/hello.txt
を作成しようとします。 - カーネル -> FUSE モジュール ->
sqlitefs
プロセスにLookup
リクエスト ("hello.txt")。 -
sqliteRoot.Lookup
が呼ばれ、store.GetChildNode(1, "hello.txt")
を実行。 -
store
は DB を検索し、「見つからない」(store.ErrNotFound
) を返す。 -
sqliteRoot.Lookup
はsyscall.ENOENT
(ファイルなし) を FUSE モジュールに返す。 - シェルはファイルがないことを理解し、次に
Create
リクエストを送る。 - カーネル -> FUSE モジュール ->
sqlitefs
プロセスにCreate
リクエスト ("hello.txt", パーミッションなど)。 -
sqliteRoot.Create
が呼ばれる。 - (内部で再度
store.GetChildNode
を呼び存在しないことを確認) -
store.CreateNode
を呼び出し、nodes
テーブルにhello.txt
のレコード (is_dir=false) を、file_data
テーブルに空のレコードを作成。DB から新しいnode_id
(例: 2) が払い出される。 -
sqliteRoot.Create
は、新しいファイルを表すsqliteFile
(ID=2) と、ファイルを操作するためのsqliteHandle
を作成し、FUSE モジュールに返す。 - シェルはファイルが開かれたことを認識し、次に
"hi\n"
を書き込むWrite
リクエストを送る。 - カーネル -> FUSE モジュール ->
sqlitefs
プロセスにWrite
リクエスト (ハンドル、データ"hi\n"
、オフセット 0)。 -
sqliteHandle.Write
が呼ばれる。 -
store.WriteData(2, 0, []byte("hi\\n"))
を呼び出す。 -
store.WriteData
はfile_data
テーブルのnode_id=2
のdata
を更新し、nodes
テーブルのnode_id=2
のsize
,mtime
,ctime
を更新する。新しいサイズ (3) を返す。 -
sqliteHandle.Write
は書き込んだバイト数 (3) を FUSE モジュールに返す。 - シェルは書き込みが成功したことを認識し、ファイルを閉じる (
Release
リクエスト)。 - カーネル -> FUSE モジュール ->
sqlitefs
プロセスにRelease
リクエスト。 -
sqliteHandle.Release
が呼ばれる (今回は特に何もしない)。成功を返す。
このように、OS カーネル、FUSE モジュール、自作の sqlitefs
プロセス (hanwen/go-fuse
ライブラリ利用)、そして SQLite データベースが連携して、一連のファイル操作が実現されています。
少し長くなりましたが、これで全体の流れと各コンポーネントの役割が掴めたでしょうか? 不明な点があれば、さらに質問してください。

なるほどということは、通常は意識しないが xfs / afs / ext4 などを使っている場合は、カーネル空間にそれを制御するアプリが動いていて、ユーザが ls を叩くと kernel からの命令が各fsをコントロールするアプリ?に飛んで行ってるんですかね
はい、その理解は半分正しくて、半分は少し異なります。FUSEの仕組みを理解された上で、それを従来のファイルシステムに当てはめて考えてくださっているのは素晴らしいです!
-
共通点: ユーザーが
ls
などのコマンドを実行すると、OSのカーネルがその要求を受け取り、最終的にファイルシステム固有の処理が実行される、という大まかな流れは同じです。 -
大きな違い: XFS, APFS (AFSではなく、おそらくApple File Systemのことかと思います), ext4 といった従来の(ネイティブな)ファイルシステムは、FUSEのように「ユーザースペースで動く制御アプリ」を持っているわけではありません。これらのファイルシステムのロジックは、OSのカーネル空間に直接組み込まれたモジュール(ドライバ) として実装されています。
流れを比較するとこうなります。
1. FUSE ファイルシステム (今回の sqlitefs
の場合):
ユーザー操作 (ls mnt/
)
↓
カーネル (VFS: Virtual File System レイヤー)
↓
FUSE カーネルモジュール
↓ (カーネル空間 → ユーザースペースへの委譲)
sqlitefs
プロセス (ユーザースペース) ← ここが「制御アプリ」
↓
SQLite データベース (ストレージ)
↓
(結果が逆向きに返る)
2. ネイティブファイルシステム (例: ext4):
ユーザー操作 (ls /home/user/
)
↓
カーネル (VFS レイヤー)
↓ (カーネル内部での処理)
ext4 カーネルモジュール (カーネル空間) ← ロジックはカーネル内
↓
ブロックデバイス (HDD/SSD)
↓
(結果が逆向きに返る)
ポイント:
- VFS (Virtual File System): Linux や macOS などの多くの OS カーネルには VFS という共通のインターフェース層があります。ユーザーからのファイル操作要求はまず VFS が受け取ります。
- ファイルシステムドライバ: VFS は、操作対象のパスがどのファイルシステムに属しているかを判断し、そのファイルシステムを担当するカーネル内のドライバ(モジュール) に処理を依頼します。ext4 なら ext4 ドライバ、APFS なら APFS ドライバです。
- カーネル内で完結: ネイティブファイルシステムの場合、VFS から依頼を受けたドライバは、カーネル空間の中で ディスク上のデータ構造(inode やデータブロックなど)を直接読み書きして処理を完結させます。ユーザースペースの別の「制御アプリ」が動いていて、それに処理を依頼する、ということはありません。
- FUSEの特殊性: FUSE は、この VFS からの依頼をカーネル内の FUSE モジュールが受け取り、それをユーザースペースで動作している指定のプロセスに転送する、という点が特殊なのです。これにより、カーネルモジュールを直接開発することなく、比較的安全かつ容易に新しいファイルシステムを実装できます。
ですので、「カーネルから命令が各 FS をコントロールするモジュールに飛んで行ってる」という点では正しいですが、そのモジュールがネイティブファイルシステムの場合はカーネル空間にあり、FUSE の場合はユーザースペースのプロセスである、という違いがあります。
この説明で、FUSE とネイティブファイルシステムの違いがより明確になったでしょうか?

はい、今回作成したFUSEファイルシステムにおける「node (ノード)」は、文脈によっていくつかの意味合いで使われています。整理して説明しますね。
-
ファイルシステムの基本概念としての「ノード (Inode)」:
- 一般的なファイルシステム(ext4, APFS, NTFS など)では、「inode (アイノード、index node)」という概念があります。これは、ファイルやディレクトリといったファイルシステム上のオブジェクト一つ一つを表す管理情報のことです。
- inode には、そのオブジェクトの メタデータ (種類[ファイルかディレクトリか]、パーミッション、所有者ID、グループID、サイズ、タイムスタンプ、そして実際のデータがディスクのどこにあるかを示すポインタなど) が格納されています。
-
重要な点: ファイル名自体は通常 inode には含まれず、ディレクトリのデータ部分に「ファイル名と対応する inode 番号」のリストとして格納されます。
ls
コマンドは、このディレクトリ情報を読み取ってファイル名を表示し、ls -l
などでは inode 番号を使って inode 情報を取得し、詳細を表示します。
-
データベース上の表現:
models.Node
とnodes
テーブル:- 今回のプロジェクトでは、SQLite データベースを使ってファイルシステムの構造を保存しています。
-
store
パッケージ内のmodels/node.go
で定義したmodels.Node
構造体が、データベース上の表現に対応します。 - この構造体は、
nodes
テーブルのカラム(id
,parent_id
,name
,is_dir
,mode
,size
,uid
,gid
,atime
,mtime
,ctime
)にマッピングされます。 - つまり、
models.Node
は、我々のファイルシステムにおける 永続化された inode 情報 と考えることができます。ただし、本来の inode と違い、便宜上name
も含んでいます。
-
FUSE ライブラリ内の表現:
fs.Inode
(hanwen/go-fuse/v2
)- 使用した Go FUSE ライブラリ (
hanwen/go-fuse/v2
) も、内部でファイルシステム上のノードを管理するためのfs.Inode
という構造体を持っています。 - これは、FUSE カーネルモジュールと通信し、ノードの状態 (例えば、カーネルがどのノードを認識しているか、参照カウントなど) を管理するためのライブラリ内部の表現です。
- 我々が実装した
sqliteNode
は、このfs.Inode
を埋め込む (embed) ことで、FUSE ライブラリの機能と連携しています。
- 使用した Go FUSE ライブラリ (
-
我々の実装における表現:
sqliteNode
,sqliteRoot
,sqliteDir
,sqliteFile
-
fusefs
パッケージ内で定義したこれらの構造体が、FUSE ライブラリからのリクエストに応答する具体的な実装です。 -
sqliteNode
:-
fs.Inode
を埋め込み、FUSE ライブラリとの基本的な接続を提供します。 -
store.Store
(データベースアクセス用) と*models.Node
(DBから読み込んだメタデータ) への参照を持ちます。 -
Getattr
のような共通の操作の一部を実装します。
-
-
sqliteRoot
,sqliteDir
,sqliteFile
:- それぞれファイルシステムのルート、ディレクトリ、ファイルを表します。
-
sqliteNode
を埋め込み、共通機能を引き継ぎます。 - それぞれの種類に応じた FUSE 操作 (インターフェース) を実装します。例えば、
sqliteDir
はLookup
,Readdir
,Mkdir
などを、sqliteFile
はOpen
,Read
,Write
などを実装します。
-
まとめ:
今回のプロジェクトにおける「node」は、
-
データベース上:
models.Node
構造体とnodes
テーブルとして存在し、ファイル/ディレクトリのメタデータを永続的に保存します。 -
FUSEライブラリ内:
fs.Inode
として存在し、カーネルとの通信状態を管理します。 -
我々のGoコード内:
sqliteNode
をベースとし、具体的な種類 (sqliteRoot
,sqliteDir
,sqliteFile
) として存在し、FUSE 操作に対する実際の振る舞い (ロジック) を実装します。これらの構造体がfs.Inode
を埋め込み、models.Node
を参照することで、全体が連携しています。
このように、異なる層で「ノード」という概念がそれぞれの役割を持って表現されている、と理解すると分かりやすいかと思います。

はい、承知いたしました。各コンポーネントの関係性を Mermaid を使って図示します。
図の説明:
-
ユーザー操作:
ls
やecho
などのコマンドが起点です。 -
OS カーネル空間:
- カーネル (VFS) が操作を受け付け、それが FUSE マウントポイントに対するものだと認識します。
- FUSE カーネルモジュールに処理を依頼します。
-
ユーザースペース (
sqlitefs
プロセス):-
FUSE ライブラリ: FUSE カーネルモジュールからの低レベルな要求を受け取り、Go のインターフェース (
fs.NodeLookuper
など) に変換して呼び出します。ノードの状態 (fs.Inode
) も管理します。 -
ファイルシステム実装 (
fusefs
): 私たちが実装した部分です。ライブラリのインターフェースを実装し、具体的な処理を行います (sqliteNode
をベースにsqliteRoot
,sqliteDir
,sqliteFile
,sqliteHandle
が対応)。 -
データストア層 (
store
): ファイルシステム実装からの要求を受け、データベース (SQLiteDB
) に対する実際の読み書き (SQL実行) を行います。 -
データモデル (
models
): データベースのテーブル構造 (nodes
) に対応する Go の構造体 (models.Node
) を定義します。
-
FUSE ライブラリ: FUSE カーネルモジュールからの低レベルな要求を受け取り、Go のインターフェース (
- SQLite データベース: 実際のメタデータとファイル内容が保存される場所です。
処理の流れ (例: ls mnt/
)
-
ls
コマンド実行。 - カーネル (VFS) が FUSE モジュールに
mnt/
のReaddir
(ディレクトリ内容読み取り) を要求。 - FUSE モジュールが
sqlitefs
プロセスにReaddir
リクエストを送信。 - Go FUSE ライブラリがリクエストを受け取り、
sqliteRoot
(またはsqliteDir
) のReaddir
メソッドを呼び出す。 -
Readdir
メソッドはstore
パッケージのListChildren
関数を呼び出す。 -
ListChildren
は SQLite DB のnodes
テーブルをSELECT
し、子ノードのリストを取得して返す。 -
Readdir
メソッドは取得したリストを FUSE が要求する形式に変換し、ライブラリに返す。 - ライブラリ → FUSE モジュール → カーネル (VFS) →
ls
コマンド へ結果が返され、ファイル名が表示される。
この図で、各コンポーネントがどのように連携しているかイメージが掴みやすくなったでしょうか?