gcs.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. package gcs
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "strconv"
  8. "strings"
  9. "cloud.google.com/go/storage"
  10. "github.com/pkg/errors"
  11. "google.golang.org/api/option"
  12. raw "google.golang.org/api/storage/v1"
  13. htransport "google.golang.org/api/transport/http"
  14. "github.com/imgproxy/imgproxy/v3/config"
  15. "github.com/imgproxy/imgproxy/v3/httprange"
  16. "github.com/imgproxy/imgproxy/v3/ierrors"
  17. defaultTransport "github.com/imgproxy/imgproxy/v3/transport"
  18. "github.com/imgproxy/imgproxy/v3/transport/common"
  19. "github.com/imgproxy/imgproxy/v3/transport/notmodified"
  20. )
  21. // For tests
  22. var noAuth bool = false
  23. type transport struct {
  24. client *storage.Client
  25. }
  26. func buildHTTPClient(opts ...option.ClientOption) (*http.Client, error) {
  27. trans, err := defaultTransport.New(false)
  28. if err != nil {
  29. return nil, err
  30. }
  31. htrans, err := htransport.NewTransport(context.Background(), trans, opts...)
  32. if err != nil {
  33. return nil, errors.Wrap(err, "error creating GCS transport")
  34. }
  35. return &http.Client{Transport: htrans}, nil
  36. }
  37. func New() (http.RoundTripper, error) {
  38. var client *storage.Client
  39. opts := []option.ClientOption{
  40. option.WithScopes(raw.DevstorageReadOnlyScope),
  41. option.WithUserAgent(config.UserAgent),
  42. }
  43. if len(config.GCSKey) > 0 {
  44. opts = append(opts, option.WithCredentialsJSON([]byte(config.GCSKey)))
  45. }
  46. if len(config.GCSEndpoint) > 0 {
  47. opts = append(opts, option.WithEndpoint(config.GCSEndpoint))
  48. }
  49. if noAuth {
  50. opts = append(opts, option.WithoutAuthentication())
  51. }
  52. httpClient, err := buildHTTPClient(opts...)
  53. if err != nil {
  54. return nil, err
  55. }
  56. opts = append(opts, option.WithHTTPClient(httpClient))
  57. client, err = storage.NewClient(context.Background(), opts...)
  58. if err != nil {
  59. return nil, ierrors.Wrap(err, 0, ierrors.WithPrefix("Can't create GCS client"))
  60. }
  61. return transport{client}, nil
  62. }
  63. func (t transport) RoundTrip(req *http.Request) (*http.Response, error) {
  64. bucket, key, query := common.GetBucketAndKey(req.URL)
  65. if len(bucket) == 0 || len(key) == 0 {
  66. body := strings.NewReader("Invalid GCS URL: bucket name or object key is empty")
  67. return &http.Response{
  68. StatusCode: http.StatusNotFound,
  69. Proto: "HTTP/1.0",
  70. ProtoMajor: 1,
  71. ProtoMinor: 0,
  72. Header: http.Header{"Content-Type": {"text/plain"}},
  73. ContentLength: int64(body.Len()),
  74. Body: io.NopCloser(body),
  75. Close: false,
  76. Request: req,
  77. }, nil
  78. }
  79. bkt := t.client.Bucket(bucket)
  80. obj := bkt.Object(key)
  81. if g, err := strconv.ParseInt(query, 10, 64); err == nil && g > 0 {
  82. obj = obj.Generation(g)
  83. }
  84. var (
  85. reader *storage.Reader
  86. statusCode int
  87. size int64
  88. )
  89. header := make(http.Header)
  90. if r := req.Header.Get("Range"); len(r) != 0 {
  91. start, end, err := httprange.Parse(r)
  92. if err != nil {
  93. return httprange.InvalidHTTPRangeResponse(req), nil
  94. }
  95. if end != 0 {
  96. length := end - start + 1
  97. if end < 0 {
  98. length = -1
  99. }
  100. reader, err = obj.NewRangeReader(req.Context(), start, length)
  101. if err != nil {
  102. return nil, err
  103. }
  104. if end < 0 || end >= reader.Attrs.Size {
  105. end = reader.Attrs.Size - 1
  106. }
  107. size = end - reader.Attrs.StartOffset + 1
  108. statusCode = http.StatusPartialContent
  109. header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", reader.Attrs.StartOffset, end, reader.Attrs.Size))
  110. }
  111. }
  112. // We haven't initialize reader yet, this means that we need non-ranged reader
  113. if reader == nil {
  114. if config.ETagEnabled || config.LastModifiedEnabled {
  115. attrs, err := obj.Attrs(req.Context())
  116. if err != nil {
  117. return handleError(req, err)
  118. }
  119. if config.ETagEnabled {
  120. header.Set("ETag", attrs.Etag)
  121. }
  122. if config.LastModifiedEnabled {
  123. header.Set("Last-Modified", attrs.Updated.Format(http.TimeFormat))
  124. }
  125. }
  126. if resp := notmodified.Response(req, header); resp != nil {
  127. return resp, nil
  128. }
  129. var err error
  130. reader, err = obj.NewReader(req.Context())
  131. if err != nil {
  132. return handleError(req, err)
  133. }
  134. statusCode = 200
  135. size = reader.Attrs.Size
  136. }
  137. header.Set("Accept-Ranges", "bytes")
  138. header.Set("Content-Length", strconv.Itoa(int(size)))
  139. header.Set("Content-Type", reader.Attrs.ContentType)
  140. header.Set("Cache-Control", reader.Attrs.CacheControl)
  141. return &http.Response{
  142. StatusCode: statusCode,
  143. Proto: "HTTP/1.0",
  144. ProtoMajor: 1,
  145. ProtoMinor: 0,
  146. Header: header,
  147. ContentLength: reader.Attrs.Size,
  148. Body: reader,
  149. Close: true,
  150. Request: req,
  151. }, nil
  152. }
  153. func handleError(req *http.Request, err error) (*http.Response, error) {
  154. if err != storage.ErrBucketNotExist && err != storage.ErrObjectNotExist {
  155. return nil, err
  156. }
  157. return &http.Response{
  158. StatusCode: http.StatusNotFound,
  159. Proto: "HTTP/1.0",
  160. ProtoMajor: 1,
  161. ProtoMinor: 0,
  162. Header: http.Header{"Content-Type": {"text/plain"}},
  163. ContentLength: int64(len(err.Error())),
  164. Body: io.NopCloser(strings.NewReader(err.Error())),
  165. Close: false,
  166. Request: req,
  167. }, nil
  168. }