From 9093d25c1b424a8de058b56237b073aaec863d82 Mon Sep 17 00:00:00 2001 From: Christian Stewart Date: Thu, 14 Jul 2022 15:01:29 -0700 Subject: [PATCH] httpfs: implement http.FileSystem interfaces Implements the http.FileSystem interfaces. This allows using http.FileServer with a BillyFS. Signed-off-by: Christian Stewart --- httpfs/dir.go | 52 +++++++++++++++++++++++++++++++++++ httpfs/file.go | 40 +++++++++++++++++++++++++++ httpfs/filesystem.go | 58 +++++++++++++++++++++++++++++++++++++++ httpfs/filesystem_test.go | 50 +++++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+) create mode 100644 httpfs/dir.go create mode 100644 httpfs/file.go create mode 100644 httpfs/filesystem.go create mode 100644 httpfs/filesystem_test.go diff --git a/httpfs/dir.go b/httpfs/dir.go new file mode 100644 index 0000000..842704d --- /dev/null +++ b/httpfs/dir.go @@ -0,0 +1,52 @@ +package httpfs + +import ( + "errors" + "io/fs" + "net/http" +) + +// Dir implements the HTTP directory. +type Dir struct { + // fs is the base filesysetm + fs BillyFs + // path is the path to this dir + path string +} + +// NewDir constructs the Dir from a Billy Dir. +func NewDir(fs BillyFs, path string) *Dir { + return &Dir{fs: fs, path: path} +} + +func (f *Dir) Stat() (fs.FileInfo, error) { + return f.fs.Stat(f.path) +} + +// Readdir reads the directory contents. +func (f *Dir) Readdir(count int) ([]fs.FileInfo, error) { + ents, err := f.fs.ReadDir(f.path) + if err != nil { + return nil, err + } + if count > 0 && count > len(ents) { + ents = ents[:count] + } + return ents, err +} + +func (f *Dir) Read(p []byte) (n int, err error) { + return 0, errors.New("not a file") +} + +func (f *Dir) Seek(offset int64, whence int) (int64, error) { + return 0, errors.New("not a file") +} + +func (f *Dir) Close() error { + // no-op. + return nil +} + +// _ is a type assertion +var _ http.File = ((*Dir)(nil)) diff --git a/httpfs/file.go b/httpfs/file.go new file mode 100644 index 0000000..c85f381 --- /dev/null +++ b/httpfs/file.go @@ -0,0 +1,40 @@ +package httpfs + +import ( + "errors" + "io/fs" + "net/http" + + "github.com/go-git/go-billy/v5" +) + +// File implements the HTTP file. +type File struct { + // File is the billy file + billy.File + // path is the path to File + path string + // fs is the filesystem + fs BillyFs +} + +// NewFile constructs the File from a Billy File. +func NewFile(fs BillyFs, path string) (*File, error) { + f, err := fs.Open(path) + if err != nil { + return nil, err + } + return &File{File: f, path: path, fs: fs}, nil +} + +func (f *File) Readdir(count int) ([]fs.FileInfo, error) { + // ENOTDIR + return nil, errors.New("not a directory") +} + +func (f *File) Stat() (fs.FileInfo, error) { + return f.fs.Stat(f.path) +} + +// _ is a type assertion +var _ http.File = ((*File)(nil)) diff --git a/httpfs/filesystem.go b/httpfs/filesystem.go new file mode 100644 index 0000000..4a48b69 --- /dev/null +++ b/httpfs/filesystem.go @@ -0,0 +1,58 @@ +package httpfs + +import ( + "net/http" + "path" + "strings" + + "github.com/go-git/go-billy/v5" +) + +// BillyFs is the set of required billy filesystem interfaces. +type BillyFs interface { + billy.Basic + billy.Dir +} + +// FileSystem implements the HTTP filesystem. +type FileSystem struct { + // fs is the billy filesystem + fs BillyFs + // prefix is the filesystem prefix for HTTP + prefix string +} + +// NewFileSystem constructs the FileSystem from a Billy FileSystem. +// +// Prefix is a path prefix to prepend to file paths for HTTP. +// The prefix is trimmed from the paths when opening files. +func NewFileSystem(fs BillyFs, prefix string) *FileSystem { + if len(prefix) != 0 { + prefix = path.Clean(prefix) + } + return &FileSystem{fs: fs, prefix: prefix} +} + +// Open opens the file at the given path. +func (f *FileSystem) Open(name string) (http.File, error) { + name = path.Clean(name) + if len(f.prefix) != 0 { + name = strings.TrimPrefix(name, f.prefix) + name = path.Clean(name) + } + if strings.HasPrefix(name, "/") { + name = name[1:] + } + + fi, err := f.fs.Stat(name) + if err != nil { + return nil, err + } + if fi.IsDir() { + return NewDir(f.fs, name), nil + } + return NewFile(f.fs, name) +} + +// _ is a type assertion +var _ http.FileSystem = ((*FileSystem)(nil)) diff --git a/httpfs/filesystem_test.go b/httpfs/filesystem_test.go new file mode 100644 index 0000000..487fd79 --- /dev/null +++ b/httpfs/filesystem_test.go @@ -0,0 +1,50 @@ +package httpfs + +import ( + "bytes" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-git/go-billy/v5/memfs" + "github.com/go-git/go-billy/v5/util" +) + +// TestFileSystem tests the HTTP filesystem. +func TestFileSystem(t *testing.T) { + mfs := memfs.New() + + err := mfs.MkdirAll("./stuff", 0755) + if err != nil { + t.Fatal(err.Error()) + } + + data := []byte("hello world!\n") + err = util.WriteFile(mfs, "./stuff/test.txt", data, 0755) + if err != nil { + t.Fatal(err.Error()) + } + + var hfs http.FileSystem = NewFileSystem(mfs, "/test") + + mux := http.NewServeMux() + mux.Handle("/", http.FileServer(hfs)) + + req := httptest.NewRequest("GET", "/test/stuff/test.txt", nil) + rw := httptest.NewRecorder() + mux.ServeHTTP(rw, req) + + res := rw.Result() + if res.StatusCode != 200 { + t.Fatalf("status code: %d", res.StatusCode) + } + + readData, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatal(err.Error()) + } + if !bytes.Equal(readData, data) { + t.Fail() + } +}