mirror of
https://github.com/rclone/rclone
synced 2024-12-15 03:45:54 +01:00
20da3e6352
* Add options to Put, PutUnchecked and Update for all Fses * Use these to create HashOption * Implement this in local * Pass the option in fs.Copy This has the effect that we only calculate hashes we need to in the local Fs which speeds up transfers significantly.
736 lines
18 KiB
Go
736 lines
18 KiB
Go
// Package ftp interfaces with FTP servers
|
|
package ftp
|
|
|
|
import (
|
|
"io"
|
|
"net/textproto"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/jlaffaye/ftp"
|
|
"github.com/ncw/rclone/fs"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// Register with Fs
|
|
func init() {
|
|
fs.Register(&fs.RegInfo{
|
|
Name: "ftp",
|
|
Description: "FTP Connection",
|
|
NewFs: NewFs,
|
|
Options: []fs.Option{
|
|
{
|
|
Name: "host",
|
|
Help: "FTP host to connect to",
|
|
Optional: false,
|
|
Examples: []fs.OptionExample{{
|
|
Value: "ftp.example.com",
|
|
Help: "Connect to ftp.example.com",
|
|
}},
|
|
}, {
|
|
Name: "user",
|
|
Help: "FTP username, leave blank for current username, " + os.Getenv("USER"),
|
|
Optional: true,
|
|
}, {
|
|
Name: "port",
|
|
Help: "FTP port, leave blank to use default (21) ",
|
|
Optional: true,
|
|
}, {
|
|
Name: "pass",
|
|
Help: "FTP password",
|
|
IsPassword: true,
|
|
Optional: false,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
// Fs represents a remote FTP server
|
|
type Fs struct {
|
|
name string // name of this remote
|
|
root string // the path we are working on if any
|
|
features *fs.Features // optional features
|
|
url string
|
|
user string
|
|
pass string
|
|
dialAddr string
|
|
poolMu sync.Mutex
|
|
pool []*ftp.ServerConn
|
|
}
|
|
|
|
// Object describes an FTP file
|
|
type Object struct {
|
|
fs *Fs
|
|
remote string
|
|
info *FileInfo
|
|
}
|
|
|
|
// FileInfo is the metadata known about an FTP file
|
|
type FileInfo struct {
|
|
Name string
|
|
Size uint64
|
|
ModTime time.Time
|
|
IsDir bool
|
|
}
|
|
|
|
// ------------------------------------------------------------
|
|
|
|
// Name of this fs
|
|
func (f *Fs) Name() string {
|
|
return f.name
|
|
}
|
|
|
|
// Root of the remote (as passed into NewFs)
|
|
func (f *Fs) Root() string {
|
|
return f.root
|
|
}
|
|
|
|
// String returns a description of the FS
|
|
func (f *Fs) String() string {
|
|
return f.url
|
|
}
|
|
|
|
// Features returns the optional features of this Fs
|
|
func (f *Fs) Features() *fs.Features {
|
|
return f.features
|
|
}
|
|
|
|
// Open a new connection to the FTP server.
|
|
func (f *Fs) ftpConnection() (*ftp.ServerConn, error) {
|
|
fs.Debugf(f, "Connecting to FTP server")
|
|
c, err := ftp.DialTimeout(f.dialAddr, fs.Config.ConnectTimeout)
|
|
if err != nil {
|
|
fs.Errorf(f, "Error while Dialing %s: %s", f.dialAddr, err)
|
|
return nil, errors.Wrap(err, "ftpConnection Dial")
|
|
}
|
|
err = c.Login(f.user, f.pass)
|
|
if err != nil {
|
|
_ = c.Quit()
|
|
fs.Errorf(f, "Error while Logging in into %s: %s", f.dialAddr, err)
|
|
return nil, errors.Wrap(err, "ftpConnection Login")
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// Get an FTP connection from the pool, or open a new one
|
|
func (f *Fs) getFtpConnection() (c *ftp.ServerConn, err error) {
|
|
f.poolMu.Lock()
|
|
if len(f.pool) > 0 {
|
|
c = f.pool[0]
|
|
f.pool = f.pool[1:]
|
|
}
|
|
f.poolMu.Unlock()
|
|
if c != nil {
|
|
return c, nil
|
|
}
|
|
return f.ftpConnection()
|
|
}
|
|
|
|
// Return an FTP connection to the pool
|
|
//
|
|
// It nils the pointed to connection out so it can't be reused
|
|
//
|
|
// if err is not nil then it checks the connection is alive using a
|
|
// NOOP request
|
|
func (f *Fs) putFtpConnection(pc **ftp.ServerConn, err error) {
|
|
c := *pc
|
|
*pc = nil
|
|
if err != nil {
|
|
// If not a regular FTP error code then check the connection
|
|
_, isRegularError := errors.Cause(err).(*textproto.Error)
|
|
if !isRegularError {
|
|
nopErr := c.NoOp()
|
|
if nopErr != nil {
|
|
fs.Debugf(f, "Connection failed, closing: %v", nopErr)
|
|
_ = c.Quit()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
f.poolMu.Lock()
|
|
f.pool = append(f.pool, c)
|
|
f.poolMu.Unlock()
|
|
}
|
|
|
|
// NewFs contstructs an Fs from the path, container:path
|
|
func NewFs(name, root string) (ff fs.Fs, err error) {
|
|
// defer fs.Trace(nil, "name=%q, root=%q", name, root)("fs=%v, err=%v", &ff, &err)
|
|
// FIXME Convert the old scheme used for the first beta - remove after release
|
|
if ftpURL := fs.ConfigFileGet(name, "url"); ftpURL != "" {
|
|
fs.Infof(name, "Converting old configuration")
|
|
u, err := url.Parse(ftpURL)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "Failed to parse old url %q", ftpURL)
|
|
}
|
|
parts := strings.Split(u.Host, ":")
|
|
fs.ConfigFileSet(name, "host", parts[0])
|
|
if len(parts) > 1 {
|
|
fs.ConfigFileSet(name, "port", parts[1])
|
|
}
|
|
fs.ConfigFileSet(name, "host", u.Host)
|
|
fs.ConfigFileSet(name, "user", fs.ConfigFileGet(name, "username"))
|
|
fs.ConfigFileSet(name, "pass", fs.ConfigFileGet(name, "password"))
|
|
fs.ConfigFileDeleteKey(name, "username")
|
|
fs.ConfigFileDeleteKey(name, "password")
|
|
fs.ConfigFileDeleteKey(name, "url")
|
|
fs.SaveConfig()
|
|
if u.Path != "" && u.Path != "/" {
|
|
fs.Errorf(name, "Path %q in FTP URL no longer supported - put it on the end of the remote %s:%s", u.Path, name, u.Path)
|
|
}
|
|
}
|
|
host := fs.ConfigFileGet(name, "host")
|
|
user := fs.ConfigFileGet(name, "user")
|
|
pass := fs.ConfigFileGet(name, "pass")
|
|
port := fs.ConfigFileGet(name, "port")
|
|
pass, err = fs.Reveal(pass)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "NewFS decrypt password")
|
|
}
|
|
if user == "" {
|
|
user = os.Getenv("USER")
|
|
}
|
|
if port == "" {
|
|
port = "21"
|
|
}
|
|
|
|
dialAddr := host + ":" + port
|
|
u := "ftp://" + path.Join(dialAddr+"/", root)
|
|
f := &Fs{
|
|
name: name,
|
|
root: root,
|
|
url: u,
|
|
user: user,
|
|
pass: pass,
|
|
dialAddr: dialAddr,
|
|
}
|
|
f.features = (&fs.Features{}).Fill(f)
|
|
// Make a connection and pool it to return errors early
|
|
c, err := f.getFtpConnection()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "NewFs")
|
|
}
|
|
f.putFtpConnection(&c, nil)
|
|
if root != "" {
|
|
// Check to see if the root actually an existing file
|
|
remote := path.Base(root)
|
|
f.root = path.Dir(root)
|
|
if f.root == "." {
|
|
f.root = ""
|
|
}
|
|
_, err := f.NewObject(remote)
|
|
if err != nil {
|
|
if err == fs.ErrorObjectNotFound || errors.Cause(err) == fs.ErrorNotAFile {
|
|
// File doesn't exist so return old f
|
|
f.root = root
|
|
return f, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
// return an error with an fs which points to the parent
|
|
return f, fs.ErrorIsFile
|
|
}
|
|
return f, err
|
|
}
|
|
|
|
// translateErrorFile turns FTP errors into rclone errors if possible for a file
|
|
func translateErrorFile(err error) error {
|
|
switch errX := err.(type) {
|
|
case *textproto.Error:
|
|
switch errX.Code {
|
|
case ftp.StatusFileUnavailable:
|
|
err = fs.ErrorObjectNotFound
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// translateErrorDir turns FTP errors into rclone errors if possible for a directory
|
|
func translateErrorDir(err error) error {
|
|
switch errX := err.(type) {
|
|
case *textproto.Error:
|
|
switch errX.Code {
|
|
case ftp.StatusFileUnavailable:
|
|
err = fs.ErrorDirNotFound
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// NewObject finds the Object at remote. If it can't be found
|
|
// it returns the error fs.ErrorObjectNotFound.
|
|
func (f *Fs) NewObject(remote string) (o fs.Object, err error) {
|
|
// defer fs.Trace(remote, "")("o=%v, err=%v", &o, &err)
|
|
fullPath := path.Join(f.root, remote)
|
|
dir := path.Dir(fullPath)
|
|
base := path.Base(fullPath)
|
|
|
|
c, err := f.getFtpConnection()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "NewObject")
|
|
}
|
|
files, err := c.List(dir)
|
|
f.putFtpConnection(&c, err)
|
|
if err != nil {
|
|
return nil, translateErrorFile(err)
|
|
}
|
|
for i, file := range files {
|
|
if file.Type != ftp.EntryTypeFolder && file.Name == base {
|
|
o := &Object{
|
|
fs: f,
|
|
remote: remote,
|
|
}
|
|
info := &FileInfo{
|
|
Name: remote,
|
|
Size: files[i].Size,
|
|
ModTime: files[i].Time,
|
|
}
|
|
o.info = info
|
|
|
|
return o, nil
|
|
}
|
|
}
|
|
return nil, fs.ErrorObjectNotFound
|
|
}
|
|
|
|
func (f *Fs) list(out fs.ListOpts, dir string, curlevel int) {
|
|
// defer fs.Trace(dir, "curlevel=%d", curlevel)("")
|
|
c, err := f.getFtpConnection()
|
|
if err != nil {
|
|
out.SetError(errors.Wrap(err, "list"))
|
|
return
|
|
}
|
|
files, err := c.List(path.Join(f.root, dir))
|
|
f.putFtpConnection(&c, err)
|
|
if err != nil {
|
|
out.SetError(translateErrorDir(err))
|
|
return
|
|
}
|
|
for i := range files {
|
|
object := files[i]
|
|
newremote := path.Join(dir, object.Name)
|
|
switch object.Type {
|
|
case ftp.EntryTypeFolder:
|
|
if object.Name == "." || object.Name == ".." {
|
|
continue
|
|
}
|
|
if out.IncludeDirectory(newremote) {
|
|
d := &fs.Dir{
|
|
Name: newremote,
|
|
When: object.Time,
|
|
Bytes: 0,
|
|
Count: -1,
|
|
}
|
|
if curlevel < out.Level() {
|
|
f.list(out, path.Join(dir, object.Name), curlevel+1)
|
|
}
|
|
if out.AddDir(d) {
|
|
return
|
|
}
|
|
}
|
|
default:
|
|
o := &Object{
|
|
fs: f,
|
|
remote: newremote,
|
|
}
|
|
info := &FileInfo{
|
|
Name: newremote,
|
|
Size: object.Size,
|
|
ModTime: object.Time,
|
|
}
|
|
o.info = info
|
|
if out.Add(o) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// List the objects and directories of the Fs starting from dir
|
|
//
|
|
// dir should be "" to start from the root, and should not
|
|
// have trailing slashes.
|
|
//
|
|
// This should return ErrDirNotFound (using out.SetError())
|
|
// if the directory isn't found.
|
|
//
|
|
// Fses must support recursion levels of fs.MaxLevel and 1.
|
|
// They may return ErrorLevelNotSupported otherwise.
|
|
func (f *Fs) List(out fs.ListOpts, dir string) {
|
|
// defer fs.Trace(dir, "")("")
|
|
f.list(out, dir, 1)
|
|
out.Finished()
|
|
}
|
|
|
|
// Hashes are not supported
|
|
func (f *Fs) Hashes() fs.HashSet {
|
|
return 0
|
|
}
|
|
|
|
// Precision shows Modified Time not supported
|
|
func (f *Fs) Precision() time.Duration {
|
|
return fs.ModTimeNotSupported
|
|
}
|
|
|
|
// Put in to the remote path with the modTime given of the given size
|
|
//
|
|
// May create the object even if it returns an error - if so
|
|
// will return the object and the error, otherwise will return
|
|
// nil and the error
|
|
func (f *Fs) Put(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
|
// fs.Debugf(f, "Trying to put file %s", src.Remote())
|
|
err := f.mkParentDir(src.Remote())
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "Put mkParentDir failed")
|
|
}
|
|
o := &Object{
|
|
fs: f,
|
|
remote: src.Remote(),
|
|
}
|
|
err = o.Update(in, src, options...)
|
|
return o, err
|
|
}
|
|
|
|
// getInfo reads the FileInfo for a path
|
|
func (f *Fs) getInfo(remote string) (fi *FileInfo, err error) {
|
|
// defer fs.Trace(remote, "")("fi=%v, err=%v", &fi, &err)
|
|
dir := path.Dir(remote)
|
|
base := path.Base(remote)
|
|
|
|
c, err := f.getFtpConnection()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getInfo")
|
|
}
|
|
files, err := c.List(dir)
|
|
f.putFtpConnection(&c, err)
|
|
if err != nil {
|
|
return nil, translateErrorFile(err)
|
|
}
|
|
|
|
for i := range files {
|
|
if files[i].Name == base {
|
|
info := &FileInfo{
|
|
Name: remote,
|
|
Size: files[i].Size,
|
|
ModTime: files[i].Time,
|
|
IsDir: files[i].Type == ftp.EntryTypeFolder,
|
|
}
|
|
return info, nil
|
|
}
|
|
}
|
|
return nil, fs.ErrorObjectNotFound
|
|
}
|
|
|
|
// mkdir makes the directory and parents using unrooted paths
|
|
func (f *Fs) mkdir(abspath string) error {
|
|
if abspath == "." || abspath == "/" {
|
|
return nil
|
|
}
|
|
fi, err := f.getInfo(abspath)
|
|
if err == nil {
|
|
if fi.IsDir {
|
|
return nil
|
|
}
|
|
return fs.ErrorIsFile
|
|
} else if err != fs.ErrorObjectNotFound {
|
|
return errors.Wrapf(err, "mkdir %q failed", abspath)
|
|
}
|
|
parent := path.Dir(abspath)
|
|
err = f.mkdir(parent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c, connErr := f.getFtpConnection()
|
|
if connErr != nil {
|
|
return errors.Wrap(connErr, "mkdir")
|
|
}
|
|
err = c.MakeDir(abspath)
|
|
f.putFtpConnection(&c, err)
|
|
return err
|
|
}
|
|
|
|
// mkParentDir makes the parent of remote if necessary and any
|
|
// directories above that
|
|
func (f *Fs) mkParentDir(remote string) error {
|
|
parent := path.Dir(remote)
|
|
return f.mkdir(path.Join(f.root, parent))
|
|
}
|
|
|
|
// Mkdir creates the directory if it doesn't exist
|
|
func (f *Fs) Mkdir(dir string) (err error) {
|
|
// defer fs.Trace(dir, "")("err=%v", &err)
|
|
root := path.Join(f.root, dir)
|
|
return f.mkdir(root)
|
|
}
|
|
|
|
// Rmdir removes the directory (container, bucket) if empty
|
|
//
|
|
// Return an error if it doesn't exist or isn't empty
|
|
func (f *Fs) Rmdir(dir string) error {
|
|
c, err := f.getFtpConnection()
|
|
if err != nil {
|
|
return errors.Wrap(translateErrorFile(err), "Rmdir")
|
|
}
|
|
err = c.RemoveDir(path.Join(f.root, dir))
|
|
f.putFtpConnection(&c, err)
|
|
return translateErrorDir(err)
|
|
}
|
|
|
|
// Move renames a remote file object
|
|
func (f *Fs) Move(src fs.Object, remote string) (fs.Object, error) {
|
|
srcObj, ok := src.(*Object)
|
|
if !ok {
|
|
fs.Debugf(src, "Can't move - not same remote type")
|
|
return nil, fs.ErrorCantMove
|
|
}
|
|
err := f.mkParentDir(remote)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "Move mkParentDir failed")
|
|
}
|
|
c, err := f.getFtpConnection()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "Move")
|
|
}
|
|
err = c.Rename(
|
|
path.Join(srcObj.fs.root, srcObj.remote),
|
|
path.Join(f.root, remote),
|
|
)
|
|
f.putFtpConnection(&c, err)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "Move Rename failed")
|
|
}
|
|
dstObj, err := f.NewObject(remote)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "Move NewObject failed")
|
|
}
|
|
return dstObj, nil
|
|
}
|
|
|
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
|
// using server side move operations.
|
|
//
|
|
// Will only be called if src.Fs().Name() == f.Name()
|
|
//
|
|
// If it isn't possible then return fs.ErrorCantDirMove
|
|
//
|
|
// If destination exists then return fs.ErrorDirExists
|
|
func (f *Fs) DirMove(src fs.Fs, srcRemote, dstRemote string) error {
|
|
srcFs, ok := src.(*Fs)
|
|
if !ok {
|
|
fs.Debugf(srcFs, "Can't move directory - not same remote type")
|
|
return fs.ErrorCantDirMove
|
|
}
|
|
srcPath := path.Join(srcFs.root, srcRemote)
|
|
dstPath := path.Join(f.root, dstRemote)
|
|
|
|
// Check if destination exists
|
|
fi, err := f.getInfo(dstPath)
|
|
if err == nil {
|
|
if fi.IsDir {
|
|
return fs.ErrorDirExists
|
|
}
|
|
return fs.ErrorIsFile
|
|
} else if err != fs.ErrorObjectNotFound {
|
|
return errors.Wrapf(err, "DirMove getInfo failed")
|
|
}
|
|
|
|
// Make sure the parent directory exists
|
|
err = f.mkdir(path.Dir(dstPath))
|
|
if err != nil {
|
|
return errors.Wrap(err, "DirMove mkParentDir dst failed")
|
|
}
|
|
|
|
// Do the move
|
|
c, err := f.getFtpConnection()
|
|
if err != nil {
|
|
return errors.Wrap(err, "DirMove")
|
|
}
|
|
err = c.Rename(
|
|
srcPath,
|
|
dstPath,
|
|
)
|
|
f.putFtpConnection(&c, err)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "DirMove Rename(%q,%q) failed", srcPath, dstPath)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ------------------------------------------------------------
|
|
|
|
// Fs returns the parent Fs
|
|
func (o *Object) Fs() fs.Info {
|
|
return o.fs
|
|
}
|
|
|
|
// String version of o
|
|
func (o *Object) String() string {
|
|
if o == nil {
|
|
return "<nil>"
|
|
}
|
|
return o.remote
|
|
}
|
|
|
|
// Remote returns the remote path
|
|
func (o *Object) Remote() string {
|
|
return o.remote
|
|
}
|
|
|
|
// Hash returns the hash of an object returning a lowercase hex string
|
|
func (o *Object) Hash(t fs.HashType) (string, error) {
|
|
return "", fs.ErrHashUnsupported
|
|
}
|
|
|
|
// Size returns the size of an object in bytes
|
|
func (o *Object) Size() int64 {
|
|
return int64(o.info.Size)
|
|
}
|
|
|
|
// ModTime returns the modification time of the object
|
|
func (o *Object) ModTime() time.Time {
|
|
return o.info.ModTime
|
|
}
|
|
|
|
// SetModTime sets the modification time of the object
|
|
func (o *Object) SetModTime(modTime time.Time) error {
|
|
return nil
|
|
}
|
|
|
|
// Storable returns a boolean as to whether this object is storable
|
|
func (o *Object) Storable() bool {
|
|
return true
|
|
}
|
|
|
|
// ftpReadCloser implements io.ReadCloser for FTP objects.
|
|
type ftpReadCloser struct {
|
|
rc io.ReadCloser
|
|
c *ftp.ServerConn
|
|
f *Fs
|
|
err error // errors found during read
|
|
}
|
|
|
|
// Read bytes into p
|
|
func (f *ftpReadCloser) Read(p []byte) (n int, err error) {
|
|
n, err = f.rc.Read(p)
|
|
if err != nil && err != io.EOF {
|
|
f.err = err // store any errors for Close to examine
|
|
}
|
|
return
|
|
}
|
|
|
|
// Close the FTP reader and return the connection to the pool
|
|
func (f *ftpReadCloser) Close() error {
|
|
err := f.rc.Close()
|
|
// if errors while reading or closing, dump the connection
|
|
if err != nil || f.err != nil {
|
|
_ = f.c.Quit()
|
|
} else {
|
|
f.f.putFtpConnection(&f.c, nil)
|
|
}
|
|
// mask the error if it was caused by a premature close
|
|
switch errX := err.(type) {
|
|
case *textproto.Error:
|
|
switch errX.Code {
|
|
case ftp.StatusTransfertAborted, ftp.StatusFileUnavailable:
|
|
err = nil
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Open an object for read
|
|
func (o *Object) Open(options ...fs.OpenOption) (rc io.ReadCloser, err error) {
|
|
// defer fs.Trace(o, "")("rc=%v, err=%v", &rc, &err)
|
|
path := path.Join(o.fs.root, o.remote)
|
|
var offset int64
|
|
for _, option := range options {
|
|
switch x := option.(type) {
|
|
case *fs.SeekOption:
|
|
offset = x.Offset
|
|
default:
|
|
if option.Mandatory() {
|
|
fs.Logf(o, "Unsupported mandatory option: %v", option)
|
|
}
|
|
}
|
|
}
|
|
c, err := o.fs.getFtpConnection()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "open")
|
|
}
|
|
fd, err := c.RetrFrom(path, uint64(offset))
|
|
if err != nil {
|
|
o.fs.putFtpConnection(&c, err)
|
|
return nil, errors.Wrap(err, "open")
|
|
}
|
|
rc = &ftpReadCloser{rc: fd, c: c, f: o.fs}
|
|
return rc, nil
|
|
}
|
|
|
|
// Update the already existing object
|
|
//
|
|
// Copy the reader into the object updating modTime and size
|
|
//
|
|
// The new object may have been created if an error is returned
|
|
func (o *Object) Update(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) {
|
|
// defer fs.Trace(o, "src=%v", src)("err=%v", &err)
|
|
path := path.Join(o.fs.root, o.remote)
|
|
// remove the file if upload failed
|
|
remove := func() {
|
|
removeErr := o.Remove()
|
|
if removeErr != nil {
|
|
fs.Debugf(o, "Failed to remove: %v", removeErr)
|
|
} else {
|
|
fs.Debugf(o, "Removed after failed upload: %v", err)
|
|
}
|
|
}
|
|
c, err := o.fs.getFtpConnection()
|
|
if err != nil {
|
|
return errors.Wrap(err, "Update")
|
|
}
|
|
err = c.Stor(path, in)
|
|
if err != nil {
|
|
_ = c.Quit()
|
|
remove()
|
|
return errors.Wrap(err, "update stor")
|
|
}
|
|
o.fs.putFtpConnection(&c, nil)
|
|
o.info, err = o.fs.getInfo(path)
|
|
if err != nil {
|
|
return errors.Wrap(err, "update getinfo")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Remove an object
|
|
func (o *Object) Remove() (err error) {
|
|
// defer fs.Trace(o, "")("err=%v", &err)
|
|
path := path.Join(o.fs.root, o.remote)
|
|
// Check if it's a directory or a file
|
|
info, err := o.fs.getInfo(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.IsDir {
|
|
err = o.fs.Rmdir(o.remote)
|
|
} else {
|
|
c, err := o.fs.getFtpConnection()
|
|
if err != nil {
|
|
return errors.Wrap(err, "Remove")
|
|
}
|
|
err = c.Delete(path)
|
|
o.fs.putFtpConnection(&c, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Check the interfaces are satisfied
|
|
var (
|
|
_ fs.Fs = &Fs{}
|
|
_ fs.Mover = &Fs{}
|
|
_ fs.DirMover = &Fs{}
|
|
_ fs.Object = &Object{}
|
|
)
|