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