はじめに
Goのio/fs
パッケージはファイルシステムを抽象化してプログラム内で扱えるようにするためのインターフェイスを提供するもので、Go 1.16から利用可能になりました。
今回はこのパッケージの理解のために、あらかじめ用意した固定内容を返却する仮想的なファイルシステムを実装してみます。
ゴール
下記の内容を読み取ることができるようにfs.FS
を実装します。
ファイルパス | 内容 |
---|---|
foo |
(ディレクトリ) |
foo/bar |
hello |
foo/baz |
world |
foo/qux |
(ディレクトリ) |
foo/qux/quux |
hello world |
また、実装したファイルシステムが期待通りに動作していることを確認するためにhttp.FileServer
を利用してブラウザ上でファイルの内容を確認できるようにします。
実装すべきインターフェイス
下記はGoのファイルシステムを実装する上で必須となるものです。
-
fs.FS
- これを実装することが最終的なゴールとなります。
-
fs.File
-
fs.FS
を実装する上で必要となります。 - ファイルの内容を担当します。
-
-
fs.FileInfo
-
fs.File
を実装する上で必要となります。 - ファイルのメタデータを担当します。
-
下記は実装するファイルシステムの内容(ディレクトリを扱わない等)によっては実装が不要となる場合もありますが、今回はディレクトリを扱うため実装する必要があります。
-
fs.ReadDirFile
-
fs.File
を拡張するインターフェイスです
-
-
fs.DirEntry
-
fs.ReadDirFile
を実装する上で必要となります。
-
実装
fs.FileInfo
およびfs.DirEntry
の実装
Goのファイルシステムを実装するにあたり一番末端に位置するインターフェイスです。
これらは実装すべき内容が非常に似通っているため、今回は同じ構造体に両者を実装するようにします。
また、今回は手軽に実装するため、ファイルの内容や種別(ディレクトリかどうか)をすべてここに持たせるようにします。
type myFileInfo struct {
path string
content []byte
isDir bool
}
var _ fs.FileInfo = (*myFileInfo)(nil)
// 以下は fs.FileInfo で必要な関数
func (f *myFileInfo) Name() string {
return filepath.Base(f.path)
}
func (f *myFileInfo) Size() int64 {
if f.IsDir() {
return 0
} else {
return int64(len(f.content))
}
}
func (f *myFileInfo) Mode() fs.FileMode {
if f.IsDir() {
return fs.ModeDir | 0755
}
return 0644
}
func (f *myFileInfo) ModTime() time.Time {
return time.Now()
}
func (f *myFileInfo) IsDir() bool {
return f.isDir
}
func (f *myFileInfo) Sys() any {
return nil
}
var _ fs.DirEntry = (*myFileInfo)(nil)
// 以下は fs.DirEntry で必要な関数
func (f *myFileInfo) Info() (fs.FileInfo, error) {
return f, nil
}
func (f *myFileInfo) Type() fs.FileMode {
return f.Mode().Type()
}
ファイル名やサイズ、ディレクトリかどうか等を返却するようにします。
ディレクトリの場合はIsDir()
でtrue
を返すのみでなく、Mode()
にてfs.ModeDir
を立てておく必要があります(Type()
の返却値に影響します)。
ここでName()
はファイルパスではなく末端のファイル名を返却する必要があります。例えばファイルパスがfoo/qux/quux
である場合は返却値はquux
となります。
fs.File
およびfs.ReadDirFile
の実装
fs.File
はファイルの内容の読み出しを、fs.ReadDirFile
はディレクトリ内容の読み出しを担当します。
今回はmyFileInfo
にファイル内容をすべて持たせているため、自ファイルおよび自ディレクトリ配下のmyFileInfo
を持つようにします。
type myFile struct {
fileInfo *myFileInfo
children []*myFileInfo
offset int64
}
var _ fs.File = (*myFile)(nil)
// 以下は fs.File で必要な関数
func (f *myFile) Stat() (fs.FileInfo, error) {
return f.fileInfo, nil
}
func (f *myFile) Read(b []byte) (int, error) {
if f.fileInfo.IsDir() {
return 0, &fs.PathError{Op: "read", Path: f.fileInfo.path, Err: fs.ErrInvalid}
}
if f.offset >= int64(len(f.fileInfo.content)) {
return 0, io.EOF
}
n := copy(b, f.fileInfo.content[f.offset:])
f.offset += int64(n)
return n, nil
}
func (f *myFile) Close() error {
return nil
}
var _ fs.ReadDirFile = (*myFile)(nil)
// 以下は fs.ReadDirFile で必要な関数
func (f *myFile) ReadDir(count int) ([]fs.DirEntry, error) {
n := int64(len(f.children)) - f.offset
if n == 0 {
if count <= 0 {
return nil, nil
}
return nil, io.EOF
}
if count > 0 && n > int64(count) {
n = int64(count)
}
list := make([]fs.DirEntry, n)
for i := range list {
list[i] = f.children[f.offset+int64(i)]
}
f.offset += n
return list, nil
}
var _ io.Seeker = (*myFile)(nil)
// 以下は io.Seeker で必要な関数
func (f *myFile) Seek(offset int64, whence int) (int64, error) {
switch whence {
case io.SeekStart:
offset = 0
case io.SeekCurrent:
offset += f.offset
case io.SeekEnd:
offset += in
}
if offset < 0 || offset > int64(len(f.fileInfo.content)) {
return 0, &fs.PathError{Op: "seek", Path: f.fileInfo.path, Err: fs.ErrInvalid}
}
f.offset = offset
return offset, nil
}
Read()
はファイル内容の読み込みを行います。
ディレクトリの場合は読み込みを行えないのでエラーを返却させます。ファイルの内容を引数として渡された[]byte
に書き込み、その長さを返却します。書き込み先のキャパシティが足らず一度で読み込みを完了できない場合にはオフセットを保持して次回の呼び出し時に再開できるようにします。
ファイルの最後まで読み終えたらio.EOF
を返却します。
Stat()
では先ほど実装したmyFileInfo
を返却します。
ReadDir()
はディレクトリに含まれるファイルを返却します。
引数として一度に取得する件数が渡されるので、一度に全件を返却できない場合はオフセットを保持するようにします。すべてのファイルを返却し終えたらio.EOF
を返却します。
また、この例ではio.Seeker
を実装しています。これはGoのファイルシステムを実装する上では必須ではありませんが、http.FileServer
を利用する上で必要となります。
fs.FS
の実装
ここまでで必要なものが揃ったので、fs.FS
を実装します。今回の例ではファイルの情報を持ったmyFileInfo
をすべてここに持たせ、必要に応じてmyFile
を生成するようにします。
type myFS struct {
files []*myFileInfo
}
var _ fs.FS = (*myFS)(nil)
// 以下は fs.FS で必要な関数
func (fsys myFS) Open(name string) (fs.File, error) {
var found *myFileInfo
var children []*myFileInfo = []*myFileInfo{}
for _, fi := range fsys.files {
if fi.path == name {
found = fi
} else if strings.HasPrefix(fi.path, name+"/") && !strings.ContainsRune(strings.TrimPrefix(fi.path, name+"/"), '/') {
children = append(children, fi)
}
}
if found == nil {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
}
return &myFile{found, children, 0}, nil
}
Open()
に渡される引数はファイルパスです。
手持ちの情報からそのファイルパスが存在するものであるかどうかを確認し、ファイルとして存在するのではればfs.File
を、ディレクトリとして存在しているのであればfs.DirReadFile
を返却します。今回はどちらもmyFile
に実装しているため、区別なく返却しています。
これで、簡単なGoのファイルシステムの実装が完了しました。
実装したファイルシステムの利用
実装したmyFS
は下記のように利用できます。
func main() {
fsys := &myFS{
[]*myFileInfo{
{"foo", nil, true},
{"foo/bar", []byte("hello"), false},
{"foo/baz", []byte("world"), false},
{"foo/qux", nil, true},
{"foo/qux/quux", []byte("hello world"), false},
},
}
http.Handle("/", http.FileServer(http.FS(fsys)))
log.Fatal(http.ListenAndServe(":8080", http.FileServer(http.FS(fsys))))
}
上記を実行し、Webブラウザでhttp://localhost:8080/foo
にアクセスすると期待通りの動作をしていることを確認できます。
まとめ
fs.FS
の実装に必要なインターフェイスは下記のような依存の階層をしており、これらを実装することでGoのファイルシステムを作成できることを確認しました。
-
fs.FS
-
fs.File
fs.FileInfo
-
fs.ReadDirFile
fs.DirEntry
-
標準ライブラリやその他のライブラリにはfs.FS
を受け付けるものが多くあるため(場合によっては追加のインターフェイスの実装を要求するものもありますが)、独自のツリー構造を持つアプリケーションではファイルシステムとして実装することでそれらの恩恵をうけることができそうです。