forked from TrueCloudLab/rclone
drive: add document links
This commit is contained in:
parent
0b2fc621fc
commit
70b30d5ca4
3 changed files with 187 additions and 5 deletions
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue