link: Add --expire and --unlink flags

This adds expire and unlink fields to the PublicLink interface.

This fixes up the affected backends and removes unlink parameters
where they are present.
This commit is contained in:
Roman Kredentser 2020-06-01 00:18:01 +03:00 committed by Nick Craig-Wood
parent fb61ed8506
commit 55ad1354b6
22 changed files with 147 additions and 55 deletions

View File

@ -1357,7 +1357,7 @@ func (f *Fs) getDownloadAuthorization(ctx context.Context, bucket, remote string
}
// PublicLink returns a link for downloading without account
func (f *Fs) PublicLink(ctx context.Context, remote string) (link string, err error) {
func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (link string, err error) {
bucket, bucketPath := f.split(remote)
var RootURL string
if f.opt.DownloadURL == "" {

View File

@ -1024,7 +1024,7 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
}
// PublicLink adds a "readable by anyone with link" permission on the given file or folder.
func (f *Fs) PublicLink(ctx context.Context, remote string) (string, error) {
func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (string, error) {
id, err := f.dirCache.FindDir(ctx, remote, false)
var opts rest.Opts
if err == nil {

View File

@ -656,7 +656,7 @@ func (f *Fs) DirCacheFlush() {
}
// PublicLink generates a public link to the remote path (usually readable by anyone)
func (f *Fs) PublicLink(ctx context.Context, remote string) (string, error) {
func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (string, error) {
do := f.Fs.Features().PublicLink
if do == nil {
return "", errors.New("PublicLink not supported")
@ -664,9 +664,9 @@ func (f *Fs) PublicLink(ctx context.Context, remote string) (string, error) {
o, err := f.NewObject(ctx, remote)
if err != nil {
// assume it is a directory
return do(ctx, f.cipher.EncryptDirName(remote))
return do(ctx, f.cipher.EncryptDirName(remote), expire, unlink)
}
return do(ctx, o.(*Object).Object.Remote())
return do(ctx, o.(*Object).Object.Remote(), expire, unlink)
}
// ChangeNotify calls the passed function with a path

View File

@ -2488,7 +2488,7 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
}
// PublicLink adds a "readable by anyone with link" permission on the given file or folder.
func (f *Fs) PublicLink(ctx context.Context, remote string) (link string, err error) {
func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (link string, err error) {
id, err := f.dirCache.FindDir(ctx, remote, false)
if err == nil {
fs.Debugf(f, "attempting to share directory '%s'", remote)

View File

@ -782,11 +782,14 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object,
}
// PublicLink adds a "readable by anyone with link" permission on the given file or folder.
func (f *Fs) PublicLink(ctx context.Context, remote string) (link string, err error) {
func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (link string, err error) {
absPath := f.opt.Enc.FromStandardPath(path.Join(f.slashRoot, remote))
fs.Debugf(f, "attempting to share '%s' (absolute path: %s)", remote, absPath)
createArg := sharing.CreateSharedLinkWithSettingsArg{
Path: absPath,
Settings: &sharing.SharedLinkSettings{
Expires: time.Now().Add(time.Duration(expire)),
},
}
var linkRes sharing.IsSharedLinkMetadata
err = f.pacer.Call(func() (bool, error) {

View File

@ -150,11 +150,6 @@ func init() {
Help: "Delete files permanently rather than putting them into the trash.",
Default: false,
Advanced: true,
}, {
Name: "unlink",
Help: "Remove existing public link to file/folder with link command rather than creating.\nDefault is false, meaning link command will create or retrieve public link.",
Default: false,
Advanced: true,
}, {
Name: "upload_resume_limit",
Help: "Files bigger than this can be resumed if the upload fail's.",
@ -181,7 +176,6 @@ type Options struct {
MD5MemoryThreshold fs.SizeSuffix `config:"md5_memory_limit"`
TrashedOnly bool `config:"trashed_only"`
HardDelete bool `config:"hard_delete"`
Unlink bool `config:"unlink"`
UploadThreshold fs.SizeSuffix `config:"upload_resume_limit"`
Enc encoder.MultiEncoder `config:"encoding"`
}
@ -1002,14 +996,14 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
}
// PublicLink generates a public link to the remote path (usually readable by anyone)
func (f *Fs) PublicLink(ctx context.Context, remote string) (link string, err error) {
func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (link string, err error) {
opts := rest.Opts{
Method: "GET",
Path: f.filePath(remote),
Parameters: url.Values{},
}
if f.opt.Unlink {
if unlink {
opts.Parameters.Set("mode", "disableShare")
} else {
opts.Parameters.Set("mode", "enableShare")
@ -1029,12 +1023,12 @@ func (f *Fs) PublicLink(ctx context.Context, remote string) (link string, err er
}
}
if err != nil {
if f.opt.Unlink {
if unlink {
return "", errors.Wrap(err, "couldn't remove public link")
}
return "", errors.Wrap(err, "couldn't create public link")
}
if f.opt.Unlink {
if unlink {
if result.PublicSharePath != "" {
return "", errors.Errorf("couldn't remove public link - %q", result.PublicSharePath)
}

View File

@ -603,7 +603,7 @@ func createLink(c *koofrclient.KoofrClient, mountID string, path string) (*link,
}
// PublicLink creates a public link to the remote path
func (f *Fs) PublicLink(ctx context.Context, remote string) (string, error) {
func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (string, error) {
linkData, err := createLink(f.client, f.mountID, f.fullPath(remote))
if err != nil {
return "", translateErrorsDir(err)

View File

@ -1450,7 +1450,7 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
}
// PublicLink generates a public link to the remote path (usually readable by anyone)
func (f *Fs) PublicLink(ctx context.Context, remote string) (link string, err error) {
func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (link string, err error) {
// fs.Debugf(f, ">>> PublicLink %q", remote)
token, err := f.accessToken()

View File

@ -836,7 +836,7 @@ func (f *Fs) Hashes() hash.Set {
}
// PublicLink generates a public link to the remote path (usually readable by anyone)
func (f *Fs) PublicLink(ctx context.Context, remote string) (link string, err error) {
func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (link string, err error) {
root, err := f.findRoot(false)
if err != nil {
return "", errors.Wrap(err, "PublicLink failed to find root node")

View File

@ -1311,7 +1311,7 @@ func (f *Fs) Hashes() hash.Set {
}
// PublicLink returns a link for downloading without account.
func (f *Fs) PublicLink(ctx context.Context, remote string) (link string, err error) {
func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (link string, err error) {
info, _, err := f.readMetaDataForPath(ctx, f.rootPath(remote))
if err != nil {
return "", err

View File

@ -822,7 +822,7 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
}
// PublicLink adds a "readable by anyone with link" permission on the given file or folder.
func (f *Fs) PublicLink(ctx context.Context, remote string) (string, error) {
func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (string, error) {
_, err := f.dirCache.FindDir(ctx, remote, false)
if err == nil {
return "", fs.ErrorCantShareDirectories

View File

@ -972,7 +972,7 @@ func (f *Fs) UserInfo(ctx context.Context) (map[string]string, error) {
// ==================== Optional Interface fs.PublicLinker ====================
// PublicLink generates a public link to the remote path (usually readable by anyone)
func (f *Fs) PublicLink(ctx context.Context, remote string) (string, error) {
func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (string, error) {
libraryName, filePath := f.splitPath(remote)
if libraryName == "" {
// We cannot share the whole seafile server, we need at least a library

View File

@ -1099,7 +1099,7 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
}
// PublicLink adds a "readable by anyone with link" permission on the given file or folder.
func (f *Fs) PublicLink(ctx context.Context, remote string) (string, error) {
func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (string, error) {
obj, err := f.NewObject(ctx, remote)
if err != nil {
return "", err

View File

@ -73,11 +73,6 @@ func init() {
}, {
Name: config.ConfigClientSecret,
Help: "Yandex Client Secret\nLeave blank normally.",
}, {
Name: "unlink",
Help: "Remove existing public link to file/folder with link command rather than creating.\nDefault is false, meaning link command will create or retrieve public link.",
Default: false,
Advanced: true,
}, {
Name: config.ConfigEncoding,
Help: config.ConfigEncodingHelp,
@ -92,9 +87,8 @@ func init() {
// Options defines the configuration for this backend
type Options struct {
Token string `config:"token"`
Unlink bool `config:"unlink"`
Enc encoder.MultiEncoder `config:"encoding"`
Token string `config:"token"`
Enc encoder.MultiEncoder `config:"encoding"`
}
// Fs represents a remote yandex
@ -801,9 +795,9 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string
}
// PublicLink generates a public link to the remote path (usually readable by anyone)
func (f *Fs) PublicLink(ctx context.Context, remote string) (link string, err error) {
func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (link string, err error) {
var path string
if f.opt.Unlink {
if unlink {
path = "/resources/unpublish"
} else {
path = "/resources/publish"
@ -830,7 +824,7 @@ func (f *Fs) PublicLink(ctx context.Context, remote string) (link string, err er
}
}
if err != nil {
if f.opt.Unlink {
if unlink {
return "", errors.Wrap(err, "couldn't remove public link")
}
return "", errors.Wrap(err, "couldn't create public link")

View File

@ -3,35 +3,57 @@ package link
import (
"context"
"fmt"
"time"
"github.com/rclone/rclone/cmd"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/flags"
"github.com/rclone/rclone/fs/operations"
"github.com/spf13/cobra"
)
var (
expire = fs.Duration(time.Hour * 24 * 365 * 100)
unlink = false
)
func init() {
cmd.Root.AddCommand(commandDefinition)
cmdFlags := commandDefinition.Flags()
flags.FVarP(cmdFlags, &expire, "expire", "", "The amount of time that the link will be valid")
flags.BoolVarP(cmdFlags, &unlink, "unlink", "", unlink, "Remove existing public link to file/folder")
}
var commandDefinition = &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.
Long: `rclone link will create, retrieve or remove a public link to the given
file or folder.
rclone link remote:path/to/file
rclone link remote:path/to/folder/
rclone link --unlink remote:path/to/folder/
rclone link --expire 1d remote:path/to/file
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.
If you supply the --expire flag, it will set the expiration time
otherwise it will use the default (100 years). **Note** not all
backends support the --expire flag - if the backend doesn't support it
then the link returned won't expire.
Use the --unlink flag to remove existing public links to the file or
folder. **Note** not all backends support "--unlink" flag - those that
don't will just ignore it.
If successful, the last line of the output will contain the
link. Exact capabilities depend on the remote, but the link will
always by default 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(context.Background(), fsrc, remote)
link, err := operations.PublicLink(context.Background(), fsrc, remote, expire, unlink)
if err != nil {
return err
}

View File

@ -575,7 +575,7 @@ type Features struct {
DirCacheFlush func()
// PublicLink generates a public link to the remote path (usually readable by anyone)
PublicLink func(ctx context.Context, remote string) (string, error)
PublicLink func(ctx context.Context, remote string, expire Duration, unlink bool) (string, error)
// Put in to the remote path with the modTime given of the given size
//
@ -988,7 +988,7 @@ type PutStreamer interface {
// PublicLinker is an optional interface for Fs
type PublicLinker interface {
// PublicLink generates a public link to the remote path (usually readable by anyone)
PublicLink(ctx context.Context, remote string) (string, error)
PublicLink(ctx context.Context, remote string, expire Duration, unlink bool) (string, error)
}
// MergeDirser is an option interface for Fs

View File

@ -1416,12 +1416,12 @@ func Rcat(ctx context.Context, fdst fs.Fs, dstFileName string, in io.ReadCloser,
}
// PublicLink adds a "readable by anyone with link" permission on the given file or folder.
func PublicLink(ctx context.Context, f fs.Fs, remote string) (string, error) {
func PublicLink(ctx context.Context, f fs.Fs, remote string, expire fs.Duration, unlink bool) (string, error) {
doPublicLink := f.Features().PublicLink
if doPublicLink == nil {
return "", errors.Errorf("%v doesn't support public links", f)
}
return doPublicLink(ctx, remote)
return doPublicLink(ctx, remote, expire, unlink)
}
// Rmdirs removes any empty directories (or directories only

View File

@ -271,6 +271,8 @@ func init() {
- fs - a remote name string eg "drive:"
- remote - a path within that remote eg "dir"
- unlink - boolean - if set removes the link rather than adding it (optional)
- expire - string - the expiry time of the link eg "1d" (optional)
Returns
@ -287,7 +289,12 @@ func rcPublicLink(ctx context.Context, in rc.Params) (out rc.Params, err error)
if err != nil {
return nil, err
}
url, err := PublicLink(ctx, f, remote)
unlink, _ := in.GetBool("unlink")
expire, err := in.GetDuration("expire")
if err != nil && !rc.IsErrParamNotFound(err) {
return nil, err
}
url, err := PublicLink(ctx, f, remote, fs.Duration(expire), unlink)
if err != nil {
return nil, err
}

View File

@ -192,7 +192,7 @@ func TestRcDeletefile(t *testing.T) {
fstest.CheckItems(t, r.Fremote, file2)
}
// operations/list: List the given remote and path in JSON format
// operations/list: List the given remote and path in JSON format.
func TestRcList(t *testing.T) {
r, call := rcNewRun(t, "operations/list")
defer r.Finalise()
@ -402,6 +402,8 @@ func TestRcPublicLink(t *testing.T) {
in := rc.Params{
"fs": r.FremoteName,
"remote": "",
"expire": "5m",
"unlink": false,
}
_, err := call.Fn(context.Background(), in)
require.Error(t, err)

View File

@ -7,8 +7,11 @@ import (
"fmt"
"math"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/rclone/rclone/fs"
)
// Params is the input and output type for the Func
@ -212,3 +215,16 @@ func (p Params) GetStructMissingOK(key string, out interface{}) error {
}
return p.GetStruct(key, out)
}
// GetDuration get the duration parameters from in
func (p Params) GetDuration(key string) (time.Duration, error) {
s, err := p.GetString(key)
if err != nil {
return 0, err
}
duration, err := fs.ParseDuration(s)
if err != nil {
return 0, ErrParamInvalid{errors.Wrap(err, "parse duration")}
}
return duration, nil
}

View File

@ -3,10 +3,13 @@ package rc
import (
"fmt"
"testing"
"time"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/rclone/rclone/fs"
)
func TestErrParamNotFoundError(t *testing.T) {
@ -173,6 +176,57 @@ func TestParamsGetFloat64(t *testing.T) {
assert.Equal(t, true, IsErrParamInvalid(e3), e3.Error())
}
func TestParamsGetDuration(t *testing.T) {
for _, test := range []struct {
value interface{}
result time.Duration
errString string
}{
{"86400", time.Hour * 24, ""},
{"1y", time.Hour * 24 * 365, ""},
{"60", time.Minute * 1, ""},
{"0", 0, ""},
{"-45", -time.Second * 45, ""},
{"2", time.Second * 2, ""},
{"2h4m7s", time.Hour*2 + 4*time.Minute + 7*time.Second, ""},
{"3d", time.Hour * 24 * 3, ""},
{"off", time.Duration(fs.DurationOff), ""},
{"", 0, "parse duration"},
{12, 0, "expecting string"},
{"34y", time.Hour * 24 * 365 * 34, ""},
{"30d", time.Hour * 24 * 30, ""},
{"2M", time.Hour * 24 * 60, ""},
{"wrong", 0, "parse duration"},
} {
t.Run(fmt.Sprintf("%T=%v", test.value, test.value), func(t *testing.T) {
in := Params{
"key": test.value,
}
v1, e1 := in.GetDuration("key")
if test.errString == "" {
require.NoError(t, e1)
assert.Equal(t, test.result, v1)
} else {
require.NotNil(t, e1)
require.Error(t, e1)
assert.Contains(t, e1.Error(), test.errString)
assert.Equal(t, time.Duration(0), v1)
}
})
}
in := Params{
"notDuration": []string{"a", "b"},
}
v2, e2 := in.GetDuration("notOK")
assert.Error(t, e2)
assert.Equal(t, time.Duration(0), v2)
assert.Equal(t, ErrParamNotFound("notOK"), e2)
v3, e3 := in.GetDuration("notDuration")
assert.Error(t, e3)
assert.Equal(t, time.Duration(0), v3)
assert.Equal(t, true, IsErrParamInvalid(e3), e3.Error())
}
func TestParamsGetBool(t *testing.T) {
for _, test := range []struct {
value interface{}

View File

@ -1467,29 +1467,29 @@ func Run(t *testing.T, opt *Opt) {
}
// if object not found
link, err := doPublicLink(ctx, file1.Path+"_does_not_exist")
link, err := doPublicLink(ctx, file1.Path+"_does_not_exist", fs.Duration(0), false)
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(ctx, file1.Path)
link1, err := doPublicLink(ctx, file1.Path, fs.Duration(0), false)
require.NoError(t, err)
require.NotEqual(t, "", link1, "Link should not be empty")
link2, err := doPublicLink(ctx, file2.Path)
link2, err := doPublicLink(ctx, file2.Path, fs.Duration(0), false)
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(ctx, file1.Path)
link1, err = doPublicLink(ctx, file1.Path, fs.Duration(0), false)
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(ctx, path)
link3, err := doPublicLink(ctx, path, fs.Duration(0), false)
if err != nil && errors.Cause(err) == fs.ErrorCantShareDirectories {
t.Log("skipping directory tests as not supported on this backend")
} else {
@ -1497,7 +1497,7 @@ func Run(t *testing.T, opt *Opt) {
require.NotEqual(t, "", link3, "Link should not be empty")
// sharing directory for the second time
link3, err = doPublicLink(ctx, path)
link3, err = doPublicLink(ctx, path, fs.Duration(0), false)
require.NoError(t, err)
require.NotEqual(t, "", link3, "Link should not be empty")
@ -1511,7 +1511,7 @@ func Run(t *testing.T, opt *Opt) {
_, err = subRemote.Put(ctx, buf, obji)
require.NoError(t, err)
link4, err := subRemote.Features().PublicLink(ctx, "")
link4, err := subRemote.Features().PublicLink(ctx, "", fs.Duration(0), false)
require.NoError(t, err, "Sharing root in a sub-remote should work")
require.NotEqual(t, "", link4, "Link should not be empty")
}