Forward port 58a82cd578 into cmount branch

allow the fuse directory cached to be cleaned manually
This commit is contained in:
Nick Craig-Wood 2017-05-07 13:04:20 +01:00
parent ee1111e4c9
commit e1516e0159
7 changed files with 151 additions and 48 deletions

View file

@ -11,11 +11,14 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"os/signal"
"runtime" "runtime"
"syscall"
"time" "time"
"github.com/billziss-gh/cgofuse/fuse" "github.com/billziss-gh/cgofuse/fuse"
"github.com/ncw/rclone/cmd" "github.com/ncw/rclone/cmd"
"github.com/ncw/rclone/cmd/mountlib"
"github.com/ncw/rclone/fs" "github.com/ncw/rclone/fs"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -123,6 +126,21 @@ mount won't do that, so will be less reliable than the rclone command.
Note that all the rclone filters can be used to select a subset of the Note that all the rclone filters can be used to select a subset of the
files to be visible in the mount. files to be visible in the mount.
### Directory Cache ###
Using the ` + "`--dir-cache-time`" + ` flag, you can set how long a
directory should be considered up to date and not refreshed from the
backend. Changes made locally in the mount may appear immediately or
invalidate the cache. However, changes done on the remote will only
be picked up once the cache expires.
Alternatively, you can send a ` + "`SIGHUP`" + ` signal to rclone for
it to flush all directory caches, regardless of how old they are.
Assuming only one rlcone instance is running, you can reset the cache
like this:
kill -SIGHUP $(pidof rclone)
### Bugs ### ### Bugs ###
* All the remotes should work for read, but some may not for write * All the remotes should work for read, but some may not for write
@ -190,16 +208,16 @@ func mountOptions(device string, mountpoint string) (options []string) {
// //
// returns an error, and an error channel for the serve process to // returns an error, and an error channel for the serve process to
// report an error when fusermount is called. // report an error when fusermount is called.
func mount(f fs.Fs, mountpoint string) (<-chan error, func() error, error) { func mount(f fs.Fs, mountpoint string) (*mountlib.FS, <-chan error, func() error, error) {
fs.Debugf(f, "Mounting on %q", mountpoint) fs.Debugf(f, "Mounting on %q", mountpoint)
// Check the mountpoint // Check the mountpoint
fi, err := os.Stat(mountpoint) fi, err := os.Stat(mountpoint)
if err != nil { if err != nil {
return nil, nil, errors.Wrap(err, "mountpoint") return nil, nil, nil, errors.Wrap(err, "mountpoint")
} }
if !fi.IsDir() { if !fi.IsDir() {
return nil, nil, errors.New("mountpoint is not a directory") return nil, nil, nil, errors.New("mountpoint is not a directory")
} }
// Create underlying FS // Create underlying FS
@ -235,7 +253,7 @@ func mount(f fs.Fs, mountpoint string) (<-chan error, func() error, error) {
// Wait for the filesystem to become ready // Wait for the filesystem to become ready
<-fsys.ready <-fsys.ready
return errChan, unmount, nil return fsys.FS, errChan, unmount, nil
} }
// Mount mounts the remote at mountpoint. // Mount mounts the remote at mountpoint.
@ -253,15 +271,33 @@ func Mount(f fs.Fs, mountpoint string) error {
} }
// Mount it // Mount it
errChan, _, err := mount(f, mountpoint) FS, errChan, _, err := mount(f, mountpoint)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to mount FUSE fs") return errors.Wrap(err, "failed to mount FUSE fs")
} }
// Note cgofuse unmounts the fs on SIGINT etc // Note cgofuse unmounts the fs on SIGINT etc
// Wait for mount to finish sigHup := make(chan os.Signal, 1)
err = <-errChan signal.Notify(sigHup, syscall.SIGHUP)
waitloop:
for {
select {
// umount triggered outside the app
case err = <-errChan:
break waitloop
// user sent SIGHUP to clear the cache
case <-sigHup:
root, err := FS.Root()
if err != nil {
fs.Errorf(f, "Error reading root: %v", err)
} else {
root.ForgetAll()
}
}
}
if err != nil { if err != nil {
return errors.Wrap(err, "failed to umount FUSE fs") return errors.Wrap(err, "failed to umount FUSE fs")
} }

View file

@ -9,25 +9,25 @@ import (
"github.com/ncw/rclone/cmd/mountlib/mounttest" "github.com/ncw/rclone/cmd/mountlib/mounttest"
) )
func TestMain(m *testing.M) { mounttest.TestMain(m, mount, dirPerms, filePerms) } func TestMain(m *testing.M) { mounttest.TestMain(m, mount, dirPerms, filePerms) }
func TestDirLs(t *testing.T) { mounttest.TestDirLs(t) } func TestDirLs(t *testing.T) { mounttest.TestDirLs(t) }
func TestDirCreateAndRemoveDir(t *testing.T) { mounttest.TestDirCreateAndRemoveDir(t) } func TestDirCreateAndRemoveDir(t *testing.T) { mounttest.TestDirCreateAndRemoveDir(t) }
func TestDirCreateAndRemoveFile(t *testing.T) { mounttest.TestDirCreateAndRemoveFile(t) } func TestDirCreateAndRemoveFile(t *testing.T) { mounttest.TestDirCreateAndRemoveFile(t) }
func TestDirRenameFile(t *testing.T) { mounttest.TestDirRenameFile(t) } func TestDirRenameFile(t *testing.T) { mounttest.TestDirRenameFile(t) }
func TestDirRenameEmptyDir(t *testing.T) { mounttest.TestDirRenameEmptyDir(t) } func TestDirRenameEmptyDir(t *testing.T) { mounttest.TestDirRenameEmptyDir(t) }
func TestDirRenameFullDir(t *testing.T) { mounttest.TestDirRenameFullDir(t) } func TestDirRenameFullDir(t *testing.T) { mounttest.TestDirRenameFullDir(t) }
func TestDirModTime(t *testing.T) { mounttest.TestDirModTime(t) } func TestDirModTime(t *testing.T) { mounttest.TestDirModTime(t) }
func TestFileModTime(t *testing.T) { mounttest.TestFileModTime(t) } func TestDirCacheFlush(t *testing.T) { mounttest.TestDirCacheFlush(t) }
func TestDirCacheFlushOnDirRename(t *testing.T) { mounttest.TestDirCacheFlushOnDirRename(t) }
func TestFileModTimeWithOpenWriters(t *testing.T) { mounttest.TestFileModTimeWithOpenWriters(t) } func TestFileModTime(t *testing.T) { mounttest.TestFileModTime(t) }
func TestFileModTimeWithOpenWriters(t *testing.T) {} // FIXME mounttest.TestFileModTimeWithOpenWriters(t)
func TestMount(t *testing.T) { mounttest.TestMount(t) } func TestMount(t *testing.T) { mounttest.TestMount(t) }
func TestRoot(t *testing.T) { mounttest.TestRoot(t) } func TestRoot(t *testing.T) { mounttest.TestRoot(t) }
func TestReadByByte(t *testing.T) { mounttest.TestReadByByte(t) } func TestReadByByte(t *testing.T) { mounttest.TestReadByByte(t) }
func TestReadFileDoubleClose(t *testing.T) { mounttest.TestReadFileDoubleClose(t) } func TestReadFileDoubleClose(t *testing.T) { mounttest.TestReadFileDoubleClose(t) }
func TestReadSeek(t *testing.T) { mounttest.TestReadSeek(t) } func TestReadSeek(t *testing.T) { mounttest.TestReadSeek(t) }
func TestWriteFileNoWrite(t *testing.T) { mounttest.TestWriteFileNoWrite(t) } func TestWriteFileNoWrite(t *testing.T) { mounttest.TestWriteFileNoWrite(t) }
func TestWriteFileWrite(t *testing.T) { mounttest.TestWriteFileWrite(t) } func TestWriteFileWrite(t *testing.T) { mounttest.TestWriteFileWrite(t) }
func TestWriteFileOverwrite(t *testing.T) { mounttest.TestWriteFileOverwrite(t) } func TestWriteFileOverwrite(t *testing.T) { mounttest.TestWriteFileOverwrite(t) }
func TestWriteFileDoubleClose(t *testing.T) { mounttest.TestWriteFileDoubleClose(t) } func TestWriteFileDoubleClose(t *testing.T) { mounttest.TestWriteFileDoubleClose(t) }
func TestWriteFileFsync(t *testing.T) { mounttest.TestWriteFileFsync(t) } func TestWriteFileFsync(t *testing.T) { mounttest.TestWriteFileFsync(t) }

View file

@ -14,6 +14,7 @@ import (
"bazil.org/fuse" "bazil.org/fuse"
fusefs "bazil.org/fuse/fs" fusefs "bazil.org/fuse/fs"
"github.com/ncw/rclone/cmd" "github.com/ncw/rclone/cmd"
"github.com/ncw/rclone/cmd/mountlib"
"github.com/ncw/rclone/fs" "github.com/ncw/rclone/fs"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -198,11 +199,11 @@ func mountOptions(device string) (options []fuse.MountOption) {
// //
// returns an error, and an error channel for the serve process to // returns an error, and an error channel for the serve process to
// report an error when fusermount is called. // report an error when fusermount is called.
func mount(f fs.Fs, mountpoint string) (<-chan error, func() error, error) { func mount(f fs.Fs, mountpoint string) (*mountlib.FS, <-chan error, func() error, error) {
fs.Debugf(f, "Mounting on %q", mountpoint) fs.Debugf(f, "Mounting on %q", mountpoint)
c, err := fuse.Mount(mountpoint, mountOptions(f.Name()+":"+f.Root())...) c, err := fuse.Mount(mountpoint, mountOptions(f.Name()+":"+f.Root())...)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, nil, err
} }
filesys := NewFS(f) filesys := NewFS(f)
@ -222,14 +223,14 @@ func mount(f fs.Fs, mountpoint string) (<-chan error, func() error, error) {
// check if the mount process has an error to report // check if the mount process has an error to report
<-c.Ready <-c.Ready
if err := c.MountError; err != nil { if err := c.MountError; err != nil {
return nil, nil, err return nil, nil, nil, err
} }
unmount := func() error { unmount := func() error {
return fuse.Unmount(mountpoint) return fuse.Unmount(mountpoint)
} }
return errChan, unmount, nil return filesys.FS, errChan, unmount, nil
} }
// Mount mounts the remote at mountpoint. // Mount mounts the remote at mountpoint.
@ -253,21 +254,35 @@ func Mount(f fs.Fs, mountpoint string) error {
} }
// Mount it // Mount it
errChan, unmount, err := mount(f, mountpoint) FS, errChan, unmount, err := mount(f, mountpoint)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to mount FUSE fs") return errors.Wrap(err, "failed to mount FUSE fs")
} }
sigChan := make(chan os.Signal, 1) sigInt := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigInt, syscall.SIGINT, syscall.SIGTERM)
sigHup := make(chan os.Signal, 1)
signal.Notify(sigHup, syscall.SIGHUP)
select { waitloop:
// umount triggered outside the app for {
case err = <-errChan: select {
break // umount triggered outside the app
// Program abort: umount case err = <-errChan:
case <-sigChan: break waitloop
err = unmount() // Program abort: umount
case <-sigInt:
err = unmount()
break waitloop
// user sent SIGHUP to clear the cache
case <-sigHup:
root, err := FS.Root()
if err != nil {
fs.Errorf(f, "Error reading root: %v", err)
} else {
root.ForgetAll()
}
}
} }
if err != nil { if err != nil {

View file

@ -14,6 +14,8 @@ func TestDirRenameFile(t *testing.T) { mounttest.TestDirRenameFile(
func TestDirRenameEmptyDir(t *testing.T) { mounttest.TestDirRenameEmptyDir(t) } func TestDirRenameEmptyDir(t *testing.T) { mounttest.TestDirRenameEmptyDir(t) }
func TestDirRenameFullDir(t *testing.T) { mounttest.TestDirRenameFullDir(t) } func TestDirRenameFullDir(t *testing.T) { mounttest.TestDirRenameFullDir(t) }
func TestDirModTime(t *testing.T) { mounttest.TestDirModTime(t) } func TestDirModTime(t *testing.T) { mounttest.TestDirModTime(t) }
func TestDirCacheFlush(t *testing.T) { mounttest.TestDirCacheFlush(t) }
func TestDirCacheFlushOnDirRename(t *testing.T) { mounttest.TestDirCacheFlushOnDirRename(t) }
func TestFileModTime(t *testing.T) { mounttest.TestFileModTime(t) } func TestFileModTime(t *testing.T) { mounttest.TestFileModTime(t) }
func TestFileModTimeWithOpenWriters(t *testing.T) { mounttest.TestFileModTimeWithOpenWriters(t) } func TestFileModTimeWithOpenWriters(t *testing.T) { mounttest.TestFileModTimeWithOpenWriters(t) }
func TestMount(t *testing.T) { mounttest.TestMount(t) } func TestMount(t *testing.T) { mounttest.TestMount(t) }

View file

@ -2,6 +2,7 @@ package mountlib
import ( import (
"path" "path"
"strings"
"sync" "sync"
"time" "time"
@ -58,14 +59,58 @@ func (d *Dir) Node() Node {
return d return d
} }
// ForgetAll ensures the directory and all its children are purged
// from the cache.
func (d *Dir) ForgetAll() {
d.ForgetPath("")
}
// ForgetPath clears the cache for itself and all subdirectories if
// they match the given path. The path is specified relative from the
// directory it is called from.
// It is not possible to traverse the directory tree upwards, i.e.
// you cannot clear the cache for the Dir's ancestors or siblings.
func (d *Dir) ForgetPath(relativePath string) {
absPath := path.Join(d.path, relativePath)
if absPath == "." {
absPath = ""
}
d.walk(absPath, func(dir *Dir) {
fs.Debugf(dir.path, "forgetting directory cache")
dir.read = time.Time{}
dir.items = nil
})
}
// walk runs a function on all directories whose path matches
// the given absolute one. It will be called on a directory's
// children first. It will not apply the function to parent
// nodes, regardless of the given path.
func (d *Dir) walk(absPath string, fun func(*Dir)) {
if d.items != nil {
for _, entry := range d.items {
if dir, ok := entry.Node.(*Dir); ok {
dir.walk(absPath, fun)
}
}
}
if d.path == absPath || absPath == "" || strings.HasPrefix(d.path, absPath+"/") {
d.mu.Lock()
defer d.mu.Unlock()
fun(d)
}
}
// rename should be called after the directory is renamed // rename should be called after the directory is renamed
// //
// Reset the directory to new state, discarding all the objects and // Reset the directory to new state, discarding all the objects and
// reading everything again // reading everything again
func (d *Dir) rename(newParent *Dir, fsDir *fs.Dir) { func (d *Dir) rename(newParent *Dir, fsDir *fs.Dir) {
d.ForgetAll()
d.path = fsDir.Name d.path = fsDir.Name
d.modTime = fsDir.When d.modTime = fsDir.When
d.items = nil
d.read = time.Time{} d.read = time.Time{}
} }

View file

@ -170,12 +170,15 @@ func TestDirCacheFlush(t *testing.T) {
err := run.fremote.Mkdir("dir/subdir") err := run.fremote.Mkdir("dir/subdir")
require.NoError(t, err) require.NoError(t, err)
root, err := run.filesys.Root()
require.NoError(t, err)
// expect newly created "subdir" on remote to not show up // expect newly created "subdir" on remote to not show up
run.mountFS.rootDir.ForgetPath("otherdir") root.ForgetPath("otherdir")
run.readLocal(t, localDm, "") run.readLocal(t, localDm, "")
assert.Equal(t, dm, localDm, "expected vs fuse mount") assert.Equal(t, dm, localDm, "expected vs fuse mount")
run.mountFS.rootDir.ForgetPath("dir") root.ForgetPath("dir")
dm = newDirMap("otherdir/|otherdir/file 1|dir/|dir/file 1|dir/subdir/") dm = newDirMap("otherdir/|otherdir/file 1|dir/|dir/file 1|dir/subdir/")
run.readLocal(t, localDm, "") run.readLocal(t, localDm, "")
assert.Equal(t, dm, localDm, "expected vs fuse mount") assert.Equal(t, dm, localDm, "expected vs fuse mount")

View file

@ -15,6 +15,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/ncw/rclone/cmd/mountlib"
"github.com/ncw/rclone/fs" "github.com/ncw/rclone/fs"
_ "github.com/ncw/rclone/fs/all" _ "github.com/ncw/rclone/fs/all"
"github.com/ncw/rclone/fstest" "github.com/ncw/rclone/fstest"
@ -35,7 +36,7 @@ var (
type ( type (
UnmountFn func() error UnmountFn func() error
MountFn func(f fs.Fs, mountpoint string) (<-chan error, func() error, error) MountFn func(f fs.Fs, mountpoint string) (*mountlib.FS, <-chan error, func() error, error)
) )
var ( var (
@ -56,6 +57,7 @@ func TestMain(m *testing.M, fn MountFn, dirPerms, filePerms os.FileMode) {
// Run holds the remotes for a test run // Run holds the remotes for a test run
type Run struct { type Run struct {
filesys *mountlib.FS
mountPath string mountPath string
fremote fs.Fs fremote fs.Fs
fremoteName string fremoteName string
@ -116,7 +118,7 @@ func newRun() *Run {
func (r *Run) mount() { func (r *Run) mount() {
log.Printf("mount %q %q", r.fremote, r.mountPath) log.Printf("mount %q %q", r.fremote, r.mountPath)
var err error var err error
r.umountResult, r.umountFn, err = mountFn(r.fremote, r.mountPath) r.filesys, r.umountResult, r.umountFn, err = mountFn(r.fremote, r.mountPath)
if err != nil { if err != nil {
log.Printf("mount failed: %v", err) log.Printf("mount failed: %v", err)
r.skip = true r.skip = true