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 6.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  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. // NewMinioStorage returns a minio storage
  61. func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage, error) {
  62. config := cfg.MinioConfig
  63. if config.ChecksumAlgorithm != "" && config.ChecksumAlgorithm != "default" && config.ChecksumAlgorithm != "md5" {
  64. return nil, fmt.Errorf("invalid minio checksum algorithm: %s", config.ChecksumAlgorithm)
  65. }
  66. log.Info("Creating Minio storage at %s:%s with base path %s", config.Endpoint, config.Bucket, config.BasePath)
  67. minioClient, err := minio.New(config.Endpoint, &minio.Options{
  68. Creds: credentials.NewStaticV4(config.AccessKeyID, config.SecretAccessKey, ""),
  69. Secure: config.UseSSL,
  70. Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}},
  71. })
  72. if err != nil {
  73. return nil, convertMinioErr(err)
  74. }
  75. if err := minioClient.MakeBucket(ctx, config.Bucket, minio.MakeBucketOptions{
  76. Region: config.Location,
  77. }); err != nil {
  78. // Check to see if we already own this bucket (which happens if you run this twice)
  79. exists, errBucketExists := minioClient.BucketExists(ctx, config.Bucket)
  80. if !exists || errBucketExists != nil {
  81. return nil, convertMinioErr(err)
  82. }
  83. }
  84. return &MinioStorage{
  85. cfg: &config,
  86. ctx: ctx,
  87. client: minioClient,
  88. bucket: config.Bucket,
  89. basePath: config.BasePath,
  90. }, nil
  91. }
  92. func (m *MinioStorage) buildMinioPath(p string) string {
  93. p = util.PathJoinRelX(m.basePath, p)
  94. if p == "." {
  95. p = "" // minio doesn't use dot as relative path
  96. }
  97. return p
  98. }
  99. // Open opens a file
  100. func (m *MinioStorage) Open(path string) (Object, error) {
  101. opts := minio.GetObjectOptions{}
  102. object, err := m.client.GetObject(m.ctx, m.bucket, m.buildMinioPath(path), opts)
  103. if err != nil {
  104. return nil, convertMinioErr(err)
  105. }
  106. return &minioObject{object}, nil
  107. }
  108. // Save saves a file to minio
  109. func (m *MinioStorage) Save(path string, r io.Reader, size int64) (int64, error) {
  110. uploadInfo, err := m.client.PutObject(
  111. m.ctx,
  112. m.bucket,
  113. m.buildMinioPath(path),
  114. r,
  115. size,
  116. minio.PutObjectOptions{
  117. ContentType: "application/octet-stream",
  118. // some storages like:
  119. // * https://developers.cloudflare.com/r2/api/s3/api/
  120. // * https://www.backblaze.com/b2/docs/s3_compatible_api.html
  121. // do not support "x-amz-checksum-algorithm" header, so use legacy MD5 checksum
  122. SendContentMd5: m.cfg.ChecksumAlgorithm == "md5",
  123. },
  124. )
  125. if err != nil {
  126. return 0, convertMinioErr(err)
  127. }
  128. return uploadInfo.Size, nil
  129. }
  130. type minioFileInfo struct {
  131. minio.ObjectInfo
  132. }
  133. func (m minioFileInfo) Name() string {
  134. return path.Base(m.ObjectInfo.Key)
  135. }
  136. func (m minioFileInfo) Size() int64 {
  137. return m.ObjectInfo.Size
  138. }
  139. func (m minioFileInfo) ModTime() time.Time {
  140. return m.LastModified
  141. }
  142. func (m minioFileInfo) IsDir() bool {
  143. return strings.HasSuffix(m.ObjectInfo.Key, "/")
  144. }
  145. func (m minioFileInfo) Mode() os.FileMode {
  146. return os.ModePerm
  147. }
  148. func (m minioFileInfo) Sys() any {
  149. return nil
  150. }
  151. // Stat returns the stat information of the object
  152. func (m *MinioStorage) Stat(path string) (os.FileInfo, error) {
  153. info, err := m.client.StatObject(
  154. m.ctx,
  155. m.bucket,
  156. m.buildMinioPath(path),
  157. minio.StatObjectOptions{},
  158. )
  159. if err != nil {
  160. return nil, convertMinioErr(err)
  161. }
  162. return &minioFileInfo{info}, nil
  163. }
  164. // Delete delete a file
  165. func (m *MinioStorage) Delete(path string) error {
  166. err := m.client.RemoveObject(m.ctx, m.bucket, m.buildMinioPath(path), minio.RemoveObjectOptions{})
  167. return convertMinioErr(err)
  168. }
  169. // URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
  170. func (m *MinioStorage) URL(path, name string) (*url.URL, error) {
  171. reqParams := make(url.Values)
  172. // 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?
  173. reqParams.Set("response-content-disposition", "attachment; filename=\""+quoteEscaper.Replace(name)+"\"")
  174. u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), 5*time.Minute, reqParams)
  175. return u, convertMinioErr(err)
  176. }
  177. // IterateObjects iterates across the objects in the miniostorage
  178. func (m *MinioStorage) IterateObjects(dirName string, fn func(path string, obj Object) error) error {
  179. opts := minio.GetObjectOptions{}
  180. lobjectCtx, cancel := context.WithCancel(m.ctx)
  181. defer cancel()
  182. basePath := m.basePath
  183. if dirName != "" {
  184. // ending slash is required for avoiding matching like "foo/" and "foobar/" with prefix "foo"
  185. basePath = m.buildMinioPath(dirName) + "/"
  186. }
  187. for mObjInfo := range m.client.ListObjects(lobjectCtx, m.bucket, minio.ListObjectsOptions{
  188. Prefix: basePath,
  189. Recursive: true,
  190. }) {
  191. object, err := m.client.GetObject(lobjectCtx, m.bucket, mObjInfo.Key, opts)
  192. if err != nil {
  193. return convertMinioErr(err)
  194. }
  195. if err := func(object *minio.Object, fn func(path string, obj Object) error) error {
  196. defer object.Close()
  197. return fn(strings.TrimPrefix(mObjInfo.Key, m.basePath), &minioObject{object})
  198. }(object, fn); err != nil {
  199. return convertMinioErr(err)
  200. }
  201. }
  202. return nil
  203. }
  204. func init() {
  205. RegisterStorageType(setting.MinioStorageType, NewMinioStorage)
  206. }