diff --git a/README.md b/README.md
index 95d60bf43..bfc4d574e 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,7 @@ Rclone is a command line program to sync files and directories to and from
* Hubic
* Microsoft OneDrive
* Openstack Swift / Rackspace cloud files / Memset Memstore
+ * QingStor
* SFTP
* Yandex Disk
* The local filesystem
diff --git a/bin/make_manual.py b/bin/make_manual.py
index a5135c570..f9e8f5877 100755
--- a/bin/make_manual.py
+++ b/bin/make_manual.py
@@ -33,6 +33,7 @@ docs = [
"http.md",
"hubic.md",
"onedrive.md",
+ "qingstor.md",
"swift.md",
"sftp.md",
"yandex.md",
diff --git a/cmd/cmd.go b/cmd/cmd.go
index 960b2408b..22be617f1 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -53,6 +53,7 @@ from various cloud storage systems and using file transfer services, such as:
* Hubic
* Microsoft OneDrive
* Openstack Swift / Rackspace cloud files / Memset Memstore
+ * QingStor
* SFTP
* Yandex Disk
* The local filesystem
diff --git a/docs/content/about.md b/docs/content/about.md
index 14cc929f8..edef66a43 100644
--- a/docs/content/about.md
+++ b/docs/content/about.md
@@ -25,6 +25,7 @@ Rclone is a command line program to sync files and directories to and from
* Hubic
* Microsoft OneDrive
* Openstack Swift / Rackspace cloud files / Memset Memstore
+ * QingStor
* SFTP
* Yandex Disk
* The local filesystem
diff --git a/docs/content/docs.md b/docs/content/docs.md
index 392399aba..780db1ce6 100644
--- a/docs/content/docs.md
+++ b/docs/content/docs.md
@@ -31,8 +31,9 @@ See the following for detailed instructions for
* [HTTP](/http/)
* [Hubic](/hubic/)
* [Microsoft OneDrive](/onedrive/)
+ * [Openstack Swift / Rackspace Cloudfiles / Memset Memstore](/swift/)
+ * [QingStor](/qingstor/)
* [SFTP](/sftp/)
- * [Swift / Rackspace Cloudfiles / Memset Memstore](/swift/)
* [Yandex Disk](/yandex/)
* [The local filesystem](/local/)
diff --git a/docs/content/overview.md b/docs/content/overview.md
index bae33a05a..76589cc4d 100644
--- a/docs/content/overview.md
+++ b/docs/content/overview.md
@@ -29,6 +29,7 @@ Here is an overview of the major features of each cloud storage system.
| Hubic | MD5 | Yes | No | No | R/W |
| Microsoft OneDrive | SHA1 | Yes | Yes | No | R |
| Openstack Swift | MD5 | Yes | No | No | R/W |
+| QingStor | - | No | No | No | R/W |
| SFTP | - | Yes | Depends | No | - |
| Yandex Disk | MD5 | Yes | No | No | R/W |
| The local filesystem | All | Yes | Depends | No | - |
@@ -124,6 +125,7 @@ operations more efficient.
| Hubic | Yes † | Yes | No | No | No | Yes |
| Microsoft OneDrive | Yes | Yes | Yes | No [#197](https://github.com/ncw/rclone/issues/197) | No [#575](https://github.com/ncw/rclone/issues/575) | No |
| Openstack Swift | Yes † | Yes | No | No | No | Yes |
+| QingStor | No | Yes | No | No | No | Yes |
| SFTP | No | No | Yes | Yes | No | No |
| Yandex Disk | Yes | No | No | No | No [#575](https://github.com/ncw/rclone/issues/575) | Yes |
| The local filesystem | Yes | No | Yes | Yes | No | No |
diff --git a/docs/content/qingstor.md b/docs/content/qingstor.md
new file mode 100644
index 000000000..8fb814960
--- /dev/null
+++ b/docs/content/qingstor.md
@@ -0,0 +1,147 @@
+---
+title: "QingStor"
+description: "Rclone docs for QingStor Object Storage"
+date: "2017-06-26"
+---
+
+ QingStor
+---------------------------------------
+
+Paths are specified as `remote:bucket` (or `remote:` for the `lsd`
+command.) You may put subdirectories in too, eg `remote:bucket/path/to/dir`.
+
+Here is an example of making an QingStor configuration. First run
+
+ rclone config
+
+This will guide you through an interactive setup process.
+
+```
+No remotes found - make a new one
+n) New remote
+r) Rename remote
+c) Copy remote
+s) Set configuration password
+q) Quit config
+n/r/c/s/q> n
+name> remote
+Type of storage to configure.
+Choose a number from below, or type in your own value
+ 1 / Amazon Drive
+ \ "amazon cloud drive"
+ 2 / Amazon S3 (also Dreamhost, Ceph, Minio)
+ \ "s3"
+ 3 / Backblaze B2
+ \ "b2"
+ 4 / Dropbox
+ \ "dropbox"
+ 5 / Encrypt/Decrypt a remote
+ \ "crypt"
+ 6 / FTP Connection
+ \ "ftp"
+ 7 / Google Cloud Storage (this is not Google Drive)
+ \ "google cloud storage"
+ 8 / Google Drive
+ \ "drive"
+ 9 / Hubic
+ \ "hubic"
+10 / Local Disk
+ \ "local"
+11 / Microsoft OneDrive
+ \ "onedrive"
+12 / Openstack Swift (Rackspace Cloud Files, Memset Memstore, OVH)
+ \ "swift"
+13 / QingStor Object Storage
+ \ "qingstor"
+14 / SSH/SFTP Connection
+ \ "sftp"
+15 / Yandex Disk
+ \ "yandex"
+Storage> 13
+Get QingStor credentials from runtime. Only applies if access_key_id and secret_access_key is blank.
+Choose a number from below, or type in your own value
+ 1 / Enter QingStor credentials in the next step
+ \ "false"
+ 2 / Get QingStor credentials from the environment (env vars or IAM)
+ \ "true"
+env_auth> 1
+QingStor Access Key ID - leave blank for anonymous access or runtime credentials.
+access_key_id> access_key
+QingStor Secret Access Key (password) - leave blank for anonymous access or runtime credentials.
+secret_access_key> secret_key
+Enter a endpoint URL to connection QingStor API.
+Leave blank will use the default value "https://qingstor.com:443"
+endpoint>
+Zone connect to. Default is "pek3a".
+Choose a number from below, or type in your own value
+ / The Beijing (China) Three Zone
+ 1 | Needs location constraint pek3a.
+ \ "pek3a"
+ / The Shanghai (China) First Zone
+ 2 | Needs location constraint sh1a.
+ \ "sh1a"
+zone> 1
+Number of connnection retry.
+Leave blank will use the default value "3".
+connection_retries>
+Remote config
+--------------------
+[remote]
+env_auth = false
+access_key_id = access_key
+secret_access_key = secret_key
+endpoint =
+zone = pek3a
+connection_retries =
+--------------------
+y) Yes this is OK
+e) Edit this remote
+d) Delete this remote
+y/e/d> y
+```
+
+This remote is called `remote` and can now be used like this
+
+See all buckets
+
+ rclone lsd remote:
+
+Make a new bucket
+
+ rclone mkdir remote:bucket
+
+List the contents of a bucket
+
+ rclone ls remote:bucket
+
+Sync `/home/local/directory` to the remote bucket, deleting any excess
+files in the bucket.
+
+ rclone sync /home/local/directory remote:bucket
+
+### Multipart uploads ###
+
+rclone supports multipart uploads with QingStor which means that it can
+upload files bigger than 5GB. Note that files uploaded with multipart
+upload don't have an MD5SUM.
+
+### Buckets and Zone ###
+
+With QingStor you can list buckets (`rclone lsd`) using any zone,
+but you can only access the content of a bucket from the zone it was
+created in. If you attempt to access a bucket from the wrong zone,
+you will get an error, `incorrect zone, the bucket is not in 'XXX'
+zone`.
+
+### Authentication ###
+There are two ways to supply `rclone` with a set of QingStor
+credentials. In order of precedence:
+
+ - Directly in the rclone configuration file (as configured by `rclone config`)
+ - set `access_key_id` and `secret_access_key`
+ - Runtime configuration:
+ - set `env_auth` to `true` in the config file
+ - Exporting the following environment variables before running `rclone`
+ - Access Key ID: `QS_ACCESS_KEY_ID` or `QS_ACCESS_KEY`
+ - Secret Access Key: `QS_SECRET_ACCESS_KEY` or `QS_SECRET_KEY`
+
diff --git a/docs/layouts/chrome/navbar.html b/docs/layouts/chrome/navbar.html
index 5f3068f09..59f94b838 100644
--- a/docs/layouts/chrome/navbar.html
+++ b/docs/layouts/chrome/navbar.html
@@ -61,6 +61,7 @@
HTTP
Hubic
Microsoft OneDrive
+ QingStor
Openstack Swift
SFTP
Yandex Disk
diff --git a/fs/all/all.go b/fs/all/all.go
index 43e21cdfb..4cb95b39c 100644
--- a/fs/all/all.go
+++ b/fs/all/all.go
@@ -14,6 +14,7 @@ import (
_ "github.com/ncw/rclone/hubic"
_ "github.com/ncw/rclone/local"
_ "github.com/ncw/rclone/onedrive"
+ _ "github.com/ncw/rclone/qingstor"
_ "github.com/ncw/rclone/s3"
_ "github.com/ncw/rclone/sftp"
_ "github.com/ncw/rclone/swift"
diff --git a/fs/test_all.go b/fs/test_all.go
index b4f1d8349..ae13c9ee4 100644
--- a/fs/test_all.go
+++ b/fs/test_all.go
@@ -103,6 +103,11 @@ var (
SubDir: false,
FastList: false,
},
+ {
+ Name: "TestQingStor:",
+ SubDir: false,
+ FastList: false,
+ },
}
binary = "fs.test"
// Flags
diff --git a/fstest/fstests/gen_tests.go b/fstest/fstests/gen_tests.go
index 12c764ec2..171688b1e 100644
--- a/fstest/fstests/gen_tests.go
+++ b/fstest/fstests/gen_tests.go
@@ -162,5 +162,6 @@ func main() {
generateTestProgram(t, fns, "Sftp")
generateTestProgram(t, fns, "FTP")
generateTestProgram(t, fns, "Box")
+ generateTestProgram(t, fns, "QingStor")
log.Printf("Done")
}
diff --git a/qingstor/qingstor.go b/qingstor/qingstor.go
new file mode 100644
index 000000000..275ef6f36
--- /dev/null
+++ b/qingstor/qingstor.go
@@ -0,0 +1,982 @@
+// Package qingstor provides an interface to QingStor object storage
+// Home: https://www.qingcloud.com/
+package qingstor
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "net/http"
+ "path"
+ "regexp"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/ncw/rclone/fs"
+ "github.com/pkg/errors"
+ "github.com/yunify/qingstor-sdk-go/config"
+ qsErr "github.com/yunify/qingstor-sdk-go/request/errors"
+ qs "github.com/yunify/qingstor-sdk-go/service"
+)
+
+// Register with Fs
+func init() {
+ fs.Register(&fs.RegInfo{
+ Name: "qingstor",
+ Description: "QingClound Object Storage",
+ NewFs: NewFs,
+ Options: []fs.Option{{
+ Name: "env_auth",
+ Help: "Get QingStor credentials from runtime. Only applies if access_key_id and secret_access_key is blank.",
+ Examples: []fs.OptionExample{
+ {
+ Value: "false",
+ Help: "Enter QingStor credentials in the next step",
+ }, {
+ Value: "true",
+ Help: "Get QingStor credentials from the environment (env vars or IAM)",
+ },
+ },
+ }, {
+ Name: "access_key_id",
+ Help: "QingStor Access Key ID - leave blank for anonymous access or runtime credentials.",
+ }, {
+ Name: "secret_access_key",
+ Help: "QingStor Secret Access Key (password) - leave blank for anonymous access or runtime credentials.",
+ }, {
+ Name: "endpoint",
+ Help: "Enter a endpoint URL to connection QingStor API.\nLeave blank will use the default value \"https://qingstor.com:443\"",
+ }, {
+ Name: "zone",
+ Help: "Choose or Enter a zone to connect. Default is \"pek3a\".",
+ Examples: []fs.OptionExample{
+ {
+ Value: "pek3a",
+
+ Help: "The Beijing (China) Three Zone\nNeeds location constraint pek3a.",
+ },
+ {
+ Value: "sh1a",
+
+ Help: "The Shanghai (China) First Zone\nNeeds location constraint sh1a.",
+ },
+ },
+ }, {
+ Name: "connection_retries",
+ Help: "Number of connnection retry.\nLeave blank will use the default value \"3\".",
+ }},
+ })
+}
+
+// Constants
+const (
+ listLimitSize = 1000 // Number of items to read at once
+ maxSizeForCopy = 1024 * 1024 * 1024 * 5 // The maximum size of object we can COPY
+ maxSizeForPart = 1024 * 1024 * 1024 * 1 // The maximum size of object we can Upload in Multipart Upload API
+ multipartUploadSize = 1024 * 1024 * 64 // The size of multipart upload object as once.
+ MaxMultipleParts = 10000 // The maximum number of upload multiple parts
+)
+
+// Globals
+func timestampToTime(tp int64) time.Time {
+ timeLayout := time.RFC3339Nano
+ ts := time.Unix(tp, 0).Format(timeLayout)
+ tm, _ := time.Parse(timeLayout, ts)
+ return tm.UTC()
+}
+
+// Fs represents a remote qingstor server
+type Fs struct {
+ name string // The name of the remote
+ zone string // The zone we are working on
+ bucket string // The bucket we are working on
+ bucketOK bool // true if we have created the bucket
+ bucketMtx sync.Mutex // mutex to protect bucket
+ root string // The root is a subdir, is a special object
+ features *fs.Features // optional features
+ svc *qs.Service // The connection to the qingstor server
+}
+
+// Object describes a qingstor object
+type Object struct {
+ // Will definitely have everything but meta which may be nil
+ //
+ // List will read everything but meta & mimeType - to fill
+ // that in you need to call readMetaData
+ fs *Fs // what this object is part of
+ remote string // object of remote
+ etag string // md5sum of the object
+ size int64 // length of the object content
+ mimeType string // ContentType of object - may be ""
+ lastModified time.Time // Last modified
+ encrypted bool // whether the object is encryption
+ algo string // Custom encryption algorithms
+}
+
+// ------------------------------------------------------------
+
+// parseParse parses a qingstor 'url'
+func qsParsePath(path string) (bucket, key string, err error) {
+ // Pattern to match a qingstor path
+ var matcher = regexp.MustCompile(`^([^/]*)(.*)$`)
+ parts := matcher.FindStringSubmatch(path)
+ if parts == nil {
+ err = errors.Errorf("Couldn't parse bucket out of qingstor path %q", path)
+ } else {
+ bucket, key = parts[1], parts[2]
+ key = strings.Trim(key, "/")
+ }
+ return
+}
+
+// Split an URL into three parts: protocol host and port
+func qsParseEndpoint(endpoint string) (protocol, host, port string, err error) {
+ /*
+ Pattern to match a endpoint,
+ eg: "http(s)://qingstor.com:443" --> "http(s)", "qingstor.com", 443
+ "http(s)//qingstor.com" --> "http(s)", "qingstor.com", ""
+ "qingstor.com" --> "", "qingstor.com", ""
+ */
+ defer func() {
+ if r := recover(); r != nil {
+ switch x := r.(type) {
+ case error:
+ err = x
+ default:
+ err = nil
+ }
+ }
+ }()
+ var mather = regexp.MustCompile(`^(?:(http|https)://)*(\w+\.(?:[\w\.])*)(?::(\d{0,5}))*$`)
+ parts := mather.FindStringSubmatch(endpoint)
+ protocol, host, port = parts[1], parts[2], parts[3]
+ return
+}
+
+// qsConnection makes a connection to qingstor
+func qsServiceConnection(name string) (*qs.Service, error) {
+ accessKeyID := fs.ConfigFileGet(name, "access_key_id")
+ secretAccessKey := fs.ConfigFileGet(name, "secret_access_key")
+
+ switch {
+ case fs.ConfigFileGetBool(name, "env_auth", false):
+ // No need for empty checks if "env_auth" is true
+ case accessKeyID == "" && secretAccessKey == "":
+ // if no access key/secret and iam is explicitly disabled then fall back to anon interaction
+ case accessKeyID == "":
+ return nil, errors.New("access_key_id not found")
+ case secretAccessKey == "":
+ return nil, errors.New("secret_access_key not found")
+ }
+
+ protocol := "https"
+ host := "qingstor.com"
+ port := 443
+
+ endpoint := fs.ConfigFileGet(name, "endpoint", "")
+ if endpoint != "" {
+ _protocol, _host, _port, err := qsParseEndpoint(endpoint)
+
+ if err != nil {
+ return nil, fmt.Errorf("The endpoint \"%s\" format error", endpoint)
+ }
+
+ if _protocol != "" {
+ protocol = _protocol
+ }
+ host = _host
+ if _port != "" {
+ port, _ = strconv.Atoi(_port)
+ }
+
+ }
+
+ connectionRetries := 3
+ retries := fs.ConfigFileGet(name, "connection_retries", "")
+ if retries != "" {
+ connectionRetries, _ = strconv.Atoi(retries)
+ }
+
+ cf, err := config.NewDefault()
+ cf.AccessKeyID = accessKeyID
+ cf.SecretAccessKey = secretAccessKey
+ cf.Protocol = protocol
+ cf.Host = host
+ cf.Port = port
+ cf.ConnectionRetries = connectionRetries
+
+ svc, _ := qs.Init(cf)
+
+ return svc, err
+}
+
+// NewFs constructs an Fs from the path, bucket:path
+func NewFs(name, root string) (fs.Fs, error) {
+ bucket, key, err := qsParsePath(root)
+ if err != nil {
+ return nil, err
+ }
+ svc, err := qsServiceConnection(name)
+ if err != nil {
+ return nil, err
+ }
+
+ zone := fs.ConfigFileGet(name, "zone")
+ if zone == "" {
+ zone = "pek3a"
+ }
+
+ f := &Fs{
+ name: name,
+ zone: zone,
+ root: key,
+ bucket: bucket,
+ svc: svc,
+ }
+ f.features = (&fs.Features{ReadMimeType: true, WriteMimeType: true}).Fill(f)
+
+ if f.root != "" {
+ if !strings.HasSuffix(f.root, "/") {
+ f.root += "/"
+ }
+ //Check to see if the object exists
+ bucketInit, err := svc.Bucket(bucket, zone)
+ if err != nil {
+ return nil, err
+ }
+ _, err = bucketInit.HeadObject(key, &qs.HeadObjectInput{})
+ if err == nil {
+ f.root = path.Dir(key)
+ if f.root == "." {
+ f.root = ""
+ } else {
+ f.root += "/"
+ }
+ // return an error with an fs which points to the parent
+ return f, fs.ErrorIsFile
+ }
+ }
+ return f, nil
+}
+
+// Name of the remote (as passed into NewFs)
+func (f *Fs) Name() string {
+ return f.name
+}
+
+// Root of the remote (as passed into NewFs)
+func (f *Fs) Root() string {
+ if f.root == "" {
+ return f.bucket
+ }
+ return f.bucket + "/" + f.root
+}
+
+// String converts this Fs to a string
+func (f *Fs) String() string {
+ if f.root == "" {
+ return fmt.Sprintf("QingStor bucket %s", f.bucket)
+ }
+ return fmt.Sprintf("QingStor bucket %s root %s", f.bucket, f.root)
+}
+
+// Precision of the remote
+func (f *Fs) Precision() time.Duration {
+ //return time.Nanosecond
+ //Not supported temporary
+ return fs.ModTimeNotSupported
+}
+
+// Hashes returns the supported hash sets.
+func (f *Fs) Hashes() fs.HashSet {
+ //return fs.HashSet(fs.HashMD5)
+ //Not supported temporary
+ return fs.HashSet(fs.HashNone)
+}
+
+// Features returns the optional features of this Fs
+func (f *Fs) Features() *fs.Features {
+ return f.features
+}
+
+// Put created a new object
+func (f *Fs) Put(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
+ fsObj := &Object{
+ fs: f,
+ remote: src.Remote(),
+ }
+ return fsObj, fsObj.Update(in, src, options...)
+}
+
+// Copy src to this remote using server side copy operations.
+//
+// This is stored with the remote path given
+//
+// It returns the destination Object and a possible error
+//
+// Will only be called if src.Fs().Name() == f.Name()
+//
+// If it isn't possible then return fs.ErrorCantCopy
+func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) {
+ srcObj, ok := src.(*Object)
+ if !ok {
+ fs.Debugf(src, "Can't copy - not same remote type")
+ return nil, fs.ErrorCantCopy
+ }
+ srcFs := srcObj.fs
+ key := f.root + remote
+ source := path.Join("/"+srcFs.bucket, srcFs.root+srcObj.remote)
+
+ fs.Debugf(f, fmt.Sprintf("Copied, source key is: %s, and dst key is: %s", source, key))
+ req := qs.PutObjectInput{
+ XQSCopySource: &source,
+ }
+ bucketInit, err := f.svc.Bucket(f.bucket, f.zone)
+
+ if err != nil {
+ return nil, err
+ }
+ _, err = bucketInit.PutObject(key, &req)
+ if err != nil {
+ fs.Debugf(f, fmt.Sprintf("Copied Faild, API Error: %s", err))
+ return nil, err
+ }
+ return f.NewObject(remote)
+}
+
+// NewObject finds the Object at remote. If it can't be found
+// it returns the error fs.ErrorObjectNotFound.
+func (f *Fs) NewObject(remote string) (fs.Object, error) {
+ return f.newObjectWithInfo(remote, nil)
+}
+
+// Return an Object from a path
+//
+//If it can't be found it returns the error ErrorObjectNotFound.
+func (f *Fs) newObjectWithInfo(remote string, info *qs.KeyType) (fs.Object, error) {
+ o := &Object{
+ fs: f,
+ remote: remote,
+ }
+ if info != nil {
+ // Set info
+ if info.Size != nil {
+ o.size = *info.Size
+ }
+
+ if info.Etag != nil {
+ o.etag = qs.StringValue(info.Etag)
+ }
+ if info.Modified == nil {
+ fs.Logf(o, "Failed to read last modified")
+ o.lastModified = time.Now()
+ } else {
+ o.lastModified = timestampToTime(int64(*info.Modified))
+ }
+
+ if info.MimeType != nil {
+ o.mimeType = qs.StringValue(info.MimeType)
+ }
+
+ if info.Encrypted != nil {
+ o.encrypted = qs.BoolValue(info.Encrypted)
+ }
+
+ } else {
+ err := o.readMetaData() // reads info and meta, returning an error
+ if err != nil {
+ return nil, err
+ }
+ }
+ return o, nil
+}
+
+// listFn is called from list to handle an object.
+type listFn func(remote string, object *qs.KeyType, isDirectory bool) error
+
+// list the objects into the function supplied
+//
+// dir is the starting directory, "" for root
+//
+// Set recurse to read sub directories
+func (f *Fs) list(dir string, recurse bool, fn listFn) error {
+ prefix := f.root
+ if dir != "" {
+ prefix += dir + "/"
+ }
+
+ delimiter := ""
+ if !recurse {
+ delimiter = "/"
+ }
+
+ maxLimit := int(listLimitSize)
+ var marker *string
+
+ for {
+ bucketInit, err := f.svc.Bucket(f.bucket, f.zone)
+ if err != nil {
+ return err
+ }
+ // FIXME need to implement ALL loop
+ req := qs.ListObjectsInput{
+ Delimiter: &delimiter,
+ Prefix: &prefix,
+ Limit: &maxLimit,
+ Marker: marker,
+ }
+ resp, err := bucketInit.ListObjects(&req)
+ if err != nil {
+ if e, ok := err.(*qsErr.QingStorError); ok {
+ if e.StatusCode == http.StatusNotFound {
+ err = fs.ErrorDirNotFound
+ }
+ }
+ return err
+ }
+ rootLength := len(f.root)
+ if !recurse {
+ for _, commonPrefix := range resp.CommonPrefixes {
+ if commonPrefix == nil {
+ fs.Logf(f, "Nil common prefix received")
+ continue
+ }
+ remote := *commonPrefix
+ if !strings.HasPrefix(remote, f.root) {
+ fs.Logf(f, "Odd name received %q", remote)
+ continue
+ }
+ remote = remote[rootLength:]
+ if strings.HasSuffix(remote, "/") {
+ remote = remote[:len(remote)-1]
+ }
+
+ err = fn(remote, &qs.KeyType{Key: &remote}, true)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ for _, object := range resp.Keys {
+ key := qs.StringValue(object.Key)
+ if !strings.HasPrefix(key, f.root) {
+ fs.Logf(f, "Odd name received %q", key)
+ continue
+ }
+ remote := key[rootLength:]
+ err = fn(remote, object, false)
+ if err != nil {
+ return err
+ }
+ }
+ // Use NextMarker if set, otherwise use last Key
+ if resp.NextMarker == nil || *resp.NextMarker == "" {
+ //marker = resp.Keys[len(resp.Keys)-1].Key
+ break
+ } else {
+ marker = resp.NextMarker
+ }
+ }
+ return nil
+}
+
+// Convert a list item into a BasicInfo
+func (f *Fs) itemToDirEntry(remote string, object *qs.KeyType, isDirectory bool) (fs.DirEntry, error) {
+ if isDirectory {
+ size := int64(0)
+ if object.Size != nil {
+ size = *object.Size
+ }
+ d := fs.NewDir(remote, time.Time{}).SetSize(size)
+ return d, nil
+ }
+ o, err := f.newObjectWithInfo(remote, object)
+ if err != nil {
+ return nil, err
+ }
+ return o, nil
+}
+
+// listDir lists files and directories to out
+func (f *Fs) listDir(dir string) (entries fs.DirEntries, err error) {
+ // List the objects and directories
+ err = f.list(dir, false, func(remote string, object *qs.KeyType, isDirectory bool) error {
+ entry, err := f.itemToDirEntry(remote, object, isDirectory)
+ if err != nil {
+ return err
+ }
+ if entry != nil {
+ entries = append(entries, entry)
+ }
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ return entries, nil
+}
+
+// listBuckets lists the buckets to out
+func (f *Fs) listBuckets(dir string) (entries fs.DirEntries, err error) {
+ if dir != "" {
+ return nil, fs.ErrorListBucketRequired
+ }
+
+ req := qs.ListBucketsInput{
+ Location: &f.zone,
+ }
+ resp, err := f.svc.ListBuckets(&req)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, bucket := range resp.Buckets {
+ d := fs.NewDir(qs.StringValue(bucket.Name), qs.TimeValue(bucket.Created))
+ entries = append(entries, d)
+ }
+ return entries, nil
+}
+
+// List the objects and directories in dir into entries. The
+// entries can be returned in any order but should be for a
+// complete directory.
+//
+// dir should be "" to list the root, and should not have
+// trailing slashes.
+//
+// This should return ErrDirNotFound if the directory isn't
+// found.
+func (f *Fs) List(dir string) (entries fs.DirEntries, err error) {
+ if f.bucket == "" {
+ return f.listBuckets(dir)
+ }
+ return f.listDir(dir)
+}
+
+// ListR lists the objects and directories of the Fs starting
+// from dir recursively into out.
+//
+// dir should be "" to start from the root, and should not
+// have trailing slashes.
+//
+// This should return ErrDirNotFound if the directory isn't
+// found.
+//
+// It should call callback for each tranche of entries read.
+// These need not be returned in any particular order. If
+// callback returns an error then the listing will stop
+// immediately.
+//
+// Don't implement this unless you have a more efficient way
+// of listing recursively that doing a directory traversal.
+func (f *Fs) ListR(dir string, callback fs.ListRCallback) (err error) {
+ if f.bucket == "" {
+ return fs.ErrorListBucketRequired
+ }
+ list := fs.NewListRHelper(callback)
+ err = f.list(dir, true, func(remote string, object *qs.KeyType, isDirectory bool) error {
+ entry, err := f.itemToDirEntry(remote, object, isDirectory)
+ if err != nil {
+ return err
+ }
+ return list.Add(entry)
+ })
+ if err != nil {
+ return err
+ }
+ return list.Flush()
+}
+
+// Check if the bucket exists
+func (f *Fs) dirExists() (bool, error) {
+ bucketInit, err := f.svc.Bucket(f.bucket, f.zone)
+ if err != nil {
+ return false, err
+ }
+
+ _, err = bucketInit.Head()
+ if err == nil {
+ return true, nil
+ }
+
+ if e, ok := err.(*qsErr.QingStorError); ok {
+ if e.StatusCode == http.StatusNotFound {
+ err = nil
+ }
+ }
+ return false, err
+}
+
+// Mkdir creates the bucket if it doesn't exist
+func (f *Fs) Mkdir(dir string) error {
+ f.bucketMtx.Lock()
+ defer f.bucketMtx.Unlock()
+ if f.bucketOK {
+ return nil
+ }
+
+ exists, err := f.dirExists()
+ if err == nil {
+ f.bucketOK = exists
+ }
+ if err != nil || exists {
+ return err
+ }
+
+ bucketInit, err := f.svc.Bucket(f.bucket, f.zone)
+ if err != nil {
+ return err
+ }
+ _, err = bucketInit.Put()
+ if e, ok := err.(*qsErr.QingStorError); ok {
+ if e.StatusCode == http.StatusConflict {
+ err = nil
+ }
+ }
+
+ if err == nil {
+ f.bucketOK = true
+ }
+
+ return err
+}
+
+// dirIsEmpty check if the bucket empty
+func (f *Fs) dirIsEmpty() (bool, error) {
+ limit := 8
+ bucketInit, err := f.svc.Bucket(f.bucket, f.zone)
+ if err != nil {
+ return true, err
+ }
+
+ req := qs.ListObjectsInput{
+ Limit: &limit,
+ }
+ rsp, err := bucketInit.ListObjects(&req)
+
+ if err != nil {
+ return false, err
+ }
+ if len(rsp.Keys) == 0 {
+ return true, nil
+ }
+ return false, nil
+}
+
+// Rmdir delete a bucket
+func (f *Fs) Rmdir(dir string) error {
+ f.bucketMtx.Lock()
+ defer f.bucketMtx.Unlock()
+ if f.root != "" || dir != "" {
+ return nil
+ }
+
+ isEmpty, err := f.dirIsEmpty()
+ if err != nil {
+ return err
+ }
+ if !isEmpty {
+ fs.Debugf(f, fmt.Sprintf("The bucket %s you tried to delete not empty.", f.bucket))
+ return errors.New("BucketNotEmpty: The bucket you tried to delete is not empty")
+ }
+
+ fs.Debugf(f, fmt.Sprintf("Tried to delete the bucket %s", f.bucket))
+ bucketInit, err := f.svc.Bucket(f.bucket, f.zone)
+ if err != nil {
+ return err
+ }
+ _, err = bucketInit.Delete()
+ if err == nil {
+ f.bucketOK = false
+ }
+ return err
+}
+
+// readMetaData gets the metadata if it hasn't already been fetched
+//
+// it also sets the info
+func (o *Object) readMetaData() (err error) {
+ bucketInit, err := o.fs.svc.Bucket(o.fs.bucket, o.fs.zone)
+ if err != nil {
+ return err
+ }
+
+ key := o.fs.root + o.remote
+ fs.Debugf(o, fmt.Sprintf("Read metadata of key: %s", key))
+ resp, err := bucketInit.HeadObject(key, &qs.HeadObjectInput{})
+ if err != nil {
+ fs.Debugf(o, fmt.Sprintf("Read metadata faild, API Error: %s", err))
+ if e, ok := err.(*qsErr.QingStorError); ok {
+ if e.StatusCode == http.StatusNotFound {
+ return fs.ErrorObjectNotFound
+ }
+ }
+ return err
+ }
+ // Ignore missing Content-Length assuming it is 0
+ if resp.ContentLength != nil {
+ o.size = *resp.ContentLength
+ }
+
+ if resp.ETag != nil {
+ o.etag = qs.StringValue(resp.ETag)
+ }
+
+ if resp.LastModified == nil {
+ fs.Logf(o, "Failed to read last modified from HEAD: %v", err)
+ o.lastModified = time.Now()
+ } else {
+ o.lastModified = *resp.LastModified
+ }
+
+ if resp.ContentType != nil {
+ o.mimeType = qs.StringValue(resp.ContentType)
+ }
+
+ if resp.XQSEncryptionCustomerAlgorithm != nil {
+ o.algo = qs.StringValue(resp.XQSEncryptionCustomerAlgorithm)
+ o.encrypted = true
+ }
+
+ return nil
+}
+
+// ModTime returns the modification date of the file
+// It should return a best guess if one isn't available
+func (o *Object) ModTime() time.Time {
+ err := o.readMetaData()
+ if err != nil {
+ fs.Logf(o, "Failed to read metadata, %v", err)
+ return time.Now()
+ }
+ modTime := o.lastModified
+ return modTime
+}
+
+// SetModTime sets the modification time of the local fs object
+func (o *Object) SetModTime(modTime time.Time) error {
+ err := o.readMetaData()
+ if err != nil {
+ return err
+ }
+ o.lastModified = modTime
+ mimeType := fs.MimeType(o)
+
+ if o.size >= maxSizeForCopy {
+ fs.Debugf(o, "SetModTime is unsupported for objects bigger than %v bytes", fs.SizeSuffix(maxSizeForCopy))
+ return nil
+ }
+ // Copy the object to itself to update the metadata
+ key := o.fs.root + o.remote
+ sourceKey := path.Join("/", o.fs.bucket, key)
+
+ bucketInit, err := o.fs.svc.Bucket(o.fs.bucket, o.fs.zone)
+ if err != nil {
+ return err
+ }
+
+ req := qs.PutObjectInput{
+ XQSCopySource: &sourceKey,
+ ContentType: &mimeType,
+ }
+ _, err = bucketInit.PutObject(key, &req)
+
+ return err
+}
+
+// Open opens the file for read. Call Close() on the returned io.ReadCloser
+func (o *Object) Open(options ...fs.OpenOption) (io.ReadCloser, error) {
+ bucketInit, err := o.fs.svc.Bucket(o.fs.bucket, o.fs.zone)
+ if err != nil {
+ return nil, err
+ }
+
+ key := o.fs.root + o.remote
+ req := qs.GetObjectInput{}
+ for _, option := range options {
+ switch option.(type) {
+ case *fs.RangeOption, *fs.SeekOption:
+ _, value := option.Header()
+ req.Range = &value
+ default:
+ if option.Mandatory() {
+ fs.Logf(o, "Unsupported mandatory option: %v", option)
+ }
+ }
+ }
+ resp, err := bucketInit.GetObject(key, &req)
+ if err != nil {
+ return nil, err
+ }
+ return resp.Body, nil
+}
+
+// Update in to the object
+func (o *Object) Update(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
+ // The maximum size of upload object is multipartUploadSize * MaxMultipleParts
+ err := o.fs.Mkdir("")
+ if err != nil {
+ return err
+ }
+
+ bucketInit, err := o.fs.svc.Bucket(o.fs.bucket, o.fs.zone)
+ if err != nil {
+ return err
+ }
+ //Initiate Upload Multipart
+ key := o.fs.root + o.remote
+ var objectParts = []*qs.ObjectPartType{}
+ var uploadID *string
+ var partNumber int
+
+ defer func() {
+ if err != nil {
+ fs.Errorf(o, fmt.Sprintf("Create Object Faild, API ERROR: %s", err))
+ // Abort Upload when init success and upload failed
+ if uploadID != nil {
+ fs.Debugf(o, fmt.Sprintf("Abort Upload Multipart, upload_id: %s, objectParts: %s", *uploadID, objectParts))
+ abortReq := qs.AbortMultipartUploadInput{
+ UploadID: uploadID,
+ }
+ _, _ = bucketInit.AbortMultipartUpload(key, &abortReq)
+ }
+ }
+ }()
+
+ fs.Debugf(o, fmt.Sprintf("Initiate Upload Multipart, key: %s", key))
+ mimeType := fs.MimeType(src)
+ initReq := qs.InitiateMultipartUploadInput{
+ ContentType: &mimeType,
+ }
+ rsp, err := bucketInit.InitiateMultipartUpload(key, &initReq)
+ if err != nil {
+ return err
+ }
+ uploadID = rsp.UploadID
+
+ // Create an new buffer
+ buffer := new(bytes.Buffer)
+
+ for {
+ size, er := io.CopyN(buffer, in, multipartUploadSize)
+ if er != nil && er != io.EOF {
+ err = fmt.Errorf("read upload data failed, error: %s", er)
+ return err
+ }
+ if size == 0 && partNumber > 0 {
+ break
+ }
+ // Upload Multipart Object
+ number := partNumber
+ req := qs.UploadMultipartInput{
+ PartNumber: &number,
+ UploadID: uploadID,
+ ContentLength: &size,
+ Body: buffer,
+ }
+ fs.Debugf(o, fmt.Sprintf("Upload Multipart, upload_id: %s, part_number: %d", *uploadID, number))
+ _, err = bucketInit.UploadMultipart(key, &req)
+ if err != nil {
+ return err
+ }
+ part := qs.ObjectPartType{
+ PartNumber: &number,
+ Size: &size,
+ }
+ objectParts = append(objectParts, &part)
+ partNumber++
+ }
+
+ // Complete Multipart Upload
+ fs.Debugf(o, fmt.Sprintf("Complete Upload Multipart, upload_id: %s, objectParts: %d", *uploadID, objectParts))
+ completeReq := qs.CompleteMultipartUploadInput{
+ UploadID: uploadID,
+ ObjectParts: objectParts,
+ }
+ _, err = bucketInit.CompleteMultipartUpload(key, &completeReq)
+ if err != nil {
+ return err
+ }
+
+ // Read Metadata of object
+ err = o.readMetaData()
+ return err
+}
+
+// Remove this object
+func (o *Object) Remove() error {
+ bucketInit, err := o.fs.svc.Bucket(o.fs.bucket, o.fs.zone)
+ if err != nil {
+ return err
+ }
+
+ key := o.fs.root + o.remote
+ _, err = bucketInit.DeleteObject(key)
+ return err
+}
+
+// Fs returns read only access to the Fs that this object is part of
+func (o *Object) Fs() fs.Info {
+ return o.fs
+}
+
+// Hash returns the selected checksum of the file
+// If no checksum is available it returns ""
+func (o *Object) Hash(t fs.HashType) (string, error) {
+ var matchMd5 = regexp.MustCompile(`^[0-9a-f]{32}$`)
+ if t != fs.HashMD5 {
+ return "", fs.ErrHashUnsupported
+ }
+ etag := strings.Trim(strings.ToLower(o.etag), `"`)
+ // Check the etag is a valid md5sum
+ if !matchMd5.MatchString(etag) {
+ fs.Debugf(o, "Invalid md5sum (probably multipart uploaded) - ignoring: %q", etag)
+ return "", nil
+ }
+ return etag, nil
+}
+
+// Storable says whether this object can be stored
+func (o *Object) Storable() bool {
+ return true
+}
+
+// String returns a description of the Object
+func (o *Object) String() string {
+ if o == nil {
+ return ""
+ }
+ return o.remote
+}
+
+// Remote returns the remote path
+func (o *Object) Remote() string {
+ return o.remote
+}
+
+// Size returns the size of the file
+func (o *Object) Size() int64 {
+ return o.size
+}
+
+// MimeType of an Object if known, "" otherwise
+func (o *Object) MimeType() string {
+ err := o.readMetaData()
+ if err != nil {
+ fs.Logf(o, "Failed to read metadata: %v", err)
+ return ""
+ }
+ return o.mimeType
+}
+
+// Check the interfaces are satisfied
+var (
+ _ fs.Fs = &Fs{}
+ _ fs.Copier = &Fs{}
+ _ fs.Object = &Object{}
+ _ fs.ListRer = &Fs{}
+ _ fs.MimeTyper = &Object{}
+)
diff --git a/qingstor/qingstor_test.go b/qingstor/qingstor_test.go
new file mode 100644
index 000000000..d6ded0639
--- /dev/null
+++ b/qingstor/qingstor_test.go
@@ -0,0 +1,72 @@
+// Test QingStor filesystem interface
+//
+// Automatically generated - DO NOT EDIT
+// Regenerate with: make gen_tests
+package qingstor_test
+
+import (
+ "testing"
+
+ "github.com/ncw/rclone/fs"
+ "github.com/ncw/rclone/fstest/fstests"
+ "github.com/ncw/rclone/qingstor"
+)
+
+func TestSetup(t *testing.T) {
+ fstests.NilObject = fs.Object((*qingstor.Object)(nil))
+ fstests.RemoteName = "TestQingStor:"
+}
+
+// Generic tests for the Fs
+func TestInit(t *testing.T) { fstests.TestInit(t) }
+func TestFsString(t *testing.T) { fstests.TestFsString(t) }
+func TestFsName(t *testing.T) { fstests.TestFsName(t) }
+func TestFsRoot(t *testing.T) { fstests.TestFsRoot(t) }
+func TestFsRmdirEmpty(t *testing.T) { fstests.TestFsRmdirEmpty(t) }
+func TestFsRmdirNotFound(t *testing.T) { fstests.TestFsRmdirNotFound(t) }
+func TestFsMkdir(t *testing.T) { fstests.TestFsMkdir(t) }
+func TestFsMkdirRmdirSubdir(t *testing.T) { fstests.TestFsMkdirRmdirSubdir(t) }
+func TestFsListEmpty(t *testing.T) { fstests.TestFsListEmpty(t) }
+func TestFsListDirEmpty(t *testing.T) { fstests.TestFsListDirEmpty(t) }
+func TestFsListRDirEmpty(t *testing.T) { fstests.TestFsListRDirEmpty(t) }
+func TestFsNewObjectNotFound(t *testing.T) { fstests.TestFsNewObjectNotFound(t) }
+func TestFsPutFile1(t *testing.T) { fstests.TestFsPutFile1(t) }
+func TestFsPutError(t *testing.T) { fstests.TestFsPutError(t) }
+func TestFsPutFile2(t *testing.T) { fstests.TestFsPutFile2(t) }
+func TestFsUpdateFile1(t *testing.T) { fstests.TestFsUpdateFile1(t) }
+func TestFsListDirFile2(t *testing.T) { fstests.TestFsListDirFile2(t) }
+func TestFsListRDirFile2(t *testing.T) { fstests.TestFsListRDirFile2(t) }
+func TestFsListDirRoot(t *testing.T) { fstests.TestFsListDirRoot(t) }
+func TestFsListRDirRoot(t *testing.T) { fstests.TestFsListRDirRoot(t) }
+func TestFsListSubdir(t *testing.T) { fstests.TestFsListSubdir(t) }
+func TestFsListRSubdir(t *testing.T) { fstests.TestFsListRSubdir(t) }
+func TestFsListLevel2(t *testing.T) { fstests.TestFsListLevel2(t) }
+func TestFsListRLevel2(t *testing.T) { fstests.TestFsListRLevel2(t) }
+func TestFsListFile1(t *testing.T) { fstests.TestFsListFile1(t) }
+func TestFsNewObject(t *testing.T) { fstests.TestFsNewObject(t) }
+func TestFsListFile1and2(t *testing.T) { fstests.TestFsListFile1and2(t) }
+func TestFsNewObjectDir(t *testing.T) { fstests.TestFsNewObjectDir(t) }
+func TestFsCopy(t *testing.T) { fstests.TestFsCopy(t) }
+func TestFsMove(t *testing.T) { fstests.TestFsMove(t) }
+func TestFsDirMove(t *testing.T) { fstests.TestFsDirMove(t) }
+func TestFsRmdirFull(t *testing.T) { fstests.TestFsRmdirFull(t) }
+func TestFsPrecision(t *testing.T) { fstests.TestFsPrecision(t) }
+func TestFsDirChangeNotify(t *testing.T) { fstests.TestFsDirChangeNotify(t) }
+func TestObjectString(t *testing.T) { fstests.TestObjectString(t) }
+func TestObjectFs(t *testing.T) { fstests.TestObjectFs(t) }
+func TestObjectRemote(t *testing.T) { fstests.TestObjectRemote(t) }
+func TestObjectHashes(t *testing.T) { fstests.TestObjectHashes(t) }
+func TestObjectModTime(t *testing.T) { fstests.TestObjectModTime(t) }
+func TestObjectMimeType(t *testing.T) { fstests.TestObjectMimeType(t) }
+func TestObjectSetModTime(t *testing.T) { fstests.TestObjectSetModTime(t) }
+func TestObjectSize(t *testing.T) { fstests.TestObjectSize(t) }
+func TestObjectOpen(t *testing.T) { fstests.TestObjectOpen(t) }
+func TestObjectOpenSeek(t *testing.T) { fstests.TestObjectOpenSeek(t) }
+func TestObjectPartialRead(t *testing.T) { fstests.TestObjectPartialRead(t) }
+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 TestObjectRemove(t *testing.T) { fstests.TestObjectRemove(t) }
+func TestObjectPurge(t *testing.T) { fstests.TestObjectPurge(t) }
+func TestFinalise(t *testing.T) { fstests.TestFinalise(t) }