From a8267d16282099f1135c4652fb5900dcd1690e14 Mon Sep 17 00:00:00 2001 From: Stefan Date: Thu, 29 Mar 2018 09:10:19 +0200 Subject: [PATCH] link: allow creating public link to files and folders - closes #1562 --- .../amazonclouddrive/amazonclouddrive_test.go | 1 + backend/azureblob/azureblob_test.go | 1 + backend/b2/b2_test.go | 1 + backend/box/box_test.go | 1 + backend/cache/cache_test.go | 1 + backend/crypt/crypt2_test.go | 1 + backend/crypt/crypt3_test.go | 1 + backend/crypt/crypt_test.go | 1 + backend/drive/drive.go | 36 ++++++++ backend/drive/drive_test.go | 1 + backend/dropbox/dropbox.go | 85 +++++++++++++++---- backend/dropbox/dropbox_test.go | 1 + backend/ftp/ftp_test.go | 1 + .../googlecloudstorage_test.go | 1 + backend/hubic/hubic_test.go | 1 + backend/local/local_test.go | 1 + backend/onedrive/onedrive_test.go | 1 + backend/pcloud/pcloud_test.go | 1 + backend/qingstor/qingstor_test.go | 1 + backend/s3/s3_test.go | 1 + backend/sftp/sftp_test.go | 1 + backend/swift/swift_test.go | 1 + backend/webdav/webdav_test.go | 1 + backend/yandex/yandex_test.go | 1 + cmd/all/all.go | 1 + cmd/cmd.go | 6 +- cmd/link/link.go | 41 +++++++++ docs/content/overview.md | 48 ++++++----- fs/fs.go | 12 +++ fs/operations/operations.go | 9 ++ fstest/fstests/fstests.go | 56 ++++++++++++ 31 files changed, 275 insertions(+), 41 deletions(-) create mode 100644 cmd/link/link.go diff --git a/backend/amazonclouddrive/amazonclouddrive_test.go b/backend/amazonclouddrive/amazonclouddrive_test.go index 7c76a66a9..2a581aec9 100644 --- a/backend/amazonclouddrive/amazonclouddrive_test.go +++ b/backend/amazonclouddrive/amazonclouddrive_test.go @@ -71,6 +71,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/azureblob/azureblob_test.go b/backend/azureblob/azureblob_test.go index 4ad3b2fe3..5082ab292 100644 --- a/backend/azureblob/azureblob_test.go +++ b/backend/azureblob/azureblob_test.go @@ -71,6 +71,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/b2/b2_test.go b/backend/b2/b2_test.go index 6a81f3a16..8397c8697 100644 --- a/backend/b2/b2_test.go +++ b/backend/b2/b2_test.go @@ -68,6 +68,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/box/box_test.go b/backend/box/box_test.go index cfe47f3b4..83d09fc5d 100644 --- a/backend/box/box_test.go +++ b/backend/box/box_test.go @@ -68,6 +68,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/cache/cache_test.go b/backend/cache/cache_test.go index 30b5e6df2..ba292d7c1 100644 --- a/backend/cache/cache_test.go +++ b/backend/cache/cache_test.go @@ -72,6 +72,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/crypt/crypt2_test.go b/backend/crypt/crypt2_test.go index 7924c2993..db5987596 100644 --- a/backend/crypt/crypt2_test.go +++ b/backend/crypt/crypt2_test.go @@ -69,6 +69,7 @@ func TestObjectUpdate2(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable2(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile2(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound2(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink2(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove2(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream2(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge2(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/crypt/crypt3_test.go b/backend/crypt/crypt3_test.go index 06c9dc9bc..38d175959 100644 --- a/backend/crypt/crypt3_test.go +++ b/backend/crypt/crypt3_test.go @@ -69,6 +69,7 @@ func TestObjectUpdate3(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable3(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile3(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound3(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink3(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove3(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream3(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge3(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/crypt/crypt_test.go b/backend/crypt/crypt_test.go index 749cd2f8f..6eb8bcb73 100644 --- a/backend/crypt/crypt_test.go +++ b/backend/crypt/crypt_test.go @@ -69,6 +69,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/drive/drive.go b/backend/drive/drive.go index abcdb46c3..0d66f2511 100644 --- a/backend/drive/drive.go +++ b/backend/drive/drive.go @@ -1095,6 +1095,41 @@ func (f *Fs) Move(src fs.Object, remote string) (fs.Object, error) { return dstObj, nil } +// PublicLink adds a "readable by anyone with link" permission on the given file or folder. +func (f *Fs) PublicLink(remote string) (link string, err error) { + id, err := f.dirCache.FindDir(remote, false) + if err == nil { + fs.Debugf(f, "attempting to share directory '%s'", remote) + } else { + fs.Debugf(f, "attempting to share single file '%s'", remote) + o := &Object{ + fs: f, + remote: remote, + } + if err = o.readMetaData(); err != nil { + return + } + id = o.id + } + + permission := &drive.Permission{ + AllowFileDiscovery: false, + Role: "reader", + Type: "anyone", + } + + err = f.pacer.Call(func() (bool, error) { + // TODO: On TeamDrives this might fail if lacking permissions to change ACLs. + // Need to either check `canShare` attribute on the object or see if a sufficient permission is already present. + _, err = f.svc.Permissions.Create(id, permission).Fields(googleapi.Field("id")).SupportsTeamDrives(f.isTeamDrive).Do() + return shouldRetry(err) + }) + if err != nil { + return "", err + } + return fmt.Sprintf("https://drive.google.com/open?id=%s", id), nil +} + // DirMove moves src, srcRemote to this remote at dstRemote // using server side move operations. // @@ -1597,6 +1632,7 @@ var ( _ fs.DirCacheFlusher = (*Fs)(nil) _ fs.ChangeNotifier = (*Fs)(nil) _ fs.PutUncheckeder = (*Fs)(nil) + _ fs.PublicLinker = (*Fs)(nil) _ fs.MergeDirser = (*Fs)(nil) _ fs.Object = (*Object)(nil) _ fs.MimeTyper = &Object{} diff --git a/backend/drive/drive_test.go b/backend/drive/drive_test.go index a2b1232df..3b644993f 100644 --- a/backend/drive/drive_test.go +++ b/backend/drive/drive_test.go @@ -68,6 +68,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/dropbox/dropbox.go b/backend/dropbox/dropbox.go index 66fbdbe6c..05689ab1b 100644 --- a/backend/dropbox/dropbox.go +++ b/backend/dropbox/dropbox.go @@ -36,6 +36,7 @@ import ( "github.com/dropbox/dropbox-sdk-go-unofficial/dropbox" "github.com/dropbox/dropbox-sdk-go-unofficial/dropbox/files" + "github.com/dropbox/dropbox-sdk-go-unofficial/dropbox/sharing" "github.com/ncw/rclone/fs" "github.com/ncw/rclone/fs/config" "github.com/ncw/rclone/fs/config/flags" @@ -126,13 +127,14 @@ func init() { // Fs represents a remote dropbox server type Fs struct { - name string // name of this remote - root string // the path we are working on - features *fs.Features // optional features - srv files.Client // the connection to the dropbox server - slashRoot string // root with "/" prefix, lowercase - slashRootSlash string // root with "/" prefix and postfix, lowercase - pacer *pacer.Pacer // To pace the API calls + name string // name of this remote + root string // the path we are working on + features *fs.Features // optional features + srv files.Client // the connection to the dropbox server + sharingClient sharing.Client // as above, but for generating sharing links + slashRoot string // root with "/" prefix, lowercase + slashRootSlash string // root with "/" prefix and postfix, lowercase + pacer *pacer.Pacer // To pace the API calls } // Object describes a dropbox object @@ -210,11 +212,13 @@ func NewFs(name, root string) (fs.Fs, error) { Client: oAuthClient, // maybe??? } srv := files.New(config) + sharingClient := sharing.New(config) f := &Fs{ - name: name, - srv: srv, - pacer: pacer.New().SetMinSleep(minSleep).SetMaxSleep(maxSleep).SetDecayConstant(decayConstant), + name: name, + srv: srv, + sharingClient: sharingClient, + pacer: pacer.New().SetMinSleep(minSleep).SetMaxSleep(maxSleep).SetDecayConstant(decayConstant), } f.features = (&fs.Features{ CaseInsensitive: true, @@ -640,6 +644,52 @@ func (f *Fs) Move(src fs.Object, remote string) (fs.Object, error) { return dstObj, nil } +// PublicLink adds a "readable by anyone with link" permission on the given file or folder. +func (f *Fs) PublicLink(remote string) (link string, err error) { + absPath := "/" + path.Join(f.Root(), remote) + fs.Debugf(f, "attempting to share '%s' (absolute path: %s)", remote, absPath) + createArg := sharing.CreateSharedLinkWithSettingsArg{ + Path: absPath, + } + var linkRes sharing.IsSharedLinkMetadata + err = f.pacer.Call(func() (bool, error) { + linkRes, err = f.sharingClient.CreateSharedLinkWithSettings(&createArg) + return shouldRetry(err) + }) + + if err != nil && strings.Contains(err.Error(), sharing.CreateSharedLinkWithSettingsErrorSharedLinkAlreadyExists) { + fs.Debugf(absPath, "has a public link already, attempting to retrieve it") + listArg := sharing.ListSharedLinksArg{ + Path: absPath, + DirectOnly: true, + } + var listRes *sharing.ListSharedLinksResult + err = f.pacer.Call(func() (bool, error) { + listRes, err = f.sharingClient.ListSharedLinks(&listArg) + return shouldRetry(err) + }) + if err != nil { + return + } + if len(listRes.Links) == 0 { + err = errors.New("Dropbox says the sharing link already exists, but list came back empty") + return + } + linkRes = listRes.Links[0] + } + if err == nil { + switch res := linkRes.(type) { + case *sharing.FileLinkMetadata: + link = res.Url + case *sharing.FolderLinkMetadata: + link = res.Url + default: + err = fmt.Errorf("Don't know how to extract link, response has unknown format: %T", res) + } + } + return +} + // DirMove moves src, srcRemote to this remote at dstRemote // using server side move operations. // @@ -975,11 +1025,12 @@ func (o *Object) Remove() (err error) { // Check the interfaces are satisfied var ( - _ fs.Fs = (*Fs)(nil) - _ fs.Copier = (*Fs)(nil) - _ fs.Purger = (*Fs)(nil) - _ fs.PutStreamer = (*Fs)(nil) - _ fs.Mover = (*Fs)(nil) - _ fs.DirMover = (*Fs)(nil) - _ fs.Object = (*Object)(nil) + _ fs.Fs = (*Fs)(nil) + _ fs.Copier = (*Fs)(nil) + _ fs.Purger = (*Fs)(nil) + _ fs.PutStreamer = (*Fs)(nil) + _ fs.Mover = (*Fs)(nil) + _ fs.PublicLinker = (*Fs)(nil) + _ fs.DirMover = (*Fs)(nil) + _ fs.Object = (*Object)(nil) ) diff --git a/backend/dropbox/dropbox_test.go b/backend/dropbox/dropbox_test.go index 917b9f828..7db74df50 100644 --- a/backend/dropbox/dropbox_test.go +++ b/backend/dropbox/dropbox_test.go @@ -71,6 +71,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/ftp/ftp_test.go b/backend/ftp/ftp_test.go index daafe98b3..86262cf12 100644 --- a/backend/ftp/ftp_test.go +++ b/backend/ftp/ftp_test.go @@ -68,6 +68,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/googlecloudstorage/googlecloudstorage_test.go b/backend/googlecloudstorage/googlecloudstorage_test.go index 3dc46a839..15fe2a9f8 100644 --- a/backend/googlecloudstorage/googlecloudstorage_test.go +++ b/backend/googlecloudstorage/googlecloudstorage_test.go @@ -68,6 +68,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/hubic/hubic_test.go b/backend/hubic/hubic_test.go index 1dc7ca061..0987a0fe0 100644 --- a/backend/hubic/hubic_test.go +++ b/backend/hubic/hubic_test.go @@ -68,6 +68,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/local/local_test.go b/backend/local/local_test.go index febe5aff0..e8c3ebd58 100644 --- a/backend/local/local_test.go +++ b/backend/local/local_test.go @@ -68,6 +68,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/onedrive/onedrive_test.go b/backend/onedrive/onedrive_test.go index 5539083c9..070aa58d6 100644 --- a/backend/onedrive/onedrive_test.go +++ b/backend/onedrive/onedrive_test.go @@ -68,6 +68,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/pcloud/pcloud_test.go b/backend/pcloud/pcloud_test.go index d4414d17f..b49d98400 100644 --- a/backend/pcloud/pcloud_test.go +++ b/backend/pcloud/pcloud_test.go @@ -68,6 +68,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/qingstor/qingstor_test.go b/backend/qingstor/qingstor_test.go index d530d0063..7c0880e13 100644 --- a/backend/qingstor/qingstor_test.go +++ b/backend/qingstor/qingstor_test.go @@ -71,6 +71,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/s3/s3_test.go b/backend/s3/s3_test.go index c3da338ac..718ce0d26 100644 --- a/backend/s3/s3_test.go +++ b/backend/s3/s3_test.go @@ -68,6 +68,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/sftp/sftp_test.go b/backend/sftp/sftp_test.go index 232720e0b..1df429aaf 100644 --- a/backend/sftp/sftp_test.go +++ b/backend/sftp/sftp_test.go @@ -71,6 +71,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/swift/swift_test.go b/backend/swift/swift_test.go index 9cb980426..d0d312863 100644 --- a/backend/swift/swift_test.go +++ b/backend/swift/swift_test.go @@ -68,6 +68,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/webdav/webdav_test.go b/backend/webdav/webdav_test.go index 15bd22081..825d26d2b 100644 --- a/backend/webdav/webdav_test.go +++ b/backend/webdav/webdav_test.go @@ -68,6 +68,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/backend/yandex/yandex_test.go b/backend/yandex/yandex_test.go index 1c888a791..30c57314d 100644 --- a/backend/yandex/yandex_test.go +++ b/backend/yandex/yandex_test.go @@ -68,6 +68,7 @@ func TestObjectUpdate(t *testing.T) { fstests.TestObjectUpdate(t) } func TestObjectStorable(t *testing.T) { fstests.TestObjectStorable(t) } func TestFsIsFile(t *testing.T) { fstests.TestFsIsFile(t) } func TestFsIsFileNotFound(t *testing.T) { fstests.TestFsIsFileNotFound(t) } +func TestPublicLink(t *testing.T) { fstests.TestPublicLink(t) } func TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) } func TestFsPutStream(t *testing.T) { fstests.TestFsPutStream(t) } func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) } diff --git a/cmd/all/all.go b/cmd/all/all.go index 2b53370c2..388a86c8a 100644 --- a/cmd/all/all.go +++ b/cmd/all/all.go @@ -21,6 +21,7 @@ import ( _ "github.com/ncw/rclone/cmd/genautocomplete" _ "github.com/ncw/rclone/cmd/gendocs" _ "github.com/ncw/rclone/cmd/info" + _ "github.com/ncw/rclone/cmd/link" _ "github.com/ncw/rclone/cmd/listremotes" _ "github.com/ncw/rclone/cmd/ls" _ "github.com/ncw/rclone/cmd/lsd" diff --git a/cmd/cmd.go b/cmd/cmd.go index c71f0de2e..2ea397a69 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -141,10 +141,10 @@ func ShowVersion() { fmt.Printf("- go version: %s\n", runtime.Version()) } -// newFsFile creates a dst Fs from a name but may point to a file. +// NewFsFile creates a dst Fs from a name but may point to a file. // // It returns a string with the file name if points to a file -func newFsFile(remote string) (fs.Fs, string) { +func NewFsFile(remote string) (fs.Fs, string) { fsInfo, configName, fsPath, err := fs.ParseRemote(remote) if err != nil { fs.CountError(err) @@ -169,7 +169,7 @@ func newFsFile(remote string) (fs.Fs, string) { // // This can point to a file func newFsSrc(remote string) (fs.Fs, string) { - f, fileName := newFsFile(remote) + f, fileName := NewFsFile(remote) if fileName != "" { if !filter.Active.InActive() { err := errors.Errorf("Can't limit to single files when using filters: %v", remote) diff --git a/cmd/link/link.go b/cmd/link/link.go new file mode 100644 index 000000000..c725f8eae --- /dev/null +++ b/cmd/link/link.go @@ -0,0 +1,41 @@ +package link + +import ( + "fmt" + + "github.com/ncw/rclone/cmd" + "github.com/ncw/rclone/fs/operations" + "github.com/spf13/cobra" +) + +func init() { + cmd.Root.AddCommand(commandDefintion) +} + +var commandDefintion = &cobra.Command{ + Use: "link remote:path", + Short: `Generate public link to file/folder.`, + Long: ` +rclone link will create or retrieve a public link to the given file or folder. + + rclone link remote:path/to/file + rclone link remote:path/to/folder/ + +If successful, the last line of the output will contain the link. Exact +capabilities depend on the remote, but the link will always be created with +the least constraints – e.g. no expiry, no password protection, accessible +without account. +`, + Run: func(command *cobra.Command, args []string) { + cmd.CheckArgs(1, 1, command, args) + fsrc, remote := cmd.NewFsFile(args[0]) + cmd.Run(false, false, command, func() error { + link, err := operations.PublicLink(fsrc, remote) + if err != nil { + return err + } + fmt.Println(link) + return nil + }) + }, +} diff --git a/docs/content/overview.md b/docs/content/overview.md index 8da19f88e..5851e3541 100644 --- a/docs/content/overview.md +++ b/docs/content/overview.md @@ -119,27 +119,27 @@ All the remotes support a basic set of features, but there are some optional features supported by some remotes used to make some operations more efficient. -| Name | Purge | Copy | Move | DirMove | CleanUp | ListR | StreamUpload | -| ---------------------------- |:-----:|:----:|:----:|:-------:|:-------:|:-----:|:------------:| -| Amazon Drive | Yes | No | Yes | Yes | No [#575](https://github.com/ncw/rclone/issues/575) | No | No | -| Amazon S3 | No | Yes | No | No | No | Yes | Yes | -| Backblaze B2 | No | No | No | No | Yes | Yes | Yes | -| Box | Yes | Yes | Yes | Yes | No [#575](https://github.com/ncw/rclone/issues/575) | No | Yes | -| Dropbox | Yes | Yes | Yes | Yes | No [#575](https://github.com/ncw/rclone/issues/575) | No | Yes | -| FTP | No | No | Yes | Yes | No | No | Yes | -| Google Cloud Storage | Yes | Yes | No | No | No | Yes | Yes | -| Google Drive | Yes | Yes | Yes | Yes | Yes | No | Yes | -| HTTP | No | No | No | No | No | No | No | -| Hubic | Yes † | Yes | No | No | No | Yes | Yes | -| Microsoft Azure Blob Storage | Yes | Yes | No | No | No | Yes | No | -| Microsoft OneDrive | Yes | Yes | Yes | No [#197](https://github.com/ncw/rclone/issues/197) | No [#575](https://github.com/ncw/rclone/issues/575) | No | No | -| Openstack Swift | Yes † | Yes | No | No | No | Yes | Yes | -| pCloud | Yes | Yes | Yes | Yes | Yes | No | No | -| QingStor | No | Yes | No | No | No | Yes | No | -| SFTP | No | No | Yes | Yes | No | No | Yes | -| WebDAV | Yes | Yes | Yes | Yes | No | No | Yes ‡ | -| Yandex Disk | Yes | No | No | No | Yes | Yes | Yes | -| The local filesystem | Yes | No | Yes | Yes | No | No | Yes | +| Name | Purge | Copy | Move | DirMove | CleanUp | ListR | StreamUpload | LinkSharing | +| ---------------------------- |:-----:|:----:|:----:|:-------:|:-------:|:-----:|:------------:|:------------:| +| Amazon Drive | Yes | No | Yes | Yes | No [#575](https://github.com/ncw/rclone/issues/575) | No | No | No [#2178](https://github.com/ncw/rclone/issues/2178) | +| Amazon S3 | No | Yes | No | No | No | Yes | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | +| Backblaze B2 | No | No | No | No | Yes | Yes | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | +| Box | Yes | Yes | Yes | Yes | No [#575](https://github.com/ncw/rclone/issues/575) | No | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | +| Dropbox | Yes | Yes | Yes | Yes | No [#575](https://github.com/ncw/rclone/issues/575) | No | Yes | Yes | +| FTP | No | No | Yes | Yes | No | No | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | +| Google Cloud Storage | Yes | Yes | No | No | No | Yes | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | +| Google Drive | Yes | Yes | Yes | Yes | Yes | No | Yes | Yes | +| HTTP | No | No | No | No | No | No | No | No [#2178](https://github.com/ncw/rclone/issues/2178) | +| Hubic | Yes † | Yes | No | No | No | Yes | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | +| Microsoft Azure Blob Storage | Yes | Yes | No | No | No | Yes | No | No [#2178](https://github.com/ncw/rclone/issues/2178) | +| Microsoft OneDrive | Yes | Yes | Yes | No [#197](https://github.com/ncw/rclone/issues/197) | No [#575](https://github.com/ncw/rclone/issues/575) | No | No | No [#2178](https://github.com/ncw/rclone/issues/2178) | +| Openstack Swift | Yes † | Yes | No | No | No | Yes | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | +| pCloud | Yes | Yes | Yes | Yes | Yes | No | No | No [#2178](https://github.com/ncw/rclone/issues/2178) | +| QingStor | No | Yes | No | No | No | Yes | No | No [#2178](https://github.com/ncw/rclone/issues/2178) | +| SFTP | No | No | Yes | Yes | No | No | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | +| WebDAV | Yes | Yes | Yes | Yes | No | No | Yes ‡ | No [#2178](https://github.com/ncw/rclone/issues/2178) | +| Yandex Disk | Yes | No | No | No | Yes | Yes | Yes | No [#2178](https://github.com/ncw/rclone/issues/2178) | +| The local filesystem | Yes | No | Yes | Yes | No | No | Yes | No | ### Purge ### @@ -196,3 +196,9 @@ See the [rclone docs](/docs/#fast-list) for more details. Some remotes allow files to be uploaded without knowing the file size in advance. This allows certain operations to work without spooling the file to local disk first, e.g. `rclone rcat`. + +### LinkSharing ### + +Sets the necessary permissions on a file or folder and prints a link +that allows others to access them, even if they don't have an account +on the particular cloud provider. diff --git a/fs/fs.go b/fs/fs.go index c46d7b35e..f60e507fc 100644 --- a/fs/fs.go +++ b/fs/fs.go @@ -328,6 +328,9 @@ type Features struct { // as an optional interface DirCacheFlush func() + // PublicLink generates a public link to the remote path (usually readable by anyone) + PublicLink func(remote string) (string, error) + // Put in to the remote path with the modTime given of the given size // // May create the object even if it returns an error - if so @@ -443,6 +446,9 @@ func (ft *Features) Fill(f Fs) *Features { if do, ok := f.(DirCacheFlusher); ok { ft.DirCacheFlush = do.DirCacheFlush } + if do, ok := f.(PublicLinker); ok { + ft.PublicLink = do.PublicLink + } if do, ok := f.(PutUncheckeder); ok { ft.PutUnchecked = do.PutUnchecked } @@ -642,6 +648,12 @@ type PutStreamer interface { PutStream(in io.Reader, src ObjectInfo, options ...OpenOption) (Object, error) } +// PublicLinker is an optional interface for Fs +type PublicLinker interface { + // PublicLink generates a public link to the remote path (usually readable by anyone) + PublicLink(remote string) (string, error) +} + // MergeDirser is an option interface for Fs type MergeDirser interface { // MergeDirs merges the contents of all the directories passed diff --git a/fs/operations/operations.go b/fs/operations/operations.go index 891a6c36b..6c930c035 100644 --- a/fs/operations/operations.go +++ b/fs/operations/operations.go @@ -1470,6 +1470,15 @@ func Rcat(fdst fs.Fs, dstFileName string, in io.ReadCloser, modTime time.Time) ( return dst, nil } +// PublicLink adds a "readable by anyone with link" permission on the given file or folder. +func PublicLink(f fs.Fs, remote string) (string, error) { + doPublicLink := f.Features().PublicLink + if doPublicLink == nil { + return "", errors.Errorf("%v doesn't support public links", f) + } + return doPublicLink(remote) +} + // Rmdirs removes any empty directories (or directories only // containing empty directories) under f, including f. func Rmdirs(f fs.Fs, dir string, leaveRoot bool) error { diff --git a/fstest/fstests/fstests.go b/fstest/fstests/fstests.go index e1a1ac1ac..c0e19e0d3 100644 --- a/fstest/fstests/fstests.go +++ b/fstest/fstests/fstests.go @@ -948,6 +948,62 @@ func TestFsIsFileNotFound(t *testing.T) { fstest.CheckListing(t, fileRemote, []fstest.Item{}) } +// TestPublicLink tests creation of sharable, public links +func TestPublicLink(t *testing.T) { + skipIfNotOk(t) + + doPublicLink := remote.Features().PublicLink + if doPublicLink == nil { + t.Skip("FS has no PublicLinker interface") + } + + // if object not found + link, err := doPublicLink(file1.Path + "_does_not_exist") + require.Error(t, err, "Expected to get error when file doesn't exist") + require.Equal(t, "", link, "Expected link to be empty on error") + + // sharing file for the first time + link1, err := doPublicLink(file1.Path) + require.NoError(t, err) + require.NotEqual(t, "", link1, "Link should not be empty") + + link2, err := doPublicLink(file2.Path) + require.NoError(t, err) + require.NotEqual(t, "", link2, "Link should not be empty") + + require.NotEqual(t, link1, link2, "Links to different files should differ") + + // sharing file for the 2nd time + link1, err = doPublicLink(file1.Path) + require.NoError(t, err) + require.NotEqual(t, "", link1, "Link should not be empty") + + // sharing directory for the first time + path := path.Dir(file2.Path) + link3, err := doPublicLink(path) + require.NoError(t, err) + require.NotEqual(t, "", link3, "Link should not be empty") + + // sharing directory for the second time + link3, err = doPublicLink(path) + require.NoError(t, err) + require.NotEqual(t, "", link3, "Link should not be empty") + + // sharing the "root" directory in a subremote + subRemote, _, removeSubRemote, err := fstest.RandomRemote(RemoteName, false) + require.NoError(t, err) + defer removeSubRemote() + // ensure sub remote isn't empty + buf := bytes.NewBufferString("somecontent") + obji := object.NewStaticObjectInfo("somefile", time.Now(), int64(buf.Len()), true, nil, nil) + _, err = subRemote.Put(buf, obji) + require.NoError(t, err) + + link4, err := subRemote.Features().PublicLink("") + require.NoError(t, err, "Sharing root in a sub-remote should work") + require.NotEqual(t, "", link4, "Link should not be empty") +} + // TestObjectRemove tests Remove func TestObjectRemove(t *testing.T) { skipIfNotOk(t)