diff --git a/backend/drive/drive.go b/backend/drive/drive.go
index f582e4574..250b20b0b 100644
--- a/backend/drive/drive.go
+++ b/backend/drive/drive.go
@@ -21,6 +21,7 @@ import (
"strconv"
"strings"
"sync"
+ "text/template"
"time"
"github.com/ncw/rclone/fs"
@@ -103,10 +104,18 @@ var (
"text/plain": ".txt",
"text/tab-separated-values": ".tsv",
}
- partialFields = "id,name,size,md5Checksum,trashed,modifiedTime,createdTime,mimeType,parents"
- fetchFormatsOnce sync.Once // make sure we fetch the export/import formats only once
- _exportFormats map[string][]string // allowed export MIME type conversions
- _importFormats map[string][]string // allowed import MIME type conversions
+ _mimeTypeToExtensionLinks = map[string]string{
+ "application/x-link-desktop": ".desktop",
+ "application/x-link-html": ".link.html",
+ "application/x-link-url": ".url",
+ "application/x-link-webloc": ".webloc",
+ }
+ partialFields = "id,name,size,md5Checksum,trashed,modifiedTime,createdTime,mimeType,parents,webViewLink"
+ fetchFormatsOnce sync.Once // make sure we fetch the export/import formats only once
+ _exportFormats map[string][]string // allowed export MIME type conversions
+ _importFormats map[string][]string // allowed import MIME type conversions
+ templatesOnce sync.Once // parse link templates only once
+ _linkTemplates map[string]*template.Template // available link types
)
// Register with Fs
@@ -284,7 +293,9 @@ func init() {
// register duplicate MIME types first
// this allows them to be used with mime.ExtensionsByType() but
// mime.TypeByExtension() will return the later registered MIME type
- for _, m := range []map[string]string{_mimeTypeToExtensionDuplicates, _mimeTypeToExtension} {
+ for _, m := range []map[string]string{
+ _mimeTypeToExtensionDuplicates, _mimeTypeToExtension, _mimeTypeToExtensionLinks,
+ } {
for mimeType, extension := range m {
if err := mime.AddExtensionType(extension, mimeType); err != nil {
log.Fatalf("Failed to register MIME type %q: %v", mimeType, err)
@@ -351,6 +362,11 @@ type documentObject struct {
documentMimeType string // the original document MIME type
extLen int // The length of the added export extension
}
+type linkObject struct {
+ baseObject
+ content []byte // The file content generated by a link template
+ extLen int // The length of the added export extension
+}
// Object describes a drive object
type Object struct {
@@ -590,6 +606,9 @@ func fixMimeTypeMap(m map[string][]string) map[string][]string {
func isInternalMimeType(mimeType string) bool {
return strings.HasPrefix(mimeType, "application/vnd.google-apps.")
}
+func isLinkMimeType(mimeType string) bool {
+ return strings.HasPrefix(mimeType, "application/x-link-")
+}
// parseExtensions parses a list of comma separated extensions
// into a list of unique extensions with leading "." and a list of associated MIME types
@@ -886,6 +905,32 @@ func (f *Fs) newDocumentObject(remote string, info *drive.File, extension, expor
}, nil
}
+// newLinkObject creates a 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) {
+ t := linkTemplate(exportMimeType)
+ if t == nil {
+ return nil, errors.Errorf("unsupported link type %s", exportMimeType)
+ }
+ var buf bytes.Buffer
+ err := t.Execute(&buf, struct {
+ URL, Title string
+ }{
+ info.WebViewLink, info.Name,
+ })
+ if err != nil {
+ return nil, errors.Wrap(err, "executing template failed")
+ }
+
+ baseObject := f.newBaseObject(remote+extension, info)
+ baseObject.bytes = int64(buf.Len())
+ baseObject.mimeType = exportMimeType
+ return &linkObject{
+ baseObject: baseObject,
+ content: buf.Bytes(),
+ extLen: len(extension),
+ }, nil
+}
+
// newObjectWithInfo creates a fs.Object for any drive.File
//
// When the drive.File cannot be represented as a fs.Object it will return (nil, nil).
@@ -922,6 +967,9 @@ func (f *Fs) newObjectWithExportInfo(
fs.Debugf(remote, "No export formats found for %q", info.MimeType)
return nil, nil
}
+ if isLinkMimeType(exportMimeType) {
+ return f.newLinkObject(remote, info, extension, exportMimeType)
+ }
return f.newDocumentObject(remote, info, extension, exportMimeType)
}
}
@@ -1003,6 +1051,23 @@ func isAuthOwned(item *drive.File) bool {
return false
}
+// linkTemplate returns the Template for a MIME type or nil if the
+// MIME type does not represent a link
+func linkTemplate(mt string) *template.Template {
+ templatesOnce.Do(func() {
+ _linkTemplates = map[string]*template.Template{
+ "application/x-link-desktop": template.Must(
+ template.New("application/x-link-desktop").Parse(desktopTemplate)),
+ "application/x-link-html": template.Must(
+ template.New("application/x-link-html").Parse(htmlTemplate)),
+ "application/x-link-url": template.Must(
+ template.New("application/x-link-url").Parse(urlTemplate)),
+ "application/x-link-webloc": template.Must(
+ template.New("application/x-link-webloc").Parse(weblocTemplate)),
+ }
+ })
+ return _linkTemplates[mt]
+}
func (f *Fs) fetchFormats() {
fetchFormatsOnce.Do(func() {
var about *drive.About
@@ -1053,6 +1118,9 @@ func (f *Fs) findExportFormatByMimeType(itemMimeType string) (
if isDocument {
for _, _extension := range f.exportExtensions {
_mimeType := mime.TypeByExtension(_extension)
+ if isLinkMimeType(_mimeType) {
+ return _extension, _mimeType, true
+ }
for _, emt := range exportMimeTypes {
if emt == _mimeType {
return _extension, _mimeType, true
@@ -1579,6 +1647,9 @@ func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) {
case *documentObject:
srcObj = &src.baseObject
srcRemote, ext = srcRemote[:len(srcRemote)-src.extLen], srcRemote[len(srcRemote)-src.extLen:]
+ case *linkObject:
+ srcObj = &src.baseObject
+ srcRemote, ext = srcRemote[:len(srcRemote)-src.extLen], srcRemote[len(srcRemote)-src.extLen:]
default:
fs.Debugf(src, "Can't copy - not same remote type")
return nil, fs.ErrorCantCopy
@@ -1709,6 +1780,9 @@ func (f *Fs) Move(src fs.Object, remote string) (fs.Object, error) {
case *documentObject:
srcObj = &src.baseObject
srcRemote, ext = srcRemote[:len(srcRemote)-src.extLen], srcRemote[len(srcRemote)-src.extLen:]
+ case *linkObject:
+ srcObj = &src.baseObject
+ srcRemote, ext = srcRemote[:len(srcRemote)-src.extLen], srcRemote[len(srcRemote)-src.extLen:]
default:
fs.Debugf(src, "Can't move - not same remote type")
return nil, fs.ErrorCantMove
@@ -2310,6 +2384,31 @@ func (o *documentObject) Open(options ...fs.OpenOption) (in io.ReadCloser, err e
}
return
}
+func (o *linkObject) Open(options ...fs.OpenOption) (in io.ReadCloser, err error) {
+ var offset, limit int64 = 0, -1
+ var data = o.content
+ for _, option := range options {
+ switch x := option.(type) {
+ case *fs.SeekOption:
+ offset = x.Offset
+ case *fs.RangeOption:
+ offset, limit = x.Decode(int64(len(data)))
+ default:
+ if option.Mandatory() {
+ fs.Logf(o, "Unsupported mandatory option: %v", option)
+ }
+ }
+ }
+ if l := int64(len(data)); offset > l {
+ offset = l
+ }
+ data = data[offset:]
+ if limit != -1 && limit < int64(len(data)) {
+ data = data[:limit]
+ }
+
+ return ioutil.NopCloser(bytes.NewReader(data)), nil
+}
func (o *baseObject) update(updateInfo *drive.File, uploadMimeType string, in io.Reader,
src fs.ObjectInfo) (info *drive.File, err error) {
@@ -2396,6 +2495,10 @@ func (o *documentObject) Update(in io.Reader, src fs.ObjectInfo, options ...fs.O
return nil
}
+func (o *linkObject) Update(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
+ return errors.New("cannot update link files")
+}
+
// Remove an object
func (o *baseObject) Remove() error {
var err error
@@ -2429,6 +2532,39 @@ func (o *baseObject) ID() string {
return o.id
}
+// templates for document link files
+const (
+ urlTemplate = `[InternetShortcut]{{"\r"}}
+URL={{ .URL }}{{"\r"}}
+`
+ weblocTemplate = `
+
+