2016-07-17 22:03:23 +00:00
|
|
|
// +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 {
|
2016-12-14 15:26:04 +00:00
|
|
|
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
|
2016-07-17 22:03:23 +00:00
|
|
|
}
|
|
|
|
|
2016-12-14 15:26:04 +00:00
|
|
|
func newDir(f fs.Fs, fsDir *fs.Dir) *Dir {
|
2016-07-17 22:03:23 +00:00
|
|
|
return &Dir{
|
2016-12-14 15:26:04 +00:00
|
|
|
f: f,
|
|
|
|
path: fsDir.Name,
|
|
|
|
modTime: fsDir.When,
|
2016-07-17 22:03:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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()
|
2016-10-18 13:44:16 +00:00
|
|
|
when := time.Now()
|
|
|
|
if d.read.IsZero() {
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Debugf(d.path, "Reading directory")
|
2016-10-18 13:44:16 +00:00
|
|
|
} else {
|
|
|
|
age := when.Sub(d.read)
|
|
|
|
if age < dirCacheTime {
|
|
|
|
return nil
|
|
|
|
}
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Debugf(d.path, "Re-reading directory (%v old)", age)
|
2016-07-17 22:03:23 +00:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
2016-10-18 13:44:16 +00:00
|
|
|
// 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
|
|
|
|
|
2016-07-17 22:03:23 +00:00
|
|
|
// 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())
|
2016-10-18 13:44:16 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|
2016-07-17 22:03:23 +00:00
|
|
|
d.items[name] = &DirEntry{
|
|
|
|
o: dir,
|
|
|
|
node: nil,
|
|
|
|
}
|
|
|
|
}
|
2016-10-18 13:44:16 +00:00
|
|
|
d.read = when
|
2016-07-17 22:03:23 +00:00
|
|
|
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 {
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Debugf(d.path, "Dir.Attr")
|
2016-09-09 07:39:19 +00:00
|
|
|
a.Gid = gid
|
|
|
|
a.Uid = uid
|
2016-07-17 22:03:23 +00:00
|
|
|
a.Mode = os.ModeDir | dirPerms
|
2016-12-14 15:26:04 +00:00
|
|
|
a.Atime = d.modTime
|
|
|
|
a.Mtime = d.modTime
|
|
|
|
a.Ctime = d.modTime
|
|
|
|
a.Crtime = d.modTime
|
2016-07-17 22:03:23 +00:00
|
|
|
// 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:
|
2016-12-14 15:26:04 +00:00
|
|
|
node, err = newDir(d.f, x), nil
|
2016-07-17 22:03:23 +00:00
|
|
|
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)
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Debugf(path, "Dir.Lookup")
|
2016-07-17 22:03:23 +00:00
|
|
|
item, err := d.lookupNode(req.Name)
|
|
|
|
if err != nil {
|
|
|
|
if err != fuse.ENOENT {
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(path, "Dir.Lookup error: %v", err)
|
2016-07-17 22:03:23 +00:00
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Debugf(path, "Dir.Lookup OK")
|
2016-07-17 22:03:23 +00:00
|
|
|
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) {
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Debugf(d.path, "Dir.ReadDirAll")
|
2016-07-17 22:03:23 +00:00
|
|
|
err = d.readDir()
|
|
|
|
if err != nil {
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Debugf(d.path, "Dir.ReadDirAll error: %v", err)
|
2016-07-17 22:03:23 +00:00
|
|
|
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)
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(d.path, "Dir.ReadDirAll error: %v", err)
|
2016-07-17 22:03:23 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
dirents = append(dirents, dirent)
|
|
|
|
}
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Debugf(d.path, "Dir.ReadDirAll OK with %d entries", len(dirents))
|
2016-07-17 22:03:23 +00:00
|
|
|
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)
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Debugf(path, "Dir.Create")
|
2016-07-17 22:03:23 +00:00
|
|
|
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 {
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(path, "Dir.Create error: %v", err)
|
2016-07-17 22:03:23 +00:00
|
|
|
return nil, nil, err
|
|
|
|
}
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Debugf(path, "Dir.Create OK")
|
2016-07-17 22:03:23 +00:00
|
|
|
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) {
|
|
|
|
path := path.Join(d.path, req.Name)
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Debugf(path, "Dir.Mkdir")
|
2017-01-06 11:24:22 +00:00
|
|
|
err := d.f.Mkdir(path)
|
|
|
|
if err != nil {
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(path, "Dir.Mkdir failed to create directory: %v", err)
|
2017-01-06 11:24:22 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2016-07-17 22:03:23 +00:00
|
|
|
fsDir := &fs.Dir{
|
|
|
|
Name: path,
|
|
|
|
When: time.Now(),
|
|
|
|
}
|
2016-12-14 15:26:04 +00:00
|
|
|
dir := newDir(d.f, fsDir)
|
2016-07-17 22:03:23 +00:00
|
|
|
d.addObject(fsDir, dir)
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Debugf(path, "Dir.Mkdir OK")
|
2016-07-17 22:03:23 +00:00
|
|
|
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)
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Debugf(path, "Dir.Remove")
|
2016-07-17 22:03:23 +00:00
|
|
|
item, err := d.lookupNode(req.Name)
|
|
|
|
if err != nil {
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(path, "Dir.Remove error: %v", err)
|
2016-07-17 22:03:23 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
switch x := item.o.(type) {
|
|
|
|
case fs.Object:
|
|
|
|
err = x.Remove()
|
|
|
|
if err != nil {
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(path, "Dir.Remove file error: %v", err)
|
2016-07-17 22:03:23 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
case *fs.Dir:
|
2017-01-06 11:24:22 +00:00
|
|
|
// Check directory is empty first
|
2016-07-17 22:03:23 +00:00
|
|
|
dir := item.node.(*Dir)
|
|
|
|
empty, err := dir.isEmpty()
|
|
|
|
if err != nil {
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(path, "Dir.Remove dir error: %v", err)
|
2016-07-17 22:03:23 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
if !empty {
|
|
|
|
// return fuse.ENOTEMPTY - doesn't exist though so use EEXIST
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(path, "Dir.Remove not empty")
|
2016-07-17 22:03:23 +00:00
|
|
|
return fuse.EEXIST
|
|
|
|
}
|
2017-01-06 11:24:22 +00:00
|
|
|
// remove directory
|
|
|
|
err = d.f.Rmdir(path)
|
|
|
|
if err != nil {
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(path, "Dir.Remove failed to remove directory: %v", err)
|
2017-01-06 11:24:22 +00:00
|
|
|
return err
|
|
|
|
}
|
2016-07-17 22:03:23 +00:00
|
|
|
default:
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(path, "Dir.Remove unknown type %T", item)
|
2016-07-17 22:03:23 +00:00
|
|
|
return errors.Errorf("unknown type %T", item)
|
|
|
|
}
|
|
|
|
// Remove the item from the directory listing
|
|
|
|
d.delObject(req.Name)
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Debugf(path, "Dir.Remove OK")
|
2016-07-17 22:03:23 +00:00
|
|
|
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)
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(oldPath, "Dir.Rename error: %v", err)
|
2016-07-17 22:03:23 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
newPath := path.Join(destDir.path, req.NewName)
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Debugf(oldPath, "Dir.Rename to %q", newPath)
|
2016-07-17 22:03:23 +00:00
|
|
|
oldItem, err := d.lookupNode(req.OldName)
|
|
|
|
if err != nil {
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(oldPath, "Dir.Rename error: %v", err)
|
2016-07-17 22:03:23 +00:00
|
|
|
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)
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(oldPath, "Dir.Rename error: %v", err)
|
2016-07-17 22:03:23 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
newObject, err := do.Move(oldObject, newPath)
|
|
|
|
if err != nil {
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(oldPath, "Dir.Rename error: %v", err)
|
2016-07-17 22:03:23 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
newObj = newObject
|
|
|
|
case *fs.Dir:
|
|
|
|
oldDir := oldItem.node.(*Dir)
|
|
|
|
empty, err := oldDir.isEmpty()
|
|
|
|
if err != nil {
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(oldPath, "Dir.Rename dir error: %v", err)
|
2016-07-17 22:03:23 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
if !empty {
|
|
|
|
// return fuse.ENOTEMPTY - doesn't exist though so use EEXIST
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(oldPath, "Dir.Rename can't rename non empty directory")
|
2016-07-17 22:03:23 +00:00
|
|
|
return fuse.EEXIST
|
|
|
|
}
|
2017-01-06 11:24:22 +00:00
|
|
|
err = d.f.Rmdir(oldPath)
|
|
|
|
if err != nil {
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(oldPath, "Dir.Rename failed to remove directory: %v", err)
|
2017-01-06 11:24:22 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
err = d.f.Mkdir(newPath)
|
|
|
|
if err != nil {
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(newPath, "Dir.Rename failed to create directory: %v", err)
|
2017-01-06 11:24:22 +00:00
|
|
|
return err
|
|
|
|
}
|
2016-07-17 22:03:23 +00:00
|
|
|
newObj = &fs.Dir{
|
|
|
|
Name: newPath,
|
|
|
|
When: time.Now(),
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
err = errors.Errorf("unknown type %T", oldItem)
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(d.path, "Dir.ReadDirAll error: %v", err)
|
2016-07-17 22:03:23 +00:00
|
|
|
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
|
|
|
|
|
2017-02-09 11:01:20 +00:00
|
|
|
fs.Errorf(newPath, "Dir.Rename renamed from %q", oldPath)
|
2016-07-17 22:03:23 +00:00
|
|
|
return nil
|
|
|
|
}
|
2017-02-02 21:30:32 +00:00
|
|
|
|
|
|
|
// Check interface satisfied
|
|
|
|
var _ fusefs.NodeFsyncer = (*Dir)(nil)
|
|
|
|
|
|
|
|
// Fsync the directory
|
|
|
|
//
|
|
|
|
// Note that we don't do anything except return OK
|
|
|
|
func (d *Dir) Fsync(ctx context.Context, req *fuse.FsyncRequest) error {
|
|
|
|
return nil
|
|
|
|
}
|