diff --git a/mod.ts b/mod.ts index 0cf40ea..17b9792 100644 --- a/mod.ts +++ b/mod.ts @@ -11,6 +11,7 @@ export type { DeleteObjectResponse, GetObjectOptions, GetObjectResponse, + HeadBucketOptions, HeadObjectResponse, ListAllObjectsOptions, ListObjectsOptions, diff --git a/src/client.ts b/src/client.ts index 75f3c29..2fb8ae1 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,5 +1,5 @@ import { AWSSignerV4 } from "../deps.ts"; -import type { CreateBucketOptions } from "./types.ts"; +import type { CreateBucketOptions, HeadBucketOptions } from "./types.ts"; import { S3Error } from "./error.ts"; import { S3Bucket } from "./bucket.ts"; import { doRequest, encoder } from "./request.ts"; @@ -50,6 +50,44 @@ export class S3 { }); } + /** + * Determine if a bucket exists and if you have permission to access it. + * The method returns a S3Bucket if the bucket exists and if you have + * permission to access it. If the bucket does not exist or you do not have + * permission to access it an `S3Error` is thrown. + * + * To use this operation, you must have permissions to perform the + * s3:ListBucket action. The bucket owner has this permission by default and + * can grant this permission to others. + */ + async headBucket( + bucket: string, + options?: HeadBucketOptions, + ): Promise { + const headers: Params = {}; + + if (options?.expectedBucketOwner) { + headers["x-amz-expected-bucket-owner"] = options.expectedBucketOwner; + } + + const resp = await doRequest({ + host: this.#host, + signer: this.#signer, + path: bucket, + method: "HEAD", + headers, + }); + + if (resp.status !== 200) { + throw new S3Error( + `Failed to get bucket "${bucket}": ${resp.status} ${resp.statusText}`, + await resp.text(), + ); + } + + return this.getBucket(bucket); + } + /** * Creates a new S3 bucket. By default, the bucket is created in the region * specified with the S3 options. If not specified the US East (N. Virginia) diff --git a/src/client_test.ts b/src/client_test.ts index b1e24a6..742ca78 100644 --- a/src/client_test.ts +++ b/src/client_test.ts @@ -11,7 +11,7 @@ const s3 = new S3({ }); Deno.test({ - name: "[client] should get an existing bucket", + name: "[client] should get a bucket instance", async fn() { const bucket = await s3.getBucket("test"); @@ -26,6 +26,28 @@ Deno.test({ }, }); +Deno.test({ + name: "[client] should determine if a bucket exists", + async fn() { + const bucket = await s3.headBucket("test"); + + // Check if returned bucket instance is working. + await bucket.putObject("test", encoder.encode("test")); + const resp = await bucket.getObject("test"); + const body = await new Response(resp?.body).text(); + assertEquals(body, "test"); + + await assertThrowsAsync( + () => s3.headBucket("not-existing-bucket"), + S3Error, + 'Failed to get bucket "not-existing-bucket": 404 Not Found', + ); + + // teardown + await bucket.deleteObject("test"); + }, +}); + Deno.test({ name: "[client] should create a new bucket", async fn() { diff --git a/src/types.ts b/src/types.ts index 9819225..6861aa0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -537,6 +537,15 @@ export interface DeleteObjectResponse { deleteMarker: boolean; } +export interface HeadBucketOptions { + /** + * The account ID of the expected bucket owner. If the bucket is owned by a + * different account, the request will fail with an HTTP 403 (Access Denied) + * error. + */ + expectedBucketOwner?: string; +} + export type LocationConstraint = | "af-south-1" | "ap-east-1"