Skip to content

Commit f8439c8

Browse files
authored
chore: update dependencies + streaming body + add example (#24)
1 parent f1796f1 commit f8439c8

File tree

7 files changed

+154
-120
lines changed

7 files changed

+154
-120
lines changed

README.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,46 @@
11
# deno_s3
22

3+
![ci](https://github.com/lucacasonato/deno_aws_sign_v4/workflows/ci/badge.svg)
4+
[![deno doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/deno.land/x/[email protected]/mod.ts)
5+
36
Amazon S3 for Deno
47

58
> ⚠️ This project is work in progress. Expect breaking changes.
69
7-
## Examples
10+
## Example
11+
12+
```ts
13+
import { S3Bucket } from "https://deno.land/x/[email protected]/mod.ts";
14+
15+
const bucket = new S3Bucket({
16+
accessKeyID: Deno.env.get("AWS_ACCESS_KEY_ID")!,
17+
secretKey: Deno.env.get("AWS_SECRET_ACCESS_KEY")!,
18+
bucket: "test",
19+
region: "us-east-1",
20+
endpointURL: Deno.env.get("S3_ENDPOINT_URL"),
21+
});
22+
23+
const encoder = new TextEncoder();
824

9-
Coming soon...
25+
// Put an object into a bucket.
26+
await bucket.putObject("test", encoder.encode("Test1"), {
27+
contentType: "text/plain",
28+
});
29+
30+
// Retrieve an object form a bucket.
31+
const { body } = await bucket.getObject("test");
32+
const data = await new Response(body).text();
33+
console.log("File 'test' contains:", data);
34+
35+
// List objects in the bucket.
36+
const list = bucket.listAllObjects({});
37+
for await (const obj of list) {
38+
console.log("Item in bucket:", obj.key);
39+
}
40+
41+
// Delete an object from a bucket.
42+
await bucket.deleteObject("test");
43+
```
1044

1145
## Contributing
1246

deps.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@ export {
22
AWSSignerV4,
33
toAmz,
44
toDateStamp,
5-
} from "https://deno.land/x/aws_sign_v4@0.1.5/mod.ts";
5+
} from "https://deno.land/x/aws_sign_v4@1.0.0/mod.ts";
66
export type {
77
Credentials,
88
Signer,
9-
} from "https://deno.land/x/aws_sign_v4@0.1.5/mod.ts";
10-
import { createHash } from "https://deno.land/std@0.83.0/hash/mod.ts";
9+
} from "https://deno.land/x/aws_sign_v4@1.0.0/mod.ts";
10+
import { createHash } from "https://deno.land/std@0.84.0/hash/mod.ts";
1111
export function sha256Hex(data: string | Uint8Array): string {
1212
const hasher = createHash("sha256");
1313
hasher.update(data);
1414
return hasher.toString("hex");
1515
}
1616
export { default as parseXML } from "https://raw.githubusercontent.com/nekobato/deno-xml-parser/0bc4c2bd2f5fad36d274279978ca57eec57c680c/index.ts";
1717
export { decode as decodeXMLEntities } from "https://deno.land/x/[email protected]/lib/xml-entities.js";
18-
export { pooledMap } from "https://deno.land/std@0.83.0/async/pool.ts";
18+
export { pooledMap } from "https://deno.land/std@0.84.0/async/pool.ts";

mod.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
export * from "./src/bucket.ts";
22
export type {
3+
CommonPrefix,
4+
CopyDirective,
35
CopyObjectOptions,
46
CopyObjectResponse,
57
DeleteObjectOptions,
68
DeleteObjectResponse,
79
GetObjectOptions,
810
GetObjectResponse,
11+
HeadObjectResponse,
12+
ListAllObjectsOptions,
913
ListObjectsOptions,
1014
ListObjectsResponse,
15+
LockMode,
1116
PutObjectOptions,
1217
PutObjectResponse,
18+
ReplicationStatus,
19+
S3Object,
20+
StorageClass,
1321
} from "./src/types.ts";

src/bucket.ts

Lines changed: 69 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export class S3Bucket {
154154
etag: JSON.parse(res.headers.get("etag")!),
155155
lastModified: new Date(res.headers.get("Last-Modified")!),
156156
missingMeta: parseInt(res.headers.get("x-amz-missing-meta") ?? "0"),
157-
storageClass: res.headers.get("x-amz-storage-class") as StorageClass ??
157+
storageClass: (res.headers.get("x-amz-storage-class") as StorageClass) ??
158158
"STANDARD",
159159
taggingCount: parseInt(res.headers.get("x-amz-tagging-count") ?? "0"),
160160

@@ -164,13 +164,13 @@ export class S3Bucket {
164164
contentLanguage: res.headers.get("Content-Language") ?? undefined,
165165
contentType: res.headers.get("Content-Type") ?? undefined,
166166
expires: expires ? new Date(expires) : undefined,
167-
legalHold: legalHold ? true : (legalHold === "OFF" ? false : undefined),
168-
lockMode: res.headers.get("x-amz-object-lock-mode") as LockMode ??
167+
legalHold: legalHold ? true : legalHold === "OFF" ? false : undefined,
168+
lockMode: (res.headers.get("x-amz-object-lock-mode") as LockMode) ??
169169
undefined,
170170
lockRetainUntil: lockRetainUntil ? new Date(lockRetainUntil) : undefined,
171171
partsCount: partsCount ? parseInt(partsCount) : undefined,
172172
replicationStatus:
173-
res.headers.get("x-amz-replication-status") as ReplicationStatus ??
173+
(res.headers.get("x-amz-replication-status") as ReplicationStatus) ??
174174
undefined,
175175
versionId: res.headers.get("x-amz-version-id") ?? undefined,
176176
websiteRedirectLocation:
@@ -245,14 +245,18 @@ export class S3Bucket {
245245
}
246246
}
247247

248+
if (res.body == null) {
249+
throw new S3Error("S3 did not return a body for a getObject call.", "");
250+
}
251+
248252
return {
249-
body: new Uint8Array(await res.arrayBuffer()),
253+
body: res.body,
250254
contentLength: parseInt(res.headers.get("Content-Length")!),
251255
deleteMarker: res.headers.get("x-amz-delete-marker") === "true",
252256
etag: JSON.parse(res.headers.get("etag")!),
253257
lastModified: new Date(res.headers.get("Last-Modified")!),
254258
missingMeta: parseInt(res.headers.get("x-amz-missing-meta") ?? "0"),
255-
storageClass: res.headers.get("x-amz-storage-class") as StorageClass ??
259+
storageClass: (res.headers.get("x-amz-storage-class") as StorageClass) ??
256260
"STANDARD",
257261
taggingCount: parseInt(res.headers.get("x-amz-tagging-count") ?? "0"),
258262

@@ -262,13 +266,13 @@ export class S3Bucket {
262266
contentLanguage: res.headers.get("Content-Language") ?? undefined,
263267
contentType: res.headers.get("Content-Type") ?? undefined,
264268
expires: expires ? new Date(expires) : undefined,
265-
legalHold: legalHold ? true : (legalHold === "OFF" ? false : undefined),
266-
lockMode: res.headers.get("x-amz-object-lock-mode") as LockMode ??
269+
legalHold: legalHold ? true : legalHold === "OFF" ? false : undefined,
270+
lockMode: (res.headers.get("x-amz-object-lock-mode") as LockMode) ??
267271
undefined,
268272
lockRetainUntil: lockRetainUntil ? new Date(lockRetainUntil) : undefined,
269273
partsCount: partsCount ? parseInt(partsCount) : undefined,
270274
replicationStatus:
271-
res.headers.get("x-amz-replication-status") as ReplicationStatus ??
275+
(res.headers.get("x-amz-replication-status") as ReplicationStatus) ??
272276
undefined,
273277
versionId: res.headers.get("x-amz-version-id") ?? undefined,
274278
websiteRedirectLocation:
@@ -295,12 +299,7 @@ export class S3Bucket {
295299
params["continuation-token"] = options.continuationToken;
296300
}
297301

298-
const res = await this._doRequest(
299-
`/`,
300-
params,
301-
"GET",
302-
headers,
303-
);
302+
const res = await this._doRequest(`/`, params, "GET", headers);
304303
if (res.status === 404) {
305304
// clean up http body
306305
await res.arrayBuffer();
@@ -345,37 +344,38 @@ export class S3Bucket {
345344
isTruncated: extractContent(root, "IsTruncated") === "true"
346345
? true
347346
: false,
348-
contents: root.children.filter((node) => node.name === "Contents").map<
349-
S3Object
350-
>((s3obj) => {
351-
let lastmod: Date | undefined;
352-
let content = extractContent(s3obj, "LastModified");
353-
if (content) {
354-
lastmod = new Date(content);
355-
}
347+
contents: root.children
348+
.filter((node) => node.name === "Contents")
349+
.map<S3Object>((s3obj) => {
350+
let lastmod: Date | undefined;
351+
let content = extractContent(s3obj, "LastModified");
352+
if (content) {
353+
lastmod = new Date(content);
354+
}
356355

357-
let size: number | undefined;
358-
content = extractContent(s3obj, "Size");
359-
if (content) {
360-
size = parseInt(content);
361-
}
356+
let size: number | undefined;
357+
content = extractContent(s3obj, "Size");
358+
if (content) {
359+
size = parseInt(content);
360+
}
362361

363-
return {
364-
key: extractContent(s3obj, "Key"),
365-
lastModified: lastmod,
366-
eTag: extractContent(s3obj, "ETag"),
367-
size: size,
368-
storageClass: extractContent(s3obj, "StorageClass"),
369-
owner: extractContent(s3obj, "Owner"),
370-
};
371-
}),
362+
return {
363+
key: extractContent(s3obj, "Key"),
364+
lastModified: lastmod,
365+
eTag: extractContent(s3obj, "ETag"),
366+
size: size,
367+
storageClass: extractContent(s3obj, "StorageClass"),
368+
owner: extractContent(s3obj, "Owner"),
369+
};
370+
}),
372371
name: extractContent(root, "Name"),
373372
prefix: extractContent(root, "Prefix"),
374373
delimiter: extractContent(root, "Delimiter"),
375374
maxKeys: maxkeys,
376-
commonPrefixes: extractField(root, "CommonPrefixes")?.children.map<
377-
CommonPrefix
378-
>((prefix) => {
375+
commonPrefixes: extractField(
376+
root,
377+
"CommonPrefixes",
378+
)?.children.map<CommonPrefix>((prefix) => {
379379
return {
380380
prefix: extractContent(prefix, "Prefix"),
381381
};
@@ -449,8 +449,9 @@ export class S3Bucket {
449449
}
450450
if (options?.lockMode) headers["x-amz-object-lock-mode"] = options.lockMode;
451451
if (options?.lockRetainUntil) {
452-
headers["x-amz-object-lock-retain-until-date"] = options.lockRetainUntil
453-
.toString();
452+
headers[
453+
"x-amz-object-lock-retain-until-date"
454+
] = options.lockRetainUntil.toString();
454455
}
455456
if (options?.legalHold) {
456457
headers["x-amz-object-lock-legal-hold"] = options.legalHold
@@ -463,13 +464,7 @@ export class S3Bucket {
463464
}
464465
}
465466

466-
const resp = await this._doRequest(
467-
key,
468-
{},
469-
"PUT",
470-
headers,
471-
body,
472-
);
467+
const resp = await this._doRequest(key, {}, "PUT", headers, body);
473468
if (resp.status !== 200) {
474469
throw new S3Error(
475470
`Failed to put object: ${resp.status} ${resp.statusText}`,
@@ -490,8 +485,10 @@ export class S3Bucket {
490485
options?: CopyObjectOptions,
491486
): Promise<PutObjectResponse> {
492487
const headers: Params = {};
493-
headers["x-amz-copy-source"] = new URL(encodeURIS3(source), this.#host)
494-
.toString();
488+
headers["x-amz-copy-source"] = new URL(
489+
encodeURIS3(source),
490+
this.#host,
491+
).toString();
495492
if (options?.acl) headers["x-amz-acl"] = options.acl;
496493
if (options?.cacheControl) headers["Cache-Control"] = options.cacheControl;
497494
if (options?.contentDisposition) {
@@ -511,14 +508,14 @@ export class S3Bucket {
511508
headers["x-amz-copy-source-if-none-match"] = options.copyOnlyIfNoneMatch;
512509
}
513510
if (options?.copyOnlyIfModifiedSince) {
514-
headers["x-amz-copy-source-if-modified-since"] = options
515-
.copyOnlyIfModifiedSince
516-
.toISOString();
511+
headers[
512+
"x-amz-copy-source-if-modified-since"
513+
] = options.copyOnlyIfModifiedSince.toISOString();
517514
}
518515
if (options?.copyOnlyIfUnmodifiedSince) {
519-
headers["x-amz-copy-source-if-unmodified-since"] = options
520-
.copyOnlyIfUnmodifiedSince
521-
.toISOString();
516+
headers[
517+
"x-amz-copy-source-if-unmodified-since"
518+
] = options.copyOnlyIfUnmodifiedSince.toISOString();
522519
}
523520
if (options?.grantFullControl) {
524521
headers["x-amz-grant-full-control"] = options.grantFullControl;
@@ -544,8 +541,9 @@ export class S3Bucket {
544541
}
545542
if (options?.lockMode) headers["x-amz-object-lock-mode"] = options.lockMode;
546543
if (options?.lockRetainUntil) {
547-
headers["x-amz-object-lock-retain-until-date"] = options.lockRetainUntil
548-
.toString();
544+
headers[
545+
"x-amz-object-lock-retain-until-date"
546+
] = options.lockRetainUntil.toString();
549547
}
550548
if (options?.legalHold) {
551549
headers["x-amz-object-lock-legal-hold"] = options.legalHold
@@ -559,12 +557,7 @@ export class S3Bucket {
559557
headers["x-amz-tagging-directive"] = options.taggingDirective;
560558
}
561559

562-
const resp = await this._doRequest(
563-
destination,
564-
{},
565-
"PUT",
566-
headers,
567-
);
560+
const resp = await this._doRequest(destination, {}, "PUT", headers);
568561
if (resp.status !== 200) {
569562
throw new S3Error(
570563
`Failed to copy object: ${resp.status} ${resp.statusText}`,
@@ -629,8 +622,12 @@ function encodeURIS3(input: string): string {
629622
let result = "";
630623
for (const ch of input) {
631624
if (
632-
(ch >= "A" && ch <= "Z") || (ch >= "a" && ch <= "z") ||
633-
(ch >= "0" && ch <= "9") || ch == "_" || ch == "-" || ch == "~" ||
625+
(ch >= "A" && ch <= "Z") ||
626+
(ch >= "a" && ch <= "z") ||
627+
(ch >= "0" && ch <= "9") ||
628+
ch == "_" ||
629+
ch == "-" ||
630+
ch == "~" ||
634631
ch == "."
635632
) {
636633
result += ch;
@@ -646,7 +643,9 @@ function encodeURIS3(input: string): string {
646643
const encoder = new TextEncoder();
647644

648645
function stringToHex(input: string) {
649-
return [...encoder.encode(input)].map((s) => "%" + s.toString(16)).join("")
646+
return [...encoder.encode(input)]
647+
.map((s) => "%" + s.toString(16))
648+
.join("")
650649
.toUpperCase();
651650
}
652651

@@ -674,17 +673,11 @@ function extractRoot(doc: Document, name: string): Xml {
674673
return doc.root;
675674
}
676675

677-
function extractField(
678-
node: Xml,
679-
name: string,
680-
): Xml | undefined {
676+
function extractField(node: Xml, name: string): Xml | undefined {
681677
return node.children.find((node) => node.name === name);
682678
}
683679

684-
function extractContent(
685-
node: Xml,
686-
name: string,
687-
): string | undefined {
680+
function extractContent(node: Xml, name: string): string | undefined {
688681
const field = extractField(node, name);
689682
const content = field?.content;
690683
if (content === undefined) {

0 commit comments

Comments
 (0)