github.com/10XDev/rclone@v1.52.3-0.20200626220027-16af9ab76b2a/backend/swift/swift.go (about) 1 // Package swift provides an interface to the Swift object storage system 2 package swift 3 4 import ( 5 "bufio" 6 "bytes" 7 "context" 8 "fmt" 9 "io" 10 "net/url" 11 "path" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/ncw/swift" 17 "github.com/pkg/errors" 18 "github.com/rclone/rclone/fs" 19 "github.com/rclone/rclone/fs/config" 20 "github.com/rclone/rclone/fs/config/configmap" 21 "github.com/rclone/rclone/fs/config/configstruct" 22 "github.com/rclone/rclone/fs/fserrors" 23 "github.com/rclone/rclone/fs/fshttp" 24 "github.com/rclone/rclone/fs/hash" 25 "github.com/rclone/rclone/fs/operations" 26 "github.com/rclone/rclone/fs/walk" 27 "github.com/rclone/rclone/lib/bucket" 28 "github.com/rclone/rclone/lib/encoder" 29 "github.com/rclone/rclone/lib/pacer" 30 "github.com/rclone/rclone/lib/readers" 31 ) 32 33 // Constants 34 const ( 35 directoryMarkerContentType = "application/directory" // content type of directory marker objects 36 listChunks = 1000 // chunk size to read directory listings 37 defaultChunkSize = 5 * fs.GibiByte 38 minSleep = 10 * time.Millisecond // In case of error, start at 10ms sleep. 39 ) 40 41 // SharedOptions are shared between swift and hubic 42 var SharedOptions = []fs.Option{{ 43 Name: "chunk_size", 44 Help: `Above this size files will be chunked into a _segments container. 45 46 Above this size files will be chunked into a _segments container. The 47 default for this is 5GB which is its maximum value.`, 48 Default: defaultChunkSize, 49 Advanced: true, 50 }, { 51 Name: "no_chunk", 52 Help: `Don't chunk files during streaming upload. 53 54 When doing streaming uploads (eg using rcat or mount) setting this 55 flag will cause the swift backend to not upload chunked files. 56 57 This will limit the maximum upload size to 5GB. However non chunked 58 files are easier to deal with and have an MD5SUM. 59 60 Rclone will still chunk files bigger than chunk_size when doing normal 61 copy operations.`, 62 Default: false, 63 Advanced: true, 64 }, { 65 Name: config.ConfigEncoding, 66 Help: config.ConfigEncodingHelp, 67 Advanced: true, 68 Default: (encoder.EncodeInvalidUtf8 | 69 encoder.EncodeSlash), 70 }} 71 72 // Register with Fs 73 func init() { 74 fs.Register(&fs.RegInfo{ 75 Name: "swift", 76 Description: "OpenStack Swift (Rackspace Cloud Files, Memset Memstore, OVH)", 77 NewFs: NewFs, 78 Options: append([]fs.Option{{ 79 Name: "env_auth", 80 Help: "Get swift credentials from environment variables in standard OpenStack form.", 81 Default: false, 82 Examples: []fs.OptionExample{ 83 { 84 Value: "false", 85 Help: "Enter swift credentials in the next step", 86 }, { 87 Value: "true", 88 Help: "Get swift credentials from environment vars. Leave other fields blank if using this.", 89 }, 90 }, 91 }, { 92 Name: "user", 93 Help: "User name to log in (OS_USERNAME).", 94 }, { 95 Name: "key", 96 Help: "API key or password (OS_PASSWORD).", 97 }, { 98 Name: "auth", 99 Help: "Authentication URL for server (OS_AUTH_URL).", 100 Examples: []fs.OptionExample{{ 101 Help: "Rackspace US", 102 Value: "https://auth.api.rackspacecloud.com/v1.0", 103 }, { 104 Help: "Rackspace UK", 105 Value: "https://lon.auth.api.rackspacecloud.com/v1.0", 106 }, { 107 Help: "Rackspace v2", 108 Value: "https://identity.api.rackspacecloud.com/v2.0", 109 }, { 110 Help: "Memset Memstore UK", 111 Value: "https://auth.storage.memset.com/v1.0", 112 }, { 113 Help: "Memset Memstore UK v2", 114 Value: "https://auth.storage.memset.com/v2.0", 115 }, { 116 Help: "OVH", 117 Value: "https://auth.cloud.ovh.net/v3", 118 }}, 119 }, { 120 Name: "user_id", 121 Help: "User ID to log in - optional - most swift systems use user and leave this blank (v3 auth) (OS_USER_ID).", 122 }, { 123 Name: "domain", 124 Help: "User domain - optional (v3 auth) (OS_USER_DOMAIN_NAME)", 125 }, { 126 Name: "tenant", 127 Help: "Tenant name - optional for v1 auth, this or tenant_id required otherwise (OS_TENANT_NAME or OS_PROJECT_NAME)", 128 }, { 129 Name: "tenant_id", 130 Help: "Tenant ID - optional for v1 auth, this or tenant required otherwise (OS_TENANT_ID)", 131 }, { 132 Name: "tenant_domain", 133 Help: "Tenant domain - optional (v3 auth) (OS_PROJECT_DOMAIN_NAME)", 134 }, { 135 Name: "region", 136 Help: "Region name - optional (OS_REGION_NAME)", 137 }, { 138 Name: "storage_url", 139 Help: "Storage URL - optional (OS_STORAGE_URL)", 140 }, { 141 Name: "auth_token", 142 Help: "Auth Token from alternate authentication - optional (OS_AUTH_TOKEN)", 143 }, { 144 Name: "application_credential_id", 145 Help: "Application Credential ID (OS_APPLICATION_CREDENTIAL_ID)", 146 }, { 147 Name: "application_credential_name", 148 Help: "Application Credential Name (OS_APPLICATION_CREDENTIAL_NAME)", 149 }, { 150 Name: "application_credential_secret", 151 Help: "Application Credential Secret (OS_APPLICATION_CREDENTIAL_SECRET)", 152 }, { 153 Name: "auth_version", 154 Help: "AuthVersion - optional - set to (1,2,3) if your auth URL has no version (ST_AUTH_VERSION)", 155 Default: 0, 156 }, { 157 Name: "endpoint_type", 158 Help: "Endpoint type to choose from the service catalogue (OS_ENDPOINT_TYPE)", 159 Default: "public", 160 Examples: []fs.OptionExample{{ 161 Help: "Public (default, choose this if not sure)", 162 Value: "public", 163 }, { 164 Help: "Internal (use internal service net)", 165 Value: "internal", 166 }, { 167 Help: "Admin", 168 Value: "admin", 169 }}, 170 }, { 171 Name: "storage_policy", 172 Help: `The storage policy to use when creating a new container 173 174 This applies the specified storage policy when creating a new 175 container. The policy cannot be changed afterwards. The allowed 176 configuration values and their meaning depend on your Swift storage 177 provider.`, 178 Default: "", 179 Examples: []fs.OptionExample{{ 180 Help: "Default", 181 Value: "", 182 }, { 183 Help: "OVH Public Cloud Storage", 184 Value: "pcs", 185 }, { 186 Help: "OVH Public Cloud Archive", 187 Value: "pca", 188 }}, 189 }}, SharedOptions...), 190 }) 191 } 192 193 // Options defines the configuration for this backend 194 type Options struct { 195 EnvAuth bool `config:"env_auth"` 196 User string `config:"user"` 197 Key string `config:"key"` 198 Auth string `config:"auth"` 199 UserID string `config:"user_id"` 200 Domain string `config:"domain"` 201 Tenant string `config:"tenant"` 202 TenantID string `config:"tenant_id"` 203 TenantDomain string `config:"tenant_domain"` 204 Region string `config:"region"` 205 StorageURL string `config:"storage_url"` 206 AuthToken string `config:"auth_token"` 207 AuthVersion int `config:"auth_version"` 208 ApplicationCredentialID string `config:"application_credential_id"` 209 ApplicationCredentialName string `config:"application_credential_name"` 210 ApplicationCredentialSecret string `config:"application_credential_secret"` 211 StoragePolicy string `config:"storage_policy"` 212 EndpointType string `config:"endpoint_type"` 213 ChunkSize fs.SizeSuffix `config:"chunk_size"` 214 NoChunk bool `config:"no_chunk"` 215 Enc encoder.MultiEncoder `config:"encoding"` 216 } 217 218 // Fs represents a remote swift server 219 type Fs struct { 220 name string // name of this remote 221 root string // the path we are working on if any 222 features *fs.Features // optional features 223 opt Options // options for this backend 224 c *swift.Connection // the connection to the swift server 225 rootContainer string // container part of root (if any) 226 rootDirectory string // directory part of root (if any) 227 cache *bucket.Cache // cache of container status 228 noCheckContainer bool // don't check the container before creating it 229 pacer *fs.Pacer // To pace the API calls 230 } 231 232 // Object describes a swift object 233 // 234 // Will definitely have info but maybe not meta 235 type Object struct { 236 fs *Fs // what this object is part of 237 remote string // The remote path 238 size int64 239 lastModified time.Time 240 contentType string 241 md5 string 242 headers swift.Headers // The object headers if known 243 } 244 245 // ------------------------------------------------------------ 246 247 // Name of the remote (as passed into NewFs) 248 func (f *Fs) Name() string { 249 return f.name 250 } 251 252 // Root of the remote (as passed into NewFs) 253 func (f *Fs) Root() string { 254 return f.root 255 } 256 257 // String converts this Fs to a string 258 func (f *Fs) String() string { 259 if f.rootContainer == "" { 260 return fmt.Sprintf("Swift root") 261 } 262 if f.rootDirectory == "" { 263 return fmt.Sprintf("Swift container %s", f.rootContainer) 264 } 265 return fmt.Sprintf("Swift container %s path %s", f.rootContainer, f.rootDirectory) 266 } 267 268 // Features returns the optional features of this Fs 269 func (f *Fs) Features() *fs.Features { 270 return f.features 271 } 272 273 // retryErrorCodes is a slice of error codes that we will retry 274 var retryErrorCodes = []int{ 275 401, // Unauthorized (eg "Token has expired") 276 408, // Request Timeout 277 409, // Conflict - various states that could be resolved on a retry 278 429, // Rate exceeded. 279 500, // Get occasional 500 Internal Server Error 280 503, // Service Unavailable/Slow Down - "Reduce your request rate" 281 504, // Gateway Time-out 282 } 283 284 // shouldRetry returns a boolean as to whether this err deserves to be 285 // retried. It returns the err as a convenience 286 func shouldRetry(err error) (bool, error) { 287 // If this is a swift.Error object extract the HTTP error code 288 if swiftError, ok := err.(*swift.Error); ok { 289 for _, e := range retryErrorCodes { 290 if swiftError.StatusCode == e { 291 return true, err 292 } 293 } 294 } 295 // Check for generic failure conditions 296 return fserrors.ShouldRetry(err), err 297 } 298 299 // shouldRetryHeaders returns a boolean as to whether this err 300 // deserves to be retried. It reads the headers passed in looking for 301 // `Retry-After`. It returns the err as a convenience 302 func shouldRetryHeaders(headers swift.Headers, err error) (bool, error) { 303 if swiftError, ok := err.(*swift.Error); ok && swiftError.StatusCode == 429 { 304 if value := headers["Retry-After"]; value != "" { 305 retryAfter, parseErr := strconv.Atoi(value) 306 if parseErr != nil { 307 fs.Errorf(nil, "Failed to parse Retry-After: %q: %v", value, parseErr) 308 } else { 309 duration := time.Second * time.Duration(retryAfter) 310 if duration <= 60*time.Second { 311 // Do a short sleep immediately 312 fs.Debugf(nil, "Sleeping for %v to obey Retry-After", duration) 313 time.Sleep(duration) 314 return true, err 315 } 316 // Delay a long sleep for a retry 317 return false, fserrors.NewErrorRetryAfter(duration) 318 } 319 } 320 } 321 return shouldRetry(err) 322 } 323 324 // parsePath parses a remote 'url' 325 func parsePath(path string) (root string) { 326 root = strings.Trim(path, "/") 327 return 328 } 329 330 // split returns container and containerPath from the rootRelativePath 331 // relative to f.root 332 func (f *Fs) split(rootRelativePath string) (container, containerPath string) { 333 container, containerPath = bucket.Split(path.Join(f.root, rootRelativePath)) 334 return f.opt.Enc.FromStandardName(container), f.opt.Enc.FromStandardPath(containerPath) 335 } 336 337 // split returns container and containerPath from the object 338 func (o *Object) split() (container, containerPath string) { 339 return o.fs.split(o.remote) 340 } 341 342 // swiftConnection makes a connection to swift 343 func swiftConnection(opt *Options, name string) (*swift.Connection, error) { 344 c := &swift.Connection{ 345 // Keep these in the same order as the Config for ease of checking 346 UserName: opt.User, 347 ApiKey: opt.Key, 348 AuthUrl: opt.Auth, 349 UserId: opt.UserID, 350 Domain: opt.Domain, 351 Tenant: opt.Tenant, 352 TenantId: opt.TenantID, 353 TenantDomain: opt.TenantDomain, 354 Region: opt.Region, 355 StorageUrl: opt.StorageURL, 356 AuthToken: opt.AuthToken, 357 AuthVersion: opt.AuthVersion, 358 ApplicationCredentialId: opt.ApplicationCredentialID, 359 ApplicationCredentialName: opt.ApplicationCredentialName, 360 ApplicationCredentialSecret: opt.ApplicationCredentialSecret, 361 EndpointType: swift.EndpointType(opt.EndpointType), 362 ConnectTimeout: 10 * fs.Config.ConnectTimeout, // Use the timeouts in the transport 363 Timeout: 10 * fs.Config.Timeout, // Use the timeouts in the transport 364 Transport: fshttp.NewTransport(fs.Config), 365 } 366 if opt.EnvAuth { 367 err := c.ApplyEnvironment() 368 if err != nil { 369 return nil, errors.Wrap(err, "failed to read environment variables") 370 } 371 } 372 StorageUrl, AuthToken := c.StorageUrl, c.AuthToken // nolint 373 if !c.Authenticated() { 374 if (c.ApplicationCredentialId != "" || c.ApplicationCredentialName != "") && c.ApplicationCredentialSecret == "" { 375 if c.UserName == "" && c.UserId == "" { 376 return nil, errors.New("user name or user id not found for authentication (and no storage_url+auth_token is provided)") 377 } 378 if c.ApiKey == "" { 379 return nil, errors.New("key not found") 380 } 381 } 382 if c.AuthUrl == "" { 383 return nil, errors.New("auth not found") 384 } 385 err := c.Authenticate() // fills in c.StorageUrl and c.AuthToken 386 if err != nil { 387 return nil, err 388 } 389 } 390 // Make sure we re-auth with the AuthToken and StorageUrl 391 // provided by wrapping the existing auth, so we can just 392 // override one or the other or both. 393 if StorageUrl != "" || AuthToken != "" { 394 // Re-write StorageURL and AuthToken if they are being 395 // overridden as c.Authenticate above will have 396 // overwritten them. 397 if StorageUrl != "" { 398 c.StorageUrl = StorageUrl 399 } 400 if AuthToken != "" { 401 c.AuthToken = AuthToken 402 } 403 c.Auth = newAuth(c.Auth, StorageUrl, AuthToken) 404 } 405 return c, nil 406 } 407 408 func checkUploadChunkSize(cs fs.SizeSuffix) error { 409 const minChunkSize = fs.Byte 410 if cs < minChunkSize { 411 return errors.Errorf("%s is less than %s", cs, minChunkSize) 412 } 413 return nil 414 } 415 416 func (f *Fs) setUploadChunkSize(cs fs.SizeSuffix) (old fs.SizeSuffix, err error) { 417 err = checkUploadChunkSize(cs) 418 if err == nil { 419 old, f.opt.ChunkSize = f.opt.ChunkSize, cs 420 } 421 return 422 } 423 424 // setRoot changes the root of the Fs 425 func (f *Fs) setRoot(root string) { 426 f.root = parsePath(root) 427 f.rootContainer, f.rootDirectory = bucket.Split(f.root) 428 } 429 430 // NewFsWithConnection constructs an Fs from the path, container:path 431 // and authenticated connection. 432 // 433 // if noCheckContainer is set then the Fs won't check the container 434 // exists before creating it. 435 func NewFsWithConnection(opt *Options, name, root string, c *swift.Connection, noCheckContainer bool) (fs.Fs, error) { 436 f := &Fs{ 437 name: name, 438 opt: *opt, 439 c: c, 440 noCheckContainer: noCheckContainer, 441 pacer: fs.NewPacer(pacer.NewS3(pacer.MinSleep(minSleep))), 442 cache: bucket.NewCache(), 443 } 444 f.setRoot(root) 445 f.features = (&fs.Features{ 446 ReadMimeType: true, 447 WriteMimeType: true, 448 BucketBased: true, 449 BucketBasedRootOK: true, 450 }).Fill(f) 451 if f.rootContainer != "" && f.rootDirectory != "" { 452 // Check to see if the object exists - ignoring directory markers 453 var info swift.Object 454 var err error 455 encodedDirectory := f.opt.Enc.FromStandardPath(f.rootDirectory) 456 err = f.pacer.Call(func() (bool, error) { 457 var rxHeaders swift.Headers 458 info, rxHeaders, err = f.c.Object(f.rootContainer, encodedDirectory) 459 return shouldRetryHeaders(rxHeaders, err) 460 }) 461 if err == nil && info.ContentType != directoryMarkerContentType { 462 newRoot := path.Dir(f.root) 463 if newRoot == "." { 464 newRoot = "" 465 } 466 f.setRoot(newRoot) 467 // return an error with an fs which points to the parent 468 return f, fs.ErrorIsFile 469 } 470 } 471 return f, nil 472 } 473 474 // NewFs constructs an Fs from the path, container:path 475 func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) { 476 // Parse config into Options struct 477 opt := new(Options) 478 err := configstruct.Set(m, opt) 479 if err != nil { 480 return nil, err 481 } 482 err = checkUploadChunkSize(opt.ChunkSize) 483 if err != nil { 484 return nil, errors.Wrap(err, "swift: chunk size") 485 } 486 487 c, err := swiftConnection(opt, name) 488 if err != nil { 489 return nil, err 490 } 491 return NewFsWithConnection(opt, name, root, c, false) 492 } 493 494 // Return an Object from a path 495 // 496 // If it can't be found it returns the error fs.ErrorObjectNotFound. 497 func (f *Fs) newObjectWithInfo(remote string, info *swift.Object) (fs.Object, error) { 498 o := &Object{ 499 fs: f, 500 remote: remote, 501 } 502 // Note that due to a quirk of swift, dynamic large objects are 503 // returned as 0 bytes in the listing. Correct this here by 504 // making sure we read the full metadata for all 0 byte files. 505 // We don't read the metadata for directory marker objects. 506 if info != nil && info.Bytes == 0 && info.ContentType != "application/directory" { 507 info = nil 508 } 509 if info != nil { 510 // Set info but not headers 511 err := o.decodeMetaData(info) 512 if err != nil { 513 return nil, err 514 } 515 } else { 516 err := o.readMetaData() // reads info and headers, returning an error 517 if err != nil { 518 return nil, err 519 } 520 } 521 return o, nil 522 } 523 524 // NewObject finds the Object at remote. If it can't be found it 525 // returns the error fs.ErrorObjectNotFound. 526 func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { 527 return f.newObjectWithInfo(remote, nil) 528 } 529 530 // listFn is called from list and listContainerRoot to handle an object. 531 type listFn func(remote string, object *swift.Object, isDirectory bool) error 532 533 // listContainerRoot lists the objects into the function supplied from 534 // the container and directory supplied. The remote has prefix 535 // removed from it and if addContainer is set then it adds the 536 // container to the start. 537 // 538 // Set recurse to read sub directories 539 func (f *Fs) listContainerRoot(container, directory, prefix string, addContainer bool, recurse bool, fn listFn) error { 540 if prefix != "" && !strings.HasSuffix(prefix, "/") { 541 prefix += "/" 542 } 543 if directory != "" && !strings.HasSuffix(directory, "/") { 544 directory += "/" 545 } 546 // Options for ObjectsWalk 547 opts := swift.ObjectsOpts{ 548 Prefix: directory, 549 Limit: listChunks, 550 } 551 if !recurse { 552 opts.Delimiter = '/' 553 } 554 return f.c.ObjectsWalk(container, &opts, func(opts *swift.ObjectsOpts) (interface{}, error) { 555 var objects []swift.Object 556 var err error 557 err = f.pacer.Call(func() (bool, error) { 558 objects, err = f.c.Objects(container, opts) 559 return shouldRetry(err) 560 }) 561 if err == nil { 562 for i := range objects { 563 object := &objects[i] 564 isDirectory := false 565 if !recurse { 566 isDirectory = strings.HasSuffix(object.Name, "/") 567 } 568 remote := f.opt.Enc.ToStandardPath(object.Name) 569 if !strings.HasPrefix(remote, prefix) { 570 fs.Logf(f, "Odd name received %q", remote) 571 continue 572 } 573 if remote == prefix { 574 // If we have zero length directory markers ending in / then swift 575 // will return them in the listing for the directory which causes 576 // duplicate directories. Ignore them here. 577 continue 578 } 579 remote = remote[len(prefix):] 580 if addContainer { 581 remote = path.Join(container, remote) 582 } 583 err = fn(remote, object, isDirectory) 584 if err != nil { 585 break 586 } 587 } 588 } 589 return objects, err 590 }) 591 } 592 593 type addEntryFn func(fs.DirEntry) error 594 595 // list the objects into the function supplied 596 func (f *Fs) list(container, directory, prefix string, addContainer bool, recurse bool, fn addEntryFn) error { 597 err := f.listContainerRoot(container, directory, prefix, addContainer, recurse, func(remote string, object *swift.Object, isDirectory bool) (err error) { 598 if isDirectory { 599 remote = strings.TrimRight(remote, "/") 600 d := fs.NewDir(remote, time.Time{}).SetSize(object.Bytes) 601 err = fn(d) 602 } else { 603 // newObjectWithInfo does a full metadata read on 0 size objects which might be dynamic large objects 604 var o fs.Object 605 o, err = f.newObjectWithInfo(remote, object) 606 if err != nil { 607 return err 608 } 609 if o.Storable() { 610 err = fn(o) 611 } 612 } 613 return err 614 }) 615 if err == swift.ContainerNotFound { 616 err = fs.ErrorDirNotFound 617 } 618 return err 619 } 620 621 // listDir lists a single directory 622 func (f *Fs) listDir(container, directory, prefix string, addContainer bool) (entries fs.DirEntries, err error) { 623 if container == "" { 624 return nil, fs.ErrorListBucketRequired 625 } 626 // List the objects 627 err = f.list(container, directory, prefix, addContainer, false, func(entry fs.DirEntry) error { 628 entries = append(entries, entry) 629 return nil 630 }) 631 if err != nil { 632 return nil, err 633 } 634 // container must be present if listing succeeded 635 f.cache.MarkOK(container) 636 return entries, nil 637 } 638 639 // listContainers lists the containers 640 func (f *Fs) listContainers(ctx context.Context) (entries fs.DirEntries, err error) { 641 var containers []swift.Container 642 err = f.pacer.Call(func() (bool, error) { 643 containers, err = f.c.ContainersAll(nil) 644 return shouldRetry(err) 645 }) 646 if err != nil { 647 return nil, errors.Wrap(err, "container listing failed") 648 } 649 for _, container := range containers { 650 f.cache.MarkOK(container.Name) 651 d := fs.NewDir(f.opt.Enc.ToStandardName(container.Name), time.Time{}).SetSize(container.Bytes).SetItems(container.Count) 652 entries = append(entries, d) 653 } 654 return entries, nil 655 } 656 657 // List the objects and directories in dir into entries. The 658 // entries can be returned in any order but should be for a 659 // complete directory. 660 // 661 // dir should be "" to list the root, and should not have 662 // trailing slashes. 663 // 664 // This should return ErrDirNotFound if the directory isn't 665 // found. 666 func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 667 container, directory := f.split(dir) 668 if container == "" { 669 if directory != "" { 670 return nil, fs.ErrorListBucketRequired 671 } 672 return f.listContainers(ctx) 673 } 674 return f.listDir(container, directory, f.rootDirectory, f.rootContainer == "") 675 } 676 677 // ListR lists the objects and directories of the Fs starting 678 // from dir recursively into out. 679 // 680 // dir should be "" to start from the root, and should not 681 // have trailing slashes. 682 // 683 // This should return ErrDirNotFound if the directory isn't 684 // found. 685 // 686 // It should call callback for each tranche of entries read. 687 // These need not be returned in any particular order. If 688 // callback returns an error then the listing will stop 689 // immediately. 690 // 691 // Don't implement this unless you have a more efficient way 692 // of listing recursively than doing a directory traversal. 693 func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (err error) { 694 container, directory := f.split(dir) 695 list := walk.NewListRHelper(callback) 696 listR := func(container, directory, prefix string, addContainer bool) error { 697 return f.list(container, directory, prefix, addContainer, true, func(entry fs.DirEntry) error { 698 return list.Add(entry) 699 }) 700 } 701 if container == "" { 702 entries, err := f.listContainers(ctx) 703 if err != nil { 704 return err 705 } 706 for _, entry := range entries { 707 err = list.Add(entry) 708 if err != nil { 709 return err 710 } 711 container := entry.Remote() 712 err = listR(container, "", f.rootDirectory, true) 713 if err != nil { 714 return err 715 } 716 // container must be present if listing succeeded 717 f.cache.MarkOK(container) 718 } 719 } else { 720 err = listR(container, directory, f.rootDirectory, f.rootContainer == "") 721 if err != nil { 722 return err 723 } 724 // container must be present if listing succeeded 725 f.cache.MarkOK(container) 726 } 727 return list.Flush() 728 } 729 730 // About gets quota information 731 func (f *Fs) About(ctx context.Context) (*fs.Usage, error) { 732 var containers []swift.Container 733 var err error 734 err = f.pacer.Call(func() (bool, error) { 735 containers, err = f.c.ContainersAll(nil) 736 return shouldRetry(err) 737 }) 738 if err != nil { 739 return nil, errors.Wrap(err, "container listing failed") 740 } 741 var total, objects int64 742 for _, c := range containers { 743 total += c.Bytes 744 objects += c.Count 745 } 746 usage := &fs.Usage{ 747 Used: fs.NewUsageValue(total), // bytes in use 748 Objects: fs.NewUsageValue(objects), // objects in use 749 } 750 return usage, nil 751 } 752 753 // Put the object into the container 754 // 755 // Copy the reader in to the new object which is returned 756 // 757 // The new object may have been created if an error is returned 758 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 759 // Temporary Object under construction 760 fs := &Object{ 761 fs: f, 762 remote: src.Remote(), 763 headers: swift.Headers{}, // Empty object headers to stop readMetaData being called 764 } 765 return fs, fs.Update(ctx, in, src, options...) 766 } 767 768 // PutStream uploads to the remote path with the modTime given of indeterminate size 769 func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 770 return f.Put(ctx, in, src, options...) 771 } 772 773 // Mkdir creates the container if it doesn't exist 774 func (f *Fs) Mkdir(ctx context.Context, dir string) error { 775 container, _ := f.split(dir) 776 return f.makeContainer(ctx, container) 777 } 778 779 // makeContainer creates the container if it doesn't exist 780 func (f *Fs) makeContainer(ctx context.Context, container string) error { 781 return f.cache.Create(container, func() error { 782 // Check to see if container exists first 783 var err error = swift.ContainerNotFound 784 if !f.noCheckContainer { 785 err = f.pacer.Call(func() (bool, error) { 786 var rxHeaders swift.Headers 787 _, rxHeaders, err = f.c.Container(container) 788 return shouldRetryHeaders(rxHeaders, err) 789 }) 790 } 791 if err == swift.ContainerNotFound { 792 headers := swift.Headers{} 793 if f.opt.StoragePolicy != "" { 794 headers["X-Storage-Policy"] = f.opt.StoragePolicy 795 } 796 err = f.pacer.Call(func() (bool, error) { 797 err = f.c.ContainerCreate(container, headers) 798 return shouldRetry(err) 799 }) 800 if err == nil { 801 fs.Infof(f, "Container %q created", container) 802 } 803 } 804 return err 805 }, nil) 806 } 807 808 // Rmdir deletes the container if the fs is at the root 809 // 810 // Returns an error if it isn't empty 811 func (f *Fs) Rmdir(ctx context.Context, dir string) error { 812 container, directory := f.split(dir) 813 if container == "" || directory != "" { 814 return nil 815 } 816 err := f.cache.Remove(container, func() error { 817 err := f.pacer.Call(func() (bool, error) { 818 err := f.c.ContainerDelete(container) 819 return shouldRetry(err) 820 }) 821 if err == nil { 822 fs.Infof(f, "Container %q removed", container) 823 } 824 return err 825 }) 826 return err 827 } 828 829 // Precision of the remote 830 func (f *Fs) Precision() time.Duration { 831 return time.Nanosecond 832 } 833 834 // Purge deletes all the files and directories 835 // 836 // Implemented here so we can make sure we delete directory markers 837 func (f *Fs) Purge(ctx context.Context) error { 838 // Delete all the files including the directory markers 839 toBeDeleted := make(chan fs.Object, fs.Config.Transfers) 840 delErr := make(chan error, 1) 841 go func() { 842 delErr <- operations.DeleteFiles(ctx, toBeDeleted) 843 }() 844 err := f.list(f.rootContainer, f.rootDirectory, f.rootDirectory, f.rootContainer == "", true, func(entry fs.DirEntry) error { 845 if o, ok := entry.(*Object); ok { 846 toBeDeleted <- o 847 } 848 return nil 849 }) 850 close(toBeDeleted) 851 delError := <-delErr 852 if err == nil { 853 err = delError 854 } 855 if err != nil { 856 return err 857 } 858 return f.Rmdir(ctx, "") 859 } 860 861 // Copy src to this remote using server side copy operations. 862 // 863 // This is stored with the remote path given 864 // 865 // It returns the destination Object and a possible error 866 // 867 // Will only be called if src.Fs().Name() == f.Name() 868 // 869 // If it isn't possible then return fs.ErrorCantCopy 870 func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 871 dstContainer, dstPath := f.split(remote) 872 err := f.makeContainer(ctx, dstContainer) 873 if err != nil { 874 return nil, err 875 } 876 srcObj, ok := src.(*Object) 877 if !ok { 878 fs.Debugf(src, "Can't copy - not same remote type") 879 return nil, fs.ErrorCantCopy 880 } 881 srcContainer, srcPath := srcObj.split() 882 err = f.pacer.Call(func() (bool, error) { 883 var rxHeaders swift.Headers 884 rxHeaders, err = f.c.ObjectCopy(srcContainer, srcPath, dstContainer, dstPath, nil) 885 return shouldRetryHeaders(rxHeaders, err) 886 }) 887 if err != nil { 888 return nil, err 889 } 890 return f.NewObject(ctx, remote) 891 } 892 893 // Hashes returns the supported hash sets. 894 func (f *Fs) Hashes() hash.Set { 895 return hash.Set(hash.MD5) 896 } 897 898 // ------------------------------------------------------------ 899 900 // Fs returns the parent Fs 901 func (o *Object) Fs() fs.Info { 902 return o.fs 903 } 904 905 // Return a string version 906 func (o *Object) String() string { 907 if o == nil { 908 return "<nil>" 909 } 910 return o.remote 911 } 912 913 // Remote returns the remote path 914 func (o *Object) Remote() string { 915 return o.remote 916 } 917 918 // Hash returns the Md5sum of an object returning a lowercase hex string 919 func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) { 920 if t != hash.MD5 { 921 return "", hash.ErrUnsupported 922 } 923 isDynamicLargeObject, err := o.isDynamicLargeObject() 924 if err != nil { 925 return "", err 926 } 927 isStaticLargeObject, err := o.isStaticLargeObject() 928 if err != nil { 929 return "", err 930 } 931 if isDynamicLargeObject || isStaticLargeObject { 932 fs.Debugf(o, "Returning empty Md5sum for swift large object") 933 return "", nil 934 } 935 return strings.ToLower(o.md5), nil 936 } 937 938 // hasHeader checks for the header passed in returning false if the 939 // object isn't found. 940 func (o *Object) hasHeader(header string) (bool, error) { 941 err := o.readMetaData() 942 if err != nil { 943 if err == fs.ErrorObjectNotFound { 944 return false, nil 945 } 946 return false, err 947 } 948 _, isDynamicLargeObject := o.headers[header] 949 return isDynamicLargeObject, nil 950 } 951 952 // isDynamicLargeObject checks for X-Object-Manifest header 953 func (o *Object) isDynamicLargeObject() (bool, error) { 954 return o.hasHeader("X-Object-Manifest") 955 } 956 957 // isStaticLargeObjectFile checks for the X-Static-Large-Object header 958 func (o *Object) isStaticLargeObject() (bool, error) { 959 return o.hasHeader("X-Static-Large-Object") 960 } 961 962 func (o *Object) isInContainerVersioning(container string) (bool, error) { 963 _, headers, err := o.fs.c.Container(container) 964 if err != nil { 965 return false, err 966 } 967 xHistoryLocation := headers["X-History-Location"] 968 if len(xHistoryLocation) > 0 { 969 return true, nil 970 } 971 return false, nil 972 } 973 974 // Size returns the size of an object in bytes 975 func (o *Object) Size() int64 { 976 return o.size 977 } 978 979 // decodeMetaData sets the metadata in the object from a swift.Object 980 // 981 // Sets 982 // o.lastModified 983 // o.size 984 // o.md5 985 // o.contentType 986 func (o *Object) decodeMetaData(info *swift.Object) (err error) { 987 o.lastModified = info.LastModified 988 o.size = info.Bytes 989 o.md5 = info.Hash 990 o.contentType = info.ContentType 991 return nil 992 } 993 994 // readMetaData gets the metadata if it hasn't already been fetched 995 // 996 // it also sets the info 997 // 998 // it returns fs.ErrorObjectNotFound if the object isn't found 999 func (o *Object) readMetaData() (err error) { 1000 if o.headers != nil { 1001 return nil 1002 } 1003 var info swift.Object 1004 var h swift.Headers 1005 container, containerPath := o.split() 1006 err = o.fs.pacer.Call(func() (bool, error) { 1007 info, h, err = o.fs.c.Object(container, containerPath) 1008 return shouldRetryHeaders(h, err) 1009 }) 1010 if err != nil { 1011 if err == swift.ObjectNotFound { 1012 return fs.ErrorObjectNotFound 1013 } 1014 return err 1015 } 1016 o.headers = h 1017 err = o.decodeMetaData(&info) 1018 if err != nil { 1019 return err 1020 } 1021 return nil 1022 } 1023 1024 // ModTime returns the modification time of the object 1025 // 1026 // 1027 // It attempts to read the objects mtime and if that isn't present the 1028 // LastModified returned in the http headers 1029 func (o *Object) ModTime(ctx context.Context) time.Time { 1030 if fs.Config.UseServerModTime { 1031 return o.lastModified 1032 } 1033 err := o.readMetaData() 1034 if err != nil { 1035 fs.Debugf(o, "Failed to read metadata: %s", err) 1036 return o.lastModified 1037 } 1038 modTime, err := o.headers.ObjectMetadata().GetModTime() 1039 if err != nil { 1040 // fs.Logf(o, "Failed to read mtime from object: %v", err) 1041 return o.lastModified 1042 } 1043 return modTime 1044 } 1045 1046 // SetModTime sets the modification time of the local fs object 1047 func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error { 1048 err := o.readMetaData() 1049 if err != nil { 1050 return err 1051 } 1052 meta := o.headers.ObjectMetadata() 1053 meta.SetModTime(modTime) 1054 newHeaders := meta.ObjectHeaders() 1055 for k, v := range newHeaders { 1056 o.headers[k] = v 1057 } 1058 // Include any other metadata from request 1059 for k, v := range o.headers { 1060 if strings.HasPrefix(k, "X-Object-") { 1061 newHeaders[k] = v 1062 } 1063 } 1064 container, containerPath := o.split() 1065 return o.fs.pacer.Call(func() (bool, error) { 1066 err = o.fs.c.ObjectUpdate(container, containerPath, newHeaders) 1067 return shouldRetry(err) 1068 }) 1069 } 1070 1071 // Storable returns if this object is storable 1072 // 1073 // It compares the Content-Type to directoryMarkerContentType - that 1074 // makes it a directory marker which is not storable. 1075 func (o *Object) Storable() bool { 1076 return o.contentType != directoryMarkerContentType 1077 } 1078 1079 // Open an object for read 1080 func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) { 1081 fs.FixRangeOption(options, o.size) 1082 headers := fs.OpenOptionHeaders(options) 1083 _, isRanging := headers["Range"] 1084 container, containerPath := o.split() 1085 err = o.fs.pacer.Call(func() (bool, error) { 1086 var rxHeaders swift.Headers 1087 in, rxHeaders, err = o.fs.c.ObjectOpen(container, containerPath, !isRanging, headers) 1088 return shouldRetryHeaders(rxHeaders, err) 1089 }) 1090 return 1091 } 1092 1093 // min returns the smallest of x, y 1094 func min(x, y int64) int64 { 1095 if x < y { 1096 return x 1097 } 1098 return y 1099 } 1100 1101 // removeSegments removes any old segments from o 1102 // 1103 // if except is passed in then segments with that prefix won't be deleted 1104 func (o *Object) removeSegments(except string) error { 1105 segmentsContainer, prefix, err := o.getSegmentsDlo() 1106 err = o.fs.listContainerRoot(segmentsContainer, prefix, "", false, true, func(remote string, object *swift.Object, isDirectory bool) error { 1107 if isDirectory { 1108 return nil 1109 } 1110 if except != "" && strings.HasPrefix(remote, except) { 1111 // fs.Debugf(o, "Ignoring current segment file %q in container %q", segmentsRoot+remote, segmentsContainer) 1112 return nil 1113 } 1114 fs.Debugf(o, "Removing segment file %q in container %q", remote, segmentsContainer) 1115 var err error 1116 return o.fs.pacer.Call(func() (bool, error) { 1117 err = o.fs.c.ObjectDelete(segmentsContainer, remote) 1118 return shouldRetry(err) 1119 }) 1120 }) 1121 if err != nil { 1122 return err 1123 } 1124 // remove the segments container if empty, ignore errors 1125 err = o.fs.pacer.Call(func() (bool, error) { 1126 err = o.fs.c.ContainerDelete(segmentsContainer) 1127 if err == swift.ContainerNotFound || err == swift.ContainerNotEmpty { 1128 return false, err 1129 } 1130 return shouldRetry(err) 1131 }) 1132 if err == nil { 1133 fs.Debugf(o, "Removed empty container %q", segmentsContainer) 1134 } 1135 return nil 1136 } 1137 1138 func (o *Object) getSegmentsDlo() (segmentsContainer string, prefix string, err error) { 1139 if err = o.readMetaData(); err != nil { 1140 return 1141 } 1142 dirManifest := o.headers["X-Object-Manifest"] 1143 dirManifest, err = url.PathUnescape(dirManifest) 1144 if err != nil { 1145 return 1146 } 1147 delimiter := strings.Index(dirManifest, "/") 1148 if len(dirManifest) == 0 || delimiter < 0 { 1149 err = errors.New("Missing or wrong structure of manifest of Dynamic large object") 1150 return 1151 } 1152 return dirManifest[:delimiter], dirManifest[delimiter+1:], nil 1153 } 1154 1155 // urlEncode encodes a string so that it is a valid URL 1156 // 1157 // We don't use any of Go's standard methods as we need `/` not 1158 // encoded but we need '&' encoded. 1159 func urlEncode(str string) string { 1160 var buf bytes.Buffer 1161 for i := 0; i < len(str); i++ { 1162 c := str[i] 1163 if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '/' || c == '.' { 1164 _ = buf.WriteByte(c) 1165 } else { 1166 _, _ = buf.WriteString(fmt.Sprintf("%%%02X", c)) 1167 } 1168 } 1169 return buf.String() 1170 } 1171 1172 // updateChunks updates the existing object using chunks to a separate 1173 // container. It returns a string which prefixes current segments. 1174 func (o *Object) updateChunks(in0 io.Reader, headers swift.Headers, size int64, contentType string) (string, error) { 1175 container, containerPath := o.split() 1176 segmentsContainer := container + "_segments" 1177 // Create the segmentsContainer if it doesn't exist 1178 var err error 1179 err = o.fs.pacer.Call(func() (bool, error) { 1180 var rxHeaders swift.Headers 1181 _, rxHeaders, err = o.fs.c.Container(segmentsContainer) 1182 return shouldRetryHeaders(rxHeaders, err) 1183 }) 1184 if err == swift.ContainerNotFound { 1185 headers := swift.Headers{} 1186 if o.fs.opt.StoragePolicy != "" { 1187 headers["X-Storage-Policy"] = o.fs.opt.StoragePolicy 1188 } 1189 err = o.fs.pacer.Call(func() (bool, error) { 1190 err = o.fs.c.ContainerCreate(segmentsContainer, headers) 1191 return shouldRetry(err) 1192 }) 1193 } 1194 if err != nil { 1195 return "", err 1196 } 1197 // Upload the chunks 1198 left := size 1199 i := 0 1200 uniquePrefix := fmt.Sprintf("%s/%d", swift.TimeToFloatString(time.Now()), size) 1201 segmentsPath := path.Join(containerPath, uniquePrefix) 1202 in := bufio.NewReader(in0) 1203 segmentInfos := make([]string, 0, ((size / int64(o.fs.opt.ChunkSize)) + 1)) 1204 for { 1205 // can we read at least one byte? 1206 if _, err := in.Peek(1); err != nil { 1207 if left > 0 { 1208 return "", err // read less than expected 1209 } 1210 fs.Debugf(o, "Uploading segments into %q seems done (%v)", segmentsContainer, err) 1211 break 1212 } 1213 n := int64(o.fs.opt.ChunkSize) 1214 if size != -1 { 1215 n = min(left, n) 1216 headers["Content-Length"] = strconv.FormatInt(n, 10) // set Content-Length as we know it 1217 left -= n 1218 } 1219 segmentReader := io.LimitReader(in, n) 1220 segmentPath := fmt.Sprintf("%s/%08d", segmentsPath, i) 1221 fs.Debugf(o, "Uploading segment file %q into %q", segmentPath, segmentsContainer) 1222 err = o.fs.pacer.CallNoRetry(func() (bool, error) { 1223 var rxHeaders swift.Headers 1224 rxHeaders, err = o.fs.c.ObjectPut(segmentsContainer, segmentPath, segmentReader, true, "", "", headers) 1225 if err == nil { 1226 segmentInfos = append(segmentInfos, segmentPath) 1227 } 1228 return shouldRetryHeaders(rxHeaders, err) 1229 }) 1230 if err != nil { 1231 deleteChunks(o, segmentsContainer, segmentInfos) 1232 segmentInfos = nil 1233 return "", err 1234 } 1235 i++ 1236 } 1237 // Upload the manifest 1238 headers["X-Object-Manifest"] = urlEncode(fmt.Sprintf("%s/%s", segmentsContainer, segmentsPath)) 1239 headers["Content-Length"] = "0" // set Content-Length as we know it 1240 emptyReader := bytes.NewReader(nil) 1241 err = o.fs.pacer.Call(func() (bool, error) { 1242 var rxHeaders swift.Headers 1243 rxHeaders, err = o.fs.c.ObjectPut(container, containerPath, emptyReader, true, "", contentType, headers) 1244 return shouldRetryHeaders(rxHeaders, err) 1245 }) 1246 if err != nil { 1247 deleteChunks(o, segmentsContainer, segmentInfos) 1248 segmentInfos = nil 1249 } 1250 return uniquePrefix + "/", err 1251 } 1252 1253 func deleteChunks(o *Object, segmentsContainer string, segmentInfos []string) { 1254 if segmentInfos != nil && len(segmentInfos) > 0 { 1255 for _, v := range segmentInfos { 1256 fs.Debugf(o, "Delete segment file %q on %q", v, segmentsContainer) 1257 e := o.fs.c.ObjectDelete(segmentsContainer, v) 1258 if e != nil { 1259 fs.Errorf(o, "Error occurred in delete segment file %q on %q, error: %q", v, segmentsContainer, e) 1260 } 1261 } 1262 } 1263 } 1264 1265 // Update the object with the contents of the io.Reader, modTime and size 1266 // 1267 // The new object may have been created if an error is returned 1268 func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error { 1269 container, containerPath := o.split() 1270 if container == "" { 1271 return fserrors.FatalError(errors.New("can't upload files to the root")) 1272 } 1273 err := o.fs.makeContainer(ctx, container) 1274 if err != nil { 1275 return err 1276 } 1277 size := src.Size() 1278 modTime := src.ModTime(ctx) 1279 1280 // Note whether this is a dynamic large object before starting 1281 isDynamicLargeObject, err := o.isDynamicLargeObject() 1282 if err != nil { 1283 return err 1284 } 1285 1286 // Set the mtime 1287 m := swift.Metadata{} 1288 m.SetModTime(modTime) 1289 contentType := fs.MimeType(ctx, src) 1290 headers := m.ObjectHeaders() 1291 fs.OpenOptionAddHeaders(options, headers) 1292 uniquePrefix := "" 1293 if size > int64(o.fs.opt.ChunkSize) || (size == -1 && !o.fs.opt.NoChunk) { 1294 uniquePrefix, err = o.updateChunks(in, headers, size, contentType) 1295 if err != nil { 1296 return err 1297 } 1298 o.headers = nil // wipe old metadata 1299 } else { 1300 var inCount *readers.CountingReader 1301 if size >= 0 { 1302 headers["Content-Length"] = strconv.FormatInt(size, 10) // set Content-Length if we know it 1303 } else { 1304 // otherwise count the size for later 1305 inCount = readers.NewCountingReader(in) 1306 in = inCount 1307 } 1308 var rxHeaders swift.Headers 1309 err = o.fs.pacer.CallNoRetry(func() (bool, error) { 1310 rxHeaders, err = o.fs.c.ObjectPut(container, containerPath, in, true, "", contentType, headers) 1311 return shouldRetryHeaders(rxHeaders, err) 1312 }) 1313 if err != nil { 1314 return err 1315 } 1316 // set Metadata since ObjectPut checked the hash and length so we know the 1317 // object has been safely uploaded 1318 o.lastModified = modTime 1319 o.size = size 1320 o.md5 = rxHeaders["ETag"] 1321 o.contentType = contentType 1322 o.headers = headers 1323 if inCount != nil { 1324 // update the size if streaming from the reader 1325 o.size = int64(inCount.BytesRead()) 1326 } 1327 } 1328 1329 // If file was a dynamic large object then remove old/all segments 1330 if isDynamicLargeObject { 1331 err = o.removeSegments(uniquePrefix) 1332 if err != nil { 1333 fs.Logf(o, "Failed to remove old segments - carrying on with upload: %v", err) 1334 } 1335 } 1336 1337 // Read the metadata from the newly created object if necessary 1338 return o.readMetaData() 1339 } 1340 1341 // Remove an object 1342 func (o *Object) Remove(ctx context.Context) (err error) { 1343 container, containerPath := o.split() 1344 1345 // Remove file/manifest first 1346 err = o.fs.pacer.Call(func() (bool, error) { 1347 err = o.fs.c.ObjectDelete(container, containerPath) 1348 return shouldRetry(err) 1349 }) 1350 if err != nil { 1351 return err 1352 } 1353 isDynamicLargeObject, err := o.isDynamicLargeObject() 1354 if err != nil { 1355 return err 1356 } 1357 // ...then segments if required 1358 if isDynamicLargeObject { 1359 isInContainerVersioning, err := o.isInContainerVersioning(container) 1360 if err != nil { 1361 return err 1362 } 1363 if !isInContainerVersioning { 1364 err = o.removeSegments("") 1365 if err != nil { 1366 return err 1367 } 1368 } 1369 } 1370 return nil 1371 } 1372 1373 // MimeType of an Object if known, "" otherwise 1374 func (o *Object) MimeType(ctx context.Context) string { 1375 return o.contentType 1376 } 1377 1378 // Check the interfaces are satisfied 1379 var ( 1380 _ fs.Fs = &Fs{} 1381 _ fs.Purger = &Fs{} 1382 _ fs.PutStreamer = &Fs{} 1383 _ fs.Copier = &Fs{} 1384 _ fs.ListRer = &Fs{} 1385 _ fs.Object = &Object{} 1386 _ fs.MimeTyper = &Object{} 1387 )