From 97324efc537b944b26afff0373611ed067640e78 Mon Sep 17 00:00:00 2001 From: P R <25353498+BigUstad@users.noreply.github.com> Date: Mon, 1 Feb 2021 22:03:00 -0800 Subject: [PATCH] Add builder pattern support for CopyObject API. (#485) --- Docs/API.md | 20 +- Minio.Examples/Cases/CopyObject.cs | 26 +- Minio.Examples/Cases/CopyObjectMetadata.cs | 18 +- Minio.Examples/Cases/CopyObjectReplaceTags.cs | 57 +++ Minio.Functional.Tests/FunctionalTest.cs | 142 +++++- Minio/ApiEndpoints/IObjectOperations.cs | 9 + Minio/ApiEndpoints/ObjectOperations.cs | 265 ++++++++---- Minio/DataModel/BucketArgs.cs | 4 +- Minio/DataModel/ObjectOperationsArgs.cs | 404 ++++++++++++++++++ Minio/DataModel/ObjectOperationsResponse.cs | 38 ++ Minio/DataModel/ObjectWriteArgs.cs | 61 +++ Minio/DataModel/Tagging.cs | 16 + 12 files changed, 929 insertions(+), 131 deletions(-) create mode 100644 Minio.Examples/Cases/CopyObjectReplaceTags.cs create mode 100644 Minio/DataModel/ObjectWriteArgs.cs diff --git a/Docs/API.md b/Docs/API.md index 56b8b7808..ff0c44fa5 100644 --- a/Docs/API.md +++ b/Docs/API.md @@ -2075,9 +2075,9 @@ catch(MinioException e) ``` -### CopyObjectAsync(string bucketName, string objectName, string destBucketName, string destObjectName = null, CopyConditions copyConditions = null, Dictionary metadata = null, ServerSideEncryption sseSrc = null, ServerSideEncryption sseDest = null) +### CopyObjectAsync(CopyObjectArgs args) -*`Task CopyObjectAsync(string bucketName, string objectName, string destBucketName, string destObjectName = null, CopyConditions copyConditions = null, Dictionary metadata = null, ServerSideEncryption sseSrc = null, ServerSideEncryption sseDest = null, CancellationToken cancellationToken = default(CancellationToken))`* +*`Task CopyObjectAsync(CopyObjectArgs args, CancellationToken cancellationToken = default(CancellationToken))`* Copies content from objectName to destObjectName. @@ -2087,21 +2087,19 @@ __Parameters__ |Param | Type | Description | |:--- |:--- |:--- | -| ``bucketName`` | _string_ | Name of the source bucket | -| ``objectName`` | _string_ | Object name in the source bucket to be copied | -| ``destBucketName`` | _string_ | Destination bucket name | -| ``destObjectName`` | _string_ | Destination object name to be created, if not provided defaults to source object name| -| ``copyConditions`` | _CopyConditions_ | Map of conditions useful for applying restrictions on copy operation| -| ``metadata`` | _Dictionary_ | Dictionary of meta data headers and their values on the destination side.Defaults to null.| -| ``sseSrc`` | _ServerSideEncryption_ | Server-side encryption option for source object | Optional parameter. Defaults to null | -| ``sseDest`` | _ServerSideEncryption_ | Server-side encryption option for destination object| Optional parameter. Defaults to null | +| ``args`` | _CopyObjectArgs_ | Arguments object - bucket name, object name, destination bucket name, destination object name, copy conditions, metadata, Source SSE, Destination SSE. etc. | | ``cancellationToken``| _System.Threading.CancellationToken_ | Optional parameter. Defaults to default(CancellationToken) | | Return Type | Exceptions | |:--- |:--- | | ``Task`` | Listed Exceptions: | -| | ``InvalidBucketNameException`` : upon invalid bucket name | +| | ``AuthorizationException`` : upon access or secret key wrong or not found | +| | ``InvalidBucketNameException`` : upon invalid bucket name | +| | ``InvalidObjectNameException`` : upon invalid object name | +| | ``BucketNotFoundException`` : upon bucket with name not found | +| | ``ObjectNotFoundException`` : upon object with name not found | +| | ``MalFormedXMLException`` : upon configuration XML in http request validation failure | | | ``ConnectionException`` : upon connection error | | | ``InternalClientException`` : upon internal library error | | | ``ArgumentException`` : upon missing bucket/object names | diff --git a/Minio.Examples/Cases/CopyObject.cs b/Minio.Examples/Cases/CopyObject.cs index 3dc5c02aa..e65e12e57 100644 --- a/Minio.Examples/Cases/CopyObject.cs +++ b/Minio.Examples/Cases/CopyObject.cs @@ -1,5 +1,5 @@ /* - * MinIO .NET Library for Amazon S3 Compatible Cloud Storage, (C) 2017 MinIO, Inc. + * MinIO .NET Library for Amazon S3 Compatible Cloud Storage, (C) 2017-2021 MinIO, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ using Minio.DataModel; using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace Minio.Examples.Cases @@ -34,14 +35,21 @@ public async static Task Run(MinioClient minio, try { Console.WriteLine("Running example for API: CopyObjectAsync"); + var metaData = new Dictionary + { + { "Test-Metadata", "Test Test" } + }; // Optionally pass copy conditions - await minio.CopyObjectAsync(fromBucketName, - fromObjectName, - destBucketName, - destObjectName, - copyConditions: null, - sseSrc: sseSrc, - sseDest: sseDest); + CopySourceObjectArgs cpSrcArgs = new CopySourceObjectArgs() + .WithBucket(fromBucketName) + .WithObject(fromObjectName) + .WithServerSideEncryption(sseSrc); + CopyObjectArgs args = new CopyObjectArgs() + .WithBucket(destBucketName) + .WithObject(destObjectName) + .WithCopyObjectSource(cpSrcArgs) + .WithServerSideEncryption(sseDest); + await minio.CopyObjectAsync(args); Console.WriteLine("Copied object {0} from bucket {1} to bucket {2}", fromObjectName, fromBucketName, destBucketName); Console.WriteLine(); } @@ -51,4 +59,4 @@ await minio.CopyObjectAsync(fromBucketName, } } } -} +} \ No newline at end of file diff --git a/Minio.Examples/Cases/CopyObjectMetadata.cs b/Minio.Examples/Cases/CopyObjectMetadata.cs index 646dc0e74..aa3e33df4 100644 --- a/Minio.Examples/Cases/CopyObjectMetadata.cs +++ b/Minio.Examples/Cases/CopyObjectMetadata.cs @@ -1,5 +1,5 @@ /* - * MinIO .NET Library for Amazon S3 Compatible Cloud Storage, (C) 2018 MinIO, Inc. + * MinIO .NET Library for Amazon S3 Compatible Cloud Storage, (C) 2018-2021 MinIO, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,12 +45,16 @@ public async static Task Run(MinioClient minio, { "Mynewkey", "my-new-value" } }; - await minio.CopyObjectAsync(fromBucketName, - fromObjectName, - destBucketName, - destObjectName, - copyConditions:copyCond, - metadata: metadata); + CopySourceObjectArgs copySourceObjectArgs = new CopySourceObjectArgs() + .WithBucket(fromBucketName) + .WithObject(fromObjectName) + .WithCopyConditions(copyCond); + CopyObjectArgs copyObjectArgs = new CopyObjectArgs() + .WithBucket(destBucketName) + .WithObject(destObjectName) + .WithHeaders(metadata) + .WithCopyObjectSource(copySourceObjectArgs); + await minio.CopyObjectAsync(copyObjectArgs); Console.WriteLine($"Copied object {fromObjectName} from bucket {fromBucketName} to bucket {destBucketName}"); Console.WriteLine(); diff --git a/Minio.Examples/Cases/CopyObjectReplaceTags.cs b/Minio.Examples/Cases/CopyObjectReplaceTags.cs new file mode 100644 index 000000000..875126fcc --- /dev/null +++ b/Minio.Examples/Cases/CopyObjectReplaceTags.cs @@ -0,0 +1,57 @@ +/* + * MinIO .NET Library for Amazon S3 Compatible Cloud Storage, (C) 2017-2021 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading.Tasks; + +namespace Minio.Examples.Cases +{ + class CopyObjectReplaceTags + { + // Copy object from one bucket to another, replace tags in the copied object + public async static Task Run(MinioClient minio, + string fromBucketName = "from-bucket-name", + string fromObjectName = "from-object-name", + string destBucketName = "dest-bucket", + string destObjectName =" to-object-name") + { + try + { + Console.WriteLine("Running example for API: CopyObjectAsync with Tags"); + var tags = new Dictionary + { + { "Test-TagKey", "Test-TagValue" }, + }; + CopySourceObjectArgs cpSrcArgs = new CopySourceObjectArgs() + .WithBucket(fromBucketName) + .WithObject(fromObjectName); + CopyObjectArgs args = new CopyObjectArgs() + .WithBucket(destBucketName) + .WithObject(destObjectName) + .WithTagKeyValuePairs(tags) + .WithReplaceTagsDirective(true) + .WithCopyObjectSource(cpSrcArgs); + await minio.CopyObjectAsync(args).ConfigureAwait(false); + } + catch (Exception e) + { + Console.WriteLine("[Bucket] Exception: {0}", e); + } + } + } +} \ No newline at end of file diff --git a/Minio.Functional.Tests/FunctionalTest.cs b/Minio.Functional.Tests/FunctionalTest.cs index b9c6abd62..a470787c5 100644 --- a/Minio.Functional.Tests/FunctionalTest.cs +++ b/Minio.Functional.Tests/FunctionalTest.cs @@ -57,8 +57,8 @@ public class FunctionalTest private const string putObjectSignature1 = "Task PutObjectAsync(string bucketName, string objectName, Stream data, long size, string contentType, Dictionary metaData=null, CancellationToken cancellationToken = default(CancellationToken))"; private const string putObjectSignature2 = "Task PutObjectAsync(string bucketName, string objectName, string filePath, string contentType=null, Dictionary metaData=null, CancellationToken cancellationToken = default(CancellationToken))"; private const string listenBucketNotificationsSignature = "IObservable ListenBucketNotificationsAsync(ListenBucketNotificationsArgs args, CancellationToken cancellationToken = default(CancellationToken))"; + private const string copyObjectSignature = "Task CopyObjectAsync(CopyObjectArgs args, CancellationToken cancellationToken = default(CancellationToken))"; private const string statObjectSignature = "Task StatObjectAsync(StatObjectArgs args, CancellationToken cancellationToken = default(CancellationToken))"; - private const string copyObjectSignature = "Task CopyObjectAsync(string bucketName, string objectName, string destBucketName, string destObjectName = null, CopyConditions copyConditions = null, CancellationToken cancellationToken = default(CancellationToken))"; private const string removeObjectSignature1 = "Task RemoveObjectAsync(RemoveObjectArgs args, CancellationToken cancellationToken = default(CancellationToken))"; private const string removeObjectSignature2 = "Task> RemoveObjectsAsync(RemoveObjectsArgs, CancellationToken cancellationToken = default(CancellationToken))"; private const string removeIncompleteUploadSignature = "Task RemoveIncompleteUploadAsync(RemoveIncompleteUploadArgs args, CancellationToken cancellationToken = default(CancellationToken))"; @@ -1141,7 +1141,15 @@ await minio.PutObjectAsync(bucketName, filestream, filestream.Length, null); } - await minio.CopyObjectAsync(bucketName, objectName, destBucketName, destObjectName); + CopySourceObjectArgs copySourceObjectArgs = new CopySourceObjectArgs() + .WithBucket(bucketName) + .WithObject(objectName); + CopyObjectArgs copyObjectArgs = new CopyObjectArgs() + .WithCopyObjectSource(copySourceObjectArgs) + .WithBucket(destBucketName) + .WithObject(destObjectName); + + await minio.CopyObjectAsync(copyObjectArgs); GetObjectArgs getObjectArgs = new GetObjectArgs() .WithBucket(destBucketName) @@ -1213,12 +1221,22 @@ await minio.PutObjectAsync(bucketName, conditions.SetMatchETag("TestETag"); try { - await minio.CopyObjectAsync(bucketName, objectName, destBucketName, destObjectName, conditions); + CopySourceObjectArgs copySourceObjectArgs = new CopySourceObjectArgs() + .WithBucket(bucketName) + .WithObject(objectName) + .WithCopyConditions(conditions); + CopyObjectArgs copyObjectArgs = new CopyObjectArgs() + .WithCopyObjectSource(copySourceObjectArgs) + .WithBucket(destBucketName) + .WithObject(destObjectName); + + await minio.CopyObjectAsync(copyObjectArgs); } catch (MinioException ex) { - Assert.AreEqual(ex.Message, "MinIO API responded with message=At least one of the pre-conditions you specified did not hold"); + Console.WriteLine(ex.Message); + StringAssert.Equals(ex.Message, "MinIO API responded with message=At least one of the pre-conditions you specified did not hold"); } RemoveObjectArgs rmArgs = new RemoveObjectArgs() @@ -1278,7 +1296,16 @@ await minio.PutObjectAsync(bucketName, CopyConditions conditions = new CopyConditions(); conditions.SetMatchETag(stats.ETag); - await minio.CopyObjectAsync(bucketName, objectName, destBucketName, destObjectName, conditions); + CopySourceObjectArgs copySourceObjectArgs = new CopySourceObjectArgs() + .WithBucket(bucketName) + .WithObject(objectName) + .WithCopyConditions(conditions); + CopyObjectArgs copyObjectArgs = new CopyObjectArgs() + .WithCopyObjectSource(copySourceObjectArgs) + .WithBucket(destBucketName) + .WithObject(destObjectName); + + await minio.CopyObjectAsync(copyObjectArgs); GetObjectArgs getObjectArgs = new GetObjectArgs() .WithBucket(destBucketName) .WithObject(destObjectName) @@ -1354,7 +1381,14 @@ await minio.PutObjectAsync(bucketName, CopyConditions conditions = new CopyConditions(); conditions.SetMatchETag("TestETag"); // omit dest bucket name. - await minio.CopyObjectAsync(bucketName, objectName, destBucketName); + CopySourceObjectArgs copySourceObjectArgs = new CopySourceObjectArgs() + .WithBucket(bucketName) + .WithObject(objectName); + CopyObjectArgs copyObjectArgs = new CopyObjectArgs() + .WithCopyObjectSource(copySourceObjectArgs) + .WithBucket(destBucketName); + + await minio.CopyObjectAsync(copyObjectArgs); GetObjectArgs getObjectArgs = new GetObjectArgs() .WithBucket(bucketName) @@ -1430,8 +1464,15 @@ await minio.PutObjectAsync(bucketName, conditions.SetByteRange(1024, 6291455); // omit dest object name. - await minio.CopyObjectAsync(bucketName, objectName, destBucketName, copyConditions: conditions); + CopySourceObjectArgs copySourceObjectArgs = new CopySourceObjectArgs() + .WithBucket(bucketName) + .WithObject(objectName) + .WithCopyConditions(conditions); + CopyObjectArgs copyObjectArgs = new CopyObjectArgs() + .WithCopyObjectSource(copySourceObjectArgs) + .WithBucket(destBucketName); + await minio.CopyObjectAsync(copyObjectArgs); GetObjectArgs getObjectArgs = new GetObjectArgs() .WithBucket(bucketName) .WithObject(objectName) @@ -1520,15 +1561,24 @@ await minio.PutObjectAsync(bucketName, CopyConditions conditions = new CopyConditions(); conditions.SetModified(new DateTime(2017, 8, 18)); // Should copy object since modification date header < object modification date. - await minio.CopyObjectAsync(bucketName, objectName, destBucketName, destObjectName, conditions); + CopySourceObjectArgs copySourceObjectArgs = new CopySourceObjectArgs() + .WithBucket(bucketName) + .WithObject(objectName) + .WithCopyConditions(conditions); + CopyObjectArgs copyObjectArgs = new CopyObjectArgs() + .WithCopyObjectSource(copySourceObjectArgs) + .WithBucket(destBucketName) + .WithObject(destObjectName); + + await minio.CopyObjectAsync(copyObjectArgs); GetObjectArgs getObjectArgs = new GetObjectArgs() .WithBucket(destBucketName) .WithObject(destObjectName) .WithFile(outFileName); await minio.GetObjectAsync(getObjectArgs); statObjectArgs = new StatObjectArgs() - .WithBucket(destBucketName) - .WithObject(destObjectName); + .WithBucket(destBucketName) + .WithObject(destObjectName); ObjectStat dstats = await minio.StatObjectAsync(statObjectArgs); Assert.IsNotNull(dstats); StringAssert.Equals(dstats.ObjectName, destObjectName); @@ -1605,8 +1655,15 @@ await minio.PutObjectAsync(bucketName, // Should not copy object since modification date header > object modification date. try { - await minio.CopyObjectAsync(bucketName, objectName, destBucketName, destObjectName, conditions); - + CopySourceObjectArgs copySourceObjectArgs = new CopySourceObjectArgs() + .WithBucket(bucketName) + .WithObject(objectName) + .WithCopyConditions(conditions); + CopyObjectArgs copyObjectArgs = new CopyObjectArgs() + .WithCopyObjectSource(copySourceObjectArgs) + .WithBucket(destBucketName) + .WithObject(destObjectName); + await minio.CopyObjectAsync(copyObjectArgs); } catch (Exception ex) { @@ -1688,13 +1745,26 @@ await minio.PutObjectAsync(bucketName, { "Content-Type", "application/css" }, { "Mynewkey", "test test" } }; - await minio.CopyObjectAsync(bucketName, objectName, destBucketName, destObjectName, copyConditions:copyCond, metadata: metadata); + CopySourceObjectArgs copySourceObjectArgs = new CopySourceObjectArgs() + .WithBucket(bucketName) + .WithObject(objectName) + .WithCopyConditions(copyCond); + CopyObjectArgs copyObjectArgs = new CopyObjectArgs() + .WithCopyObjectSource(copySourceObjectArgs) + .WithBucket(destBucketName) + .WithHeaders(metadata) + .WithObject(destObjectName); + + await minio.CopyObjectAsync(copyObjectArgs); statObjectArgs = new StatObjectArgs() .WithBucket(destBucketName) .WithObject(destObjectName); ObjectStat dstats = await minio.StatObjectAsync(statObjectArgs); + Assert.IsTrue(dstats.MetaData["Content-Type"] != null); Assert.IsTrue(dstats.MetaData["Mynewkey"] != null); + StringAssert.Equals(dstats.MetaData["Content-Type"], "application/css"); + StringAssert.Equals(dstats.MetaData["Mynewkey"], "test test"); RemoveObjectArgs rmArgs = new RemoveObjectArgs() .WithBucket(bucketName) .WithObject(objectName); @@ -1767,8 +1837,16 @@ await minio.PutObjectAsync(bucketName, filestream, filestream.Length, null, sse:ssec); } - await minio.CopyObjectAsync(bucketName, objectName, destBucketName, destObjectName, sseSrc:sseCpy, sseDest:ssecDst); - + CopySourceObjectArgs copySourceObjectArgs = new CopySourceObjectArgs() + .WithBucket(bucketName) + .WithObject(objectName) + .WithServerSideEncryption(sseCpy); + CopyObjectArgs copyObjectArgs = new CopyObjectArgs() + .WithCopyObjectSource(copySourceObjectArgs) + .WithBucket(destBucketName) + .WithObject(destObjectName) + .WithServerSideEncryption(ssecDst); + await minio.CopyObjectAsync(copyObjectArgs); GetObjectArgs getObjectArgs = new GetObjectArgs() .WithBucket(destBucketName) .WithObject(destObjectName) @@ -1841,7 +1919,16 @@ await minio.PutObjectAsync(bucketName, filestream, filestream.Length, null, sse:ssec); } - await minio.CopyObjectAsync(bucketName, objectName, destBucketName, destObjectName, sseSrc:sseCpy, sseDest:null); + CopySourceObjectArgs copySourceObjectArgs = new CopySourceObjectArgs() + .WithBucket(bucketName) + .WithObject(objectName) + .WithServerSideEncryption(sseCpy); + CopyObjectArgs copyObjectArgs = new CopyObjectArgs() + .WithCopyObjectSource(copySourceObjectArgs) + .WithBucket(destBucketName) + .WithObject(destObjectName) + .WithServerSideEncryption(null); + await minio.CopyObjectAsync(copyObjectArgs); GetObjectArgs getObjectArgs = new GetObjectArgs() .WithBucket(destBucketName) @@ -1916,8 +2003,16 @@ await minio.PutObjectAsync(bucketName, filestream, filestream.Length, null, sse:ssec); } - await minio.CopyObjectAsync(bucketName, objectName, destBucketName, destObjectName, sseSrc:sseCpy, sseDest:sses3); - + CopySourceObjectArgs copySourceObjectArgs = new CopySourceObjectArgs() + .WithBucket(bucketName) + .WithObject(objectName) + .WithServerSideEncryption(sseCpy); + CopyObjectArgs copyObjectArgs = new CopyObjectArgs() + .WithCopyObjectSource(copySourceObjectArgs) + .WithBucket(destBucketName) + .WithObject(destObjectName) + .WithServerSideEncryption(sses3); + await minio.CopyObjectAsync(copyObjectArgs); GetObjectArgs getObjectArgs = new GetObjectArgs() .WithBucket(destBucketName) .WithObject(destObjectName) @@ -1987,7 +2082,16 @@ await minio.PutObjectAsync(bucketName, filestream, filestream.Length, null, sse:sses3); } - await minio.CopyObjectAsync(bucketName, objectName, destBucketName, destObjectName, sseSrc:null, sseDest:sses3); + CopySourceObjectArgs copySourceObjectArgs = new CopySourceObjectArgs() + .WithBucket(bucketName) + .WithObject(objectName) + .WithServerSideEncryption(null); + CopyObjectArgs copyObjectArgs = new CopyObjectArgs() + .WithCopyObjectSource(copySourceObjectArgs) + .WithBucket(destBucketName) + .WithObject(destObjectName) + .WithServerSideEncryption(sses3); + await minio.CopyObjectAsync(copyObjectArgs); GetObjectArgs getObjectArgs = new GetObjectArgs() .WithBucket(destBucketName) diff --git a/Minio/ApiEndpoints/IObjectOperations.cs b/Minio/ApiEndpoints/IObjectOperations.cs index 0bd109640..9338e2d89 100644 --- a/Minio/ApiEndpoints/IObjectOperations.cs +++ b/Minio/ApiEndpoints/IObjectOperations.cs @@ -108,6 +108,14 @@ public interface IObjectOperations /// Observable that returns delete error while deleting objects if any Task> RemoveObjectsAsync(RemoveObjectsArgs args, CancellationToken cancellationToken = default(CancellationToken)); + /// + /// Copy a source object into a new destination object. + /// + /// CopyObjectArgs Arguments Object which encapsulates bucket name, object name, destination bucket, destination object names, Copy conditions object, metadata, SSE source, destination objects + /// Optional cancellation token to cancel the operation + /// + Task CopyObjectAsync(CopyObjectArgs args, CancellationToken cancellationToken = default(CancellationToken)); + /// /// Get an object. The object will be streamed to the callback given by the user. /// @@ -215,6 +223,7 @@ public interface IObjectOperations /// Optional Server-side encryption option for destination. Defaults to null. /// Optional cancellation token to cancel the operation /// + [Obsolete("Use CopyObjectAsync method with CopyObjectArgs object. Refer CopyObject example code.")] Task CopyObjectAsync(string bucketName, string objectName, string destBucketName, string destObjectName = null, CopyConditions copyConditions = null, Dictionary metadata = null, ServerSideEncryption sseSrc = null, ServerSideEncryption sseDest = null, CancellationToken cancellationToken = default(CancellationToken)); /// diff --git a/Minio/ApiEndpoints/ObjectOperations.cs b/Minio/ApiEndpoints/ObjectOperations.cs index e46fcc72c..91caac544 100644 --- a/Minio/ApiEndpoints/ObjectOperations.cs +++ b/Minio/ApiEndpoints/ObjectOperations.cs @@ -21,6 +21,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Net; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; @@ -480,6 +481,173 @@ public async Task PresignedPutObjectAsync(PresignedPutObjectArgs args) } + /// + /// Copy a source object into a new destination object. + /// + /// CopyObjectArgs Arguments Object which encapsulates bucket name, object name, destination bucket, destination object names, Copy conditions object, metadata, SSE source, destination objects + /// Optional cancellation token to cancel the operation + /// + public async Task CopyObjectAsync(CopyObjectArgs args, CancellationToken cancellationToken = default(CancellationToken)) + { + StatObjectArgs statArgs = new StatObjectArgs() + .WithBucket(args.CopySourceObject.BucketName) + .WithObject(args.CopySourceObject.ObjectName) + .WithServerSideEncryption(args.CopySourceObject.SSE); + ObjectStat stat = await this.StatObjectAsync(statArgs, cancellationToken: cancellationToken).ConfigureAwait(false); + args.WithCopyObjectSourceStats(stat); + args.Validate(); + bool copyReplaceMeta = (args.CopySourceObject.CopyOperationConditions != null )?args.CopySourceObject.CopyOperationConditions.HasReplaceMetadataDirective() : false; + if (string.IsNullOrEmpty(args.ObjectName)) + { + args.ObjectName = args.CopySourceObject.ObjectName; + } + if (args.CopySourceObject.SSE != null && args.CopySourceObject.SSE is SSECopy sSECopy ) + { + args.SSE = sSECopy.CloneToSSEC(); + } + if (!copyReplaceMeta) + { + args.HeaderMap = args.CopySourceObject.HeaderMap; + } + else + { + args.CopySourceObject.HeaderMap = null; + } + Dictionary meta = new Dictionary(); + if (args.HeaderMap != null) + { + foreach (var item in args.HeaderMap) + { + var key = item.Key; + if (!OperationsUtil.IsSupportedHeader(item.Key) && !item.Key.StartsWith("x-amz-meta", StringComparison.OrdinalIgnoreCase)) + { + key = "x-amz-meta-" + key.ToLowerInvariant(); + } + meta[key] = item.Value; + } + } + args.HeaderMap = args.HeaderMap.Concat(meta).GroupBy(item => item.Key).ToDictionary(item => item.Key, item => item.First().Value); + long srcByteRangeSize = (args.CopySourceObject.CopyOperationConditions != null)? args.CopySourceObject.CopyOperationConditions.GetByteRange():0L; + long copySize = (srcByteRangeSize == 0) ? args.CopySourceObjectInfo.Size : srcByteRangeSize; + if ((srcByteRangeSize > args.CopySourceObjectInfo.Size) || ((srcByteRangeSize > 0) && (args.CopySourceObject.CopyOperationConditions.byteRangeEnd >= args.CopySourceObjectInfo.Size))) + { + throw new ArgumentException("Specified byte range (" + args.CopySourceObject.CopyOperationConditions.byteRangeStart.ToString() + "-" + args.CopySourceObject.CopyOperationConditions.byteRangeEnd.ToString() + ") does not fit within source object (size=" + args.CopySourceObjectInfo.Size.ToString() + ")"); + } + + if ((copySize > Constants.MaxSingleCopyObjectSize) || (srcByteRangeSize > 0 && (srcByteRangeSize != args.CopySourceObjectInfo.Size))) + { + MultipartCopyUploadArgs multiArgs = new MultipartCopyUploadArgs(args) + .WithCopySize(copySize); + await MultipartCopyUploadAsync(multiArgs, cancellationToken).ConfigureAwait(false); + } + else + { + CopyObjectRequestArgs cpReqArgs = new CopyObjectRequestArgs(args) + .WithCopyOperationObjectType(typeof(CopyObjectResult)); + if (cpReqArgs.CopySourceObject.SSE != null && cpReqArgs.CopySourceObject.SSE is SSECopy) + { + cpReqArgs.CopySourceObject.SSE.Marshal(args.HeaderMap); + } + if (cpReqArgs.SSE != null) + { + cpReqArgs.SSE.Marshal(args.HeaderMap); + } + await this.CopyObjectRequestAsync(cpReqArgs, cancellationToken).ConfigureAwait(false); + } + } + + + /// + /// Make a multi part copy upload for objects larger than 5GB or if CopyCondition specifies a byte range. + /// + /// MultipartCopyUploadArgs Arguments object encapsulating destination and source bucket, object names, copy conditions, size, metadata, SSE + /// Optional cancellation token to cancel the operation + private async Task MultipartCopyUploadAsync(MultipartCopyUploadArgs args, CancellationToken cancellationToken = default(CancellationToken)) + { + dynamic multiPartInfo = utils.CalculateMultiPartSize(args.CopySize); + double partSize = multiPartInfo.partSize; + double partCount = multiPartInfo.partCount; + double lastPartSize = multiPartInfo.lastPartSize; + Part[] totalParts = new Part[(int)partCount]; + + args.SSEHeaders = args.SSEHeaders ?? new Dictionary(); + + NewMultipartUploadArgs nmuArgs = new NewMultipartUploadArgs(args); + // No need to resume upload since this is a Server-side copy. Just initiate a new upload. + string uploadId = await this.NewMultipartUploadAsync(nmuArgs, cancellationToken).ConfigureAwait(false); + double expectedReadSize = partSize; + int partNumber; + for (partNumber = 1; partNumber <= partCount; partNumber++) + { + CopyConditions partCondition = args.CopySourceObject.CopyOperationConditions.Clone(); + partCondition.byteRangeStart = (long)partSize * (partNumber - 1) + partCondition.byteRangeStart; + if (partNumber < partCount) + { + partCondition.byteRangeEnd = partCondition.byteRangeStart + (long)partSize - 1; + } + else + { + partCondition.byteRangeEnd = partCondition.byteRangeStart + (long)lastPartSize - 1; + } + + var queryMap = new Dictionary(); + if (!string.IsNullOrEmpty(uploadId) && partNumber > 0) + { + queryMap.Add("uploadId",uploadId); + queryMap.Add("partNumber",partNumber.ToString()); + } + + args.HeaderMap = args.HeaderMap ?? new Dictionary(); + args.HeaderMap["x-amz-copy-source-range"] = "bytes=" + partCondition.byteRangeStart.ToString() + "-" + partCondition.byteRangeEnd.ToString(); + + if (args.CopySourceObject.SSE != null && args.CopySourceObject.SSE is SSECopy) + { + args.CopySourceObject.SSE.Marshal(args.SSEHeaders); + } + if (args.SSE != null) + { + args.SSE.Marshal(args.SSEHeaders); + } + CopyObjectRequestArgs cpPartArgs = new CopyObjectRequestArgs(args) + .WithCopyOperationObjectType(typeof(CopyPartResult)); + CopyPartResult cpPartResult = (CopyPartResult)await this.CopyObjectRequestAsync(cpPartArgs, cancellationToken).ConfigureAwait(false); + + totalParts[partNumber - 1] = new Part { PartNumber = partNumber, ETag = cpPartResult.ETag, Size = (long)expectedReadSize }; + } + + } + + + /// + /// Start a new multi-part upload request + /// + /// NewMultipartUploadArgs arguments object encapsulating bucket name, object name, Headers, SSE Headers + /// Optional cancellation token to cancel the operation + /// + private async Task NewMultipartUploadAsync(NewMultipartUploadArgs args, CancellationToken cancellationToken = default(CancellationToken)) + { + args.Validate(); + RestRequest request = await this.CreateRequest(args).ConfigureAwait(false); + IRestResponse response = await this.ExecuteAsync(this.NoErrorHandlers, request, cancellationToken); + NewMultipartUploadResponse uploadResponse = new NewMultipartUploadResponse(response.StatusCode, response.Content); + return uploadResponse.UploadId; + } + + /// + /// Create the copy request, execute it and return the copy result. + /// + /// CopyObjectRequestArgs Arguments Object encapsulating + /// Optional cancellation token to cancel the operation + private async Task CopyObjectRequestAsync(CopyObjectRequestArgs args, CancellationToken cancellationToken) + { + args.Validate(); + RestRequest request = await this.CreateRequest(args).ConfigureAwait(false); + IRestResponse response = await this.ExecuteAsync(this.NoErrorHandlers, request, cancellationToken).ConfigureAwait(false); + CopyObjectResponse copyObjectResponse = new CopyObjectResponse(response.StatusCode, response.Content, args.CopyOperationObjectType); + return copyObjectResponse.CopyPartRequestResult; + } + + /// /// Get an object. The object will be streamed to the callback given by the user. /// @@ -1170,90 +1338,21 @@ internal async Task ReadFullAsync(Stream data, int currentPartSize) /// Optional destination encryption options.Defaults to null. /// Optional cancellation token to cancel the operation /// - public async Task CopyObjectAsync(string bucketName, string objectName, string destBucketName, string destObjectName = null, CopyConditions copyConditions = null, Dictionary metadata = null, ServerSideEncryption sseSrc = null, ServerSideEncryption sseDest = null, CancellationToken cancellationToken = default(CancellationToken)) + [Obsolete("Use CopyObjectAsync method with CopyObjectArgs object. Refer CopyObject example code.")] + public Task CopyObjectAsync(string bucketName, string objectName, string destBucketName, string destObjectName = null, CopyConditions copyConditions = null, Dictionary metadata = null, ServerSideEncryption sseSrc = null, ServerSideEncryption sseDest = null, CancellationToken cancellationToken = default(CancellationToken)) { - if (bucketName == null) - { - throw new ArgumentException("Source bucket name cannot be empty", nameof(bucketName)); - } - if (objectName == null) - { - throw new ArgumentException("Source object name cannot be empty", nameof(objectName)); - } - if (destBucketName == null) - { - throw new ArgumentException("Destination bucket name cannot be empty", nameof(destBucketName)); - } - // Escape source object path. - string sourceObjectPath = $"{bucketName}/{utils.UrlEncode(objectName)}"; - - // Destination object name is optional, if empty default to source object name. - if (destObjectName == null) - { - destObjectName = objectName; - } - - ServerSideEncryption sseGet = sseSrc; - if (sseSrc is SSECopy sseCpy) - { - sseGet = sseCpy.CloneToSSEC(); - } - // Get Stats on the source object - StatObjectArgs statArgs = new StatObjectArgs() - .WithBucket(bucketName) - .WithObject(objectName) - .WithServerSideEncryption(sseGet); - ObjectStat srcStats = await this.StatObjectAsync(statArgs, cancellationToken: cancellationToken).ConfigureAwait(false); - // Copy metadata from the source object if no metadata replace directive - var meta = new Dictionary(StringComparer.OrdinalIgnoreCase); - Dictionary m = metadata; - if (copyConditions != null && !copyConditions.HasReplaceMetadataDirective()) - { - m = srcStats.MetaData; - } - - if (m != null) - { - foreach (var item in m) - { - var key = item.Key; - if (!OperationsUtil.IsSupportedHeader(key) && !key.StartsWith("x-amz-meta-", StringComparison.OrdinalIgnoreCase)) - { - key = "x-amz-meta-" + key.ToLowerInvariant(); - } - meta[key] = item.Value; - } - } - - long srcByteRangeSize = 0L; - - if (copyConditions != null) - { - srcByteRangeSize = copyConditions.GetByteRange(); - } - long copySize = (srcByteRangeSize == 0) ? srcStats.Size : srcByteRangeSize; - - if ((srcByteRangeSize > srcStats.Size) || ((srcByteRangeSize > 0) && (copyConditions.byteRangeEnd >= srcStats.Size))) - { - throw new ArgumentException("Specified byte range (" + copyConditions.byteRangeStart.ToString() + "-" + copyConditions.byteRangeEnd.ToString() + ") does not fit within source object (size=" + srcStats.Size.ToString() + ")"); - } - - if ((copySize > Constants.MaxSingleCopyObjectSize) || (srcByteRangeSize > 0 && (srcByteRangeSize != srcStats.Size))) - { - await MultipartCopyUploadAsync(bucketName, objectName, destBucketName, destObjectName, copyConditions, copySize, meta, sseSrc, sseDest, cancellationToken).ConfigureAwait(false); - } - else - { - if (sseSrc != null && sseSrc is SSECopy) - { - sseSrc.Marshal(meta); - } - if (sseDest != null) - { - sseDest.Marshal(meta); - } - await this.CopyObjectRequestAsync(bucketName, objectName, destBucketName, destObjectName, copyConditions, meta, null, cancellationToken, typeof(CopyObjectResult)).ConfigureAwait(false); - } + CopySourceObjectArgs cpSrcArgs = new CopySourceObjectArgs() + .WithBucket(bucketName) + .WithObject(objectName) + .WithCopyConditions(copyConditions) + .WithServerSideEncryption(sseSrc); + CopyObjectArgs args = new CopyObjectArgs() + .WithBucket(destBucketName) + .WithObject(destObjectName) + .WithCopyObjectSource(cpSrcArgs) + .WithHeaders(metadata) + .WithServerSideEncryption(sseDest); + return this.CopyObjectAsync(args, cancellationToken); } /// diff --git a/Minio/DataModel/BucketArgs.cs b/Minio/DataModel/BucketArgs.cs index e194020a2..2acf56490 100644 --- a/Minio/DataModel/BucketArgs.cs +++ b/Minio/DataModel/BucketArgs.cs @@ -37,14 +37,14 @@ public T WithBucket(string bucket) public T WithHeaders(Dictionary headers) { - if (headers == null || headers.Count > 0) + if (headers == null || headers.Count <= 0) { return (T)this; } this.HeaderMap = this.HeaderMap ?? new Dictionary(); foreach (string key in headers.Keys) { - this.HeaderMap.Add(key, headers[key]); + this.HeaderMap[key] = headers[key]; } return (T)this; } diff --git a/Minio/DataModel/ObjectOperationsArgs.cs b/Minio/DataModel/ObjectOperationsArgs.cs index e4ee094a5..c374eb2b9 100644 --- a/Minio/DataModel/ObjectOperationsArgs.cs +++ b/Minio/DataModel/ObjectOperationsArgs.cs @@ -21,6 +21,7 @@ using System.Globalization; using System.Xml.Linq; using System.Xml; +using System.Linq; using Minio.DataModel; using Minio.Exceptions; @@ -250,6 +251,7 @@ public override void Validate() } } + public class PresignedPostPolicyArgs : ObjectArgs { internal PostPolicy Policy { get; set; } @@ -853,4 +855,406 @@ public override RestRequest BuildRequest(RestRequest request) return request; } } + + public class CopySourceObjectArgs : ObjectQueryArgs + { + internal string CopySourceObjectPath { get; set; } + internal CopyConditions CopyOperationConditions { get; set; } + public CopySourceObjectArgs() + { + this.RequestMethod = Method.PUT; + this.CopyOperationConditions = new CopyConditions(); + } + + public override void Validate() + { + base.Validate(); + } + + public CopySourceObjectArgs WithCopyConditions(CopyConditions cp) + { + this.CopyOperationConditions = (cp != null)? cp.Clone() : new CopyConditions(); + return this; + } + } + + internal class CopyObjectRequestArgs : ObjectWriteArgs + { + internal CopySourceObjectArgs CopySourceObject { get; set; } + internal ObjectStat CopySourceObjectInfo { get; set; } + internal Type CopyOperationObjectType { get; set; } + internal bool ReplaceTagsDirective { get; set; } + internal bool ReplaceMetadataDirective { get; set; } + internal string StorageClass { get; set; } + + public CopyObjectRequestArgs(CopyObjectArgs cpArgs) + { + if (cpArgs == null || cpArgs.CopySourceObject == null) + { + string message = (cpArgs == null)? $"The constructor of " + nameof(CopyObjectRequestArgs) + "initialized with arguments of CopyObjectArgs null." : + $"The constructor of " + nameof(CopyObjectRequestArgs) + "initialized with arguments of CopyObjectArgs type but with " + nameof(cpArgs.CopySourceObject) + " not initialized."; + throw new InvalidOperationException(message); + } + this.CopySourceObject = new CopySourceObjectArgs(); + this.CopySourceObject.BucketName = cpArgs.CopySourceObject.BucketName; + this.CopySourceObject.ObjectName = cpArgs.CopySourceObject.ObjectName; + this.CopySourceObject.VersionId = cpArgs.CopySourceObject.VersionId; + this.CopySourceObject.CopyOperationConditions = cpArgs.CopySourceObject.CopyOperationConditions.Clone(); + if (cpArgs.CopySourceObject.HeaderMap != null) + { + this.CopySourceObject.HeaderMap = this.CopySourceObject.HeaderMap ?? new Dictionary(); + this.CopySourceObject.HeaderMap = this.CopySourceObject.HeaderMap.Concat(cpArgs.CopySourceObject.HeaderMap).GroupBy(item => item.Key).ToDictionary(item => item.Key, item => item.First().Value); + } + this.HeaderMap = (cpArgs.HeaderMap != null)?new Dictionary(cpArgs.HeaderMap):new Dictionary(); + this.CopySourceObjectInfo = cpArgs.CopySourceObjectInfo; + if (cpArgs.CopySourceObjectInfo.MetaData != null && cpArgs.CopySourceObjectInfo.MetaData.Count > 0) + { + this.HeaderMap = this.HeaderMap.Concat(cpArgs.CopySourceObjectInfo.MetaData).GroupBy(item => item.Key).ToDictionary(item => item.Key, item => item.First().Value); + } + this.CopySourceObject.SSE = cpArgs.CopySourceObject.SSE; + this.RequestMethod = Method.PUT; + this.BucketName = cpArgs.BucketName; + this.ObjectName = cpArgs.ObjectName; + this.HeaderMap = cpArgs.HeaderMap ?? new Dictionary(); + if (this.CopySourceObject.HeaderMap != null) + { + this.HeaderMap = this.HeaderMap.Concat(this.CopySourceObject.HeaderMap).GroupBy(item => item.Key).ToDictionary(item => item.Key, item => item.First().Value); + } + + this.RequestBody = cpArgs.RequestBody; + this.SSE = cpArgs.SSE; + this.SSEHeaders = cpArgs.SSEHeaders ?? new Dictionary(); + if (this.CopySourceObject.SSEHeaders != null) + { + this.SSEHeaders = this.SSEHeaders.Concat(this.CopySourceObject.SSEHeaders).GroupBy(item => item.Key).ToDictionary(item => item.Key, item => item.First().Value); + } + this.VersionId = cpArgs.VersionId; + this.ObjectTags = (cpArgs.ObjectTags != null && cpArgs.ObjectTags.TaggingSet.Tag.Count > 0)?cpArgs.ObjectTags:null; + this.ReplaceTagsDirective = cpArgs.ReplaceTagsDirective; + } + + public CopyObjectRequestArgs(MultipartCopyUploadArgs mcArgs) + { + if (mcArgs == null) + { + throw new InvalidOperationException("The copy source object needed for copy operation is not initialized."); + } + mcArgs.Validate(); + + this.CopySourceObject = new CopySourceObjectArgs(); + this.CopySourceObject.BucketName = mcArgs.BucketName; + this.CopySourceObject.ObjectName = mcArgs.ObjectName; + this.CopySourceObject.HeaderMap = mcArgs.HeaderMap; + this.CopySourceObject.MatchETag = mcArgs.CopySourceObject.MatchETag; + this.CopySourceObject.ModifiedSince = mcArgs.CopySourceObject.ModifiedSince; + this.CopySourceObject.NotMatchETag = mcArgs.CopySourceObject.NotMatchETag; + this.CopySourceObject.UnModifiedSince = mcArgs.CopySourceObject.UnModifiedSince; + this.CopySourceObject.CopyOperationConditions = mcArgs.CopySourceObject.CopyOperationConditions; + + this.BucketName = mcArgs.BucketName; + this.ObjectName = mcArgs.ObjectName ?? mcArgs.CopySourceObject.ObjectName; + + this.HeaderMap = mcArgs.HeaderMap; + } + + public override RestRequest BuildRequest(RestRequest request) + { + request = base.BuildRequest(request); + string sourceObjectPath = this.CopySourceObject.BucketName + "/" + utils.UrlEncode(this.CopySourceObject.ObjectName); + if(!string.IsNullOrEmpty(this.CopySourceObject.VersionId)) + { + sourceObjectPath += "?versionId=" + this.CopySourceObject.VersionId; + } + // Set the object source + request.AddHeader("x-amz-copy-source", sourceObjectPath); + + if (this.HeaderMap != null) + { + foreach (var hdr in this.HeaderMap) + { + request.AddQueryParameter(hdr.Key,hdr.Value); + } + } + + if (this.CopySourceObject.CopyOperationConditions != null) + { + foreach (var item in this.CopySourceObject.CopyOperationConditions.GetConditions()) + { + request.AddHeader(item.Key, item.Value); + } + } + if (!string.IsNullOrEmpty(this.MatchETag)) + { + request.AddOrUpdateParameter("x-amz-copy-source-if-match", this.MatchETag, ParameterType.HttpHeader); + } + if (!string.IsNullOrEmpty(this.NotMatchETag)) + { + request.AddOrUpdateParameter("x-amz-copy-source-if-none-match", this.NotMatchETag, ParameterType.HttpHeader); + } + if (this.ModifiedSince != null && this.ModifiedSince != default(DateTime)) + { + request.AddOrUpdateParameter("x-amz-copy-source-if-unmodified-since", this.ModifiedSince, ParameterType.HttpHeader); + } + if(this.UnModifiedSince != null && this.UnModifiedSince != default(DateTime)) + { + request.AddOrUpdateParameter("x-amz-copy-source-if-modified-since", this.UnModifiedSince, ParameterType.HttpHeader); + } + if (this.ObjectTags != null && this.ObjectTags.TaggingSet != null + && this.ObjectTags.TaggingSet.Tag.Count > 0) + { + request.AddOrUpdateParameter("x-amz-tagging", this.ObjectTags.GetTagString(), ParameterType.HttpHeader); + request.AddOrUpdateParameter("x-amz-tagging-directive", + this.ReplaceTagsDirective?"REPLACE":"COPY", + ParameterType.HttpHeader); + } + if (this.ReplaceMetadataDirective) + { + request.AddOrUpdateParameter("x-amz-metadata-directive", "REPLACE", ParameterType.HttpHeader); + } + if (!string.IsNullOrEmpty(this.StorageClass)) + { + request.AddOrUpdateParameter("x-amz-storage-class", this.StorageClass, ParameterType.HttpHeader); + } + + return request; + } + + public CopyObjectRequestArgs WithCopyOperationObjectType(Type cp) + { + this.CopyOperationObjectType = cp; + return this; + } + + public override void Validate() + { + base.Validate(); + if (this.CopySourceObject == null) + { + throw new InvalidOperationException(nameof(this.CopySourceObject) + " has not been assigned."); + } + } + } + + public class CopyObjectArgs : ObjectWriteArgs + { + internal CopySourceObjectArgs CopySourceObject { get; set; } + internal ObjectStat CopySourceObjectInfo { get; set; } + internal bool ReplaceTagsDirective { get; set; } + internal bool ReplaceMetadataDirective { get; set; } + internal string StorageClass { get; set; } + + public CopyObjectArgs() + { + this.RequestMethod = Method.PUT; + this.CopySourceObject = new CopySourceObjectArgs(); + this.ReplaceTagsDirective = false; + } + + public override void Validate() + { + // We don't need to call base validate. + // If object name is empty we default to source object name. + this.CopySourceObject.Validate(); + utils.ValidateBucketName(this.BucketName); + if (this.CopySourceObject == null) + { + throw new InvalidOperationException(nameof(this.CopySourceObject) + " has not been assigned. Please use " + nameof(this.WithCopyObjectSource)); + } + if (this.CopySourceObjectInfo == null) + { + throw new InvalidOperationException("StatObject result for the copy source object needed to continue copy operation. Use " + nameof(WithCopyObjectSourceStats) + " to initialize StatObject result."); + } + } + + public CopyObjectArgs WithCopyObjectSource(CopySourceObjectArgs cs) + { + if (cs == null) + { + throw new InvalidOperationException("The copy source object needed for copy operation is not initialized."); + } + cs.Validate(); + + this.CopySourceObject = this.CopySourceObject ?? new CopySourceObjectArgs(); + this.CopySourceObject.BucketName = cs.BucketName; + this.CopySourceObject.ObjectName = cs.ObjectName; + this.CopySourceObject.VersionId = cs.VersionId; + this.CopySourceObject.RequestMethod = Method.PUT; + this.CopySourceObject.SSE = cs.SSE; + this.CopySourceObject.SSEHeaders = cs.SSEHeaders; + this.CopySourceObject.HeaderMap = cs.HeaderMap; + this.CopySourceObject.MatchETag = cs.MatchETag; + this.CopySourceObject.ModifiedSince = cs.ModifiedSince; + this.CopySourceObject.NotMatchETag = cs.NotMatchETag; + this.CopySourceObject.UnModifiedSince = cs.UnModifiedSince; + this.CopySourceObject.CopySourceObjectPath = $"{cs.BucketName}/{utils.UrlEncode(cs.ObjectName)}"; + this.CopySourceObject.CopyOperationConditions = cs.CopyOperationConditions?.Clone(); + return this; + } + + public CopyObjectArgs WithReplaceTagsDirective(bool replace) + { + this.ReplaceTagsDirective = replace; + return this; + } + internal CopyObjectArgs WithCopyObjectSourceStats(ObjectStat info) + { + this.CopySourceObjectInfo = info; + if (info.MetaData != null) + { + this.CopySourceObject.HeaderMap = this.CopySourceObject.HeaderMap ?? new Dictionary(); + this.CopySourceObject.HeaderMap = this.CopySourceObject.HeaderMap.Concat(info.MetaData).GroupBy(item => item.Key).ToDictionary(item => item.Key, item => item.First().Value); + } + return this; + } + + public CopyObjectArgs WithStorageClass(string storageClass) + { + this.StorageClass = storageClass; + return this; + } + + public override RestRequest BuildRequest(RestRequest request) + { + request = base.BuildRequest(request); + foreach (var hdr in this.SSEHeaders) + { + this.HeaderMap[hdr.Key] = hdr.Value; + } + foreach (var hdr in this.CopySourceObject.SSEHeaders) + { + this.HeaderMap[hdr.Key] = hdr.Value; + } + if (!string.IsNullOrEmpty(this.MatchETag)) + { + request.AddOrUpdateParameter("x-amz-copy-source-if-match", this.MatchETag, ParameterType.HttpHeader); + } + if (!string.IsNullOrEmpty(this.NotMatchETag)) + { + request.AddOrUpdateParameter("x-amz-copy-source-if-none-match", this.NotMatchETag, ParameterType.HttpHeader); + } + if (this.ModifiedSince != null && this.ModifiedSince != default(DateTime)) + { + request.AddOrUpdateParameter("x-amz-copy-source-if-unmodified-since", this.ModifiedSince, ParameterType.HttpHeader); + } + if(this.UnModifiedSince != null && this.UnModifiedSince != default(DateTime)) + { + request.AddOrUpdateParameter("x-amz-copy-source-if-modified-since", this.UnModifiedSince, ParameterType.HttpHeader); + } + if( !string.IsNullOrEmpty(this.VersionId) ) + { + request.AddQueryParameter("versionId", this.VersionId); + } + if (this.ObjectTags != null && this.ObjectTags.TaggingSet != null + && this.ObjectTags.TaggingSet.Tag.Count > 0) + { + request.AddOrUpdateParameter("x-amz-tagging", this.ObjectTags.GetTagString(), ParameterType.HttpHeader); + request.AddOrUpdateParameter("x-amz-tagging-directive", + this.ReplaceTagsDirective?"COPY":"REPLACE", + ParameterType.HttpHeader); + } + if (this.ReplaceMetadataDirective) + { + request.AddOrUpdateParameter("x-amz-metadata-directive", "REPLACE", ParameterType.HttpHeader); + } + if (!string.IsNullOrEmpty(this.StorageClass)) + { + request.AddOrUpdateParameter("x-amz-storage-class", this.StorageClass, ParameterType.HttpHeader); + } + return request; + } + } + + public class NewMultipartUploadArgs: EncryptionArgs + { + public NewMultipartUploadArgs() + { + this.RequestMethod = Method.POST; + } + + public NewMultipartUploadArgs(MultipartCopyUploadArgs args) + { + // destBucketName, destObjectName, metadata, sseHeaders + this.BucketName = args.BucketName; + this.ObjectName = args.ObjectName; + this.HeaderMap = args.HeaderMap; + this.SSE = args.SSE; + this.SSE?.Marshal(this.SSEHeaders); + } + + public override RestRequest BuildRequest(RestRequest request) + { + request = base.BuildRequest(request); + request.AddQueryParameter("uploads",""); + return request; + } + } + + public class MultipartCopyUploadArgs : ObjectVersionArgs + { + internal CopySourceObjectArgs CopySourceObject { get; set; } + internal ObjectStat CopySourceObjectInfo { get; set; } + internal long CopySize { get; set; } + public MultipartCopyUploadArgs(CopyObjectArgs cpArgs) + { + if (cpArgs == null || cpArgs.CopySourceObject == null) + { + string message = (cpArgs == null)? $"The constructor of " + nameof(CopyObjectRequestArgs) + "initialized with arguments of CopyObjectArgs null." : + $"The constructor of " + nameof(CopyObjectRequestArgs) + "initialized with arguments of CopyObjectArgs type but with " + nameof(cpArgs.CopySourceObject) + " not initialized."; + throw new InvalidOperationException(message); + } + this.RequestMethod = Method.PUT; + + this.CopySourceObject = new CopySourceObjectArgs(); + this.CopySourceObject.BucketName = cpArgs.CopySourceObject.BucketName; + this.CopySourceObject.ObjectName = cpArgs.CopySourceObject.ObjectName; + this.CopySourceObject.CopyOperationConditions = cpArgs.CopySourceObject.CopyOperationConditions.Clone(); + if (cpArgs.CopySourceObject.HeaderMap != null) + { + this.CopySourceObject.HeaderMap = new Dictionary(); + this.CopySourceObject.HeaderMap = this.CopySourceObject.HeaderMap.Concat(cpArgs.CopySourceObject.HeaderMap).GroupBy(item => item.Key).ToDictionary(item => item.Key, item => item.First().Value); + } + this.CopySourceObject.MatchETag = cpArgs.CopySourceObject.MatchETag; + this.CopySourceObject.ModifiedSince = cpArgs.CopySourceObject.ModifiedSince; + this.CopySourceObject.NotMatchETag = cpArgs.CopySourceObject.NotMatchETag; + this.CopySourceObject.UnModifiedSince = cpArgs.CopySourceObject.UnModifiedSince; + this.CopySourceObject.HeaderMap = this.CopySourceObject.HeaderMap ?? new Dictionary(); + this.CopySourceObject.HeaderMap = this.CopySourceObject.HeaderMap.Concat(cpArgs.CopySourceObject.HeaderMap).GroupBy(item => item.Key).ToDictionary(item => item.Key, item => item.First().Value); + this.CopySourceObject.CopyOperationConditions = cpArgs.CopySourceObject.CopyOperationConditions?.Clone(); + + this.BucketName = cpArgs.BucketName; + this.ObjectName = cpArgs.ObjectName; + + this.HeaderMap = cpArgs.HeaderMap; + this.SSE = cpArgs.SSE; + this.SSE?.Marshal(this.SSEHeaders); + + this.HeaderMap = this.HeaderMap ?? new Dictionary(); + this.HeaderMap = this.HeaderMap.Concat(cpArgs.HeaderMap).GroupBy(item => item.Key).ToDictionary(item => item.Key, item => item.First().Value); + this.VersionId = cpArgs.VersionId; + this.CopySourceObjectInfo = cpArgs.CopySourceObjectInfo; + if (this.CopySourceObjectInfo.MetaData != null && this.CopySourceObjectInfo.MetaData.Count > 0) + { + this.HeaderMap = this.HeaderMap ?? new Dictionary(); + this.HeaderMap = this.CopySourceObject.HeaderMap.Concat(cpArgs.CopySourceObject.HeaderMap).GroupBy(item => item.Key).ToDictionary(item => item.Key, item => item.First().Value); + } + } + + internal MultipartCopyUploadArgs() + { + this.RequestMethod = Method.PUT; + } + + internal MultipartCopyUploadArgs WithCopySize(long copySize) + { + this.CopySize = copySize; + return this; + } + + public override RestRequest BuildRequest(RestRequest request) + { + request = base.BuildRequest(request); + return request; + } + + } } \ No newline at end of file diff --git a/Minio/DataModel/ObjectOperationsResponse.cs b/Minio/DataModel/ObjectOperationsResponse.cs index cfdbd0f19..ff6945f0f 100644 --- a/Minio/DataModel/ObjectOperationsResponse.cs +++ b/Minio/DataModel/ObjectOperationsResponse.cs @@ -168,4 +168,42 @@ public GetRetentionResponse(HttpStatusCode statusCode, string responseContent) } } } + + + internal class CopyObjectResponse : GenericResponse + { + internal CopyObjectResult CopyObjectRequestResult { get; set; } + internal CopyPartResult CopyPartRequestResult { get; set; } + + public CopyObjectResponse(HttpStatusCode statusCode, string content, Type reqType) + : base(statusCode, content) + { + using (var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(content))) + { + if (reqType == typeof(CopyObjectResult)) + { + this.CopyObjectRequestResult = (CopyObjectResult)new XmlSerializer(typeof(CopyObjectResult)).Deserialize(stream); + } + else + { + this.CopyPartRequestResult = (CopyPartResult)new XmlSerializer(typeof(CopyPartResult)).Deserialize(stream); + } + } + } + } + + internal class NewMultipartUploadResponse: GenericResponse + { + internal string UploadId { get; private set; } + internal NewMultipartUploadResponse(HttpStatusCode statusCode, string responseContent) + : base(statusCode, responseContent) + { + InitiateMultipartUploadResult newUpload = null; + using (var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(responseContent))) + { + newUpload = (InitiateMultipartUploadResult)new XmlSerializer(typeof(InitiateMultipartUploadResult)).Deserialize(stream); + } + this.UploadId = newUpload.UploadId; + } + } } \ No newline at end of file diff --git a/Minio/DataModel/ObjectWriteArgs.cs b/Minio/DataModel/ObjectWriteArgs.cs new file mode 100644 index 000000000..d517e3702 --- /dev/null +++ b/Minio/DataModel/ObjectWriteArgs.cs @@ -0,0 +1,61 @@ +/* + * MinIO .NET Library for Amazon S3 Compatible Cloud Storage, (C) 2020 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Generic; + +using Minio.DataModel; + +namespace Minio +{ + public abstract class ObjectWriteArgs : ObjectQueryArgs + where T : ObjectWriteArgs + { + internal Tagging ObjectTags { get; set; } + internal ObjectRetentionConfiguration Retention { get; set; } + internal bool? LegalHoldEnabled { get; set; } + internal string ContentType { get; set; } + + public T WithTagKeyValuePairs(Dictionary kv) + { + this.ObjectTags = Tagging.GetObjectTags(kv); + return (T)this; + } + + public T WithTagging(Tagging tagging) + { + this.ObjectTags = tagging; + return (T)this; + } + + public T WithContentType(string type) + { + this.ContentType = type; + return (T)this; + } + + public T WithRetentionConfiguration(ObjectRetentionConfiguration retentionConfiguration) + { + this.Retention = retentionConfiguration; + return (T)this; + } + + public T WithLegalHold(bool? legalHold) + { + this.LegalHoldEnabled = legalHold; + return (T)this; + } + } +} \ No newline at end of file diff --git a/Minio/DataModel/Tagging.cs b/Minio/DataModel/Tagging.cs index 2b98602f5..7e66d6283 100644 --- a/Minio/DataModel/Tagging.cs +++ b/Minio/DataModel/Tagging.cs @@ -158,5 +158,21 @@ public static Tagging GetObjectTags(Dictionary tags) { return new Tagging(tags, true); } + + public string GetTagString() + { + if (this.TaggingSet == null || this.TaggingSet.Tag == null && this.TaggingSet.Tag.Count == 0) + { + return null; + } + string tagStr = ""; + int i = 0; + foreach(var tag in this.TaggingSet.Tag) + { + string append = (i++ < (this.TaggingSet.Tag.Count - 1))?"&":""; + tagStr += tag.Key + "=" + tag.Value + append; + } + return tagStr; + } } } \ No newline at end of file