drive: add document links

This commit is contained in:
Fabian Möller 2018-08-21 12:51:36 +02:00 committed by Nick Craig-Wood
parent 0b2fc621fc
commit 70b30d5ca4
3 changed files with 187 additions and 5 deletions

View File

@ -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 = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>URL</key>
<string>{{ .URL }}</string>
</dict>
</plist>
`
desktopTemplate = `[Desktop Entry]
Encoding=UTF-8
Name={{ .Title }}
URL={{ .URL }}
Icon=text-html
Type=Link
`
htmlTemplate = `<html>
<head>
<meta http-equiv="refresh" content="0; url={{ .URL }}" />
<title>{{ .Title }}</title>
</head>
<body>
Loading <a href="{{ .URL }}">{{ .Title }}</a>
</body>
</html>
`
)
// Check the interfaces are satisfied
var (
_ fs.Fs = (*Fs)(nil)
@ -2451,4 +2587,7 @@ var (
_ fs.Object = (*documentObject)(nil)
_ fs.MimeTyper = (*documentObject)(nil)
_ fs.IDer = (*documentObject)(nil)
_ fs.Object = (*linkObject)(nil)
_ fs.MimeTyper = (*linkObject)(nil)
_ fs.IDer = (*linkObject)(nil)
)

View File

@ -7,6 +7,7 @@ import (
"io/ioutil"
"mime"
"path/filepath"
"strings"
"testing"
_ "github.com/ncw/rclone/backend/local"
@ -213,10 +214,39 @@ func (f *Fs) InternalTestDocumentExport(t *testing.T) {
}
}
func (f *Fs) InternalTestDocumentLink(t *testing.T) {
var buf bytes.Buffer
var err error
f.exportExtensions, _, err = parseExtensions("link.html")
require.NoError(t, err)
obj, err := f.NewObject("example2.link.html")
require.NoError(t, err)
rc, err := obj.Open()
require.NoError(t, err)
defer func() { require.NoError(t, rc.Close()) }()
_, err = io.Copy(&buf, rc)
require.NoError(t, err)
text := buf.String()
require.True(t, strings.HasPrefix(text, "<html>"))
require.True(t, strings.HasSuffix(text, "</html>\n"))
for _, excerpt := range []string{
`<meta http-equiv="refresh"`,
`Loading <a href="`,
} {
require.Contains(t, text, excerpt)
}
}
func (f *Fs) InternalTest(t *testing.T) {
t.Run("DocumentImport", f.InternalTestDocumentImport)
t.Run("DocumentUpdate", f.InternalTestDocumentUpdate)
t.Run("DocumentExport", f.InternalTestDocumentExport)
t.Run("DocumentLink", f.InternalTestDocumentLink)
}
var _ fstests.InternalTester = (*Fs)(nil)

View File

@ -495,6 +495,19 @@ represent the currently available converions.
| xlsx | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet | Microsoft Office Spreadsheet |
| zip | application/zip | A ZIP file of HTML, Images CSS |
Google douments can also be exported as link files. These files will
open a browser window for the Google Docs website of that dument
when opened. The link file extension has to be specified as a
`--drive-export-formats` parameter. They will match all available
Google Documents.
| Extension | Description | OS Support |
| --------- | ----------- | ---------- |
| desktop | freedesktop.org specified desktop entry | Linux |
| link.html | An HTML Document with a redirect | All |
| url | INI style link file | macOS, Windows |
| webloc | macOS specific XML format | macOS |
#### --drive-alternate-export ####
If this option is set this instructs rclone to use an alternate set of