diff --git a/README.md b/README.md index 41a5b7c..7c44351 100644 --- a/README.md +++ b/README.md @@ -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")` @@ -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 diff --git a/client.ts b/client.ts index 7df069f..d012295 100644 --- a/client.ts +++ b/client.ts @@ -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", @@ -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 @@ -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; } }); @@ -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 { const bucketName = this.getBucketName(options); const sourceBucketName = source.sourceBucketName ?? bucketName; @@ -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. diff --git a/integration.ts b/integration.ts index 544a919..d4e9ef0 100644 --- a/integration.ts +++ b/integration.ts @@ -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 () => {