Skip to content

Commit 9093d25

Browse files
committed
httpfs: implement http.FileSystem interfaces
Implements the http.FileSystem interfaces. This allows using http.FileServer with a BillyFS. Signed-off-by: Christian Stewart <[email protected]>
1 parent 7ab80d7 commit 9093d25

File tree

4 files changed

+200
-0
lines changed

4 files changed

+200
-0
lines changed

httpfs/dir.go

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package httpfs
2+
3+
import (
4+
"errors"
5+
"io/fs"
6+
"net/http"
7+
)
8+
9+
// Dir implements the HTTP directory.
10+
type Dir struct {
11+
// fs is the base filesysetm
12+
fs BillyFs
13+
// path is the path to this dir
14+
path string
15+
}
16+
17+
// NewDir constructs the Dir from a Billy Dir.
18+
func NewDir(fs BillyFs, path string) *Dir {
19+
return &Dir{fs: fs, path: path}
20+
}
21+
22+
func (f *Dir) Stat() (fs.FileInfo, error) {
23+
return f.fs.Stat(f.path)
24+
}
25+
26+
// Readdir reads the directory contents.
27+
func (f *Dir) Readdir(count int) ([]fs.FileInfo, error) {
28+
ents, err := f.fs.ReadDir(f.path)
29+
if err != nil {
30+
return nil, err
31+
}
32+
if count > 0 && count > len(ents) {
33+
ents = ents[:count]
34+
}
35+
return ents, err
36+
}
37+
38+
func (f *Dir) Read(p []byte) (n int, err error) {
39+
return 0, errors.New("not a file")
40+
}
41+
42+
func (f *Dir) Seek(offset int64, whence int) (int64, error) {
43+
return 0, errors.New("not a file")
44+
}
45+
46+
func (f *Dir) Close() error {
47+
// no-op.
48+
return nil
49+
}
50+
51+
// _ is a type assertion
52+
var _ http.File = ((*Dir)(nil))

httpfs/file.go

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package httpfs
2+
3+
import (
4+
"errors"
5+
"io/fs"
6+
"net/http"
7+
8+
"github.com/go-git/go-billy/v5"
9+
)
10+
11+
// File implements the HTTP file.
12+
type File struct {
13+
// File is the billy file
14+
billy.File
15+
// path is the path to File
16+
path string
17+
// fs is the filesystem
18+
fs BillyFs
19+
}
20+
21+
// NewFile constructs the File from a Billy File.
22+
func NewFile(fs BillyFs, path string) (*File, error) {
23+
f, err := fs.Open(path)
24+
if err != nil {
25+
return nil, err
26+
}
27+
return &File{File: f, path: path, fs: fs}, nil
28+
}
29+
30+
func (f *File) Readdir(count int) ([]fs.FileInfo, error) {
31+
// ENOTDIR
32+
return nil, errors.New("not a directory")
33+
}
34+
35+
func (f *File) Stat() (fs.FileInfo, error) {
36+
return f.fs.Stat(f.path)
37+
}
38+
39+
// _ is a type assertion
40+
var _ http.File = ((*File)(nil))

httpfs/filesystem.go

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package httpfs
2+
3+
import (
4+
"net/http"
5+
"path"
6+
"strings"
7+
8+
"github.com/go-git/go-billy/v5"
9+
)
10+
11+
// BillyFs is the set of required billy filesystem interfaces.
12+
type BillyFs interface {
13+
billy.Basic
14+
billy.Dir
15+
}
16+
17+
// FileSystem implements the HTTP filesystem.
18+
type FileSystem struct {
19+
// fs is the billy filesystem
20+
fs BillyFs
21+
// prefix is the filesystem prefix for HTTP
22+
prefix string
23+
}
24+
25+
// NewFileSystem constructs the FileSystem from a Billy FileSystem.
26+
//
27+
// Prefix is a path prefix to prepend to file paths for HTTP.
28+
// The prefix is trimmed from the paths when opening files.
29+
func NewFileSystem(fs BillyFs, prefix string) *FileSystem {
30+
if len(prefix) != 0 {
31+
prefix = path.Clean(prefix)
32+
}
33+
return &FileSystem{fs: fs, prefix: prefix}
34+
}
35+
36+
// Open opens the file at the given path.
37+
func (f *FileSystem) Open(name string) (http.File, error) {
38+
name = path.Clean(name)
39+
if len(f.prefix) != 0 {
40+
name = strings.TrimPrefix(name, f.prefix)
41+
name = path.Clean(name)
42+
}
43+
if strings.HasPrefix(name, "/") {
44+
name = name[1:]
45+
}
46+
47+
fi, err := f.fs.Stat(name)
48+
if err != nil {
49+
return nil, err
50+
}
51+
if fi.IsDir() {
52+
return NewDir(f.fs, name), nil
53+
}
54+
return NewFile(f.fs, name)
55+
}
56+
57+
// _ is a type assertion
58+
var _ http.FileSystem = ((*FileSystem)(nil))

httpfs/filesystem_test.go

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package httpfs
2+
3+
import (
4+
"bytes"
5+
"io/ioutil"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"github.com/go-git/go-billy/v5/memfs"
11+
"github.com/go-git/go-billy/v5/util"
12+
)
13+
14+
// TestFileSystem tests the HTTP filesystem.
15+
func TestFileSystem(t *testing.T) {
16+
mfs := memfs.New()
17+
18+
err := mfs.MkdirAll("./stuff", 0755)
19+
if err != nil {
20+
t.Fatal(err.Error())
21+
}
22+
23+
data := []byte("hello world!\n")
24+
err = util.WriteFile(mfs, "./stuff/test.txt", data, 0755)
25+
if err != nil {
26+
t.Fatal(err.Error())
27+
}
28+
29+
var hfs http.FileSystem = NewFileSystem(mfs, "/test")
30+
31+
mux := http.NewServeMux()
32+
mux.Handle("/", http.FileServer(hfs))
33+
34+
req := httptest.NewRequest("GET", "/test/stuff/test.txt", nil)
35+
rw := httptest.NewRecorder()
36+
mux.ServeHTTP(rw, req)
37+
38+
res := rw.Result()
39+
if res.StatusCode != 200 {
40+
t.Fatalf("status code: %d", res.StatusCode)
41+
}
42+
43+
readData, err := ioutil.ReadAll(res.Body)
44+
if err != nil {
45+
t.Fatal(err.Error())
46+
}
47+
if !bytes.Equal(readData, data) {
48+
t.Fail()
49+
}
50+
}

0 commit comments

Comments
 (0)