Skip to content

Commit

Permalink
feat: add createBucket & getBucket method (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
c4spar authored Nov 22, 2021
1 parent 83ea6e4 commit 11732fd
Show file tree
Hide file tree
Showing 10 changed files with 386 additions and 90 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ test:
aws --endpoint-url=http://localhost:9000 s3 rb s3://test || true
aws --endpoint-url=http://localhost:9000 s3 mb s3://test

aws --endpoint-url=http://localhost:9000 s3 rm --recursive s3://create-bucket-test || true
aws --endpoint-url=http://localhost:9000 s3 rb s3://create-bucket-test || true

deno test -A ${DENO_ARGS}

cleanup:
Expand Down
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,24 @@ Amazon S3 for Deno
## Example

```ts
import { S3Bucket } from "https://deno.land/x/[email protected]/mod.ts";
import { S3, S3Bucket } from "https://deno.land/x/[email protected]/mod.ts";

const bucket = new S3Bucket({
// Create a S3 instance.
const s3 = new S3({
accessKeyID: Deno.env.get("AWS_ACCESS_KEY_ID")!,
secretKey: Deno.env.get("AWS_SECRET_ACCESS_KEY")!,
region: "us-east-1",
endpointURL: Deno.env.get("S3_ENDPOINT_URL"),
});

// Create a new bucket.
let bucket = await s3.createBucket("test", { acl: "private" });

// Get an existing bucket.
bucket = s3.getBucket("test");

// Create a bucket instance manuely.
bucket = new S3Bucket({
accessKeyID: Deno.env.get("AWS_ACCESS_KEY_ID")!,
secretKey: Deno.env.get("AWS_SECRET_ACCESS_KEY")!,
bucket: "test",
Expand Down
5 changes: 3 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ version: "3"
services:
minio:
image: minio/minio
command: server /data
command: server /data --console-address ":9001"
environment:
MINIO_ACCESS_KEY: AKIAIOSFODNN7EXAMPLE
MINIO_SECRET_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
ports:
- "9000:9000"
- "9000:9000"
- "9001:9001"
4 changes: 4 additions & 0 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
export * from "./src/client.ts";
export * from "./src/bucket.ts";
export type {
CommonPrefix,
CopyDirective,
CopyObjectOptions,
CopyObjectResponse,
CreateBucketConfiguration,
CreateBucketOptions,
DeleteObjectOptions,
DeleteObjectResponse,
GetObjectOptions,
Expand All @@ -12,6 +15,7 @@ export type {
ListAllObjectsOptions,
ListObjectsOptions,
ListObjectsResponse,
LocationConstraint,
LockMode,
PutObjectOptions,
PutObjectResponse,
Expand Down
118 changes: 47 additions & 71 deletions src/bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
decodeXMLEntities,
parseXML,
pooledMap,
sha256Hex,
} from "../deps.ts";
import type { S3Config } from "./client.ts";
import type {
Expand All @@ -26,10 +25,8 @@ import type {
} from "./types.ts";
import { S3Error } from "./error.ts";
import type { Signer } from "../deps.ts";

interface Params {
[key: string]: string;
}
import { doRequest, encodeURIS3 } from "./request.ts";
import type { Params } from "./request.ts";

export interface S3BucketConfig extends S3Config {
bucket: string;
Expand All @@ -38,49 +35,20 @@ export interface S3BucketConfig extends S3Config {
export class S3Bucket {
#signer: Signer;
#host: string;
#bucket: string;

constructor(config: S3BucketConfig) {
this.#signer = new AWSSignerV4(config.region, {
awsAccessKeyId: config.accessKeyID,
awsSecretKey: config.secretKey,
sessionToken: config.sessionToken,
});
this.#bucket = config.bucket;
this.#host = config.endpointURL
? new URL(`/${config.bucket}/`, config.endpointURL).toString()
: config.bucket.indexOf(".") >= 0
? `https://s3.${config.region}.amazonaws.com/${config.bucket}/`
: `https://${config.bucket}.s3.${config.region}.amazonaws.com/`;
}

private async _doRequest(
path: string,
params: Params,
method: string,
headers: Params,
body?: Uint8Array | undefined,
): Promise<Response> {
const url = path == "/"
? new URL(this.#host)
: new URL(encodeURIS3(path), this.#host);
for (const key in params) {
url.searchParams.set(key, params[key]);
}
const request = new Request(url.toString(), {
headers,
method,
body,
});

const signedRequest = await this.#signer.sign("s3", request);
signedRequest.headers.set("x-amz-content-sha256", sha256Hex(body ?? ""));
if (body) {
signedRequest.headers.set("content-length", body.length.toFixed(0));
}
return fetch(signedRequest);
}

async headObject(
key: string,
options?: GetObjectOptions,
Expand Down Expand Up @@ -120,7 +88,14 @@ export class S3Bucket {
params["VersionId"] = options.versionId;
}

const res = await this._doRequest(key, params, "HEAD", headers);
const res = await doRequest({
host: this.#host,
signer: this.#signer,
path: key,
method: "HEAD",
params,
headers,
});
if (res.body) {
await res.arrayBuffer();
}
Expand Down Expand Up @@ -218,7 +193,14 @@ export class S3Bucket {
params["VersionId"] = options.versionId;
}

const res = await this._doRequest(key, params, "GET", headers);
const res = await doRequest({
host: this.#host,
signer: this.#signer,
path: key,
method: "GET",
params,
headers,
});
if (res.status === 404) {
// clean up http body
await res.arrayBuffer();
Expand Down Expand Up @@ -299,7 +281,13 @@ export class S3Bucket {
params["continuation-token"] = options.continuationToken;
}

const res = await this._doRequest(`/`, params, "GET", headers);
const res = await doRequest({
host: this.#host,
signer: this.#signer,
method: "GET",
params,
headers,
});
if (res.status === 404) {
// clean up http body
await res.arrayBuffer();
Expand Down Expand Up @@ -464,7 +452,14 @@ export class S3Bucket {
}
}

const resp = await this._doRequest(key, {}, "PUT", headers, body);
const resp = await doRequest({
host: this.#host,
signer: this.#signer,
path: key,
method: "PUT",
headers,
body,
});
if (resp.status !== 200) {
throw new S3Error(
`Failed to put object: ${resp.status} ${resp.statusText}`,
Expand Down Expand Up @@ -557,7 +552,13 @@ export class S3Bucket {
headers["x-amz-tagging-directive"] = options.taggingDirective;
}

const resp = await this._doRequest(destination, {}, "PUT", headers);
const resp = await doRequest({
host: this.#host,
signer: this.#signer,
path: destination,
method: "PUT",
headers,
});
if (resp.status !== 200) {
throw new S3Error(
`Failed to copy object: ${resp.status} ${resp.statusText}`,
Expand All @@ -580,7 +581,13 @@ export class S3Bucket {
if (options?.versionId) {
params.versionId = options.versionId;
}
const resp = await this._doRequest(key, params, "DELETE", {});
const resp = await doRequest({
host: this.#host,
signer: this.#signer,
path: key,
method: "DELETE",
params,
});
if (resp.status !== 204) {
throw new S3Error(
`Failed to put object: ${resp.status} ${resp.statusText}`,
Expand Down Expand Up @@ -618,37 +625,6 @@ export class S3Bucket {
}
}

function encodeURIS3(input: string): string {
let result = "";
for (const ch of input) {
if (
(ch >= "A" && ch <= "Z") ||
(ch >= "a" && ch <= "z") ||
(ch >= "0" && ch <= "9") ||
ch == "_" ||
ch == "-" ||
ch == "~" ||
ch == "."
) {
result += ch;
} else if (ch == "/") {
result += "/";
} else {
result += stringToHex(ch);
}
}
return result;
}

const encoder = new TextEncoder();

function stringToHex(input: string) {
return [...encoder.encode(input)]
.map((s) => "%" + s.toString(16))
.join("")
.toUpperCase();
}

interface Document {
declaration: {
attributes: Record<string, unknown>;
Expand Down
24 changes: 12 additions & 12 deletions src/bucket_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const bucket = new S3Bucket({
const encoder = new TextEncoder();

Deno.test({
name: "put object",
name: "[bucket] put object",
async fn() {
await bucket.putObject("test", encoder.encode("Test1"), {
contentType: "text/plain",
Expand All @@ -24,7 +24,7 @@ Deno.test({
});

Deno.test({
name: "put object with % in key",
name: "[bucket] put object with % in key",
async fn() {
await bucket.putObject(
"ltest/versions/1.0.0/raw/fixtures/%",
Expand All @@ -38,7 +38,7 @@ Deno.test({
});

Deno.test({
name: "put object with @ in key",
name: "[bucket] put object with @ in key",
async fn() {
await bucket.putObject(
"dex/versions/1.0.0/raw/lib/deps/[email protected]/README.md",
Expand All @@ -54,7 +54,7 @@ Deno.test({
});

Deno.test({
name: "put object with 日本語 in key",
name: "[bucket] put object with 日本語 in key",
async fn() {
await bucket.putObject(
"servest/versions/1.0.0/raw/fixtures/日本語.txt",
Expand All @@ -68,7 +68,7 @@ Deno.test({
});

Deno.test({
name: "head object success",
name: "[bucket] head object success",
async fn() {
// setup
await bucket.putObject("test", encoder.encode("Test1"), {
Expand All @@ -92,14 +92,14 @@ Deno.test({
});

Deno.test({
name: "head object not found",
name: "[bucket] head object not found",
async fn() {
assertEquals(await bucket.headObject("test2"), undefined);
},
});

Deno.test({
name: "get object success",
name: "[bucket] get object success",
async fn() {
// setup
await bucket.putObject("test", encoder.encode("Test1"), {
Expand All @@ -125,14 +125,14 @@ Deno.test({
});

Deno.test({
name: "get object not found",
name: "[bucket] get object not found",
async fn() {
assertEquals(await bucket.getObject("test2"), undefined);
},
});

Deno.test({
name: "delete object",
name: "[bucket] delete object",
async fn() {
// setup
await bucket.putObject("test", encoder.encode("test"));
Expand All @@ -149,7 +149,7 @@ Deno.test({
});

Deno.test({
name: "copy object",
name: "[bucket] copy object",
async fn() {
await bucket.putObject("test3", encoder.encode("Test1"));
await bucket
Expand All @@ -172,7 +172,7 @@ Deno.test({
});

Deno.test({
name: "list objects",
name: "[bucket] list objects",
async fn() {
// setup
const content = encoder.encode("Test1");
Expand Down Expand Up @@ -255,7 +255,7 @@ Deno.test({
});

Deno.test({
name: "empty bucket",
name: "[bucket] empty bucket",
async fn() {
// setup
const content = encoder.encode("Test1");
Expand Down
Loading

0 comments on commit 11732fd

Please sign in to comment.