mirror of
https://github.com/rclone/rclone.git
synced 2025-01-22 06:09:21 +01:00
local: implement modtime and metadata for directories
A consequence of this is that fs.Directory returned by the local backend will now have a correct size in (rather than -1). Some tests depended on this and have been fixed by this commit too.
This commit is contained in:
parent
39db8caff1
commit
7b01564f83
@ -81,10 +81,12 @@ func TestNewFS(t *testing.T) {
|
|||||||
for i, gotEntry := range gotEntries {
|
for i, gotEntry := range gotEntries {
|
||||||
what := fmt.Sprintf("%s, entry=%d", what, i)
|
what := fmt.Sprintf("%s, entry=%d", what, i)
|
||||||
wantEntry := test.entries[i]
|
wantEntry := test.entries[i]
|
||||||
|
_, isDir := gotEntry.(fs.Directory)
|
||||||
|
|
||||||
require.Equal(t, wantEntry.remote, gotEntry.Remote(), what)
|
require.Equal(t, wantEntry.remote, gotEntry.Remote(), what)
|
||||||
require.Equal(t, wantEntry.size, gotEntry.Size(), what)
|
if !isDir {
|
||||||
_, isDir := gotEntry.(fs.Directory)
|
require.Equal(t, wantEntry.size, gotEntry.Size(), what)
|
||||||
|
}
|
||||||
require.Equal(t, wantEntry.isDir, isDir, what)
|
require.Equal(t, wantEntry.isDir, isDir, what)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,8 @@ netbsd, macOS and Solaris. It is **not** supported on Windows yet
|
|||||||
|
|
||||||
User metadata is stored as extended attributes (which may not be
|
User metadata is stored as extended attributes (which may not be
|
||||||
supported by all file systems) under the "user.*" prefix.
|
supported by all file systems) under the "user.*" prefix.
|
||||||
|
|
||||||
|
Metadata is supported on files and directories.
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
Options: []fs.Option{{
|
Options: []fs.Option{{
|
||||||
@ -270,6 +272,11 @@ type Object struct {
|
|||||||
translatedLink bool // Is this object a translated link
|
translatedLink bool // Is this object a translated link
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Directory represents a local filesystem directory
|
||||||
|
type Directory struct {
|
||||||
|
Object
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -301,15 +308,20 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
|||||||
}
|
}
|
||||||
f.root = cleanRootPath(root, f.opt.NoUNC, f.opt.Enc)
|
f.root = cleanRootPath(root, f.opt.NoUNC, f.opt.Enc)
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
CaseInsensitive: f.caseInsensitive(),
|
CaseInsensitive: f.caseInsensitive(),
|
||||||
CanHaveEmptyDirectories: true,
|
CanHaveEmptyDirectories: true,
|
||||||
IsLocal: true,
|
IsLocal: true,
|
||||||
SlowHash: true,
|
SlowHash: true,
|
||||||
ReadMetadata: true,
|
ReadMetadata: true,
|
||||||
WriteMetadata: true,
|
WriteMetadata: true,
|
||||||
UserMetadata: xattrSupported, // can only R/W general purpose metadata if xattrs are supported
|
ReadDirMetadata: true,
|
||||||
FilterAware: true,
|
WriteDirMetadata: true,
|
||||||
PartialUploads: true,
|
WriteDirSetModTime: true,
|
||||||
|
UserDirMetadata: xattrSupported, // can only R/W general purpose metadata if xattrs are supported
|
||||||
|
DirModTimeUpdatesOnWrite: true,
|
||||||
|
UserMetadata: xattrSupported, // can only R/W general purpose metadata if xattrs are supported
|
||||||
|
FilterAware: true,
|
||||||
|
PartialUploads: true,
|
||||||
}).Fill(ctx, f)
|
}).Fill(ctx, f)
|
||||||
if opt.FollowSymlinks {
|
if opt.FollowSymlinks {
|
||||||
f.lstat = os.Stat
|
f.lstat = os.Stat
|
||||||
@ -453,6 +465,15 @@ func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
|||||||
return f.newObjectWithInfo(remote, nil)
|
return f.newObjectWithInfo(remote, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create new directory object from the info passed in
|
||||||
|
func (f *Fs) newDirectory(dir string, fi os.FileInfo) *Directory {
|
||||||
|
o := f.newObject(dir)
|
||||||
|
o.setMetadata(fi)
|
||||||
|
return &Directory{
|
||||||
|
Object: *o,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// List the objects and directories in dir into entries. The
|
// List the objects and directories in dir into entries. The
|
||||||
// entries can be returned in any order but should be for a
|
// entries can be returned in any order but should be for a
|
||||||
// complete directory.
|
// complete directory.
|
||||||
@ -563,7 +584,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
|||||||
// Ignore directories which are symlinks. These are junction points under windows which
|
// Ignore directories which are symlinks. These are junction points under windows which
|
||||||
// are kind of a souped up symlink. Unix doesn't have directories which are symlinks.
|
// are kind of a souped up symlink. Unix doesn't have directories which are symlinks.
|
||||||
if (mode&os.ModeSymlink) == 0 && f.dev == readDevice(fi, f.opt.OneFileSystem) {
|
if (mode&os.ModeSymlink) == 0 && f.dev == readDevice(fi, f.opt.OneFileSystem) {
|
||||||
d := fs.NewDir(newRemote, fi.ModTime())
|
d := f.newDirectory(newRemote, fi)
|
||||||
entries = append(entries, d)
|
entries = append(entries, d)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -653,6 +674,48 @@ func (f *Fs) DirSetModTime(ctx context.Context, dir string, modTime time.Time) e
|
|||||||
return o.SetModTime(ctx, modTime)
|
return o.SetModTime(ctx, modTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MkdirMetadata makes the directory passed in as dir.
|
||||||
|
//
|
||||||
|
// It shouldn't return an error if it already exists.
|
||||||
|
//
|
||||||
|
// If the metadata is not nil it is set.
|
||||||
|
//
|
||||||
|
// It returns the directory that was created.
|
||||||
|
func (f *Fs) MkdirMetadata(ctx context.Context, dir string, metadata fs.Metadata) (fs.Directory, error) {
|
||||||
|
// Find and or create the directory
|
||||||
|
localPath := f.localPath(dir)
|
||||||
|
fi, err := f.lstat(localPath)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
err := f.Mkdir(ctx, dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("mkdir metadata: failed make directory: %w", err)
|
||||||
|
}
|
||||||
|
fi, err = f.lstat(localPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("mkdir metadata: failed to read info: %w", err)
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create directory object
|
||||||
|
d := f.newDirectory(dir, fi)
|
||||||
|
|
||||||
|
// Set metadata on the directory object if provided
|
||||||
|
if metadata != nil {
|
||||||
|
err = d.writeMetadata(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to set metadata on directory: %w", err)
|
||||||
|
}
|
||||||
|
// Re-read info now we have finished setting stuff
|
||||||
|
err = d.lstat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("mkdir metadata: failed to re-read info: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Rmdir removes the directory
|
// Rmdir removes the directory
|
||||||
//
|
//
|
||||||
// If it isn't empty it will return an error
|
// If it isn't empty it will return an error
|
||||||
@ -1473,16 +1536,52 @@ func cleanRootPath(s string, noUNC bool, enc encoder.MultiEncoder) string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Items returns the count of items in this directory or this
|
||||||
|
// directory and subdirectories if known, -1 for unknown
|
||||||
|
func (d *Directory) Items() int64 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns the internal ID of this directory if known, or
|
||||||
|
// "" otherwise
|
||||||
|
func (d *Directory) ID() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMetadata sets metadata for a Directory
|
||||||
|
//
|
||||||
|
// It should return fs.ErrorNotImplemented if it can't set metadata
|
||||||
|
func (d *Directory) SetMetadata(ctx context.Context, metadata fs.Metadata) error {
|
||||||
|
err := d.writeMetadata(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SetMetadata failed on Directory: %w", err)
|
||||||
|
}
|
||||||
|
// Re-read info now we have finished setting stuff
|
||||||
|
return d.lstat()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash does nothing on a directory
|
||||||
|
//
|
||||||
|
// This method is implemented with the incorrect type signature to
|
||||||
|
// stop the Directory type asserting to fs.Object or fs.ObjectInfo
|
||||||
|
func (d *Directory) Hash() {
|
||||||
|
// Does nothing
|
||||||
|
}
|
||||||
|
|
||||||
// Check the interfaces are satisfied
|
// Check the interfaces are satisfied
|
||||||
var (
|
var (
|
||||||
_ fs.Fs = &Fs{}
|
_ fs.Fs = &Fs{}
|
||||||
_ fs.Purger = &Fs{}
|
_ fs.Purger = &Fs{}
|
||||||
_ fs.PutStreamer = &Fs{}
|
_ fs.PutStreamer = &Fs{}
|
||||||
_ fs.Mover = &Fs{}
|
_ fs.Mover = &Fs{}
|
||||||
_ fs.DirMover = &Fs{}
|
_ fs.DirMover = &Fs{}
|
||||||
_ fs.Commander = &Fs{}
|
_ fs.Commander = &Fs{}
|
||||||
_ fs.OpenWriterAter = &Fs{}
|
_ fs.OpenWriterAter = &Fs{}
|
||||||
_ fs.DirSetModTimer = &Fs{}
|
_ fs.DirSetModTimer = &Fs{}
|
||||||
_ fs.Object = &Object{}
|
_ fs.MkdirMetadataer = &Fs{}
|
||||||
_ fs.Metadataer = &Object{}
|
_ fs.Object = &Object{}
|
||||||
|
_ fs.Metadataer = &Object{}
|
||||||
|
_ fs.Directory = &Directory{}
|
||||||
|
_ fs.SetModTimer = &Directory{}
|
||||||
|
_ fs.SetMetadataer = &Directory{}
|
||||||
)
|
)
|
||||||
|
@ -231,6 +231,10 @@ func Lsf(ctx context.Context, fsrc fs.Fs, out io.Writer) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return operations.ListJSON(ctx, fsrc, "", &opt, func(item *operations.ListJSONItem) error {
|
return operations.ListJSON(ctx, fsrc, "", &opt, func(item *operations.ListJSONItem) error {
|
||||||
|
// Make size deterministic for tests
|
||||||
|
if item.IsDir {
|
||||||
|
item.Size = -1
|
||||||
|
}
|
||||||
_, _ = fmt.Fprintln(out, list.Format(item))
|
_, _ = fmt.Fprintln(out, list.Format(item))
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
@ -61,7 +61,7 @@ Here is an overview of the major features of each cloud storage system.
|
|||||||
| WebDAV | MD5, SHA1 ³ | R ⁴ | Depends | No | - | - |
|
| WebDAV | MD5, SHA1 ³ | R ⁴ | Depends | No | - | - |
|
||||||
| Yandex Disk | MD5 | R/W | No | No | R | - |
|
| Yandex Disk | MD5 | R/W | No | No | R | - |
|
||||||
| Zoho WorkDrive | - | - | No | No | - | - |
|
| Zoho WorkDrive | - | - | No | No | - | - |
|
||||||
| The local filesystem | All | R/W | Depends | No | - | RWU |
|
| The local filesystem | All | DR/W | Depends | No | - | DRWU |
|
||||||
|
|
||||||
¹ Dropbox supports [its own custom
|
¹ Dropbox supports [its own custom
|
||||||
hash](https://www.dropbox.com/developers/reference/content-hash).
|
hash](https://www.dropbox.com/developers/reference/content-hash).
|
||||||
@ -115,13 +115,21 @@ systems they must support a common hash type.
|
|||||||
Almost all cloud storage systems store some sort of timestamp
|
Almost all cloud storage systems store some sort of timestamp
|
||||||
on objects, but several of them not something that is appropriate
|
on objects, but several of them not something that is appropriate
|
||||||
to use for syncing. E.g. some backends will only write a timestamp
|
to use for syncing. E.g. some backends will only write a timestamp
|
||||||
that represent the time of the upload. To be relevant for syncing
|
that represents the time of the upload. To be relevant for syncing
|
||||||
it should be able to store the modification time of the source
|
it should be able to store the modification time of the source
|
||||||
object. If this is not the case, rclone will only check the file
|
object. If this is not the case, rclone will only check the file
|
||||||
size by default, though can be configured to check the file hash
|
size by default, though can be configured to check the file hash
|
||||||
(with the `--checksum` flag). Ideally it should also be possible to
|
(with the `--checksum` flag). Ideally it should also be possible to
|
||||||
change the timestamp of an existing file without having to re-upload it.
|
change the timestamp of an existing file without having to re-upload it.
|
||||||
|
|
||||||
|
| Key | Explanation |
|
||||||
|
|-----|-------------|
|
||||||
|
| `-` | ModTimes not supported - times likely the upload time |
|
||||||
|
| `R` | ModTimes supported on files but can't be changed without re-upload |
|
||||||
|
| `R/W` | Read and Write ModTimes fully supported on files |
|
||||||
|
| `DR` | ModTimes supported on files and directories but can't be changed without re-upload |
|
||||||
|
| `DR/W` | Read and Write ModTimes fully supported on files and directories |
|
||||||
|
|
||||||
Storage systems with a `-` in the ModTime column, means the
|
Storage systems with a `-` in the ModTime column, means the
|
||||||
modification read on objects is not the modification time of the
|
modification read on objects is not the modification time of the
|
||||||
file when uploaded. It is most likely the time the file was uploaded,
|
file when uploaded. It is most likely the time the file was uploaded,
|
||||||
@ -143,6 +151,9 @@ in a `mount` will be silently ignored.
|
|||||||
Storage systems with `R/W` (for read/write) in the ModTime column,
|
Storage systems with `R/W` (for read/write) in the ModTime column,
|
||||||
means they do also support modtime-only operations.
|
means they do also support modtime-only operations.
|
||||||
|
|
||||||
|
Storage systems with `D` in the ModTime column means that the
|
||||||
|
following symbols apply to directories as well as files.
|
||||||
|
|
||||||
### Case Insensitive ###
|
### Case Insensitive ###
|
||||||
|
|
||||||
If a cloud storage systems is case sensitive then it is possible to
|
If a cloud storage systems is case sensitive then it is possible to
|
||||||
@ -455,9 +466,12 @@ The levels of metadata support are
|
|||||||
|
|
||||||
| Key | Explanation |
|
| Key | Explanation |
|
||||||
|-----|-------------|
|
|-----|-------------|
|
||||||
| `R` | Read only System Metadata |
|
| `R` | Read only System Metadata on files only|
|
||||||
| `RW` | Read and write System Metadata |
|
| `RW` | Read and write System Metadata on files only|
|
||||||
| `RWU` | Read and write System Metadata and read and write User Metadata |
|
| `RWU` | Read and write System Metadata and read and write User Metadata on files only|
|
||||||
|
| `DR` | Read only System Metadata on files and directories |
|
||||||
|
| `DRW` | Read and write System Metadata on files and directories|
|
||||||
|
| `DRWU` | Read and write System Metadata and read and write User Metadata on files and directories |
|
||||||
|
|
||||||
See [the metadata docs](/docs/#metadata) for more info.
|
See [the metadata docs](/docs/#metadata) for more info.
|
||||||
|
|
||||||
|
@ -225,7 +225,7 @@ func TestRcList(t *testing.T) {
|
|||||||
checkSubdir := func(got *operations.ListJSONItem) {
|
checkSubdir := func(got *operations.ListJSONItem) {
|
||||||
assert.Equal(t, "subdir", got.Path)
|
assert.Equal(t, "subdir", got.Path)
|
||||||
assert.Equal(t, "subdir", got.Name)
|
assert.Equal(t, "subdir", got.Name)
|
||||||
assert.Equal(t, int64(-1), got.Size)
|
// assert.Equal(t, int64(-1), got.Size) // size can vary for directories
|
||||||
assert.Equal(t, "inode/directory", got.MimeType)
|
assert.Equal(t, "inode/directory", got.MimeType)
|
||||||
assert.Equal(t, true, got.IsDir)
|
assert.Equal(t, true, got.IsDir)
|
||||||
}
|
}
|
||||||
@ -298,7 +298,7 @@ func TestRcStat(t *testing.T) {
|
|||||||
stat := fetch(t, "subdir")
|
stat := fetch(t, "subdir")
|
||||||
assert.Equal(t, "subdir", stat.Path)
|
assert.Equal(t, "subdir", stat.Path)
|
||||||
assert.Equal(t, "subdir", stat.Name)
|
assert.Equal(t, "subdir", stat.Name)
|
||||||
assert.Equal(t, int64(-1), stat.Size)
|
// assert.Equal(t, int64(-1), stat.Size) // size can vary for directories
|
||||||
assert.Equal(t, "inode/directory", stat.MimeType)
|
assert.Equal(t, "inode/directory", stat.MimeType)
|
||||||
assert.Equal(t, true, stat.IsDir)
|
assert.Equal(t, true, stat.IsDir)
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user