Skip to content

Commit 17848f4

Browse files
author
Sean O'Brien
committed
remove copy-props
1 parent 8efcfeb commit 17848f4

7 files changed

Lines changed: 146 additions & 197 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## 3.385.0 - 2026-06-16
44

5-
* `Aws\S3` - Added support for copying tags and annotations to the destination object in `MultipartCopy`. Set `copy_props` to `default` to copy metadata, tags, and annotations. Set `tags_directive` and `annotations_directive` to override individually. Tag and annotation work runs only when explicitly opted in to preserve backwards compatibility.
5+
* `Aws\S3` - Added support for copying tags and annotations to the destination object in `MultipartCopy`. Set `tags_directive` and `annotations_directive` to override individually. Tag and annotation work runs only when explicitly opted in to preserve backwards compatibility.
66

77
## 3.384.11 - 2026-06-16
88

features/multipart/s3.feature

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,19 @@ Feature: S3 Multipart Uploads
6969
Then the copied file "tags-no-auto-copy" should have tags "k=v&Project=X"
7070

7171
@s3annotations
72-
Scenario: copy_props=default copies tags to the destination
72+
Scenario: tags_directive=COPY copies tags to the destination
7373
Given I have an s3 client and an uploaded file named "tags-default" with tags
74-
When I call multipartCopy on "tags-default" with copy_props "default"
74+
When I call multipartCopy on "tags-default" with tags_directive "COPY"
7575
Then the copied file "tags-default-copy" should have the same tags as "tags-default"
7676

77-
Scenario: copy_props=metadata-directive does not copy tags
77+
Scenario: Default directives skip tag copying
7878
Given I have an s3 client and an uploaded file named "tags-skip" with tags
79-
When I call multipartCopy on "tags-skip" with copy_props "metadata-directive"
79+
When I call multipartCopy on "tags-skip" to a new key in the same bucket
8080
Then the copied file "tags-skip-copy" should have no tags
8181

82-
Scenario: copy_props=none strips metadata and tags
82+
Scenario: REPLACE+UNSPECIFIED+EXCLUDE strips metadata and tags
8383
Given I have an s3 client and an uploaded file named "none-strip" with metadata and tags
84-
When I call multipartCopy on "none-strip" with copy_props "none"
84+
When I call multipartCopy on "none-strip" with metadata_directive "REPLACE" and tags_directive "UNSPECIFIED" and annotations_directive "EXCLUDE"
8585
Then the copied file "none-strip-copy" should have no user-defined metadata
8686
And the copied file "none-strip-copy" should have no tags
8787

@@ -90,25 +90,20 @@ Feature: S3 Multipart Uploads
9090
When I call multipartCopy on "tags-replace" with tags_directive "REPLACE" and tagging "Project=Override&Env=prod"
9191
Then the copied file "tags-replace-copy" should have tags "Project=Override&Env=prod"
9292

93-
Scenario: tags_directive=COPY explicitly enables tag copying under metadata-directive copy_props
94-
Given I have an s3 client and an uploaded file named "tags-explicit" with tags
95-
When I call multipartCopy on "tags-explicit" with copy_props "metadata-directive" and tags_directive "COPY"
96-
Then the copied file "tags-explicit-copy" should have the same tags as "tags-explicit"
97-
9893

9994
# TODO: re-enable once concurrent PutObjectAnnotation behavior enabled.
10095
# Tracking: <ticket-id>. ETA: <date>.
10196
# @s3annotations
102-
# Scenario: copy_props=default copies annotations to the destination
97+
# Scenario: annotations_directive=COPY copies annotations to the destination
10398
# Given I have an s3 client and an uploaded file named "annot-default" with annotations
104-
# When I call multipartCopy on "annot-default" with copy_props "default"
99+
# When I call multipartCopy on "annot-default" with annotations_directive "COPY"
105100
# Then the copied file "annot-default-copy" should have the same annotations as "annot-default"
106101

107102

108103
@s3annotations
109-
Scenario: annotations_directive=EXCLUDE skips annotation copying under copy_props=default
104+
Scenario: annotations_directive=EXCLUDE skips annotation copying
110105
Given I have an s3 client and an uploaded file named "annot-exclude" with annotations
111-
When I call multipartCopy on "annot-exclude" with copy_props "default" and annotations_directive "EXCLUDE"
106+
When I call multipartCopy on "annot-exclude" with annotations_directive "EXCLUDE"
112107
Then the copied file "annot-exclude-copy" should have no annotations
113108

114109
@versioned

src/Multipart/UploadState.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ class UploadState
4747
/**
4848
* @var array Subset of upload-manager config retained for resume flows.
4949
*
50-
* Carries the original caller's directives (e.g. `copy_props`,
51-
* `metadata_directive`, `tags_directive`, `annotations_directive`) so a
52-
* later `getStateFromService(...) → new MultipartCopy(['state' => $s])`
53-
* can replay Phase 3 correctly without the caller having to re-specify.
50+
* Carries the original caller's directives (`metadata_directive`,
51+
* `tags_directive`, `annotations_directive`) so a later
52+
* `getStateFromService(...) → new MultipartCopy(['state' => $s])` can
53+
* replay Phase 3 correctly without the caller having to re-specify.
5454
*/
5555
private array $config = [];
5656

src/S3/MultipartCopy.php

Lines changed: 38 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,6 @@ class MultipartCopy extends AbstractUploadManager
2626
'REPLACE' => true,
2727
];
2828

29-
// copy_props presets: caller-facing top-level switch.
30-
private const COPY_PROPS_NONE = 'none';
31-
private const COPY_PROPS_METADATA_DIRECTIVE = 'metadata-directive';
32-
private const COPY_PROPS_DEFAULT = 'default';
33-
34-
private const VALID_COPY_PROPS = [
35-
self::COPY_PROPS_NONE => true,
36-
self::COPY_PROPS_METADATA_DIRECTIVE => true,
37-
self::COPY_PROPS_DEFAULT => true,
38-
];
39-
4029
private const TAGS_DIRECTIVE_UNSPECIFIED = 'UNSPECIFIED';
4130
private const TAGS_DIRECTIVE_COPY = 'COPY';
4231
private const TAGS_DIRECTIVE_REPLACE = 'REPLACE';
@@ -118,36 +107,24 @@ class MultipartCopy extends AbstractUploadManager
118107
* of the multipart upload and that is used to resume a previous upload.
119108
* When this option is provided, the `bucket`, `key`, and `part_size`
120109
* options are ignored.
121-
* - copy_props: (string, default='metadata-directive') Top-level preset that
122-
* resolves defaults for metadata_directive, tags_directive, and
123-
* annotations_directive. Explicit values for those directives take
124-
* precedence. Values:
125-
* - 'none': nothing is copied. Resolves metadata_directive=REPLACE,
126-
* tags_directive=UNSPECIFIED, annotations_directive=EXCLUDE.
127-
* - 'metadata-directive': legacy default. Resolves metadata_directive=COPY,
128-
* tags_directive=UNSPECIFIED, annotations_directive=UNSPECIFIED.
129-
* - 'default': metadata, tags, and annotations are all copied.
130-
* Resolves metadata_directive=COPY, tags_directive=COPY,
131-
* annotations_directive=COPY.
132-
* - metadata_directive: (string) 'COPY' or 'REPLACE'. Defaults to COPY,
133-
* except when `copy_props` is 'none' (then REPLACE). Caller-supplied
134-
* `params['Metadata']` does NOT change the directive — set this option
135-
* explicitly to opt into REPLACE. When 'COPY', source metadata fields
136-
* (Metadata, CacheControl, ContentDisposition, ContentEncoding,
137-
* ContentLanguage, ContentType, Expires) are forwarded and any
138-
* matching caller-supplied fields are dropped. When 'REPLACE', no
139-
* source metadata is read and caller-supplied params are used as-is.
140-
* - tags_directive: (string) 'UNSPECIFIED', 'COPY', or 'REPLACE'.
141-
* Defaults derived from `copy_props`. UNSPECIFIED means no tag work.
142-
* COPY reads source tags via GetObjectTagging and writes to the
143-
* destination via PutObjectTagging after CompleteMultipartUpload.
144-
* REPLACE skips the read and writes caller-supplied `params['Tagging']`
145-
* to the destination.
146-
* - annotations_directive: (string) 'UNSPECIFIED', 'COPY', or 'EXCLUDE'.
147-
* Defaults derived from `copy_props`. UNSPECIFIED and EXCLUDE both
148-
* skip annotation work. COPY reads source annotations via
149-
* ListObjectAnnotations and per-name GetObjectAnnotation, then writes
150-
* them to the destination via per-name PutObjectAnnotation.
110+
* - metadata_directive: (string, default='COPY') 'COPY' or 'REPLACE'.
111+
* Caller-supplied `params['Metadata']` does NOT change the directive,
112+
* set this option explicitly to opt into REPLACE. When 'COPY', source
113+
* metadata fields (Metadata, CacheControl, ContentDisposition,
114+
* ContentEncoding, ContentLanguage, ContentType, Expires) are forwarded
115+
* and any matching caller-supplied fields are dropped. When 'REPLACE',
116+
* no source metadata is read and caller-supplied params are used as-is.
117+
* - tags_directive: (string, default='UNSPECIFIED') 'UNSPECIFIED', 'COPY',
118+
* or 'REPLACE'. UNSPECIFIED means no tag work. COPY reads source tags
119+
* via GetObjectTagging and writes to the destination via
120+
* PutObjectTagging after CompleteMultipartUpload. REPLACE skips the
121+
* read and writes caller-supplied `params['Tagging']` to the
122+
* destination.
123+
* - annotations_directive: (string, default='UNSPECIFIED') 'UNSPECIFIED',
124+
* 'COPY', or 'EXCLUDE'. UNSPECIFIED and EXCLUDE both skip annotation
125+
* work. COPY reads source annotations via ListObjectAnnotations and
126+
* per-name GetObjectAnnotation, then writes them to the destination
127+
* via per-name PutObjectAnnotation.
151128
* - source_metadata: (Aws\ResultInterface) The result of a HeadObject call
152129
* on the copy source. If not provided, the SDK makes a HeadObject request
153130
* to obtain the source object's size and metadata. Providing this avoids
@@ -849,23 +826,6 @@ private function putAnnotationWithRetries(array $baseParams): PromiseInterface
849826
});
850827
}
851828

852-
/**
853-
* @return string
854-
* @throws \InvalidArgumentException
855-
*/
856-
private function resolveCopyProps(): string
857-
{
858-
$value = $this->config['copy_props'] ?? self::COPY_PROPS_METADATA_DIRECTIVE;
859-
if (!isset(self::VALID_COPY_PROPS[$value])) {
860-
throw new \InvalidArgumentException(
861-
"Invalid copy_props value '$value'. Must be one of: "
862-
. implode(', ', array_keys(self::VALID_COPY_PROPS)) . '.'
863-
);
864-
}
865-
866-
return $value;
867-
}
868-
869829
/**
870830
* @return string
871831
*/
@@ -876,11 +836,6 @@ private function resolveMetadataDirective(): string
876836
return strtoupper((string) $explicit);
877837
}
878838

879-
// copy_props=none implies REPLACE.
880-
if ($this->resolveCopyProps() === self::COPY_PROPS_NONE) {
881-
return 'REPLACE';
882-
}
883-
884839
return 'COPY';
885840
}
886841

@@ -891,22 +846,19 @@ private function resolveMetadataDirective(): string
891846
private function resolveTagsDirective(): string
892847
{
893848
$explicit = $this->config['tags_directive'] ?? null;
894-
if ($explicit !== null) {
895-
$value = strtoupper((string) $explicit);
896-
if (!isset(self::VALID_TAGS_DIRECTIVES[$value])) {
897-
throw new \InvalidArgumentException(
898-
"Invalid tags_directive value '$value'. Must be one of: "
899-
. implode(', ', array_keys(self::VALID_TAGS_DIRECTIVES)) . '.'
900-
);
901-
}
849+
if ($explicit === null) {
850+
return self::TAGS_DIRECTIVE_UNSPECIFIED;
851+
}
902852

903-
return $value;
853+
$value = strtoupper((string) $explicit);
854+
if (!isset(self::VALID_TAGS_DIRECTIVES[$value])) {
855+
throw new \InvalidArgumentException(
856+
"Invalid tags_directive value '$value'. Must be one of: "
857+
. implode(', ', array_keys(self::VALID_TAGS_DIRECTIVES)) . '.'
858+
);
904859
}
905860

906-
return match ($this->resolveCopyProps()) {
907-
self::COPY_PROPS_DEFAULT => self::TAGS_DIRECTIVE_COPY,
908-
default => self::TAGS_DIRECTIVE_UNSPECIFIED,
909-
};
861+
return $value;
910862
}
911863

912864
/**
@@ -916,23 +868,19 @@ private function resolveTagsDirective(): string
916868
private function resolveAnnotationsDirective(): string
917869
{
918870
$explicit = $this->config['annotations_directive'] ?? null;
919-
if ($explicit !== null) {
920-
$value = strtoupper((string) $explicit);
921-
if (!isset(self::VALID_ANNOTATIONS_DIRECTIVES[$value])) {
922-
throw new \InvalidArgumentException(
923-
"Invalid annotations_directive value '$value'. Must be one of: "
924-
. implode(', ', array_keys(self::VALID_ANNOTATIONS_DIRECTIVES)) . '.'
925-
);
926-
}
871+
if ($explicit === null) {
872+
return self::ANNOTATIONS_DIRECTIVE_UNSPECIFIED;
873+
}
927874

928-
return $value;
875+
$value = strtoupper((string) $explicit);
876+
if (!isset(self::VALID_ANNOTATIONS_DIRECTIVES[$value])) {
877+
throw new \InvalidArgumentException(
878+
"Invalid annotations_directive value '$value'. Must be one of: "
879+
. implode(', ', array_keys(self::VALID_ANNOTATIONS_DIRECTIVES)) . '.'
880+
);
929881
}
930882

931-
return match ($this->resolveCopyProps()) {
932-
self::COPY_PROPS_DEFAULT => self::ANNOTATIONS_DIRECTIVE_COPY,
933-
self::COPY_PROPS_NONE => self::ANNOTATIONS_DIRECTIVE_EXCLUDE,
934-
default => self::ANNOTATIONS_DIRECTIVE_UNSPECIFIED,
935-
};
883+
return $value;
936884
}
937885

938886
/**

src/S3/MultipartUploadingTrait.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ trait MultipartUploadingTrait
1919
* @param string $uploadId Upload ID for the multipart upload.
2020
* @param array $config Optional config to retain on the
2121
* state. Pass the directive keys
22-
* (`copy_props`, `metadata_directive`,
22+
* (`metadata_directive`,
2323
* `tags_directive`,
24-
* `annotations_directive`, etc.) the
24+
* `annotations_directive`) the
2525
* original copy was launched with so
2626
* a resumed `MultipartCopy` replays
2727
* Phase 3 with the same behavior. The

tests/Integ/MultipartContext.php

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -534,37 +534,42 @@ public function iHaveAnS3ClientAndAnUploadedFileNamedWithMetadataAndTags($filena
534534
}
535535

536536
/**
537-
* @When I call multipartCopy on :filename with copy_props :copyProps
537+
* @When I call multipartCopy on :filename with tags_directive :tagsDir
538538
*/
539-
public function iCallMultipartCopyOnWithCopyProps($filename, $copyProps)
539+
public function iCallMultipartCopyOnWithTagsDirective($filename, $tagsDir)
540540
{
541541
$bucket = self::getResourceName();
542542
$copier = new MultipartCopy(
543543
$this->s3Client,
544544
'/' . $bucket . '/' . $filename,
545545
[
546-
'bucket' => $bucket,
547-
'key' => $filename . '-copy',
548-
'copy_props' => $copyProps,
546+
'bucket' => $bucket,
547+
'key' => $filename . '-copy',
548+
'tags_directive' => $tagsDir,
549549
]
550550
);
551551
$this->runCopy($copier);
552552
}
553553

554554
/**
555-
* @When I call multipartCopy on :filename with copy_props :copyProps and tags_directive :tagsDir
555+
* @When I call multipartCopy on :filename with metadata_directive :metaDir and tags_directive :tagsDir and annotations_directive :annotDir
556556
*/
557-
public function iCallMultipartCopyOnWithCopyPropsAndTagsDirective($filename, $copyProps, $tagsDir)
558-
{
557+
public function iCallMultipartCopyOnWithAllThreeDirectives(
558+
$filename,
559+
$metaDir,
560+
$tagsDir,
561+
$annotDir
562+
) {
559563
$bucket = self::getResourceName();
560564
$copier = new MultipartCopy(
561565
$this->s3Client,
562566
'/' . $bucket . '/' . $filename,
563567
[
564-
'bucket' => $bucket,
565-
'key' => $filename . '-copy',
566-
'copy_props' => $copyProps,
567-
'tags_directive' => $tagsDir,
568+
'bucket' => $bucket,
569+
'key' => $filename . '-copy',
570+
'metadata_directive' => $metaDir,
571+
'tags_directive' => $tagsDir,
572+
'annotations_directive' => $annotDir,
568573
]
569574
);
570575
$this->runCopy($copier);
@@ -664,9 +669,9 @@ public function iHaveAnS3ClientAndAnUploadedFileNamedWithAnnotations($filename)
664669
}
665670

666671
/**
667-
* @When I call multipartCopy on :filename with copy_props :copyProps and annotations_directive :annotDir
672+
* @When I call multipartCopy on :filename with annotations_directive :annotDir
668673
*/
669-
public function iCallMultipartCopyOnWithCopyPropsAndAnnotationsDirective($filename, $copyProps, $annotDir)
674+
public function iCallMultipartCopyOnWithAnnotationsDirective($filename, $annotDir)
670675
{
671676
$bucket = self::getResourceName();
672677
$copier = new MultipartCopy(
@@ -675,7 +680,6 @@ public function iCallMultipartCopyOnWithCopyPropsAndAnnotationsDirective($filena
675680
[
676681
'bucket' => $bucket,
677682
'key' => $filename . '-copy',
678-
'copy_props' => $copyProps,
679683
'annotations_directive' => $annotDir,
680684
]
681685
);

0 commit comments

Comments
 (0)