pkg/download/blob.go (125 lines of code) (raw):

package download import ( "fmt" "net/http" "net/url" "os" "path/filepath" "strings" "time" "github.com/Azure/azure-sdk-for-go/storage" "github.com/Azure/run-command-handler-linux/pkg/blobutil" "github.com/google/uuid" "github.com/pkg/errors" ) const ( // blobSASDuration describes the duration for which the generated // Shared Access Signature for the blob is valid. blobSASDuration = time.Minute * 30 ) type blobDownload struct { accountName, accountKey string blob blobutil.AzureBlobRef } func (b blobDownload) GetRequest() (*http.Request, error) { url, err := b.getURL() if err != nil { return nil, err } req, err := http.NewRequest("GET", url, nil) if req != nil { req.Header.Set(xMsClientRequestIdHeaderName, uuid.New().String()) } return req, err } // getURL returns publicly downloadable URL of the Azure Blob // by generating a URL with a temporary Shared Access Signature. func (b blobDownload) getURL() (string, error) { client, err := storage.NewClient(b.accountName, b.accountKey, b.blob.StorageBase, storage.DefaultAPIVersion, true) if err != nil { return "", errors.Wrap(err, "failed to initialize azure storage client") } // get read-only blobStorageClient := client.GetBlobService() container := blobStorageClient.GetContainerReference(b.blob.Container) blob := container.GetBlobReference(b.blob.Blob) options := storage.BlobSASOptions{ BlobServiceSASPermissions: storage.BlobServiceSASPermissions{Read: true}, SASOptions: storage.SASOptions{ Expiry: time.Now().UTC().Add(blobSASDuration), }, } sasURL, err := blob.GetSASURI(options) if err != nil { return "", errors.Wrap(err, "failed to generate SAS key for blob") } return sasURL, nil } // NewBlobDownload creates a new Downloader for a blob hosted in Azure Blob Storage. func NewBlobDownload(accountName, accountKey string, blob blobutil.AzureBlobRef) Downloader { return blobDownload{accountName, accountKey, blob} } // GetSASBlob download a blob with specified uri and sas authorization and saves it to the target directory // Returns the filePath where the blob was downloaded func GetSASBlob(blobURI, blobSas, targetDir string) (string, error) { bloburl, err := url.Parse(blobURI + blobSas) loggableBlobUri := GetUriForLogging(blobURI) if err != nil { return "", errors.Wrapf(err, "unable to parse URL: %q", loggableBlobUri) } containerRef, err := storage.GetContainerReferenceFromSASURI(*bloburl) if err != nil { return "", errors.Wrapf(err, "unable to open storage container: %q", loggableBlobUri) } // Extract the blob path after container name fileName, blobPathError := getBlobPathAfterContainerName(blobURI, containerRef.Name) if fileName == "" { return "", errors.Wrapf(blobPathError, "cannot extract blob path name from URL: %q", loggableBlobUri) } blobref := containerRef.GetBlobReference(fileName) reader, err := blobref.Get(nil) if err != nil { return "", errors.Wrapf(err, "unable to open storage blob: %q", loggableBlobUri) } scriptFilePath := filepath.Join(targetDir, fileName) const mode = 0500 // scripts should have execute permissions file, err := os.OpenFile(scriptFilePath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, mode) if err != nil { return "", errors.Wrapf(err, "failed to open file '%s' for writing: ", scriptFilePath) } defer file.Close() var buff = make([]byte, 1000) for numBytes, _ := reader.Read(buff); numBytes > 0; numBytes, _ = reader.Read(buff) { writtenBytes, writeErr := file.Write(buff[:numBytes]) if writtenBytes != numBytes || writeErr != nil { return "", errors.Wrapf(writeErr, "failed to write to the file '%s': ", scriptFilePath) } } return scriptFilePath, nil } // CreateOrReplaceAppendBlob creates a reference to an append blob. If blob exists - it gets deleted first. func CreateOrReplaceAppendBlob(blobURI, blobSas string) (*storage.Blob, error) { bloburl, err := url.Parse(blobURI + blobSas) if err != nil { return nil, err } containerRef, err := storage.GetContainerReferenceFromSASURI(*bloburl) if err != nil { return nil, err } fileName, blobPathError := getBlobPathAfterContainerName(blobURI, containerRef.Name) if fileName == "" { return nil, errors.Wrapf(blobPathError, "cannot extract blob path name from URL: %q", GetUriForLogging(blobURI)) } blobref := containerRef.GetBlobReference(fileName) err = blobref.PutAppendBlob(nil) // Create the append blob if err != nil { return nil, err } return blobref, nil } // Extract the suffix after the container name from blob uri // Example: blobURI - https://mystorageaccount.blob.core.windows.net/mycontainer/dir2/dir3/outputL.txt, // Returns "dir2/dir3/outputL.txt" (Blobs would be created under the container under nested directories mycontainer/dir2/dir3 as expected) func getBlobPathAfterContainerName(blobURI string, containerName string) (string, error) { blobURL, err := url.Parse(blobURI) if err != nil { return "", err } containerNameSearchString := containerName + "/" blobPathWithoutHost := blobURL.Path index := strings.Index(blobPathWithoutHost, containerNameSearchString) if index >= 0 { return blobPathWithoutHost[index+len(containerNameSearchString):], nil } else { return "", errors.New(fmt.Sprintf("Unable to find '%s' in blobURI '%s'. Unable to get blob path suffix after container name.", containerNameSearchString, GetUriForLogging(blobURI))) } }