You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

minio.go 7.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. // Copyright 2020 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package storage
  4. import (
  5. "context"
  6. "crypto/tls"
  7. "fmt"
  8. "io"
  9. "net/http"
  10. "net/url"
  11. "os"
  12. "path"
  13. "strings"
  14. "time"
  15. "code.gitea.io/gitea/modules/log"
  16. "code.gitea.io/gitea/modules/setting"
  17. "code.gitea.io/gitea/modules/util"
  18. "github.com/minio/minio-go/v7"
  19. "github.com/minio/minio-go/v7/pkg/credentials"
  20. )
  21. var (
  22. _ ObjectStorage = &MinioStorage{}
  23. quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
  24. )
  25. type minioObject struct {
  26. *minio.Object
  27. }
  28. func (m *minioObject) Stat() (os.FileInfo, error) {
  29. oi, err := m.Object.Stat()
  30. if err != nil {
  31. return nil, convertMinioErr(err)
  32. }
  33. return &minioFileInfo{oi}, nil
  34. }
  35. // MinioStorage returns a minio bucket storage
  36. type MinioStorage struct {
  37. cfg *setting.MinioStorageConfig
  38. ctx context.Context
  39. client *minio.Client
  40. bucket string
  41. basePath string
  42. }
  43. func convertMinioErr(err error) error {
  44. if err == nil {
  45. return nil
  46. }
  47. errResp, ok := err.(minio.ErrorResponse)
  48. if !ok {
  49. return err
  50. }
  51. // Convert two responses to standard analogues
  52. switch errResp.Code {
  53. case "NoSuchKey":
  54. return os.ErrNotExist
  55. case "AccessDenied":
  56. return os.ErrPermission
  57. }
  58. return err
  59. }
  60. var getBucketVersioning = func(ctx context.Context, minioClient *minio.Client, bucket string) error {
  61. _, err := minioClient.GetBucketVersioning(ctx, bucket)
  62. return err
  63. }
  64. // NewMinioStorage returns a minio storage
  65. func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage, error) {
  66. config := cfg.MinioConfig
  67. if config.ChecksumAlgorithm != "" && config.ChecksumAlgorithm != "default" && config.ChecksumAlgorithm != "md5" {
  68. return nil, fmt.Errorf("invalid minio checksum algorithm: %s", config.ChecksumAlgorithm)
  69. }
  70. log.Info("Creating Minio storage at %s:%s with base path %s", config.Endpoint, config.Bucket, config.BasePath)
  71. minioClient, err := minio.New(config.Endpoint, &minio.Options{
  72. Creds: credentials.NewStaticV4(config.AccessKeyID, config.SecretAccessKey, ""),
  73. Secure: config.UseSSL,
  74. Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}},
  75. Region: config.Location,
  76. })
  77. if err != nil {
  78. return nil, convertMinioErr(err)
  79. }
  80. // The GetBucketVersioning is only used for checking whether the Object Storage parameters are generally good. It doesn't need to succeed.
  81. // The assumption is that if the API returns the HTTP code 400, then the parameters could be incorrect.
  82. // Otherwise even if the request itself fails (403, 404, etc), the code should still continue because the parameters seem "good" enough.
  83. // Keep in mind that GetBucketVersioning requires "owner" to really succeed, so it can't be used to check the existence.
  84. // Not using "BucketExists (HeadBucket)" because it doesn't include detailed failure reasons.
  85. err = getBucketVersioning(ctx, minioClient, config.Bucket)
  86. if err != nil {
  87. errResp, ok := err.(minio.ErrorResponse)
  88. if !ok {
  89. return nil, err
  90. }
  91. if errResp.StatusCode == http.StatusBadRequest {
  92. log.Error("S3 storage connection failure at %s:%s with base path %s and region: %s", config.Endpoint, config.Bucket, config.Location, errResp.Message)
  93. return nil, err
  94. }
  95. }
  96. // Check to see if we already own this bucket
  97. exists, err := minioClient.BucketExists(ctx, config.Bucket)
  98. if err != nil {
  99. return nil, convertMinioErr(err)
  100. }
  101. if !exists {
  102. if err := minioClient.MakeBucket(ctx, config.Bucket, minio.MakeBucketOptions{
  103. Region: config.Location,
  104. }); err != nil {
  105. return nil, convertMinioErr(err)
  106. }
  107. }
  108. return &MinioStorage{
  109. cfg: &config,
  110. ctx: ctx,
  111. client: minioClient,
  112. bucket: config.Bucket,
  113. basePath: config.BasePath,
  114. }, nil
  115. }
  116. func (m *MinioStorage) buildMinioPath(p string) string {
  117. p = strings.TrimPrefix(util.PathJoinRelX(m.basePath, p), "/") // object store doesn't use slash for root path
  118. if p == "." {
  119. p = "" // object store doesn't use dot as relative path
  120. }
  121. return p
  122. }
  123. func (m *MinioStorage) buildMinioDirPrefix(p string) string {
  124. // ending slash is required for avoiding matching like "foo/" and "foobar/" with prefix "foo"
  125. p = m.buildMinioPath(p) + "/"
  126. if p == "/" {
  127. p = "" // object store doesn't use slash for root path
  128. }
  129. return p
  130. }
  131. // Open opens a file
  132. func (m *MinioStorage) Open(path string) (Object, error) {
  133. opts := minio.GetObjectOptions{}
  134. object, err := m.client.GetObject(m.ctx, m.bucket, m.buildMinioPath(path), opts)
  135. if err != nil {
  136. return nil, convertMinioErr(err)
  137. }
  138. return &minioObject{object}, nil
  139. }
  140. // Save saves a file to minio
  141. func (m *MinioStorage) Save(path string, r io.Reader, size int64) (int64, error) {
  142. uploadInfo, err := m.client.PutObject(
  143. m.ctx,
  144. m.bucket,
  145. m.buildMinioPath(path),
  146. r,
  147. size,
  148. minio.PutObjectOptions{
  149. ContentType: "application/octet-stream",
  150. // some storages like:
  151. // * https://developers.cloudflare.com/r2/api/s3/api/
  152. // * https://www.backblaze.com/b2/docs/s3_compatible_api.html
  153. // do not support "x-amz-checksum-algorithm" header, so use legacy MD5 checksum
  154. SendContentMd5: m.cfg.ChecksumAlgorithm == "md5",
  155. },
  156. )
  157. if err != nil {
  158. return 0, convertMinioErr(err)
  159. }
  160. return uploadInfo.Size, nil
  161. }
  162. type minioFileInfo struct {
  163. minio.ObjectInfo
  164. }
  165. func (m minioFileInfo) Name() string {
  166. return path.Base(m.ObjectInfo.Key)
  167. }
  168. func (m minioFileInfo) Size() int64 {
  169. return m.ObjectInfo.Size
  170. }
  171. func (m minioFileInfo) ModTime() time.Time {
  172. return m.LastModified
  173. }
  174. func (m minioFileInfo) IsDir() bool {
  175. return strings.HasSuffix(m.ObjectInfo.Key, "/")
  176. }
  177. func (m minioFileInfo) Mode() os.FileMode {
  178. return os.ModePerm
  179. }
  180. func (m minioFileInfo) Sys() any {
  181. return nil
  182. }
  183. // Stat returns the stat information of the object
  184. func (m *MinioStorage) Stat(path string) (os.FileInfo, error) {
  185. info, err := m.client.StatObject(
  186. m.ctx,
  187. m.bucket,
  188. m.buildMinioPath(path),
  189. minio.StatObjectOptions{},
  190. )
  191. if err != nil {
  192. return nil, convertMinioErr(err)
  193. }
  194. return &minioFileInfo{info}, nil
  195. }
  196. // Delete delete a file
  197. func (m *MinioStorage) Delete(path string) error {
  198. err := m.client.RemoveObject(m.ctx, m.bucket, m.buildMinioPath(path), minio.RemoveObjectOptions{})
  199. return convertMinioErr(err)
  200. }
  201. // URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
  202. func (m *MinioStorage) URL(path, name string) (*url.URL, error) {
  203. reqParams := make(url.Values)
  204. // TODO it may be good to embed images with 'inline' like ServeData does, but we don't want to have to read the file, do we?
  205. reqParams.Set("response-content-disposition", "attachment; filename=\""+quoteEscaper.Replace(name)+"\"")
  206. u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), 5*time.Minute, reqParams)
  207. return u, convertMinioErr(err)
  208. }
  209. // IterateObjects iterates across the objects in the miniostorage
  210. func (m *MinioStorage) IterateObjects(dirName string, fn func(path string, obj Object) error) error {
  211. opts := minio.GetObjectOptions{}
  212. for mObjInfo := range m.client.ListObjects(m.ctx, m.bucket, minio.ListObjectsOptions{
  213. Prefix: m.buildMinioDirPrefix(dirName),
  214. Recursive: true,
  215. }) {
  216. object, err := m.client.GetObject(m.ctx, m.bucket, mObjInfo.Key, opts)
  217. if err != nil {
  218. return convertMinioErr(err)
  219. }
  220. if err := func(object *minio.Object, fn func(path string, obj Object) error) error {
  221. defer object.Close()
  222. return fn(strings.TrimPrefix(mObjInfo.Key, m.basePath), &minioObject{object})
  223. }(object, fn); err != nil {
  224. return convertMinioErr(err)
  225. }
  226. }
  227. return nil
  228. }
  229. func init() {
  230. RegisterStorageType(setting.MinioStorageType, NewMinioStorage)
  231. }