drive: follow shortcuts by default, skip with --drive-skip-shortcuts
Before this change rclone would skip all shortcuts with a message Ignoring unknown document type "application/vnd.google-apps.shortcut" After this message rclone resolves the shortcuts by default to the actual files that they point to. See the docs for more info. The --drive-skip-shortcuts flag can be used to skip shortcuts.
This commit is contained in:
parent
b03cad3cf6
commit
f2b1fedc4f
2 changed files with 273 additions and 78 deletions
|
@ -54,6 +54,7 @@ const (
|
||||||
rcloneClientID = "202264815644.apps.googleusercontent.com"
|
rcloneClientID = "202264815644.apps.googleusercontent.com"
|
||||||
rcloneEncryptedClientSecret = "eX8GpZTVx3vxMWVkuuBdDWmAUE6rGhTwVrvG9GhllYccSdj2-mvHVg"
|
rcloneEncryptedClientSecret = "eX8GpZTVx3vxMWVkuuBdDWmAUE6rGhTwVrvG9GhllYccSdj2-mvHVg"
|
||||||
driveFolderType = "application/vnd.google-apps.folder"
|
driveFolderType = "application/vnd.google-apps.folder"
|
||||||
|
shortcutMimeType = "application/vnd.google-apps.shortcut"
|
||||||
timeFormatIn = time.RFC3339
|
timeFormatIn = time.RFC3339
|
||||||
timeFormatOut = "2006-01-02T15:04:05.000000000Z07:00"
|
timeFormatOut = "2006-01-02T15:04:05.000000000Z07:00"
|
||||||
defaultMinSleep = fs.Duration(100 * time.Millisecond)
|
defaultMinSleep = fs.Duration(100 * time.Millisecond)
|
||||||
|
@ -65,7 +66,7 @@ const (
|
||||||
// 1<<18 is the minimum size supported by the Google uploader, and there is no maximum.
|
// 1<<18 is the minimum size supported by the Google uploader, and there is no maximum.
|
||||||
minChunkSize = 256 * fs.KibiByte
|
minChunkSize = 256 * fs.KibiByte
|
||||||
defaultChunkSize = 8 * fs.MebiByte
|
defaultChunkSize = 8 * fs.MebiByte
|
||||||
partialFields = "id,name,size,md5Checksum,trashed,modifiedTime,createdTime,mimeType,parents,webViewLink"
|
partialFields = "id,name,size,md5Checksum,trashed,modifiedTime,createdTime,mimeType,parents,webViewLink,shortcutDetails"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Globals
|
// Globals
|
||||||
|
@ -467,6 +468,16 @@ Google don't document so it may break in the future.
|
||||||
See: https://github.com/rclone/rclone/issues/3857
|
See: https://github.com/rclone/rclone/issues/3857
|
||||||
`,
|
`,
|
||||||
Advanced: true,
|
Advanced: true,
|
||||||
|
}, {
|
||||||
|
Name: "skip_shortcuts",
|
||||||
|
Help: `If set skip shortcut files
|
||||||
|
|
||||||
|
Normally rclone dereferences shortcut files making them appear as if
|
||||||
|
they are the original file (see [the shortcuts section](#shortcuts)).
|
||||||
|
If this flag is set then rclone will ignore shortcut files completely.
|
||||||
|
`,
|
||||||
|
Advanced: true,
|
||||||
|
Default: false,
|
||||||
}, {
|
}, {
|
||||||
Name: config.ConfigEncoding,
|
Name: config.ConfigEncoding,
|
||||||
Help: config.ConfigEncodingHelp,
|
Help: config.ConfigEncodingHelp,
|
||||||
|
@ -524,6 +535,7 @@ type Options struct {
|
||||||
ServerSideAcrossConfigs bool `config:"server_side_across_configs"`
|
ServerSideAcrossConfigs bool `config:"server_side_across_configs"`
|
||||||
DisableHTTP2 bool `config:"disable_http2"`
|
DisableHTTP2 bool `config:"disable_http2"`
|
||||||
StopOnUploadLimit bool `config:"stop_on_upload_limit"`
|
StopOnUploadLimit bool `config:"stop_on_upload_limit"`
|
||||||
|
SkipShortcuts bool `config:"skip_shortcuts"`
|
||||||
Enc encoder.MultiEncoder `config:"encoding"`
|
Enc encoder.MultiEncoder `config:"encoding"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -542,6 +554,7 @@ type Fs struct {
|
||||||
exportExtensions []string // preferred extensions to download docs
|
exportExtensions []string // preferred extensions to download docs
|
||||||
importMimeTypes []string // MIME types to convert to docs
|
importMimeTypes []string // MIME types to convert to docs
|
||||||
isTeamDrive bool // true if this is a team drive
|
isTeamDrive bool // true if this is a team drive
|
||||||
|
fileFields googleapi.Field // fields to fetch file info with
|
||||||
}
|
}
|
||||||
|
|
||||||
type baseObject struct {
|
type baseObject struct {
|
||||||
|
@ -726,7 +739,7 @@ func (f *Fs) list(ctx context.Context, dirIDs []string, title string, directorie
|
||||||
query = append(query, titleQuery.String())
|
query = append(query, titleQuery.String())
|
||||||
}
|
}
|
||||||
if directoriesOnly {
|
if directoriesOnly {
|
||||||
query = append(query, fmt.Sprintf("mimeType='%s'", driveFolderType))
|
query = append(query, fmt.Sprintf("(mimeType='%s' or mimeType='%s')", driveFolderType, shortcutMimeType))
|
||||||
}
|
}
|
||||||
if filesOnly {
|
if filesOnly {
|
||||||
query = append(query, fmt.Sprintf("mimeType!='%s'", driveFolderType))
|
query = append(query, fmt.Sprintf("mimeType!='%s'", driveFolderType))
|
||||||
|
@ -750,22 +763,7 @@ func (f *Fs) list(ctx context.Context, dirIDs []string, title string, directorie
|
||||||
list.Spaces("appDataFolder")
|
list.Spaces("appDataFolder")
|
||||||
}
|
}
|
||||||
|
|
||||||
var fields = partialFields
|
fields := fmt.Sprintf("files(%s),nextPageToken,incompleteSearch", f.fileFields)
|
||||||
|
|
||||||
if f.opt.AuthOwnerOnly {
|
|
||||||
fields += ",owners"
|
|
||||||
}
|
|
||||||
if f.opt.UseSharedDate {
|
|
||||||
fields += ",sharedWithMeTime"
|
|
||||||
}
|
|
||||||
if f.opt.SkipChecksumGphotos {
|
|
||||||
fields += ",spaces"
|
|
||||||
}
|
|
||||||
if f.opt.SizeAsQuota {
|
|
||||||
fields += ",quotaBytesUsed"
|
|
||||||
}
|
|
||||||
|
|
||||||
fields = fmt.Sprintf("files(%s),nextPageToken,incompleteSearch", fields)
|
|
||||||
|
|
||||||
OUTER:
|
OUTER:
|
||||||
for {
|
for {
|
||||||
|
@ -782,6 +780,24 @@ OUTER:
|
||||||
}
|
}
|
||||||
for _, item := range files.Files {
|
for _, item := range files.Files {
|
||||||
item.Name = f.opt.Enc.ToStandardName(item.Name)
|
item.Name = f.opt.Enc.ToStandardName(item.Name)
|
||||||
|
if isShortcut(item) {
|
||||||
|
// ignore shortcuts if directed
|
||||||
|
if f.opt.SkipShortcuts {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// skip file shortcuts if directory only
|
||||||
|
if directoriesOnly && item.ShortcutDetails.TargetMimeType != driveFolderType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// skip directory shortcuts if file only
|
||||||
|
if filesOnly && item.ShortcutDetails.TargetMimeType == driveFolderType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item, err = f.resolveShortcut(item)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "list")
|
||||||
|
}
|
||||||
|
}
|
||||||
// Check the case of items is correct since
|
// Check the case of items is correct since
|
||||||
// the `=` operator is case insensitive.
|
// the `=` operator is case insensitive.
|
||||||
if title != "" && title != item.Name {
|
if title != "" && title != item.Name {
|
||||||
|
@ -1066,6 +1082,7 @@ func NewFs(name, path string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
pacer: newPacer(opt),
|
pacer: newPacer(opt),
|
||||||
}
|
}
|
||||||
f.isTeamDrive = opt.TeamDriveID != ""
|
f.isTeamDrive = opt.TeamDriveID != ""
|
||||||
|
f.fileFields = f.getFileFields()
|
||||||
f.features = (&fs.Features{
|
f.features = (&fs.Features{
|
||||||
DuplicateFiles: true,
|
DuplicateFiles: true,
|
||||||
ReadMimeType: true,
|
ReadMimeType: true,
|
||||||
|
@ -1181,6 +1198,24 @@ func (f *Fs) newBaseObject(remote string, info *drive.File) baseObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getFileFields gets the fields for a normal file Get or List
|
||||||
|
func (f *Fs) getFileFields() (fields googleapi.Field) {
|
||||||
|
fields = partialFields
|
||||||
|
if f.opt.AuthOwnerOnly {
|
||||||
|
fields += ",owners"
|
||||||
|
}
|
||||||
|
if f.opt.UseSharedDate {
|
||||||
|
fields += ",sharedWithMeTime"
|
||||||
|
}
|
||||||
|
if f.opt.SkipChecksumGphotos {
|
||||||
|
fields += ",spaces"
|
||||||
|
}
|
||||||
|
if f.opt.SizeAsQuota {
|
||||||
|
fields += ",quotaBytesUsed"
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
// newRegularObject creates a fs.Object for a normal drive.File
|
// newRegularObject creates a fs.Object for a normal drive.File
|
||||||
func (f *Fs) newRegularObject(remote string, info *drive.File) fs.Object {
|
func (f *Fs) newRegularObject(remote string, info *drive.File) fs.Object {
|
||||||
// wipe checksum if SkipChecksumGphotos and file is type Photo or Video
|
// wipe checksum if SkipChecksumGphotos and file is type Photo or Video
|
||||||
|
@ -1194,7 +1229,7 @@ func (f *Fs) newRegularObject(remote string, info *drive.File) fs.Object {
|
||||||
}
|
}
|
||||||
return &Object{
|
return &Object{
|
||||||
baseObject: f.newBaseObject(remote, info),
|
baseObject: f.newBaseObject(remote, info),
|
||||||
url: fmt.Sprintf("%sfiles/%s?alt=media", f.svc.BasePath, info.Id),
|
url: fmt.Sprintf("%sfiles/%s?alt=media", f.svc.BasePath, actualID(info.Id)),
|
||||||
md5sum: strings.ToLower(info.Md5Checksum),
|
md5sum: strings.ToLower(info.Md5Checksum),
|
||||||
v2Download: f.opt.V2DownloadMinSize != -1 && info.Size >= int64(f.opt.V2DownloadMinSize),
|
v2Download: f.opt.V2DownloadMinSize != -1 && info.Size >= int64(f.opt.V2DownloadMinSize),
|
||||||
}
|
}
|
||||||
|
@ -1206,17 +1241,18 @@ func (f *Fs) newDocumentObject(remote string, info *drive.File, extension, expor
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
url := fmt.Sprintf("%sfiles/%s/export?mimeType=%s", f.svc.BasePath, info.Id, url.QueryEscape(mediaType))
|
id := actualID(info.Id)
|
||||||
|
url := fmt.Sprintf("%sfiles/%s/export?mimeType=%s", f.svc.BasePath, id, url.QueryEscape(mediaType))
|
||||||
if f.opt.AlternateExport {
|
if f.opt.AlternateExport {
|
||||||
switch info.MimeType {
|
switch info.MimeType {
|
||||||
case "application/vnd.google-apps.drawing":
|
case "application/vnd.google-apps.drawing":
|
||||||
url = fmt.Sprintf("https://docs.google.com/drawings/d/%s/export/%s", info.Id, extension[1:])
|
url = fmt.Sprintf("https://docs.google.com/drawings/d/%s/export/%s", id, extension[1:])
|
||||||
case "application/vnd.google-apps.document":
|
case "application/vnd.google-apps.document":
|
||||||
url = fmt.Sprintf("https://docs.google.com/document/d/%s/export?format=%s", info.Id, extension[1:])
|
url = fmt.Sprintf("https://docs.google.com/document/d/%s/export?format=%s", id, extension[1:])
|
||||||
case "application/vnd.google-apps.spreadsheet":
|
case "application/vnd.google-apps.spreadsheet":
|
||||||
url = fmt.Sprintf("https://docs.google.com/spreadsheets/d/%s/export?format=%s", info.Id, extension[1:])
|
url = fmt.Sprintf("https://docs.google.com/spreadsheets/d/%s/export?format=%s", id, extension[1:])
|
||||||
case "application/vnd.google-apps.presentation":
|
case "application/vnd.google-apps.presentation":
|
||||||
url = fmt.Sprintf("https://docs.google.com/presentation/d/%s/export/%s", info.Id, extension[1:])
|
url = fmt.Sprintf("https://docs.google.com/presentation/d/%s/export/%s", id, extension[1:])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
baseObject := f.newBaseObject(remote+extension, info)
|
baseObject := f.newBaseObject(remote+extension, info)
|
||||||
|
@ -1274,8 +1310,22 @@ func (f *Fs) newObjectWithInfo(remote string, info *drive.File) (fs.Object, erro
|
||||||
// When the drive.File cannot be represented as a fs.Object it will return (nil, nil).
|
// When the drive.File cannot be represented as a fs.Object it will return (nil, nil).
|
||||||
func (f *Fs) newObjectWithExportInfo(
|
func (f *Fs) newObjectWithExportInfo(
|
||||||
remote string, info *drive.File,
|
remote string, info *drive.File,
|
||||||
extension, exportName, exportMimeType string, isDocument bool) (fs.Object, error) {
|
extension, exportName, exportMimeType string, isDocument bool) (o fs.Object, err error) {
|
||||||
|
// Note that resolveShortcut will have been called already if
|
||||||
|
// we are being called from a listing. However the drive.Item
|
||||||
|
// will have been resolved so this will do nothing.
|
||||||
|
info, err = f.resolveShortcut(info)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "new object")
|
||||||
|
}
|
||||||
switch {
|
switch {
|
||||||
|
case info.MimeType == driveFolderType:
|
||||||
|
return nil, fs.ErrorNotAFile
|
||||||
|
case info.MimeType == shortcutMimeType:
|
||||||
|
// We can only get here if f.opt.SkipShortcuts is set
|
||||||
|
// and not from a listing. This is unlikely.
|
||||||
|
fs.Debugf(remote, "Ignoring shortcut as skip shortcuts is set")
|
||||||
|
return nil, fs.ErrorObjectNotFound
|
||||||
case info.Md5Checksum != "" || info.Size > 0:
|
case info.Md5Checksum != "" || info.Size > 0:
|
||||||
// If item has MD5 sum or a length it is a file stored on drive
|
// If item has MD5 sum or a length it is a file stored on drive
|
||||||
return f.newRegularObject(remote, info), nil
|
return f.newRegularObject(remote, info), nil
|
||||||
|
@ -1322,6 +1372,7 @@ func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
||||||
// FindLeaf finds a directory of name leaf in the folder with ID pathID
|
// FindLeaf finds a directory of name leaf in the folder with ID pathID
|
||||||
func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) {
|
func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) {
|
||||||
// Find the leaf in pathID
|
// Find the leaf in pathID
|
||||||
|
pathID = actualID(pathID)
|
||||||
found, err = f.list(ctx, []string{pathID}, leaf, true, false, false, func(item *drive.File) bool {
|
found, err = f.list(ctx, []string{pathID}, leaf, true, false, false, func(item *drive.File) bool {
|
||||||
if !f.opt.SkipGdocs {
|
if !f.opt.SkipGdocs {
|
||||||
_, exportName, _, isDocument := f.findExportFormat(item)
|
_, exportName, _, isDocument := f.findExportFormat(item)
|
||||||
|
@ -1515,6 +1566,7 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
directoryID = actualID(directoryID)
|
||||||
|
|
||||||
var iErr error
|
var iErr error
|
||||||
_, err = f.list(ctx, []string{directoryID}, "", false, false, false, func(item *drive.File) bool {
|
_, err = f.list(ctx, []string{directoryID}, "", false, false, false, func(item *drive.File) bool {
|
||||||
|
@ -1693,6 +1745,7 @@ func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
directoryID = actualID(directoryID)
|
||||||
|
|
||||||
mu := sync.Mutex{} // protects in and overflow
|
mu := sync.Mutex{} // protects in and overflow
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
|
@ -1706,11 +1759,12 @@ func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
if d, isDir := entry.(*fs.Dir); isDir && in != nil {
|
if d, isDir := entry.(*fs.Dir); isDir && in != nil {
|
||||||
|
job := listREntry{actualID(d.ID()), d.Remote()}
|
||||||
select {
|
select {
|
||||||
case in <- listREntry{d.ID(), d.Remote()}:
|
case in <- job:
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
default:
|
default:
|
||||||
overflow = append(overflow, listREntry{d.ID(), d.Remote()})
|
overflow = append(overflow, job)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
listed++
|
listed++
|
||||||
|
@ -1787,6 +1841,82 @@ func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shortcutSeparator = '\t'
|
||||||
|
|
||||||
|
// joinID adds an actual drive ID to the shortcut ID it came from
|
||||||
|
//
|
||||||
|
// directoryIDs in the dircache are these composite directory IDs so
|
||||||
|
// we must always unpack them before use.
|
||||||
|
func joinID(actual, shortcut string) string {
|
||||||
|
return actual + string(shortcutSeparator) + shortcut
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitID separates an actual ID and a shortcut ID from a composite
|
||||||
|
// ID. If there was no shortcut ID then it will return "" for it.
|
||||||
|
func splitID(compositeID string) (actualID, shortcutID string) {
|
||||||
|
i := strings.IndexRune(compositeID, shortcutSeparator)
|
||||||
|
if i < 0 {
|
||||||
|
return compositeID, ""
|
||||||
|
}
|
||||||
|
return compositeID[:i], compositeID[i+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// isShortcutID returns true if compositeID refers to a shortcut
|
||||||
|
func isShortcutID(compositeID string) bool {
|
||||||
|
return strings.IndexRune(compositeID, shortcutSeparator) >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// actualID returns an actual ID from a composite ID
|
||||||
|
func actualID(compositeID string) (actualID string) {
|
||||||
|
actualID, _ = splitID(compositeID)
|
||||||
|
return actualID
|
||||||
|
}
|
||||||
|
|
||||||
|
// shortcutID returns a shortcut ID from a composite ID if available,
|
||||||
|
// or the actual ID if not.
|
||||||
|
func shortcutID(compositeID string) (shortcutID string) {
|
||||||
|
actualID, shortcutID := splitID(compositeID)
|
||||||
|
if shortcutID != "" {
|
||||||
|
return shortcutID
|
||||||
|
}
|
||||||
|
return actualID
|
||||||
|
}
|
||||||
|
|
||||||
|
// isShortcut returns true of the item is a shortcut
|
||||||
|
func isShortcut(item *drive.File) bool {
|
||||||
|
return item.MimeType == shortcutMimeType && item.ShortcutDetails != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dereference shortcut if required. It returns the newItem (which may
|
||||||
|
// be just item).
|
||||||
|
//
|
||||||
|
// If we return a new item then the ID will be adjusted to be a
|
||||||
|
// composite of the actual ID and the shortcut ID. This is to make
|
||||||
|
// sure that we have decided in all use places what we are doing with
|
||||||
|
// the ID.
|
||||||
|
//
|
||||||
|
// Note that we assume shortcuts can't point to shortcuts. Google
|
||||||
|
// drive web interface doesn't offer the option to create a shortcut
|
||||||
|
// to a shortcut. The documentation is silent on the issue.
|
||||||
|
func (f *Fs) resolveShortcut(item *drive.File) (newItem *drive.File, err error) {
|
||||||
|
if f.opt.SkipShortcuts || item.MimeType != shortcutMimeType {
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
if item.ShortcutDetails == nil {
|
||||||
|
fs.Errorf(nil, "Expecting shortcutDetails in %v", item)
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
newItem, err = f.getFile(item.ShortcutDetails.TargetId, f.fileFields)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to resolve shortcut")
|
||||||
|
}
|
||||||
|
// make sure we use the Name from the original item
|
||||||
|
newItem.Name = item.Name
|
||||||
|
// the new ID is a composite ID
|
||||||
|
newItem.Id = joinID(newItem.Id, item.Id)
|
||||||
|
return newItem, nil
|
||||||
|
}
|
||||||
|
|
||||||
// itemToDirEntry converts a drive.File to a fs.DirEntry.
|
// itemToDirEntry converts a drive.File to a fs.DirEntry.
|
||||||
// When the drive.File cannot be represented as a fs.DirEntry
|
// When the drive.File cannot be represented as a fs.DirEntry
|
||||||
// (nil, nil) is returned.
|
// (nil, nil) is returned.
|
||||||
|
@ -1818,6 +1948,7 @@ func (f *Fs) createFileInfo(ctx context.Context, remote string, modTime time.Tim
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
directoryID = actualID(directoryID)
|
||||||
|
|
||||||
leaf = f.opt.Enc.FromStandardName(leaf)
|
leaf = f.opt.Enc.FromStandardName(leaf)
|
||||||
// Define the metadata for the file we are going to create.
|
// Define the metadata for the file we are going to create.
|
||||||
|
@ -1921,6 +2052,18 @@ func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo,
|
||||||
// MergeDirs merges the contents of all the directories passed
|
// MergeDirs merges the contents of all the directories passed
|
||||||
// in into the first one and rmdirs the other directories.
|
// in into the first one and rmdirs the other directories.
|
||||||
func (f *Fs) MergeDirs(ctx context.Context, dirs []fs.Directory) error {
|
func (f *Fs) MergeDirs(ctx context.Context, dirs []fs.Directory) error {
|
||||||
|
if len(dirs) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
newDirs := dirs[:0]
|
||||||
|
for _, dir := range dirs {
|
||||||
|
if isShortcutID(dir.ID()) {
|
||||||
|
fs.Infof(dir, "skipping shortcut directory")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newDirs = append(newDirs, dir)
|
||||||
|
}
|
||||||
|
dirs = newDirs
|
||||||
if len(dirs) < 2 {
|
if len(dirs) < 2 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1954,7 +2097,7 @@ func (f *Fs) MergeDirs(ctx context.Context, dirs []fs.Directory) error {
|
||||||
}
|
}
|
||||||
// rmdir (into trash) the now empty source directory
|
// rmdir (into trash) the now empty source directory
|
||||||
fs.Infof(srcDir, "removing empty directory")
|
fs.Infof(srcDir, "removing empty directory")
|
||||||
err = f.rmdir(ctx, srcDir.ID(), true)
|
err = f.delete(ctx, srcDir.ID(), true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "MergeDirs move failed to rmdir %q", srcDir)
|
return errors.Wrapf(err, "MergeDirs move failed to rmdir %q", srcDir)
|
||||||
}
|
}
|
||||||
|
@ -1974,20 +2117,20 @@ func (f *Fs) Mkdir(ctx context.Context, dir string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rmdir deletes a directory unconditionally by ID
|
// delete a file or directory unconditionally by ID
|
||||||
func (f *Fs) rmdir(ctx context.Context, directoryID string, useTrash bool) error {
|
func (f *Fs) delete(ctx context.Context, id string, useTrash bool) error {
|
||||||
return f.pacer.Call(func() (bool, error) {
|
return f.pacer.Call(func() (bool, error) {
|
||||||
var err error
|
var err error
|
||||||
if useTrash {
|
if useTrash {
|
||||||
info := drive.File{
|
info := drive.File{
|
||||||
Trashed: true,
|
Trashed: true,
|
||||||
}
|
}
|
||||||
_, err = f.svc.Files.Update(directoryID, &info).
|
_, err = f.svc.Files.Update(id, &info).
|
||||||
Fields("").
|
Fields("").
|
||||||
SupportsAllDrives(true).
|
SupportsAllDrives(true).
|
||||||
Do()
|
Do()
|
||||||
} else {
|
} else {
|
||||||
err = f.svc.Files.Delete(directoryID).
|
err = f.svc.Files.Delete(id).
|
||||||
Fields("").
|
Fields("").
|
||||||
SupportsAllDrives(true).
|
SupportsAllDrives(true).
|
||||||
Do()
|
Do()
|
||||||
|
@ -2006,6 +2149,11 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
directoryID, shortcutID := splitID(directoryID)
|
||||||
|
// if directory is a shortcut remove it regardless
|
||||||
|
if shortcutID != "" {
|
||||||
|
return f.delete(ctx, shortcutID, f.opt.UseTrash)
|
||||||
|
}
|
||||||
var trashedFiles = false
|
var trashedFiles = false
|
||||||
found, err := f.list(ctx, []string{directoryID}, "", false, false, true, func(item *drive.File) bool {
|
found, err := f.list(ctx, []string{directoryID}, "", false, false, true, func(item *drive.File) bool {
|
||||||
if !item.Trashed {
|
if !item.Trashed {
|
||||||
|
@ -2026,7 +2174,7 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) error {
|
||||||
// trash the directory if it had trashed files
|
// trash the directory if it had trashed files
|
||||||
// in or the user wants to trash, otherwise
|
// in or the user wants to trash, otherwise
|
||||||
// delete it.
|
// delete it.
|
||||||
err = f.rmdir(ctx, directoryID, trashedFiles || f.opt.UseTrash)
|
err = f.delete(ctx, directoryID, trashedFiles || f.opt.UseTrash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -2087,7 +2235,7 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
||||||
|
|
||||||
if readDescription {
|
if readDescription {
|
||||||
// preserve the description on copy for docs
|
// preserve the description on copy for docs
|
||||||
info, err := f.getFile(srcObj.id, "description")
|
info, err := f.getFile(actualID(srcObj.id), "description")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to read description for Google Doc")
|
return nil, errors.Wrap(err, "failed to read description for Google Doc")
|
||||||
}
|
}
|
||||||
|
@ -2098,9 +2246,12 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
||||||
createInfo.Description = ""
|
createInfo.Description = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get the ID of the thing to copy - this is the shortcut if available
|
||||||
|
id := shortcutID(srcObj.id)
|
||||||
|
|
||||||
var info *drive.File
|
var info *drive.File
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
info, err = f.svc.Files.Copy(srcObj.id, createInfo).
|
info, err = f.svc.Files.Copy(id, createInfo).
|
||||||
Fields(partialFields).
|
Fields(partialFields).
|
||||||
SupportsAllDrives(true).
|
SupportsAllDrives(true).
|
||||||
KeepRevisionForever(f.opt.KeepRevisionForever).
|
KeepRevisionForever(f.opt.KeepRevisionForever).
|
||||||
|
@ -2139,23 +2290,7 @@ func (f *Fs) Purge(ctx context.Context) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.delete(ctx, shortcutID(f.dirCache.RootID()), f.opt.UseTrash)
|
||||||
if f.opt.UseTrash {
|
|
||||||
info := drive.File{
|
|
||||||
Trashed: true,
|
|
||||||
}
|
|
||||||
_, err = f.svc.Files.Update(f.dirCache.RootID(), &info).
|
|
||||||
Fields("").
|
|
||||||
SupportsAllDrives(true).
|
|
||||||
Do()
|
|
||||||
} else {
|
|
||||||
err = f.svc.Files.Delete(f.dirCache.RootID()).
|
|
||||||
Fields("").
|
|
||||||
SupportsAllDrives(true).
|
|
||||||
Do()
|
|
||||||
}
|
|
||||||
return f.shouldRetry(err)
|
|
||||||
})
|
|
||||||
f.dirCache.ResetRoot()
|
f.dirCache.ResetRoot()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -2261,6 +2396,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
srcParentID = actualID(srcParentID)
|
||||||
|
|
||||||
// Temporary Object under construction
|
// Temporary Object under construction
|
||||||
dstInfo, err := f.createFileInfo(ctx, remote, src.ModTime(ctx))
|
dstInfo, err := f.createFileInfo(ctx, remote, src.ModTime(ctx))
|
||||||
|
@ -2273,7 +2409,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
||||||
// Do the move
|
// Do the move
|
||||||
var info *drive.File
|
var info *drive.File
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
info, err = f.svc.Files.Update(srcObj.id, dstInfo).
|
info, err = f.svc.Files.Update(shortcutID(srcObj.id), dstInfo).
|
||||||
RemoveParents(srcParentID).
|
RemoveParents(srcParentID).
|
||||||
AddParents(dstParents).
|
AddParents(dstParents).
|
||||||
Fields(partialFields).
|
Fields(partialFields).
|
||||||
|
@ -2293,13 +2429,14 @@ func (f *Fs) PublicLink(ctx context.Context, remote string) (link string, err er
|
||||||
id, err := f.dirCache.FindDir(ctx, remote, false)
|
id, err := f.dirCache.FindDir(ctx, remote, false)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
fs.Debugf(f, "attempting to share directory '%s'", remote)
|
fs.Debugf(f, "attempting to share directory '%s'", remote)
|
||||||
|
id = shortcutID(id)
|
||||||
} else {
|
} else {
|
||||||
fs.Debugf(f, "attempting to share single file '%s'", remote)
|
fs.Debugf(f, "attempting to share single file '%s'", remote)
|
||||||
o, err := f.NewObject(ctx, remote)
|
o, err := f.NewObject(ctx, remote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
id = o.(fs.IDer).ID()
|
id = shortcutID(o.(fs.IDer).ID())
|
||||||
}
|
}
|
||||||
|
|
||||||
permission := &drive.Permission{
|
permission := &drive.Permission{
|
||||||
|
@ -2374,6 +2511,7 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
dstDirectoryID = actualID(dstDirectoryID)
|
||||||
|
|
||||||
// Check destination does not exist
|
// Check destination does not exist
|
||||||
if dstRemote != "" {
|
if dstRemote != "" {
|
||||||
|
@ -2397,19 +2535,19 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
srcDirectoryID = actualID(srcDirectoryID)
|
||||||
|
|
||||||
// Find ID of src
|
// Find ID of src
|
||||||
srcID, err := srcFs.dirCache.FindDir(ctx, srcRemote, false)
|
srcID, err := srcFs.dirCache.FindDir(ctx, srcRemote, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do the move
|
// Do the move
|
||||||
patch := drive.File{
|
patch := drive.File{
|
||||||
Name: leaf,
|
Name: leaf,
|
||||||
}
|
}
|
||||||
err = f.pacer.Call(func() (bool, error) {
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
_, err = f.svc.Files.Update(srcID, &patch).
|
_, err = f.svc.Files.Update(shortcutID(srcID), &patch).
|
||||||
RemoveParents(srcDirectoryID).
|
RemoveParents(srcDirectoryID).
|
||||||
AddParents(dstDirectoryID).
|
AddParents(dstDirectoryID).
|
||||||
Fields("").
|
Fields("").
|
||||||
|
@ -2646,6 +2784,7 @@ func (f *Fs) getRemoteInfoWithExport(ctx context.Context, remote string) (
|
||||||
}
|
}
|
||||||
return nil, "", "", "", false, err
|
return nil, "", "", "", false, err
|
||||||
}
|
}
|
||||||
|
directoryID = actualID(directoryID)
|
||||||
|
|
||||||
found, err := f.list(ctx, []string{directoryID}, leaf, false, true, false, func(item *drive.File) bool {
|
found, err := f.list(ctx, []string{directoryID}, leaf, false, true, false, func(item *drive.File) bool {
|
||||||
if !f.opt.SkipGdocs {
|
if !f.opt.SkipGdocs {
|
||||||
|
@ -2697,7 +2836,7 @@ func (o *baseObject) SetModTime(ctx context.Context, modTime time.Time) error {
|
||||||
var info *drive.File
|
var info *drive.File
|
||||||
err := o.fs.pacer.Call(func() (bool, error) {
|
err := o.fs.pacer.Call(func() (bool, error) {
|
||||||
var err error
|
var err error
|
||||||
info, err = o.fs.svc.Files.Update(o.id, updateInfo).
|
info, err = o.fs.svc.Files.Update(actualID(o.id), updateInfo).
|
||||||
Fields(partialFields).
|
Fields(partialFields).
|
||||||
SupportsAllDrives(true).
|
SupportsAllDrives(true).
|
||||||
Do()
|
Do()
|
||||||
|
@ -2826,7 +2965,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
|
||||||
if o.v2Download {
|
if o.v2Download {
|
||||||
var v2File *drive_v2.File
|
var v2File *drive_v2.File
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
v2File, err = o.fs.v2Svc.Files.Get(o.id).
|
v2File, err = o.fs.v2Svc.Files.Get(actualID(o.id)).
|
||||||
Fields("downloadUrl").
|
Fields("downloadUrl").
|
||||||
SupportsAllDrives(true).
|
SupportsAllDrives(true).
|
||||||
Do()
|
Do()
|
||||||
|
@ -2905,7 +3044,7 @@ func (o *baseObject) update(ctx context.Context, updateInfo *drive.File, uploadM
|
||||||
if size >= 0 && size < int64(o.fs.opt.UploadCutoff) {
|
if size >= 0 && size < int64(o.fs.opt.UploadCutoff) {
|
||||||
// Don't retry, return a retry error instead
|
// Don't retry, return a retry error instead
|
||||||
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
err = o.fs.pacer.CallNoRetry(func() (bool, error) {
|
||||||
info, err = o.fs.svc.Files.Update(o.id, updateInfo).
|
info, err = o.fs.svc.Files.Update(actualID(o.id), updateInfo).
|
||||||
Media(in, googleapi.ContentType(uploadMimeType)).
|
Media(in, googleapi.ContentType(uploadMimeType)).
|
||||||
Fields(partialFields).
|
Fields(partialFields).
|
||||||
SupportsAllDrives(true).
|
SupportsAllDrives(true).
|
||||||
|
@ -2925,6 +3064,26 @@ func (o *baseObject) update(ctx context.Context, updateInfo *drive.File, uploadM
|
||||||
//
|
//
|
||||||
// The new object may have been created if an error is returned
|
// The new object may have been created if an error is returned
|
||||||
func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
|
func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
|
||||||
|
// If o is a shortcut
|
||||||
|
if isShortcutID(o.id) {
|
||||||
|
// Delete it first
|
||||||
|
err := o.fs.delete(ctx, shortcutID(o.id), o.fs.opt.UseTrash)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Then put the file as a new file
|
||||||
|
newObj, err := o.fs.PutUnchecked(ctx, in, src, options...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Update the object
|
||||||
|
if newO, ok := newObj.(*Object); ok {
|
||||||
|
*o = *newO
|
||||||
|
} else {
|
||||||
|
fs.Debugf(newObj, "Failed to update object %T from new object %T", o, newObj)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
srcMimeType := fs.MimeType(ctx, src)
|
srcMimeType := fs.MimeType(ctx, src)
|
||||||
updateInfo := &drive.File{
|
updateInfo := &drive.File{
|
||||||
MimeType: srcMimeType,
|
MimeType: srcMimeType,
|
||||||
|
@ -2998,25 +3157,7 @@ func (o *baseObject) Remove(ctx context.Context) error {
|
||||||
if o.parents > 1 {
|
if o.parents > 1 {
|
||||||
return errors.New("can't delete safely - has multiple parents")
|
return errors.New("can't delete safely - has multiple parents")
|
||||||
}
|
}
|
||||||
var err error
|
return o.fs.delete(ctx, shortcutID(o.id), o.fs.opt.UseTrash)
|
||||||
err = o.fs.pacer.Call(func() (bool, error) {
|
|
||||||
if o.fs.opt.UseTrash {
|
|
||||||
info := drive.File{
|
|
||||||
Trashed: true,
|
|
||||||
}
|
|
||||||
_, err = o.fs.svc.Files.Update(o.id, &info).
|
|
||||||
Fields("").
|
|
||||||
SupportsAllDrives(true).
|
|
||||||
Do()
|
|
||||||
} else {
|
|
||||||
err = o.fs.svc.Files.Delete(o.id).
|
|
||||||
Fields("").
|
|
||||||
SupportsAllDrives(true).
|
|
||||||
Do()
|
|
||||||
}
|
|
||||||
return o.fs.shouldRetry(err)
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MimeType of an Object if known, "" otherwise
|
// MimeType of an Object if known, "" otherwise
|
||||||
|
|
|
@ -382,6 +382,46 @@ files. If deleting them permanently is required then use the
|
||||||
`--drive-use-trash=false` flag, or set the equivalent environment
|
`--drive-use-trash=false` flag, or set the equivalent environment
|
||||||
variable.
|
variable.
|
||||||
|
|
||||||
|
### Shortcuts ###
|
||||||
|
|
||||||
|
In March 2020 Google introduced a new feature in Google Drive called
|
||||||
|
[drive shortcuts](https://support.google.com/drive/answer/9700156)
|
||||||
|
([API](https://developers.google.com/drive/api/v3/shortcuts)). These
|
||||||
|
will (by September 2020) [replace the ability for files or folders to
|
||||||
|
be in multiple folders at once](https://cloud.google.com/blog/products/g-suite/simplifying-google-drives-folder-structure-and-sharing-models).
|
||||||
|
|
||||||
|
Shortcuts are files that link to other files on Google Drive somewhat
|
||||||
|
like a symlink in unix, except they point to the underlying file data
|
||||||
|
(eg the inode in unix terms) so they don't break if the source is
|
||||||
|
renamed or moved about.
|
||||||
|
|
||||||
|
Be default rclone treats these as follows.
|
||||||
|
|
||||||
|
For shortcuts pointing to files:
|
||||||
|
|
||||||
|
- When listing a file shortcut appears as the destination file.
|
||||||
|
- When downloading the contents of the destination file is downloaded.
|
||||||
|
- When updating shortcut file with a non shortcut file, the shortcut is removed then a new file is uploaded in place of the shortcut.
|
||||||
|
- When server side moving (renaming) the shortcut is renamed, not the destination file.
|
||||||
|
- When server side copying the shortcut is copied, not the contents of the shortcut.
|
||||||
|
- When deleting the shortcut is deleted not the linked file.
|
||||||
|
- When setting the modification time, the modification time of the linked file will be set.
|
||||||
|
|
||||||
|
For shortcuts pointing to folders:
|
||||||
|
|
||||||
|
- When listing the shortcut appears as a folder and that folder will contain the contents of the linked folder appear (including any sub folders)
|
||||||
|
- When downloading the contents of the linked folder and sub contents are downloaded
|
||||||
|
- When uploading to a shortcut folder the file will be placed in the linked folder
|
||||||
|
- When server side moving (renaming) the shortcut is renamed, not the destination folder
|
||||||
|
- When server side copying the contents of the linked folder is copied, not the shortcut.
|
||||||
|
- When deleting with `rclone rmdir` or `rclone purge` the shortcut is deleted not the linked folder.
|
||||||
|
- **NB** When deleting with `rclone remove` or `rclone mount` the contents of the linked folder will be deleted.
|
||||||
|
|
||||||
|
It isn't currently possible to create shortcuts with rclone.
|
||||||
|
|
||||||
|
Shortcuts can be completely ignored with the `--drive-skip-shortcuts` flag
|
||||||
|
or the corresponding `skip_shortcuts` configuration setting.
|
||||||
|
|
||||||
### Emptying trash ###
|
### Emptying trash ###
|
||||||
|
|
||||||
If you wish to empty your trash you can use the `rclone cleanup remote:`
|
If you wish to empty your trash you can use the `rclone cleanup remote:`
|
||||||
|
@ -935,6 +975,20 @@ See: https://github.com/rclone/rclone/issues/3857
|
||||||
- Type: bool
|
- Type: bool
|
||||||
- Default: false
|
- Default: false
|
||||||
|
|
||||||
|
#### --drive-skip-shortcuts
|
||||||
|
|
||||||
|
If set skip shortcut files
|
||||||
|
|
||||||
|
Normally rclone dereferences shortcut files making them appear as if
|
||||||
|
they are the original file (see [the shortcuts section](#shortcuts)).
|
||||||
|
If this flag is set then rclone will ignore shortcut files completely.
|
||||||
|
|
||||||
|
|
||||||
|
- Config: skip_shortcuts
|
||||||
|
- Env Var: RCLONE_DRIVE_SKIP_SHORTCUTS
|
||||||
|
- Type: bool
|
||||||
|
- Default: false
|
||||||
|
|
||||||
#### --drive-encoding
|
#### --drive-encoding
|
||||||
|
|
||||||
This sets the encoding for the backend.
|
This sets the encoding for the backend.
|
||||||
|
|
Loading…
Add table
Reference in a new issue