// +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 }