Skip to content

Commit

Permalink
httpfs: implement http.FileSystem interfaces
Browse files Browse the repository at this point in the history
Implements the http.FileSystem interfaces.

This allows using http.FileServer with a BillyFS.

Signed-off-by: Christian Stewart <christian@paral.in>
  • Loading branch information
paralin committed Jul 14, 2022
1 parent 7ab80d7 commit 9093d25
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 0 deletions.
52 changes: 52 additions & 0 deletions httpfs/dir.go
Original file line number Diff line number Diff line change
@@ -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))
40 changes: 40 additions & 0 deletions httpfs/file.go
Original file line number Diff line number Diff line change
@@ -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))
58 changes: 58 additions & 0 deletions httpfs/filesystem.go
Original file line number Diff line number Diff line change
@@ -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))
50 changes: 50 additions & 0 deletions httpfs/filesystem_test.go
Original file line number Diff line number Diff line change
@@ -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()
}
}

0 comments on commit 9093d25

Please sign in to comment.