gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  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. var lookup minio.BucketLookupType
  72. switch config.BucketLookUpType {
  73. case "auto", "":
  74. lookup = minio.BucketLookupAuto
  75. case "dns":
  76. lookup = minio.BucketLookupDNS
  77. case "path":
  78. lookup = minio.BucketLookupPath
  79. default:
  80. return nil, fmt.Errorf("invalid minio bucket lookup type: %s", config.BucketLookUpType)
  81. }
  82. minioClient, err := minio.New(config.Endpoint, &minio.Options{
  83. Creds: buildMinioCredentials(config),
  84. Secure: config.UseSSL,
  85. Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}},
  86. Region: config.Location,
  87. BucketLookup: lookup,
  88. })
  89. if err != nil {
  90. return nil, convertMinioErr(err)
  91. }
  92. // The GetBucketVersioning is only used for checking whether the Object Storage parameters are generally good. It doesn't need to succeed.
  93. // The assumption is that if the API returns the HTTP code 400, then the parameters could be incorrect.
  94. // Otherwise even if the request itself fails (403, 404, etc), the code should still continue because the parameters seem "good" enough.
  95. // Keep in mind that GetBucketVersioning requires "owner" to really succeed, so it can't be used to check the existence.
  96. // Not using "BucketExists (HeadBucket)" because it doesn't include detailed failure reasons.
  97. err = getBucketVersioning(ctx, minioClient, config.Bucket)
  98. if err != nil {
  99. errResp, ok := err.(minio.ErrorResponse)
  100. if !ok {
  101. return nil, err
  102. }
  103. if errResp.StatusCode == http.StatusBadRequest {
  104. log.Error("S3 storage connection failure at %s:%s with base path %s and region: %s", config.Endpoint, config.Bucket, config.Location, errResp.Message)
  105. return nil, err
  106. }
  107. }
  108. // Check to see if we already own this bucket
  109. exists, err := minioClient.BucketExists(ctx, config.Bucket)
  110. if err != nil {
  111. return nil, convertMinioErr(err)
  112. }
  113. if !exists {
  114. if err := minioClient.MakeBucket(ctx, config.Bucket, minio.MakeBucketOptions{
  115. Region: config.Location,
  116. }); err != nil {
  117. return nil, convertMinioErr(err)
  118. }
  119. }
  120. return &MinioStorage{
  121. cfg: &config,
  122. ctx: ctx,
  123. client: minioClient,
  124. bucket: config.Bucket,
  125. basePath: config.BasePath,
  126. }, nil
  127. }
  128. func (m *MinioStorage) buildMinioPath(p string) string {
  129. p = strings.TrimPrefix(util.PathJoinRelX(m.basePath, p), "/") // object store doesn't use slash for root path
  130. if p == "." {
  131. p = "" // object store doesn't use dot as relative path
  132. }
  133. return p
  134. }
  135. func (m *MinioStorage) buildMinioDirPrefix(p string) string {
  136. // ending slash is required for avoiding matching like "foo/" and "foobar/" with prefix "foo"
  137. p = m.buildMinioPath(p) + "/"
  138. if p == "/" {
  139. p = "" // object store doesn't use slash for root path
  140. }
  141. return p
  142. }
  143. func buildMinioCredentials(config setting.MinioStorageConfig) *credentials.Credentials {
  144. // If static credentials are provided, use those
  145. if config.AccessKeyID != "" {
  146. return credentials.NewStaticV4(config.AccessKeyID, config.SecretAccessKey, "")
  147. }
  148. // Otherwise, fallback to a credentials chain for S3 access
  149. chain := []credentials.Provider{
  150. // configure based upon MINIO_ prefixed environment variables
  151. &credentials.EnvMinio{},
  152. // configure based upon AWS_ prefixed environment variables
  153. &credentials.EnvAWS{},
  154. // read credentials from MINIO_SHARED_CREDENTIALS_FILE
  155. // environment variable, or default json config files
  156. &credentials.FileMinioClient{},
  157. // read credentials from AWS_SHARED_CREDENTIALS_FILE
  158. // environment variable, or default credentials file
  159. &credentials.FileAWSCredentials{},
  160. // read IAM role from EC2 metadata endpoint if available
  161. &credentials.IAM{
  162. // passing in an empty Endpoint lets the IAM Provider
  163. // decide which endpoint to resolve internally
  164. Endpoint: config.IamEndpoint,
  165. Client: &http.Client{
  166. Transport: http.DefaultTransport,
  167. },
  168. },
  169. }
  170. return credentials.NewChainCredentials(chain)
  171. }
  172. // Open opens a file
  173. func (m *MinioStorage) Open(path string) (Object, error) {
  174. opts := minio.GetObjectOptions{}
  175. object, err := m.client.GetObject(m.ctx, m.bucket, m.buildMinioPath(path), opts)
  176. if err != nil {
  177. return nil, convertMinioErr(err)
  178. }
  179. return &minioObject{object}, nil
  180. }
  181. // Save saves a file to minio
  182. func (m *MinioStorage) Save(path string, r io.Reader, size int64) (int64, error) {
  183. uploadInfo, err := m.client.PutObject(
  184. m.ctx,
  185. m.bucket,
  186. m.buildMinioPath(path),
  187. r,
  188. size,
  189. minio.PutObjectOptions{
  190. ContentType: "application/octet-stream",
  191. // some storages like:
  192. // * https://developers.cloudflare.com/r2/api/s3/api/
  193. // * https://www.backblaze.com/b2/docs/s3_compatible_api.html
  194. // do not support "x-amz-checksum-algorithm" header, so use legacy MD5 checksum
  195. SendContentMd5: m.cfg.ChecksumAlgorithm == "md5",
  196. },
  197. )
  198. if err != nil {
  199. return 0, convertMinioErr(err)
  200. }
  201. return uploadInfo.Size, nil
  202. }
  203. type minioFileInfo struct {
  204. minio.ObjectInfo
  205. }
  206. func (m minioFileInfo) Name() string {
  207. return path.Base(m.ObjectInfo.Key)
  208. }
  209. func (m minioFileInfo) Size() int64 {
  210. return m.ObjectInfo.Size
  211. }
  212. func (m minioFileInfo) ModTime() time.Time {
  213. return m.LastModified
  214. }
  215. func (m minioFileInfo) IsDir() bool {
  216. return strings.HasSuffix(m.ObjectInfo.Key, "/")
  217. }
  218. func (m minioFileInfo) Mode() os.FileMode {
  219. return os.ModePerm
  220. }
  221. func (m minioFileInfo) Sys() any {
  222. return nil
  223. }
  224. // Stat returns the stat information of the object
  225. func (m *MinioStorage) Stat(path string) (os.FileInfo, error) {
  226. info, err := m.client.StatObject(
  227. m.ctx,
  228. m.bucket,
  229. m.buildMinioPath(path),
  230. minio.StatObjectOptions{},
  231. )
  232. if err != nil {
  233. return nil, convertMinioErr(err)
  234. }
  235. return &minioFileInfo{info}, nil
  236. }
  237. // Delete delete a file
  238. func (m *MinioStorage) Delete(path string) error {
  239. err := m.client.RemoveObject(m.ctx, m.bucket, m.buildMinioPath(path), minio.RemoveObjectOptions{})
  240. return convertMinioErr(err)
  241. }
  242. // URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
  243. func (m *MinioStorage) URL(path, name, method string, serveDirectReqParams url.Values) (*url.URL, error) {
  244. // copy serveDirectReqParams
  245. reqParams, err := url.ParseQuery(serveDirectReqParams.Encode())
  246. if err != nil {
  247. return nil, err
  248. }
  249. // 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?
  250. reqParams.Set("response-content-disposition", "attachment; filename=\""+quoteEscaper.Replace(name)+"\"")
  251. expires := 5 * time.Minute
  252. if method == http.MethodHead {
  253. u, err := m.client.PresignedHeadObject(m.ctx, m.bucket, m.buildMinioPath(path), expires, reqParams)
  254. return u, convertMinioErr(err)
  255. }
  256. u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), expires, reqParams)
  257. return u, convertMinioErr(err)
  258. }
  259. // IterateObjects iterates across the objects in the miniostorage
  260. func (m *MinioStorage) IterateObjects(dirName string, fn func(path string, obj Object) error) error {
  261. opts := minio.GetObjectOptions{}
  262. for mObjInfo := range m.client.ListObjects(m.ctx, m.bucket, minio.ListObjectsOptions{
  263. Prefix: m.buildMinioDirPrefix(dirName),
  264. Recursive: true,
  265. }) {
  266. object, err := m.client.GetObject(m.ctx, m.bucket, mObjInfo.Key, opts)
  267. if err != nil {
  268. return convertMinioErr(err)
  269. }
  270. if err := func(object *minio.Object, fn func(path string, obj Object) error) error {
  271. defer object.Close()
  272. return fn(strings.TrimPrefix(mObjInfo.Key, m.basePath), &minioObject{object})
  273. }(object, fn); err != nil {
  274. return convertMinioErr(err)
  275. }
  276. }
  277. return nil
  278. }
  279. func init() {
  280. RegisterStorageType(setting.MinioStorageType, NewMinioStorage)
  281. }