diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx index 69f00e124..08f706451 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx @@ -661,7 +661,7 @@ const ListObjects = ({ uploadUrl = `${uploadUrl}?prefix=${encodedPath}`; } - const identity = btoa( + const identity = encodeFileName( `${bucketName}-${encodedPath}-${new Date().getTime()}-${Math.random()}` ); @@ -754,7 +754,7 @@ const ListObjects = ({ }; const downloadObject = (object: BucketObject | RewindObject) => { - const identityDownload = btoa( + const identityDownload = encodeFileName( `${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}` ); diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx index f35b6c51f..2c8fdaa2e 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx @@ -391,7 +391,7 @@ const ObjectDetails = ({ }; const downloadObject = (object: IFileInfo) => { - const identityDownload = btoa( + const identityDownload = encodeFileName( `${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}` ); diff --git a/restapi/client.go b/restapi/client.go index 88da77def..c68291559 100644 --- a/restapi/client.go +++ b/restapi/client.go @@ -377,23 +377,36 @@ func newMinioClient(claims *models.Principal) (*minio.Client, error) { return minioClient, nil } +// computeObjectURLWithoutEncode returns a MinIO url containing the object filename without encoding +func computeObjectURLWithoutEncode(bucketName, prefix string) (string, error) { + endpoint := getMinIOServer() + u, err := url.Parse(endpoint) + if err != nil { + return "", fmt.Errorf("the provided endpoint is invalid") + } + objectURL := fmt.Sprintf("%s:%s", u.Hostname(), u.Port()) + if strings.TrimSpace(bucketName) != "" { + objectURL = path.Join(objectURL, bucketName) + } + if strings.TrimSpace(prefix) != "" { + objectURL = path.Join(objectURL, prefix) + } + + objectURL = fmt.Sprintf("%s://%s", u.Scheme, objectURL) + return objectURL, nil +} + // newS3BucketClient creates a new mc S3Client to talk to the server based on a bucket func newS3BucketClient(claims *models.Principal, bucketName string, prefix string) (*mc.S3Client, error) { if claims == nil { return nil, fmt.Errorf("the provided credentials are invalid") } - endpoint := getMinIOServer() - u, err := url.Parse(endpoint) + // It's very important to avoid encoding the prefix since the minio client will encode the path itself + objectURL, err := computeObjectURLWithoutEncode(bucketName, prefix) if err != nil { return nil, fmt.Errorf("the provided endpoint is invalid") } - if strings.TrimSpace(bucketName) != "" { - u.Path = path.Join(u.Path, bucketName) - } - if strings.TrimSpace(prefix) != "" { - u.Path = path.Join(u.Path, prefix) - } - s3Config := newS3Config(u.String(), claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken, false) + s3Config := newS3Config(objectURL, claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken, false) client, pErr := mc.S3New(s3Config) if pErr != nil { return nil, pErr.Cause diff --git a/restapi/client_test.go b/restapi/client_test.go new file mode 100644 index 000000000..ffd24c66a --- /dev/null +++ b/restapi/client_test.go @@ -0,0 +1,90 @@ +// This file is part of MinIO Orchestrator +// Copyright (c) 2021 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package restapi + +import "testing" + +func Test_computeObjectURLWithoutEncode(t *testing.T) { + type args struct { + bucketName string + prefix string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "http://localhost:9000/bucket-1/小飼弾小飼弾小飼弾.jp", + args: args{ + bucketName: "bucket-1", + prefix: "小飼弾小飼弾小飼弾.jpg", + }, + want: "http://localhost:9000/bucket-1/小飼弾小飼弾小飼弾.jpg", + wantErr: false, + }, + { + name: "http://localhost:9000/bucket-1/a a - a a & a a - a a a.jpg", + args: args{ + bucketName: "bucket-1", + prefix: "a a - a a & a a - a a a.jpg", + }, + want: "http://localhost:9000/bucket-1/a a - a a & a a - a a a.jpg", + wantErr: false, + }, + { + name: "http://localhost:9000/bucket-1/02%20-%20FLY%20ME%20TO%20THE%20MOON%20.jpg", + args: args{ + bucketName: "bucket-1", + prefix: "02%20-%20FLY%20ME%20TO%20THE%20MOON%20.jpg", + }, + want: "http://localhost:9000/bucket-1/02%20-%20FLY%20ME%20TO%20THE%20MOON%20.jpg", + wantErr: false, + }, + { + name: "http://localhost:9000/bucket-1/!@#$%^&*()_+.jpg", + args: args{ + bucketName: "bucket-1", + prefix: "!@#$%^&*()_+.jpg", + }, + want: "http://localhost:9000/bucket-1/!@#$%^&*()_+.jpg", + wantErr: false, + }, + { + name: "http://localhost:9000/bucket-1/test/test2/小飼弾小飼弾小飼弾.jpg", + args: args{ + bucketName: "bucket-1", + prefix: "test/test2/小飼弾小飼弾小飼弾.jpg", + }, + want: "http://localhost:9000/bucket-1/test/test2/小飼弾小飼弾小飼弾.jpg", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := computeObjectURLWithoutEncode(tt.args.bucketName, tt.args.prefix) + if (err != nil) != tt.wantErr { + t.Errorf("computeObjectURLWithoutEncode() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("computeObjectURLWithoutEncode() got = %v, want %v", got, tt.want) + } + }) + } +}