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:
parent
89af681694
commit
45f10e2f81
27 changed files with 1291 additions and 440 deletions
26
fs/dir.go
Normal file
26
fs/dir.go
Normal 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
23
fs/fs.go
Normal 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
151
fs/storage.go
Normal 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
129
fs/storage_test.go
Normal 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
77
fs/torrent.go
Normal 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
47
fs/torrent_test.go
Normal 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
141
fs/zip.go
Normal 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
66
fs/zip_test.go
Normal 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue