forked from TrueCloudLab/restic
237 lines
8.3 KiB
Go
237 lines
8.3 KiB
Go
package storage
|
|
|
|
// Copyright 2017 Microsoft Corporation
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
blobCopyStatusPending = "pending"
|
|
blobCopyStatusSuccess = "success"
|
|
blobCopyStatusAborted = "aborted"
|
|
blobCopyStatusFailed = "failed"
|
|
)
|
|
|
|
// CopyOptions includes the options for a copy blob operation
|
|
type CopyOptions struct {
|
|
Timeout uint
|
|
Source CopyOptionsConditions
|
|
Destiny CopyOptionsConditions
|
|
RequestID string
|
|
}
|
|
|
|
// IncrementalCopyOptions includes the options for an incremental copy blob operation
|
|
type IncrementalCopyOptions struct {
|
|
Timeout uint
|
|
Destination IncrementalCopyOptionsConditions
|
|
RequestID string
|
|
}
|
|
|
|
// CopyOptionsConditions includes some conditional options in a copy blob operation
|
|
type CopyOptionsConditions struct {
|
|
LeaseID string
|
|
IfModifiedSince *time.Time
|
|
IfUnmodifiedSince *time.Time
|
|
IfMatch string
|
|
IfNoneMatch string
|
|
}
|
|
|
|
// IncrementalCopyOptionsConditions includes some conditional options in a copy blob operation
|
|
type IncrementalCopyOptionsConditions struct {
|
|
IfModifiedSince *time.Time
|
|
IfUnmodifiedSince *time.Time
|
|
IfMatch string
|
|
IfNoneMatch string
|
|
}
|
|
|
|
// Copy starts a blob copy operation and waits for the operation to
|
|
// complete. sourceBlob parameter must be a canonical URL to the blob (can be
|
|
// obtained using the GetURL method.) There is no SLA on blob copy and therefore
|
|
// this helper method works faster on smaller files.
|
|
//
|
|
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Copy-Blob
|
|
func (b *Blob) Copy(sourceBlob string, options *CopyOptions) error {
|
|
copyID, err := b.StartCopy(sourceBlob, options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return b.WaitForCopy(copyID)
|
|
}
|
|
|
|
// StartCopy starts a blob copy operation.
|
|
// sourceBlob parameter must be a canonical URL to the blob (can be
|
|
// obtained using the GetURL method.)
|
|
//
|
|
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Copy-Blob
|
|
func (b *Blob) StartCopy(sourceBlob string, options *CopyOptions) (string, error) {
|
|
params := url.Values{}
|
|
headers := b.Container.bsc.client.getStandardHeaders()
|
|
headers["x-ms-copy-source"] = sourceBlob
|
|
headers = b.Container.bsc.client.addMetadataToHeaders(headers, b.Metadata)
|
|
|
|
if options != nil {
|
|
params = addTimeout(params, options.Timeout)
|
|
headers = addToHeaders(headers, "x-ms-client-request-id", options.RequestID)
|
|
// source
|
|
headers = addToHeaders(headers, "x-ms-source-lease-id", options.Source.LeaseID)
|
|
headers = addTimeToHeaders(headers, "x-ms-source-if-modified-since", options.Source.IfModifiedSince)
|
|
headers = addTimeToHeaders(headers, "x-ms-source-if-unmodified-since", options.Source.IfUnmodifiedSince)
|
|
headers = addToHeaders(headers, "x-ms-source-if-match", options.Source.IfMatch)
|
|
headers = addToHeaders(headers, "x-ms-source-if-none-match", options.Source.IfNoneMatch)
|
|
//destiny
|
|
headers = addToHeaders(headers, "x-ms-lease-id", options.Destiny.LeaseID)
|
|
headers = addTimeToHeaders(headers, "x-ms-if-modified-since", options.Destiny.IfModifiedSince)
|
|
headers = addTimeToHeaders(headers, "x-ms-if-unmodified-since", options.Destiny.IfUnmodifiedSince)
|
|
headers = addToHeaders(headers, "x-ms-if-match", options.Destiny.IfMatch)
|
|
headers = addToHeaders(headers, "x-ms-if-none-match", options.Destiny.IfNoneMatch)
|
|
}
|
|
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
|
|
resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, nil, b.Container.bsc.auth)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer readAndCloseBody(resp.Body)
|
|
|
|
if err := checkRespCode(resp, []int{http.StatusAccepted, http.StatusCreated}); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
copyID := resp.Header.Get("x-ms-copy-id")
|
|
if copyID == "" {
|
|
return "", errors.New("Got empty copy id header")
|
|
}
|
|
return copyID, nil
|
|
}
|
|
|
|
// AbortCopyOptions includes the options for an abort blob operation
|
|
type AbortCopyOptions struct {
|
|
Timeout uint
|
|
LeaseID string `header:"x-ms-lease-id"`
|
|
RequestID string `header:"x-ms-client-request-id"`
|
|
}
|
|
|
|
// AbortCopy aborts a BlobCopy which has already been triggered by the StartBlobCopy function.
|
|
// copyID is generated from StartBlobCopy function.
|
|
// currentLeaseID is required IF the destination blob has an active lease on it.
|
|
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Abort-Copy-Blob
|
|
func (b *Blob) AbortCopy(copyID string, options *AbortCopyOptions) error {
|
|
params := url.Values{
|
|
"comp": {"copy"},
|
|
"copyid": {copyID},
|
|
}
|
|
headers := b.Container.bsc.client.getStandardHeaders()
|
|
headers["x-ms-copy-action"] = "abort"
|
|
|
|
if options != nil {
|
|
params = addTimeout(params, options.Timeout)
|
|
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
}
|
|
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
|
|
resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, nil, b.Container.bsc.auth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer readAndCloseBody(resp.Body)
|
|
return checkRespCode(resp, []int{http.StatusNoContent})
|
|
}
|
|
|
|
// WaitForCopy loops until a BlobCopy operation is completed (or fails with error)
|
|
func (b *Blob) WaitForCopy(copyID string) error {
|
|
for {
|
|
err := b.GetProperties(nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if b.Properties.CopyID != copyID {
|
|
return errBlobCopyIDMismatch
|
|
}
|
|
|
|
switch b.Properties.CopyStatus {
|
|
case blobCopyStatusSuccess:
|
|
return nil
|
|
case blobCopyStatusPending:
|
|
continue
|
|
case blobCopyStatusAborted:
|
|
return errBlobCopyAborted
|
|
case blobCopyStatusFailed:
|
|
return fmt.Errorf("storage: blob copy failed. Id=%s Description=%s", b.Properties.CopyID, b.Properties.CopyStatusDescription)
|
|
default:
|
|
return fmt.Errorf("storage: unhandled blob copy status: '%s'", b.Properties.CopyStatus)
|
|
}
|
|
}
|
|
}
|
|
|
|
// IncrementalCopyBlob copies a snapshot of a source blob and copies to referring blob
|
|
// sourceBlob parameter must be a valid snapshot URL of the original blob.
|
|
// THe original blob mut be public, or use a Shared Access Signature.
|
|
//
|
|
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/incremental-copy-blob .
|
|
func (b *Blob) IncrementalCopyBlob(sourceBlobURL string, snapshotTime time.Time, options *IncrementalCopyOptions) (string, error) {
|
|
params := url.Values{"comp": {"incrementalcopy"}}
|
|
|
|
// need formatting to 7 decimal places so it's friendly to Windows and *nix
|
|
snapshotTimeFormatted := snapshotTime.Format("2006-01-02T15:04:05.0000000Z")
|
|
u, err := url.Parse(sourceBlobURL)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
query := u.Query()
|
|
query.Add("snapshot", snapshotTimeFormatted)
|
|
encodedQuery := query.Encode()
|
|
encodedQuery = strings.Replace(encodedQuery, "%3A", ":", -1)
|
|
u.RawQuery = encodedQuery
|
|
snapshotURL := u.String()
|
|
|
|
headers := b.Container.bsc.client.getStandardHeaders()
|
|
headers["x-ms-copy-source"] = snapshotURL
|
|
|
|
if options != nil {
|
|
addTimeout(params, options.Timeout)
|
|
headers = addToHeaders(headers, "x-ms-client-request-id", options.RequestID)
|
|
headers = addTimeToHeaders(headers, "x-ms-if-modified-since", options.Destination.IfModifiedSince)
|
|
headers = addTimeToHeaders(headers, "x-ms-if-unmodified-since", options.Destination.IfUnmodifiedSince)
|
|
headers = addToHeaders(headers, "x-ms-if-match", options.Destination.IfMatch)
|
|
headers = addToHeaders(headers, "x-ms-if-none-match", options.Destination.IfNoneMatch)
|
|
}
|
|
|
|
// get URI of destination blob
|
|
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
|
|
resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, nil, b.Container.bsc.auth)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer readAndCloseBody(resp.Body)
|
|
|
|
if err := checkRespCode(resp, []int{http.StatusAccepted}); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
copyID := resp.Header.Get("x-ms-copy-id")
|
|
if copyID == "" {
|
|
return "", errors.New("Got empty copy id header")
|
|
}
|
|
return copyID, nil
|
|
}
|