github.com/xhghs/rclone@v1.51.1-0.20200430155106-e186a28cced8/backend/azureblob/azureblob.go (about) 1 // Package azureblob provides an interface to the Microsoft Azure blob object storage system 2 3 // +build !plan9,!solaris 4 5 package azureblob 6 7 import ( 8 "bytes" 9 "context" 10 "crypto/md5" 11 "encoding/base64" 12 "encoding/binary" 13 "encoding/hex" 14 "fmt" 15 "io" 16 "net/http" 17 "net/url" 18 "path" 19 "strconv" 20 "strings" 21 "sync" 22 "time" 23 24 "github.com/Azure/azure-pipeline-go/pipeline" 25 "github.com/Azure/azure-storage-blob-go/azblob" 26 "github.com/pkg/errors" 27 "github.com/rclone/rclone/fs" 28 "github.com/rclone/rclone/fs/accounting" 29 "github.com/rclone/rclone/fs/config" 30 "github.com/rclone/rclone/fs/config/configmap" 31 "github.com/rclone/rclone/fs/config/configstruct" 32 "github.com/rclone/rclone/fs/fserrors" 33 "github.com/rclone/rclone/fs/fshttp" 34 "github.com/rclone/rclone/fs/hash" 35 "github.com/rclone/rclone/fs/walk" 36 "github.com/rclone/rclone/lib/bucket" 37 "github.com/rclone/rclone/lib/encoder" 38 "github.com/rclone/rclone/lib/pacer" 39 ) 40 41 const ( 42 minSleep = 10 * time.Millisecond 43 maxSleep = 10 * time.Second 44 decayConstant = 1 // bigger for slower decay, exponential 45 maxListChunkSize = 5000 // number of items to read at once 46 modTimeKey = "mtime" 47 timeFormatIn = time.RFC3339 48 timeFormatOut = "2006-01-02T15:04:05.000000000Z07:00" 49 maxTotalParts = 50000 // in multipart upload 50 storageDefaultBaseURL = "blob.core.windows.net" 51 // maxUncommittedSize = 9 << 30 // can't upload bigger than this 52 defaultChunkSize = 4 * fs.MebiByte 53 maxChunkSize = 100 * fs.MebiByte 54 defaultUploadCutoff = 256 * fs.MebiByte 55 maxUploadCutoff = 256 * fs.MebiByte 56 defaultAccessTier = azblob.AccessTierNone 57 maxTryTimeout = time.Hour * 24 * 365 //max time of an azure web request response window (whether or not data is flowing) 58 // Default storage account, key and blob endpoint for emulator support, 59 // though it is a base64 key checked in here, it is publicly available secret. 60 emulatorAccount = "devstoreaccount1" 61 emulatorAccountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" 62 emulatorBlobEndpoint = "http://127.0.0.1:10000/devstoreaccount1" 63 ) 64 65 // Register with Fs 66 func init() { 67 fs.Register(&fs.RegInfo{ 68 Name: "azureblob", 69 Description: "Microsoft Azure Blob Storage", 70 NewFs: NewFs, 71 Options: []fs.Option{{ 72 Name: "account", 73 Help: "Storage Account Name (leave blank to use SAS URL or Emulator)", 74 }, { 75 Name: "key", 76 Help: "Storage Account Key (leave blank to use SAS URL or Emulator)", 77 }, { 78 Name: "sas_url", 79 Help: "SAS URL for container level access only\n(leave blank if using account/key or Emulator)", 80 }, { 81 Name: "use_emulator", 82 Help: "Uses local storage emulator if provided as 'true' (leave blank if using real azure storage endpoint)", 83 Default: false, 84 }, { 85 Name: "endpoint", 86 Help: "Endpoint for the service\nLeave blank normally.", 87 Advanced: true, 88 }, { 89 Name: "upload_cutoff", 90 Help: "Cutoff for switching to chunked upload (<= 256MB).", 91 Default: defaultUploadCutoff, 92 Advanced: true, 93 }, { 94 Name: "chunk_size", 95 Help: `Upload chunk size (<= 100MB). 96 97 Note that this is stored in memory and there may be up to 98 "--transfers" chunks stored at once in memory.`, 99 Default: defaultChunkSize, 100 Advanced: true, 101 }, { 102 Name: "list_chunk", 103 Help: `Size of blob list. 104 105 This sets the number of blobs requested in each listing chunk. Default 106 is the maximum, 5000. "List blobs" requests are permitted 2 minutes 107 per megabyte to complete. If an operation is taking longer than 2 108 minutes per megabyte on average, it will time out ( 109 [source](https://docs.microsoft.com/en-us/rest/api/storageservices/setting-timeouts-for-blob-service-operations#exceptions-to-default-timeout-interval) 110 ). This can be used to limit the number of blobs items to return, to 111 avoid the time out.`, 112 Default: maxListChunkSize, 113 Advanced: true, 114 }, { 115 Name: "access_tier", 116 Help: `Access tier of blob: hot, cool or archive. 117 118 Archived blobs can be restored by setting access tier to hot or 119 cool. Leave blank if you intend to use default access tier, which is 120 set at account level 121 122 If there is no "access tier" specified, rclone doesn't apply any tier. 123 rclone performs "Set Tier" operation on blobs while uploading, if objects 124 are not modified, specifying "access tier" to new one will have no effect. 125 If blobs are in "archive tier" at remote, trying to perform data transfer 126 operations from remote will not be allowed. User should first restore by 127 tiering blob to "Hot" or "Cool".`, 128 Advanced: true, 129 }, { 130 Name: config.ConfigEncoding, 131 Help: config.ConfigEncodingHelp, 132 Advanced: true, 133 Default: (encoder.EncodeInvalidUtf8 | 134 encoder.EncodeSlash | 135 encoder.EncodeCtl | 136 encoder.EncodeDel | 137 encoder.EncodeBackSlash | 138 encoder.EncodeRightPeriod), 139 }}, 140 }) 141 } 142 143 // Options defines the configuration for this backend 144 type Options struct { 145 Account string `config:"account"` 146 Key string `config:"key"` 147 Endpoint string `config:"endpoint"` 148 SASURL string `config:"sas_url"` 149 UploadCutoff fs.SizeSuffix `config:"upload_cutoff"` 150 ChunkSize fs.SizeSuffix `config:"chunk_size"` 151 ListChunkSize uint `config:"list_chunk"` 152 AccessTier string `config:"access_tier"` 153 UseEmulator bool `config:"use_emulator"` 154 Enc encoder.MultiEncoder `config:"encoding"` 155 } 156 157 // Fs represents a remote azure server 158 type Fs struct { 159 name string // name of this remote 160 root string // the path we are working on if any 161 opt Options // parsed config options 162 features *fs.Features // optional features 163 client *http.Client // http client we are using 164 svcURL *azblob.ServiceURL // reference to serviceURL 165 cntURLcacheMu sync.Mutex // mutex to protect cntURLcache 166 cntURLcache map[string]*azblob.ContainerURL // reference to containerURL per container 167 rootContainer string // container part of root (if any) 168 rootDirectory string // directory part of root (if any) 169 isLimited bool // if limited to one container 170 cache *bucket.Cache // cache for container creation status 171 pacer *fs.Pacer // To pace and retry the API calls 172 uploadToken *pacer.TokenDispenser // control concurrency 173 } 174 175 // Object describes a azure object 176 type Object struct { 177 fs *Fs // what this object is part of 178 remote string // The remote path 179 modTime time.Time // The modified time of the object if known 180 md5 string // MD5 hash if known 181 size int64 // Size of the object 182 mimeType string // Content-Type of the object 183 accessTier azblob.AccessTierType // Blob Access Tier 184 meta map[string]string // blob metadata 185 } 186 187 // ------------------------------------------------------------ 188 189 // Name of the remote (as passed into NewFs) 190 func (f *Fs) Name() string { 191 return f.name 192 } 193 194 // Root of the remote (as passed into NewFs) 195 func (f *Fs) Root() string { 196 return f.root 197 } 198 199 // String converts this Fs to a string 200 func (f *Fs) String() string { 201 if f.rootContainer == "" { 202 return fmt.Sprintf("Azure root") 203 } 204 if f.rootDirectory == "" { 205 return fmt.Sprintf("Azure container %s", f.rootContainer) 206 } 207 return fmt.Sprintf("Azure container %s path %s", f.rootContainer, f.rootDirectory) 208 } 209 210 // Features returns the optional features of this Fs 211 func (f *Fs) Features() *fs.Features { 212 return f.features 213 } 214 215 // parsePath parses a remote 'url' 216 func parsePath(path string) (root string) { 217 root = strings.Trim(path, "/") 218 return 219 } 220 221 // split returns container and containerPath from the rootRelativePath 222 // relative to f.root 223 func (f *Fs) split(rootRelativePath string) (containerName, containerPath string) { 224 containerName, containerPath = bucket.Split(path.Join(f.root, rootRelativePath)) 225 return f.opt.Enc.FromStandardName(containerName), f.opt.Enc.FromStandardPath(containerPath) 226 } 227 228 // split returns container and containerPath from the object 229 func (o *Object) split() (container, containerPath string) { 230 return o.fs.split(o.remote) 231 } 232 233 // validateAccessTier checks if azureblob supports user supplied tier 234 func validateAccessTier(tier string) bool { 235 switch tier { 236 case string(azblob.AccessTierHot), 237 string(azblob.AccessTierCool), 238 string(azblob.AccessTierArchive): 239 // valid cases 240 return true 241 default: 242 return false 243 } 244 } 245 246 // retryErrorCodes is a slice of error codes that we will retry 247 var retryErrorCodes = []int{ 248 401, // Unauthorized (eg "Token has expired") 249 408, // Request Timeout 250 429, // Rate exceeded. 251 500, // Get occasional 500 Internal Server Error 252 503, // Service Unavailable 253 504, // Gateway Time-out 254 } 255 256 // shouldRetry returns a boolean as to whether this resp and err 257 // deserve to be retried. It returns the err as a convenience 258 func (f *Fs) shouldRetry(err error) (bool, error) { 259 // FIXME interpret special errors - more to do here 260 if storageErr, ok := err.(azblob.StorageError); ok { 261 statusCode := storageErr.Response().StatusCode 262 for _, e := range retryErrorCodes { 263 if statusCode == e { 264 return true, err 265 } 266 } 267 } 268 return fserrors.ShouldRetry(err), err 269 } 270 271 func checkUploadChunkSize(cs fs.SizeSuffix) error { 272 const minChunkSize = fs.Byte 273 if cs < minChunkSize { 274 return errors.Errorf("%s is less than %s", cs, minChunkSize) 275 } 276 if cs > maxChunkSize { 277 return errors.Errorf("%s is greater than %s", cs, maxChunkSize) 278 } 279 return nil 280 } 281 282 func (f *Fs) setUploadChunkSize(cs fs.SizeSuffix) (old fs.SizeSuffix, err error) { 283 err = checkUploadChunkSize(cs) 284 if err == nil { 285 old, f.opt.ChunkSize = f.opt.ChunkSize, cs 286 } 287 return 288 } 289 290 func checkUploadCutoff(cs fs.SizeSuffix) error { 291 if cs > maxUploadCutoff { 292 return errors.Errorf("%v must be less than or equal to %v", cs, maxUploadCutoff) 293 } 294 return nil 295 } 296 297 func (f *Fs) setUploadCutoff(cs fs.SizeSuffix) (old fs.SizeSuffix, err error) { 298 err = checkUploadCutoff(cs) 299 if err == nil { 300 old, f.opt.UploadCutoff = f.opt.UploadCutoff, cs 301 } 302 return 303 } 304 305 // httpClientFactory creates a Factory object that sends HTTP requests 306 // to a rclone's http.Client. 307 // 308 // copied from azblob.newDefaultHTTPClientFactory 309 func httpClientFactory(client *http.Client) pipeline.Factory { 310 return pipeline.FactoryFunc(func(next pipeline.Policy, po *pipeline.PolicyOptions) pipeline.PolicyFunc { 311 return func(ctx context.Context, request pipeline.Request) (pipeline.Response, error) { 312 r, err := client.Do(request.WithContext(ctx)) 313 if err != nil { 314 err = pipeline.NewError(err, "HTTP request failed") 315 } 316 return pipeline.NewHTTPResponse(r), err 317 } 318 }) 319 } 320 321 // newPipeline creates a Pipeline using the specified credentials and options. 322 // 323 // this code was copied from azblob.NewPipeline 324 func (f *Fs) newPipeline(c azblob.Credential, o azblob.PipelineOptions) pipeline.Pipeline { 325 // Don't log stuff to syslog/Windows Event log 326 pipeline.SetForceLogEnabled(false) 327 328 // Closest to API goes first; closest to the wire goes last 329 factories := []pipeline.Factory{ 330 azblob.NewTelemetryPolicyFactory(o.Telemetry), 331 azblob.NewUniqueRequestIDPolicyFactory(), 332 azblob.NewRetryPolicyFactory(o.Retry), 333 c, 334 pipeline.MethodFactoryMarker(), // indicates at what stage in the pipeline the method factory is invoked 335 azblob.NewRequestLogPolicyFactory(o.RequestLog), 336 } 337 return pipeline.NewPipeline(factories, pipeline.Options{HTTPSender: httpClientFactory(f.client), Log: o.Log}) 338 } 339 340 // setRoot changes the root of the Fs 341 func (f *Fs) setRoot(root string) { 342 f.root = parsePath(root) 343 f.rootContainer, f.rootDirectory = bucket.Split(f.root) 344 } 345 346 // NewFs constructs an Fs from the path, container:path 347 func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) { 348 ctx := context.Background() 349 // Parse config into Options struct 350 opt := new(Options) 351 err := configstruct.Set(m, opt) 352 if err != nil { 353 return nil, err 354 } 355 356 err = checkUploadCutoff(opt.UploadCutoff) 357 if err != nil { 358 return nil, errors.Wrap(err, "azure: upload cutoff") 359 } 360 err = checkUploadChunkSize(opt.ChunkSize) 361 if err != nil { 362 return nil, errors.Wrap(err, "azure: chunk size") 363 } 364 if opt.ListChunkSize > maxListChunkSize { 365 return nil, errors.Errorf("azure: blob list size can't be greater than %v - was %v", maxListChunkSize, opt.ListChunkSize) 366 } 367 if opt.Endpoint == "" { 368 opt.Endpoint = storageDefaultBaseURL 369 } 370 371 if opt.AccessTier == "" { 372 opt.AccessTier = string(defaultAccessTier) 373 } else if !validateAccessTier(opt.AccessTier) { 374 return nil, errors.Errorf("Azure Blob: Supported access tiers are %s, %s and %s", 375 string(azblob.AccessTierHot), string(azblob.AccessTierCool), string(azblob.AccessTierArchive)) 376 } 377 378 f := &Fs{ 379 name: name, 380 opt: *opt, 381 pacer: fs.NewPacer(pacer.NewS3(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))), 382 uploadToken: pacer.NewTokenDispenser(fs.Config.Transfers), 383 client: fshttp.NewClient(fs.Config), 384 cache: bucket.NewCache(), 385 cntURLcache: make(map[string]*azblob.ContainerURL, 1), 386 } 387 f.setRoot(root) 388 f.features = (&fs.Features{ 389 ReadMimeType: true, 390 WriteMimeType: true, 391 BucketBased: true, 392 BucketBasedRootOK: true, 393 SetTier: true, 394 GetTier: true, 395 }).Fill(f) 396 397 var ( 398 u *url.URL 399 serviceURL azblob.ServiceURL 400 ) 401 switch { 402 case opt.UseEmulator: 403 credential, err := azblob.NewSharedKeyCredential(emulatorAccount, emulatorAccountKey) 404 if err != nil { 405 return nil, errors.Wrapf(err, "Failed to parse credentials") 406 } 407 u, err = url.Parse(emulatorBlobEndpoint) 408 if err != nil { 409 return nil, errors.Wrap(err, "failed to make azure storage url from account and endpoint") 410 } 411 pipeline := f.newPipeline(credential, azblob.PipelineOptions{Retry: azblob.RetryOptions{TryTimeout: maxTryTimeout}}) 412 serviceURL = azblob.NewServiceURL(*u, pipeline) 413 case opt.Account != "" && opt.Key != "": 414 credential, err := azblob.NewSharedKeyCredential(opt.Account, opt.Key) 415 if err != nil { 416 return nil, errors.Wrapf(err, "Failed to parse credentials") 417 } 418 419 u, err = url.Parse(fmt.Sprintf("https://%s.%s", opt.Account, opt.Endpoint)) 420 if err != nil { 421 return nil, errors.Wrap(err, "failed to make azure storage url from account and endpoint") 422 } 423 pipeline := f.newPipeline(credential, azblob.PipelineOptions{Retry: azblob.RetryOptions{TryTimeout: maxTryTimeout}}) 424 serviceURL = azblob.NewServiceURL(*u, pipeline) 425 case opt.SASURL != "": 426 u, err = url.Parse(opt.SASURL) 427 if err != nil { 428 return nil, errors.Wrapf(err, "failed to parse SAS URL") 429 } 430 // use anonymous credentials in case of sas url 431 pipeline := f.newPipeline(azblob.NewAnonymousCredential(), azblob.PipelineOptions{Retry: azblob.RetryOptions{TryTimeout: maxTryTimeout}}) 432 // Check if we have container level SAS or account level sas 433 parts := azblob.NewBlobURLParts(*u) 434 if parts.ContainerName != "" { 435 if f.rootContainer != "" && parts.ContainerName != f.rootContainer { 436 return nil, errors.New("Container name in SAS URL and container provided in command do not match") 437 } 438 containerURL := azblob.NewContainerURL(*u, pipeline) 439 f.cntURLcache[parts.ContainerName] = &containerURL 440 f.isLimited = true 441 } else { 442 serviceURL = azblob.NewServiceURL(*u, pipeline) 443 } 444 default: 445 return nil, errors.New("Need account+key or connectionString or sasURL") 446 } 447 f.svcURL = &serviceURL 448 449 if f.rootContainer != "" && f.rootDirectory != "" { 450 // Check to see if the (container,directory) is actually an existing file 451 oldRoot := f.root 452 newRoot, leaf := path.Split(oldRoot) 453 f.setRoot(newRoot) 454 _, err := f.NewObject(ctx, leaf) 455 if err != nil { 456 if err == fs.ErrorObjectNotFound || err == fs.ErrorNotAFile { 457 // File doesn't exist or is a directory so return old f 458 f.setRoot(oldRoot) 459 return f, nil 460 } 461 return nil, err 462 } 463 // return an error with an fs which points to the parent 464 return f, fs.ErrorIsFile 465 } 466 return f, nil 467 } 468 469 // return the container URL for the container passed in 470 func (f *Fs) cntURL(container string) (containerURL *azblob.ContainerURL) { 471 f.cntURLcacheMu.Lock() 472 defer f.cntURLcacheMu.Unlock() 473 var ok bool 474 if containerURL, ok = f.cntURLcache[container]; !ok { 475 cntURL := f.svcURL.NewContainerURL(container) 476 containerURL = &cntURL 477 f.cntURLcache[container] = containerURL 478 } 479 return containerURL 480 481 } 482 483 // Return an Object from a path 484 // 485 // If it can't be found it returns the error fs.ErrorObjectNotFound. 486 func (f *Fs) newObjectWithInfo(remote string, info *azblob.BlobItem) (fs.Object, error) { 487 o := &Object{ 488 fs: f, 489 remote: remote, 490 } 491 if info != nil { 492 err := o.decodeMetaDataFromBlob(info) 493 if err != nil { 494 return nil, err 495 } 496 } else { 497 err := o.readMetaData() // reads info and headers, returning an error 498 if err != nil { 499 return nil, err 500 } 501 } 502 return o, nil 503 } 504 505 // NewObject finds the Object at remote. If it can't be found 506 // it returns the error fs.ErrorObjectNotFound. 507 func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { 508 return f.newObjectWithInfo(remote, nil) 509 } 510 511 // getBlobReference creates an empty blob reference with no metadata 512 func (f *Fs) getBlobReference(container, containerPath string) azblob.BlobURL { 513 return f.cntURL(container).NewBlobURL(containerPath) 514 } 515 516 // updateMetadataWithModTime adds the modTime passed in to o.meta. 517 func (o *Object) updateMetadataWithModTime(modTime time.Time) { 518 // Make sure o.meta is not nil 519 if o.meta == nil { 520 o.meta = make(map[string]string, 1) 521 } 522 523 // Set modTimeKey in it 524 o.meta[modTimeKey] = modTime.Format(timeFormatOut) 525 } 526 527 // Returns whether file is a directory marker or not 528 func isDirectoryMarker(size int64, metadata azblob.Metadata, remote string) bool { 529 // Directory markers are 0 length 530 if size == 0 { 531 // Note that metadata with hdi_isfolder = true seems to be a 532 // defacto standard for marking blobs as directories. 533 endsWithSlash := strings.HasSuffix(remote, "/") 534 if endsWithSlash || remote == "" || metadata["hdi_isfolder"] == "true" { 535 return true 536 } 537 538 } 539 return false 540 } 541 542 // listFn is called from list to handle an object 543 type listFn func(remote string, object *azblob.BlobItem, isDirectory bool) error 544 545 // list lists the objects into the function supplied from 546 // the container and root supplied 547 // 548 // dir is the starting directory, "" for root 549 // 550 // The remote has prefix removed from it and if addContainer is set then 551 // it adds the container to the start. 552 func (f *Fs) list(ctx context.Context, container, directory, prefix string, addContainer bool, recurse bool, maxResults uint, fn listFn) error { 553 if f.cache.IsDeleted(container) { 554 return fs.ErrorDirNotFound 555 } 556 if prefix != "" { 557 prefix += "/" 558 } 559 if directory != "" { 560 directory += "/" 561 } 562 delimiter := "" 563 if !recurse { 564 delimiter = "/" 565 } 566 567 options := azblob.ListBlobsSegmentOptions{ 568 Details: azblob.BlobListingDetails{ 569 Copy: false, 570 Metadata: true, 571 Snapshots: false, 572 UncommittedBlobs: false, 573 Deleted: false, 574 }, 575 Prefix: directory, 576 MaxResults: int32(maxResults), 577 } 578 for marker := (azblob.Marker{}); marker.NotDone(); { 579 var response *azblob.ListBlobsHierarchySegmentResponse 580 err := f.pacer.Call(func() (bool, error) { 581 var err error 582 response, err = f.cntURL(container).ListBlobsHierarchySegment(ctx, marker, delimiter, options) 583 return f.shouldRetry(err) 584 }) 585 586 if err != nil { 587 // Check http error code along with service code, current SDK doesn't populate service code correctly sometimes 588 if storageErr, ok := err.(azblob.StorageError); ok && (storageErr.ServiceCode() == azblob.ServiceCodeContainerNotFound || storageErr.Response().StatusCode == http.StatusNotFound) { 589 return fs.ErrorDirNotFound 590 } 591 return err 592 } 593 // Advance marker to next 594 marker = response.NextMarker 595 for i := range response.Segment.BlobItems { 596 file := &response.Segment.BlobItems[i] 597 // Finish if file name no longer has prefix 598 // if prefix != "" && !strings.HasPrefix(file.Name, prefix) { 599 // return nil 600 // } 601 remote := f.opt.Enc.ToStandardPath(file.Name) 602 if !strings.HasPrefix(remote, prefix) { 603 fs.Debugf(f, "Odd name received %q", remote) 604 continue 605 } 606 remote = remote[len(prefix):] 607 if isDirectoryMarker(*file.Properties.ContentLength, file.Metadata, remote) { 608 continue // skip directory marker 609 } 610 if addContainer { 611 remote = path.Join(container, remote) 612 } 613 // Send object 614 err = fn(remote, file, false) 615 if err != nil { 616 return err 617 } 618 } 619 // Send the subdirectories 620 for _, remote := range response.Segment.BlobPrefixes { 621 remote := strings.TrimRight(remote.Name, "/") 622 remote = f.opt.Enc.ToStandardPath(remote) 623 if !strings.HasPrefix(remote, prefix) { 624 fs.Debugf(f, "Odd directory name received %q", remote) 625 continue 626 } 627 remote = remote[len(prefix):] 628 if addContainer { 629 remote = path.Join(container, remote) 630 } 631 // Send object 632 err = fn(remote, nil, true) 633 if err != nil { 634 return err 635 } 636 } 637 } 638 return nil 639 } 640 641 // Convert a list item into a DirEntry 642 func (f *Fs) itemToDirEntry(remote string, object *azblob.BlobItem, isDirectory bool) (fs.DirEntry, error) { 643 if isDirectory { 644 d := fs.NewDir(remote, time.Time{}) 645 return d, nil 646 } 647 o, err := f.newObjectWithInfo(remote, object) 648 if err != nil { 649 return nil, err 650 } 651 return o, nil 652 } 653 654 // listDir lists a single directory 655 func (f *Fs) listDir(ctx context.Context, container, directory, prefix string, addContainer bool) (entries fs.DirEntries, err error) { 656 err = f.list(ctx, container, directory, prefix, addContainer, false, f.opt.ListChunkSize, func(remote string, object *azblob.BlobItem, isDirectory bool) error { 657 entry, err := f.itemToDirEntry(remote, object, isDirectory) 658 if err != nil { 659 return err 660 } 661 if entry != nil { 662 entries = append(entries, entry) 663 } 664 return nil 665 }) 666 if err != nil { 667 return nil, err 668 } 669 // container must be present if listing succeeded 670 f.cache.MarkOK(container) 671 return entries, nil 672 } 673 674 // listContainers returns all the containers to out 675 func (f *Fs) listContainers(ctx context.Context) (entries fs.DirEntries, err error) { 676 if f.isLimited { 677 f.cntURLcacheMu.Lock() 678 for container := range f.cntURLcache { 679 d := fs.NewDir(container, time.Time{}) 680 entries = append(entries, d) 681 } 682 f.cntURLcacheMu.Unlock() 683 return entries, nil 684 } 685 err = f.listContainersToFn(func(container *azblob.ContainerItem) error { 686 d := fs.NewDir(f.opt.Enc.ToStandardName(container.Name), container.Properties.LastModified) 687 f.cache.MarkOK(container.Name) 688 entries = append(entries, d) 689 return nil 690 }) 691 if err != nil { 692 return nil, err 693 } 694 return entries, nil 695 } 696 697 // List the objects and directories in dir into entries. The 698 // entries can be returned in any order but should be for a 699 // complete directory. 700 // 701 // dir should be "" to list the root, and should not have 702 // trailing slashes. 703 // 704 // This should return ErrDirNotFound if the directory isn't 705 // found. 706 func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 707 container, directory := f.split(dir) 708 if container == "" { 709 if directory != "" { 710 return nil, fs.ErrorListBucketRequired 711 } 712 return f.listContainers(ctx) 713 } 714 return f.listDir(ctx, container, directory, f.rootDirectory, f.rootContainer == "") 715 } 716 717 // ListR lists the objects and directories of the Fs starting 718 // from dir recursively into out. 719 // 720 // dir should be "" to start from the root, and should not 721 // have trailing slashes. 722 // 723 // This should return ErrDirNotFound if the directory isn't 724 // found. 725 // 726 // It should call callback for each tranche of entries read. 727 // These need not be returned in any particular order. If 728 // callback returns an error then the listing will stop 729 // immediately. 730 // 731 // Don't implement this unless you have a more efficient way 732 // of listing recursively that doing a directory traversal. 733 func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (err error) { 734 container, directory := f.split(dir) 735 list := walk.NewListRHelper(callback) 736 listR := func(container, directory, prefix string, addContainer bool) error { 737 return f.list(ctx, container, directory, prefix, addContainer, true, f.opt.ListChunkSize, func(remote string, object *azblob.BlobItem, isDirectory bool) error { 738 entry, err := f.itemToDirEntry(remote, object, isDirectory) 739 if err != nil { 740 return err 741 } 742 return list.Add(entry) 743 }) 744 } 745 if container == "" { 746 entries, err := f.listContainers(ctx) 747 if err != nil { 748 return err 749 } 750 for _, entry := range entries { 751 err = list.Add(entry) 752 if err != nil { 753 return err 754 } 755 container := entry.Remote() 756 err = listR(container, "", f.rootDirectory, true) 757 if err != nil { 758 return err 759 } 760 // container must be present if listing succeeded 761 f.cache.MarkOK(container) 762 } 763 } else { 764 err = listR(container, directory, f.rootDirectory, f.rootContainer == "") 765 if err != nil { 766 return err 767 } 768 // container must be present if listing succeeded 769 f.cache.MarkOK(container) 770 } 771 return list.Flush() 772 } 773 774 // listContainerFn is called from listContainersToFn to handle a container 775 type listContainerFn func(*azblob.ContainerItem) error 776 777 // listContainersToFn lists the containers to the function supplied 778 func (f *Fs) listContainersToFn(fn listContainerFn) error { 779 params := azblob.ListContainersSegmentOptions{ 780 MaxResults: int32(f.opt.ListChunkSize), 781 } 782 ctx := context.Background() 783 for marker := (azblob.Marker{}); marker.NotDone(); { 784 var response *azblob.ListContainersSegmentResponse 785 err := f.pacer.Call(func() (bool, error) { 786 var err error 787 response, err = f.svcURL.ListContainersSegment(ctx, marker, params) 788 return f.shouldRetry(err) 789 }) 790 if err != nil { 791 return err 792 } 793 794 for i := range response.ContainerItems { 795 err = fn(&response.ContainerItems[i]) 796 if err != nil { 797 return err 798 } 799 } 800 marker = response.NextMarker 801 } 802 803 return nil 804 } 805 806 // Put the object into the container 807 // 808 // Copy the reader in to the new object which is returned 809 // 810 // The new object may have been created if an error is returned 811 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 812 // Temporary Object under construction 813 fs := &Object{ 814 fs: f, 815 remote: src.Remote(), 816 } 817 return fs, fs.Update(ctx, in, src, options...) 818 } 819 820 // Mkdir creates the container if it doesn't exist 821 func (f *Fs) Mkdir(ctx context.Context, dir string) error { 822 container, _ := f.split(dir) 823 return f.makeContainer(ctx, container) 824 } 825 826 // makeContainer creates the container if it doesn't exist 827 func (f *Fs) makeContainer(ctx context.Context, container string) error { 828 return f.cache.Create(container, func() error { 829 // now try to create the container 830 return f.pacer.Call(func() (bool, error) { 831 _, err := f.cntURL(container).Create(ctx, azblob.Metadata{}, azblob.PublicAccessNone) 832 if err != nil { 833 if storageErr, ok := err.(azblob.StorageError); ok { 834 switch storageErr.ServiceCode() { 835 case azblob.ServiceCodeContainerAlreadyExists: 836 return false, nil 837 case azblob.ServiceCodeContainerBeingDeleted: 838 // From https://docs.microsoft.com/en-us/rest/api/storageservices/delete-container 839 // When a container is deleted, a container with the same name cannot be created 840 // for at least 30 seconds; the container may not be available for more than 30 841 // seconds if the service is still processing the request. 842 time.Sleep(6 * time.Second) // default 10 retries will be 60 seconds 843 f.cache.MarkDeleted(container) 844 return true, err 845 } 846 } 847 } 848 return f.shouldRetry(err) 849 }) 850 }, nil) 851 } 852 853 // isEmpty checks to see if a given (container, directory) is empty and returns an error if not 854 func (f *Fs) isEmpty(ctx context.Context, container, directory string) (err error) { 855 empty := true 856 err = f.list(ctx, container, directory, f.rootDirectory, f.rootContainer == "", true, 1, func(remote string, object *azblob.BlobItem, isDirectory bool) error { 857 empty = false 858 return nil 859 }) 860 if err != nil { 861 return err 862 } 863 if !empty { 864 return fs.ErrorDirectoryNotEmpty 865 } 866 return nil 867 } 868 869 // deleteContainer deletes the container. It can delete a full 870 // container so use isEmpty if you don't want that. 871 func (f *Fs) deleteContainer(ctx context.Context, container string) error { 872 return f.cache.Remove(container, func() error { 873 options := azblob.ContainerAccessConditions{} 874 return f.pacer.Call(func() (bool, error) { 875 _, err := f.cntURL(container).GetProperties(ctx, azblob.LeaseAccessConditions{}) 876 if err == nil { 877 _, err = f.cntURL(container).Delete(ctx, options) 878 } 879 880 if err != nil { 881 // Check http error code along with service code, current SDK doesn't populate service code correctly sometimes 882 if storageErr, ok := err.(azblob.StorageError); ok && (storageErr.ServiceCode() == azblob.ServiceCodeContainerNotFound || storageErr.Response().StatusCode == http.StatusNotFound) { 883 return false, fs.ErrorDirNotFound 884 } 885 886 return f.shouldRetry(err) 887 } 888 889 return f.shouldRetry(err) 890 }) 891 }) 892 } 893 894 // Rmdir deletes the container if the fs is at the root 895 // 896 // Returns an error if it isn't empty 897 func (f *Fs) Rmdir(ctx context.Context, dir string) error { 898 container, directory := f.split(dir) 899 if container == "" || directory != "" { 900 return nil 901 } 902 err := f.isEmpty(ctx, container, directory) 903 if err != nil { 904 return err 905 } 906 return f.deleteContainer(ctx, container) 907 } 908 909 // Precision of the remote 910 func (f *Fs) Precision() time.Duration { 911 return time.Nanosecond 912 } 913 914 // Hashes returns the supported hash sets. 915 func (f *Fs) Hashes() hash.Set { 916 return hash.Set(hash.MD5) 917 } 918 919 // Purge deletes all the files and directories including the old versions. 920 func (f *Fs) Purge(ctx context.Context) error { 921 dir := "" // forward compat! 922 container, directory := f.split(dir) 923 if container == "" || directory != "" { 924 // Delegate to caller if not root of a container 925 return fs.ErrorCantPurge 926 } 927 return f.deleteContainer(ctx, container) 928 } 929 930 // Copy src to this remote using server side copy operations. 931 // 932 // This is stored with the remote path given 933 // 934 // It returns the destination Object and a possible error 935 // 936 // Will only be called if src.Fs().Name() == f.Name() 937 // 938 // If it isn't possible then return fs.ErrorCantCopy 939 func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 940 dstContainer, dstPath := f.split(remote) 941 err := f.makeContainer(ctx, dstContainer) 942 if err != nil { 943 return nil, err 944 } 945 srcObj, ok := src.(*Object) 946 if !ok { 947 fs.Debugf(src, "Can't copy - not same remote type") 948 return nil, fs.ErrorCantCopy 949 } 950 dstBlobURL := f.getBlobReference(dstContainer, dstPath) 951 srcBlobURL := srcObj.getBlobReference() 952 953 source, err := url.Parse(srcBlobURL.String()) 954 if err != nil { 955 return nil, err 956 } 957 958 options := azblob.BlobAccessConditions{} 959 var startCopy *azblob.BlobStartCopyFromURLResponse 960 961 err = f.pacer.Call(func() (bool, error) { 962 startCopy, err = dstBlobURL.StartCopyFromURL(ctx, *source, nil, azblob.ModifiedAccessConditions{}, options) 963 return f.shouldRetry(err) 964 }) 965 if err != nil { 966 return nil, err 967 } 968 969 copyStatus := startCopy.CopyStatus() 970 for copyStatus == azblob.CopyStatusPending { 971 time.Sleep(1 * time.Second) 972 getMetadata, err := dstBlobURL.GetProperties(ctx, options) 973 if err != nil { 974 return nil, err 975 } 976 copyStatus = getMetadata.CopyStatus() 977 } 978 979 return f.NewObject(ctx, remote) 980 } 981 982 // ------------------------------------------------------------ 983 984 // Fs returns the parent Fs 985 func (o *Object) Fs() fs.Info { 986 return o.fs 987 } 988 989 // Return a string version 990 func (o *Object) String() string { 991 if o == nil { 992 return "<nil>" 993 } 994 return o.remote 995 } 996 997 // Remote returns the remote path 998 func (o *Object) Remote() string { 999 return o.remote 1000 } 1001 1002 // Hash returns the MD5 of an object returning a lowercase hex string 1003 func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) { 1004 if t != hash.MD5 { 1005 return "", hash.ErrUnsupported 1006 } 1007 // Convert base64 encoded md5 into lower case hex 1008 if o.md5 == "" { 1009 return "", nil 1010 } 1011 data, err := base64.StdEncoding.DecodeString(o.md5) 1012 if err != nil { 1013 return "", errors.Wrapf(err, "Failed to decode Content-MD5: %q", o.md5) 1014 } 1015 return hex.EncodeToString(data), nil 1016 } 1017 1018 // Size returns the size of an object in bytes 1019 func (o *Object) Size() int64 { 1020 return o.size 1021 } 1022 1023 func (o *Object) setMetadata(metadata azblob.Metadata) { 1024 if len(metadata) > 0 { 1025 o.meta = metadata 1026 if modTime, ok := metadata[modTimeKey]; ok { 1027 when, err := time.Parse(timeFormatIn, modTime) 1028 if err != nil { 1029 fs.Debugf(o, "Couldn't parse %v = %q: %v", modTimeKey, modTime, err) 1030 } 1031 o.modTime = when 1032 } 1033 } else { 1034 o.meta = nil 1035 } 1036 } 1037 1038 // decodeMetaDataFromPropertiesResponse sets the metadata from the data passed in 1039 // 1040 // Sets 1041 // o.id 1042 // o.modTime 1043 // o.size 1044 // o.md5 1045 // o.meta 1046 func (o *Object) decodeMetaDataFromPropertiesResponse(info *azblob.BlobGetPropertiesResponse) (err error) { 1047 metadata := info.NewMetadata() 1048 size := info.ContentLength() 1049 if isDirectoryMarker(size, metadata, o.remote) { 1050 return fs.ErrorNotAFile 1051 } 1052 // NOTE - Client library always returns MD5 as base64 decoded string, Object needs to maintain 1053 // this as base64 encoded string. 1054 o.md5 = base64.StdEncoding.EncodeToString(info.ContentMD5()) 1055 o.mimeType = info.ContentType() 1056 o.size = size 1057 o.modTime = info.LastModified() 1058 o.accessTier = azblob.AccessTierType(info.AccessTier()) 1059 o.setMetadata(metadata) 1060 1061 return nil 1062 } 1063 1064 func (o *Object) decodeMetaDataFromBlob(info *azblob.BlobItem) (err error) { 1065 metadata := info.Metadata 1066 size := *info.Properties.ContentLength 1067 if isDirectoryMarker(size, metadata, o.remote) { 1068 return fs.ErrorNotAFile 1069 } 1070 // NOTE - Client library always returns MD5 as base64 decoded string, Object needs to maintain 1071 // this as base64 encoded string. 1072 o.md5 = base64.StdEncoding.EncodeToString(info.Properties.ContentMD5) 1073 o.mimeType = *info.Properties.ContentType 1074 o.size = size 1075 o.modTime = info.Properties.LastModified 1076 o.accessTier = info.Properties.AccessTier 1077 o.setMetadata(metadata) 1078 return nil 1079 } 1080 1081 // getBlobReference creates an empty blob reference with no metadata 1082 func (o *Object) getBlobReference() azblob.BlobURL { 1083 container, directory := o.split() 1084 return o.fs.getBlobReference(container, directory) 1085 } 1086 1087 // clearMetaData clears enough metadata so readMetaData will re-read it 1088 func (o *Object) clearMetaData() { 1089 o.modTime = time.Time{} 1090 } 1091 1092 // readMetaData gets the metadata if it hasn't already been fetched 1093 // 1094 // Sets 1095 // o.id 1096 // o.modTime 1097 // o.size 1098 // o.md5 1099 func (o *Object) readMetaData() (err error) { 1100 if !o.modTime.IsZero() { 1101 return nil 1102 } 1103 blob := o.getBlobReference() 1104 1105 // Read metadata (this includes metadata) 1106 options := azblob.BlobAccessConditions{} 1107 ctx := context.Background() 1108 var blobProperties *azblob.BlobGetPropertiesResponse 1109 err = o.fs.pacer.Call(func() (bool, error) { 1110 blobProperties, err = blob.GetProperties(ctx, options) 1111 return o.fs.shouldRetry(err) 1112 }) 1113 if err != nil { 1114 // On directories - GetProperties does not work and current SDK does not populate service code correctly hence check regular http response as well 1115 if storageErr, ok := err.(azblob.StorageError); ok && (storageErr.ServiceCode() == azblob.ServiceCodeBlobNotFound || storageErr.Response().StatusCode == http.StatusNotFound) { 1116 return fs.ErrorObjectNotFound 1117 } 1118 return err 1119 } 1120 1121 return o.decodeMetaDataFromPropertiesResponse(blobProperties) 1122 } 1123 1124 // parseTimeString converts a decimal string number of milliseconds 1125 // elapsed since January 1, 1970 UTC into a time.Time and stores it in 1126 // the modTime variable. 1127 func (o *Object) parseTimeString(timeString string) (err error) { 1128 if timeString == "" { 1129 return nil 1130 } 1131 unixMilliseconds, err := strconv.ParseInt(timeString, 10, 64) 1132 if err != nil { 1133 fs.Debugf(o, "Failed to parse mod time string %q: %v", timeString, err) 1134 return err 1135 } 1136 o.modTime = time.Unix(unixMilliseconds/1e3, (unixMilliseconds%1e3)*1e6).UTC() 1137 return nil 1138 } 1139 1140 // ModTime returns the modification time of the object 1141 // 1142 // It attempts to read the objects mtime and if that isn't present the 1143 // LastModified returned in the http headers 1144 func (o *Object) ModTime(ctx context.Context) (result time.Time) { 1145 // The error is logged in readMetaData 1146 _ = o.readMetaData() 1147 return o.modTime 1148 } 1149 1150 // SetModTime sets the modification time of the local fs object 1151 func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error { 1152 // Make sure o.meta is not nil 1153 if o.meta == nil { 1154 o.meta = make(map[string]string, 1) 1155 } 1156 // Set modTimeKey in it 1157 o.meta[modTimeKey] = modTime.Format(timeFormatOut) 1158 1159 blob := o.getBlobReference() 1160 err := o.fs.pacer.Call(func() (bool, error) { 1161 _, err := blob.SetMetadata(ctx, o.meta, azblob.BlobAccessConditions{}) 1162 return o.fs.shouldRetry(err) 1163 }) 1164 if err != nil { 1165 return err 1166 } 1167 o.modTime = modTime 1168 return nil 1169 } 1170 1171 // Storable returns if this object is storable 1172 func (o *Object) Storable() bool { 1173 return true 1174 } 1175 1176 // Open an object for read 1177 func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) { 1178 // Offset and Count for range download 1179 var offset int64 1180 var count int64 1181 if o.AccessTier() == azblob.AccessTierArchive { 1182 return nil, errors.Errorf("Blob in archive tier, you need to set tier to hot or cool first") 1183 } 1184 fs.FixRangeOption(options, o.size) 1185 for _, option := range options { 1186 switch x := option.(type) { 1187 case *fs.RangeOption: 1188 offset, count = x.Decode(o.size) 1189 if count < 0 { 1190 count = o.size - offset 1191 } 1192 case *fs.SeekOption: 1193 offset = x.Offset 1194 default: 1195 if option.Mandatory() { 1196 fs.Logf(o, "Unsupported mandatory option: %v", option) 1197 } 1198 } 1199 } 1200 blob := o.getBlobReference() 1201 ac := azblob.BlobAccessConditions{} 1202 var dowloadResponse *azblob.DownloadResponse 1203 err = o.fs.pacer.Call(func() (bool, error) { 1204 dowloadResponse, err = blob.Download(ctx, offset, count, ac, false) 1205 return o.fs.shouldRetry(err) 1206 }) 1207 if err != nil { 1208 return nil, errors.Wrap(err, "failed to open for download") 1209 } 1210 in = dowloadResponse.Body(azblob.RetryReaderOptions{}) 1211 return in, nil 1212 } 1213 1214 // dontEncode is the characters that do not need percent-encoding 1215 // 1216 // The characters that do not need percent-encoding are a subset of 1217 // the printable ASCII characters: upper-case letters, lower-case 1218 // letters, digits, ".", "_", "-", "/", "~", "!", "$", "'", "(", ")", 1219 // "*", ";", "=", ":", and "@". All other byte values in a UTF-8 must 1220 // be replaced with "%" and the two-digit hex value of the byte. 1221 const dontEncode = (`abcdefghijklmnopqrstuvwxyz` + 1222 `ABCDEFGHIJKLMNOPQRSTUVWXYZ` + 1223 `0123456789` + 1224 `._-/~!$'()*;=:@`) 1225 1226 // noNeedToEncode is a bitmap of characters which don't need % encoding 1227 var noNeedToEncode [256]bool 1228 1229 func init() { 1230 for _, c := range dontEncode { 1231 noNeedToEncode[c] = true 1232 } 1233 } 1234 1235 // readSeeker joins an io.Reader and an io.Seeker 1236 type readSeeker struct { 1237 io.Reader 1238 io.Seeker 1239 } 1240 1241 // uploadMultipart uploads a file using multipart upload 1242 // 1243 // Write a larger blob, using CreateBlockBlob, PutBlock, and PutBlockList. 1244 func (o *Object) uploadMultipart(in io.Reader, size int64, blob *azblob.BlobURL, httpHeaders *azblob.BlobHTTPHeaders) (err error) { 1245 // Calculate correct chunkSize 1246 chunkSize := int64(o.fs.opt.ChunkSize) 1247 var totalParts int64 1248 for { 1249 // Calculate number of parts 1250 var remainder int64 1251 totalParts, remainder = size/chunkSize, size%chunkSize 1252 if remainder != 0 { 1253 totalParts++ 1254 } 1255 if totalParts < maxTotalParts { 1256 break 1257 } 1258 // Double chunk size if the number of parts is too big 1259 chunkSize *= 2 1260 if chunkSize > int64(maxChunkSize) { 1261 return errors.Errorf("can't upload as it is too big %v - takes more than %d chunks of %v", fs.SizeSuffix(size), totalParts, fs.SizeSuffix(chunkSize/2)) 1262 } 1263 } 1264 fs.Debugf(o, "Multipart upload session started for %d parts of size %v", totalParts, fs.SizeSuffix(chunkSize)) 1265 1266 // https://godoc.org/github.com/Azure/azure-storage-blob-go/2017-07-29/azblob#example-BlockBlobURL 1267 // Utilities are cloned from above example 1268 // These helper functions convert a binary block ID to a base-64 string and vice versa 1269 // NOTE: The blockID must be <= 64 bytes and ALL blockIDs for the block must be the same length 1270 blockIDBinaryToBase64 := func(blockID []byte) string { return base64.StdEncoding.EncodeToString(blockID) } 1271 // These helper functions convert an int block ID to a base-64 string and vice versa 1272 blockIDIntToBase64 := func(blockID uint64) string { 1273 binaryBlockID := (&[8]byte{})[:] // All block IDs are 8 bytes long 1274 binary.LittleEndian.PutUint64(binaryBlockID, blockID) 1275 return blockIDBinaryToBase64(binaryBlockID) 1276 } 1277 1278 // block ID variables 1279 var ( 1280 rawID uint64 1281 blockID = "" // id in base64 encoded form 1282 blocks []string 1283 ) 1284 1285 // increment the blockID 1286 nextID := func() { 1287 rawID++ 1288 blockID = blockIDIntToBase64(rawID) 1289 blocks = append(blocks, blockID) 1290 } 1291 1292 // Get BlockBlobURL, we will use default pipeline here 1293 blockBlobURL := blob.ToBlockBlobURL() 1294 ctx := context.Background() 1295 ac := azblob.LeaseAccessConditions{} // Use default lease access conditions 1296 1297 // unwrap the accounting from the input, we use wrap to put it 1298 // back on after the buffering 1299 in, wrap := accounting.UnWrap(in) 1300 1301 // Upload the chunks 1302 remaining := size 1303 position := int64(0) 1304 errs := make(chan error, 1) 1305 var wg sync.WaitGroup 1306 outer: 1307 for part := 0; part < int(totalParts); part++ { 1308 // Check any errors 1309 select { 1310 case err = <-errs: 1311 break outer 1312 default: 1313 } 1314 1315 reqSize := remaining 1316 if reqSize >= chunkSize { 1317 reqSize = chunkSize 1318 } 1319 1320 // Make a block of memory 1321 buf := make([]byte, reqSize) 1322 1323 // Read the chunk 1324 _, err = io.ReadFull(in, buf) 1325 if err != nil { 1326 err = errors.Wrap(err, "multipart upload failed to read source") 1327 break outer 1328 } 1329 1330 // Transfer the chunk 1331 nextID() 1332 wg.Add(1) 1333 o.fs.uploadToken.Get() 1334 go func(part int, position int64, blockID string) { 1335 defer wg.Done() 1336 defer o.fs.uploadToken.Put() 1337 fs.Debugf(o, "Uploading part %d/%d offset %v/%v part size %v", part+1, totalParts, fs.SizeSuffix(position), fs.SizeSuffix(size), fs.SizeSuffix(chunkSize)) 1338 1339 // Upload the block, with MD5 for check 1340 md5sum := md5.Sum(buf) 1341 transactionalMD5 := md5sum[:] 1342 err = o.fs.pacer.Call(func() (bool, error) { 1343 bufferReader := bytes.NewReader(buf) 1344 wrappedReader := wrap(bufferReader) 1345 rs := readSeeker{wrappedReader, bufferReader} 1346 _, err = blockBlobURL.StageBlock(ctx, blockID, &rs, ac, transactionalMD5) 1347 return o.fs.shouldRetry(err) 1348 }) 1349 1350 if err != nil { 1351 err = errors.Wrap(err, "multipart upload failed to upload part") 1352 select { 1353 case errs <- err: 1354 default: 1355 } 1356 return 1357 } 1358 }(part, position, blockID) 1359 1360 // ready for next block 1361 remaining -= chunkSize 1362 position += chunkSize 1363 } 1364 wg.Wait() 1365 if err == nil { 1366 select { 1367 case err = <-errs: 1368 default: 1369 } 1370 } 1371 if err != nil { 1372 return err 1373 } 1374 1375 // Finalise the upload session 1376 err = o.fs.pacer.Call(func() (bool, error) { 1377 _, err := blockBlobURL.CommitBlockList(ctx, blocks, *httpHeaders, o.meta, azblob.BlobAccessConditions{}) 1378 return o.fs.shouldRetry(err) 1379 }) 1380 if err != nil { 1381 return errors.Wrap(err, "multipart upload failed to finalize") 1382 } 1383 return nil 1384 } 1385 1386 // Update the object with the contents of the io.Reader, modTime and size 1387 // 1388 // The new object may have been created if an error is returned 1389 func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) { 1390 container, _ := o.split() 1391 err = o.fs.makeContainer(ctx, container) 1392 if err != nil { 1393 return err 1394 } 1395 size := src.Size() 1396 // Update Mod time 1397 o.updateMetadataWithModTime(src.ModTime(ctx)) 1398 if err != nil { 1399 return err 1400 } 1401 1402 blob := o.getBlobReference() 1403 httpHeaders := azblob.BlobHTTPHeaders{} 1404 httpHeaders.ContentType = fs.MimeType(ctx, o) 1405 // Compute the Content-MD5 of the file, for multiparts uploads it 1406 // will be set in PutBlockList API call using the 'x-ms-blob-content-md5' header 1407 // Note: If multipart, a MD5 checksum will also be computed for each uploaded block 1408 // in order to validate its integrity during transport 1409 if sourceMD5, _ := src.Hash(ctx, hash.MD5); sourceMD5 != "" { 1410 sourceMD5bytes, err := hex.DecodeString(sourceMD5) 1411 if err == nil { 1412 httpHeaders.ContentMD5 = sourceMD5bytes 1413 } else { 1414 fs.Debugf(o, "Failed to decode %q as MD5: %v", sourceMD5, err) 1415 } 1416 } 1417 1418 putBlobOptions := azblob.UploadStreamToBlockBlobOptions{ 1419 BufferSize: int(o.fs.opt.ChunkSize), 1420 MaxBuffers: 4, 1421 Metadata: o.meta, 1422 BlobHTTPHeaders: httpHeaders, 1423 } 1424 // FIXME Until https://github.com/Azure/azure-storage-blob-go/pull/75 1425 // is merged the SDK can't upload a single blob of exactly the chunk 1426 // size, so upload with a multpart upload to work around. 1427 // See: https://github.com/rclone/rclone/issues/2653 1428 multipartUpload := size >= int64(o.fs.opt.UploadCutoff) 1429 if size == int64(o.fs.opt.ChunkSize) { 1430 multipartUpload = true 1431 fs.Debugf(o, "Setting multipart upload for file of chunk size (%d) to work around SDK bug", size) 1432 } 1433 1434 // Don't retry, return a retry error instead 1435 err = o.fs.pacer.CallNoRetry(func() (bool, error) { 1436 if multipartUpload { 1437 // If a large file upload in chunks 1438 err = o.uploadMultipart(in, size, &blob, &httpHeaders) 1439 } else { 1440 // Write a small blob in one transaction 1441 blockBlobURL := blob.ToBlockBlobURL() 1442 _, err = azblob.UploadStreamToBlockBlob(ctx, in, blockBlobURL, putBlobOptions) 1443 } 1444 return o.fs.shouldRetry(err) 1445 }) 1446 if err != nil { 1447 return err 1448 } 1449 // Refresh metadata on object 1450 o.clearMetaData() 1451 err = o.readMetaData() 1452 if err != nil { 1453 return err 1454 } 1455 1456 // If tier is not changed or not specified, do not attempt to invoke `SetBlobTier` operation 1457 if o.fs.opt.AccessTier == string(defaultAccessTier) || o.fs.opt.AccessTier == string(o.AccessTier()) { 1458 return nil 1459 } 1460 1461 // Now, set blob tier based on configured access tier 1462 return o.SetTier(o.fs.opt.AccessTier) 1463 } 1464 1465 // Remove an object 1466 func (o *Object) Remove(ctx context.Context) error { 1467 blob := o.getBlobReference() 1468 snapShotOptions := azblob.DeleteSnapshotsOptionNone 1469 ac := azblob.BlobAccessConditions{} 1470 return o.fs.pacer.Call(func() (bool, error) { 1471 _, err := blob.Delete(ctx, snapShotOptions, ac) 1472 return o.fs.shouldRetry(err) 1473 }) 1474 } 1475 1476 // MimeType of an Object if known, "" otherwise 1477 func (o *Object) MimeType(ctx context.Context) string { 1478 return o.mimeType 1479 } 1480 1481 // AccessTier of an object, default is of type none 1482 func (o *Object) AccessTier() azblob.AccessTierType { 1483 return o.accessTier 1484 } 1485 1486 // SetTier performs changing object tier 1487 func (o *Object) SetTier(tier string) error { 1488 if !validateAccessTier(tier) { 1489 return errors.Errorf("Tier %s not supported by Azure Blob Storage", tier) 1490 } 1491 1492 // Check if current tier already matches with desired tier 1493 if o.GetTier() == tier { 1494 return nil 1495 } 1496 desiredAccessTier := azblob.AccessTierType(tier) 1497 blob := o.getBlobReference() 1498 ctx := context.Background() 1499 err := o.fs.pacer.Call(func() (bool, error) { 1500 _, err := blob.SetTier(ctx, desiredAccessTier, azblob.LeaseAccessConditions{}) 1501 return o.fs.shouldRetry(err) 1502 }) 1503 1504 if err != nil { 1505 return errors.Wrap(err, "Failed to set Blob Tier") 1506 } 1507 1508 // Set access tier on local object also, this typically 1509 // gets updated on get blob properties 1510 o.accessTier = desiredAccessTier 1511 fs.Debugf(o, "Successfully changed object tier to %s", tier) 1512 1513 return nil 1514 } 1515 1516 // GetTier returns object tier in azure as string 1517 func (o *Object) GetTier() string { 1518 return string(o.accessTier) 1519 } 1520 1521 // Check the interfaces are satisfied 1522 var ( 1523 _ fs.Fs = &Fs{} 1524 _ fs.Copier = &Fs{} 1525 _ fs.Purger = &Fs{} 1526 _ fs.ListRer = &Fs{} 1527 _ fs.Object = &Object{} 1528 _ fs.MimeTyper = &Object{} 1529 _ fs.GetTierer = &Object{} 1530 _ fs.SetTierer = &Object{} 1531 )