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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. // Copyright 2020 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package storage
  5. import (
  6. "context"
  7. "io"
  8. "net/url"
  9. "os"
  10. "path"
  11. "strings"
  12. "time"
  13. "code.gitea.io/gitea/modules/log"
  14. "github.com/minio/minio-go/v7"
  15. "github.com/minio/minio-go/v7/pkg/credentials"
  16. )
  17. var (
  18. _ ObjectStorage = &MinioStorage{}
  19. quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
  20. )
  21. type minioObject struct {
  22. *minio.Object
  23. }
  24. func (m *minioObject) Stat() (os.FileInfo, error) {
  25. oi, err := m.Object.Stat()
  26. if err != nil {
  27. return nil, convertMinioErr(err)
  28. }
  29. return &minioFileInfo{oi}, nil
  30. }
  31. // MinioStorageType is the type descriptor for minio storage
  32. const MinioStorageType Type = "minio"
  33. // MinioStorageConfig represents the configuration for a minio storage
  34. type MinioStorageConfig struct {
  35. Endpoint string `ini:"MINIO_ENDPOINT"`
  36. AccessKeyID string `ini:"MINIO_ACCESS_KEY_ID"`
  37. SecretAccessKey string `ini:"MINIO_SECRET_ACCESS_KEY"`
  38. Bucket string `ini:"MINIO_BUCKET"`
  39. Location string `ini:"MINIO_LOCATION"`
  40. BasePath string `ini:"MINIO_BASE_PATH"`
  41. UseSSL bool `ini:"MINIO_USE_SSL"`
  42. }
  43. // MinioStorage returns a minio bucket storage
  44. type MinioStorage struct {
  45. ctx context.Context
  46. client *minio.Client
  47. bucket string
  48. basePath string
  49. }
  50. func convertMinioErr(err error) error {
  51. if err == nil {
  52. return nil
  53. }
  54. errResp, ok := err.(minio.ErrorResponse)
  55. if !ok {
  56. return err
  57. }
  58. // Convert two responses to standard analogues
  59. switch errResp.Code {
  60. case "NoSuchKey":
  61. return os.ErrNotExist
  62. case "AccessDenied":
  63. return os.ErrPermission
  64. }
  65. return err
  66. }
  67. // NewMinioStorage returns a minio storage
  68. func NewMinioStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error) {
  69. configInterface, err := toConfig(MinioStorageConfig{}, cfg)
  70. if err != nil {
  71. return nil, convertMinioErr(err)
  72. }
  73. config := configInterface.(MinioStorageConfig)
  74. log.Info("Creating Minio storage at %s:%s with base path %s", config.Endpoint, config.Bucket, config.BasePath)
  75. minioClient, err := minio.New(config.Endpoint, &minio.Options{
  76. Creds: credentials.NewStaticV4(config.AccessKeyID, config.SecretAccessKey, ""),
  77. Secure: config.UseSSL,
  78. })
  79. if err != nil {
  80. return nil, convertMinioErr(err)
  81. }
  82. if err := minioClient.MakeBucket(ctx, config.Bucket, minio.MakeBucketOptions{
  83. Region: config.Location,
  84. }); err != nil {
  85. // Check to see if we already own this bucket (which happens if you run this twice)
  86. exists, errBucketExists := minioClient.BucketExists(ctx, config.Bucket)
  87. if !exists || errBucketExists != nil {
  88. return nil, convertMinioErr(err)
  89. }
  90. }
  91. return &MinioStorage{
  92. ctx: ctx,
  93. client: minioClient,
  94. bucket: config.Bucket,
  95. basePath: config.BasePath,
  96. }, nil
  97. }
  98. func (m *MinioStorage) buildMinioPath(p string) string {
  99. return strings.TrimPrefix(path.Join(m.basePath, p), "/")
  100. }
  101. // Open open a file
  102. func (m *MinioStorage) Open(path string) (Object, error) {
  103. var opts = minio.GetObjectOptions{}
  104. object, err := m.client.GetObject(m.ctx, m.bucket, m.buildMinioPath(path), opts)
  105. if err != nil {
  106. return nil, convertMinioErr(err)
  107. }
  108. return &minioObject{object}, nil
  109. }
  110. // Save save a file to minio
  111. func (m *MinioStorage) Save(path string, r io.Reader) (int64, error) {
  112. uploadInfo, err := m.client.PutObject(
  113. m.ctx,
  114. m.bucket,
  115. m.buildMinioPath(path),
  116. r,
  117. -1,
  118. minio.PutObjectOptions{ContentType: "application/octet-stream"},
  119. )
  120. if err != nil {
  121. return 0, convertMinioErr(err)
  122. }
  123. return uploadInfo.Size, nil
  124. }
  125. type minioFileInfo struct {
  126. minio.ObjectInfo
  127. }
  128. func (m minioFileInfo) Name() string {
  129. return m.ObjectInfo.Key
  130. }
  131. func (m minioFileInfo) Size() int64 {
  132. return m.ObjectInfo.Size
  133. }
  134. func (m minioFileInfo) ModTime() time.Time {
  135. return m.LastModified
  136. }
  137. func (m minioFileInfo) IsDir() bool {
  138. return strings.HasSuffix(m.ObjectInfo.Key, "/")
  139. }
  140. func (m minioFileInfo) Mode() os.FileMode {
  141. return os.ModePerm
  142. }
  143. func (m minioFileInfo) Sys() interface{} {
  144. return nil
  145. }
  146. // Stat returns the stat information of the object
  147. func (m *MinioStorage) Stat(path string) (os.FileInfo, error) {
  148. info, err := m.client.StatObject(
  149. m.ctx,
  150. m.bucket,
  151. m.buildMinioPath(path),
  152. minio.StatObjectOptions{},
  153. )
  154. if err != nil {
  155. return nil, convertMinioErr(err)
  156. }
  157. return &minioFileInfo{info}, nil
  158. }
  159. // Delete delete a file
  160. func (m *MinioStorage) Delete(path string) error {
  161. err := m.client.RemoveObject(m.ctx, m.bucket, m.buildMinioPath(path), minio.RemoveObjectOptions{})
  162. return convertMinioErr(err)
  163. }
  164. // URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
  165. func (m *MinioStorage) URL(path, name string) (*url.URL, error) {
  166. reqParams := make(url.Values)
  167. // 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?
  168. reqParams.Set("response-content-disposition", "attachment; filename=\""+quoteEscaper.Replace(name)+"\"")
  169. u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), 5*time.Minute, reqParams)
  170. return u, convertMinioErr(err)
  171. }
  172. // IterateObjects iterates across the objects in the miniostorage
  173. func (m *MinioStorage) IterateObjects(fn func(path string, obj Object) error) error {
  174. var opts = minio.GetObjectOptions{}
  175. lobjectCtx, cancel := context.WithCancel(m.ctx)
  176. defer cancel()
  177. for mObjInfo := range m.client.ListObjects(lobjectCtx, m.bucket, minio.ListObjectsOptions{
  178. Prefix: m.basePath,
  179. Recursive: true,
  180. }) {
  181. object, err := m.client.GetObject(lobjectCtx, m.bucket, mObjInfo.Key, opts)
  182. if err != nil {
  183. return convertMinioErr(err)
  184. }
  185. if err := func(object *minio.Object, fn func(path string, obj Object) error) error {
  186. defer object.Close()
  187. return fn(strings.TrimPrefix(m.basePath, mObjInfo.Key), &minioObject{object})
  188. }(object, fn); err != nil {
  189. return convertMinioErr(err)
  190. }
  191. }
  192. return nil
  193. }
  194. func init() {
  195. RegisterStorageType(MinioStorageType, NewMinioStorage)
  196. }