mirror of
https://github.com/rclone/rclone.git
synced 2025-12-15 15:53:41 +00:00
drive: add read/write metadata support
- fetch metadata with listings and fetch permissions in parallel
- only write permissions out if they are not inherited.
- make setting labels, owner and permissions work controlled by flags
- `--drive-metadata-labels`, `--drive-metadata-owner`, `--drive-metadata-permissions`
This commit is contained in:
@@ -143,6 +143,41 @@ var (
|
||||
_linkTemplates map[string]*template.Template // available link types
|
||||
)
|
||||
|
||||
// rwChoices type for fs.Bits
|
||||
type rwChoices struct{}
|
||||
|
||||
func (rwChoices) Choices() []fs.BitsChoicesInfo {
|
||||
return []fs.BitsChoicesInfo{
|
||||
{Bit: uint64(rwOff), Name: "off"},
|
||||
{Bit: uint64(rwRead), Name: "read"},
|
||||
{Bit: uint64(rwWrite), Name: "write"},
|
||||
}
|
||||
}
|
||||
|
||||
// rwChoice type alias
|
||||
type rwChoice = fs.Bits[rwChoices]
|
||||
|
||||
const (
|
||||
rwRead rwChoice = 1 << iota
|
||||
rwWrite
|
||||
rwOff rwChoice = 0
|
||||
)
|
||||
|
||||
// Examples for the options
|
||||
var rwExamples = fs.OptionExamples{{
|
||||
Value: rwOff.String(),
|
||||
Help: "Do not read or write the value",
|
||||
}, {
|
||||
Value: rwRead.String(),
|
||||
Help: "Read the value only",
|
||||
}, {
|
||||
Value: rwWrite.String(),
|
||||
Help: "Write the value only",
|
||||
}, {
|
||||
Value: (rwRead | rwWrite).String(),
|
||||
Help: "Read and Write the value.",
|
||||
}}
|
||||
|
||||
// Parse the scopes option returning a slice of scopes
|
||||
func driveScopes(scopesString string) (scopes []string) {
|
||||
if scopesString == "" {
|
||||
@@ -250,6 +285,10 @@ func init() {
|
||||
}
|
||||
return nil, fmt.Errorf("unknown state %q", config.State)
|
||||
},
|
||||
MetadataInfo: &fs.MetadataInfo{
|
||||
System: systemMetadataInfo,
|
||||
Help: `User metadata is stored in the properties field of the drive object.`,
|
||||
},
|
||||
Options: append(driveOAuthOptions(), []fs.Option{{
|
||||
Name: "scope",
|
||||
Help: "Comma separated list of scopes that rclone should use when requesting access from drive.",
|
||||
@@ -639,6 +678,56 @@ having trouble with like many empty directories.
|
||||
`,
|
||||
Advanced: true,
|
||||
Default: true,
|
||||
}, {
|
||||
Name: "metadata_owner",
|
||||
Help: `Control whether owner should be read or written in metadata.
|
||||
|
||||
Owner is a standard part of the file metadata so is easy to read. But it
|
||||
isn't always desirable to set the owner from the metadata.
|
||||
|
||||
Note that you can't set the owner on Shared Drives, and that setting
|
||||
ownership will generate an email to the new owner (this can't be
|
||||
disabled), and you can't transfer ownership to someone outside your
|
||||
organization.
|
||||
`,
|
||||
Advanced: true,
|
||||
Default: rwRead,
|
||||
Examples: rwExamples,
|
||||
}, {
|
||||
Name: "metadata_permissions",
|
||||
Help: `Control whether permissions should be read or written in metadata.
|
||||
|
||||
Reading permissions metadata from files can be done quickly, but it
|
||||
isn't always desirable to set the permissions from the metadata.
|
||||
|
||||
Note that rclone drops any inherited permissions on Shared Drives and
|
||||
any owner permission on My Drives as these are duplicated in the owner
|
||||
metadata.
|
||||
`,
|
||||
Advanced: true,
|
||||
Default: rwOff,
|
||||
Examples: rwExamples,
|
||||
}, {
|
||||
Name: "metadata_labels",
|
||||
Help: `Control whether labels should be read or written in metadata.
|
||||
|
||||
Reading labels metadata from files takes an extra API transaction and
|
||||
will slow down listings. It isn't always desirable to set the labels
|
||||
from the metadata.
|
||||
|
||||
The format of labels is documented in the drive API documentation at
|
||||
https://developers.google.com/drive/api/reference/rest/v3/Label -
|
||||
rclone just provides a JSON dump of this format.
|
||||
|
||||
When setting labels, the label and fields must already exist - rclone
|
||||
will not create them. This means that if you are transferring labels
|
||||
from two different accounts you will have to create the labels in
|
||||
advance and use the metadata mapper to translate the IDs between the
|
||||
two accounts.
|
||||
`,
|
||||
Advanced: true,
|
||||
Default: rwOff,
|
||||
Examples: rwExamples,
|
||||
}, {
|
||||
Name: config.ConfigEncoding,
|
||||
Help: config.ConfigEncodingHelp,
|
||||
@@ -715,6 +804,9 @@ type Options struct {
|
||||
SkipDanglingShortcuts bool `config:"skip_dangling_shortcuts"`
|
||||
ResourceKey string `config:"resource_key"`
|
||||
FastListBugFix bool `config:"fast_list_bug_fix"`
|
||||
MetadataOwner rwChoice `config:"metadata_owner"`
|
||||
MetadataPermissions rwChoice `config:"metadata_permissions"`
|
||||
MetadataLabels rwChoice `config:"metadata_labels"`
|
||||
Enc encoder.MultiEncoder `config:"encoding"`
|
||||
EnvAuth bool `config:"env_auth"`
|
||||
}
|
||||
@@ -738,21 +830,24 @@ type Fs struct {
|
||||
isTeamDrive bool // true if this is a team drive
|
||||
fileFields googleapi.Field // fields to fetch file info with
|
||||
m configmap.Mapper
|
||||
grouping int32 // number of IDs to search at once in ListR - read with atomic
|
||||
listRmu *sync.Mutex // protects listRempties
|
||||
listRempties map[string]struct{} // IDs of supposedly empty directories which triggered grouping disable
|
||||
dirResourceKeys *sync.Map // map directory ID to resource key
|
||||
grouping int32 // number of IDs to search at once in ListR - read with atomic
|
||||
listRmu *sync.Mutex // protects listRempties
|
||||
listRempties map[string]struct{} // IDs of supposedly empty directories which triggered grouping disable
|
||||
dirResourceKeys *sync.Map // map directory ID to resource key
|
||||
permissionsMu *sync.Mutex // protect the below
|
||||
permissions map[string]*drive.Permission // map permission IDs to Permissions
|
||||
}
|
||||
|
||||
type baseObject struct {
|
||||
fs *Fs // what this object is part of
|
||||
remote string // The remote path
|
||||
id string // Drive Id of this object
|
||||
modifiedDate string // RFC3339 time it was last modified
|
||||
mimeType string // The object MIME type
|
||||
bytes int64 // size of the object
|
||||
parents []string // IDs of the parent directories
|
||||
resourceKey *string // resourceKey is needed for link shared objects
|
||||
fs *Fs // what this object is part of
|
||||
remote string // The remote path
|
||||
id string // Drive Id of this object
|
||||
modifiedDate string // RFC3339 time it was last modified
|
||||
mimeType string // The object MIME type
|
||||
bytes int64 // size of the object
|
||||
parents []string // IDs of the parent directories
|
||||
resourceKey *string // resourceKey is needed for link shared objects
|
||||
metadata *fs.Metadata // metadata if known
|
||||
}
|
||||
type documentObject struct {
|
||||
baseObject
|
||||
@@ -1275,6 +1370,8 @@ func newFs(ctx context.Context, name, path string, m configmap.Mapper) (*Fs, err
|
||||
listRmu: new(sync.Mutex),
|
||||
listRempties: make(map[string]struct{}),
|
||||
dirResourceKeys: new(sync.Map),
|
||||
permissionsMu: new(sync.Mutex),
|
||||
permissions: make(map[string]*drive.Permission),
|
||||
}
|
||||
f.isTeamDrive = opt.TeamDriveID != ""
|
||||
f.fileFields = f.getFileFields()
|
||||
@@ -1285,6 +1382,9 @@ func newFs(ctx context.Context, name, path string, m configmap.Mapper) (*Fs, err
|
||||
CanHaveEmptyDirectories: true,
|
||||
ServerSideAcrossConfigs: opt.ServerSideAcrossConfigs,
|
||||
FilterAware: true,
|
||||
ReadMetadata: true,
|
||||
WriteMetadata: true,
|
||||
UserMetadata: true,
|
||||
}).Fill(ctx, f)
|
||||
|
||||
// Create a new authorized Drive client.
|
||||
@@ -1389,7 +1489,7 @@ func NewFs(ctx context.Context, name, path string, m configmap.Mapper) (fs.Fs, e
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (f *Fs) newBaseObject(remote string, info *drive.File) baseObject {
|
||||
func (f *Fs) newBaseObject(ctx context.Context, remote string, info *drive.File) (o baseObject, err error) {
|
||||
modifiedDate := info.ModifiedTime
|
||||
if f.opt.UseCreatedDate {
|
||||
modifiedDate = info.CreatedTime
|
||||
@@ -1400,7 +1500,7 @@ func (f *Fs) newBaseObject(remote string, info *drive.File) baseObject {
|
||||
if f.opt.SizeAsQuota {
|
||||
size = info.QuotaBytesUsed
|
||||
}
|
||||
return baseObject{
|
||||
o = baseObject{
|
||||
fs: f,
|
||||
remote: remote,
|
||||
id: info.Id,
|
||||
@@ -1409,6 +1509,11 @@ func (f *Fs) newBaseObject(remote string, info *drive.File) baseObject {
|
||||
bytes: size,
|
||||
parents: info.Parents,
|
||||
}
|
||||
err = nil
|
||||
if f.ci.Metadata {
|
||||
err = o.parseMetadata(ctx, info)
|
||||
}
|
||||
return o, err
|
||||
}
|
||||
|
||||
// getFileFields gets the fields for a normal file Get or List
|
||||
@@ -1426,11 +1531,14 @@ func (f *Fs) getFileFields() (fields googleapi.Field) {
|
||||
if f.opt.SizeAsQuota {
|
||||
fields += ",quotaBytesUsed"
|
||||
}
|
||||
if f.ci.Metadata {
|
||||
fields += "," + metadataFields
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// newRegularObject creates an fs.Object for a normal drive.File
|
||||
func (f *Fs) newRegularObject(remote string, info *drive.File) fs.Object {
|
||||
func (f *Fs) newRegularObject(ctx context.Context, remote string, info *drive.File) (obj fs.Object, err error) {
|
||||
// wipe checksum if SkipChecksumGphotos and file is type Photo or Video
|
||||
if f.opt.SkipChecksumGphotos {
|
||||
for _, space := range info.Spaces {
|
||||
@@ -1443,27 +1551,33 @@ func (f *Fs) newRegularObject(remote string, info *drive.File) fs.Object {
|
||||
}
|
||||
}
|
||||
o := &Object{
|
||||
baseObject: f.newBaseObject(remote, info),
|
||||
url: fmt.Sprintf("%sfiles/%s?alt=media", f.svc.BasePath, actualID(info.Id)),
|
||||
md5sum: strings.ToLower(info.Md5Checksum),
|
||||
sha1sum: strings.ToLower(info.Sha1Checksum),
|
||||
sha256sum: strings.ToLower(info.Sha256Checksum),
|
||||
v2Download: f.opt.V2DownloadMinSize != -1 && info.Size >= int64(f.opt.V2DownloadMinSize),
|
||||
}
|
||||
o.baseObject, err = f.newBaseObject(ctx, remote, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.ResourceKey != "" {
|
||||
o.resourceKey = &info.ResourceKey
|
||||
}
|
||||
return o
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// newDocumentObject creates an fs.Object for a google docs drive.File
|
||||
func (f *Fs) newDocumentObject(remote string, info *drive.File, extension, exportMimeType string) (fs.Object, error) {
|
||||
func (f *Fs) newDocumentObject(ctx context.Context, remote string, info *drive.File, extension, exportMimeType string) (fs.Object, error) {
|
||||
mediaType, _, err := mime.ParseMediaType(exportMimeType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := info.ExportLinks[mediaType]
|
||||
baseObject := f.newBaseObject(remote+extension, info)
|
||||
baseObject, err := f.newBaseObject(ctx, remote+extension, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseObject.bytes = -1
|
||||
baseObject.mimeType = exportMimeType
|
||||
return &documentObject{
|
||||
@@ -1475,7 +1589,7 @@ func (f *Fs) newDocumentObject(remote string, info *drive.File, extension, expor
|
||||
}
|
||||
|
||||
// newLinkObject creates an fs.Object that represents a link a google docs drive.File
|
||||
func (f *Fs) newLinkObject(remote string, info *drive.File, extension, exportMimeType string) (fs.Object, error) {
|
||||
func (f *Fs) newLinkObject(ctx context.Context, remote string, info *drive.File, extension, exportMimeType string) (fs.Object, error) {
|
||||
t := linkTemplate(exportMimeType)
|
||||
if t == nil {
|
||||
return nil, fmt.Errorf("unsupported link type %s", exportMimeType)
|
||||
@@ -1494,7 +1608,10 @@ func (f *Fs) newLinkObject(remote string, info *drive.File, extension, exportMim
|
||||
return nil, fmt.Errorf("executing template failed: %w", err)
|
||||
}
|
||||
|
||||
baseObject := f.newBaseObject(remote+extension, info)
|
||||
baseObject, err := f.newBaseObject(ctx, remote+extension, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseObject.bytes = int64(buf.Len())
|
||||
baseObject.mimeType = exportMimeType
|
||||
return &linkObject{
|
||||
@@ -1510,7 +1627,7 @@ func (f *Fs) newLinkObject(remote string, info *drive.File, extension, exportMim
|
||||
func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *drive.File) (fs.Object, error) {
|
||||
// If item has MD5 sum it is a file stored on drive
|
||||
if info.Md5Checksum != "" {
|
||||
return f.newRegularObject(remote, info), nil
|
||||
return f.newRegularObject(ctx, remote, info)
|
||||
}
|
||||
|
||||
extension, exportName, exportMimeType, isDocument := f.findExportFormat(ctx, info)
|
||||
@@ -1541,15 +1658,15 @@ func (f *Fs) newObjectWithExportInfo(
|
||||
case info.MimeType == shortcutMimeTypeDangling:
|
||||
// Pretend a dangling shortcut is a regular object
|
||||
// It will error if used, but appear in listings so it can be deleted
|
||||
return f.newRegularObject(remote, info), nil
|
||||
return f.newRegularObject(ctx, remote, info)
|
||||
case info.Md5Checksum != "":
|
||||
// If item has MD5 sum it is a file stored on drive
|
||||
return f.newRegularObject(remote, info), nil
|
||||
return f.newRegularObject(ctx, remote, info)
|
||||
case f.opt.SkipGdocs:
|
||||
fs.Debugf(remote, "Skipping google document type %q", info.MimeType)
|
||||
return nil, fs.ErrorObjectNotFound
|
||||
case f.opt.ShowAllGdocs:
|
||||
return f.newDocumentObject(remote, info, "", info.MimeType)
|
||||
return f.newDocumentObject(ctx, remote, info, "", info.MimeType)
|
||||
default:
|
||||
// If item MimeType is in the ExportFormats then it is a google doc
|
||||
if !isDocument {
|
||||
@@ -1561,9 +1678,9 @@ func (f *Fs) newObjectWithExportInfo(
|
||||
return nil, fs.ErrorObjectNotFound
|
||||
}
|
||||
if isLinkMimeType(exportMimeType) {
|
||||
return f.newLinkObject(remote, info, extension, exportMimeType)
|
||||
return f.newLinkObject(ctx, remote, info, extension, exportMimeType)
|
||||
}
|
||||
return f.newDocumentObject(remote, info, extension, exportMimeType)
|
||||
return f.newDocumentObject(ctx, remote, info, extension, exportMimeType)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2323,6 +2440,10 @@ func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo,
|
||||
} else {
|
||||
createInfo.MimeType = fs.MimeTypeFromName(remote)
|
||||
}
|
||||
updateMetadata, err := f.fetchAndUpdateMetadata(ctx, src, options, createInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var info *drive.File
|
||||
if size >= 0 && size < int64(f.opt.UploadCutoff) {
|
||||
@@ -2347,6 +2468,10 @@ func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo,
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
err = updateMetadata(ctx, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.newObjectWithInfo(ctx, remote, info)
|
||||
}
|
||||
|
||||
@@ -3944,10 +4069,20 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
||||
MimeType: srcMimeType,
|
||||
ModifiedTime: src.ModTime(ctx).Format(timeFormatOut),
|
||||
}
|
||||
|
||||
updateMetadata, err := o.fs.fetchAndUpdateMetadata(ctx, src, options, updateInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info, err := o.baseObject.update(ctx, updateInfo, srcMimeType, in, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = updateMetadata(ctx, info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newO, err := o.fs.newObjectWithInfo(ctx, o.remote, info)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -4033,6 +4168,26 @@ func (o *baseObject) ParentID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Metadata returns metadata for an object
|
||||
//
|
||||
// It should return nil if there is no Metadata
|
||||
func (o *baseObject) Metadata(ctx context.Context) (metadata fs.Metadata, err error) {
|
||||
if o.metadata != nil {
|
||||
return *o.metadata, nil
|
||||
}
|
||||
fs.Debugf(o, "Fetching metadata")
|
||||
id := actualID(o.id)
|
||||
info, err := o.fs.getFile(ctx, id, o.fs.fileFields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = o.parseMetadata(ctx, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return *o.metadata, nil
|
||||
}
|
||||
|
||||
func (o *documentObject) ext() string {
|
||||
return o.baseObject.remote[len(o.baseObject.remote)-o.extLen:]
|
||||
}
|
||||
@@ -4094,6 +4249,7 @@ var (
|
||||
_ fs.MimeTyper = (*Object)(nil)
|
||||
_ fs.IDer = (*Object)(nil)
|
||||
_ fs.ParentIDer = (*Object)(nil)
|
||||
_ fs.Metadataer = (*Object)(nil)
|
||||
_ fs.Object = (*documentObject)(nil)
|
||||
_ fs.MimeTyper = (*documentObject)(nil)
|
||||
_ fs.IDer = (*documentObject)(nil)
|
||||
|
||||
Reference in New Issue
Block a user