Skip to content

Commit

Permalink
Merge branch 'view'
Browse files Browse the repository at this point in the history
  • Loading branch information
maxkagamine committed Jun 14, 2024
2 parents 0488816 + 92a3393 commit 85924ad
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 17 deletions.
20 changes: 15 additions & 5 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,15 @@ $ open http://localhost:3939

[SQLiteアーカイブ](https://sqlite.org/sqlar.html)(通称:sqlar)とはファイルをBLOBとして保存するための標準テーブルスキーマ<sup>1</sup>のあるただのSQLiteデータベースである。そうする理由はメインページがいくつか説明する(私見では、主のはリレーショナルかインデックスされたデータとファイルが一緒に格納されて、同じORMが使えて、そしてファイルが外部に保存される場合に不可能の外部キー制約ができることだ<sup>2</sup>)でもsqlarフォーマットを特別に作ったテーブルの代わりに使用するメリットはsqlite3のCLIのtarみたいなオプション(そしてこれ)が使えることだ。

私がこれを作る動機は、大きいデータセットと付属オーディオファイルを持って、導入時にそれぞれElasticsearchとS3にプッシュするけどその時までどこかの中間の場所で保存しないとならなかった。私がすでにEntity Frameworkを使ってデータをsqliteファイルに書き込んでいて、最初はオーディオファイルをフォルダーに保存していたけど、ファイル名がSHA1ハッシュだと考えると、このようにアクセスできるようにするのは特に意味がなかった。逆に、Windowsファイルパスや存在しないファイル等を扱うことをかかえた。ファイルをデータベースに移動するのは主キーがS3のオブジェクト名と同じもので、そのテーブルを外部キーによって参照してデータ整合性を強化できるという意味した。それにラップトップとデスクトップを切り替えるために一つのファイルをコピーしていいから楽になった<sup>3</sup>。
> [!TIP]
> [ビュー](https://ja.wikipedia.org/wiki/%E3%83%93%E3%83%A5%E3%83%BC_(%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9))を使用して、既存のテーブルをsqlarスキーマに合わせることができる(現在sqlarserverのみサポートする):
> ```sql
> CREATE VIEW sqlar(rowid, name, mode, mtime, sz, data) AS
> SELECT rowid, filename, 33279, 0, length(content), content FROM files;
> ```
> サーバーはパフォーマンスのためにSQLiteのBlob APIを使うので、rowid(ビューに含まれた)と元になるテーブル名と列名を知る必要がある。実行時に`-e BlobTable=files -e BlobColumn=content`として渡す。
私がこれを作る動機は、大きいデータセットと付属オーディオファイルを持って、導入時にそれぞれElasticsearchとS3にプッシュするけどその時までどこかの中間の場所で保存しないとならなかった。最初はオーディオファイルをフォルダーに保存していたけど、ファイル名がSHA1ハッシュだと考えると、このようにアクセスできるようにするのは特に意味がなかった。逆に、Windowsファイルパスや存在しないファイル等を扱うことをかかえた。ファイルをデータベースに移動したため、主キーがS3のオブジェクト名と同じもので、そのテーブルを外部キーによって参照してデータ整合性を強化できるようになった。それにラップトップとデスクトップを切り替えるために一つのファイルをコピーしていいから楽になった<sup>3</sup>。
でも問題があった。開発環境ではローカルサーバーが本番のS3バケットではなくそのローカルのファイルに指してほしかった。別の開発バケットなんて不要だった。ディスク上のファイルだったら単純に静的ファイルとして提供できたけど、本番で要らないMicrosoft.Data.Sqliteへの依存関係を追加してDockerイメージを膨らましたくなかった。プリプロセッサ ディレクティブは醜い。どうしよう?BLOBを静的ファイルとして提供するだけの別のコンテイナーをComposeファイルに追加して開発でS3の代わりにしようか!そしてNginxやApacheみたいなディレクトリリスト機能も入れって、必要のないのにシンボリックリンク対応を実装しよう!それでそれで……FTPサーバーを!なんていいアイデアだ!けして全く別のプロジェクトに膨大してしまわないでしょう!
Expand All @@ -52,13 +60,15 @@ $ open http://localhost:3939
|環&#8288;境&#8288;変&#8288;数|デ&#8288;フ&#8288;ォ&#8288;ル&#8288;ト&#8288;値&nbsp;&nbsp;|説&#8288;明|
|---|---|---|
|`TZ`|UTC|変更時刻を表示するためのタイムゾーン ([_List of tz database time zones_](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)を参照<!-- English only -->)|
|`TZ`|UTC|変更時刻を表示するための[タイムゾーン](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)<!-- English only -->|
|`LANG`|en_US|数値等をフォーマットするためのロケール|
|`SizeFormat`|Binary|Bytes = ファイルサイズを書式なしでバイトで表示する<br />Binary = バイナリ単位を使う(KiB、MiB、GiB、TiB)<br />SI = SI単位を使う(KB、MB、GB、TB)|
|`DirectoriesFirst`|true|ディレクトリをファイルの前にグループする|
|`CaseInsensitive`|false|大文字小文字を区別しないファイルシステムとして扱う|
|`StaticSite`|false|ディレクトリリストを無効して、存在するとindex.htmlを提供して、見つけられない場合/404.htmlを提供する|
|`Charset`|utf-8|ファイルストリームのContent-Typeヘッダーの文字コードを設定する。無効にするには空の文字列に設定して。|
|`BlobTable`|sqlar|BLOBを含むテーブル名<br />※ sqlarテーブルとしてビューを使用することに関する上記のTipを参照してください|
|`BlobColumn`|data|BLOBを含む列名<br />※ sqlarテーブルとしてビューを使用することに関する上記のTipを参照してください|
|`EnableFtp`|false|FTPサーバーを起動する|
|`FtpPasvPorts`|10000-10009|受動モードのためのポート範囲。ホストとコンテイナーのポートは一致する必要がある。ポート数が大きい場合はDockerが遅くなる可能性があるので、広い範囲は推奨しない。|
|`FtpPasvAddress`|127.0.0.1|FTPサーバーの外部IPアドレス|
Expand All @@ -75,7 +85,7 @@ $ open http://localhost:3939
<sup>rin.avifイラストは<a href="https://twitter.com/Noartnolife1227/status/1531168810098917376">としたのあ (@Noartnolife1227)</a>による</sup>
Junkerの[C#のFTPサーバー](https://github.com/FubarDevelopment/FtpServer/)がファイルシステムの抽象化のインタフェースがあるのお陰で、私はバックエンドとしてsqlarserverの内部用のファイルツリーを使う実装を作成できた。これでSQLiteアーカイブの内容をFTP経由で参照できる!
FubarDevの[C#のFTPサーバー](https://github.com/FubarDevelopment/FtpServer/)がファイルシステムの抽象化のインタフェースがあるのお陰で、私はバックエンドとしてsqlarserverの内部用のファイルツリーを使う実装を作成できた。これでSQLiteアーカイブの内容をFTP経由で参照できる!
![だがなぜ](.github/images/but%20why.avif)
Expand All @@ -95,7 +105,7 @@ Junkerの[C#のFTPサーバー](https://github.com/FubarDevelopment/FtpServer/)
ポート21は何でもにマッピングできるけど、FTPプロトコルの一部はサーバーがデータ転送のためにどのIPとポートに接続すべきだとクライアントに伝えることなので、PASV<sup>1</sup>のポート範囲の10000-10009はホストとコンテイナーが一致する必要がある。`FtpPasvPorts`の設定で変更できる。もしサーバーがlocalhostで実行してなければ`FtpPasvAddress`をFTPクライアントに入力すると同じIPアドレスに設定する必要がある。

> <sup>1</sup> PASVとはFTPの受動モード(英:passive mode)に指して、サーバーがデータ転送のためにポートを開けてクライアントに接続するように指し示すことだ。(ポート21はコントロールで、コマンド通信のためでけに使われる。)その反対はアクティブモードで、ファイアウォールの前の時代🦕からだしサーバーが直接にクライアントへの接続を確立することだった。
> <sup>1</sup> PASVとはFTPの受動モード(英:passive mode)に指して、サーバーがデータ転送のためにポートを開けてクライアントに接続するように指し示すことだ。(ポート21はコントロールで、コマンド通信のためだけに使われる。)その反対はアクティブモードで、ファイアウォールの前の時代🦕からだしサーバーが直接にクライアントへの接続を確立することだった。
> [!WARNING]
> Dockerはデフォルトで公開されたポートを0.0.0.0にバインドして外部からアクセスできるようにファイアウォール規則を作るのだ。信頼できないネットワークでは、またはマシンがインターネットに開けてる場合は、明示的にlocalhostにバインドする(例えば、`-p 127.0.0.1:21:21`)か[デフォルトのバインド・アドレスを変更](https://docs.docker.com/network/packet-filtering-firewalls/#setting-the-default-bind-address-for-containers)しないとならない。(DockerのLinux版を使用しているWSLユーザーはこれを気にする必要はないはずだ。)
Expand All @@ -119,7 +129,7 @@ $ docker run -it --rm -v .:/srv -p 3939:80 -e StaticSite=true ghcr.io/maxkagamin
>
> <sup>2</sup> 中身を覗いてみて物事を実行しているDockerコンテイナーを見ない限りよね。でもインフラを無視するこそがサーバーレスじゃない?
>
> <sup>3</sup> 2をご参照。でもこれを[AOTコンパイル](https://learn.microsoft.com/ja-jp/aspnet/core/fundamentals/native-aot?view=aspnetcore-8.0)できるなら、SQLiteデータベースを実行可能に埋め込む方法があると思う…🤔
> <sup>3</sup> 2をご参照。でもこれを[AOTコンパイル](https://learn.microsoft.com/ja-jp/aspnet/core/fundamentals/native-aot?view=aspnetcore-8.0)できるなら、SQLiteデータベースを実行ファイルに埋め込む方法があると思う…🤔
## ライセンス

Expand Down
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,15 @@ $ open http://localhost:3939

An [SQLite Archive](https://sqlite.org/sqlar.html), or _sqlar_ for short (probably pronounced /ˈɛs kjuː&#8202;ˈlɑːr/, but I sometimes pronounce it /sklɑːr/ because "sqlarball" sounds funnier that way), is simply an SQLite database that uses a standard table schema¹ for storing files as blobs. The main page goes over some reasons for doing this (the big ones IMO being that your relational/indexed data and files are kept together, use the same ORM, and can have foreign key constraints that simply aren't possible when files are stored externally²), but the advantage of using the sqlar format over an ad hoc table is that it enables use of the sqlite3 CLI's tar-like options &mdash; and now this as well.

My motivation for making this was I had a large dataset and accompanying audio files, where upon deployment these would be pushed to Elasticsearch and S3 respectively, but until then needed to be stored in some intermediate location. I was already using Entity Framework and dumping the data into an sqlite file; at first, I was saving the audio files to a folder, but given that their filenames were SHA1 hashes, there wasn't really any meaning in having them accessible in this way. Rather, it burdened me with having to deal with Windows file paths, potential for missing files, and so on. Moving them into the sqlite file itself meant that the primary key was literally the S3 object name, and I could reference this table via a foreign key to enforce data integrity. Switching between my laptop and desktop is easier, too, as I can just copy the file over.³
> [!TIP]
> You can use a [view](https://en.wikipedia.org/wiki/View_(SQL)) to adapt an existing table to the sqlar schema (only sqlarserver supports this currently):
> ```sql
> CREATE VIEW sqlar(rowid, name, mode, mtime, sz, data) AS
> SELECT rowid, filename, 33279, 0, length(content), content FROM files;
> ```
> The server uses SQLite's blob API for performance, so it needs to know the rowid (which we've included in the view) and the name of the underlying table and its blob column, which should be passed when running the server as `-e BlobTable=files -e BlobColumn=content`.
My motivation for making this was I had a large dataset and accompanying audio files, where upon deployment these would be pushed to Elasticsearch and S3 respectively, but until then needed to be stored in some intermediate location. At first, I was saving the audio files to a folder, but given that their filenames were SHA1 hashes, there wasn't really any meaning in having them accessible in this way. Rather, it burdened me with having to deal with Windows file paths, potential for missing files, and so on. Moving them into the sqlite file itself meant that the primary key was literally the S3 object name, and I could reference this table via a foreign key to enforce data integrity. Switching between my laptop and desktop is easier, too, as I can just copy the file over.³
But this provided a challenge: for development, I wanted my local server to point to the local audio files, not the production S3 bucket. A separate dev bucket would be overkill. Had these been files on disk, I could simply serve them as static files, but I didn't want to add Microsoft.Data.Sqlite as a dependency and inflate the docker image when it wouldn't need that in production. Preprocessor directives are ugly. What to do? Why not add a separate container to the compose file that just serves the blobs as static files, and use that as a stand-in for S3 in dev! And then, let's add directory listing, like Nginx or Apache! And symlink support, too, even though I wouldn't need it! And, and... _an FTP server!_ What a great idea! Surely this won't balloon into a whole project!
Expand All @@ -52,13 +60,15 @@ The following options can be passed as environment variables:
|Environment&nbsp;variable|Default&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|Description|
|---|---|---|
|`TZ`|UTC|Timezone for displaying date modified (see [_List of tz database time zones_](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones))|
|`TZ`|UTC|[Timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for displaying date modified|
|`LANG`|en_US|Locale used for formatting numbers etc.|
|`SizeFormat`|Binary|Bytes = Display file sizes in bytes without formatting<br />Binary = Use binary units (KiB, MiB, GiB, TiB)<br />SI = Use SI units (KB, MB, GB, TB)|
|`DirectoriesFirst`|true|Group directories before files|
|`CaseInsensitive`|false|Treat the archive as a case-insensitive filesystem|
|`StaticSite`|false|Disable directory listing and serve index.html files where present and /404.html when not found|
|`Charset`|utf-8|Sets the charset in the Content-Type header of file streams. Empty string to disable.|
|`BlobTable`|sqlar|Name of the table holding the blob<br />_See the tip above on using a view for the sqlar table_|
|`BlobColumn`|data|Name of the column holding the blob<br />_See the tip above on using a view for the sqlar table_|
|`EnableFtp`|false|Start the FTP server|
|`FtpPasvPorts`|10000-10009|Port range used for passive mode. Host and container ports must match. Avoid too large a range, as many ports can make docker slow.|
|`FtpPasvAddress`|127.0.0.1|The FTP server's external IP address|
Expand All @@ -75,7 +85,7 @@ The following options can be passed as environment variables:
<sup>rin.avif artwork by <a href="https://twitter.com/Noartnolife1227/status/1531168810098917376">としたのあ (@Noartnolife1227)</a></sup>
Thanks to Junker's [C# FTP server](https://github.com/FubarDevelopment/FtpServer/) having an abstracted file system interface, I was able to write an implementation that uses sqlarserver's internal file node tree as a backend. Now you can browse the contents of an SQLite Archive via FTP!
Thanks to FubarDev's [C# FTP server](https://github.com/FubarDevelopment/FtpServer/) having an abstracted file system interface, I was able to write an implementation that uses sqlarserver's internal file node tree as a backend. Now you can browse the contents of an SQLite Archive via FTP!
![But why?](.github/images/but%20why.avif)
Expand Down
2 changes: 2 additions & 0 deletions SqliteArchive.Server/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"CaseInsensitive": false,
"StaticSite": false,
"Charset": "utf-8",
"BlobTable": "sqlar",
"BlobColumn": "data",
"EnableFtp": false,
"FtpPasvPorts": "10000-10009",
"FtpPasvAddress": "127.0.0.1"
Expand Down
Loading

0 comments on commit 85924ad

Please sign in to comment.