mirror of
https://github.com/rclone/rclone.git
synced 2024-11-25 02:47:18 +08:00
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.
This commit is contained in:
parent
1d14e30383
commit
118a8b949e
|
@ -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/)
|
* Hubic [:page_facing_up:](https://rclone.org/hubic/)
|
||||||
* Jottacloud [:page_facing_up:](https://rclone.org/jottacloud/)
|
* Jottacloud [:page_facing_up:](https://rclone.org/jottacloud/)
|
||||||
* IBM COS S3 [:page_facing_up:](https://rclone.org/s3/#ibm-cos-s3)
|
* 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/)
|
* Memset Memstore [:page_facing_up:](https://rclone.org/swift/)
|
||||||
* Mega [:page_facing_up:](https://rclone.org/mega/)
|
* Mega [:page_facing_up:](https://rclone.org/mega/)
|
||||||
* Microsoft Azure Blob Storage [:page_facing_up:](https://rclone.org/azureblob/)
|
* Microsoft Azure Blob Storage [:page_facing_up:](https://rclone.org/azureblob/)
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
_ "github.com/ncw/rclone/backend/http"
|
_ "github.com/ncw/rclone/backend/http"
|
||||||
_ "github.com/ncw/rclone/backend/hubic"
|
_ "github.com/ncw/rclone/backend/hubic"
|
||||||
_ "github.com/ncw/rclone/backend/jottacloud"
|
_ "github.com/ncw/rclone/backend/jottacloud"
|
||||||
|
_ "github.com/ncw/rclone/backend/koofr"
|
||||||
_ "github.com/ncw/rclone/backend/local"
|
_ "github.com/ncw/rclone/backend/local"
|
||||||
_ "github.com/ncw/rclone/backend/mega"
|
_ "github.com/ncw/rclone/backend/mega"
|
||||||
_ "github.com/ncw/rclone/backend/onedrive"
|
_ "github.com/ncw/rclone/backend/onedrive"
|
||||||
|
|
589
backend/koofr/koofr.go
Normal file
589
backend/koofr/koofr.go
Normal file
|
@ -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
|
||||||
|
}
|
14
backend/koofr/koofr_test.go
Normal file
14
backend/koofr/koofr_test.go
Normal file
|
@ -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:",
|
||||||
|
})
|
||||||
|
}
|
|
@ -36,6 +36,7 @@ docs = [
|
||||||
"http.md",
|
"http.md",
|
||||||
"hubic.md",
|
"hubic.md",
|
||||||
"jottacloud.md",
|
"jottacloud.md",
|
||||||
|
"koofr.md",
|
||||||
"mega.md",
|
"mega.md",
|
||||||
"azureblob.md",
|
"azureblob.md",
|
||||||
"onedrive.md",
|
"onedrive.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="Hubic" home="https://hubic.com/" config="/hubic/" >}}
|
||||||
* {{< provider name="Jottacloud" home="https://www.jottacloud.com/en/" config="/jottacloud/" >}}
|
* {{< 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="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="Memset Memstore" home="https://www.memset.com/cloud/storage/" config="/swift/" >}}
|
||||||
* {{< provider name="Mega" home="https://mega.nz/" config="/mega/" >}}
|
* {{< 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/" >}}
|
* {{< provider name="Microsoft Azure Blob Storage" home="https://azure.microsoft.com/en-us/services/storage/blobs/" config="/azureblob/" >}}
|
||||||
|
|
|
@ -243,3 +243,4 @@ Contributors
|
||||||
* calisro <robert.calistri@gmail.com>
|
* calisro <robert.calistri@gmail.com>
|
||||||
* Dr.Rx <david.rey@nventive.com>
|
* Dr.Rx <david.rey@nventive.com>
|
||||||
* marcintustin <marcintustin@users.noreply.github.com>
|
* marcintustin <marcintustin@users.noreply.github.com>
|
||||||
|
* jaKa Močnik <jaka@koofr.net>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
title: "Documentation"
|
title: "Documentation"
|
||||||
description: "Rclone Usage"
|
description: "Rclone Usage"
|
||||||
date: "2015-06-06"
|
date: "2019-02-25"
|
||||||
---
|
---
|
||||||
|
|
||||||
Configure
|
Configure
|
||||||
|
@ -34,6 +34,7 @@ See the following for detailed instructions for
|
||||||
* [HTTP](/http/)
|
* [HTTP](/http/)
|
||||||
* [Hubic](/hubic/)
|
* [Hubic](/hubic/)
|
||||||
* [Jottacloud](/jottacloud/)
|
* [Jottacloud](/jottacloud/)
|
||||||
|
* [Koofr](/koofr/)
|
||||||
* [Mega](/mega/)
|
* [Mega](/mega/)
|
||||||
* [Microsoft Azure Blob Storage](/azureblob/)
|
* [Microsoft Azure Blob Storage](/azureblob/)
|
||||||
* [Microsoft OneDrive](/onedrive/)
|
* [Microsoft OneDrive](/onedrive/)
|
||||||
|
|
189
docs/content/koofr.md
Normal file
189
docs/content/koofr.md
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
---
|
||||||
|
title: "Koofr"
|
||||||
|
description: "Rclone docs for Koofr"
|
||||||
|
date: "2019-02-25"
|
||||||
|
---
|
||||||
|
|
||||||
|
<i class="fa fa-suitcase"></i> 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
|
||||||
|
|
||||||
|
<!--- autogenerated options start - DO NOT EDIT, instead edit fs.RegInfo in backend/koofr/koofr.go then run make backenddocs -->
|
||||||
|
### 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: ""
|
||||||
|
|
||||||
|
<!--- autogenerated options stop -->
|
||||||
|
|
||||||
|
### Limitations ###
|
||||||
|
|
||||||
|
Note that Koofr is case insensitive so you can't have a file called
|
||||||
|
"Hello.doc" and one called "hello.doc".
|
|
@ -2,7 +2,7 @@
|
||||||
title: "Overview of cloud storage systems"
|
title: "Overview of cloud storage systems"
|
||||||
description: "Overview of cloud storage systems"
|
description: "Overview of cloud storage systems"
|
||||||
type: page
|
type: page
|
||||||
date: "2015-09-06"
|
date: "2019-02-25"
|
||||||
---
|
---
|
||||||
|
|
||||||
# Overview of cloud storage systems #
|
# 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 |
|
| HTTP | - | No | No | No | R |
|
||||||
| Hubic | MD5 | Yes | No | No | R/W |
|
| Hubic | MD5 | Yes | No | No | R/W |
|
||||||
| Jottacloud | MD5 | Yes | Yes | No | R/W |
|
| Jottacloud | MD5 | Yes | Yes | No | R/W |
|
||||||
|
| Koofr | MD5 | No | Yes | No | - |
|
||||||
| Mega | - | No | No | Yes | - |
|
| Mega | - | No | No | Yes | - |
|
||||||
| Microsoft Azure Blob Storage | MD5 | Yes | No | No | R/W |
|
| Microsoft Azure Blob Storage | MD5 | Yes | No | No | R/W |
|
||||||
| Microsoft OneDrive | SHA1 ‡‡ | Yes | Yes | No | R |
|
| Microsoft OneDrive | SHA1 ‡‡ | Yes | Yes | No | R |
|
||||||
|
|
|
@ -67,6 +67,7 @@
|
||||||
<li><a href="/http/"><i class="fa fa-globe"></i> HTTP</a></li>
|
<li><a href="/http/"><i class="fa fa-globe"></i> HTTP</a></li>
|
||||||
<li><a href="/hubic/"><i class="fa fa-space-shuttle"></i> Hubic</a></li>
|
<li><a href="/hubic/"><i class="fa fa-space-shuttle"></i> Hubic</a></li>
|
||||||
<li><a href="/jottacloud/"><i class="fa fa-cloud"></i> Jottacloud</a></li>
|
<li><a href="/jottacloud/"><i class="fa fa-cloud"></i> Jottacloud</a></li>
|
||||||
|
<li><a href="/koofr/"><i class="fa fa-suitcase"></i> Koofr</a></li>
|
||||||
<li><a href="/mega/"><i class="fa fa-archive"></i> Mega</a></li>
|
<li><a href="/mega/"><i class="fa fa-archive"></i> Mega</a></li>
|
||||||
<li><a href="/azureblob/"><i class="fa fa-windows"></i> Microsoft Azure Blob Storage</a></li>
|
<li><a href="/azureblob/"><i class="fa fa-windows"></i> Microsoft Azure Blob Storage</a></li>
|
||||||
<li><a href="/onedrive/"><i class="fa fa-windows"></i> Microsoft OneDrive</a></li>
|
<li><a href="/onedrive/"><i class="fa fa-windows"></i> Microsoft OneDrive</a></li>
|
||||||
|
|
|
@ -138,3 +138,7 @@ backends:
|
||||||
remote: "TestUnion:"
|
remote: "TestUnion:"
|
||||||
subdir: false
|
subdir: false
|
||||||
fastlist: false
|
fastlist: false
|
||||||
|
- backend: "koofr"
|
||||||
|
remote: "TestKoofr:"
|
||||||
|
subdir: false
|
||||||
|
fastlist: false
|
||||||
|
|
Loading…
Reference in New Issue
Block a user