0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Goのファイルシステム(io/fs)を実装してみる

Posted at

はじめに

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を受け付けるものが多くあるため(場合によっては追加のインターフェイスの実装を要求するものもありますが)、独自のツリー構造を持つアプリケーションではファイルシステムとして実装することでそれらの恩恵をうけることができそうです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?