forked from TrueCloudLab/rclone
cmount: a new mount option based on cgofuse.
This with the aid of WinFSP should work on Windows. Unfinished bits * 1 test doesn't pass * docs * build
This commit is contained in:
parent
268fe0004c
commit
ee1111e4c9
5 changed files with 857 additions and 0 deletions
|
@ -8,6 +8,7 @@ import (
|
||||||
_ "github.com/ncw/rclone/cmd/cat"
|
_ "github.com/ncw/rclone/cmd/cat"
|
||||||
_ "github.com/ncw/rclone/cmd/check"
|
_ "github.com/ncw/rclone/cmd/check"
|
||||||
_ "github.com/ncw/rclone/cmd/cleanup"
|
_ "github.com/ncw/rclone/cmd/cleanup"
|
||||||
|
_ "github.com/ncw/rclone/cmd/cmount"
|
||||||
_ "github.com/ncw/rclone/cmd/config"
|
_ "github.com/ncw/rclone/cmd/config"
|
||||||
_ "github.com/ncw/rclone/cmd/copy"
|
_ "github.com/ncw/rclone/cmd/copy"
|
||||||
_ "github.com/ncw/rclone/cmd/copyto"
|
_ "github.com/ncw/rclone/cmd/copyto"
|
||||||
|
|
547
cmd/cmount/fs.go
Normal file
547
cmd/cmount/fs.go
Normal file
|
@ -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
|
||||||
|
}
|
270
cmd/cmount/mount.go
Normal file
270
cmd/cmount/mount.go
Normal file
|
@ -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
|
||||||
|
}
|
33
cmd/cmount/mount_test.go
Normal file
33
cmd/cmount/mount_test.go
Normal file
|
@ -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) }
|
6
cmd/cmount/mount_unsupported.go
Normal file
6
cmd/cmount/mount_unsupported.go
Normal file
|
@ -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
|
Loading…
Add table
Reference in a new issue