github.com/minio/console@v1.4.1/api/user_objects.go (about) 1 // This file is part of MinIO Console Server 2 // Copyright (c) 2021 MinIO, Inc. 3 // 4 // This program is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Affero General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // This program is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Affero General Public License for more details. 13 // 14 // You should have received a copy of the GNU Affero General Public License 15 // along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17 package api 18 19 import ( 20 "context" 21 "encoding/base64" 22 b64 "encoding/base64" 23 "errors" 24 "fmt" 25 "io" 26 "net/http" 27 "net/url" 28 "path/filepath" 29 "regexp" 30 "strconv" 31 "strings" 32 "time" 33 34 "github.com/minio/minio-go/v7" 35 36 "github.com/minio/console/pkg/utils" 37 38 "github.com/go-openapi/runtime" 39 "github.com/go-openapi/runtime/middleware" 40 "github.com/klauspost/compress/zip" 41 "github.com/minio/console/api/operations" 42 objectApi "github.com/minio/console/api/operations/object" 43 "github.com/minio/console/models" 44 mc "github.com/minio/mc/cmd" 45 "github.com/minio/mc/pkg/probe" 46 "github.com/minio/minio-go/v7/pkg/tags" 47 "github.com/minio/pkg/v3/mimedb" 48 ) 49 50 // enum types 51 const ( 52 objectStorage = iota // MinIO and S3 compatible cloud storage 53 fileSystem // POSIX compatible file systems 54 ) 55 56 func registerObjectsHandlers(api *operations.ConsoleAPI) { 57 // list objects 58 api.ObjectListObjectsHandler = objectApi.ListObjectsHandlerFunc(func(params objectApi.ListObjectsParams, session *models.Principal) middleware.Responder { 59 resp, err := getListObjectsResponse(session, params) 60 if err != nil { 61 return objectApi.NewListObjectsDefault(err.Code).WithPayload(err.APIError) 62 } 63 return objectApi.NewListObjectsOK().WithPayload(resp) 64 }) 65 // delete object 66 api.ObjectDeleteObjectHandler = objectApi.DeleteObjectHandlerFunc(func(params objectApi.DeleteObjectParams, session *models.Principal) middleware.Responder { 67 if err := getDeleteObjectResponse(session, params); err != nil { 68 return objectApi.NewDeleteObjectDefault(err.Code).WithPayload(err.APIError) 69 } 70 return objectApi.NewDeleteObjectOK() 71 }) 72 // delete multiple objects 73 api.ObjectDeleteMultipleObjectsHandler = objectApi.DeleteMultipleObjectsHandlerFunc(func(params objectApi.DeleteMultipleObjectsParams, session *models.Principal) middleware.Responder { 74 if err := getDeleteMultiplePathsResponse(session, params); err != nil { 75 return objectApi.NewDeleteMultipleObjectsDefault(err.Code).WithPayload(err.APIError) 76 } 77 return objectApi.NewDeleteMultipleObjectsOK() 78 }) 79 // download object 80 api.ObjectDownloadObjectHandler = objectApi.DownloadObjectHandlerFunc(func(params objectApi.DownloadObjectParams, session *models.Principal) middleware.Responder { 81 isFolder := false 82 ctx := params.HTTPRequest.Context() 83 var prefix string 84 if params.Prefix != "" { 85 encodedPrefix := SanitizeEncodedPrefix(params.Prefix) 86 decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) 87 if err != nil { 88 apiErr := ErrorWithContext(ctx, err) 89 return objectApi.NewDownloadObjectDefault(400).WithPayload(apiErr.APIError) 90 } 91 prefix = string(decodedPrefix) 92 } 93 94 folders := strings.Split(prefix, "/") 95 if folders[len(folders)-1] == "" { 96 isFolder = true 97 } 98 var resp middleware.Responder 99 var err *CodedAPIError 100 101 if isFolder { 102 resp, err = getDownloadFolderResponse(session, params) 103 } else { 104 resp, err = getDownloadObjectResponse(session, params) 105 } 106 107 if err != nil { 108 return objectApi.NewDownloadObjectDefault(err.Code).WithPayload(err.APIError) 109 } 110 return resp 111 }) 112 // download multiple objects 113 api.ObjectDownloadMultipleObjectsHandler = objectApi.DownloadMultipleObjectsHandlerFunc(func(params objectApi.DownloadMultipleObjectsParams, session *models.Principal) middleware.Responder { 114 ctx := params.HTTPRequest.Context() 115 if len(params.ObjectList) < 1 { 116 errCode := ErrorWithContext(ctx, errors.New("could not download, since object list is empty")) 117 return objectApi.NewDownloadMultipleObjectsDefault(errCode.Code).WithPayload(errCode.APIError) 118 } 119 var resp middleware.Responder 120 var err *CodedAPIError 121 resp, err = getMultipleFilesDownloadResponse(session, params) 122 if err != nil { 123 return objectApi.NewDownloadMultipleObjectsDefault(err.Code).WithPayload(err.APIError) 124 } 125 return resp 126 }) 127 128 // upload object 129 api.ObjectPostBucketsBucketNameObjectsUploadHandler = objectApi.PostBucketsBucketNameObjectsUploadHandlerFunc(func(params objectApi.PostBucketsBucketNameObjectsUploadParams, session *models.Principal) middleware.Responder { 130 if err := getUploadObjectResponse(session, params); err != nil { 131 if strings.Contains(err.APIError.DetailedMessage, "413") { 132 return objectApi.NewPostBucketsBucketNameObjectsUploadDefault(413).WithPayload(err.APIError) 133 } 134 return objectApi.NewPostBucketsBucketNameObjectsUploadDefault(err.Code).WithPayload(err.APIError) 135 } 136 return objectApi.NewPostBucketsBucketNameObjectsUploadOK() 137 }) 138 // get share object url 139 api.ObjectShareObjectHandler = objectApi.ShareObjectHandlerFunc(func(params objectApi.ShareObjectParams, session *models.Principal) middleware.Responder { 140 resp, err := getShareObjectResponse(session, params) 141 if err != nil { 142 return objectApi.NewShareObjectDefault(err.Code).WithPayload(err.APIError) 143 } 144 return objectApi.NewShareObjectOK().WithPayload(*resp) 145 }) 146 // set object legalhold status 147 api.ObjectPutObjectLegalHoldHandler = objectApi.PutObjectLegalHoldHandlerFunc(func(params objectApi.PutObjectLegalHoldParams, session *models.Principal) middleware.Responder { 148 if err := getSetObjectLegalHoldResponse(session, params); err != nil { 149 return objectApi.NewPutObjectLegalHoldDefault(err.Code).WithPayload(err.APIError) 150 } 151 return objectApi.NewPutObjectLegalHoldOK() 152 }) 153 // set object retention 154 api.ObjectPutObjectRetentionHandler = objectApi.PutObjectRetentionHandlerFunc(func(params objectApi.PutObjectRetentionParams, session *models.Principal) middleware.Responder { 155 if err := getSetObjectRetentionResponse(session, params); err != nil { 156 return objectApi.NewPutObjectRetentionDefault(err.Code).WithPayload(err.APIError) 157 } 158 return objectApi.NewPutObjectRetentionOK() 159 }) 160 // delete object retention 161 api.ObjectDeleteObjectRetentionHandler = objectApi.DeleteObjectRetentionHandlerFunc(func(params objectApi.DeleteObjectRetentionParams, session *models.Principal) middleware.Responder { 162 if err := deleteObjectRetentionResponse(session, params); err != nil { 163 return objectApi.NewDeleteObjectRetentionDefault(err.Code).WithPayload(err.APIError) 164 } 165 return objectApi.NewDeleteObjectRetentionOK() 166 }) 167 // set tags in object 168 api.ObjectPutObjectTagsHandler = objectApi.PutObjectTagsHandlerFunc(func(params objectApi.PutObjectTagsParams, session *models.Principal) middleware.Responder { 169 if err := getPutObjectTagsResponse(session, params); err != nil { 170 return objectApi.NewPutObjectTagsDefault(err.Code).WithPayload(err.APIError) 171 } 172 return objectApi.NewPutObjectTagsOK() 173 }) 174 // Restore file version 175 api.ObjectPutObjectRestoreHandler = objectApi.PutObjectRestoreHandlerFunc(func(params objectApi.PutObjectRestoreParams, session *models.Principal) middleware.Responder { 176 if err := getPutObjectRestoreResponse(session, params); err != nil { 177 return objectApi.NewPutObjectRestoreDefault(err.Code).WithPayload(err.APIError) 178 } 179 return objectApi.NewPutObjectRestoreOK() 180 }) 181 // Metadata in object 182 api.ObjectGetObjectMetadataHandler = objectApi.GetObjectMetadataHandlerFunc(func(params objectApi.GetObjectMetadataParams, session *models.Principal) middleware.Responder { 183 resp, err := getObjectMetadataResponse(session, params) 184 if err != nil { 185 return objectApi.NewGetObjectMetadataDefault(err.Code).WithPayload(err.APIError) 186 } 187 return objectApi.NewGetObjectMetadataOK().WithPayload(resp) 188 }) 189 } 190 191 // getListObjectsResponse returns a list of objects 192 func getListObjectsResponse(session *models.Principal, params objectApi.ListObjectsParams) (*models.ListObjectsResponse, *CodedAPIError) { 193 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 194 defer cancel() 195 var prefix string 196 var recursive bool 197 var withVersions bool 198 var withMetadata bool 199 if params.Prefix != nil { 200 encodedPrefix := SanitizeEncodedPrefix(*params.Prefix) 201 decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) 202 if err != nil { 203 return nil, ErrorWithContext(ctx, err) 204 } 205 prefix = string(decodedPrefix) 206 } 207 if params.Recursive != nil { 208 recursive = *params.Recursive 209 } 210 if params.WithVersions != nil { 211 withVersions = *params.WithVersions 212 } 213 if params.WithMetadata != nil { 214 withMetadata = *params.WithMetadata 215 } 216 // bucket request needed to proceed 217 if params.BucketName == "" { 218 return nil, ErrorWithContext(ctx, ErrBucketNameNotInRequest) 219 } 220 mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest)) 221 if err != nil { 222 return nil, ErrorWithContext(ctx, err) 223 } 224 // create a minioClient interface implementation 225 // defining the client to be used 226 minioClient := minioClient{client: mClient} 227 228 objs, err := listBucketObjects(ListObjectsOpts{ 229 ctx: ctx, 230 client: minioClient, 231 bucketName: params.BucketName, 232 prefix: prefix, 233 recursive: recursive, 234 withVersions: withVersions, 235 withMetadata: withMetadata, 236 limit: params.Limit, 237 }) 238 if err != nil { 239 return nil, ErrorWithContext(ctx, err) 240 } 241 242 resp := &models.ListObjectsResponse{ 243 Objects: objs, 244 Total: int64(len(objs)), 245 } 246 return resp, nil 247 } 248 249 type ListObjectsOpts struct { 250 ctx context.Context 251 client MinioClient 252 bucketName string 253 prefix string 254 recursive bool 255 withVersions bool 256 withMetadata bool 257 limit *int32 258 } 259 260 // listBucketObjects gets an array of objects in a bucket 261 func listBucketObjects(listOpts ListObjectsOpts) ([]*models.BucketObject, error) { 262 var objects []*models.BucketObject 263 opts := minio.ListObjectsOptions{ 264 Prefix: listOpts.prefix, 265 Recursive: listOpts.recursive, 266 WithVersions: listOpts.withVersions, 267 WithMetadata: listOpts.withMetadata, 268 MaxKeys: 100, 269 } 270 if listOpts.withMetadata { 271 opts.MaxKeys = 1 272 } 273 if listOpts.limit != nil { 274 opts.MaxKeys = int(*listOpts.limit) 275 } 276 var totalObjs int32 277 for lsObj := range listOpts.client.listObjects(listOpts.ctx, listOpts.bucketName, opts) { 278 if lsObj.Err != nil { 279 return nil, lsObj.Err 280 } 281 282 obj := &models.BucketObject{ 283 Name: lsObj.Key, 284 Size: lsObj.Size, 285 LastModified: lsObj.LastModified.Format(time.RFC3339), 286 ContentType: lsObj.ContentType, 287 VersionID: lsObj.VersionID, 288 IsLatest: lsObj.IsLatest, 289 IsDeleteMarker: lsObj.IsDeleteMarker, 290 UserTags: lsObj.UserTags, 291 UserMetadata: lsObj.UserMetadata, 292 Etag: lsObj.ETag, 293 } 294 // only if single object with or without versions; get legalhold, retention and tags 295 if !lsObj.IsDeleteMarker && listOpts.prefix != "" && !strings.HasSuffix(listOpts.prefix, "/") { 296 // Add Legal Hold Status if available 297 legalHoldStatus, err := listOpts.client.getObjectLegalHold(listOpts.ctx, listOpts.bucketName, lsObj.Key, minio.GetObjectLegalHoldOptions{VersionID: lsObj.VersionID}) 298 if err != nil { 299 errResp := minio.ToErrorResponse(probe.NewError(err).ToGoError()) 300 if errResp.Code != "InvalidRequest" && errResp.Code != "NoSuchObjectLockConfiguration" { 301 ErrorWithContext(listOpts.ctx, fmt.Errorf("error getting legal hold status for %s : %v", lsObj.VersionID, err)) 302 } 303 } else if legalHoldStatus != nil { 304 obj.LegalHoldStatus = string(*legalHoldStatus) 305 } 306 // Add Retention Status if available 307 retention, retUntilDate, err := listOpts.client.getObjectRetention(listOpts.ctx, listOpts.bucketName, lsObj.Key, lsObj.VersionID) 308 if err != nil { 309 errResp := minio.ToErrorResponse(probe.NewError(err).ToGoError()) 310 if errResp.Code != "InvalidRequest" && errResp.Code != "NoSuchObjectLockConfiguration" { 311 ErrorWithContext(listOpts.ctx, fmt.Errorf("error getting retention status for %s : %v", lsObj.VersionID, err)) 312 } 313 } else if retention != nil && retUntilDate != nil { 314 date := *retUntilDate 315 obj.RetentionMode = string(*retention) 316 obj.RetentionUntilDate = date.Format(time.RFC3339) 317 } 318 objTags, err := listOpts.client.getObjectTagging(listOpts.ctx, listOpts.bucketName, lsObj.Key, minio.GetObjectTaggingOptions{VersionID: lsObj.VersionID}) 319 if err != nil { 320 ErrorWithContext(listOpts.ctx, fmt.Errorf("error getting object tags for %s : %v", lsObj.VersionID, err)) 321 } else { 322 obj.Tags = objTags.ToMap() 323 } 324 } 325 objects = append(objects, obj) 326 totalObjs++ 327 328 if listOpts.limit != nil { 329 if totalObjs >= *listOpts.limit { 330 break 331 } 332 } 333 } 334 return objects, nil 335 } 336 337 type httpRange struct { 338 Start int64 339 Length int64 340 } 341 342 // Example: 343 // 344 // "Content-Range": "bytes 100-200/1000" 345 // "Content-Range": "bytes 100-200/*" 346 func getRange(start, end, total int64) string { 347 // unknown total: -1 348 if total == -1 { 349 return fmt.Sprintf("bytes %d-%d/*", start, end) 350 } 351 352 return fmt.Sprintf("bytes %d-%d/%d", start, end, total) 353 } 354 355 // Example: 356 // 357 // "Range": "bytes=100-200" 358 // "Range": "bytes=-50" 359 // "Range": "bytes=150-" 360 // "Range": "bytes=0-0,-1" 361 func parseRange(s string, size int64) ([]httpRange, error) { 362 if s == "" { 363 return nil, nil // header not present 364 } 365 const b = "bytes=" 366 if !strings.HasPrefix(s, b) { 367 return nil, errors.New("invalid range") 368 } 369 var ranges []httpRange 370 for _, ra := range strings.Split(s[len(b):], ",") { 371 ra = strings.TrimSpace(ra) 372 if ra == "" { 373 continue 374 } 375 i := strings.Index(ra, "-") 376 if i < 0 { 377 return nil, errors.New("invalid range") 378 } 379 start, end := strings.TrimSpace(ra[:i]), strings.TrimSpace(ra[i+1:]) 380 var r httpRange 381 if start == "" { 382 // If no start is specified, end specifies the 383 // range start relative to the end of the file. 384 i, err := strconv.ParseInt(end, 10, 64) 385 if err != nil { 386 return nil, errors.New("invalid range") 387 } 388 if i > size { 389 i = size 390 } 391 r.Start = size - i 392 r.Length = size - r.Start 393 } else { 394 i, err := strconv.ParseInt(start, 10, 64) 395 if err != nil || i >= size || i < 0 { 396 return nil, errors.New("invalid range") 397 } 398 r.Start = i 399 if end == "" { 400 // If no end is specified, range extends to end of the file. 401 r.Length = size - r.Start 402 } else { 403 i, err := strconv.ParseInt(end, 10, 64) 404 if err != nil || r.Start > i { 405 return nil, errors.New("invalid range") 406 } 407 if i >= size { 408 i = size - 1 409 } 410 r.Length = i - r.Start + 1 411 } 412 } 413 ranges = append(ranges, r) 414 } 415 return ranges, nil 416 } 417 418 func getDownloadObjectResponse(session *models.Principal, params objectApi.DownloadObjectParams) (middleware.Responder, *CodedAPIError) { 419 ctx := params.HTTPRequest.Context() 420 var prefix string 421 mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest)) 422 if err != nil { 423 return nil, ErrorWithContext(ctx, err) 424 } 425 if params.Prefix != "" { 426 encodedPrefix := SanitizeEncodedPrefix(params.Prefix) 427 decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) 428 if err != nil { 429 return nil, ErrorWithContext(ctx, err) 430 } 431 prefix = string(decodedPrefix) 432 } 433 434 opts := minio.GetObjectOptions{} 435 436 if params.VersionID != nil && *params.VersionID != "" { 437 opts.VersionID = *params.VersionID 438 } 439 440 resp, err := mClient.GetObject(ctx, params.BucketName, prefix, opts) 441 if err != nil { 442 return nil, ErrorWithContext(ctx, err) 443 } 444 445 return middleware.ResponderFunc(func(rw http.ResponseWriter, _ runtime.Producer) { 446 defer resp.Close() 447 448 isPreview := params.Preview != nil && *params.Preview 449 // override filename is set 450 decodeOverride, err := base64.StdEncoding.DecodeString(*params.OverrideFileName) 451 if err != nil { 452 fmtError := ErrorWithContext(ctx, fmt.Errorf("unable to decode OverrideFileName: %v", err)) 453 http.Error(rw, fmtError.APIError.DetailedMessage, http.StatusBadRequest) 454 return 455 } 456 457 overrideName := string(decodeOverride) 458 459 // indicate it's a download / inline content to the browser, and the size of the object 460 var filename string 461 prefixElements := strings.Split(prefix, "/") 462 if len(prefixElements) > 0 && overrideName == "" { 463 if prefixElements[len(prefixElements)-1] == "" { 464 filename = prefixElements[len(prefixElements)-2] 465 } else { 466 filename = prefixElements[len(prefixElements)-1] 467 } 468 } else if overrideName != "" { 469 filename = overrideName 470 } 471 472 escapedName := url.PathEscape(filename) 473 474 // indicate object size & content type 475 stat, err := resp.Stat() 476 if err != nil { 477 minErr := minio.ToErrorResponse(err) 478 fmtError := ErrorWithContext(ctx, fmt.Errorf("failed to get Stat() response from server for %s (version %s): %v", prefix, opts.VersionID, minErr.Error())) 479 http.Error(rw, fmtError.APIError.DetailedMessage, http.StatusInternalServerError) 480 return 481 } 482 483 // if we are getting a Range Request (video) handle that specially 484 ranges, err := parseRange(params.HTTPRequest.Header.Get("Range"), stat.Size) 485 if err != nil { 486 fmtError := ErrorWithContext(ctx, fmt.Errorf("unable to parse range header input %s: %v", params.HTTPRequest.Header.Get("Range"), err)) 487 http.Error(rw, fmtError.APIError.DetailedMessage, http.StatusInternalServerError) 488 return 489 } 490 contentType := stat.ContentType 491 rw.Header().Set("X-XSS-Protection", "1; mode=block") 492 493 if isPreview && isSafeToPreview(contentType) { 494 rw.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", escapedName)) 495 rw.Header().Set("X-Frame-Options", "SAMEORIGIN") 496 } else { 497 rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", escapedName)) 498 } 499 500 rw.Header().Set("Last-Modified", stat.LastModified.UTC().Format(http.TimeFormat)) 501 502 if isPreview { 503 // In case content type was uploaded as octet-stream, we double verify content type 504 if stat.ContentType == "application/octet-stream" { 505 contentType = mimedb.TypeByExtension(filepath.Ext(escapedName)) 506 } 507 } 508 rw.Header().Set("Content-Type", contentType) 509 length := stat.Size 510 if len(ranges) > 0 { 511 start := ranges[0].Start 512 length = ranges[0].Length 513 514 _, err = resp.Seek(start, io.SeekStart) 515 if err != nil { 516 fmtError := ErrorWithContext(ctx, fmt.Errorf("unable to seek at offset %d: %v", start, err)) 517 http.Error(rw, fmtError.APIError.DetailedMessage, http.StatusInternalServerError) 518 return 519 } 520 521 rw.Header().Set("Accept-Ranges", "bytes") 522 rw.Header().Set("Access-Control-Allow-Origin", "*") 523 rw.Header().Set("Content-Range", getRange(start, start+length-1, stat.Size)) 524 rw.WriteHeader(http.StatusPartialContent) 525 } 526 527 rw.Header().Set("Content-Length", fmt.Sprintf("%d", length)) 528 _, err = io.Copy(rw, io.LimitReader(resp, length)) 529 if err != nil { 530 ErrorWithContext(ctx, fmt.Errorf("unable to write all data to client: %v", err)) 531 // You can't change headers after you already started writing the body. 532 // Handle incomplete write in client. 533 return 534 } 535 }), nil 536 } 537 538 func getDownloadFolderResponse(session *models.Principal, params objectApi.DownloadObjectParams) (middleware.Responder, *CodedAPIError) { 539 ctx := params.HTTPRequest.Context() 540 var prefix string 541 mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest)) 542 if params.Prefix != "" { 543 encodedPrefix := SanitizeEncodedPrefix(params.Prefix) 544 decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) 545 if err != nil { 546 return nil, ErrorWithContext(ctx, err) 547 } 548 prefix = string(decodedPrefix) 549 } 550 551 folders := strings.Split(prefix, "/") 552 553 if err != nil { 554 return nil, ErrorWithContext(ctx, err) 555 } 556 minioClient := minioClient{client: mClient} 557 objects, err := listBucketObjects(ListObjectsOpts{ 558 ctx: ctx, 559 client: minioClient, 560 bucketName: params.BucketName, 561 prefix: prefix, 562 recursive: true, 563 withVersions: false, 564 withMetadata: false, 565 }) 566 if err != nil { 567 return nil, ErrorWithContext(ctx, err) 568 } 569 570 resp, pw := io.Pipe() 571 // Create file async 572 go func() { 573 defer pw.Close() 574 zipw := zip.NewWriter(pw) 575 var folder string 576 if len(folders) > 1 { 577 folder = folders[len(folders)-2] 578 } 579 defer zipw.Close() 580 581 for i, obj := range objects { 582 name := folder + objects[i].Name[len(prefix)-1:] 583 object, err := mClient.GetObject(ctx, params.BucketName, obj.Name, minio.GetObjectOptions{}) 584 if err != nil { 585 // Ignore errors, move to next 586 continue 587 } 588 modified, _ := time.Parse(time.RFC3339, obj.LastModified) 589 f, err := zipw.CreateHeader(&zip.FileHeader{ 590 Name: name, 591 NonUTF8: false, 592 Method: zip.Deflate, 593 Modified: modified, 594 }) 595 if err != nil { 596 // Ignore errors, move to next 597 continue 598 } 599 _, err = io.Copy(f, object) 600 if err != nil { 601 // We have a partial object, report error. 602 pw.CloseWithError(err) 603 return 604 } 605 } 606 }() 607 608 return middleware.ResponderFunc(func(rw http.ResponseWriter, _ runtime.Producer) { 609 defer resp.Close() 610 611 // indicate it's a download / inline content to the browser, and the size of the object 612 var prefixPath string 613 var filename string 614 if params.Prefix != "" { 615 encodedPrefix := SanitizeEncodedPrefix(params.Prefix) 616 decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) 617 if err != nil { 618 fmtError := ErrorWithContext(ctx, fmt.Errorf("unable to parse encoded prefix %s: %v", encodedPrefix, err)) 619 http.Error(rw, fmtError.APIError.DetailedMessage, http.StatusInternalServerError) 620 return 621 } 622 623 prefixPath = string(decodedPrefix) 624 } 625 prefixElements := strings.Split(prefixPath, "/") 626 if len(prefixElements) > 0 { 627 if prefixElements[len(prefixElements)-1] == "" { 628 filename = prefixElements[len(prefixElements)-2] 629 } else { 630 filename = prefixElements[len(prefixElements)-1] 631 } 632 } 633 escapedName := url.PathEscape(filename) 634 635 rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", escapedName)) 636 rw.Header().Set("Content-Type", "application/zip") 637 638 // Copy the stream 639 _, err := io.Copy(rw, resp) 640 if err != nil { 641 ErrorWithContext(ctx, fmt.Errorf("unable to write all the requested data: %v", err)) 642 // You can't change headers after you already started writing the body. 643 // Handle incomplete write in client. 644 return 645 } 646 }), nil 647 } 648 649 func getMultipleFilesDownloadResponse(session *models.Principal, params objectApi.DownloadMultipleObjectsParams) (middleware.Responder, *CodedAPIError) { 650 ctx := params.HTTPRequest.Context() 651 mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest)) 652 if err != nil { 653 return nil, ErrorWithContext(ctx, err) 654 } 655 minioClient := minioClient{client: mClient} 656 657 resp, pw := io.Pipe() 658 // Create file async 659 go func() { 660 defer pw.Close() 661 zipw := zip.NewWriter(pw) 662 defer zipw.Close() 663 664 addToZip := func(name string, modified time.Time) (io.Writer, error) { 665 f, err := zipw.CreateHeader(&zip.FileHeader{ 666 Name: name, 667 NonUTF8: false, 668 Method: zip.Deflate, 669 Modified: modified, 670 }) 671 return f, err 672 } 673 674 for _, dObj := range params.ObjectList { 675 // if a prefix is selected, list and add objects recursively 676 // the prefixes are not base64 encoded. 677 if strings.HasSuffix(dObj, "/") { 678 prefix := dObj 679 680 folders := strings.Split(prefix, "/") 681 682 var folder string 683 if len(folders) > 1 { 684 folder = folders[len(folders)-2] 685 } 686 687 objects, err := listBucketObjects(ListObjectsOpts{ 688 ctx: ctx, 689 client: minioClient, 690 bucketName: params.BucketName, 691 prefix: prefix, 692 recursive: true, 693 withVersions: false, 694 withMetadata: false, 695 }) 696 if err != nil { 697 pw.CloseWithError(err) 698 } 699 700 for i, obj := range objects { 701 name := folder + objects[i].Name[len(prefix)-1:] 702 703 object, err := mClient.GetObject(ctx, params.BucketName, obj.Name, minio.GetObjectOptions{}) 704 if err != nil { 705 // Ignore errors, move to next 706 continue 707 } 708 modified, _ := time.Parse(time.RFC3339, obj.LastModified) 709 710 f, err := addToZip(name, modified) 711 if err != nil { 712 // Ignore errors, move to next 713 continue 714 } 715 _, err = io.Copy(f, object) 716 if err != nil { 717 // We have a partial object, report error. 718 pw.CloseWithError(err) 719 return 720 } 721 } 722 723 } else { 724 // add selected individual object 725 objectData, err := mClient.StatObject(ctx, params.BucketName, dObj, minio.StatObjectOptions{}) 726 if err != nil { 727 // Ignore errors, move to next 728 continue 729 } 730 object, err := mClient.GetObject(ctx, params.BucketName, dObj, minio.GetObjectOptions{}) 731 if err != nil { 732 // Ignore errors, move to next 733 continue 734 } 735 736 prefixes := strings.Split(dObj, "/") 737 // truncate upper level prefixes to make the download as flat at the current level. 738 objectName := prefixes[len(prefixes)-1] 739 f, err := addToZip(objectName, objectData.LastModified) 740 if err != nil { 741 // Ignore errors, move to next 742 continue 743 } 744 _, err = io.Copy(f, object) 745 if err != nil { 746 // We have a partial object, report error. 747 pw.CloseWithError(err) 748 return 749 } 750 } 751 } 752 }() 753 754 return middleware.ResponderFunc(func(rw http.ResponseWriter, _ runtime.Producer) { 755 defer resp.Close() 756 757 // indicate it's a download / inline content to the browser, and the size of the object 758 fileName := "selected_files_" + strings.ReplaceAll(strings.ReplaceAll(time.Now().UTC().Format(time.RFC3339), ":", ""), "-", "") 759 760 rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", fileName)) 761 rw.Header().Set("Content-Type", "application/zip") 762 763 // Copy the stream 764 _, err := io.Copy(rw, resp) 765 if err != nil { 766 ErrorWithContext(ctx, fmt.Errorf("unable to write all the requested data: %v", err)) 767 // You can't change headers after you already started writing the body. 768 // Handle incomplete write in client. 769 return 770 } 771 }), nil 772 } 773 774 // getDeleteObjectResponse returns whether there was an error on deletion of object 775 func getDeleteObjectResponse(session *models.Principal, params objectApi.DeleteObjectParams) *CodedAPIError { 776 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 777 defer cancel() 778 var prefix string 779 if params.Prefix != "" { 780 encodedPrefix := SanitizeEncodedPrefix(params.Prefix) 781 decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) 782 if err != nil { 783 return ErrorWithContext(ctx, err) 784 } 785 prefix = string(decodedPrefix) 786 } 787 s3Client, err := newS3BucketClient(session, params.BucketName, prefix, getClientIP(params.HTTPRequest)) 788 if err != nil { 789 return ErrorWithContext(ctx, err) 790 } 791 // create a mc S3Client interface implementation 792 // defining the client to be used 793 mcClient := mcClient{client: s3Client} 794 var rec bool 795 var version string 796 var allVersions bool 797 var nonCurrentVersions bool 798 var bypass bool 799 if params.Recursive != nil { 800 rec = *params.Recursive 801 } 802 if params.VersionID != nil { 803 version = *params.VersionID 804 } 805 if params.AllVersions != nil { 806 allVersions = *params.AllVersions 807 } 808 if params.NonCurrentVersions != nil { 809 nonCurrentVersions = *params.NonCurrentVersions 810 } 811 if params.Bypass != nil { 812 bypass = *params.Bypass 813 } 814 815 if allVersions && nonCurrentVersions { 816 err := errors.New("cannot set delete all versions and delete non-current versions flags at the same time") 817 return ErrorWithContext(ctx, err) 818 } 819 820 err = deleteObjects(ctx, mcClient, params.BucketName, prefix, version, rec, allVersions, nonCurrentVersions, bypass) 821 if err != nil { 822 return ErrorWithContext(ctx, err) 823 } 824 return nil 825 } 826 827 // getDeleteMultiplePathsResponse returns whether there was an error on deletion of any object 828 func getDeleteMultiplePathsResponse(session *models.Principal, params objectApi.DeleteMultipleObjectsParams) *CodedAPIError { 829 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 830 defer cancel() 831 var version string 832 var allVersions bool 833 var bypass bool 834 if params.AllVersions != nil { 835 allVersions = *params.AllVersions 836 } 837 if params.Bypass != nil { 838 bypass = *params.Bypass 839 } 840 for i := 0; i < len(params.Files); i++ { 841 if params.Files[i].VersionID != "" { 842 version = params.Files[i].VersionID 843 } 844 prefix := params.Files[i].Path 845 s3Client, err := newS3BucketClient(session, params.BucketName, prefix, getClientIP(params.HTTPRequest)) 846 if err != nil { 847 return ErrorWithContext(ctx, err) 848 } 849 // create a mc S3Client interface implementation 850 // defining the client to be used 851 mcClient := mcClient{client: s3Client} 852 err = deleteObjects(ctx, mcClient, params.BucketName, params.Files[i].Path, version, params.Files[i].Recursive, allVersions, false, bypass) 853 if err != nil { 854 return ErrorWithContext(ctx, err) 855 } 856 } 857 return nil 858 } 859 860 // deleteObjects deletes either a single object or multiple objects based on recursive flag 861 func deleteObjects(ctx context.Context, client MCClient, bucket string, path string, versionID string, recursive, allVersions, nonCurrentVersionsOnly, bypass bool) error { 862 // Delete All non-Current versions only. 863 if nonCurrentVersionsOnly { 864 return deleteNonCurrentVersions(ctx, client, bypass) 865 } 866 867 if recursive || allVersions { 868 return deleteMultipleObjects(ctx, client, path, recursive, allVersions, bypass) 869 } 870 871 return deleteSingleObject(ctx, client, bucket, path, versionID, bypass) 872 } 873 874 // Return standardized URL to be used to compare later. 875 func getStandardizedURL(targetURL string) string { 876 return filepath.FromSlash(targetURL) 877 } 878 879 // deleteMultipleObjects uses listing before removal, it can list recursively or not, 880 // 881 // Use cases: 882 // * Remove objects recursively 883 func deleteMultipleObjects(ctx context.Context, client MCClient, path string, recursive, allVersions, isBypass bool) error { 884 // Constants defined to make this code more readable 885 const ( 886 isIncomplete = false 887 isRemoveBucket = false 888 forceDelete = false // Force delete not meant to be used by console UI. 889 ) 890 891 listOpts := mc.ListOptions{ 892 Recursive: recursive, 893 Incomplete: isIncomplete, 894 ShowDir: mc.DirNone, 895 WithOlderVersions: allVersions, 896 WithDeleteMarkers: allVersions, 897 } 898 899 lctx, cancel := context.WithCancel(ctx) 900 defer cancel() 901 902 contentCh := make(chan *mc.ClientContent) 903 904 go func() { 905 defer close(contentCh) 906 907 for content := range client.list(lctx, listOpts) { 908 if content.Err != nil { 909 continue 910 } 911 912 if !strings.HasSuffix(getStandardizedURL(content.URL.Path), path) && !strings.HasSuffix(path, "/") { 913 continue 914 } 915 916 select { 917 case contentCh <- content: 918 case <-lctx.Done(): 919 return 920 } 921 } 922 }() 923 924 for result := range client.remove(ctx, isIncomplete, isRemoveBucket, isBypass, forceDelete, contentCh) { 925 if result.Err != nil { 926 return result.Err.Cause 927 } 928 } 929 930 return nil 931 } 932 933 func deleteSingleObject(ctx context.Context, client MCClient, bucket, object string, versionID string, isBypass bool) error { 934 targetURL := fmt.Sprintf("%s/%s", bucket, object) 935 contentCh := make(chan *mc.ClientContent, 1) 936 contentCh <- &mc.ClientContent{URL: *newClientURL(targetURL), VersionID: versionID} 937 close(contentCh) 938 939 isIncomplete := false 940 isRemoveBucket := false 941 942 resultCh := client.remove(ctx, isIncomplete, isRemoveBucket, isBypass, false, contentCh) 943 for result := range resultCh { 944 if result.Err != nil { 945 return result.Err.Cause 946 } 947 } 948 return nil 949 } 950 951 func deleteNonCurrentVersions(ctx context.Context, client MCClient, isBypass bool) error { 952 lctx, cancel := context.WithCancel(ctx) 953 defer cancel() 954 955 contentCh := make(chan *mc.ClientContent) 956 957 go func() { 958 defer close(contentCh) 959 960 // Get current object versions 961 for lsObj := range client.list(lctx, mc.ListOptions{ 962 WithDeleteMarkers: true, 963 WithOlderVersions: true, 964 Recursive: true, 965 }) { 966 if lsObj.Err != nil { 967 continue 968 } 969 970 if lsObj.IsLatest { 971 continue 972 } 973 974 // All non-current objects proceed to purge. 975 select { 976 case contentCh <- lsObj: 977 case <-lctx.Done(): 978 return 979 } 980 } 981 }() 982 983 for result := range client.remove(ctx, false, false, isBypass, false, contentCh) { 984 if result.Err != nil { 985 return result.Err.Cause 986 } 987 } 988 989 return nil 990 } 991 992 func getUploadObjectResponse(session *models.Principal, params objectApi.PostBucketsBucketNameObjectsUploadParams) *CodedAPIError { 993 ctx := params.HTTPRequest.Context() 994 mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest)) 995 if err != nil { 996 return ErrorWithContext(ctx, err) 997 } 998 // create a minioClient interface implementation 999 // defining the client to be used 1000 minioClient := minioClient{client: mClient} 1001 if err := uploadFiles(ctx, minioClient, params); err != nil { 1002 return ErrorWithContext(ctx, err, ErrDefault) 1003 } 1004 return nil 1005 } 1006 1007 // uploadFiles gets files from http.Request form and uploads them to MinIO 1008 func uploadFiles(ctx context.Context, client MinioClient, params objectApi.PostBucketsBucketNameObjectsUploadParams) error { 1009 var prefix string 1010 if params.Prefix != nil { 1011 encodedPrefix := SanitizeEncodedPrefix(*params.Prefix) 1012 decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) 1013 if err != nil { 1014 return err 1015 } 1016 prefix = string(decodedPrefix) 1017 // trim any leading '/', since that is not expected 1018 // for any object. 1019 prefix = strings.TrimPrefix(prefix, "/") 1020 } 1021 1022 // parse a request body as multipart/form-data. 1023 // 32 << 20 is default max memory 1024 mr, err := params.HTTPRequest.MultipartReader() 1025 if err != nil { 1026 return err 1027 } 1028 1029 for { 1030 p, err := mr.NextPart() 1031 if err == io.EOF { 1032 break 1033 } 1034 1035 size, err := strconv.ParseInt(p.FormName(), 10, 64) 1036 if err != nil { 1037 return err 1038 } 1039 1040 contentType := p.Header.Get("content-type") 1041 if contentType == "" { 1042 contentType = mimedb.TypeByExtension(filepath.Ext(p.FileName())) 1043 } 1044 objectName := prefix // prefix will have complete object path e.g: /test-prefix/test-object.txt 1045 _, err = client.putObject(ctx, params.BucketName, objectName, p, size, minio.PutObjectOptions{ 1046 ContentType: contentType, 1047 DisableMultipart: true, // Do not upload as multipart stream for console uploader. 1048 }) 1049 if err != nil { 1050 return err 1051 } 1052 } 1053 1054 return nil 1055 } 1056 1057 // getShareObjectResponse returns a share object url 1058 func getShareObjectResponse(session *models.Principal, params objectApi.ShareObjectParams) (*string, *CodedAPIError) { 1059 ctx := params.HTTPRequest.Context() 1060 clientIP := utils.ClientIPFromContext(ctx) 1061 var prefix string 1062 if params.Prefix != "" { 1063 encodedPrefix := SanitizeEncodedPrefix(params.Prefix) 1064 decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) 1065 if err != nil { 1066 return nil, ErrorWithContext(ctx, err) 1067 } 1068 prefix = string(decodedPrefix) 1069 } 1070 s3Client, err := newS3BucketClient(session, params.BucketName, prefix, clientIP) 1071 if err != nil { 1072 return nil, ErrorWithContext(ctx, err) 1073 } 1074 // create a mc S3Client interface implementation 1075 // defining the client to be used 1076 mcClient := mcClient{client: s3Client} 1077 var expireDuration string 1078 if params.Expires != nil { 1079 expireDuration = *params.Expires 1080 } 1081 url, err := getShareObjectURL(ctx, mcClient, params.HTTPRequest, params.VersionID, expireDuration) 1082 if err != nil { 1083 return nil, ErrorWithContext(ctx, err) 1084 } 1085 1086 return url, nil 1087 } 1088 1089 func getShareObjectURL(ctx context.Context, client MCClient, r *http.Request, versionID string, duration string) (url *string, err error) { 1090 // default duration 7d if not defined 1091 if strings.TrimSpace(duration) == "" { 1092 duration = "168h" 1093 } 1094 expiresDuration, err := time.ParseDuration(duration) 1095 if err != nil { 1096 return nil, err 1097 } 1098 minioURL, pErr := client.shareDownload(ctx, versionID, expiresDuration) 1099 if pErr != nil { 1100 return nil, pErr.Cause 1101 } 1102 1103 encodedMinIOURL := b64.URLEncoding.EncodeToString([]byte(minioURL)) 1104 requestURL := getRequestURLWithScheme(r) 1105 objURL := fmt.Sprintf("%s/api/v1/download-shared-object/%s", requestURL, encodedMinIOURL) 1106 return &objURL, nil 1107 } 1108 1109 func getRequestURLWithScheme(r *http.Request) string { 1110 scheme := "http" 1111 if r.TLS != nil { 1112 scheme = "https" 1113 } 1114 1115 redirectURL := getConsoleBrowserRedirectURL() 1116 if redirectURL != "" { 1117 return strings.TrimSuffix(redirectURL, "/") 1118 } 1119 1120 return fmt.Sprintf("%s://%s", scheme, r.Host) 1121 } 1122 1123 func getSetObjectLegalHoldResponse(session *models.Principal, params objectApi.PutObjectLegalHoldParams) *CodedAPIError { 1124 ctx := params.HTTPRequest.Context() 1125 mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest)) 1126 if err != nil { 1127 return ErrorWithContext(ctx, err) 1128 } 1129 // create a minioClient interface implementation 1130 // defining the client to be used 1131 minioClient := minioClient{client: mClient} 1132 var prefix string 1133 if params.Prefix != "" { 1134 encodedPrefix := SanitizeEncodedPrefix(params.Prefix) 1135 decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) 1136 if err != nil { 1137 return ErrorWithContext(ctx, err) 1138 } 1139 prefix = string(decodedPrefix) 1140 } 1141 err = setObjectLegalHold(ctx, minioClient, params.BucketName, prefix, params.VersionID, *params.Body.Status) 1142 if err != nil { 1143 return ErrorWithContext(ctx, err) 1144 } 1145 return nil 1146 } 1147 1148 func setObjectLegalHold(ctx context.Context, client MinioClient, bucketName, prefix, versionID string, status models.ObjectLegalHoldStatus) error { 1149 var lstatus minio.LegalHoldStatus 1150 if status == models.ObjectLegalHoldStatusEnabled { 1151 lstatus = minio.LegalHoldEnabled 1152 } else { 1153 lstatus = minio.LegalHoldDisabled 1154 } 1155 return client.putObjectLegalHold(ctx, bucketName, prefix, minio.PutObjectLegalHoldOptions{VersionID: versionID, Status: &lstatus}) 1156 } 1157 1158 func getSetObjectRetentionResponse(session *models.Principal, params objectApi.PutObjectRetentionParams) *CodedAPIError { 1159 ctx := params.HTTPRequest.Context() 1160 mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest)) 1161 if err != nil { 1162 return ErrorWithContext(ctx, err) 1163 } 1164 // create a minioClient interface implementation 1165 // defining the client to be used 1166 minioClient := minioClient{client: mClient} 1167 var prefix string 1168 if params.Prefix != "" { 1169 encodedPrefix := SanitizeEncodedPrefix(params.Prefix) 1170 decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) 1171 if err != nil { 1172 return ErrorWithContext(ctx, err) 1173 } 1174 prefix = string(decodedPrefix) 1175 } 1176 err = setObjectRetention(ctx, minioClient, params.BucketName, params.VersionID, prefix, params.Body) 1177 if err != nil { 1178 return ErrorWithContext(ctx, err) 1179 } 1180 return nil 1181 } 1182 1183 func setObjectRetention(ctx context.Context, client MinioClient, bucketName, versionID, prefix string, retentionOps *models.PutObjectRetentionRequest) error { 1184 if retentionOps == nil { 1185 return errors.New("object retention options can't be nil") 1186 } 1187 if retentionOps.Expires == nil { 1188 return errors.New("object retention expires can't be nil") 1189 } 1190 1191 var mode minio.RetentionMode 1192 if *retentionOps.Mode == models.ObjectRetentionModeGovernance { 1193 mode = minio.Governance 1194 } else { 1195 mode = minio.Compliance 1196 } 1197 retentionUntilDate, err := time.Parse(time.RFC3339, *retentionOps.Expires) 1198 if err != nil { 1199 return err 1200 } 1201 opts := minio.PutObjectRetentionOptions{ 1202 GovernanceBypass: retentionOps.GovernanceBypass, 1203 RetainUntilDate: &retentionUntilDate, 1204 Mode: &mode, 1205 VersionID: versionID, 1206 } 1207 return client.putObjectRetention(ctx, bucketName, prefix, opts) 1208 } 1209 1210 func deleteObjectRetentionResponse(session *models.Principal, params objectApi.DeleteObjectRetentionParams) *CodedAPIError { 1211 ctx := params.HTTPRequest.Context() 1212 mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest)) 1213 if err != nil { 1214 return ErrorWithContext(ctx, err) 1215 } 1216 // create a minioClient interface implementation 1217 // defining the client to be used 1218 minioClient := minioClient{client: mClient} 1219 var prefix string 1220 if params.Prefix != "" { 1221 encodedPrefix := SanitizeEncodedPrefix(params.Prefix) 1222 decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) 1223 if err != nil { 1224 return ErrorWithContext(ctx, err) 1225 } 1226 prefix = string(decodedPrefix) 1227 } 1228 err = deleteObjectRetention(ctx, minioClient, params.BucketName, prefix, params.VersionID) 1229 if err != nil { 1230 return ErrorWithContext(ctx, err) 1231 } 1232 return nil 1233 } 1234 1235 func deleteObjectRetention(ctx context.Context, client MinioClient, bucketName, prefix, versionID string) error { 1236 opts := minio.PutObjectRetentionOptions{ 1237 GovernanceBypass: true, 1238 VersionID: versionID, 1239 } 1240 1241 return client.putObjectRetention(ctx, bucketName, prefix, opts) 1242 } 1243 1244 func getPutObjectTagsResponse(session *models.Principal, params objectApi.PutObjectTagsParams) *CodedAPIError { 1245 ctx := params.HTTPRequest.Context() 1246 mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest)) 1247 if err != nil { 1248 return ErrorWithContext(ctx, err) 1249 } 1250 // create a minioClient interface implementation 1251 // defining the client to be used 1252 minioClient := minioClient{client: mClient} 1253 var prefix string 1254 if params.Prefix != "" { 1255 encodedPrefix := SanitizeEncodedPrefix(params.Prefix) 1256 decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) 1257 if err != nil { 1258 return ErrorWithContext(ctx, err) 1259 } 1260 prefix = string(decodedPrefix) 1261 } 1262 err = putObjectTags(ctx, minioClient, params.BucketName, prefix, params.VersionID, params.Body.Tags) 1263 if err != nil { 1264 return ErrorWithContext(ctx, err) 1265 } 1266 return nil 1267 } 1268 1269 func putObjectTags(ctx context.Context, client MinioClient, bucketName, prefix, versionID string, tagMap map[string]string) error { 1270 opt := minio.PutObjectTaggingOptions{ 1271 VersionID: versionID, 1272 } 1273 otags, err := tags.MapToObjectTags(tagMap) 1274 if err != nil { 1275 return err 1276 } 1277 return client.putObjectTagging(ctx, bucketName, prefix, otags, opt) 1278 } 1279 1280 // Restore Object Version 1281 func getPutObjectRestoreResponse(session *models.Principal, params objectApi.PutObjectRestoreParams) *CodedAPIError { 1282 ctx := params.HTTPRequest.Context() 1283 mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest)) 1284 if err != nil { 1285 return ErrorWithContext(ctx, err) 1286 } 1287 // create a minioClient interface implementation 1288 // defining the client to be used 1289 minioClient := minioClient{client: mClient} 1290 1291 var prefix string 1292 if params.Prefix != "" { 1293 encodedPrefix := SanitizeEncodedPrefix(params.Prefix) 1294 decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) 1295 if err != nil { 1296 return ErrorWithContext(ctx, err) 1297 } 1298 prefix = string(decodedPrefix) 1299 } 1300 1301 err = restoreObject(ctx, minioClient, params.BucketName, prefix, params.VersionID) 1302 if err != nil { 1303 return ErrorWithContext(ctx, err) 1304 } 1305 return nil 1306 } 1307 1308 func restoreObject(ctx context.Context, client MinioClient, bucketName, prefix, versionID string) error { 1309 // Select required version 1310 srcOpts := minio.CopySrcOptions{ 1311 Bucket: bucketName, 1312 Object: prefix, 1313 VersionID: versionID, 1314 } 1315 1316 // Destination object, same as current bucket 1317 replaceMetadata := make(map[string]string) 1318 replaceMetadata["copy-source"] = versionID 1319 1320 dstOpts := minio.CopyDestOptions{ 1321 Bucket: bucketName, 1322 Object: prefix, 1323 UserMetadata: replaceMetadata, 1324 } 1325 1326 // Copy object call 1327 _, err := client.copyObject(ctx, dstOpts, srcOpts) 1328 if err != nil { 1329 return err 1330 } 1331 1332 return nil 1333 } 1334 1335 // Metadata Response from minio-go API 1336 func getObjectMetadataResponse(session *models.Principal, params objectApi.GetObjectMetadataParams) (*models.Metadata, *CodedAPIError) { 1337 ctx := params.HTTPRequest.Context() 1338 mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest)) 1339 if err != nil { 1340 return nil, ErrorWithContext(ctx, err) 1341 } 1342 // create a minioClient interface implementation 1343 // defining the client to be used 1344 minioClient := minioClient{client: mClient} 1345 var prefix string 1346 var versionID string 1347 1348 if params.Prefix != "" { 1349 encodedPrefix := SanitizeEncodedPrefix(params.Prefix) 1350 decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix) 1351 if err != nil { 1352 return nil, ErrorWithContext(ctx, err) 1353 } 1354 prefix = string(decodedPrefix) 1355 } 1356 1357 if params.VersionID != nil { 1358 versionID = *params.VersionID 1359 } 1360 1361 objectInfo, err := getObjectInfo(ctx, minioClient, params.BucketName, prefix, versionID) 1362 if err != nil { 1363 return nil, ErrorWithContext(ctx, err) 1364 } 1365 1366 metadata := &models.Metadata{ObjectMetadata: objectInfo.Metadata} 1367 1368 return metadata, nil 1369 } 1370 1371 func getObjectInfo(ctx context.Context, client MinioClient, bucketName, prefix, versionID string) (minio.ObjectInfo, error) { 1372 objectData, err := client.statObject(ctx, bucketName, prefix, minio.GetObjectOptions{VersionID: versionID}) 1373 if err != nil { 1374 return minio.ObjectInfo{}, err 1375 } 1376 1377 return objectData, nil 1378 } 1379 1380 // newClientURL returns an abstracted URL for filesystems and object storage. 1381 func newClientURL(urlStr string) *mc.ClientURL { 1382 scheme, rest := getScheme(urlStr) 1383 if strings.HasPrefix(rest, "//") { 1384 // if rest has '//' prefix, skip them 1385 var authority string 1386 authority, rest = splitSpecial(rest[2:], "/", false) 1387 if rest == "" { 1388 rest = "/" 1389 } 1390 host := getHost(authority) 1391 if host != "" && (scheme == "http" || scheme == "https") { 1392 return &mc.ClientURL{ 1393 Scheme: scheme, 1394 Type: objectStorage, 1395 Host: host, 1396 Path: rest, 1397 SchemeSeparator: "://", 1398 Separator: '/', 1399 } 1400 } 1401 } 1402 return &mc.ClientURL{ 1403 Type: fileSystem, 1404 Path: rest, 1405 Separator: filepath.Separator, 1406 } 1407 } 1408 1409 // Maybe rawurl is of the form scheme:path. (Scheme must be [a-zA-Z][a-zA-Z0-9+-.]*) 1410 // If so, return scheme, path; else return "", rawurl. 1411 func getScheme(rawurl string) (scheme, path string) { 1412 urlSplits := strings.Split(rawurl, "://") 1413 if len(urlSplits) == 2 { 1414 scheme, uri := urlSplits[0], "//"+urlSplits[1] 1415 // ignore numbers in scheme 1416 validScheme := regexp.MustCompile("^[a-zA-Z]+$") 1417 if uri != "" { 1418 if validScheme.MatchString(scheme) { 1419 return scheme, uri 1420 } 1421 } 1422 } 1423 return "", rawurl 1424 } 1425 1426 // Assuming s is of the form [s delimiter s]. 1427 // If so, return s, [delimiter]s or return s, s if cutdelimiter == true 1428 // If no delimiter found return s, "". 1429 func splitSpecial(s string, delimiter string, cutdelimiter bool) (string, string) { 1430 i := strings.Index(s, delimiter) 1431 if i < 0 { 1432 // if delimiter not found return as is. 1433 return s, "" 1434 } 1435 // if delimiter should be removed, remove it. 1436 if cutdelimiter { 1437 return s[0:i], s[i+len(delimiter):] 1438 } 1439 // return split strings with delimiter 1440 return s[0:i], s[i:] 1441 } 1442 1443 // getHost - extract host from authority string, we do not support ftp style username@ yet. 1444 func getHost(authority string) (host string) { 1445 i := strings.LastIndex(authority, "@") 1446 if i >= 0 { 1447 // TODO support, username@password style userinfo, useful for ftp support. 1448 return 1449 } 1450 return authority 1451 }