|
|
@@ -0,0 +1,320 @@
|
|
|
+package imguploader
|
|
|
+
|
|
|
+import (
|
|
|
+ "bytes"
|
|
|
+ "context"
|
|
|
+ "crypto/hmac"
|
|
|
+ "crypto/sha256"
|
|
|
+ "encoding/base64"
|
|
|
+ "encoding/xml"
|
|
|
+ "fmt"
|
|
|
+ "io"
|
|
|
+ "io/ioutil"
|
|
|
+ "mime"
|
|
|
+ "net/http"
|
|
|
+ "net/url"
|
|
|
+ "os"
|
|
|
+ "path"
|
|
|
+ "sort"
|
|
|
+ "strconv"
|
|
|
+ "strings"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "github.com/grafana/grafana/pkg/log"
|
|
|
+ "github.com/grafana/grafana/pkg/util"
|
|
|
+)
|
|
|
+
|
|
|
+type AzureBlobUploader struct {
|
|
|
+ account_name string
|
|
|
+ account_key string
|
|
|
+ container_name string
|
|
|
+ log log.Logger
|
|
|
+}
|
|
|
+
|
|
|
+func NewAzureBlobUploader(account_name string, account_key string, container_name string) *AzureBlobUploader {
|
|
|
+ return &AzureBlobUploader{
|
|
|
+ account_name: account_name,
|
|
|
+ account_key: account_key,
|
|
|
+ container_name: container_name,
|
|
|
+ log: log.New("azureBlobUploader"),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Receive path of image on disk and return azure blob url
|
|
|
+func (az *AzureBlobUploader) Upload(ctx context.Context, imageDiskPath string) (string, error) {
|
|
|
+ // setup client
|
|
|
+ blob := NewStorageClient(az.account_name, az.account_key)
|
|
|
+
|
|
|
+ file, err := os.Open(imageDiskPath)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+ randomFileName := util.GetRandomString(30) + ".png"
|
|
|
+ // upload image
|
|
|
+ az.log.Debug("Uploading image to azure_blob", "conatiner_name", az.container_name, "blob_name", randomFileName)
|
|
|
+ resp, err := blob.FileUpload(az.container_name, randomFileName, file)
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+
|
|
|
+ if resp.StatusCode > 400 && resp.StatusCode < 600 {
|
|
|
+ body, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
|
|
+ aerr := &Error{
|
|
|
+ Code: resp.StatusCode,
|
|
|
+ Status: resp.Status,
|
|
|
+ Body: body,
|
|
|
+ Header: resp.Header,
|
|
|
+ }
|
|
|
+ aerr.parseXML()
|
|
|
+ resp.Body.Close()
|
|
|
+ return "", aerr
|
|
|
+ }
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+
|
|
|
+ url := fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s", az.account_name, az.container_name, randomFileName)
|
|
|
+ return url, nil
|
|
|
+}
|
|
|
+
|
|
|
+// --- AZURE LIBRARY
|
|
|
+type Blobs struct {
|
|
|
+ XMLName xml.Name `xml:"EnumerationResults"`
|
|
|
+ Items []Blob `xml:"Blobs>Blob"`
|
|
|
+}
|
|
|
+
|
|
|
+type Blob struct {
|
|
|
+ Name string `xml:"Name"`
|
|
|
+ Property Property `xml:"Properties"`
|
|
|
+}
|
|
|
+
|
|
|
+type Property struct {
|
|
|
+ LastModified string `xml:"Last-Modified"`
|
|
|
+ Etag string `xml:"Etag"`
|
|
|
+ ContentLength int `xml:"Content-Length"`
|
|
|
+ ContentType string `xml:"Content-Type"`
|
|
|
+ BlobType string `xml:"BlobType"`
|
|
|
+ LeaseStatus string `xml:"LeaseStatus"`
|
|
|
+}
|
|
|
+
|
|
|
+type Error struct {
|
|
|
+ Code int
|
|
|
+ Status string
|
|
|
+ Body []byte
|
|
|
+ Header http.Header
|
|
|
+
|
|
|
+ AzureCode string
|
|
|
+}
|
|
|
+
|
|
|
+func (e *Error) Error() string {
|
|
|
+ return fmt.Sprintf("status %d: %s", e.Code, e.Body)
|
|
|
+}
|
|
|
+
|
|
|
+func (e *Error) parseXML() {
|
|
|
+ var xe xmlError
|
|
|
+ _ = xml.NewDecoder(bytes.NewReader(e.Body)).Decode(&xe)
|
|
|
+ e.AzureCode = xe.Code
|
|
|
+}
|
|
|
+
|
|
|
+type xmlError struct {
|
|
|
+ XMLName xml.Name `xml:"Error"`
|
|
|
+ Code string
|
|
|
+ Message string
|
|
|
+}
|
|
|
+
|
|
|
+const ms_date_layout = "Mon, 02 Jan 2006 15:04:05 GMT"
|
|
|
+const version = "2017-04-17"
|
|
|
+
|
|
|
+var client = &http.Client{}
|
|
|
+
|
|
|
+type StorageClient struct {
|
|
|
+ Auth *Auth
|
|
|
+ Transport http.RoundTripper
|
|
|
+}
|
|
|
+
|
|
|
+func (c *StorageClient) transport() http.RoundTripper {
|
|
|
+ if c.Transport != nil {
|
|
|
+ return c.Transport
|
|
|
+ }
|
|
|
+ return http.DefaultTransport
|
|
|
+}
|
|
|
+
|
|
|
+func NewStorageClient(account, accessKey string) *StorageClient {
|
|
|
+ return &StorageClient{
|
|
|
+ Auth: &Auth{
|
|
|
+ account,
|
|
|
+ accessKey,
|
|
|
+ },
|
|
|
+ Transport: nil,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (c *StorageClient) absUrl(format string, a ...interface{}) string {
|
|
|
+ part := fmt.Sprintf(format, a...)
|
|
|
+ return fmt.Sprintf("https://%s.blob.core.windows.net/%s", c.Auth.Account, part)
|
|
|
+}
|
|
|
+
|
|
|
+func copyHeadersToRequest(req *http.Request, headers map[string]string) {
|
|
|
+ for k, v := range headers {
|
|
|
+ req.Header[k] = []string{v}
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (c *StorageClient) FileUpload(container, blobName string, body io.Reader) (*http.Response, error) {
|
|
|
+ blobName = escape(blobName)
|
|
|
+ extension := strings.ToLower(path.Ext(blobName))
|
|
|
+ contentType := mime.TypeByExtension(extension)
|
|
|
+ buf := new(bytes.Buffer)
|
|
|
+ buf.ReadFrom(body)
|
|
|
+ req, err := http.NewRequest(
|
|
|
+ "PUT",
|
|
|
+ c.absUrl("%s/%s", container, blobName),
|
|
|
+ buf,
|
|
|
+ )
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ copyHeadersToRequest(req, map[string]string{
|
|
|
+ "x-ms-blob-type": "BlockBlob",
|
|
|
+ "x-ms-date": time.Now().UTC().Format(ms_date_layout),
|
|
|
+ "x-ms-version": version,
|
|
|
+ "Accept-Charset": "UTF-8",
|
|
|
+ "Content-Type": contentType,
|
|
|
+ "Content-Length": strconv.Itoa(buf.Len()),
|
|
|
+ })
|
|
|
+
|
|
|
+ c.Auth.SignRequest(req)
|
|
|
+
|
|
|
+ return c.transport().RoundTrip(req)
|
|
|
+}
|
|
|
+
|
|
|
+func escape(content string) string {
|
|
|
+ content = url.QueryEscape(content)
|
|
|
+ // the Azure's behavior uses %20 to represent whitespace instead of + (plus)
|
|
|
+ content = strings.Replace(content, "+", "%20", -1)
|
|
|
+ // the Azure's behavior uses slash instead of + %2F
|
|
|
+ content = strings.Replace(content, "%2F", "/", -1)
|
|
|
+
|
|
|
+ return content
|
|
|
+}
|
|
|
+
|
|
|
+type Auth struct {
|
|
|
+ Account string
|
|
|
+ Key string
|
|
|
+}
|
|
|
+
|
|
|
+func (a *Auth) SignRequest(req *http.Request) {
|
|
|
+ strToSign := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s",
|
|
|
+ strings.ToUpper(req.Method),
|
|
|
+ tryget(req.Header, "Content-Encoding"),
|
|
|
+ tryget(req.Header, "Content-Language"),
|
|
|
+ tryget(req.Header, "Content-Length"),
|
|
|
+ tryget(req.Header, "Content-MD5"),
|
|
|
+ tryget(req.Header, "Content-Type"),
|
|
|
+ tryget(req.Header, "Date"),
|
|
|
+ tryget(req.Header, "If-Modified-Since"),
|
|
|
+ tryget(req.Header, "If-Match"),
|
|
|
+ tryget(req.Header, "If-None-Match"),
|
|
|
+ tryget(req.Header, "If-Unmodified-Since"),
|
|
|
+ tryget(req.Header, "Range"),
|
|
|
+ a.canonicalizedHeaders(req),
|
|
|
+ a.canonicalizedResource(req),
|
|
|
+ )
|
|
|
+ decodedKey, _ := base64.StdEncoding.DecodeString(a.Key)
|
|
|
+
|
|
|
+ sha256 := hmac.New(sha256.New, []byte(decodedKey))
|
|
|
+ sha256.Write([]byte(strToSign))
|
|
|
+
|
|
|
+ signature := base64.StdEncoding.EncodeToString(sha256.Sum(nil))
|
|
|
+
|
|
|
+ copyHeadersToRequest(req, map[string]string{
|
|
|
+ "Authorization": fmt.Sprintf("SharedKey %s:%s", a.Account, signature),
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+func tryget(headers map[string][]string, key string) string {
|
|
|
+ // We default to empty string for "0" values to match server side behavior when generating signatures.
|
|
|
+ if len(headers[key]) > 0 { // && headers[key][0] != "0" { //&& key != "Content-Length" {
|
|
|
+ return headers[key][0]
|
|
|
+ }
|
|
|
+ return ""
|
|
|
+}
|
|
|
+
|
|
|
+//
|
|
|
+// The following is copied ~95% verbatim from:
|
|
|
+// http://github.com/loldesign/azure/ -> core/core.go
|
|
|
+//
|
|
|
+
|
|
|
+/*
|
|
|
+Based on Azure docs:
|
|
|
+ Link: http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx#Constructing_Element
|
|
|
+
|
|
|
+ 1) Retrieve all headers for the resource that begin with x-ms-, including the x-ms-date header.
|
|
|
+ 2) Convert each HTTP header name to lowercase.
|
|
|
+ 3) Sort the headers lexicographically by header name, in ascending order. Note that each header may appear only once in the string.
|
|
|
+ 4) Unfold the string by replacing any breaking white space with a single space.
|
|
|
+ 5) Trim any white space around the colon in the header.
|
|
|
+ 6) Finally, append a new line character to each canonicalized header in the resulting list. Construct the CanonicalizedHeaders string by concatenating all headers in this list into a single string.
|
|
|
+*/
|
|
|
+func (a *Auth) canonicalizedHeaders(req *http.Request) string {
|
|
|
+ var buffer bytes.Buffer
|
|
|
+
|
|
|
+ for key, value := range req.Header {
|
|
|
+ lowerKey := strings.ToLower(key)
|
|
|
+
|
|
|
+ if strings.HasPrefix(lowerKey, "x-ms-") {
|
|
|
+ if buffer.Len() == 0 {
|
|
|
+ buffer.WriteString(fmt.Sprintf("%s:%s", lowerKey, value[0]))
|
|
|
+ } else {
|
|
|
+ buffer.WriteString(fmt.Sprintf("\n%s:%s", lowerKey, value[0]))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ splitted := strings.Split(buffer.String(), "\n")
|
|
|
+ sort.Strings(splitted)
|
|
|
+
|
|
|
+ return strings.Join(splitted, "\n")
|
|
|
+}
|
|
|
+
|
|
|
+/*
|
|
|
+Based on Azure docs
|
|
|
+ Link: http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx#Constructing_Element
|
|
|
+
|
|
|
+1) Beginning with an empty string (""), append a forward slash (/), followed by the name of the account that owns the resource being accessed.
|
|
|
+2) Append the resource's encoded URI path, without any query parameters.
|
|
|
+3) Retrieve all query parameters on the resource URI, including the comp parameter if it exists.
|
|
|
+4) Convert all parameter names to lowercase.
|
|
|
+5) Sort the query parameters lexicographically by parameter name, in ascending order.
|
|
|
+6) URL-decode each query parameter name and value.
|
|
|
+7) Append each query parameter name and value to the string in the following format, making sure to include the colon (:) between the name and the value:
|
|
|
+ parameter-name:parameter-value
|
|
|
+
|
|
|
+8) If a query parameter has more than one value, sort all values lexicographically, then include them in a comma-separated list:
|
|
|
+ parameter-name:parameter-value-1,parameter-value-2,parameter-value-n
|
|
|
+
|
|
|
+9) Append a new line character (\n) after each name-value pair.
|
|
|
+
|
|
|
+Rules:
|
|
|
+ 1) Avoid using the new line character (\n) in values for query parameters. If it must be used, ensure that it does not affect the format of the canonicalized resource string.
|
|
|
+ 2) Avoid using commas in query parameter values.
|
|
|
+*/
|
|
|
+func (a *Auth) canonicalizedResource(req *http.Request) string {
|
|
|
+ var buffer bytes.Buffer
|
|
|
+
|
|
|
+ buffer.WriteString(fmt.Sprintf("/%s%s", a.Account, req.URL.Path))
|
|
|
+ queries := req.URL.Query()
|
|
|
+
|
|
|
+ for key, values := range queries {
|
|
|
+ sort.Strings(values)
|
|
|
+ buffer.WriteString(fmt.Sprintf("\n%s:%s", key, strings.Join(values, ",")))
|
|
|
+ }
|
|
|
+
|
|
|
+ splitted := strings.Split(buffer.String(), "\n")
|
|
|
+ sort.Strings(splitted)
|
|
|
+
|
|
|
+ return strings.Join(splitted, "\n")
|
|
|
+}
|