diff --git a/backend/alias/alias.go b/backend/alias/alias.go new file mode 100644 index 000000000..e92135e99 --- /dev/null +++ b/backend/alias/alias.go @@ -0,0 +1,45 @@ +package alias + +import ( + "errors" + "path" + "path/filepath" + "strings" + + "github.com/ncw/rclone/fs" + "github.com/ncw/rclone/fs/config" +) + +// Register with Fs +func init() { + fsi := &fs.RegInfo{ + Name: "alias", + Description: "Alias for a existing remote", + NewFs: NewFs, + Options: []fs.Option{{ + Name: "remote", + Help: "Remote or path to alias.\nCan be \"myremote:path/to/dir\", \"myremote:bucket\", \"myremote:\" or \"/local/path\".", + }}, + } + fs.Register(fsi) +} + +// NewFs contstructs an Fs from the path. +// +// The returned Fs is the actual Fs, referenced by remote in the config +func NewFs(name, root string) (fs.Fs, error) { + remote := config.FileGet(name, "remote") + if remote == "" { + return nil, errors.New("alias can't point to an empty remote - check the value of the remote setting") + } + if strings.HasPrefix(remote, name+":") { + return nil, errors.New("can't point alias remote at itself - check the value of the remote setting") + } + fsInfo, configName, fsPath, err := fs.ParseRemote(remote) + if err != nil { + return nil, err + } + + root = filepath.ToSlash(root) + return fsInfo.NewFs(configName, path.Join(fsPath, root)) +} diff --git a/backend/alias/alias_internal_test.go b/backend/alias/alias_internal_test.go new file mode 100644 index 000000000..4b7d7e750 --- /dev/null +++ b/backend/alias/alias_internal_test.go @@ -0,0 +1,106 @@ +package alias + +import ( + "fmt" + "path" + "path/filepath" + "sort" + "testing" + + _ "github.com/ncw/rclone/backend/local" // pull in test backend + "github.com/ncw/rclone/fs" + "github.com/ncw/rclone/fs/config" + "github.com/stretchr/testify/require" +) + +var ( + remoteName = "TestAlias" + testPath = "test" + filesPath = filepath.Join(testPath, "files") +) + +func prepare(t *testing.T, root string) { + config.LoadConfig() + + // Configure the remote + config.FileSet(remoteName, "type", "alias") + config.FileSet(remoteName, "remote", root) +} + +func TestNewFS(t *testing.T) { + type testEntry struct { + remote string + size int64 + isDir bool + } + for testi, test := range []struct { + remoteRoot string + fsRoot string + fsList string + wantOK bool + entries []testEntry + }{ + {"", "", "", true, []testEntry{ + {"four", -1, true}, + {"one%.txt", 6, false}, + {"three", -1, true}, + {"two.html", 7, false}, + }}, + {"", "four", "", true, []testEntry{ + {"five", -1, true}, + {"under four.txt", 9, false}, + }}, + {"", "", "four", true, []testEntry{ + {"four/five", -1, true}, + {"four/under four.txt", 9, false}, + }}, + {"four", "..", "", true, []testEntry{ + {"four", -1, true}, + {"one%.txt", 6, false}, + {"three", -1, true}, + {"two.html", 7, false}, + }}, + {"four", "../three", "", true, []testEntry{ + {"underthree.txt", 9, false}, + }}, + } { + what := fmt.Sprintf("test %d remoteRoot=%q, fsRoot=%q, fsList=%q", testi, test.remoteRoot, test.fsRoot, test.fsList) + + remoteRoot, err := filepath.Abs(filepath.FromSlash(path.Join("test/files", test.remoteRoot))) + require.NoError(t, err, what) + prepare(t, remoteRoot) + f, err := fs.NewFs(fmt.Sprintf("%s:%s", remoteName, test.fsRoot)) + require.NoError(t, err, what) + gotEntries, err := f.List(test.fsList) + require.NoError(t, err, what) + + sort.Sort(gotEntries) + + require.Equal(t, len(test.entries), len(gotEntries), what) + for i, gotEntry := range gotEntries { + what := fmt.Sprintf("%s, entry=%d", what, i) + wantEntry := test.entries[i] + + require.Equal(t, wantEntry.remote, gotEntry.Remote(), what) + require.Equal(t, wantEntry.size, int64(gotEntry.Size()), what) + _, isDir := gotEntry.(fs.Directory) + require.Equal(t, wantEntry.isDir, isDir, what) + } + } +} + +func TestNewFSNoRemote(t *testing.T) { + prepare(t, "") + f, err := fs.NewFs(fmt.Sprintf("%s:", remoteName)) + + require.Error(t, err) + require.Nil(t, f) +} + +func TestNewFSInvalidRemote(t *testing.T) { + prepare(t, "not_existing_test_remote:") + f, err := fs.NewFs(fmt.Sprintf("%s:", remoteName)) + + require.Error(t, err) + require.Nil(t, f) +} diff --git a/backend/alias/test/files/four/five/underfive.txt b/backend/alias/test/files/four/five/underfive.txt new file mode 100644 index 000000000..4c479deff --- /dev/null +++ b/backend/alias/test/files/four/five/underfive.txt @@ -0,0 +1 @@ +apple diff --git a/backend/alias/test/files/four/under four.txt b/backend/alias/test/files/four/under four.txt new file mode 100644 index 000000000..748393ce8 --- /dev/null +++ b/backend/alias/test/files/four/under four.txt @@ -0,0 +1 @@ +beetroot diff --git a/backend/alias/test/files/one%.txt b/backend/alias/test/files/one%.txt new file mode 100644 index 000000000..ce0136250 --- /dev/null +++ b/backend/alias/test/files/one%.txt @@ -0,0 +1 @@ +hello diff --git a/backend/alias/test/files/three/underthree.txt b/backend/alias/test/files/three/underthree.txt new file mode 100644 index 000000000..1031dc5b4 --- /dev/null +++ b/backend/alias/test/files/three/underthree.txt @@ -0,0 +1 @@ +rutabaga diff --git a/backend/alias/test/files/two.html b/backend/alias/test/files/two.html new file mode 100644 index 000000000..4bc562871 --- /dev/null +++ b/backend/alias/test/files/two.html @@ -0,0 +1 @@ +potato diff --git a/backend/all/all.go b/backend/all/all.go index 8a901be24..6249e68b4 100644 --- a/backend/all/all.go +++ b/backend/all/all.go @@ -2,6 +2,7 @@ package all import ( // Active file systems + _ "github.com/ncw/rclone/backend/alias" _ "github.com/ncw/rclone/backend/amazonclouddrive" _ "github.com/ncw/rclone/backend/azureblob" _ "github.com/ncw/rclone/backend/b2" diff --git a/bin/make_manual.py b/bin/make_manual.py index 6de72d79f..b7e040195 100755 --- a/bin/make_manual.py +++ b/bin/make_manual.py @@ -21,6 +21,7 @@ docs = [ "overview.md", # Keep these alphabetical by full name + "alias.md", "amazonclouddrive.md", "s3.md", "b2.md", diff --git a/docs/content/alias.md b/docs/content/alias.md new file mode 100644 index 000000000..77f54e099 --- /dev/null +++ b/docs/content/alias.md @@ -0,0 +1,130 @@ +--- +title: "Alias" +description: "Remote Aliases" +date: "2018-01-30" +--- + + Alias +----------------------------------------- + +The `alias` remote provides a new name for another remote. + +Paths may be as deep as required or a local path, +eg `remote:directory/subdirectory` or `/directory/subdirectory`. + +During the initial setup with `rclone config` you will specify the target +remote. The target remote can either be a local path or another remote. + +Subfolders can be used in target remote. Asume a alias remote named `backup` +with the target `mydrive:private/backup`. Invoking `rclone mkdir backup:desktop` +is exactly the same as invoking `rclone mkdir mydrive:private/backup/desktop`. + +There will be no special handling of paths containing `..` segments. +Invoking `rclone mkdir backup:../desktop` is exactly the same as invoking +`rclone mkdir mydrive:private/backup/../desktop`. +The empty path is not allowed as a remote. To alias the current directory +use `.` instead. + +Here is an example of how to make a alias called `remote` for local folder. +First run: + + rclone config + +This will guide you through an interactive setup process: + +``` +No remotes found - make a new one +n) New remote +s) Set configuration password +q) Quit config +n/s/q> n +name> remote +Type of storage to configure. +Choose a number from below, or type in your own value + 1 / Alias for a existing remote + \ "alias" + 2 / Amazon Drive + \ "amazon cloud drive" + 3 / Amazon S3 (also Dreamhost, Ceph, Minio) + \ "s3" + 4 / Backblaze B2 + \ "b2" + 5 / Box + \ "box" + 6 / Cache a remote + \ "cache" + 7 / Dropbox + \ "dropbox" + 8 / Encrypt/Decrypt a remote + \ "crypt" + 9 / FTP Connection + \ "ftp" +10 / Google Cloud Storage (this is not Google Drive) + \ "google cloud storage" +11 / Google Drive + \ "drive" +12 / Hubic + \ "hubic" +13 / Local Disk + \ "local" +14 / Microsoft Azure Blob Storage + \ "azureblob" +15 / Microsoft OneDrive + \ "onedrive" +16 / Openstack Swift (Rackspace Cloud Files, Memset Memstore, OVH) + \ "swift" +17 / Pcloud + \ "pcloud" +18 / QingCloud Object Storage + \ "qingstor" +19 / SSH/SFTP Connection + \ "sftp" +20 / Webdav + \ "webdav" +21 / Yandex Disk + \ "yandex" +22 / http Connection + \ "http" +Storage> 1 +Remote or path to alias. +Can be "myremote:path/to/dir", "myremote:bucket", "myremote:" or "/local/path". +remote> /mnt/storage/backup +Remote config +-------------------- +[remote] +remote = /mnt/storage/backup +-------------------- +y) Yes this is OK +e) Edit this remote +d) Delete this remote +y/e/d> y +Current remotes: + +Name Type +==== ==== +remote alias + +e) Edit existing remote +n) New remote +d) Delete remote +r) Rename remote +c) Copy remote +s) Set configuration password +q) Quit config +e/n/d/r/c/s/q> q +``` + +Once configured you can then use `rclone` like this, + +List directories in top level in `/mnt/storage/backup` + + rclone lsd remote: + +List all the files in `/mnt/storage/backup` + + rclone ls remote: + +Copy another local directory to the alias directory called source + + rclone copy /home/source remote:source + diff --git a/docs/content/docs.md b/docs/content/docs.md index 243e1fb54..9fdfbf750 100644 --- a/docs/content/docs.md +++ b/docs/content/docs.md @@ -19,6 +19,7 @@ option: See the following for detailed instructions for + * [Alias](/alias/) * [Amazon Drive](/amazonclouddrive/) * [Amazon S3](/s3/) * [Backblaze B2](/b2/)