diff --git a/cmd/all/all.go b/cmd/all/all.go index 2057d2930..0c2d2e3b0 100644 --- a/cmd/all/all.go +++ b/cmd/all/all.go @@ -8,6 +8,7 @@ import ( _ "github.com/ncw/rclone/cmd/cat" _ "github.com/ncw/rclone/cmd/check" _ "github.com/ncw/rclone/cmd/cleanup" + _ "github.com/ncw/rclone/cmd/cmount" _ "github.com/ncw/rclone/cmd/config" _ "github.com/ncw/rclone/cmd/copy" _ "github.com/ncw/rclone/cmd/copyto" diff --git a/cmd/cmount/fs.go b/cmd/cmount/fs.go new file mode 100644 index 000000000..fb05f45c4 --- /dev/null +++ b/cmd/cmount/fs.go @@ -0,0 +1,547 @@ +// +build cgo +// +build linux darwin freebsd windows + +package cmount + +import ( + "os" + "path" + "sync" + "time" + + "github.com/billziss-gh/cgofuse/fuse" + "github.com/ncw/rclone/cmd/mountlib" + "github.com/ncw/rclone/fs" + "github.com/pkg/errors" +) + +const fhUnset = ^uint64(0) + +// FS represents the top level filing system +type FS struct { + fuse.FileSystemBase + FS *mountlib.FS + f fs.Fs + openDirs *OpenFiles + openFilesWr *OpenFiles + openFilesRd *OpenFiles + ready chan (struct{}) +} + +// NewFS makes a new FS +func NewFS(f fs.Fs) *FS { + fsys := &FS{ + FS: mountlib.NewFS(f), + f: f, + openDirs: NewOpenFiles(0x01), + openFilesWr: NewOpenFiles(0x02), + openFilesRd: NewOpenFiles(0x03), + ready: make(chan (struct{})), + } + if noSeek { + fsys.FS.NoSeek() + } + return fsys +} + +type OpenFiles struct { + mu sync.Mutex + mark uint8 + nodes []interface{} +} + +func NewOpenFiles(mark uint8) *OpenFiles { + return &OpenFiles{ + mark: mark, + } +} + +// Open a node returning a file handle +func (of *OpenFiles) Open(node interface{}) (fh uint64) { + of.mu.Lock() + defer of.mu.Unlock() + var i int + var oldNode interface{} + for i, oldNode = range of.nodes { + if oldNode == nil { + of.nodes[i] = node + goto found + } + } + of.nodes = append(of.nodes, node) + i = len(of.nodes) - 1 +found: + return uint64((i << 8) | int(of.mark)) +} + +// get the node for fh, call with the lock held +func (of *OpenFiles) get(fh uint64) (i int, node interface{}, errc int) { + receivedMark := uint8(fh) + if receivedMark != of.mark { + return i, nil, -fuse.EBADF + } + i64 := fh >> 8 + if i64 > uint64(len(of.nodes)) { + return i, nil, -fuse.EBADF + } + i = int(i64) + node = of.nodes[i] + if node == nil { + return i, nil, -fuse.EBADF + } + return i, node, 0 +} + +// Get the node for the file handle +func (of *OpenFiles) Get(fh uint64) (node interface{}, errc int) { + of.mu.Lock() + _, node, errc = of.get(fh) + of.mu.Unlock() + return +} + +// Close the node +func (of *OpenFiles) Close(fh uint64) (errc int) { + of.mu.Lock() + i, _, errc := of.get(fh) + if errc == 0 { + of.nodes[i] = nil + } + of.mu.Unlock() + return +} + +// lookup a Node given a path +func (fsys *FS) lookupNode(path string) (node mountlib.Node, errc int) { + node, err := fsys.FS.Lookup(path) + return node, translateError(err) +} + +// lookup a Dir given a path +func (fsys *FS) lookupDir(path string) (dir *mountlib.Dir, errc int) { + node, errc := fsys.lookupNode(path) + if errc != 0 { + return nil, errc + } + dir, ok := node.(*mountlib.Dir) + if !ok { + return nil, -fuse.ENOTDIR + } + return dir, 0 +} + +// lookup a parent Dir given a path returning the dir and the leaf +func (fsys *FS) lookupParentDir(filePath string) (leaf string, dir *mountlib.Dir, errc int) { + parentDir, leaf := path.Split(filePath) + dir, errc = fsys.lookupDir(parentDir) + return leaf, dir, errc +} + +// lookup a File given a path +func (fsys *FS) lookupFile(path string) (file *mountlib.File, errc int) { + node, errc := fsys.lookupNode(path) + if errc != 0 { + return nil, errc + } + file, ok := node.(*mountlib.File) + if !ok { + return nil, -fuse.EISDIR + } + return file, 0 +} + +// get a read or write file handle +func (fsys *FS) getReadOrWriteFh(fh uint64) (handle interface{}, errc int) { + handle, errc = fsys.openFilesRd.Get(fh) + if errc == 0 { + return + } + return fsys.openFilesWr.Get(fh) +} + +// get a node from the path or from the fh if not fhUnset +func (fsys *FS) getNode(path string, fh uint64) (node mountlib.Node, errc int) { + if fh == fhUnset { + node, errc = fsys.lookupNode(path) + } else { + var n interface{} + n, errc = fsys.getReadOrWriteFh(fh) + if errc == 0 { + if get, ok := n.(mountlib.Noder); ok { + node = get.Node() + } else { + fs.Errorf(path, "Bad node type %T", n) + errc = -fuse.EIO + } + } + } + return +} + +// stat fills up the stat block for Node +func (fsys *FS) stat(node mountlib.Node, stat *fuse.Stat_t) (errc int) { + var Size uint64 + var Blocks uint64 + var modTime time.Time + var Mode os.FileMode + switch x := node.(type) { + case *mountlib.Dir: + modTime = x.ModTime() + Mode = dirPerms | fuse.S_IFDIR + case *mountlib.File: + var err error + modTime, Size, Blocks, err = x.Attr(noModTime) + if err != nil { + return translateError(err) + } + Mode = filePerms | fuse.S_IFREG + } + //stat.Dev = 1 + stat.Ino = node.Inode() // FIXME do we need to set the inode number? + stat.Mode = uint32(Mode) + stat.Nlink = 1 + stat.Uid = uid + stat.Gid = gid + //stat.Rdev + stat.Size = int64(Size) + t := fuse.NewTimespec(modTime) + stat.Atim = t + stat.Mtim = t + stat.Ctim = t + stat.Blksize = 512 + stat.Blocks = int64(Blocks) + stat.Birthtim = t + // fs.Debugf(nil, "stat = %+v", *stat) + return 0 +} + +// Init is called after the filesystem is ready +func (fsys *FS) Init() { + close(fsys.ready) +} + +// Destroy() call when it is unmounted (note that depending on how the +// file system is terminated the file system may not receive the +// Destroy() call). +func (fsys *FS) Destroy() { +} + +// Getattr reads the attributes for path +func (fsys *FS) Getattr(path string, stat *fuse.Stat_t, fh uint64) (errc int) { + fs.Debugf(path, "Getattr(path=%q,fh=%d)", path, fh) + node, errc := fsys.getNode(path, fh) + if errc == 0 { + errc = fsys.stat(node, stat) + } + fs.Debugf(path, "Getattr returns %d", errc) + return +} + +// Opendir opens path as a directory +func (fsys *FS) Opendir(path string) (errc int, fh uint64) { + fs.Debugf(path, "Opendir()") + dir, errc := fsys.lookupDir(path) + if errc == 0 { + fh = fsys.openDirs.Open(dir) + } else { + fh = fhUnset + } + fs.Debugf(path, "Opendir returns errc=%d, fh=%d", errc, fh) + return +} + +// Readdir reads the directory at dirPath +func (fsys *FS) Readdir(dirPath string, + fill func(name string, stat *fuse.Stat_t, ofst int64) bool, + ofst int64, + fh uint64) (errc int) { + fs.Debugf(dirPath, "Readdir(ofst=%d,fh=%d)", ofst, fh) + + node, errc := fsys.openDirs.Get(fh) + if errc != 0 { + return errc + } + + dir, ok := node.(*mountlib.Dir) + if !ok { + return -fuse.ENOTDIR + } + + items, err := dir.ReadDirAll() + if err != nil { + return translateError(err) + } + + // Optionally, create a struct stat that describes the file as + // for getattr (but FUSE only looks at st_ino and the + // file-type bits of st_mode). + // + // FIXME If you call host.SetCapReaddirPlus() then WinFsp will + // use the full stat information - a Useful optimization on + // Windows. + // + // NB we are using the first mode for readdir: The readdir + // implementation ignores the offset parameter, and passes + // zero to the filler function's offset. The filler function + // will not return '1' (unless an error happens), so the whole + // directory is read in a single readdir operation. + fill(".", nil, 0) + fill("..", nil, 0) + for _, item := range items { + name := path.Base(item.Obj.Remote()) + fill(name, nil, 0) + } + return 0 +} + +// Releasedir finished reading the directory +func (fsys *FS) Releasedir(path string, fh uint64) (errc int) { + fs.Debugf(path, "Releasedir(fh=%d)", fh) + return fsys.openDirs.Close(fh) +} + +// Statfs reads overall stats on the filessystem +// FIXME doesn't seem to be ever called +func (fsys *FS) Statfs(path string, stat *fuse.Statfs_t) (errc int) { + fs.Debugf(path, "Statfs()") + const blockSize = 4096 + const fsBlocks = (1 << 50) / blockSize + stat.Blocks = fsBlocks // Total data blocks in file system. + stat.Bfree = fsBlocks // Free blocks in file system. + stat.Bavail = fsBlocks // Free blocks in file system if you're not root. + stat.Files = 1E9 // Total files in file system. + stat.Ffree = 1E9 // Free files in file system. + stat.Bsize = blockSize // Block size + stat.Namemax = 255 // Maximum file name length? + stat.Frsize = blockSize // Fragment size, smallest addressable data size in the file system. + return 0 +} + +func (fsys *FS) Open(path string, flags int) (errc int, fh uint64) { + file, errc := fsys.lookupFile(path) + if errc != 0 { + return errc, fhUnset + } + rdwrMode := flags & fuse.O_ACCMODE + var err error + var handle interface{} + switch { + case rdwrMode == fuse.O_RDONLY: + handle, err = file.OpenRead() + if err != nil { + return translateError(err), fhUnset + } + return 0, fsys.openFilesRd.Open(handle) + case rdwrMode == fuse.O_WRONLY || (rdwrMode == fuse.O_RDWR && (flags&fuse.O_TRUNC) != 0): + handle, err = file.OpenWrite() + if err != nil { + return translateError(err), fhUnset + } + return 0, fsys.openFilesWr.Open(handle) + case rdwrMode == fuse.O_RDWR: + fs.Errorf(path, "Can't open for Read and Write") + return -fuse.EPERM, fhUnset + } + fs.Errorf(path, "Can't figure out how to open with flags: 0x%X", flags) + return -fuse.EPERM, fhUnset +} + +// Create creates and opens a file. +func (fsys *FS) Create(filePath string, flags int, mode uint32) (errc int, fh uint64) { + leaf, parentDir, errc := fsys.lookupParentDir(filePath) + if errc != 0 { + return errc, fhUnset + } + _, handle, err := parentDir.Create(leaf) + if err != nil { + return translateError(err), fhUnset + } + return 0, fsys.openFilesWr.Open(handle) +} + +func (fsys *FS) Truncate(path string, size int64, fh uint64) (errc int) { + node, errc := fsys.getNode(path, fh) + if errc != 0 { + return errc + } + file, ok := node.(*mountlib.File) + if !ok { + return -fuse.EIO + } + // Read the size so far + _, currentSize, _, err := file.Attr(true) + if err != nil { + return translateError(err) + } + fs.Debugf(path, "truncate to %d, currentSize %d", size, currentSize) + if int64(currentSize) != size { + fs.Errorf(path, "Can't truncate files") + return -fuse.EPERM + } + return 0 +} + +func (fsys *FS) Read(path string, buff []byte, ofst int64, fh uint64) (n int) { + // FIXME detect seek + handle, errc := fsys.openFilesRd.Get(fh) + if errc != 0 { + return errc + } + rfh, ok := handle.(*mountlib.ReadFileHandle) + if !ok { + // Can only read from read file handle + return -fuse.EIO + } + data, err := rfh.Read(int64(len(buff)), ofst) + if err != nil { + return translateError(err) + } + n = copy(buff, data) + return n +} + +func (fsys *FS) Write(path string, buff []byte, ofst int64, fh uint64) (n int) { + // FIXME detect seek + handle, errc := fsys.openFilesWr.Get(fh) + if errc != 0 { + return errc + } + wfh, ok := handle.(*mountlib.WriteFileHandle) + if !ok { + // Can only write to write file handle + return -fuse.EIO + } + // FIXME made Write return int and Read take int since must fit in RAM + n64, err := wfh.Write(buff, ofst) + if err != nil { + return translateError(err) + } + return int(n64) +} + +func (fsys *FS) Flush(path string, fh uint64) (errc int) { + handle, errc := fsys.getReadOrWriteFh(fh) + if errc != 0 { + return errc + } + var err error + switch x := handle.(type) { + case *mountlib.ReadFileHandle: + err = x.Flush() + case *mountlib.WriteFileHandle: + err = x.Flush() + default: + return -fuse.EIO + } + return translateError(err) +} + +func (fsys *FS) Release(path string, fh uint64) (errc int) { + handle, errc := fsys.getReadOrWriteFh(fh) + if errc != 0 { + return errc + } + var err error + switch x := handle.(type) { + case *mountlib.ReadFileHandle: + err = x.Release() + case *mountlib.WriteFileHandle: + err = x.Release() + default: + return -fuse.EIO + } + return translateError(err) +} + +// Unlink removes a file. +func (fsys *FS) Unlink(filePath string) int { + fs.Debugf(filePath, "Unlink()") + leaf, parentDir, errc := fsys.lookupParentDir(filePath) + if errc != 0 { + return errc + } + return translateError(parentDir.Remove(leaf)) +} + +// Mkdir creates a directory. +func (fsys *FS) Mkdir(dirPath string, mode uint32) (errc int) { + fs.Debugf(dirPath, "Mkdir(0%o)", mode) + leaf, parentDir, errc := fsys.lookupParentDir(dirPath) + if errc != 0 { + return errc + } + _, err := parentDir.Mkdir(leaf) + return translateError(err) +} + +// Rmdir removes a directory +func (fsys *FS) Rmdir(dirPath string) int { + fs.Debugf(dirPath, "Rmdir()") + leaf, parentDir, errc := fsys.lookupParentDir(dirPath) + if errc != 0 { + return errc + } + return translateError(parentDir.Remove(leaf)) +} + +// Rename renames a file. +func (fsys *FS) Rename(oldPath string, newPath string) (errc int) { + oldLeaf, oldParentDir, errc := fsys.lookupParentDir(oldPath) + if errc != 0 { + return errc + } + newLeaf, newParentDir, errc := fsys.lookupParentDir(newPath) + if errc != 0 { + return errc + } + return translateError(oldParentDir.Rename(oldLeaf, newLeaf, newParentDir)) +} + +// Utimens changes the access and modification times of a file. +func (fsys *FS) Utimens(path string, tmsp []fuse.Timespec) (errc int) { + fs.Debugf(path, "Utimens %+v", tmsp) + node, errc := fsys.lookupNode(path) + if errc != 0 { + return errc + } + var t time.Time + if tmsp == nil || len(tmsp) < 2 { + t = time.Now() + } else { + t = tmsp[1].Time() + } + var err error + switch x := node.(type) { + case *mountlib.Dir: + err = x.SetModTime(t) + case *mountlib.File: + err = x.SetModTime(t) + } + return translateError(err) +} + +// Translate errors from mountlib +func translateError(err error) int { + if err == nil { + return 0 + } + cause := errors.Cause(err) + if mErr, ok := cause.(mountlib.Error); ok { + switch mErr { + case mountlib.OK: + return 0 + case mountlib.ENOENT: + return -fuse.ENOENT + case mountlib.ENOTEMPTY: + return -fuse.ENOTEMPTY + case mountlib.EEXIST: + return -fuse.EEXIST + case mountlib.ESPIPE: + return -fuse.ESPIPE + case mountlib.EBADF: + return -fuse.EBADF + } + } + fs.Errorf(nil, "IO error: %v", err) + return -fuse.EIO +} diff --git a/cmd/cmount/mount.go b/cmd/cmount/mount.go new file mode 100644 index 000000000..497d2d508 --- /dev/null +++ b/cmd/cmount/mount.go @@ -0,0 +1,270 @@ +// Package cmount implents a FUSE mounting system for rclone remotes. +// +// This uses the cgo based cgofuse library + +// +build cgo +// +build linux darwin freebsd windows + +package cmount + +import ( + "fmt" + "log" + "os" + "runtime" + "time" + + "github.com/billziss-gh/cgofuse/fuse" + "github.com/ncw/rclone/cmd" + "github.com/ncw/rclone/fs" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "golang.org/x/sys/unix" +) + +// Globals +var ( + noModTime = false + debugFUSE = false + noSeek = false + dirCacheTime = 5 * 60 * time.Second + // mount options + readOnly = false + allowNonEmpty = false + allowRoot = false + allowOther = false + defaultPermissions = false + writebackCache = false + maxReadAhead fs.SizeSuffix = 128 * 1024 + umask = 0 + uid = uint32(unix.Geteuid()) + gid = uint32(unix.Getegid()) + // foreground = false + // default permissions for directories - modified by umask in Mount + dirPerms = os.FileMode(0777) + filePerms = os.FileMode(0666) +) + +func init() { + umask = unix.Umask(0) // read the umask + unix.Umask(umask) // set it back to what it was + cmd.Root.AddCommand(commandDefintion) + commandDefintion.Flags().BoolVarP(&noModTime, "no-modtime", "", noModTime, "Don't read/write the modification time (can speed things up).") + commandDefintion.Flags().BoolVarP(&debugFUSE, "debug-fuse", "", debugFUSE, "Debug the FUSE internals - needs -v.") + commandDefintion.Flags().BoolVarP(&noSeek, "no-seek", "", noSeek, "Don't allow seeking in files.") + commandDefintion.Flags().DurationVarP(&dirCacheTime, "dir-cache-time", "", dirCacheTime, "Time to cache directory entries for.") + // mount options + commandDefintion.Flags().BoolVarP(&readOnly, "read-only", "", readOnly, "Mount read-only.") + commandDefintion.Flags().BoolVarP(&allowNonEmpty, "allow-non-empty", "", allowNonEmpty, "Allow mounting over a non-empty directory.") + commandDefintion.Flags().BoolVarP(&allowRoot, "allow-root", "", allowRoot, "Allow access to root user.") + commandDefintion.Flags().BoolVarP(&allowOther, "allow-other", "", allowOther, "Allow access to other users.") + commandDefintion.Flags().BoolVarP(&defaultPermissions, "default-permissions", "", defaultPermissions, "Makes kernel enforce access control based on the file mode.") + commandDefintion.Flags().BoolVarP(&writebackCache, "write-back-cache", "", writebackCache, "Makes kernel buffer writes before sending them to rclone. Without this, writethrough caching is used.") + commandDefintion.Flags().VarP(&maxReadAhead, "max-read-ahead", "", "The number of bytes that can be prefetched for sequential reads.") + commandDefintion.Flags().IntVarP(&umask, "umask", "", umask, "Override the permission bits set by the filesystem.") + commandDefintion.Flags().Uint32VarP(&uid, "uid", "", uid, "Override the uid field set by the filesystem.") + commandDefintion.Flags().Uint32VarP(&gid, "gid", "", gid, "Override the gid field set by the filesystem.") + //commandDefintion.Flags().BoolVarP(&foreground, "foreground", "", foreground, "Do not detach.") +} + +var commandDefintion = &cobra.Command{ + Use: "cmount remote:path /path/to/mountpoint", + Short: `Mount the remote as a mountpoint. **EXPERIMENTAL**`, + Long: ` +rclone mount allows Linux, FreeBSD and macOS to mount any of Rclone's +cloud storage systems as a file system with FUSE. + +This is **EXPERIMENTAL** - use with care. + +First set up your remote using ` + "`rclone config`" + `. Check it works with ` + "`rclone ls`" + ` etc. + +Start the mount like this + + rclone mount remote:path/to/files /path/to/local/mount + +When the program ends, either via Ctrl+C or receiving a SIGINT or SIGTERM signal, +the mount is automatically stopped. + +The umount operation can fail, for example when the mountpoint is busy. +When that happens, it is the user's responsibility to stop the mount manually with + + # Linux + fusermount -u /path/to/local/mount + # OS X + umount /path/to/local/mount + +### Limitations ### + +This can only write files seqentially, it can only seek when reading. +This means that many applications won't work with their files on an +rclone mount. + +The bucket based remotes (eg Swift, S3, Google Compute Storage, B2, +Hubic) won't work from the root - you will need to specify a bucket, +or a path within the bucket. So ` + "`swift:`" + ` won't work whereas +` + "`swift:bucket`" + ` will as will ` + "`swift:bucket/path`" + `. +None of these support the concept of directories, so empty +directories will have a tendency to disappear once they fall out of +the directory cache. + +Only supported on Linux, FreeBSD and OS X at the moment. + +### rclone mount vs rclone sync/copy ## + +File systems expect things to be 100% reliable, whereas cloud storage +systems are a long way from 100% reliable. The rclone sync/copy +commands cope with this with lots of retries. However rclone mount +can't use retries in the same way without making local copies of the +uploads. This might happen in the future, but for the moment rclone +mount won't do that, so will be less reliable than the rclone command. + +### Filters ### + +Note that all the rclone filters can be used to select a subset of the +files to be visible in the mount. + +### Bugs ### + + * All the remotes should work for read, but some may not for write + * those which need to know the size in advance won't - eg B2 + * maybe should pass in size as -1 to mean work it out + * Or put in an an upload cache to cache the files on disk first + +### TODO ### + + * Check hashes on upload/download +`, + Run: func(command *cobra.Command, args []string) { + cmd.CheckArgs(2, 2, command, args) + fdst := cmd.NewFsDst(args) + err := Mount(fdst, args[1]) + if err != nil { + log.Fatalf("Fatal error: %v", err) + } + }, +} + +// mountOptions configures the options from the command line flags +func mountOptions(device string, mountpoint string) (options []string) { + // Options + options = []string{ + "-o", "fsname=" + device, + "-o", "subtype=rclone", + "-o", fmt.Sprintf("max_readahead=%d", maxReadAhead), + } + if debugFUSE { + options = append(options, "-o", "debug") + } + + // OSX options + if runtime.GOOS == "darwin" { + options = append(options, "-o", "volname="+device) + options = append(options, "-o", "noappledouble") + options = append(options, "-o", "noapplexattr") + } + + if allowNonEmpty { + options = append(options, "-o", "nonempty") + } + if allowOther { + options = append(options, "-o", "allow_other") + } + if allowRoot { + options = append(options, "-o", "allow_root") + } + if defaultPermissions { + options = append(options, "-o", "default_permissions") + } + if readOnly { + options = append(options, "-o", "ro") + } + if writebackCache { + // FIXME? options = append(options, "-o", WritebackCache()) + } + return options +} + +// mount the file system +// +// The mount point will be ready when this returns. +// +// returns an error, and an error channel for the serve process to +// report an error when fusermount is called. +func mount(f fs.Fs, mountpoint string) (<-chan error, func() error, error) { + fs.Debugf(f, "Mounting on %q", mountpoint) + + // Check the mountpoint + fi, err := os.Stat(mountpoint) + if err != nil { + return nil, nil, errors.Wrap(err, "mountpoint") + } + if !fi.IsDir() { + return nil, nil, errors.New("mountpoint is not a directory") + } + + // Create underlying FS + fsys := NewFS(f) + host := fuse.NewFileSystemHost(fsys) + + // Create options + options := mountOptions(f.Name()+":"+f.Root(), mountpoint) + fs.Debugf(f, "Mounting with options: %q", options) + + // Serve the mount point in the background returning error to errChan + errChan := make(chan error, 1) + go func() { + var err error + ok := host.Mount(mountpoint, options) + if !ok { + err = errors.New("mount failed") + fs.Errorf(f, "Mount failed") + } + errChan <- err + }() + + // unmount + unmount := func() error { + fs.Debugf(nil, "Calling host.Unmount") + if host.Unmount() { + fs.Debugf(nil, "host.Unmount succeeded") + return nil + } + fs.Debugf(nil, "host.Unmount failed") + return errors.New("host unmount failed") + } + + // Wait for the filesystem to become ready + <-fsys.ready + return errChan, unmount, nil +} + +// Mount mounts the remote at mountpoint. +// +// If noModTime is set then it +func Mount(f fs.Fs, mountpoint string) error { + // Set permissions + dirPerms = 0777 &^ os.FileMode(umask) + filePerms = 0666 &^ os.FileMode(umask) + + // Show stats if the user has specifically requested them + if cmd.ShowStats() { + stopStats := cmd.StartStats() + defer close(stopStats) + } + + // Mount it + errChan, _, err := mount(f, mountpoint) + if err != nil { + return errors.Wrap(err, "failed to mount FUSE fs") + } + + // Note cgofuse unmounts the fs on SIGINT etc + + // Wait for mount to finish + err = <-errChan + if err != nil { + return errors.Wrap(err, "failed to umount FUSE fs") + } + + return nil +} diff --git a/cmd/cmount/mount_test.go b/cmd/cmount/mount_test.go new file mode 100644 index 000000000..37cd2dcba --- /dev/null +++ b/cmd/cmount/mount_test.go @@ -0,0 +1,33 @@ +// +build cgo +// +build linux darwin freebsd windows + +package cmount + +import ( + "testing" + + "github.com/ncw/rclone/cmd/mountlib/mounttest" +) + +func TestMain(m *testing.M) { mounttest.TestMain(m, mount, dirPerms, filePerms) } +func TestDirLs(t *testing.T) { mounttest.TestDirLs(t) } +func TestDirCreateAndRemoveDir(t *testing.T) { mounttest.TestDirCreateAndRemoveDir(t) } +func TestDirCreateAndRemoveFile(t *testing.T) { mounttest.TestDirCreateAndRemoveFile(t) } +func TestDirRenameFile(t *testing.T) { mounttest.TestDirRenameFile(t) } +func TestDirRenameEmptyDir(t *testing.T) { mounttest.TestDirRenameEmptyDir(t) } +func TestDirRenameFullDir(t *testing.T) { mounttest.TestDirRenameFullDir(t) } +func TestDirModTime(t *testing.T) { mounttest.TestDirModTime(t) } +func TestFileModTime(t *testing.T) { mounttest.TestFileModTime(t) } + +func TestFileModTimeWithOpenWriters(t *testing.T) { mounttest.TestFileModTimeWithOpenWriters(t) } + +func TestMount(t *testing.T) { mounttest.TestMount(t) } +func TestRoot(t *testing.T) { mounttest.TestRoot(t) } +func TestReadByByte(t *testing.T) { mounttest.TestReadByByte(t) } +func TestReadFileDoubleClose(t *testing.T) { mounttest.TestReadFileDoubleClose(t) } +func TestReadSeek(t *testing.T) { mounttest.TestReadSeek(t) } +func TestWriteFileNoWrite(t *testing.T) { mounttest.TestWriteFileNoWrite(t) } +func TestWriteFileWrite(t *testing.T) { mounttest.TestWriteFileWrite(t) } +func TestWriteFileOverwrite(t *testing.T) { mounttest.TestWriteFileOverwrite(t) } +func TestWriteFileDoubleClose(t *testing.T) { mounttest.TestWriteFileDoubleClose(t) } +func TestWriteFileFsync(t *testing.T) { mounttest.TestWriteFileFsync(t) } diff --git a/cmd/cmount/mount_unsupported.go b/cmd/cmount/mount_unsupported.go new file mode 100644 index 000000000..11d69ee62 --- /dev/null +++ b/cmd/cmount/mount_unsupported.go @@ -0,0 +1,6 @@ +// Build for cmount for unsupported platforms to stop go complaining +// about "no buildable Go source files " + +// +build !linux,!darwin,!freebsd,!windows !cgo + +package cmount