diff --git a/README.md b/README.md index 30226c1..783305f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ For support and discussion, see [![Gitter chat](https://badges.gitter.im/desync- Among the distinguishing factors: -- Supported on MacOS, though there could be incompatibilities when exchanging catar-files between Linux and Mac for example since devices and filemodes differ slightly. \*BSD should work as well but hasn't been tested. Windows supports a subset of commands. +- Supported on MacOS, though there could be incompatibilities when exchanging catar-files between Linux, Mac and Windows for example since devices and filemodes differ slightly. \*BSD should work as well but hasn't been tested. - Where the upstream command has chosen to optimize for storage efficiency (f/e, being able to use local files as "seeds", building temporary indexes into them), this command chooses to optimize for runtime performance (maintaining a local explicit chunk store, avoiding the need to reindex) at cost to storage efficiency. - Where the upstream command has chosen to take full advantage of Linux platform features, this client chooses to implement a minimum featureset and, while high-value platform-specific features (such as support for btrfs reflinks into a decompressed local chunk cache) might be added in the future, the ability to build without them on other platforms will be maintained. - Both, SHA512/256 and SHA256 are supported hash functions. @@ -73,12 +73,21 @@ The tool is provided for convenience. It uses the desync library and makes most ### Installation -If GOPATH is set correctly, building the tool and installing it into `$GOPATH/bin` can be done with: +#### Linux ```text go get -u github.com/folbricht/desync/cmd/desync ``` +#### Windows + +Windows builds require a gcc compiler in PATH (for example [http://mingw-w64.org/](http://mingw-w64.org/)) and [WinFsp](https://github.com/billziss-gh/winfsp). + +```text +set CPATH=C:\Program Files (x86)\WinFsp\inc\fuse +go get -u github.com/folbricht/desync/cmd/desync +``` + ### Subcommands - `extract` - build a blob from an index file, optionally using seed indexes+blobs diff --git a/cmd/desync/mount-index.go b/cmd/desync/mount-index.go index d2f4e61..67345a6 100644 --- a/cmd/desync/mount-index.go +++ b/cmd/desync/mount-index.go @@ -1,5 +1,3 @@ -// +build !windows - package main import ( diff --git a/cmd/desync/mount-index_windows.go b/cmd/desync/mount-index_windows.go deleted file mode 100644 index 6e9336d..0000000 --- a/cmd/desync/mount-index_windows.go +++ /dev/null @@ -1,18 +0,0 @@ -package main - -import ( - "context" - "errors" - - "github.com/spf13/cobra" -) - -func newMountIndexCommand(ctx context.Context) *cobra.Command { - return &cobra.Command{ - Hidden: true, - RunE: func(cmd *cobra.Command, args []string) error { - return errors.New("command not available on this platform") - }, - SilenceUsage: true, - } -} diff --git a/go.mod b/go.mod index 5db154c..854ffe3 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/folbricht/desync go 1.11 require ( + github.com/billziss-gh/cgofuse v1.2.0 github.com/datadog/zstd v1.4.4 github.com/davecgh/go-spew v1.1.1 // indirect github.com/dchest/siphash v1.2.0 diff --git a/go.sum b/go.sum index 6dea1f6..4620f90 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/billziss-gh/cgofuse v1.2.0 h1:FMdQSygSBpD4yEPENJcmvfCdmNWMVkPLlD7wWdl/7IA= +github.com/billziss-gh/cgofuse v1.2.0/go.mod h1:LJjoaUojlVjgo5GQoEJTcJNqZJeRU0nCR84CyxKt2YM= github.com/datadog/zstd v1.4.4 h1:VXuvuDMxP8fBcMt66fD2Z4LSC+VmNIhm59jslWe9J5o= github.com/datadog/zstd v1.4.4/go.mod h1:inRp+etsHuvVqMPNTXaFlpf/Tj7wqviBtdJoPVrPEFQ= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/mount-index_windows.go b/mount-index_windows.go new file mode 100644 index 0000000..40c33c6 --- /dev/null +++ b/mount-index_windows.go @@ -0,0 +1,132 @@ +package desync + +import ( + "context" + "fmt" + "io" + "os" + "sync" + + "github.com/billziss-gh/cgofuse/fuse" +) + +// IndexMountFS is used to FUSE mount an index file (as a blob, not an archive). +// It present a single file underneath the mountpoint. +type IndexMountFS struct { + fuse.FileSystemBase + + FName string // File name in the mountpoint + Idx Index // Index of the blob + Store Store + + mu sync.Mutex + handles map[uint64]*indexFileHandle + handleCounter uint64 +} + +// NewIndexMountFS initializes a FUSE filesystem mount based on an index and a chunk store. +func NewIndexMountFS(idx Index, name string, s Store) *IndexMountFS { + return &IndexMountFS{ + FName: name, + Idx: idx, + Store: s, + handles: make(map[uint64]*indexFileHandle), + } +} + +func (fs *IndexMountFS) Open(path string, flags int) (errc int, fh uint64) { + if path != "/"+fs.FName { + return -fuse.ENOENT, ^uint64(0) + } + fs.mu.Lock() + defer fs.mu.Unlock() + fs.handleCounter++ + fs.handles[fs.handleCounter] = newIndexFileHandle(fs.Idx, fs.Store) + return 0, fs.handleCounter +} + +func (fs *IndexMountFS) Release(path string, fh uint64) int { + fs.mu.Lock() + defer fs.mu.Unlock() + _, ok := fs.handles[fh] + if !ok { + return -fuse.ENOSYS + } + delete(fs.handles, fh) + return 0 +} + +func (fs *IndexMountFS) Getattr(path string, stat *fuse.Stat_t, fh uint64) (errc int) { + switch path { + case "/": + stat.Mode = fuse.S_IFDIR | 0555 + return 0 + case "/" + fs.FName: + stat.Mode = fuse.S_IFREG | 0444 + stat.Size = fs.Idx.Length() + return 0 + default: + return -fuse.ENOENT + } +} + +func (fs *IndexMountFS) Read(path string, b []byte, offset int64, fh uint64) (n int) { + fs.mu.Lock() + f, ok := fs.handles[fh] + fs.mu.Unlock() + if !ok { + return 0 // apparently this means error??? The method has no dedicated error + } + n, err := f.read(b, offset) + if err != nil { // don't ignore the error + fmt.Fprintf(os.Stderr, "error reading: %v", err) + } + return n +} + +func (fs *IndexMountFS) Readdir(path string, fill func(name string, stat *fuse.Stat_t, ofst int64) bool, offset int64, fh uint64) (errc int) { + fill(".", nil, 0) + fill("..", nil, 0) + fill(fs.FName, nil, 0) + return 0 +} + +// indexFileHandle represents a (read-only) file handle on a blob in a FUSE mounted filesystem +type indexFileHandle struct { + r *IndexPos + + // perhaps not needed, but in case something is trying to use the same filehandle concurrently + mu sync.Mutex +} + +// NewIndexMountFile initializes a blob file opened in a FUSE mount. +func newIndexFileHandle(idx Index, s Store) *indexFileHandle { + return &indexFileHandle{ + r: NewIndexReadSeeker(idx, s), + } +} + +// read from a blob file in a FUSE mount. +func (f *indexFileHandle) read(dest []byte, off int64) (int, error) { + f.mu.Lock() + defer f.mu.Unlock() + if _, err := f.r.Seek(off, io.SeekStart); err != nil { + fmt.Fprintln(os.Stderr, err) + return 0, err + } + n, err := f.r.Read(dest) + if err != nil && err != io.EOF { + fmt.Fprintln(os.Stderr, err) + return 0, err + } + return n, nil +} + +// MountIndex mounts an index file under a FUSE mount point. The mount will only expose a single +// blob file as represented by the index. +func MountIndex(ctx context.Context, idx Index, path, name string, s Store, n int) error { + ifs := NewIndexMountFS(idx, name, s) + host := fuse.NewFileSystemHost(ifs) + host.Mount(path, nil) + return nil +}