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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  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. "github.com/minio/minio-go/v7"
  17. "github.com/minio/minio-go/v7/pkg/credentials"
  18. )
  19. var (
  20. _ ObjectStorage = &MinioStorage{}
  21. quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
  22. )
  23. type minioObject struct {
  24. *minio.Object
  25. }
  26. func (m *minioObject) Stat() (os.FileInfo, error) {
  27. oi, err := m.Object.Stat()
  28. if err != nil {
  29. return nil, convertMinioErr(err)
  30. }
  31. return &minioFileInfo{oi}, nil
  32. }
  33. // MinioStorageType is the type descriptor for minio storage
  34. const MinioStorageType Type = "minio"
  35. // MinioStorageConfig represents the configuration for a minio storage
  36. type MinioStorageConfig struct {
  37. Endpoint string `ini:"MINIO_ENDPOINT"`
  38. AccessKeyID string `ini:"MINIO_ACCESS_KEY_ID"`
  39. SecretAccessKey string `ini:"MINIO_SECRET_ACCESS_KEY"`
  40. Bucket string `ini:"MINIO_BUCKET"`
  41. Location string `ini:"MINIO_LOCATION"`
  42. BasePath string `ini:"MINIO_BASE_PATH"`
  43. UseSSL bool `ini:"MINIO_USE_SSL"`
  44. InsecureSkipVerify bool `ini:"MINIO_INSECURE_SKIP_VERIFY"`
  45. ChecksumAlgorithm string `ini:"MINIO_CHECKSUM_ALGORITHM"`
  46. }
  47. // MinioStorage returns a minio bucket storage
  48. type MinioStorage struct {
  49. cfg *MinioStorageConfig
  50. ctx context.Context
  51. client *minio.Client
  52. bucket string
  53. basePath string
  54. }
  55. func convertMinioErr(err error) error {
  56. if err == nil {
  57. return nil
  58. }
  59. errResp, ok := err.(minio.ErrorResponse)
  60. if !ok {
  61. return err
  62. }
  63. // Convert two responses to standard analogues
  64. switch errResp.Code {
  65. case "NoSuchKey":
  66. return os.ErrNotExist
  67. case "AccessDenied":
  68. return os.ErrPermission
  69. }
  70. return err
  71. }
  72. // NewMinioStorage returns a minio storage
  73. func NewMinioStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error) {
  74. configInterface, err := toConfig(MinioStorageConfig{}, cfg)
  75. if err != nil {
  76. return nil, convertMinioErr(err)
  77. }
  78. config := configInterface.(MinioStorageConfig)
  79. if config.ChecksumAlgorithm != "" && config.ChecksumAlgorithm != "default" && config.ChecksumAlgorithm != "md5" {
  80. return nil, fmt.Errorf("invalid minio checksum algorithm: %s", config.ChecksumAlgorithm)
  81. }
  82. log.Info("Creating Minio storage at %s:%s with base path %s", config.Endpoint, config.Bucket, config.BasePath)
  83. minioClient, err := minio.New(config.Endpoint, &minio.Options{
  84. Creds: credentials.NewStaticV4(config.AccessKeyID, config.SecretAccessKey, ""),
  85. Secure: config.UseSSL,
  86. Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}},
  87. })
  88. if err != nil {
  89. return nil, convertMinioErr(err)
  90. }
  91. if err := minioClient.MakeBucket(ctx, config.Bucket, minio.MakeBucketOptions{
  92. Region: config.Location,
  93. }); err != nil {
  94. // Check to see if we already own this bucket (which happens if you run this twice)
  95. exists, errBucketExists := minioClient.BucketExists(ctx, config.Bucket)
  96. if !exists || errBucketExists != nil {
  97. return nil, convertMinioErr(err)
  98. }
  99. }
  100. return &MinioStorage{
  101. cfg: &config,
  102. ctx: ctx,
  103. client: minioClient,
  104. bucket: config.Bucket,
  105. basePath: config.BasePath,
  106. }, nil
  107. }
  108. func (m *MinioStorage) buildMinioPath(p string) string {
  109. return strings.TrimPrefix(path.Join(m.basePath, path.Clean("/" + strings.ReplaceAll(p, "\\", "/"))[1:]), "/")
  110. }
  111. // Open opens a file
  112. func (m *MinioStorage) Open(path string) (Object, error) {
  113. opts := minio.GetObjectOptions{}
  114. object, err := m.client.GetObject(m.ctx, m.bucket, m.buildMinioPath(path), opts)
  115. if err != nil {
  116. return nil, convertMinioErr(err)
  117. }
  118. return &minioObject{object}, nil
  119. }
  120. // Save saves a file to minio
  121. func (m *MinioStorage) Save(path string, r io.Reader, size int64) (int64, error) {
  122. uploadInfo, err := m.client.PutObject(
  123. m.ctx,
  124. m.bucket,
  125. m.buildMinioPath(path),
  126. r,
  127. size,
  128. minio.PutObjectOptions{
  129. ContentType: "application/octet-stream",
  130. // some storages like:
  131. // * https://developers.cloudflare.com/r2/api/s3/api/
  132. // * https://www.backblaze.com/b2/docs/s3_compatible_api.html
  133. // do not support "x-amz-checksum-algorithm" header, so use legacy MD5 checksum
  134. SendContentMd5: m.cfg.ChecksumAlgorithm == "md5",
  135. },
  136. )
  137. if err != nil {
  138. return 0, convertMinioErr(err)
  139. }
  140. return uploadInfo.Size, nil
  141. }
  142. type minioFileInfo struct {
  143. minio.ObjectInfo
  144. }
  145. func (m minioFileInfo) Name() string {
  146. return path.Base(m.ObjectInfo.Key)
  147. }
  148. func (m minioFileInfo) Size() int64 {
  149. return m.ObjectInfo.Size
  150. }
  151. func (m minioFileInfo) ModTime() time.Time {
  152. return m.LastModified
  153. }
  154. func (m minioFileInfo) IsDir() bool {
  155. return strings.HasSuffix(m.ObjectInfo.Key, "/")
  156. }
  157. func (m minioFileInfo) Mode() os.FileMode {
  158. return os.ModePerm
  159. }
  160. func (m minioFileInfo) Sys() interface{} {
  161. return nil
  162. }
  163. // Stat returns the stat information of the object
  164. func (m *MinioStorage) Stat(path string) (os.FileInfo, error) {
  165. info, err := m.client.StatObject(
  166. m.ctx,
  167. m.bucket,
  168. m.buildMinioPath(path),
  169. minio.StatObjectOptions{},
  170. )
  171. if err != nil {
  172. return nil, convertMinioErr(err)
  173. }
  174. return &minioFileInfo{info}, nil
  175. }
  176. // Delete delete a file
  177. func (m *MinioStorage) Delete(path string) error {
  178. err := m.client.RemoveObject(m.ctx, m.bucket, m.buildMinioPath(path), minio.RemoveObjectOptions{})
  179. return convertMinioErr(err)
  180. }
  181. // URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
  182. func (m *MinioStorage) URL(path, name string) (*url.URL, error) {
  183. reqParams := make(url.Values)
  184. // 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?
  185. reqParams.Set("response-content-disposition", "attachment; filename=\""+quoteEscaper.Replace(name)+"\"")
  186. u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), 5*time.Minute, reqParams)
  187. return u, convertMinioErr(err)
  188. }
  189. // IterateObjects iterates across the objects in the miniostorage
  190. func (m *MinioStorage) IterateObjects(fn func(path string, obj Object) error) error {
  191. opts := minio.GetObjectOptions{}
  192. lobjectCtx, cancel := context.WithCancel(m.ctx)
  193. defer cancel()
  194. for mObjInfo := range m.client.ListObjects(lobjectCtx, m.bucket, minio.ListObjectsOptions{
  195. Prefix: m.basePath,
  196. Recursive: true,
  197. }) {
  198. object, err := m.client.GetObject(lobjectCtx, m.bucket, mObjInfo.Key, opts)
  199. if err != nil {
  200. return convertMinioErr(err)
  201. }
  202. if err := func(object *minio.Object, fn func(path string, obj Object) error) error {
  203. defer object.Close()
  204. return fn(strings.TrimPrefix(mObjInfo.Key, m.basePath), &minioObject{object})
  205. }(object, fn); err != nil {
  206. return convertMinioErr(err)
  207. }
  208. }
  209. return nil
  210. }
  211. func init() {
  212. RegisterStorageType(MinioStorageType, NewMinioStorage)
  213. }