Refactoring and first steps to make multi OS compatible.

- Using cgofuse to be compatible with multiple OSes
- Refactor to make possible better testing
- Add a bunch of tests
- Add code coverage

Signed-off-by: Antonio Navarro Perez <antnavper@gmail.com>
This commit is contained in:
Antonio Navarro Perez 2020-09-27 21:23:47 +02:00
parent 89af681694
commit 45f10e2f81
27 changed files with 1291 additions and 440 deletions

26
fs/dir.go Normal file
View file

@ -0,0 +1,26 @@
package fs
var _ File = &Dir{}
type Dir struct {
}
func (d *Dir) Size() int64 {
return 0
}
func (d *Dir) IsDir() bool {
return true
}
func (d *Dir) Close() error {
return nil
}
func (d *Dir) Read(p []byte) (n int, err error) {
return 0, nil
}
func (d *Dir) ReadAt(p []byte, off int64) (n int, err error) {
return 0, nil
}

23
fs/fs.go Normal file
View file

@ -0,0 +1,23 @@
package fs
import (
"github.com/ajnavarro/distribyted/iio"
)
type File interface {
IsDir() bool
Size() int64
iio.Reader
}
type Filesystem interface {
// Open opens the named file for reading. If successful, methods on the
// returned file can be used for reading; the associated file descriptor has
// mode O_RDONLY.
Open(filename string) (File, error)
// ReadDir reads the directory named by dirname and returns a list of
// directory entries.
ReadDir(path string) (map[string]File, error)
}

151
fs/storage.go Normal file
View file

@ -0,0 +1,151 @@
package fs
import (
"os"
"path"
"path/filepath"
"strings"
)
type FsFactory func(f File) (Filesystem, error)
var SupportedFactories = map[string]FsFactory{
".zip": func(f File) (Filesystem, error) {
return NewZip(f, f.Size()), nil
},
}
type storage struct {
factories map[string]FsFactory
files map[string]File
filesystems map[string]Filesystem
children map[string]map[string]File
}
func newStorage(factories map[string]FsFactory) *storage {
return &storage{
files: make(map[string]File, 0),
children: make(map[string]map[string]File, 0),
filesystems: make(map[string]Filesystem, 0),
factories: factories,
}
}
func (s *storage) Has(path string) bool {
path = clean(path)
f := s.files[path]
if f != nil {
return true
}
if f, _ := s.getFileFromFs(path); f != nil {
return true
}
return false
}
func (s *storage) Add(f File, p string) error {
p = clean(p)
if s.Has(p) {
if dir, err := s.Get(p); err == nil {
if !dir.IsDir() {
return os.ErrExist
}
}
return nil
}
ext := path.Ext(p)
if ffs := s.factories[ext]; ffs != nil {
fs, err := ffs(f)
if err != nil {
return err
}
s.filesystems[p] = fs
} else {
s.files[p] = f
}
s.createParent(p, f)
return nil
}
func (s *storage) createParent(path string, f File) error {
base, filename := filepath.Split(path)
base = clean(base)
if err := s.Add(&Dir{}, base); err != nil {
return err
}
if _, ok := s.children[base]; !ok {
s.children[base] = make(map[string]File, 0)
}
if filename != "" {
s.children[base][filename] = f
}
return nil
}
func (s *storage) Children(path string) map[string]File {
path = clean(path)
out, err := s.getDirFromFs(path)
if err == nil {
return out
}
l := make(map[string]File, 0)
for n, f := range s.children[path] {
l[n] = f
}
return l
}
func (s *storage) Get(path string) (File, error) {
path = clean(path)
if !s.Has(path) {
return nil, os.ErrNotExist
}
file, ok := s.files[path]
if ok {
return file, nil
}
return s.getFileFromFs(path)
}
func (s *storage) getFileFromFs(p string) (File, error) {
for fsp, fs := range s.filesystems {
if strings.HasPrefix(p, fsp) {
return fs.Open(string(os.PathSeparator) + strings.TrimPrefix(p, fsp))
}
}
return nil, os.ErrNotExist
}
func (s *storage) getDirFromFs(p string) (map[string]File, error) {
for fsp, fs := range s.filesystems {
if strings.HasPrefix(p, fsp) {
path := strings.TrimPrefix(p, fsp)
return fs.ReadDir(path)
}
}
return nil, os.ErrNotExist
}
func clean(path string) string {
return filepath.Clean(string(os.PathSeparator) + filepath.FromSlash(path))
}

129
fs/storage_test.go Normal file
View file

@ -0,0 +1,129 @@
package fs
import (
"os"
"testing"
"github.com/stretchr/testify/require"
)
var dummyFactories = map[string]FsFactory{
".test": func(f File) (Filesystem, error) {
return &DummyFs{}, nil
},
}
func TestStorage(t *testing.T) {
t.Parallel()
require := require.New(t)
s := newStorage(dummyFactories)
err := s.Add(&Dummy{}, "/path/to/dummy/file.txt")
require.NoError(err)
err = s.Add(&Dummy{}, "/path/to/dummy/file2.txt")
require.NoError(err)
contains := s.Has("/path")
require.True(contains)
contains = s.Has("/path/to/dummy/")
require.True(contains)
file, err := s.Get("/path/to/dummy/file.txt")
require.NoError(err)
require.Equal(&Dummy{}, file)
file, err = s.Get("/path/to/dummy/file3.txt")
require.Error(err)
require.Nil(file)
files := s.Children("/path/to/dummy/")
require.Len(files, 2)
require.Contains(files, "file.txt")
require.Contains(files, "file2.txt")
err = s.Add(&Dummy{}, "/path/to/dummy/folder/file.txt")
require.NoError(err)
files = s.Children("/path/to/dummy/")
require.Len(files, 3)
require.Contains(files, "file.txt")
require.Contains(files, "file2.txt")
require.Contains(files, "folder")
err = s.Add(&Dummy{}, "path/file4.txt")
require.NoError(err)
require.True(s.Has("/path/file4.txt"))
files = s.Children("/")
require.Len(files, 1)
err = s.Add(&Dummy{}, "/path/special_file.test")
require.NoError(err)
file, err = s.Get("/path/special_file.test/dir/here/file1.txt")
require.NoError(err)
require.Equal(&Dummy{}, file)
files = s.Children("/path/special_file.test")
require.Len(files, 0)
files = s.Children("/path/special_file.test/dir/here")
require.Len(files, 2)
err = s.Add(&Dummy{}, "/path/to/__special__path/file3.txt")
require.NoError(err)
file, err = s.Get("/path/to/__special__path/file3.txt")
require.NoError(err)
require.Equal(&Dummy{}, file)
}
var _ Filesystem = &DummyFs{}
type DummyFs struct {
}
func (d *DummyFs) Open(filename string) (File, error) {
return &Dummy{}, nil
}
func (d *DummyFs) ReadDir(path string) (map[string]File, error) {
if path == "/dir/here" {
return map[string]File{
"file1.txt": &Dummy{},
"file2.txt": &Dummy{},
}, nil
}
return nil, os.ErrNotExist
}
var _ File = &Dummy{}
type Dummy struct {
}
func (d *Dummy) Size() int64 {
return 0
}
func (d *Dummy) IsDir() bool {
return false
}
func (d *Dummy) Close() error {
return nil
}
func (d *Dummy) Read(p []byte) (n int, err error) {
return 0, nil
}
func (d *Dummy) ReadAt(p []byte, off int64) (n int, err error) {
return 0, nil
}

77
fs/torrent.go Normal file
View file

@ -0,0 +1,77 @@
package fs
import (
"github.com/ajnavarro/distribyted/iio"
"github.com/anacrolix/torrent"
)
var _ Filesystem = &Torrent{}
type Torrent struct {
t *torrent.Torrent
s *storage
}
func NewTorrent(t *torrent.Torrent) *Torrent {
return &Torrent{
t: t,
s: newStorage(SupportedFactories),
}
}
func (fs *Torrent) load() {
<-fs.t.GotInfo()
for _, file := range fs.t.Files() {
fs.s.Add(&torrentFile{readerFunc: file.NewReader, len: file.Length()}, file.Path())
}
}
func (fs *Torrent) Open(filename string) (File, error) {
fs.load()
return fs.s.Get(filename)
}
func (fs *Torrent) ReadDir(path string) (map[string]File, error) {
fs.load()
return fs.s.Children(path), nil
}
var _ File = &torrentFile{}
type torrentFile struct {
readerFunc func() torrent.Reader
reader iio.Reader
len int64
}
func (d *torrentFile) load() {
if d.reader != nil {
return
}
d.reader = iio.NewReadAtWrapper(d.readerFunc())
}
func (d *torrentFile) Size() int64 {
return d.len
}
func (d *torrentFile) IsDir() bool {
return false
}
func (d *torrentFile) Close() error {
err := d.reader.Close()
d.reader = nil
return err
}
func (d *torrentFile) Read(p []byte) (n int, err error) {
d.load()
return d.reader.Read(p)
}
func (d *torrentFile) ReadAt(p []byte, off int64) (n int, err error) {
d.load()
return d.reader.ReadAt(p, off)
}

47
fs/torrent_test.go Normal file
View file

@ -0,0 +1,47 @@
package fs
import (
"os"
"testing"
"github.com/anacrolix/torrent"
"github.com/stretchr/testify/require"
)
const testMagnet = "magnet:?xt=urn:btih:a88fda5954e89178c372716a6a78b8180ed4dad3&dn=The+WIRED+CD+-+Rip.+Sample.+Mash.+Share&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fwired-cd.torrent"
func TestTorrentFilesystem(t *testing.T) {
t.Parallel()
require := require.New(t)
cfg := torrent.NewDefaultClientConfig()
cfg.DataDir = os.TempDir()
client, err := torrent.NewClient(cfg)
require.NoError(err)
torrent, err := client.AddMagnet(testMagnet)
require.NoError(err)
tfs := NewTorrent(torrent)
files, err := tfs.ReadDir("/")
require.NoError(err)
require.Len(files, 1)
require.Contains(files, "The WIRED CD - Rip. Sample. Mash. Share")
files, err = tfs.ReadDir("/The WIRED CD - Rip. Sample. Mash. Share")
require.NoError(err)
require.Len(files, 18)
f, err := tfs.Open("/The WIRED CD - Rip. Sample. Mash. Share/not_existing_file.txt")
require.Equal(os.ErrNotExist, err)
require.Nil(f)
f, err = tfs.Open("/The WIRED CD - Rip. Sample. Mash. Share/01 - Beastie Boys - Now Get Busy.mp3")
require.NoError(err)
require.NotNil(f)
require.Equal(f.Size(), int64(1964275))
}

141
fs/zip.go Normal file
View file

@ -0,0 +1,141 @@
package fs
import (
"archive/zip"
"os"
"github.com/ajnavarro/distribyted/iio"
)
var _ Filesystem = &Zip{}
type Zip struct {
r iio.Reader
s *storage
size int64
loaded bool
}
func NewZip(r iio.Reader, size int64) *Zip {
return &Zip{
r: r,
size: size,
s: newStorage(nil),
}
}
func (fs *Zip) load() error {
if fs.loaded {
return nil
}
zr, err := zip.NewReader(fs.r, fs.size)
if err != nil {
return err
}
for _, f := range zr.File {
f := f
if f.FileInfo().IsDir() {
continue
}
err := fs.s.Add(newZipFile(
func() (iio.Reader, error) {
zr, err := f.Open()
if err != nil {
return nil, err
}
return iio.NewDiskTeeReader(zr)
},
f.FileInfo().Size(),
), string(os.PathSeparator)+f.Name)
if err != nil {
return err
}
}
fs.loaded = true
return nil
}
func (fs *Zip) Open(filename string) (File, error) {
if err := fs.load(); err != nil {
return nil, err
}
return fs.s.Get(filename)
}
func (fs *Zip) ReadDir(path string) (map[string]File, error) {
if err := fs.load(); err != nil {
return nil, err
}
return fs.s.Children(path), nil
}
var _ File = &zipFile{}
func newZipFile(readerFunc func() (iio.Reader, error), len int64) *zipFile {
return &zipFile{
readerFunc: readerFunc,
len: len,
}
}
type zipFile struct {
readerFunc func() (iio.Reader, error)
reader iio.Reader
len int64
}
func (d *zipFile) load() error {
if d.reader != nil {
return nil
}
r, err := d.readerFunc()
if err != nil {
return err
}
d.reader = r
return nil
}
func (d *zipFile) Size() int64 {
return d.len
}
func (d *zipFile) IsDir() bool {
return false
}
func (d *zipFile) Close() (err error) {
if d.reader != nil {
err = d.reader.Close()
d.reader = nil
}
return
}
func (d *zipFile) Read(p []byte) (n int, err error) {
if err := d.load(); err != nil {
return 0, err
}
return d.reader.Read(p)
}
func (d *zipFile) ReadAt(p []byte, off int64) (n int, err error) {
if err := d.load(); err != nil {
return 0, err
}
return d.reader.ReadAt(p, off)
}

66
fs/zip_test.go Normal file
View file

@ -0,0 +1,66 @@
package fs
import (
"archive/zip"
"bytes"
"io"
"testing"
"github.com/ajnavarro/distribyted/iio"
"github.com/stretchr/testify/require"
)
var fileContent []byte = []byte("Hello World")
func TestZipFilesystem(t *testing.T) {
t.Parallel()
require := require.New(t)
zReader, len := createTestZip(require)
zfs := NewZip(zReader, len)
files, err := zfs.ReadDir("/path/to/test/file")
require.NoError(err)
require.Len(files, 1)
f := files["1.txt"]
require.NotNil(f)
out := make([]byte, 11)
n, err := f.Read(out)
require.Equal(io.EOF, err)
require.Equal(11, n)
require.Equal(fileContent, out)
}
func createTestZip(require *require.Assertions) (iio.Reader, int64) {
buf := bytes.NewBuffer([]byte{})
zWriter := zip.NewWriter(buf)
f1, err := zWriter.Create("path/to/test/file/1.txt")
require.NoError(err)
_, err = f1.Write(fileContent)
require.NoError(err)
err = zWriter.Close()
require.NoError(err)
return newCBR(buf.Bytes()), int64(buf.Len())
}
type closeableByteReader struct {
*bytes.Reader
}
func newCBR(b []byte) *closeableByteReader {
return &closeableByteReader{
Reader: bytes.NewReader(b),
}
}
func (*closeableByteReader) Close() error {
return nil
}