Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow replacing metadata when copying an object, document metadata better #35

Merged
merged 2 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Supported functionality:
- Upload an object: `client.putObject("key", streamOrData, options)`
- Can upload from a `string`, `Uint8Array`, or `ReadableStream`
- Can split large uploads into multiple parts and uploads parts in parallel.
- Can set custom headers, ACLs, and other metadata on the new object (example below).
- Copy an object: `client.copyObject({ sourceKey: "source", options }, "dest", options)`
- Can copy between different buckets.
- Delete an object: `client.deleteObject("key")`
Expand Down Expand Up @@ -123,6 +124,17 @@ await result.body!.pipeTo(localOutFile.writable);
// result.text(), result.blob(), result.arrayBuffer(), or result.json()
```

Set ACLs, Content-Type, custom metadata, etc. during upload:

```ts
await s3client.putObject("key", streamOrData, {
metadata: {
"x-amz-acl": "public-read",
"x-amz-meta-custom": "value",
},
})`
```

For more examples, check out the tests in [`integration.ts`](./integration.ts)

## Developer notes
Expand Down
37 changes: 26 additions & 11 deletions client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ const metadataKeys = [
"Content-Encoding",
"Content-Language",
"Expires",
"x-amz-acl",
"x-amz-grant-full-control",
"x-amz-grant-read",
"x-amz-grant-read-acp",
Expand All @@ -66,7 +65,19 @@ const metadataKeys = [
*
* Custom keys should be like "x-amz-meta-..."
*/
export type ObjectMetadata = { [K in typeof metadataKeys[number]]?: string } & { [key: string]: string };
export type ObjectMetadata =
& {
"x-amz-acl"?:
| "private"
| "public-read"
| "public-read-write"
| "authenticated-read"
| "aws-exec-read"
| "bucket-owner-read"
| "bucket-owner-full-control";
}
& { [K in typeof metadataKeys[number]]?: string }
& { [customMetadata: `x-amz-meta-${string}`]: string };

/** Response Header Overrides
* These parameters can be used with an authenticated or presigned get object request, to
Expand Down Expand Up @@ -787,7 +798,7 @@ export class Client {
// Also add in custom metadata
response.headers.forEach((_value, key) => {
if (key.startsWith("x-amz-meta-")) {
metadata[key] = response.headers.get(key) as string;
metadata[key as `x-amz-meta-${string}`] = response.headers.get(key) as string;
}
});

Expand All @@ -808,7 +819,11 @@ export class Client {
public async copyObject(
source: { sourceBucketName?: string; sourceKey: string; sourceVersionId?: string },
objectName: string,
options?: { bucketName?: string },
options?: {
bucketName?: string;
/** Metadata for the new object. If not specified, metadata will be copied from the source. */
metadata?: ObjectMetadata;
},
): Promise<CopiedObjectInfo> {
const bucketName = this.getBucketName(options);
const sourceBucketName = source.sourceBucketName ?? bucketName;
Expand All @@ -821,13 +836,13 @@ export class Client {
let xAmzCopySource = `${sourceBucketName}/${source.sourceKey}`;
if (source.sourceVersionId) xAmzCopySource += `?versionId=${source.sourceVersionId}`;

const response = await this.makeRequest({
method: "PUT",
bucketName,
objectName,
headers: new Headers({ "x-amz-copy-source": xAmzCopySource }),
returnBody: true,
});
const headers = new Headers(options?.metadata);
if (options?.metadata !== undefined) {
headers.set("x-amz-metadata-directive", "REPLACE");
}
headers.set("x-amz-copy-source", xAmzCopySource);

const response = await this.makeRequest({ method: "PUT", bucketName, objectName, headers, returnBody: true });

const responseText = await response.text();
// Parse the response XML.
Expand Down
34 changes: 34 additions & 0 deletions integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,40 @@ Deno.test({
},
});

Deno.test({
name: "copyObject() copies metadata, but we can override it if we want to",
fn: async () => {
const contents = new Uint8Array([1, 2, 3, 4, 5, 6]);
const sourceKey = "test-copy-metadata-source.txt";
const destKeySame = "test-copy-metadata-same.txt";
const destKeyNew = "test-copy-metadata-new.txt";

// Create the source file:
const metadata = {
"Content-Type": "test/custom",
"x-amz-meta-custom-key": "custom-value",
};
await client.putObject(sourceKey, contents, { metadata });
// Make sure the destination doesn't yet exist:
await client.deleteObject(destKeySame);
assertEquals(await client.exists(destKeySame), false);

// Copy it with the same metadata:
await client.copyObject({ sourceKey }, destKeySame);
const stat = await client.statObject(destKeySame);
assertEquals(stat.metadata, metadata);

// Copy it with the different metadata:
const newMetadata = {
"Content-Type": "application/javascript",
"x-amz-meta-other": "new",
};
await client.copyObject({ sourceKey }, destKeyNew, { metadata: newMetadata });
const statNew = await client.statObject(destKeyNew);
assertEquals(statNew.metadata, newMetadata);
},
});

Deno.test({
name: "bucketExists() can check if a bucket exists",
fn: async () => {
Expand Down