13b705e227
This stops the modification times for directories just being the current time and reads them from the remote instead. This doesn't take any extra transactions.
406 lines
10 KiB
Go
406 lines
10 KiB
Go
// +build linux darwin freebsd
|
|
|
|
package mount
|
|
|
|
import (
|
|
"os"
|
|
"path"
|
|
"sync"
|
|
"time"
|
|
|
|
"bazil.org/fuse"
|
|
fusefs "bazil.org/fuse/fs"
|
|
"github.com/ncw/rclone/fs"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/net/context"
|
|
)
|
|
|
|
// DirEntry describes the contents of a directory entry
|
|
//
|
|
// It can be a file or a directory
|
|
//
|
|
// node may be nil, but o may not
|
|
type DirEntry struct {
|
|
o fs.BasicInfo
|
|
node fusefs.Node
|
|
}
|
|
|
|
// Dir represents a directory entry
|
|
type Dir struct {
|
|
f fs.Fs
|
|
path string
|
|
modTime time.Time
|
|
mu sync.RWMutex // protects the following
|
|
read time.Time // time directory entry last read
|
|
items map[string]*DirEntry
|
|
}
|
|
|
|
func newDir(f fs.Fs, fsDir *fs.Dir) *Dir {
|
|
return &Dir{
|
|
f: f,
|
|
path: fsDir.Name,
|
|
modTime: fsDir.When,
|
|
}
|
|
}
|
|
|
|
// addObject adds a new object or directory to the directory
|
|
//
|
|
// note that we add new objects rather than updating old ones
|
|
func (d *Dir) addObject(o fs.BasicInfo, node fusefs.Node) *DirEntry {
|
|
item := &DirEntry{
|
|
o: o,
|
|
node: node,
|
|
}
|
|
d.mu.Lock()
|
|
d.items[path.Base(o.Remote())] = item
|
|
d.mu.Unlock()
|
|
return item
|
|
}
|
|
|
|
// delObject removes an object from the directory
|
|
func (d *Dir) delObject(leaf string) {
|
|
d.mu.Lock()
|
|
delete(d.items, leaf)
|
|
d.mu.Unlock()
|
|
}
|
|
|
|
// read the directory
|
|
func (d *Dir) readDir() error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
when := time.Now()
|
|
if d.read.IsZero() {
|
|
fs.Debug(d.path, "Reading directory")
|
|
} else {
|
|
age := when.Sub(d.read)
|
|
if age < dirCacheTime {
|
|
return nil
|
|
}
|
|
fs.Debug(d.path, "Re-reading directory (%v old)", age)
|
|
}
|
|
objs, dirs, err := fs.NewLister().SetLevel(1).Start(d.f, d.path).GetAll()
|
|
if err == fs.ErrorDirNotFound {
|
|
// We treat directory not found as empty because we
|
|
// create directories on the fly
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
// NB when we re-read a directory after its cache has expired
|
|
// we drop the old files which should lead to correct
|
|
// behaviour but may not be very efficient.
|
|
|
|
// Keep a note of the previous contents of the directory
|
|
oldItems := d.items
|
|
|
|
// Cache the items by name
|
|
d.items = make(map[string]*DirEntry, len(objs)+len(dirs))
|
|
for _, obj := range objs {
|
|
name := path.Base(obj.Remote())
|
|
d.items[name] = &DirEntry{
|
|
o: obj,
|
|
node: nil,
|
|
}
|
|
}
|
|
for _, dir := range dirs {
|
|
name := path.Base(dir.Remote())
|
|
// Use old dir value if it exists
|
|
if oldItem, ok := oldItems[name]; ok {
|
|
if _, ok := oldItem.o.(*fs.Dir); ok {
|
|
d.items[name] = oldItem
|
|
continue
|
|
}
|
|
}
|
|
d.items[name] = &DirEntry{
|
|
o: dir,
|
|
node: nil,
|
|
}
|
|
}
|
|
d.read = when
|
|
return nil
|
|
}
|
|
|
|
// lookup a single item in the directory
|
|
//
|
|
// returns fuse.ENOENT if not found.
|
|
func (d *Dir) lookup(leaf string) (*DirEntry, error) {
|
|
err := d.readDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
d.mu.RLock()
|
|
item, ok := d.items[leaf]
|
|
d.mu.RUnlock()
|
|
if !ok {
|
|
return nil, fuse.ENOENT
|
|
}
|
|
return item, nil
|
|
}
|
|
|
|
// Check to see if a directory is empty
|
|
func (d *Dir) isEmpty() (bool, error) {
|
|
err := d.readDir()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
d.mu.RLock()
|
|
defer d.mu.RUnlock()
|
|
return len(d.items) == 0, nil
|
|
}
|
|
|
|
// Check interface satsified
|
|
var _ fusefs.Node = (*Dir)(nil)
|
|
|
|
// Attr updates the attribes of a directory
|
|
func (d *Dir) Attr(ctx context.Context, a *fuse.Attr) error {
|
|
fs.Debug(d.path, "Dir.Attr")
|
|
a.Gid = gid
|
|
a.Uid = uid
|
|
a.Mode = os.ModeDir | dirPerms
|
|
a.Atime = d.modTime
|
|
a.Mtime = d.modTime
|
|
a.Ctime = d.modTime
|
|
a.Crtime = d.modTime
|
|
// FIXME include Valid so get some caching? Also mtime
|
|
return nil
|
|
}
|
|
|
|
// lookupNode calls lookup then makes sure the node is not nil in the DirEntry
|
|
func (d *Dir) lookupNode(leaf string) (item *DirEntry, err error) {
|
|
item, err = d.lookup(leaf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if item.node != nil {
|
|
return item, nil
|
|
}
|
|
var node fusefs.Node
|
|
switch x := item.o.(type) {
|
|
case fs.Object:
|
|
node, err = newFile(d, x), nil
|
|
case *fs.Dir:
|
|
node, err = newDir(d.f, x), nil
|
|
default:
|
|
err = errors.Errorf("unknown type %T", item)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item = d.addObject(item.o, node)
|
|
return item, err
|
|
}
|
|
|
|
// Check interface satisfied
|
|
var _ fusefs.NodeRequestLookuper = (*Dir)(nil)
|
|
|
|
// Lookup looks up a specific entry in the receiver.
|
|
//
|
|
// Lookup should return a Node corresponding to the entry. If the
|
|
// name does not exist in the directory, Lookup should return ENOENT.
|
|
//
|
|
// Lookup need not to handle the names "." and "..".
|
|
func (d *Dir) Lookup(ctx context.Context, req *fuse.LookupRequest, resp *fuse.LookupResponse) (node fusefs.Node, err error) {
|
|
path := path.Join(d.path, req.Name)
|
|
fs.Debug(path, "Dir.Lookup")
|
|
item, err := d.lookupNode(req.Name)
|
|
if err != nil {
|
|
if err != fuse.ENOENT {
|
|
fs.ErrorLog(path, "Dir.Lookup error: %v", err)
|
|
}
|
|
return nil, err
|
|
}
|
|
fs.Debug(path, "Dir.Lookup OK")
|
|
return item.node, nil
|
|
}
|
|
|
|
// Check interface satisfied
|
|
var _ fusefs.HandleReadDirAller = (*Dir)(nil)
|
|
|
|
// ReadDirAll reads the contents of the directory
|
|
func (d *Dir) ReadDirAll(ctx context.Context) (dirents []fuse.Dirent, err error) {
|
|
fs.Debug(d.path, "Dir.ReadDirAll")
|
|
err = d.readDir()
|
|
if err != nil {
|
|
fs.Debug(d.path, "Dir.ReadDirAll error: %v", err)
|
|
return nil, err
|
|
}
|
|
d.mu.RLock()
|
|
defer d.mu.RUnlock()
|
|
for _, item := range d.items {
|
|
var dirent fuse.Dirent
|
|
switch x := item.o.(type) {
|
|
case fs.Object:
|
|
dirent = fuse.Dirent{
|
|
// Inode FIXME ???
|
|
Type: fuse.DT_File,
|
|
Name: path.Base(x.Remote()),
|
|
}
|
|
case *fs.Dir:
|
|
dirent = fuse.Dirent{
|
|
// Inode FIXME ???
|
|
Type: fuse.DT_Dir,
|
|
Name: path.Base(x.Remote()),
|
|
}
|
|
default:
|
|
err = errors.Errorf("unknown type %T", item)
|
|
fs.ErrorLog(d.path, "Dir.ReadDirAll error: %v", err)
|
|
return nil, err
|
|
}
|
|
dirents = append(dirents, dirent)
|
|
}
|
|
fs.Debug(d.path, "Dir.ReadDirAll OK with %d entries", len(dirents))
|
|
return dirents, nil
|
|
}
|
|
|
|
var _ fusefs.NodeCreater = (*Dir)(nil)
|
|
|
|
// Create makes a new file
|
|
func (d *Dir) Create(ctx context.Context, req *fuse.CreateRequest, resp *fuse.CreateResponse) (fusefs.Node, fusefs.Handle, error) {
|
|
path := path.Join(d.path, req.Name)
|
|
fs.Debug(path, "Dir.Create")
|
|
src := newCreateInfo(d.f, path)
|
|
// This gets added to the directory when the file is written
|
|
file := newFile(d, nil)
|
|
fh, err := newWriteFileHandle(d, file, src)
|
|
if err != nil {
|
|
fs.ErrorLog(path, "Dir.Create error: %v", err)
|
|
return nil, nil, err
|
|
}
|
|
fs.Debug(path, "Dir.Create OK")
|
|
return file, fh, nil
|
|
}
|
|
|
|
var _ fusefs.NodeMkdirer = (*Dir)(nil)
|
|
|
|
// Mkdir creates a new directory
|
|
func (d *Dir) Mkdir(ctx context.Context, req *fuse.MkdirRequest) (fusefs.Node, error) {
|
|
// We just pretend to have created the directory - rclone will
|
|
// actually create the directory if we write files into it
|
|
path := path.Join(d.path, req.Name)
|
|
fs.Debug(path, "Dir.Mkdir")
|
|
fsDir := &fs.Dir{
|
|
Name: path,
|
|
When: time.Now(),
|
|
}
|
|
dir := newDir(d.f, fsDir)
|
|
d.addObject(fsDir, dir)
|
|
fs.Debug(path, "Dir.Mkdir OK")
|
|
return dir, nil
|
|
}
|
|
|
|
var _ fusefs.NodeRemover = (*Dir)(nil)
|
|
|
|
// Remove removes the entry with the given name from
|
|
// the receiver, which must be a directory. The entry to be removed
|
|
// may correspond to a file (unlink) or to a directory (rmdir).
|
|
func (d *Dir) Remove(ctx context.Context, req *fuse.RemoveRequest) error {
|
|
path := path.Join(d.path, req.Name)
|
|
fs.Debug(path, "Dir.Remove")
|
|
item, err := d.lookupNode(req.Name)
|
|
if err != nil {
|
|
fs.ErrorLog(path, "Dir.Remove error: %v", err)
|
|
return err
|
|
}
|
|
switch x := item.o.(type) {
|
|
case fs.Object:
|
|
err = x.Remove()
|
|
if err != nil {
|
|
fs.ErrorLog(path, "Dir.Remove file error: %v", err)
|
|
return err
|
|
}
|
|
case *fs.Dir:
|
|
// Do nothing for deleting directory - rclone can't
|
|
// currently remote a random directory
|
|
//
|
|
// Check directory is empty first though
|
|
dir := item.node.(*Dir)
|
|
empty, err := dir.isEmpty()
|
|
if err != nil {
|
|
fs.ErrorLog(path, "Dir.Remove dir error: %v", err)
|
|
return err
|
|
}
|
|
if !empty {
|
|
// return fuse.ENOTEMPTY - doesn't exist though so use EEXIST
|
|
fs.ErrorLog(path, "Dir.Remove not empty")
|
|
return fuse.EEXIST
|
|
}
|
|
default:
|
|
fs.ErrorLog(path, "Dir.Remove unknown type %T", item)
|
|
return errors.Errorf("unknown type %T", item)
|
|
}
|
|
// Remove the item from the directory listing
|
|
d.delObject(req.Name)
|
|
fs.Debug(path, "Dir.Remove OK")
|
|
return nil
|
|
}
|
|
|
|
// Check interface satisfied
|
|
var _ fusefs.NodeRenamer = (*Dir)(nil)
|
|
|
|
// Rename the file
|
|
func (d *Dir) Rename(ctx context.Context, req *fuse.RenameRequest, newDir fusefs.Node) error {
|
|
oldPath := path.Join(d.path, req.OldName)
|
|
destDir, ok := newDir.(*Dir)
|
|
if !ok {
|
|
err := errors.Errorf("Unknown Dir type %T", newDir)
|
|
fs.ErrorLog(oldPath, "Dir.Rename error: %v", err)
|
|
return err
|
|
}
|
|
newPath := path.Join(destDir.path, req.NewName)
|
|
fs.Debug(oldPath, "Dir.Rename to %q", newPath)
|
|
oldItem, err := d.lookupNode(req.OldName)
|
|
if err != nil {
|
|
fs.ErrorLog(oldPath, "Dir.Rename error: %v", err)
|
|
return err
|
|
}
|
|
var newObj fs.BasicInfo
|
|
switch x := oldItem.o.(type) {
|
|
case fs.Object:
|
|
oldObject := x
|
|
do, ok := d.f.(fs.Mover)
|
|
if !ok {
|
|
err := errors.Errorf("Fs %q can't Move files", d.f)
|
|
fs.ErrorLog(oldPath, "Dir.Rename error: %v", err)
|
|
return err
|
|
}
|
|
newObject, err := do.Move(oldObject, newPath)
|
|
if err != nil {
|
|
fs.ErrorLog(oldPath, "Dir.Rename error: %v", err)
|
|
return err
|
|
}
|
|
newObj = newObject
|
|
case *fs.Dir:
|
|
oldDir := oldItem.node.(*Dir)
|
|
empty, err := oldDir.isEmpty()
|
|
if err != nil {
|
|
fs.ErrorLog(oldPath, "Dir.Rename dir error: %v", err)
|
|
return err
|
|
}
|
|
if !empty {
|
|
// return fuse.ENOTEMPTY - doesn't exist though so use EEXIST
|
|
fs.ErrorLog(oldPath, "Dir.Rename can't rename non empty directory")
|
|
return fuse.EEXIST
|
|
}
|
|
newObj = &fs.Dir{
|
|
Name: newPath,
|
|
When: time.Now(),
|
|
}
|
|
default:
|
|
err = errors.Errorf("unknown type %T", oldItem)
|
|
fs.ErrorLog(d.path, "Dir.ReadDirAll error: %v", err)
|
|
return err
|
|
}
|
|
|
|
// Show moved - delete from old dir and add to new
|
|
d.delObject(req.OldName)
|
|
destDir.addObject(newObj, nil)
|
|
|
|
// FIXME need to flush the dir also
|
|
|
|
// FIXME use DirMover to move a directory?
|
|
// or maybe use MoveDir which can move anything
|
|
// fallback to Copy/Delete if no Move?
|
|
// if dir is empty then can move it
|
|
|
|
fs.ErrorLog(newPath, "Dir.Rename renamed from %q", oldPath)
|
|
return nil
|
|
}
|