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