github.com/ncw/rclone@v1.48.1-0.20190724201158-a35aa1360e3e/backend/koofr/koofr.go (about) 1 package koofr 2 3 import ( 4 "context" 5 "encoding/base64" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "path" 11 "strings" 12 "time" 13 14 "github.com/ncw/rclone/fs" 15 "github.com/ncw/rclone/fs/config/configmap" 16 "github.com/ncw/rclone/fs/config/configstruct" 17 "github.com/ncw/rclone/fs/config/obscure" 18 "github.com/ncw/rclone/fs/hash" 19 20 httpclient "github.com/koofr/go-httpclient" 21 koofrclient "github.com/koofr/go-koofrclient" 22 ) 23 24 // Register Fs with rclone 25 func init() { 26 fs.Register(&fs.RegInfo{ 27 Name: "koofr", 28 Description: "Koofr", 29 NewFs: NewFs, 30 Options: []fs.Option{ 31 { 32 Name: "endpoint", 33 Help: "The Koofr API endpoint to use", 34 Default: "https://app.koofr.net", 35 Required: true, 36 Advanced: true, 37 }, { 38 Name: "mountid", 39 Help: "Mount ID of the mount to use. If omitted, the primary mount is used.", 40 Required: false, 41 Default: "", 42 Advanced: true, 43 }, { 44 Name: "setmtime", 45 Help: "Does the backend support setting modification time. Set this to false if you use a mount ID that points to a Dropbox or Amazon Drive backend.", 46 Default: true, 47 Required: true, 48 Advanced: true, 49 }, { 50 Name: "user", 51 Help: "Your Koofr user name", 52 Required: true, 53 }, { 54 Name: "password", 55 Help: "Your Koofr password for rclone (generate one at https://app.koofr.net/app/admin/preferences/password)", 56 IsPassword: true, 57 Required: true, 58 }, 59 }, 60 }) 61 } 62 63 // Options represent the configuration of the Koofr backend 64 type Options struct { 65 Endpoint string `config:"endpoint"` 66 MountID string `config:"mountid"` 67 User string `config:"user"` 68 Password string `config:"password"` 69 SetMTime bool `config:"setmtime"` 70 } 71 72 // A Fs is a representation of a remote Koofr Fs 73 type Fs struct { 74 name string 75 mountID string 76 root string 77 opt Options 78 features *fs.Features 79 client *koofrclient.KoofrClient 80 } 81 82 // An Object on the remote Koofr Fs 83 type Object struct { 84 fs *Fs 85 remote string 86 info koofrclient.FileInfo 87 } 88 89 func base(pth string) string { 90 rv := path.Base(pth) 91 if rv == "" || rv == "." { 92 rv = "/" 93 } 94 return rv 95 } 96 97 func dir(pth string) string { 98 rv := path.Dir(pth) 99 if rv == "" || rv == "." { 100 rv = "/" 101 } 102 return rv 103 } 104 105 // String returns a string representation of the remote Object 106 func (o *Object) String() string { 107 return o.remote 108 } 109 110 // Remote returns the remote path of the Object, relative to Fs root 111 func (o *Object) Remote() string { 112 return o.remote 113 } 114 115 // ModTime returns the modification time of the Object 116 func (o *Object) ModTime(ctx context.Context) time.Time { 117 return time.Unix(o.info.Modified/1000, (o.info.Modified%1000)*1000*1000) 118 } 119 120 // Size return the size of the Object in bytes 121 func (o *Object) Size() int64 { 122 return o.info.Size 123 } 124 125 // Fs returns a reference to the Koofr Fs containing the Object 126 func (o *Object) Fs() fs.Info { 127 return o.fs 128 } 129 130 // Hash returns an MD5 hash of the Object 131 func (o *Object) Hash(ctx context.Context, typ hash.Type) (string, error) { 132 if typ == hash.MD5 { 133 return o.info.Hash, nil 134 } 135 return "", nil 136 } 137 138 // fullPath returns full path of the remote Object (including Fs root) 139 func (o *Object) fullPath() string { 140 return o.fs.fullPath(o.remote) 141 } 142 143 // Storable returns true if the Object is storable 144 func (o *Object) Storable() bool { 145 return true 146 } 147 148 // SetModTime is not supported 149 func (o *Object) SetModTime(ctx context.Context, mtime time.Time) error { 150 return fs.ErrorCantSetModTimeWithoutDelete 151 } 152 153 // Open opens the Object for reading 154 func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadCloser, error) { 155 var sOff, eOff int64 = 0, -1 156 157 for _, option := range options { 158 switch x := option.(type) { 159 case *fs.SeekOption: 160 sOff = x.Offset 161 case *fs.RangeOption: 162 sOff = x.Start 163 eOff = x.End 164 default: 165 if option.Mandatory() { 166 fs.Logf(o, "Unsupported mandatory option: %v", option) 167 } 168 } 169 } 170 if sOff == 0 && eOff < 0 { 171 return o.fs.client.FilesGet(o.fs.mountID, o.fullPath()) 172 } 173 if sOff < 0 { 174 sOff = o.Size() - eOff 175 eOff = o.Size() 176 } 177 if eOff > o.Size() { 178 eOff = o.Size() 179 } 180 span := &koofrclient.FileSpan{ 181 Start: sOff, 182 End: eOff, 183 } 184 return o.fs.client.FilesGetRange(o.fs.mountID, o.fullPath(), span) 185 } 186 187 // Update updates the Object contents 188 func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error { 189 mtime := src.ModTime(ctx).UnixNano() / 1000 / 1000 190 putopts := &koofrclient.PutOptions{ 191 ForceOverwrite: true, 192 NoRename: true, 193 OverwriteIgnoreNonExisting: true, 194 SetModified: &mtime, 195 } 196 fullPath := o.fullPath() 197 dirPath := dir(fullPath) 198 name := base(fullPath) 199 err := o.fs.mkdir(dirPath) 200 if err != nil { 201 return err 202 } 203 info, err := o.fs.client.FilesPutWithOptions(o.fs.mountID, dirPath, name, in, putopts) 204 if err != nil { 205 return err 206 } 207 o.info = *info 208 return nil 209 } 210 211 // Remove deletes the remote Object 212 func (o *Object) Remove(ctx context.Context) error { 213 return o.fs.client.FilesDelete(o.fs.mountID, o.fullPath()) 214 } 215 216 // Name returns the name of the Fs 217 func (f *Fs) Name() string { 218 return f.name 219 } 220 221 // Root returns the root path of the Fs 222 func (f *Fs) Root() string { 223 return f.root 224 } 225 226 // String returns a string representation of the Fs 227 func (f *Fs) String() string { 228 return "koofr:" + f.mountID + ":" + f.root 229 } 230 231 // Features returns the optional features supported by this Fs 232 func (f *Fs) Features() *fs.Features { 233 return f.features 234 } 235 236 // Precision denotes that setting modification times is not supported 237 func (f *Fs) Precision() time.Duration { 238 if !f.opt.SetMTime { 239 return fs.ModTimeNotSupported 240 } 241 return time.Millisecond 242 } 243 244 // Hashes returns a set of hashes are Provided by the Fs 245 func (f *Fs) Hashes() hash.Set { 246 return hash.Set(hash.MD5) 247 } 248 249 // fullPath constructs a full, absolute path from a Fs root relative path, 250 func (f *Fs) fullPath(part string) string { 251 return path.Join("/", f.root, part) 252 } 253 254 // NewFs constructs a new filesystem given a root path and configuration options 255 func NewFs(name, root string, m configmap.Mapper) (ff fs.Fs, err error) { 256 opt := new(Options) 257 err = configstruct.Set(m, opt) 258 if err != nil { 259 return nil, err 260 } 261 pass, err := obscure.Reveal(opt.Password) 262 if err != nil { 263 return nil, err 264 } 265 client := koofrclient.NewKoofrClient(opt.Endpoint, false) 266 basicAuth := fmt.Sprintf("Basic %s", 267 base64.StdEncoding.EncodeToString([]byte(opt.User+":"+pass))) 268 client.HTTPClient.Headers.Set("Authorization", basicAuth) 269 mounts, err := client.Mounts() 270 if err != nil { 271 return nil, err 272 } 273 f := &Fs{ 274 name: name, 275 root: root, 276 opt: *opt, 277 client: client, 278 } 279 f.features = (&fs.Features{ 280 CaseInsensitive: true, 281 DuplicateFiles: false, 282 BucketBased: false, 283 CanHaveEmptyDirectories: true, 284 }).Fill(f) 285 for _, m := range mounts { 286 if opt.MountID != "" { 287 if m.Id == opt.MountID { 288 f.mountID = m.Id 289 break 290 } 291 } else if m.IsPrimary { 292 f.mountID = m.Id 293 break 294 } 295 } 296 if f.mountID == "" { 297 if opt.MountID == "" { 298 return nil, errors.New("Failed to find primary mount") 299 } 300 return nil, errors.New("Failed to find mount " + opt.MountID) 301 } 302 rootFile, err := f.client.FilesInfo(f.mountID, "/"+f.root) 303 if err == nil && rootFile.Type != "dir" { 304 f.root = dir(f.root) 305 err = fs.ErrorIsFile 306 } else { 307 err = nil 308 } 309 return f, err 310 } 311 312 // List returns a list of items in a directory 313 func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 314 files, err := f.client.FilesList(f.mountID, f.fullPath(dir)) 315 if err != nil { 316 return nil, translateErrorsDir(err) 317 } 318 entries = make([]fs.DirEntry, len(files)) 319 for i, file := range files { 320 if file.Type == "dir" { 321 entries[i] = fs.NewDir(path.Join(dir, file.Name), time.Unix(0, 0)) 322 } else { 323 entries[i] = &Object{ 324 fs: f, 325 info: file, 326 remote: path.Join(dir, file.Name), 327 } 328 } 329 } 330 return entries, nil 331 } 332 333 // NewObject creates a new remote Object for a given remote path 334 func (f *Fs) NewObject(ctx context.Context, remote string) (obj fs.Object, err error) { 335 info, err := f.client.FilesInfo(f.mountID, f.fullPath(remote)) 336 if err != nil { 337 return nil, translateErrorsObject(err) 338 } 339 if info.Type == "dir" { 340 return nil, fs.ErrorNotAFile 341 } 342 return &Object{ 343 fs: f, 344 info: info, 345 remote: remote, 346 }, nil 347 } 348 349 // Put updates a remote Object 350 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (obj fs.Object, err error) { 351 mtime := src.ModTime(ctx).UnixNano() / 1000 / 1000 352 putopts := &koofrclient.PutOptions{ 353 ForceOverwrite: true, 354 NoRename: true, 355 OverwriteIgnoreNonExisting: true, 356 SetModified: &mtime, 357 } 358 fullPath := f.fullPath(src.Remote()) 359 dirPath := dir(fullPath) 360 name := base(fullPath) 361 err = f.mkdir(dirPath) 362 if err != nil { 363 return nil, err 364 } 365 info, err := f.client.FilesPutWithOptions(f.mountID, dirPath, name, in, putopts) 366 if err != nil { 367 return nil, translateErrorsObject(err) 368 } 369 return &Object{ 370 fs: f, 371 info: *info, 372 remote: src.Remote(), 373 }, nil 374 } 375 376 // PutStream updates a remote Object with a stream of unknown size 377 func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 378 return f.Put(ctx, in, src, options...) 379 } 380 381 // isBadRequest is a predicate which holds true iff the error returned was 382 // HTTP status 400 383 func isBadRequest(err error) bool { 384 switch err := err.(type) { 385 case httpclient.InvalidStatusError: 386 if err.Got == http.StatusBadRequest { 387 return true 388 } 389 } 390 return false 391 } 392 393 // translateErrorsDir translates koofr errors to rclone errors (for a dir 394 // operation) 395 func translateErrorsDir(err error) error { 396 switch err := err.(type) { 397 case httpclient.InvalidStatusError: 398 if err.Got == http.StatusNotFound { 399 return fs.ErrorDirNotFound 400 } 401 } 402 return err 403 } 404 405 // translatesErrorsObject translates Koofr errors to rclone errors (for an object operation) 406 func translateErrorsObject(err error) error { 407 switch err := err.(type) { 408 case httpclient.InvalidStatusError: 409 if err.Got == http.StatusNotFound { 410 return fs.ErrorObjectNotFound 411 } 412 } 413 return err 414 } 415 416 // mkdir creates a directory at the given remote path. Creates ancestors if 417 // neccessary 418 func (f *Fs) mkdir(fullPath string) error { 419 if fullPath == "/" { 420 return nil 421 } 422 info, err := f.client.FilesInfo(f.mountID, fullPath) 423 if err == nil && info.Type == "dir" { 424 return nil 425 } 426 err = translateErrorsDir(err) 427 if err != nil && err != fs.ErrorDirNotFound { 428 return err 429 } 430 dirs := strings.Split(fullPath, "/") 431 parent := "/" 432 for _, part := range dirs { 433 if part == "" { 434 continue 435 } 436 info, err = f.client.FilesInfo(f.mountID, path.Join(parent, part)) 437 if err != nil || info.Type != "dir" { 438 err = translateErrorsDir(err) 439 if err != nil && err != fs.ErrorDirNotFound { 440 return err 441 } 442 err = f.client.FilesNewFolder(f.mountID, parent, part) 443 if err != nil && !isBadRequest(err) { 444 return err 445 } 446 } 447 parent = path.Join(parent, part) 448 } 449 return nil 450 } 451 452 // Mkdir creates a directory at the given remote path. Creates ancestors if 453 // necessary 454 func (f *Fs) Mkdir(ctx context.Context, dir string) error { 455 fullPath := f.fullPath(dir) 456 return f.mkdir(fullPath) 457 } 458 459 // Rmdir removes an (empty) directory at the given remote path 460 func (f *Fs) Rmdir(ctx context.Context, dir string) error { 461 files, err := f.client.FilesList(f.mountID, f.fullPath(dir)) 462 if err != nil { 463 return translateErrorsDir(err) 464 } 465 if len(files) > 0 { 466 return fs.ErrorDirectoryNotEmpty 467 } 468 err = f.client.FilesDelete(f.mountID, f.fullPath(dir)) 469 if err != nil { 470 return translateErrorsDir(err) 471 } 472 return nil 473 } 474 475 // Copy copies a remote Object to the given path 476 func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 477 dstFullPath := f.fullPath(remote) 478 dstDir := dir(dstFullPath) 479 err := f.mkdir(dstDir) 480 if err != nil { 481 return nil, fs.ErrorCantCopy 482 } 483 mtime := src.ModTime(ctx).UnixNano() / 1000 / 1000 484 err = f.client.FilesCopy((src.(*Object)).fs.mountID, 485 (src.(*Object)).fs.fullPath((src.(*Object)).remote), 486 f.mountID, dstFullPath, koofrclient.CopyOptions{SetModified: &mtime}) 487 if err != nil { 488 return nil, fs.ErrorCantCopy 489 } 490 return f.NewObject(ctx, remote) 491 } 492 493 // Move moves a remote Object to the given path 494 func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 495 srcObj := src.(*Object) 496 dstFullPath := f.fullPath(remote) 497 dstDir := dir(dstFullPath) 498 err := f.mkdir(dstDir) 499 if err != nil { 500 return nil, fs.ErrorCantMove 501 } 502 err = f.client.FilesMove(srcObj.fs.mountID, 503 srcObj.fs.fullPath(srcObj.remote), f.mountID, dstFullPath) 504 if err != nil { 505 return nil, fs.ErrorCantMove 506 } 507 return f.NewObject(ctx, remote) 508 } 509 510 // DirMove moves a remote directory to the given path 511 func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error { 512 srcFs := src.(*Fs) 513 srcFullPath := srcFs.fullPath(srcRemote) 514 dstFullPath := f.fullPath(dstRemote) 515 if srcFs.mountID == f.mountID && srcFullPath == dstFullPath { 516 return fs.ErrorDirExists 517 } 518 dstDir := dir(dstFullPath) 519 err := f.mkdir(dstDir) 520 if err != nil { 521 return fs.ErrorCantDirMove 522 } 523 err = f.client.FilesMove(srcFs.mountID, srcFullPath, f.mountID, dstFullPath) 524 if err != nil { 525 return fs.ErrorCantDirMove 526 } 527 return nil 528 } 529 530 // About reports space usage (with a MB precision) 531 func (f *Fs) About(ctx context.Context) (*fs.Usage, error) { 532 mount, err := f.client.MountsDetails(f.mountID) 533 if err != nil { 534 return nil, err 535 } 536 return &fs.Usage{ 537 Total: fs.NewUsageValue(mount.SpaceTotal * 1024 * 1024), 538 Used: fs.NewUsageValue(mount.SpaceUsed * 1024 * 1024), 539 Trashed: nil, 540 Other: nil, 541 Free: fs.NewUsageValue((mount.SpaceTotal - mount.SpaceUsed) * 1024 * 1024), 542 Objects: nil, 543 }, nil 544 } 545 546 // Purge purges the complete Fs 547 func (f *Fs) Purge(ctx context.Context) error { 548 err := translateErrorsDir(f.client.FilesDelete(f.mountID, f.fullPath(""))) 549 return err 550 } 551 552 // linkCreate is a Koofr API request for creating a public link 553 type linkCreate struct { 554 Path string `json:"path"` 555 } 556 557 // link is a Koofr API response to creating a public link 558 type link struct { 559 ID string `json:"id"` 560 Name string `json:"name"` 561 Path string `json:"path"` 562 Counter int64 `json:"counter"` 563 URL string `json:"url"` 564 ShortURL string `json:"shortUrl"` 565 Hash string `json:"hash"` 566 Host string `json:"host"` 567 HasPassword bool `json:"hasPassword"` 568 Password string `json:"password"` 569 ValidFrom int64 `json:"validFrom"` 570 ValidTo int64 `json:"validTo"` 571 PasswordRequired bool `json:"passwordRequired"` 572 } 573 574 // createLink makes a Koofr API call to create a public link 575 func createLink(c *koofrclient.KoofrClient, mountID string, path string) (*link, error) { 576 linkCreate := linkCreate{ 577 Path: path, 578 } 579 linkData := link{} 580 581 request := httpclient.RequestData{ 582 Method: "POST", 583 Path: "/api/v2/mounts/" + mountID + "/links", 584 ExpectedStatus: []int{http.StatusOK, http.StatusCreated}, 585 ReqEncoding: httpclient.EncodingJSON, 586 ReqValue: linkCreate, 587 RespEncoding: httpclient.EncodingJSON, 588 RespValue: &linkData, 589 } 590 591 _, err := c.Request(&request) 592 if err != nil { 593 return nil, err 594 } 595 return &linkData, nil 596 } 597 598 // PublicLink creates a public link to the remote path 599 func (f *Fs) PublicLink(ctx context.Context, remote string) (string, error) { 600 linkData, err := createLink(f.client, f.mountID, f.fullPath(remote)) 601 if err != nil { 602 return "", translateErrorsDir(err) 603 } 604 return linkData.ShortURL, nil 605 }