drive: implement modtime and metadata setting for directories

This commit is contained in:
Nick Craig-Wood 2024-02-07 21:17:33 +00:00
parent a60da2ef38
commit e4d0055b3e
2 changed files with 157 additions and 22 deletions

View File

@ -287,7 +287,10 @@ func init() {
}, },
MetadataInfo: &fs.MetadataInfo{ MetadataInfo: &fs.MetadataInfo{
System: systemMetadataInfo, System: systemMetadataInfo,
Help: `User metadata is stored in the properties field of the drive object.`, Help: `User metadata is stored in the properties field of the drive object.
Metadata is supported on files and directories.
`,
}, },
Options: append(driveOAuthOptions(), []fs.Option{{ Options: append(driveOAuthOptions(), []fs.Option{{
Name: "scope", Name: "scope",
@ -870,6 +873,11 @@ type Object struct {
v2Download bool // generate v2 download link ondemand v2Download bool // generate v2 download link ondemand
} }
// Directory describes a drive directory
type Directory struct {
baseObject
}
// ------------------------------------------------------------ // ------------------------------------------------------------
// Name of the remote (as passed into NewFs) // Name of the remote (as passed into NewFs)
@ -1374,15 +1382,20 @@ func newFs(ctx context.Context, name, path string, m configmap.Mapper) (*Fs, err
} }
f.isTeamDrive = opt.TeamDriveID != "" f.isTeamDrive = opt.TeamDriveID != ""
f.features = (&fs.Features{ f.features = (&fs.Features{
DuplicateFiles: true, DuplicateFiles: true,
ReadMimeType: true, ReadMimeType: true,
WriteMimeType: true, WriteMimeType: true,
CanHaveEmptyDirectories: true, CanHaveEmptyDirectories: true,
ServerSideAcrossConfigs: opt.ServerSideAcrossConfigs, ServerSideAcrossConfigs: opt.ServerSideAcrossConfigs,
FilterAware: true, FilterAware: true,
ReadMetadata: true, ReadMetadata: true,
WriteMetadata: true, WriteMetadata: true,
UserMetadata: true, UserMetadata: true,
ReadDirMetadata: true,
WriteDirMetadata: true,
WriteDirSetModTime: true,
UserDirMetadata: true,
DirModTimeUpdatesOnWrite: false, // FIXME need to check!
}).Fill(ctx, f) }).Fill(ctx, f)
// Create a new authorized Drive client. // Create a new authorized Drive client.
@ -1729,11 +1742,9 @@ func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut strin
return pathIDOut, found, err return pathIDOut, found, err
} }
// CreateDir makes a directory with pathID as parent and name leaf // createDir makes a directory with pathID as parent and name leaf with optional metadata
func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string, err error) { func (f *Fs) createDir(ctx context.Context, pathID, leaf string, metadata fs.Metadata) (info *drive.File, err error) {
leaf = f.opt.Enc.FromStandardName(leaf) leaf = f.opt.Enc.FromStandardName(leaf)
// fmt.Println("Making", path)
// Define the metadata for the directory we are going to create.
pathID = actualID(pathID) pathID = actualID(pathID)
createInfo := &drive.File{ createInfo := &drive.File{
Name: leaf, Name: leaf,
@ -1741,14 +1752,63 @@ func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string,
MimeType: driveFolderType, MimeType: driveFolderType,
Parents: []string{pathID}, Parents: []string{pathID},
} }
var info *drive.File var updateMetadata updateMetadataFn
if len(metadata) > 0 {
updateMetadata, err = f.updateMetadata(ctx, createInfo, metadata, true)
if err != nil {
return nil, fmt.Errorf("create dir: failed to update metadata: %w", err)
}
}
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
info, err = f.svc.Files.Create(createInfo). info, err = f.svc.Files.Create(createInfo).
Fields("id"). Fields(f.getFileFields(ctx)).
SupportsAllDrives(true). SupportsAllDrives(true).
Context(ctx).Do() Context(ctx).Do()
return f.shouldRetry(ctx, err) return f.shouldRetry(ctx, err)
}) })
if err != nil {
return nil, err
}
if updateMetadata != nil {
err = updateMetadata(ctx, info)
if err != nil {
return nil, err
}
}
return info, nil
}
// updateDir updates an existing a directory with the metadata passed in
func (f *Fs) updateDir(ctx context.Context, dirID string, metadata fs.Metadata) (info *drive.File, err error) {
if len(metadata) == 0 {
return f.getFile(ctx, dirID, f.getFileFields(ctx))
}
dirID = actualID(dirID)
updateInfo := &drive.File{}
updateMetadata, err := f.updateMetadata(ctx, updateInfo, metadata, true)
if err != nil {
return nil, fmt.Errorf("update dir: failed to update metadata from source object: %w", err)
}
err = f.pacer.Call(func() (bool, error) {
info, err = f.svc.Files.Update(dirID, updateInfo).
Fields(f.getFileFields(ctx)).
SupportsAllDrives(true).
Context(ctx).Do()
return f.shouldRetry(ctx, err)
})
if err != nil {
return nil, err
}
err = updateMetadata(ctx, info)
if err != nil {
return nil, err
}
return info, nil
}
// CreateDir makes a directory with pathID as parent and name leaf
func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string, err error) {
info, err := f.createDir(ctx, pathID, leaf, nil)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -2161,7 +2221,7 @@ func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (
// Send the entry to the caller, queueing any directories as new jobs // Send the entry to the caller, queueing any directories as new jobs
cb := func(entry fs.DirEntry) error { cb := func(entry fs.DirEntry) error {
if d, isDir := entry.(*fs.Dir); isDir { if d, isDir := entry.(fs.Directory); isDir {
job := listREntry{actualID(d.ID()), d.Remote()} job := listREntry{actualID(d.ID()), d.Remote()}
sendJob(job) sendJob(job)
} }
@ -2338,11 +2398,11 @@ func (f *Fs) itemToDirEntry(ctx context.Context, remote string, item *drive.File
if item.ResourceKey != "" { if item.ResourceKey != "" {
f.dirResourceKeys.Store(item.Id, item.ResourceKey) f.dirResourceKeys.Store(item.Id, item.ResourceKey)
} }
when, _ := time.Parse(timeFormatIn, item.ModifiedTime) baseObject, err := f.newBaseObject(ctx, remote, item)
d := fs.NewDir(remote, when).SetID(item.Id) if err != nil {
if len(item.Parents) > 0 { return nil, err
d.SetParentID(item.Parents[0])
} }
d := &Directory{baseObject: baseObject}
return d, nil return d, nil
case f.opt.AuthOwnerOnly && !isAuthOwned(item): case f.opt.AuthOwnerOnly && !isAuthOwned(item):
// ignore object // ignore object
@ -2535,6 +2595,45 @@ func (f *Fs) Mkdir(ctx context.Context, dir string) error {
return err return err
} }
// 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) {
var info *drive.File
dirID, err := f.dirCache.FindDir(ctx, dir, false)
if err == fs.ErrorDirNotFound {
// Directory does not exist so create it
var leaf, parentID string
leaf, parentID, err = f.dirCache.FindPath(ctx, dir, true)
if err != nil {
return nil, err
}
info, err = f.createDir(ctx, parentID, leaf, metadata)
} else if err == nil {
// Directory exists and needs updating
info, err = f.updateDir(ctx, dirID, metadata)
}
if err != nil {
return nil, err
}
// Convert the info into a directory entry
entry, err := f.itemToDirEntry(ctx, dir, info)
if err != nil {
return nil, err
}
dirEntry, ok := entry.(fs.Directory)
if !ok {
return nil, fmt.Errorf("internal error: expecting %T to be an fs.Directory", entry)
}
return dirEntry, nil
}
// DirSetModTime sets the directory modtime for dir // DirSetModTime sets the directory modtime for dir
func (f *Fs) DirSetModTime(ctx context.Context, dir string, modTime time.Time) error { func (f *Fs) DirSetModTime(ctx context.Context, dir string, modTime time.Time) error {
dirID, err := f.dirCache.FindDir(ctx, dir, false) dirID, err := f.dirCache.FindDir(ctx, dir, false)
@ -4207,6 +4306,37 @@ func (o *linkObject) ext() string {
return o.baseObject.remote[len(o.baseObject.remote)-o.extLen:] return o.baseObject.remote[len(o.baseObject.remote)-o.extLen:]
} }
// 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
}
// 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 {
info, err := d.fs.updateDir(ctx, d.id, metadata)
if err != nil {
return fmt.Errorf("failed to update directory info: %w", err)
}
// Update directory from info returned
baseObject, err := d.fs.newBaseObject(ctx, d.remote, info)
if err != nil {
return fmt.Errorf("failed to process directory info: %w", err)
}
d.baseObject = baseObject
return err
}
// 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
}
// templates for document link files // templates for document link files
const ( const (
urlTemplate = `[InternetShortcut]{{"\r"}} urlTemplate = `[InternetShortcut]{{"\r"}}
@ -4257,6 +4387,7 @@ var (
_ fs.ListRer = (*Fs)(nil) _ fs.ListRer = (*Fs)(nil)
_ fs.MergeDirser = (*Fs)(nil) _ fs.MergeDirser = (*Fs)(nil)
_ fs.DirSetModTimer = (*Fs)(nil) _ fs.DirSetModTimer = (*Fs)(nil)
_ fs.MkdirMetadataer = (*Fs)(nil)
_ fs.Abouter = (*Fs)(nil) _ fs.Abouter = (*Fs)(nil)
_ fs.Object = (*Object)(nil) _ fs.Object = (*Object)(nil)
_ fs.MimeTyper = (*Object)(nil) _ fs.MimeTyper = (*Object)(nil)
@ -4271,4 +4402,8 @@ var (
_ fs.MimeTyper = (*linkObject)(nil) _ fs.MimeTyper = (*linkObject)(nil)
_ fs.IDer = (*linkObject)(nil) _ fs.IDer = (*linkObject)(nil)
_ fs.ParentIDer = (*linkObject)(nil) _ fs.ParentIDer = (*linkObject)(nil)
_ fs.Directory = (*Directory)(nil)
_ fs.SetModTimer = (*Directory)(nil)
_ fs.SetMetadataer = (*Directory)(nil)
_ fs.ParentIDer = (*Directory)(nil)
) )

View File

@ -26,7 +26,7 @@ Here is an overview of the major features of each cloud storage system.
| Enterprise File Fabric | - | R/W | Yes | No | R/W | - | | Enterprise File Fabric | - | R/W | Yes | No | R/W | - |
| FTP | - | R/W ¹⁰ | No | No | - | - | | FTP | - | R/W ¹⁰ | No | No | - | - |
| Google Cloud Storage | MD5 | R/W | No | No | R/W | - | | Google Cloud Storage | MD5 | R/W | No | No | R/W | - |
| Google Drive | MD5, SHA1, SHA256 | R/W | No | Yes | R/W | - | | Google Drive | MD5, SHA1, SHA256 | DR/W | No | Yes | R/W | DRWU |
| Google Photos | - | - | No | Yes | R | - | | Google Photos | - | - | No | Yes | R | - |
| HDFS | - | R/W | No | No | - | - | | HDFS | - | R/W | No | No | - | - |
| HiDrive | HiDrive ¹² | R/W | No | No | - | - | | HiDrive | HiDrive ¹² | R/W | No | No | - | - |