From 118a8b949e902791508260c392b3fe9c5f76db70 Mon Sep 17 00:00:00 2001 From: jaKa Date: Fri, 22 Feb 2019 16:50:04 +0100 Subject: [PATCH] koofr: implemented a backend for Koofr cloud storage service. Implemented a Koofr REST API backend. Added said backend to tests. Added documentation for said backend. --- README.md | 1 + backend/all/all.go | 1 + backend/koofr/koofr.go | 589 ++++++++++++++++++++++++++++++++ backend/koofr/koofr_test.go | 14 + bin/make_manual.py | 1 + docs/content/about.md | 1 + docs/content/authors.md | 1 + docs/content/docs.md | 3 +- docs/content/koofr.md | 189 ++++++++++ docs/content/overview.md | 3 +- docs/layouts/chrome/navbar.html | 1 + fstest/test_all/config.yaml | 4 + 12 files changed, 806 insertions(+), 2 deletions(-) create mode 100644 backend/koofr/koofr.go create mode 100644 backend/koofr/koofr_test.go create mode 100644 docs/content/koofr.md diff --git a/README.md b/README.md index 6f2de6f19..1857eb7bc 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Rclone *("rsync for cloud storage")* is a command line program to sync files and * Hubic [:page_facing_up:](https://rclone.org/hubic/) * Jottacloud [:page_facing_up:](https://rclone.org/jottacloud/) * IBM COS S3 [:page_facing_up:](https://rclone.org/s3/#ibm-cos-s3) + * Koofr [:page_facing_up:](https://rclone.org/koofr/) * Memset Memstore [:page_facing_up:](https://rclone.org/swift/) * Mega [:page_facing_up:](https://rclone.org/mega/) * Microsoft Azure Blob Storage [:page_facing_up:](https://rclone.org/azureblob/) diff --git a/backend/all/all.go b/backend/all/all.go index d80e440c8..3d21e13b3 100644 --- a/backend/all/all.go +++ b/backend/all/all.go @@ -16,6 +16,7 @@ import ( _ "github.com/ncw/rclone/backend/http" _ "github.com/ncw/rclone/backend/hubic" _ "github.com/ncw/rclone/backend/jottacloud" + _ "github.com/ncw/rclone/backend/koofr" _ "github.com/ncw/rclone/backend/local" _ "github.com/ncw/rclone/backend/mega" _ "github.com/ncw/rclone/backend/onedrive" diff --git a/backend/koofr/koofr.go b/backend/koofr/koofr.go new file mode 100644 index 000000000..cc2221bf6 --- /dev/null +++ b/backend/koofr/koofr.go @@ -0,0 +1,589 @@ +package koofr + +import ( + "encoding/base64" + "errors" + "fmt" + "io" + "net/http" + "path" + "strings" + "time" + + "github.com/ncw/rclone/fs" + "github.com/ncw/rclone/fs/config/configmap" + "github.com/ncw/rclone/fs/config/configstruct" + "github.com/ncw/rclone/fs/config/obscure" + "github.com/ncw/rclone/fs/hash" + + httpclient "github.com/koofr/go-httpclient" + koofrclient "github.com/koofr/go-koofrclient" +) + +// Register Fs with rclone +func init() { + fs.Register(&fs.RegInfo{ + Name: "koofr", + Description: "Koofr", + NewFs: NewFs, + Options: []fs.Option{ + { + Name: "endpoint", + Help: "The Koofr API endpoint to use", + Default: "https://app.koofr.net", + Required: true, + Advanced: true, + }, { + Name: "mountid", + Help: "Mount ID of the mount to use. If omitted, the primary mount is used.", + Required: false, + Default: "", + Advanced: true, + }, { + Name: "user", + Help: "Your Koofr user name", + Required: true, + }, { + Name: "password", + Help: "Your Koofr password for rclone (generate one at https://app.koofr.net/app/admin/preferences/password)", + IsPassword: true, + Required: true, + }, + }, + }) +} + +// Options represent the configuration of the Koofr backend +type Options struct { + Endpoint string `config:"endpoint"` + MountID string `config:"mountid"` + User string `config:"user"` + Password string `config:"password"` +} + +// A Fs is a representation of a remote Koofr Fs +type Fs struct { + name string + mountID string + root string + opt Options + features *fs.Features + client *koofrclient.KoofrClient +} + +// An Object on the remote Koofr Fs +type Object struct { + fs *Fs + remote string + info koofrclient.FileInfo +} + +func base(pth string) string { + rv := path.Base(pth) + if rv == "" || rv == "." { + rv = "/" + } + return rv +} + +func dir(pth string) string { + rv := path.Dir(pth) + if rv == "" || rv == "." { + rv = "/" + } + return rv +} + +// String returns a string representation of the remote Object +func (o *Object) String() string { + return o.remote +} + +// Remote returns the remote path of the Object, relative to Fs root +func (o *Object) Remote() string { + return o.remote +} + +// ModTime returns the modification time of the Object +func (o *Object) ModTime() time.Time { + return time.Unix(o.info.Modified/1000, (o.info.Modified%1000)*1000*1000) +} + +// Size return the size of the Object in bytes +func (o *Object) Size() int64 { + return o.info.Size +} + +// Fs returns a reference to the Koofr Fs containing the Object +func (o *Object) Fs() fs.Info { + return o.fs +} + +// Hash returns an MD5 hash of the Object +func (o *Object) Hash(typ hash.Type) (string, error) { + if typ == hash.MD5 { + return o.info.Hash, nil + } + return "", nil +} + +// fullPath returns full path of the remote Object (including Fs root) +func (o *Object) fullPath() string { + return o.fs.fullPath(o.remote) +} + +// Storable returns true if the Object is storable +func (o *Object) Storable() bool { + return true +} + +// SetModTime is not supported +func (o *Object) SetModTime(mtime time.Time) error { + return nil +} + +// Open opens the Object for reading +func (o *Object) Open(options ...fs.OpenOption) (io.ReadCloser, error) { + var sOff, eOff int64 = 0, -1 + + for _, option := range options { + switch x := option.(type) { + case *fs.SeekOption: + sOff = x.Offset + case *fs.RangeOption: + sOff = x.Start + eOff = x.End + default: + if option.Mandatory() { + fs.Logf(o, "Unsupported mandatory option: %v", option) + } + } + } + if sOff == 0 && eOff < 0 { + return o.fs.client.FilesGet(o.fs.mountID, o.fullPath()) + } + if sOff < 0 { + sOff = o.Size() - eOff + eOff = o.Size() + } + if eOff > o.Size() { + eOff = o.Size() + } + span := &koofrclient.FileSpan{ + Start: sOff, + End: eOff, + } + return o.fs.client.FilesGetRange(o.fs.mountID, o.fullPath(), span) +} + +// Update updates the Object contents +func (o *Object) Update(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error { + putopts := &koofrclient.PutFilter{ + ForceOverwrite: true, + NoRename: true, + IgnoreNonExisting: true, + } + fullPath := o.fullPath() + dirPath := dir(fullPath) + name := base(fullPath) + err := o.fs.mkdir(dirPath) + if err != nil { + return err + } + info, err := o.fs.client.FilesPutOptions(o.fs.mountID, dirPath, name, in, putopts) + if err != nil { + return err + } + o.info = *info + return nil +} + +// Remove deletes the remote Object +func (o *Object) Remove() error { + return o.fs.client.FilesDelete(o.fs.mountID, o.fullPath()) +} + +// Name returns the name of the Fs +func (f *Fs) Name() string { + return f.name +} + +// Root returns the root path of the Fs +func (f *Fs) Root() string { + return f.root +} + +// String returns a string representation of the Fs +func (f *Fs) String() string { + return "koofr:" + f.mountID + ":" + f.root +} + +// Features returns the optional features supported by this Fs +func (f *Fs) Features() *fs.Features { + return f.features +} + +// Precision denotes that setting modification times is not supported +func (f *Fs) Precision() time.Duration { + return fs.ModTimeNotSupported +} + +// Hashes returns a set of hashes are Provided by the Fs +func (f *Fs) Hashes() hash.Set { + return hash.Set(hash.MD5) +} + +// fullPath constructs a full, absolute path from a Fs root relative path, +func (f *Fs) fullPath(part string) string { + return path.Join("/", f.root, part) +} + +// NewFs constructs a new filesystem given a root path and configuration options +func NewFs(name, root string, m configmap.Mapper) (ff fs.Fs, err error) { + opt := new(Options) + err = configstruct.Set(m, opt) + if err != nil { + return nil, err + } + pass, err := obscure.Reveal(opt.Password) + if err != nil { + return nil, err + } + client := koofrclient.NewKoofrClient(opt.Endpoint, false) + basicAuth := fmt.Sprintf("Basic %s", + base64.StdEncoding.EncodeToString([]byte(opt.User+":"+pass))) + client.HTTPClient.Headers.Set("Authorization", basicAuth) + mounts, err := client.Mounts() + if err != nil { + return nil, err + } + f := &Fs{ + name: name, + root: root, + opt: *opt, + client: client, + } + f.features = (&fs.Features{ + CaseInsensitive: true, + DuplicateFiles: false, + BucketBased: false, + CanHaveEmptyDirectories: true, + }).Fill(f) + for _, m := range mounts { + if opt.MountID != "" { + if m.Id == opt.MountID { + f.mountID = m.Id + break + } + } else if m.IsPrimary { + f.mountID = m.Id + break + } + } + if f.mountID == "" { + if opt.MountID == "" { + return nil, errors.New("Failed to find primary mount") + } + return nil, errors.New("Failed to find mount " + opt.MountID) + } + rootFile, err := f.client.FilesInfo(f.mountID, "/"+f.root) + if err == nil && rootFile.Type != "dir" { + f.root = dir(f.root) + err = fs.ErrorIsFile + } else { + err = nil + } + return f, err +} + +// List returns a list of items in a directory +func (f *Fs) List(dir string) (entries fs.DirEntries, err error) { + files, err := f.client.FilesList(f.mountID, f.fullPath(dir)) + if err != nil { + return nil, translateErrorsDir(err) + } + entries = make([]fs.DirEntry, len(files)) + for i, file := range files { + if file.Type == "dir" { + entries[i] = fs.NewDir(path.Join(dir, file.Name), time.Unix(0, 0)) + } else { + entries[i] = &Object{ + fs: f, + info: file, + remote: path.Join(dir, file.Name), + } + } + } + return entries, nil +} + +// NewObject creates a new remote Object for a given remote path +func (f *Fs) NewObject(remote string) (obj fs.Object, err error) { + info, err := f.client.FilesInfo(f.mountID, f.fullPath(remote)) + if err != nil { + return nil, translateErrorsObject(err) + } + if info.Type == "dir" { + return nil, fs.ErrorNotAFile + } + return &Object{ + fs: f, + info: info, + remote: remote, + }, nil +} + +// Put updates a remote Object +func (f *Fs) Put(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (obj fs.Object, err error) { + putopts := &koofrclient.PutFilter{ + ForceOverwrite: true, + NoRename: true, + IgnoreNonExisting: true, + } + fullPath := f.fullPath(src.Remote()) + dirPath := dir(fullPath) + name := base(fullPath) + err = f.mkdir(dirPath) + if err != nil { + return nil, err + } + info, err := f.client.FilesPutOptions(f.mountID, dirPath, name, in, putopts) + if err != nil { + return nil, translateErrorsObject(err) + } + return &Object{ + fs: f, + info: *info, + remote: src.Remote(), + }, nil +} + +// PutStream updates a remote Object with a stream of unknown size +func (f *Fs) PutStream(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { + return f.Put(in, src, options...) +} + +// isBadRequest is a predicate which holds true iff the error returned was +// HTTP status 400 +func isBadRequest(err error) bool { + switch err := err.(type) { + case httpclient.InvalidStatusError: + if err.Got == http.StatusBadRequest { + return true + } + } + return false +} + +// translateErrorsDir translates koofr errors to rclone errors (for a dir +// operation) +func translateErrorsDir(err error) error { + switch err := err.(type) { + case httpclient.InvalidStatusError: + if err.Got == http.StatusNotFound { + return fs.ErrorDirNotFound + } + } + return err +} + +// translatesErrorsObject translates Koofr errors to rclone errors (for an object operation) +func translateErrorsObject(err error) error { + switch err := err.(type) { + case httpclient.InvalidStatusError: + if err.Got == http.StatusNotFound { + return fs.ErrorObjectNotFound + } + } + return err +} + +// mkdir creates a directory at the given remote path. Creates ancestors if +// neccessary +func (f *Fs) mkdir(fullPath string) error { + if fullPath == "/" { + return nil + } + info, err := f.client.FilesInfo(f.mountID, fullPath) + if err == nil && info.Type == "dir" { + return nil + } + err = translateErrorsDir(err) + if err != nil && err != fs.ErrorDirNotFound { + return err + } + dirs := strings.Split(fullPath, "/") + parent := "/" + for _, part := range dirs { + if part == "" { + continue + } + info, err = f.client.FilesInfo(f.mountID, path.Join(parent, part)) + if err != nil || info.Type != "dir" { + err = translateErrorsDir(err) + if err != nil && err != fs.ErrorDirNotFound { + return err + } + err = f.client.FilesNewFolder(f.mountID, parent, part) + if err != nil && !isBadRequest(err) { + return err + } + } + parent = path.Join(parent, part) + } + return nil +} + +// Mkdir creates a directory at the given remote path. Creates ancestors if +// necessary +func (f *Fs) Mkdir(dir string) error { + fullPath := f.fullPath(dir) + return f.mkdir(fullPath) +} + +// Rmdir removes an (empty) directory at the given remote path +func (f *Fs) Rmdir(dir string) error { + files, err := f.client.FilesList(f.mountID, f.fullPath(dir)) + if err != nil { + return translateErrorsDir(err) + } + if len(files) > 0 { + return fs.ErrorDirectoryNotEmpty + } + err = f.client.FilesDelete(f.mountID, f.fullPath(dir)) + if err != nil { + return translateErrorsDir(err) + } + return nil +} + +// Copy copies a remote Object to the given path +func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) { + dstFullPath := f.fullPath(remote) + dstDir := dir(dstFullPath) + err := f.mkdir(dstDir) + if err != nil { + return nil, fs.ErrorCantCopy + } + err = f.client.FilesCopy((src.(*Object)).fs.mountID, + (src.(*Object)).fs.fullPath((src.(*Object)).remote), + f.mountID, dstFullPath) + if err != nil { + return nil, fs.ErrorCantCopy + } + return f.NewObject(remote) +} + +// Move moves a remote Object to the given path +func (f *Fs) Move(src fs.Object, remote string) (fs.Object, error) { + srcObj := src.(*Object) + dstFullPath := f.fullPath(remote) + dstDir := dir(dstFullPath) + err := f.mkdir(dstDir) + if err != nil { + return nil, fs.ErrorCantMove + } + err = f.client.FilesMove(srcObj.fs.mountID, + srcObj.fs.fullPath(srcObj.remote), f.mountID, dstFullPath) + if err != nil { + return nil, fs.ErrorCantMove + } + return f.NewObject(remote) +} + +// DirMove moves a remote directory to the given path +func (f *Fs) DirMove(src fs.Fs, srcRemote, dstRemote string) error { + srcFs := src.(*Fs) + srcFullPath := srcFs.fullPath(srcRemote) + dstFullPath := f.fullPath(dstRemote) + if srcFs.mountID == f.mountID && srcFullPath == dstFullPath { + return fs.ErrorDirExists + } + dstDir := dir(dstFullPath) + err := f.mkdir(dstDir) + if err != nil { + return fs.ErrorCantDirMove + } + err = f.client.FilesMove(srcFs.mountID, srcFullPath, f.mountID, dstFullPath) + if err != nil { + return fs.ErrorCantDirMove + } + return nil +} + +// About reports space usage (with a MB precision) +func (f *Fs) About() (*fs.Usage, error) { + mount, err := f.client.MountsDetails(f.mountID) + if err != nil { + return nil, err + } + return &fs.Usage{ + Total: fs.NewUsageValue(mount.SpaceTotal * 1024 * 1024), + Used: fs.NewUsageValue(mount.SpaceUsed * 1024 * 1024), + Trashed: nil, + Other: nil, + Free: fs.NewUsageValue((mount.SpaceTotal - mount.SpaceUsed) * 1024 * 1024), + Objects: nil, + }, nil +} + +// Purge purges the complete Fs +func (f *Fs) Purge() error { + err := translateErrorsDir(f.client.FilesDelete(f.mountID, f.fullPath(""))) + return err +} + +// linkCreate is a Koofr API request for creating a public link +type linkCreate struct { + Path string `json:"path"` +} + +// link is a Koofr API response to creating a public link +type link struct { + ID string `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + Counter int64 `json:"counter"` + URL string `json:"url"` + ShortURL string `json:"shortUrl"` + Hash string `json:"hash"` + Host string `json:"host"` + HasPassword bool `json:"hasPassword"` + Password string `json:"password"` + ValidFrom int64 `json:"validFrom"` + ValidTo int64 `json:"validTo"` + PasswordRequired bool `json:"passwordRequired"` +} + +// createLink makes a Koofr API call to create a public link +func createLink(c *koofrclient.KoofrClient, mountID string, path string) (*link, error) { + linkCreate := linkCreate{ + Path: path, + } + linkData := link{} + + request := httpclient.RequestData{ + Method: "POST", + Path: "/api/v2/mounts/" + mountID + "/links", + ExpectedStatus: []int{http.StatusOK, http.StatusCreated}, + ReqEncoding: httpclient.EncodingJSON, + ReqValue: linkCreate, + RespEncoding: httpclient.EncodingJSON, + RespValue: &linkData, + } + + _, err := c.Request(&request) + if err != nil { + return nil, err + } + return &linkData, nil +} + +// PublicLink creates a public link to the remote path +func (f *Fs) PublicLink(remote string) (string, error) { + linkData, err := createLink(f.client, f.mountID, f.fullPath(remote)) + if err != nil { + return "", translateErrorsDir(err) + } + return linkData.ShortURL, nil +} diff --git a/backend/koofr/koofr_test.go b/backend/koofr/koofr_test.go new file mode 100644 index 000000000..a910156a7 --- /dev/null +++ b/backend/koofr/koofr_test.go @@ -0,0 +1,14 @@ +package koofr_test + +import ( + "testing" + + "github.com/ncw/rclone/fstest/fstests" +) + +// TestIntegration runs integration tests against the remote +func TestIntegration(t *testing.T) { + fstests.Run(t, &fstests.Opt{ + RemoteName: "TestKoofr:", + }) +} diff --git a/bin/make_manual.py b/bin/make_manual.py index 1ef8ead70..92092809a 100755 --- a/bin/make_manual.py +++ b/bin/make_manual.py @@ -36,6 +36,7 @@ docs = [ "http.md", "hubic.md", "jottacloud.md", + "koofr.md", "mega.md", "azureblob.md", "onedrive.md", diff --git a/docs/content/about.md b/docs/content/about.md index ab7da5c13..a975b159f 100644 --- a/docs/content/about.md +++ b/docs/content/about.md @@ -29,6 +29,7 @@ Rclone is a command line program to sync files and directories to and from: * {{< provider name="Hubic" home="https://hubic.com/" config="/hubic/" >}} * {{< provider name="Jottacloud" home="https://www.jottacloud.com/en/" config="/jottacloud/" >}} * {{< provider name="IBM COS S3" home="http://www.ibm.com/cloud/object-storage" config="/s3/#ibm-cos-s3" >}} +* {{< provider name="Koofr" home="https://koofr.eu/" config="/koofr/" >}} * {{< provider name="Memset Memstore" home="https://www.memset.com/cloud/storage/" config="/swift/" >}} * {{< provider name="Mega" home="https://mega.nz/" config="/mega/" >}} * {{< provider name="Microsoft Azure Blob Storage" home="https://azure.microsoft.com/en-us/services/storage/blobs/" config="/azureblob/" >}} diff --git a/docs/content/authors.md b/docs/content/authors.md index 80333a8a8..f87063bf3 100644 --- a/docs/content/authors.md +++ b/docs/content/authors.md @@ -243,3 +243,4 @@ Contributors * calisro * Dr.Rx * marcintustin + * jaKa Močnik diff --git a/docs/content/docs.md b/docs/content/docs.md index 9dbab549a..d743c8ed6 100644 --- a/docs/content/docs.md +++ b/docs/content/docs.md @@ -1,7 +1,7 @@ --- title: "Documentation" description: "Rclone Usage" -date: "2015-06-06" +date: "2019-02-25" --- Configure @@ -34,6 +34,7 @@ See the following for detailed instructions for * [HTTP](/http/) * [Hubic](/hubic/) * [Jottacloud](/jottacloud/) + * [Koofr](/koofr/) * [Mega](/mega/) * [Microsoft Azure Blob Storage](/azureblob/) * [Microsoft OneDrive](/onedrive/) diff --git a/docs/content/koofr.md b/docs/content/koofr.md new file mode 100644 index 000000000..c9920337a --- /dev/null +++ b/docs/content/koofr.md @@ -0,0 +1,189 @@ +--- +title: "Koofr" +description: "Rclone docs for Koofr" +date: "2019-02-25" +--- + + Koofr +----------------------------------------- + +Paths are specified as `remote:path` + +Paths may be as deep as required, eg `remote:directory/subdirectory`. + +The initial setup for Koofr involves creating an application password for +rclone. You can do that by opening the Koofr +[web application](https://app.koofr.net/app/admin/preferences/password), +giving the password a nice name like `rclone` and clicking on generate. + +Here is an example of how to make a remote called `koofr`. 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> koofr +Type of storage to configure. +Enter a string value. Press Enter for the default (""). +Choose a number from below, or type in your own value + 1 / A stackable unification remote, which can appear to merge the contents of several remotes + \ "union" + 2 / Alias for a existing remote + \ "alias" + 3 / Amazon Drive + \ "amazon cloud drive" + 4 / Amazon S3 Compliant Storage Provider (AWS, Alibaba, Ceph, Digital Ocean, Dreamhost, IBM COS, Minio, etc) + \ "s3" + 5 / Backblaze B2 + \ "b2" + 6 / Box + \ "box" + 7 / Cache a remote + \ "cache" + 8 / Dropbox + \ "dropbox" + 9 / Encrypt/Decrypt a remote + \ "crypt" +10 / FTP Connection + \ "ftp" +11 / Google Cloud Storage (this is not Google Drive) + \ "google cloud storage" +12 / Google Drive + \ "drive" +13 / Hubic + \ "hubic" +14 / JottaCloud + \ "jottacloud" +15 / Koofr + \ "koofr" +16 / Local Disk + \ "local" +17 / Mega + \ "mega" +18 / Microsoft Azure Blob Storage + \ "azureblob" +19 / Microsoft OneDrive + \ "onedrive" +20 / OpenDrive + \ "opendrive" +21 / Openstack Swift (Rackspace Cloud Files, Memset Memstore, OVH) + \ "swift" +22 / Pcloud + \ "pcloud" +23 / QingCloud Object Storage + \ "qingstor" +24 / SSH/SFTP Connection + \ "sftp" +25 / Webdav + \ "webdav" +26 / Yandex Disk + \ "yandex" +27 / http Connection + \ "http" +Storage> koofr +** See help for koofr backend at: https://rclone.org/koofr/ ** + +Your Koofr user name +Enter a string value. Press Enter for the default (""). +user> USER@NAME +Your Koofr password for rclone (generate one at https://app.koofr.net/app/admin/preferences/password) +y) Yes type in my own password +g) Generate random password +y/g> y +Enter the password: +password: +Confirm the password: +password: +Edit advanced config? (y/n) +y) Yes +n) No +y/n> n +Remote config +-------------------- +[koofr] +type = koofr +baseurl = https://app.koofr.net +user = USER@NAME +password = *** ENCRYPTED *** +-------------------- +y) Yes this is OK +e) Edit this remote +d) Delete this remote +y/e/d> y +``` + +You can choose to edit advanced config in order to enter your own service URL +if you use an on-premise or white label Koofr instance, or choose an alternative +mount instead of your primary storage. + +Once configured you can then use `rclone` like this, + +List directories in top level of your Koofr + + rclone lsd koofr: + +List all the files in your Koofr + + rclone ls koofr: + +To copy a local directory to an Koofr directory called backup + + rclone copy /home/source remote:backup + + +### Standard Options + +Here are the standard options specific to koofr (Koofr). + +#### --koofr-user + +Your Koofr user name + +- Config: user +- Env Var: RCLONE_KOOFR_USER +- Type: string +- Default: "" + +#### --koofr-password + +Your Koofr password for rclone (generate one at https://app.koofr.net/app/admin/preferences/password) + +- Config: password +- Env Var: RCLONE_KOOFR_PASSWORD +- Type: string +- Default: "" + +### Advanced Options + +Here are the advanced options specific to koofr (Koofr). + +#### --koofr-baseurl + +Base URL of the Koofr API to connect to + +- Config: baseurl +- Env Var: RCLONE_KOOFR_BASEURL +- Type: string +- Default: "https://app.koofr.net" + +#### --koofr-mountid + +Mount ID of the mount to use. If omitted, the primary mount is used. + +- Config: mountid +- Env Var: RCLONE_KOOFR_MOUNTID +- Type: string +- Default: "" + + + +### Limitations ### + +Note that Koofr is case insensitive so you can't have a file called +"Hello.doc" and one called "hello.doc". diff --git a/docs/content/overview.md b/docs/content/overview.md index f6255634e..87e020e25 100644 --- a/docs/content/overview.md +++ b/docs/content/overview.md @@ -2,7 +2,7 @@ title: "Overview of cloud storage systems" description: "Overview of cloud storage systems" type: page -date: "2015-09-06" +date: "2019-02-25" --- # Overview of cloud storage systems # @@ -28,6 +28,7 @@ Here is an overview of the major features of each cloud storage system. | HTTP | - | No | No | No | R | | Hubic | MD5 | Yes | No | No | R/W | | Jottacloud | MD5 | Yes | Yes | No | R/W | +| Koofr | MD5 | No | Yes | No | - | | Mega | - | No | No | Yes | - | | Microsoft Azure Blob Storage | MD5 | Yes | No | No | R/W | | Microsoft OneDrive | SHA1 ‡‡ | Yes | Yes | No | R | diff --git a/docs/layouts/chrome/navbar.html b/docs/layouts/chrome/navbar.html index e5c2ecc18..4c1807d30 100644 --- a/docs/layouts/chrome/navbar.html +++ b/docs/layouts/chrome/navbar.html @@ -67,6 +67,7 @@
  • HTTP
  • Hubic
  • Jottacloud
  • +
  • Koofr
  • Mega
  • Microsoft Azure Blob Storage
  • Microsoft OneDrive
  • diff --git a/fstest/test_all/config.yaml b/fstest/test_all/config.yaml index ff8d1537c..1521326c6 100644 --- a/fstest/test_all/config.yaml +++ b/fstest/test_all/config.yaml @@ -138,3 +138,7 @@ backends: remote: "TestUnion:" subdir: false fastlist: false + - backend: "koofr" + remote: "TestKoofr:" + subdir: false + fastlist: false