mirror of
https://github.com/rclone/rclone.git
synced 2024-11-26 02:14:42 +01:00
ftp: add support for precise time #5655
This commit is contained in:
parent
3a03f2778c
commit
844025d053
@ -98,6 +98,11 @@ to an encrypted one. Cannot be used in combination with implicit FTP.`,
|
|||||||
Help: "Disable using MLSD even if server advertises support.",
|
Help: "Disable using MLSD even if server advertises support.",
|
||||||
Default: false,
|
Default: false,
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
|
}, {
|
||||||
|
Name: "writing_mdtm",
|
||||||
|
Help: "Use MDTM to set modification time (VsFtpd quirk)",
|
||||||
|
Default: false,
|
||||||
|
Advanced: true,
|
||||||
}, {
|
}, {
|
||||||
Name: "idle_timeout",
|
Name: "idle_timeout",
|
||||||
Default: fs.Duration(60 * time.Second),
|
Default: fs.Duration(60 * time.Second),
|
||||||
@ -169,6 +174,7 @@ type Options struct {
|
|||||||
SkipVerifyTLSCert bool `config:"no_check_certificate"`
|
SkipVerifyTLSCert bool `config:"no_check_certificate"`
|
||||||
DisableEPSV bool `config:"disable_epsv"`
|
DisableEPSV bool `config:"disable_epsv"`
|
||||||
DisableMLSD bool `config:"disable_mlsd"`
|
DisableMLSD bool `config:"disable_mlsd"`
|
||||||
|
WritingMDTM bool `config:"writing_mdtm"`
|
||||||
IdleTimeout fs.Duration `config:"idle_timeout"`
|
IdleTimeout fs.Duration `config:"idle_timeout"`
|
||||||
CloseTimeout fs.Duration `config:"close_timeout"`
|
CloseTimeout fs.Duration `config:"close_timeout"`
|
||||||
ShutTimeout fs.Duration `config:"shut_timeout"`
|
ShutTimeout fs.Duration `config:"shut_timeout"`
|
||||||
@ -192,6 +198,9 @@ type Fs struct {
|
|||||||
tokens *pacer.TokenDispenser
|
tokens *pacer.TokenDispenser
|
||||||
tlsConf *tls.Config
|
tlsConf *tls.Config
|
||||||
pacer *fs.Pacer // pacer for FTP connections
|
pacer *fs.Pacer // pacer for FTP connections
|
||||||
|
fGetTime bool // true if the ftp library accepts GetTime
|
||||||
|
fSetTime bool // true if the ftp library accepts SetTime
|
||||||
|
fLstTime bool // true if the List call returns precise time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Object describes an FTP file
|
// Object describes an FTP file
|
||||||
@ -206,6 +215,7 @@ type FileInfo struct {
|
|||||||
Name string
|
Name string
|
||||||
Size uint64
|
Size uint64
|
||||||
ModTime time.Time
|
ModTime time.Time
|
||||||
|
precise bool // true if the time is precise
|
||||||
IsDir bool
|
IsDir bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,6 +330,9 @@ func (f *Fs) ftpConnection(ctx context.Context) (c *ftp.ServerConn, err error) {
|
|||||||
if f.opt.ShutTimeout != 0 && f.opt.ShutTimeout != fs.DurationOff {
|
if f.opt.ShutTimeout != 0 && f.opt.ShutTimeout != fs.DurationOff {
|
||||||
ftpConfig = append(ftpConfig, ftp.DialWithShutTimeout(time.Duration(f.opt.ShutTimeout)))
|
ftpConfig = append(ftpConfig, ftp.DialWithShutTimeout(time.Duration(f.opt.ShutTimeout)))
|
||||||
}
|
}
|
||||||
|
if f.opt.WritingMDTM {
|
||||||
|
ftpConfig = append(ftpConfig, ftp.DialWithWritingMDTM(true))
|
||||||
|
}
|
||||||
if f.ci.Dump&(fs.DumpHeaders|fs.DumpBodies|fs.DumpRequests|fs.DumpResponses) != 0 {
|
if f.ci.Dump&(fs.DumpHeaders|fs.DumpBodies|fs.DumpRequests|fs.DumpResponses) != 0 {
|
||||||
ftpConfig = append(ftpConfig, ftp.DialWithDebugOutput(&debugLog{auth: f.ci.Dump&fs.DumpAuth != 0}))
|
ftpConfig = append(ftpConfig, ftp.DialWithDebugOutput(&debugLog{auth: f.ci.Dump&fs.DumpAuth != 0}))
|
||||||
}
|
}
|
||||||
@ -491,6 +504,12 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (ff fs.Fs
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "NewFs")
|
return nil, errors.Wrap(err, "NewFs")
|
||||||
}
|
}
|
||||||
|
f.fGetTime = c.IsGetTimeSupported()
|
||||||
|
f.fSetTime = c.IsSetTimeSupported()
|
||||||
|
f.fLstTime = c.IsTimePreciseInList()
|
||||||
|
if !f.fLstTime && f.fGetTime {
|
||||||
|
f.features.SlowModTime = true
|
||||||
|
}
|
||||||
f.putFtpConnection(&c, nil)
|
f.putFtpConnection(&c, nil)
|
||||||
if root != "" {
|
if root != "" {
|
||||||
// Check to see if the root actually an existing file
|
// Check to see if the root actually an existing file
|
||||||
@ -609,13 +628,12 @@ func (f *Fs) NewObject(ctx context.Context, remote string) (o fs.Object, err err
|
|||||||
fs: f,
|
fs: f,
|
||||||
remote: remote,
|
remote: remote,
|
||||||
}
|
}
|
||||||
info := &FileInfo{
|
o.info = &FileInfo{
|
||||||
Name: remote,
|
Name: remote,
|
||||||
Size: entry.Size,
|
Size: entry.Size,
|
||||||
ModTime: entry.Time,
|
ModTime: entry.Time,
|
||||||
|
precise: f.fLstTime,
|
||||||
}
|
}
|
||||||
o.info = info
|
|
||||||
|
|
||||||
return o, nil
|
return o, nil
|
||||||
}
|
}
|
||||||
return nil, fs.ErrorObjectNotFound
|
return nil, fs.ErrorObjectNotFound
|
||||||
@ -710,6 +728,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
Name: newremote,
|
Name: newremote,
|
||||||
Size: object.Size,
|
Size: object.Size,
|
||||||
ModTime: object.Time,
|
ModTime: object.Time,
|
||||||
|
precise: f.fLstTime,
|
||||||
}
|
}
|
||||||
o.info = info
|
o.info = info
|
||||||
entries = append(entries, o)
|
entries = append(entries, o)
|
||||||
@ -723,8 +742,18 @@ func (f *Fs) Hashes() hash.Set {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Precision shows Modified Time not supported
|
// Precision shows whether modified time is supported or not depending on the
|
||||||
|
// FTP server capabilities, namely whether FTP server:
|
||||||
|
// - accepts the MDTM command to get file time (fGetTime)
|
||||||
|
// or supports MLSD returning precise file time in the list (fLstTime)
|
||||||
|
// - accepts the MFMT command to set file time (fSetTime)
|
||||||
|
// or non-standard form of the MDTM command (fSetTime, too)
|
||||||
|
// used by VsFtpd for the same purpose (WritingMDTM)
|
||||||
|
// See "mdtm_write" in https://security.appspot.com/vsftpd/vsftpd_conf.html
|
||||||
func (f *Fs) Precision() time.Duration {
|
func (f *Fs) Precision() time.Duration {
|
||||||
|
if (f.fGetTime || f.fLstTime) && f.fSetTime {
|
||||||
|
return time.Second
|
||||||
|
}
|
||||||
return fs.ModTimeNotSupported
|
return fs.ModTimeNotSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -776,6 +805,7 @@ func (f *Fs) getInfo(ctx context.Context, remote string) (fi *FileInfo, err erro
|
|||||||
Name: remote,
|
Name: remote,
|
||||||
Size: file.Size,
|
Size: file.Size,
|
||||||
ModTime: file.Time,
|
ModTime: file.Time,
|
||||||
|
precise: f.fLstTime,
|
||||||
IsDir: file.Type == ftp.EntryTypeFolder,
|
IsDir: file.Type == ftp.EntryTypeFolder,
|
||||||
}
|
}
|
||||||
return info, nil
|
return info, nil
|
||||||
@ -961,12 +991,41 @@ func (o *Object) Size() int64 {
|
|||||||
|
|
||||||
// ModTime returns the modification time of the object
|
// ModTime returns the modification time of the object
|
||||||
func (o *Object) ModTime(ctx context.Context) time.Time {
|
func (o *Object) ModTime(ctx context.Context) time.Time {
|
||||||
|
if !o.info.precise && o.fs.fGetTime {
|
||||||
|
c, err := o.fs.getFtpConnection(ctx)
|
||||||
|
if err == nil {
|
||||||
|
path := path.Join(o.fs.root, o.remote)
|
||||||
|
path = o.fs.opt.Enc.FromStandardPath(path)
|
||||||
|
modTime, err := c.GetTime(path)
|
||||||
|
if err == nil && o.info != nil {
|
||||||
|
o.info.ModTime = modTime
|
||||||
|
o.info.precise = true
|
||||||
|
}
|
||||||
|
o.fs.putFtpConnection(&c, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
return o.info.ModTime
|
return o.info.ModTime
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetModTime sets the modification time of the object
|
// SetModTime sets the modification time of the object
|
||||||
func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
|
func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
|
||||||
return nil
|
if !o.fs.fSetTime {
|
||||||
|
fs.Errorf(o.fs, "SetModTime is not supported")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c, err := o.fs.getFtpConnection(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
path := path.Join(o.fs.root, o.remote)
|
||||||
|
path = o.fs.opt.Enc.FromStandardPath(path)
|
||||||
|
err = c.SetTime(path, modTime.In(time.UTC))
|
||||||
|
if err == nil && o.info != nil {
|
||||||
|
o.info.ModTime = modTime
|
||||||
|
o.info.precise = true
|
||||||
|
}
|
||||||
|
o.fs.putFtpConnection(&c, err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Storable returns a boolean as to whether this object is storable
|
// Storable returns a boolean as to whether this object is storable
|
||||||
@ -1108,6 +1167,9 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
return errors.Wrap(err, "update stor")
|
return errors.Wrap(err, "update stor")
|
||||||
}
|
}
|
||||||
o.fs.putFtpConnection(&c, nil)
|
o.fs.putFtpConnection(&c, nil)
|
||||||
|
if err = o.SetModTime(ctx, src.ModTime(ctx)); err != nil {
|
||||||
|
return errors.Wrap(err, "SetModTime")
|
||||||
|
}
|
||||||
o.info, err = o.fs.getInfo(ctx, path)
|
o.info, err = o.fs.getInfo(ctx, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "update getinfo")
|
return errors.Wrap(err, "update getinfo")
|
||||||
|
@ -90,9 +90,26 @@ func (f *Fs) testUploadTimeout(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rclone must support precise time with ProFtpd and PureFtpd out of the box.
|
||||||
|
// The VsFtpd server does not support the MFMT command to set file time like
|
||||||
|
// other servers but by default supports the MDTM command in the non-standard
|
||||||
|
// two-argument form for the same purpose.
|
||||||
|
// See "mdtm_write" in https://security.appspot.com/vsftpd/vsftpd_conf.html
|
||||||
|
func (f *Fs) testTimePrecision(t *testing.T) {
|
||||||
|
name := f.Name()
|
||||||
|
if pos := strings.Index(name, "{"); pos != -1 {
|
||||||
|
name = name[:pos]
|
||||||
|
}
|
||||||
|
switch name {
|
||||||
|
case "TestFTPProftpd", "TestFTPPureftpd", "TestFTPVsftpd":
|
||||||
|
assert.LessOrEqual(t, f.Precision(), time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// InternalTest dispatches all internal tests
|
// InternalTest dispatches all internal tests
|
||||||
func (f *Fs) InternalTest(t *testing.T) {
|
func (f *Fs) InternalTest(t *testing.T) {
|
||||||
t.Run("UploadTimeout", f.testUploadTimeout)
|
t.Run("UploadTimeout", f.testUploadTimeout)
|
||||||
|
t.Run("TimePrecision", f.testTimePrecision)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ fstests.InternalTester = (*Fs)(nil)
|
var _ fstests.InternalTester = (*Fs)(nil)
|
||||||
|
@ -246,6 +246,15 @@ Disable using MLSD even if server advertises support
|
|||||||
- Type: bool
|
- Type: bool
|
||||||
- Default: false
|
- Default: false
|
||||||
|
|
||||||
|
#### --ftp-writing-mdtm
|
||||||
|
|
||||||
|
Use MDTM to set modification time (VsFtpd quirk)
|
||||||
|
|
||||||
|
- Config: writing_mdtm
|
||||||
|
- Env Var: RCLONE_FTP_WRITING_MDTM
|
||||||
|
- Type: bool
|
||||||
|
- Default: false
|
||||||
|
|
||||||
#### --ftp-idle-timeout
|
#### --ftp-idle-timeout
|
||||||
|
|
||||||
Max time before closing idle connections
|
Max time before closing idle connections
|
||||||
@ -298,9 +307,6 @@ Rclone's FTP implementation is not compatible with `active` mode
|
|||||||
as [the library it uses doesn't support it](https://github.com/jlaffaye/ftp/issues/29).
|
as [the library it uses doesn't support it](https://github.com/jlaffaye/ftp/issues/29).
|
||||||
This will likely never be supported due to security concerns.
|
This will likely never be supported due to security concerns.
|
||||||
|
|
||||||
Modified times are not supported. Times you see on the FTP server
|
|
||||||
through rclone are those of upload.
|
|
||||||
|
|
||||||
Rclone's FTP backend does not support any checksums but can compare
|
Rclone's FTP backend does not support any checksums but can compare
|
||||||
file sizes.
|
file sizes.
|
||||||
|
|
||||||
@ -324,3 +330,23 @@ Rclone's FTP backend could support server-side move but does not
|
|||||||
at present.
|
at present.
|
||||||
|
|
||||||
The `ftp_proxy` environment variable is not currently supported.
|
The `ftp_proxy` environment variable is not currently supported.
|
||||||
|
|
||||||
|
#### Modified time
|
||||||
|
|
||||||
|
File modification time (timestamps) is supported to 1 second resolution
|
||||||
|
for major FTP servers: ProFTPd, PureFTPd, VsFTPd, and FileZilla FTP server.
|
||||||
|
The `VsFTPd` server has non-standard implementation of time related protocol
|
||||||
|
commands and needs a special configuration setting: `writing_mdtm = true`.
|
||||||
|
|
||||||
|
Support for precise file time with other FTP servers varies depending on what
|
||||||
|
protocol extensions they advertise. If all the `MLSD`, `MDTM` and `MFTM`
|
||||||
|
extensions are present, rclone will use them together to provide precise time.
|
||||||
|
Otherwise the times you see on the FTP server through rclone are those of the
|
||||||
|
last file upload.
|
||||||
|
|
||||||
|
You can use the following command to check whether rclone can use precise time
|
||||||
|
with your FTP server: `rclone backend features your_ftp_remote:` (the trailing
|
||||||
|
colon is important). Look for the number in the line tagged by `Precision`
|
||||||
|
designating the remote time precision expressed as nanoseconds. A value of
|
||||||
|
`1000000000` means that file time precision of 1 second is available.
|
||||||
|
A value of `3153600000000000000` (or another large number) means "unsupported".
|
||||||
|
@ -18,6 +18,7 @@ start() {
|
|||||||
echo host=$(docker_ip)
|
echo host=$(docker_ip)
|
||||||
echo user=$USER
|
echo user=$USER
|
||||||
echo pass=$(rclone obscure $PASS)
|
echo pass=$(rclone obscure $PASS)
|
||||||
|
echo writing_mdtm=true
|
||||||
echo encoding=Ctl,LeftPeriod,Slash
|
echo encoding=Ctl,LeftPeriod,Slash
|
||||||
echo _connect=$(docker_ip):21
|
echo _connect=$(docker_ip):21
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user