forked from TrueCloudLab/rclone
ftp: fix remaining issues to make tests work
* fix root * factor ftpConnection * fix path munging * fix recursive dir loops after update * use fs.Trace and comment out debugs * re-arrange and supplement docs
This commit is contained in:
parent
3ed0440bd2
commit
35c210d36f
5 changed files with 266 additions and 93 deletions
|
@ -24,8 +24,8 @@ Rclone is a command line program to sync files and directories to and from
|
||||||
* Backblaze B2
|
* Backblaze B2
|
||||||
* Yandex Disk
|
* Yandex Disk
|
||||||
* SFTP
|
* SFTP
|
||||||
* The local filesystem
|
|
||||||
* FTP
|
* FTP
|
||||||
|
* The local filesystem
|
||||||
|
|
||||||
Features
|
Features
|
||||||
|
|
||||||
|
|
|
@ -24,8 +24,8 @@ Rclone is a command line program to sync files and directories to and from
|
||||||
* Backblaze B2
|
* Backblaze B2
|
||||||
* Yandex Disk
|
* Yandex Disk
|
||||||
* SFTP
|
* SFTP
|
||||||
* The local filesystem
|
|
||||||
* FTP
|
* FTP
|
||||||
|
* The local filesystem
|
||||||
|
|
||||||
Features
|
Features
|
||||||
|
|
||||||
|
|
|
@ -7,29 +7,117 @@ date: "2017-01-01"
|
||||||
<i class="fa fa-file"></i> FTP
|
<i class="fa fa-file"></i> FTP
|
||||||
------------------------------
|
------------------------------
|
||||||
|
|
||||||
FTP support is provided via
|
FTP is the File Transfer Protocl. FTP support is provided using the
|
||||||
[github.com/jlaffaye/ftp](https://godoc.org/github.com/jlaffaye/ftp)
|
[github.com/jlaffaye/ftp](https://godoc.org/github.com/jlaffaye/ftp)
|
||||||
package.
|
package.
|
||||||
|
|
||||||
### Configuration ###
|
Here is an example of making an FTP configuration. First run
|
||||||
|
|
||||||
An Ftp backend only needs an Url and and username and password. With
|
rclone config
|
||||||
|
|
||||||
|
This will guide you through an interactive setup process. An FTP
|
||||||
|
backend only needs an URL and and username and password. With
|
||||||
anonymous FTP server you will need to use `anonymous` as username and
|
anonymous FTP server you will need to use `anonymous` as username and
|
||||||
your email address as password.
|
your email address as password.
|
||||||
|
|
||||||
Example:
|
|
||||||
```
|
```
|
||||||
|
No remotes found - make a new one
|
||||||
|
n) New remote
|
||||||
|
r) Rename remote
|
||||||
|
c) Copy remote
|
||||||
|
s) Set configuration password
|
||||||
|
q) Quit config
|
||||||
|
n/r/c/s/q> n
|
||||||
|
name> remote
|
||||||
|
Type of storage to configure.
|
||||||
|
Choose a number from below, or type in your own value
|
||||||
|
1 / Amazon Drive
|
||||||
|
\ "amazon cloud drive"
|
||||||
|
2 / Amazon S3 (also Dreamhost, Ceph, Minio)
|
||||||
|
\ "s3"
|
||||||
|
3 / Backblaze B2
|
||||||
|
\ "b2"
|
||||||
|
4 / Dropbox
|
||||||
|
\ "dropbox"
|
||||||
|
5 / Encrypt/Decrypt a remote
|
||||||
|
\ "crypt"
|
||||||
|
6 / FTP interface
|
||||||
|
\ "ftp"
|
||||||
|
7 / Google Cloud Storage (this is not Google Drive)
|
||||||
|
\ "google cloud storage"
|
||||||
|
8 / Google Drive
|
||||||
|
\ "drive"
|
||||||
|
9 / Hubic
|
||||||
|
\ "hubic"
|
||||||
|
10 / Local Disk
|
||||||
|
\ "local"
|
||||||
|
11 / Microsoft OneDrive
|
||||||
|
\ "onedrive"
|
||||||
|
12 / Openstack Swift (Rackspace Cloud Files, Memset Memstore, OVH)
|
||||||
|
\ "swift"
|
||||||
|
13 / SSH/SFTP Connection
|
||||||
|
\ "sftp"
|
||||||
|
14 / Yandex Disk
|
||||||
|
\ "yandex"
|
||||||
|
Storage> ftp
|
||||||
|
Username
|
||||||
|
username> anonymous
|
||||||
|
Password
|
||||||
|
y) Yes type in my own password
|
||||||
|
g) Generate random password
|
||||||
|
y/g> y
|
||||||
|
Enter the password:
|
||||||
|
password:
|
||||||
|
Confirm the password:
|
||||||
|
password:
|
||||||
|
FTP URL
|
||||||
|
url> ftp://ftp.mirrorservice.org/
|
||||||
|
Remote config
|
||||||
|
--------------------
|
||||||
[remote]
|
[remote]
|
||||||
type = Ftp
|
|
||||||
username = anonymous
|
username = anonymous
|
||||||
password = john.snow@example.org
|
password = *** ENCRYPTED ***
|
||||||
url = ftp://ftp.kernel.org/pub
|
url = ftp://ftp.mirrorservice.org/
|
||||||
|
--------------------
|
||||||
|
y) Yes this is OK
|
||||||
|
e) Edit this remote
|
||||||
|
d) Delete this remote
|
||||||
|
y/e/d> y
|
||||||
```
|
```
|
||||||
|
|
||||||
### Unsupported features ###
|
This remote is called `remote` and can now be used like this
|
||||||
|
|
||||||
FTP backends does not support:
|
See all directories in the home directory
|
||||||
|
|
||||||
* Any hash mechanism
|
rclone lsd remote:
|
||||||
* Modified Time
|
|
||||||
* remote copy/move
|
Make a new directory
|
||||||
|
|
||||||
|
rclone mkdir remote:path/to/directory
|
||||||
|
|
||||||
|
List the contents of a directory
|
||||||
|
|
||||||
|
rclone ls remote:path/to/directory
|
||||||
|
|
||||||
|
Sync `/home/local/directory` to the remote directory, deleting any
|
||||||
|
excess files in the directory.
|
||||||
|
|
||||||
|
rclone sync /home/local/directory remote:directory
|
||||||
|
|
||||||
|
### Modified time ###
|
||||||
|
|
||||||
|
FTP does not support modified times. Any times you see on the server
|
||||||
|
will be time of upload.
|
||||||
|
|
||||||
|
### Checksums ###
|
||||||
|
|
||||||
|
FTP does not support any checksums.
|
||||||
|
|
||||||
|
### Limitations ###
|
||||||
|
|
||||||
|
Note that since FTP isn't HTTP based the following flags don't work
|
||||||
|
with it: `--dump-headers`, `--dump-bodies`, `--dump-auth`
|
||||||
|
|
||||||
|
Note that `--timeout` and `--contimeout` aren't supported.
|
||||||
|
|
||||||
|
FTP could support server side move but doesn't yet.
|
||||||
|
|
|
@ -61,6 +61,7 @@
|
||||||
<li><a href="/local/"><i class="fa fa-file"></i> Local</a></li>
|
<li><a href="/local/"><i class="fa fa-file"></i> Local</a></li>
|
||||||
<li><a href="/yandex/"><i class="fa fa-space-shuttle"></i> Yandex Disk</a></li>
|
<li><a href="/yandex/"><i class="fa fa-space-shuttle"></i> Yandex Disk</a></li>
|
||||||
<li><a href="/sftp/"><i class="fa fa-server"></i> SFTP</a></li>
|
<li><a href="/sftp/"><i class="fa fa-server"></i> SFTP</a></li>
|
||||||
|
<li><a href="/ftp/"><i class="fa fa-file"></i> FTP</a></li>
|
||||||
<li><a href="/crypt/"><i class="fa fa-lock"></i> Crypt (encrypts the above)</a></li>
|
<li><a href="/crypt/"><i class="fa fa-lock"></i> Crypt (encrypts the above)</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
242
ftp/ftp.go
242
ftp/ftp.go
|
@ -1,8 +1,13 @@
|
||||||
// Package ftp interfaces with FTP servers
|
// Package ftp interfaces with FTP servers
|
||||||
package ftp
|
package ftp
|
||||||
|
|
||||||
|
// FIXME Mover and DirMover are possible using f.c.Rename
|
||||||
|
// FIXME Should have a pool of connections rather than a global lock
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/textproto"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -49,6 +54,9 @@ type Fs struct {
|
||||||
c *ftp.ServerConn // the connection to the FTP server
|
c *ftp.ServerConn // the connection to the FTP server
|
||||||
url *url.URL
|
url *url.URL
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
user string
|
||||||
|
pass string
|
||||||
|
dialAddr string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Object describes an FTP file
|
// Object describes an FTP file
|
||||||
|
@ -89,76 +97,124 @@ func (f *Fs) Features() *fs.Features {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open a new connection to the FTP server.
|
// Open a new connection to the FTP server.
|
||||||
func ftpConnection(name, root string) (*ftp.ServerConn, *url.URL, error) {
|
func (f *Fs) ftpConnection() (*ftp.ServerConn, error) {
|
||||||
|
globalMux.Lock()
|
||||||
|
defer globalMux.Unlock()
|
||||||
|
fs.Debugf(f, "Connecting to FTP server")
|
||||||
|
c, err := ftp.DialTimeout(f.dialAddr, 30*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
fs.Errorf(nil, "Error while Dialing %s: %s", f.dialAddr, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = c.Login(f.user, f.pass)
|
||||||
|
if err != nil {
|
||||||
|
fs.Errorf(nil, "Error while Logging in into %s: %s", f.dialAddr, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
URL := fs.ConfigFileGet(name, "url")
|
URL := fs.ConfigFileGet(name, "url")
|
||||||
user := fs.ConfigFileGet(name, "username")
|
user := fs.ConfigFileGet(name, "username")
|
||||||
pass := fs.ConfigFileGet(name, "password")
|
pass := fs.ConfigFileGet(name, "password")
|
||||||
pass, err := fs.Reveal(pass)
|
pass, err = fs.Reveal(pass)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.Wrap(err, "failed to decrypt password")
|
return nil, errors.Wrap(err, "NewFS decrypt password")
|
||||||
}
|
}
|
||||||
u, err := url.Parse(URL)
|
u, err := url.Parse(URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.Wrap(err, "open ftp connection url parse")
|
return nil, errors.Wrap(err, "NewFS URL parse")
|
||||||
}
|
}
|
||||||
u.Path = path.Join(u.Path, root)
|
urlPath := strings.Trim(u.Path, "/")
|
||||||
fs.Debugf(nil, "New ftp Connection with name %s and url %s (path %s)", name, u.String(), u.Path)
|
fullPath := root
|
||||||
globalMux.Lock()
|
if urlPath != "" && !strings.HasPrefix("/", root) {
|
||||||
defer globalMux.Unlock()
|
fullPath = path.Join(u.Path, root)
|
||||||
|
}
|
||||||
|
root = fullPath
|
||||||
dialAddr := u.Hostname()
|
dialAddr := u.Hostname()
|
||||||
if u.Port() != "" {
|
if u.Port() != "" {
|
||||||
dialAddr += ":" + u.Port()
|
dialAddr += ":" + u.Port()
|
||||||
} else {
|
} else {
|
||||||
dialAddr += ":21"
|
dialAddr += ":21"
|
||||||
}
|
}
|
||||||
c, err := ftp.DialTimeout(dialAddr, 30*time.Second)
|
f := &Fs{
|
||||||
if err != nil {
|
name: name,
|
||||||
fs.Errorf(nil, "Error while Dialing %s: %s", dialAddr, err)
|
root: root,
|
||||||
return nil, u, err
|
url: u,
|
||||||
|
user: user,
|
||||||
|
pass: pass,
|
||||||
|
dialAddr: dialAddr,
|
||||||
}
|
}
|
||||||
err = c.Login(user, pass)
|
f.features = (&fs.Features{}).Fill(f)
|
||||||
if err != nil {
|
f.c, err = f.ftpConnection()
|
||||||
fs.Errorf(nil, "Error while Logging in into %s: %s", dialAddr, err)
|
|
||||||
return nil, u, err
|
|
||||||
}
|
|
||||||
return c, u, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFs contstructs an Fs from the path, container:path
|
|
||||||
func NewFs(name, root string) (fs.Fs, error) {
|
|
||||||
fs.Debugf(nil, "ENTER function 'NewFs' with name %s and root %s", name, root)
|
|
||||||
defer fs.Debugf(nil, "EXIT function 'NewFs'")
|
|
||||||
c, u, err := ftpConnection(name, root)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
f := &Fs{
|
if root != "" {
|
||||||
name: name,
|
// Check to see if the root actually an existing file
|
||||||
root: u.Path,
|
remote := path.Base(root)
|
||||||
c: c,
|
f.root = path.Dir(root)
|
||||||
url: u,
|
if f.root == "." {
|
||||||
mu: sync.Mutex{},
|
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
|
||||||
}
|
}
|
||||||
f.features = (&fs.Features{}).Fill(f)
|
|
||||||
return f, err
|
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
|
// NewObject finds the Object at remote. If it can't be found
|
||||||
// it returns the error fs.ErrorObjectNotFound.
|
// it returns the error fs.ErrorObjectNotFound.
|
||||||
func (f *Fs) NewObject(remote string) (fs.Object, error) {
|
func (f *Fs) NewObject(remote string) (o fs.Object, err error) {
|
||||||
fs.Debugf(f, "ENTER function 'NewObject' called with remote %s", remote)
|
// defer fs.Trace(remote, "")("o=%v, err=%v", &o, &err)
|
||||||
defer fs.Debugf(f, "EXIT function 'NewObject'")
|
fullPath := path.Join(f.root, remote)
|
||||||
dir := path.Dir(remote)
|
dir := path.Dir(fullPath)
|
||||||
base := path.Base(remote)
|
base := path.Base(fullPath)
|
||||||
|
|
||||||
f.mu.Lock()
|
f.mu.Lock()
|
||||||
files, err := f.c.List(dir)
|
files, err := f.c.List(dir)
|
||||||
f.mu.Unlock()
|
f.mu.Unlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, translateErrorFile(err)
|
||||||
}
|
}
|
||||||
for i := range files {
|
for i, file := range files {
|
||||||
if files[i].Name == base {
|
if file.Type != ftp.EntryTypeFolder && file.Name == base {
|
||||||
o := &Object{
|
o := &Object{
|
||||||
fs: f,
|
fs: f,
|
||||||
remote: remote,
|
remote: remote,
|
||||||
|
@ -177,13 +233,12 @@ func (f *Fs) NewObject(remote string) (fs.Object, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fs) list(out fs.ListOpts, dir string, curlevel int) {
|
func (f *Fs) list(out fs.ListOpts, dir string, curlevel int) {
|
||||||
fs.Debugf(f, "ENTER function 'list'")
|
// defer fs.Trace(dir, "curlevel=%d", curlevel)("")
|
||||||
defer fs.Debugf(f, "EXIT function 'list'")
|
|
||||||
f.mu.Lock()
|
f.mu.Lock()
|
||||||
files, err := f.c.List(path.Join(f.root, dir))
|
files, err := f.c.List(path.Join(f.root, dir))
|
||||||
f.mu.Unlock()
|
f.mu.Unlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
out.SetError(err)
|
out.SetError(translateErrorDir(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for i := range files {
|
for i := range files {
|
||||||
|
@ -191,6 +246,9 @@ func (f *Fs) list(out fs.ListOpts, dir string, curlevel int) {
|
||||||
newremote := path.Join(dir, object.Name)
|
newremote := path.Join(dir, object.Name)
|
||||||
switch object.Type {
|
switch object.Type {
|
||||||
case ftp.EntryTypeFolder:
|
case ftp.EntryTypeFolder:
|
||||||
|
if object.Name == "." || object.Name == ".." {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if out.IncludeDirectory(newremote) {
|
if out.IncludeDirectory(newremote) {
|
||||||
d := &fs.Dir{
|
d := &fs.Dir{
|
||||||
Name: newremote,
|
Name: newremote,
|
||||||
|
@ -234,8 +292,7 @@ func (f *Fs) list(out fs.ListOpts, dir string, curlevel int) {
|
||||||
// Fses must support recursion levels of fs.MaxLevel and 1.
|
// Fses must support recursion levels of fs.MaxLevel and 1.
|
||||||
// They may return ErrorLevelNotSupported otherwise.
|
// They may return ErrorLevelNotSupported otherwise.
|
||||||
func (f *Fs) List(out fs.ListOpts, dir string) {
|
func (f *Fs) List(out fs.ListOpts, dir string) {
|
||||||
fs.Debugf(f, "ENTER function 'List' on directory '%s/%s'", f.root, dir)
|
// defer fs.Trace(dir, "")("")
|
||||||
defer fs.Debugf(f, "EXIT function 'List' for directory '%s/%s'", f.root, dir)
|
|
||||||
f.list(out, dir, 1)
|
f.list(out, dir, 1)
|
||||||
out.Finished()
|
out.Finished()
|
||||||
}
|
}
|
||||||
|
@ -256,7 +313,7 @@ func (f *Fs) Precision() time.Duration {
|
||||||
// will return the object and the error, otherwise will return
|
// will return the object and the error, otherwise will return
|
||||||
// nil and the error
|
// nil and the error
|
||||||
func (f *Fs) Put(in io.Reader, src fs.ObjectInfo) (fs.Object, error) {
|
func (f *Fs) Put(in io.Reader, src fs.ObjectInfo) (fs.Object, error) {
|
||||||
fs.Debugf(f, "Trying to put file %s", src.Remote())
|
// fs.Debugf(f, "Trying to put file %s", src.Remote())
|
||||||
o := &Object{
|
o := &Object{
|
||||||
fs: f,
|
fs: f,
|
||||||
remote: src.Remote(),
|
remote: src.Remote(),
|
||||||
|
@ -266,9 +323,8 @@ func (f *Fs) Put(in io.Reader, src fs.ObjectInfo) (fs.Object, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// getInfo reads the FileInfo for a path
|
// getInfo reads the FileInfo for a path
|
||||||
func (f *Fs) getInfo(remote string) (*FileInfo, error) {
|
func (f *Fs) getInfo(remote string) (fi *FileInfo, err error) {
|
||||||
fs.Debugf(f, "ENTER function 'getInfo' on file %s", remote)
|
// defer fs.Trace(remote, "")("fi=%v, err=%v", &fi, &err)
|
||||||
defer fs.Debugf(f, "EXIT function 'getInfo'")
|
|
||||||
dir := path.Dir(remote)
|
dir := path.Dir(remote)
|
||||||
base := path.Base(remote)
|
base := path.Base(remote)
|
||||||
|
|
||||||
|
@ -276,7 +332,7 @@ func (f *Fs) getInfo(remote string) (*FileInfo, error) {
|
||||||
files, err := f.c.List(dir)
|
files, err := f.c.List(dir)
|
||||||
f.mu.Unlock()
|
f.mu.Unlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, translateErrorFile(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range files {
|
for i := range files {
|
||||||
|
@ -295,32 +351,32 @@ func (f *Fs) getInfo(remote string) (*FileInfo, error) {
|
||||||
|
|
||||||
func (f *Fs) mkdir(abspath string) error {
|
func (f *Fs) mkdir(abspath string) error {
|
||||||
_, err := f.getInfo(abspath)
|
_, err := f.getInfo(abspath)
|
||||||
if err != nil {
|
if err == fs.ErrorObjectNotFound {
|
||||||
fs.Debugf(f, "Trying to create directory %s", abspath)
|
// fs.Debugf(f, "Trying to create directory %s", abspath)
|
||||||
f.mu.Lock()
|
f.mu.Lock()
|
||||||
err := f.c.MakeDir(abspath)
|
err = f.c.MakeDir(abspath)
|
||||||
f.mu.Unlock()
|
f.mu.Unlock()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mkdir creates the directory if it doesn't exist
|
// Mkdir creates the directory if it doesn't exist
|
||||||
func (f *Fs) Mkdir(dir string) error {
|
func (f *Fs) Mkdir(dir string) (err error) {
|
||||||
|
// defer fs.Trace(dir, "")("err=%v", &err)
|
||||||
// This actually works as mkdir -p
|
// This actually works as mkdir -p
|
||||||
fs.Debugf(f, "ENTER function 'Mkdir' on '%s/%s'", f.root, dir)
|
|
||||||
defer fs.Debugf(f, "EXIT function 'Mkdir' on '%s/%s'", f.root, dir)
|
|
||||||
abspath := path.Join(f.root, dir)
|
abspath := path.Join(f.root, dir)
|
||||||
tokens := strings.Split(abspath, "/")
|
tokens := strings.Split(abspath, "/")
|
||||||
curdir := ""
|
curdir := ""
|
||||||
for i := range tokens {
|
for i := range tokens {
|
||||||
curdir += "/" + tokens[i]
|
curdir += tokens[i]
|
||||||
|
if curdir == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
err := f.mkdir(curdir)
|
err := f.mkdir(curdir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
curdir += "/"
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -334,11 +390,11 @@ func (f *Fs) Rmdir(dir string) error {
|
||||||
files, err := f.c.List(path.Join(f.root, dir))
|
files, err := f.c.List(path.Join(f.root, dir))
|
||||||
f.mu.Unlock()
|
f.mu.Unlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "rmdir")
|
return translateErrorDir(err)
|
||||||
}
|
}
|
||||||
for i := range files {
|
for _, file := range files {
|
||||||
if files[i].Type == ftp.EntryTypeFolder {
|
if file.Type == ftp.EntryTypeFolder && file.Name != "." && file.Name != ".." {
|
||||||
err = f.Rmdir(path.Join(dir, files[i].Name))
|
err = f.Rmdir(path.Join(dir, file.Name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "rmdir")
|
return errors.Wrap(err, "rmdir")
|
||||||
}
|
}
|
||||||
|
@ -412,11 +468,21 @@ func (f *ftpReadCloser) Close() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open an object for read
|
// Open an object for read
|
||||||
func (o *Object) Open(options ...fs.OpenOption) (io.ReadCloser, error) {
|
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)
|
path := path.Join(o.fs.root, o.remote)
|
||||||
fs.Debugf(o.fs, "ENTER function 'Open' on file '%s' in root '%s'", o.remote, o.fs.root)
|
var offset int64
|
||||||
defer fs.Debugf(o.fs, "EXIT function 'Open' %s", path)
|
for _, option := range options {
|
||||||
c, _, err := ftpConnection(o.fs.name, o.fs.root)
|
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.ftpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "open")
|
return nil, errors.Wrap(err, "open")
|
||||||
}
|
}
|
||||||
|
@ -424,13 +490,22 @@ func (o *Object) Open(options ...fs.OpenOption) (io.ReadCloser, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "open")
|
return nil, errors.Wrap(err, "open")
|
||||||
}
|
}
|
||||||
return &ftpReadCloser{ReadCloser: fd, c: c}, nil
|
rc = &ftpReadCloser{ReadCloser: fd, c: c}
|
||||||
|
if offset != 0 {
|
||||||
|
_, err = io.CopyN(ioutil.Discard, fd, offset)
|
||||||
|
if err != nil {
|
||||||
|
_ = rc.Close()
|
||||||
|
return nil, errors.Wrap(err, "open skipping bytes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeAllDir creates the parent directories for the object
|
// makeAllDir creates the parent directories for the object
|
||||||
func (o *Object) makeAllDir() error {
|
func (o *Object) makeAllDir() error {
|
||||||
tokens := strings.Split(path.Dir(o.remote), "/")
|
dir, _ := path.Split(o.remote)
|
||||||
dir := ""
|
tokens := strings.Split(dir, "/")
|
||||||
|
dir = ""
|
||||||
for i := range tokens {
|
for i := range tokens {
|
||||||
dir += tokens[i] + "/"
|
dir += tokens[i] + "/"
|
||||||
err := o.fs.Mkdir(dir)
|
err := o.fs.Mkdir(dir)
|
||||||
|
@ -450,29 +525,38 @@ func (o *Object) Update(in io.Reader, src fs.ObjectInfo) error {
|
||||||
// Create all upper directory first...
|
// Create all upper directory first...
|
||||||
err := o.makeAllDir()
|
err := o.makeAllDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "update")
|
return errors.Wrap(err, "update mkdir")
|
||||||
}
|
}
|
||||||
path := path.Join(o.fs.root, o.remote)
|
path := path.Join(o.fs.root, o.remote)
|
||||||
c, _, err := ftpConnection(o.fs.name, o.fs.root)
|
c, err := o.fs.ftpConnection()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "update")
|
return errors.Wrap(err, "update connect")
|
||||||
|
}
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
err = c.Stor(path, in)
|
err = c.Stor(path, in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "update")
|
remove()
|
||||||
|
return errors.Wrap(err, "update stor")
|
||||||
}
|
}
|
||||||
o.info, err = o.fs.getInfo(path)
|
o.info, err = o.fs.getInfo(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "update")
|
return errors.Wrap(err, "update getinfo")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove an object
|
// Remove an object
|
||||||
func (o *Object) Remove() error {
|
func (o *Object) Remove() (err error) {
|
||||||
|
// defer fs.Trace(o, "")("err=%v", &err)
|
||||||
path := path.Join(o.fs.root, o.remote)
|
path := path.Join(o.fs.root, o.remote)
|
||||||
fs.Debugf(o, "ENTER function 'Remove' for obejct at %s", path)
|
|
||||||
defer fs.Debugf(o, "EXIT function 'Remove' for obejct at %s", path)
|
|
||||||
// Check if it's a directory or a file
|
// Check if it's a directory or a file
|
||||||
info, err := o.fs.getInfo(path)
|
info, err := o.fs.getInfo(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Add table
Reference in a new issue