github.com/openimsdk/tools@v0.0.49/s3/minio/minio.go (about) 1 // Copyright © 2023 OpenIM. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package minio 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "io" 22 "net/http" 23 "net/url" 24 "path" 25 "path/filepath" 26 "reflect" 27 "strconv" 28 "strings" 29 "sync" 30 "time" 31 "unsafe" 32 33 "github.com/minio/minio-go/v7" 34 "github.com/openimsdk/tools/s3" 35 36 "github.com/minio/minio-go/v7/pkg/credentials" 37 "github.com/minio/minio-go/v7/pkg/signer" 38 "github.com/openimsdk/tools/errs" 39 "github.com/openimsdk/tools/log" 40 ) 41 42 const ( 43 unsignedPayload = "UNSIGNED-PAYLOAD" 44 ) 45 46 const ( 47 minPartSize int64 = 1024 * 1024 * 5 // 5MB 48 maxPartSize int64 = 1024 * 1024 * 1024 * 5 // 5GB 49 maxNumSize int64 = 10000 50 ) 51 52 const ( 53 maxImageWidth = 1024 54 maxImageHeight = 1024 55 maxImageSize = 1024 * 1024 * 50 56 imageThumbnailPath = "openim/thumbnail" 57 ) 58 59 const successCode = http.StatusOK 60 61 var _ s3.Interface = (*Minio)(nil) 62 63 type Config struct { 64 Bucket string 65 Endpoint string 66 AccessKeyID string 67 SecretAccessKey string 68 SessionToken string 69 SignEndpoint string 70 PublicRead bool 71 } 72 73 func NewMinio(ctx context.Context, cache Cache, conf Config) (*Minio, error) { 74 u, err := url.Parse(conf.Endpoint) 75 if err != nil { 76 return nil, err 77 } 78 opts := &minio.Options{ 79 Creds: credentials.NewStaticV4(conf.AccessKeyID, conf.SecretAccessKey, conf.SessionToken), 80 Secure: u.Scheme == "https", 81 } 82 client, err := minio.New(u.Host, opts) 83 if err != nil { 84 return nil, err 85 } 86 m := &Minio{ 87 conf: conf, 88 bucket: conf.Bucket, 89 core: &minio.Core{Client: client}, 90 lock: &sync.Mutex{}, 91 init: false, 92 cache: cache, 93 } 94 if conf.SignEndpoint == "" || conf.SignEndpoint == conf.Endpoint { 95 m.opts = opts 96 m.sign = m.core.Client 97 m.prefix = u.Path 98 u.Path = "" 99 conf.Endpoint = u.String() 100 m.signEndpoint = conf.Endpoint 101 } else { 102 su, err := url.Parse(conf.SignEndpoint) 103 if err != nil { 104 return nil, err 105 } 106 m.opts = &minio.Options{ 107 Creds: credentials.NewStaticV4(conf.AccessKeyID, conf.SecretAccessKey, conf.SessionToken), 108 Secure: su.Scheme == "https", 109 } 110 m.sign, err = minio.New(su.Host, m.opts) 111 if err != nil { 112 return nil, err 113 } 114 m.prefix = su.Path 115 su.Path = "" 116 conf.SignEndpoint = su.String() 117 m.signEndpoint = conf.SignEndpoint 118 } 119 if err := m.initMinio(ctx); err != nil { 120 return nil, err 121 } 122 return m, nil 123 } 124 125 type Minio struct { 126 conf Config 127 bucket string 128 signEndpoint string 129 location string 130 opts *minio.Options 131 core *minio.Core 132 sign *minio.Client 133 lock sync.Locker 134 init bool 135 prefix string 136 cache Cache 137 } 138 139 func (m *Minio) initMinio(ctx context.Context) error { 140 if m.init { 141 return nil 142 } 143 m.lock.Lock() 144 defer m.lock.Unlock() 145 if m.init { 146 return nil 147 } 148 exists, err := m.core.Client.BucketExists(ctx, m.conf.Bucket) 149 if err != nil { 150 return fmt.Errorf("check bucket exists error: %w", err) 151 } 152 if !exists { 153 if err = m.core.Client.MakeBucket(ctx, m.conf.Bucket, minio.MakeBucketOptions{}); err != nil { 154 return fmt.Errorf("make bucket error: %w", err) 155 } 156 } 157 if m.conf.PublicRead { 158 policy := fmt.Sprintf( 159 `{"Version": "2012-10-17","Statement": [{"Action": ["s3:GetObject","s3:PutObject"],"Effect": "Allow","Principal": {"AWS": ["*"]},"Resource": ["arn:aws:s3:::%s/*"],"Sid": ""}]}`, 160 m.conf.Bucket, 161 ) 162 if err = m.core.Client.SetBucketPolicy(ctx, m.conf.Bucket, policy); err != nil { 163 return err 164 } 165 } 166 m.location, err = m.core.Client.GetBucketLocation(ctx, m.conf.Bucket) 167 if err != nil { 168 return err 169 } 170 func() { 171 if m.conf.SignEndpoint == "" || m.conf.SignEndpoint == m.conf.Endpoint { 172 return 173 } 174 defer func() { 175 if r := recover(); r != nil { 176 m.sign = m.core.Client 177 log.ZWarn( 178 context.Background(), 179 "set sign bucket location cache panic", 180 errors.New("failed to get private field value"), 181 "recover", 182 fmt.Sprintf("%+v", r), 183 "development version", 184 "github.com/minio/minio-go/v7 v7.0.61", 185 ) 186 } 187 }() 188 blc := reflect.ValueOf(m.sign).Elem().FieldByName("bucketLocCache") 189 vblc := reflect.New(reflect.PtrTo(blc.Type())) 190 *(*unsafe.Pointer)(vblc.UnsafePointer()) = unsafe.Pointer(blc.UnsafeAddr()) 191 vblc.Elem().Elem().Interface().(interface{ Set(string, string) }).Set(m.conf.Bucket, m.location) 192 }() 193 m.init = true 194 return nil 195 } 196 197 func (m *Minio) Engine() string { 198 return "minio" 199 } 200 201 func (m *Minio) PartLimit() *s3.PartLimit { 202 return &s3.PartLimit{ 203 MinPartSize: minPartSize, 204 MaxPartSize: maxPartSize, 205 MaxNumSize: maxNumSize, 206 } 207 } 208 209 func (m *Minio) InitiateMultipartUpload(ctx context.Context, name string) (*s3.InitiateMultipartUploadResult, error) { 210 if err := m.initMinio(ctx); err != nil { 211 return nil, err 212 } 213 uploadID, err := m.core.NewMultipartUpload(ctx, m.bucket, name, minio.PutObjectOptions{}) 214 if err != nil { 215 return nil, err 216 } 217 return &s3.InitiateMultipartUploadResult{ 218 Bucket: m.bucket, 219 Key: name, 220 UploadID: uploadID, 221 }, nil 222 } 223 224 func (m *Minio) CompleteMultipartUpload(ctx context.Context, uploadID string, name string, parts []s3.Part) (*s3.CompleteMultipartUploadResult, error) { 225 if err := m.initMinio(ctx); err != nil { 226 return nil, err 227 } 228 minioParts := make([]minio.CompletePart, len(parts)) 229 for i, part := range parts { 230 minioParts[i] = minio.CompletePart{ 231 PartNumber: part.PartNumber, 232 ETag: strings.ToLower(part.ETag), 233 } 234 } 235 upload, err := m.core.CompleteMultipartUpload(ctx, m.bucket, name, uploadID, minioParts, minio.PutObjectOptions{}) 236 if err != nil { 237 return nil, err 238 } 239 m.delObjectImageInfoKey(ctx, name, upload.Size) 240 return &s3.CompleteMultipartUploadResult{ 241 Location: upload.Location, 242 Bucket: upload.Bucket, 243 Key: upload.Key, 244 ETag: strings.ToLower(upload.ETag), 245 }, nil 246 } 247 248 func (m *Minio) PartSize(ctx context.Context, size int64) (int64, error) { 249 if size <= 0 { 250 return 0, errors.New("size must be greater than 0") 251 } 252 if size > maxPartSize*maxNumSize { 253 return 0, fmt.Errorf("MINIO size must be less than the maximum allowed limit") 254 } 255 if size <= minPartSize*maxNumSize { 256 return minPartSize, nil 257 } 258 partSize := size / maxNumSize 259 if size%maxNumSize != 0 { 260 partSize++ 261 } 262 return partSize, nil 263 } 264 265 func (m *Minio) AuthSign(ctx context.Context, uploadID string, name string, expire time.Duration, partNumbers []int) (*s3.AuthSignResult, error) { 266 if err := m.initMinio(ctx); err != nil { 267 return nil, err 268 } 269 creds, err := m.opts.Creds.Get() 270 if err != nil { 271 return nil, err 272 } 273 result := s3.AuthSignResult{ 274 URL: m.signEndpoint + "/" + m.bucket + "/" + name, 275 Query: url.Values{"uploadId": {uploadID}}, 276 Parts: make([]s3.SignPart, len(partNumbers)), 277 } 278 for i, partNumber := range partNumbers { 279 rawURL := result.URL + "?partNumber=" + strconv.Itoa(partNumber) + "&uploadId=" + uploadID 280 request, err := http.NewRequestWithContext(ctx, http.MethodPut, rawURL, nil) 281 if err != nil { 282 return nil, err 283 } 284 request.Header.Set("X-Amz-Content-Sha256", unsignedPayload) 285 request = signer.SignV4Trailer(*request, creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken, m.location, nil) 286 result.Parts[i] = s3.SignPart{ 287 PartNumber: partNumber, 288 Query: url.Values{"partNumber": {strconv.Itoa(partNumber)}}, 289 Header: request.Header, 290 } 291 } 292 if m.prefix != "" { 293 result.URL = m.signEndpoint + m.prefix + "/" + m.bucket + "/" + name 294 } 295 return &result, nil 296 } 297 298 func (m *Minio) PresignedPutObject(ctx context.Context, name string, expire time.Duration) (string, error) { 299 if err := m.initMinio(ctx); err != nil { 300 return "", err 301 } 302 rawURL, err := m.sign.PresignedPutObject(ctx, m.bucket, name, expire) 303 if err != nil { 304 return "", err 305 } 306 if m.prefix != "" { 307 rawURL.Path = path.Join(m.prefix, rawURL.Path) 308 } 309 return rawURL.String(), nil 310 } 311 312 func (m *Minio) DeleteObject(ctx context.Context, name string) error { 313 if err := m.initMinio(ctx); err != nil { 314 return err 315 } 316 return m.core.Client.RemoveObject(ctx, m.bucket, name, minio.RemoveObjectOptions{}) 317 } 318 319 func (m *Minio) StatObject(ctx context.Context, name string) (*s3.ObjectInfo, error) { 320 if err := m.initMinio(ctx); err != nil { 321 return nil, err 322 } 323 info, err := m.core.Client.StatObject(ctx, m.bucket, name, minio.StatObjectOptions{}) 324 if err != nil { 325 return nil, err 326 } 327 return &s3.ObjectInfo{ 328 ETag: strings.ToLower(info.ETag), 329 Key: info.Key, 330 Size: info.Size, 331 LastModified: info.LastModified, 332 }, nil 333 } 334 335 func (m *Minio) CopyObject(ctx context.Context, src string, dst string) (*s3.CopyObjectInfo, error) { 336 if err := m.initMinio(ctx); err != nil { 337 return nil, err 338 } 339 result, err := m.core.Client.CopyObject(ctx, minio.CopyDestOptions{ 340 Bucket: m.bucket, 341 Object: dst, 342 }, minio.CopySrcOptions{ 343 Bucket: m.bucket, 344 Object: src, 345 }) 346 if err != nil { 347 return nil, err 348 } 349 return &s3.CopyObjectInfo{ 350 Key: dst, 351 ETag: strings.ToLower(result.ETag), 352 }, nil 353 } 354 355 func (m *Minio) IsNotFound(err error) bool { 356 switch e := errs.Unwrap(err).(type) { 357 case minio.ErrorResponse: 358 return e.StatusCode == http.StatusNotFound || e.Code == "NoSuchKey" 359 case *minio.ErrorResponse: 360 return e.StatusCode == http.StatusNotFound || e.Code == "NoSuchKey" 361 default: 362 return false 363 } 364 } 365 366 func (m *Minio) AbortMultipartUpload(ctx context.Context, uploadID string, name string) error { 367 if err := m.initMinio(ctx); err != nil { 368 return err 369 } 370 return m.core.AbortMultipartUpload(ctx, m.bucket, name, uploadID) 371 } 372 373 func (m *Minio) ListUploadedParts(ctx context.Context, uploadID string, name string, partNumberMarker int, maxParts int) (*s3.ListUploadedPartsResult, error) { 374 if err := m.initMinio(ctx); err != nil { 375 return nil, err 376 } 377 result, err := m.core.ListObjectParts(ctx, m.bucket, name, uploadID, partNumberMarker, maxParts) 378 if err != nil { 379 return nil, err 380 } 381 res := &s3.ListUploadedPartsResult{ 382 Key: result.Key, 383 UploadID: result.UploadID, 384 MaxParts: result.MaxParts, 385 NextPartNumberMarker: result.NextPartNumberMarker, 386 UploadedParts: make([]s3.UploadedPart, len(result.ObjectParts)), 387 } 388 for i, part := range result.ObjectParts { 389 res.UploadedParts[i] = s3.UploadedPart{ 390 PartNumber: part.PartNumber, 391 LastModified: part.LastModified, 392 ETag: part.ETag, 393 Size: part.Size, 394 } 395 } 396 return res, nil 397 } 398 399 func (m *Minio) PresignedGetObject(ctx context.Context, name string, expire time.Duration, query url.Values) (string, error) { 400 if expire <= 0 { 401 expire = time.Hour * 24 * 365 * 99 // 99 years 402 } else if expire < time.Second { 403 expire = time.Second 404 } 405 var ( 406 rawURL *url.URL 407 err error 408 ) 409 if m.conf.PublicRead { 410 rawURL, err = makeTargetURL(m.sign, m.bucket, name, m.location, false, query) 411 } else { 412 rawURL, err = m.sign.PresignedGetObject(ctx, m.bucket, name, expire, query) 413 } 414 if err != nil { 415 return "", err 416 } 417 if m.prefix != "" { 418 rawURL.Path = path.Join(m.prefix, rawURL.Path) 419 } 420 return rawURL.String(), nil 421 } 422 423 func (m *Minio) AccessURL(ctx context.Context, name string, expire time.Duration, opt *s3.AccessURLOption) (string, error) { 424 if err := m.initMinio(ctx); err != nil { 425 return "", err 426 } 427 reqParams := make(url.Values) 428 if opt != nil { 429 if opt.ContentType != "" { 430 reqParams.Set("response-content-type", opt.ContentType) 431 } 432 if opt.Filename != "" { 433 reqParams.Set("response-content-disposition", `attachment; filename=`+strconv.Quote(opt.Filename)) 434 } 435 } 436 if opt.Image == nil || (opt.Image.Width < 0 && opt.Image.Height < 0 && opt.Image.Format == "") || (opt.Image.Width > maxImageWidth || opt.Image.Height > maxImageHeight) { 437 return m.PresignedGetObject(ctx, name, expire, reqParams) 438 } 439 return m.getImageThumbnailURL(ctx, name, expire, opt.Image) 440 } 441 442 func (m *Minio) getObjectData(ctx context.Context, name string, limit int64) ([]byte, error) { 443 object, err := m.core.Client.GetObject(ctx, m.bucket, name, minio.GetObjectOptions{}) 444 if err != nil { 445 return nil, err 446 } 447 defer object.Close() 448 if limit < 0 { 449 return io.ReadAll(object) 450 } 451 return io.ReadAll(io.LimitReader(object, limit)) 452 } 453 454 func (m *Minio) FormData(ctx context.Context, name string, size int64, contentType string, duration time.Duration) (*s3.FormData, error) { 455 if err := m.initMinio(ctx); err != nil { 456 return nil, err 457 } 458 policy := minio.NewPostPolicy() 459 if err := policy.SetKey(name); err != nil { 460 return nil, err 461 } 462 expires := time.Now().Add(duration) 463 if err := policy.SetExpires(expires); err != nil { 464 return nil, err 465 } 466 if size > 0 { 467 if err := policy.SetContentLengthRange(0, size); err != nil { 468 return nil, err 469 } 470 } 471 if err := policy.SetSuccessStatusAction(strconv.Itoa(successCode)); err != nil { 472 return nil, err 473 } 474 if contentType != "" { 475 if err := policy.SetContentType(contentType); err != nil { 476 return nil, err 477 } 478 } 479 if err := policy.SetBucket(m.bucket); err != nil { 480 return nil, err 481 } 482 u, fd, err := m.core.PresignedPostPolicy(ctx, policy) 483 if err != nil { 484 return nil, err 485 } 486 sign, err := url.Parse(m.signEndpoint) 487 if err != nil { 488 return nil, err 489 } 490 u.Scheme = sign.Scheme 491 u.Host = sign.Host 492 return &s3.FormData{ 493 URL: u.String(), 494 File: "file", 495 Header: nil, 496 FormData: fd, 497 Expires: expires, 498 SuccessCodes: []int{successCode}, 499 }, nil 500 } 501 502 func (m *Minio) GetImageThumbnailKey(ctx context.Context, name string) (string, error) { 503 info, img, err := m.getObjectImageInfo(ctx, name) 504 if err != nil { 505 return "", errs.Wrap(err) 506 } 507 if !info.IsImg { 508 return "", errs.New("object not image").Wrap() 509 } 510 thumbnailWidth, thumbnailHeight := getThumbnailSize(img) 511 512 cacheKey := filepath.Join(imageThumbnailPath, info.Etag, fmt.Sprintf("image_w%d_h%d.%s", thumbnailWidth, thumbnailHeight, info.Format)) 513 return cacheKey, nil 514 }