azure.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. package azure
  2. import (
  3. "errors"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "net/url"
  8. "strconv"
  9. "strings"
  10. "github.com/Azure/azure-sdk-for-go/sdk/azcore"
  11. "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
  12. "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
  13. "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
  14. "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
  15. "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blockblob"
  16. "github.com/imgproxy/imgproxy/v3/config"
  17. "github.com/imgproxy/imgproxy/v3/httprange"
  18. defaultTransport "github.com/imgproxy/imgproxy/v3/transport"
  19. "github.com/imgproxy/imgproxy/v3/transport/common"
  20. "github.com/imgproxy/imgproxy/v3/transport/notmodified"
  21. )
  22. type transport struct {
  23. client *azblob.Client
  24. }
  25. func New() (http.RoundTripper, error) {
  26. var (
  27. client *azblob.Client
  28. sharedKeyCredential *azblob.SharedKeyCredential
  29. defaultAzureCredential *azidentity.DefaultAzureCredential
  30. err error
  31. )
  32. if len(config.ABSName) == 0 {
  33. return nil, errors.New("IMGPROXY_ABS_NAME must be set")
  34. }
  35. endpoint := config.ABSEndpoint
  36. if len(endpoint) == 0 {
  37. endpoint = fmt.Sprintf("https://%s.blob.core.windows.net", config.ABSName)
  38. }
  39. endpointURL, err := url.Parse(endpoint)
  40. if err != nil {
  41. return nil, err
  42. }
  43. trans, err := defaultTransport.New(false)
  44. if err != nil {
  45. return nil, err
  46. }
  47. opts := azblob.ClientOptions{
  48. ClientOptions: policy.ClientOptions{
  49. Transport: &http.Client{Transport: trans},
  50. },
  51. }
  52. if len(config.ABSKey) > 0 {
  53. sharedKeyCredential, err = azblob.NewSharedKeyCredential(config.ABSName, config.ABSKey)
  54. if err != nil {
  55. return nil, err
  56. }
  57. client, err = azblob.NewClientWithSharedKeyCredential(endpointURL.String(), sharedKeyCredential, &opts)
  58. } else {
  59. defaultAzureCredential, err = azidentity.NewDefaultAzureCredential(nil)
  60. if err != nil {
  61. return nil, err
  62. }
  63. client, err = azblob.NewClient(endpointURL.String(), defaultAzureCredential, &opts)
  64. }
  65. if err != nil {
  66. return nil, err
  67. }
  68. return transport{client}, nil
  69. }
  70. func (t transport) RoundTrip(req *http.Request) (*http.Response, error) {
  71. container, key, _ := common.GetBucketAndKey(req.URL)
  72. if len(container) == 0 || len(key) == 0 {
  73. body := strings.NewReader("Invalid ABS URL: container name or object key is empty")
  74. return &http.Response{
  75. StatusCode: http.StatusNotFound,
  76. Proto: "HTTP/1.0",
  77. ProtoMajor: 1,
  78. ProtoMinor: 0,
  79. Header: http.Header{"Content-Type": {"text/plain"}},
  80. ContentLength: int64(body.Len()),
  81. Body: io.NopCloser(body),
  82. Close: false,
  83. Request: req,
  84. }, nil
  85. }
  86. statusCode := http.StatusOK
  87. header := make(http.Header)
  88. opts := &blob.DownloadStreamOptions{}
  89. if r := req.Header.Get("Range"); len(r) != 0 {
  90. start, end, err := httprange.Parse(r)
  91. if err != nil {
  92. return httprange.InvalidHTTPRangeResponse(req), nil
  93. }
  94. if end != 0 {
  95. length := end - start + 1
  96. if end <= 0 {
  97. length = blockblob.CountToEnd
  98. }
  99. opts.Range = blob.HTTPRange{
  100. Offset: start,
  101. Count: length,
  102. }
  103. }
  104. statusCode = http.StatusPartialContent
  105. }
  106. result, err := t.client.DownloadStream(req.Context(), container, key, opts)
  107. if err != nil {
  108. if azError, ok := err.(*azcore.ResponseError); !ok || azError.StatusCode < 100 || azError.StatusCode == 301 {
  109. return nil, err
  110. } else {
  111. body := strings.NewReader(azError.Error())
  112. return &http.Response{
  113. StatusCode: azError.StatusCode,
  114. Proto: "HTTP/1.0",
  115. ProtoMajor: 1,
  116. ProtoMinor: 0,
  117. Header: http.Header{"Content-Type": {"text/plain"}},
  118. ContentLength: int64(body.Len()),
  119. Body: io.NopCloser(body),
  120. Close: false,
  121. Request: req,
  122. }, nil
  123. }
  124. }
  125. if config.ETagEnabled && result.ETag != nil {
  126. etag := string(*result.ETag)
  127. header.Set("ETag", etag)
  128. }
  129. if config.LastModifiedEnabled && result.LastModified != nil {
  130. lastModified := result.LastModified.Format(http.TimeFormat)
  131. header.Set("Last-Modified", lastModified)
  132. }
  133. if resp := notmodified.Response(req, header); resp != nil {
  134. if result.Body != nil {
  135. result.Body.Close()
  136. }
  137. return resp, nil
  138. }
  139. header.Set("Accept-Ranges", "bytes")
  140. contentLength := int64(0)
  141. if result.ContentLength != nil {
  142. contentLength = *result.ContentLength
  143. header.Set("Content-Length", strconv.FormatInt(*result.ContentLength, 10))
  144. }
  145. if result.ContentType != nil {
  146. header.Set("Content-Type", *result.ContentType)
  147. }
  148. if result.ContentRange != nil {
  149. header.Set("Content-Range", *result.ContentRange)
  150. }
  151. if result.CacheControl != nil {
  152. header.Set("Cache-Control", *result.CacheControl)
  153. }
  154. return &http.Response{
  155. StatusCode: statusCode,
  156. Proto: "HTTP/1.0",
  157. ProtoMajor: 1,
  158. ProtoMinor: 0,
  159. Header: header,
  160. ContentLength: contentLength,
  161. Body: result.Body,
  162. Close: true,
  163. Request: req,
  164. }, nil
  165. }