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