Skip to content

Commit d8c44ad

Browse files
committed
feat: add listBuckets method
1 parent 86575ac commit d8c44ad

File tree

7 files changed

+130
-50
lines changed

7 files changed

+130
-50
lines changed

mod.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from "./src/client.ts";
22
export * from "./src/bucket.ts";
33
export type {
4+
Bucket,
45
CommonPrefix,
56
CopyDirective,
67
CopyObjectOptions,
@@ -12,9 +13,11 @@ export type {
1213
GetObjectResponse,
1314
HeadObjectResponse,
1415
ListAllObjectsOptions,
16+
ListBucketsResponses,
1517
ListObjectsOptions,
1618
ListObjectsResponse,
1719
LockMode,
20+
Owner,
1821
PutObjectOptions,
1922
PutObjectResponse,
2023
ReplicationStatus,

src/bucket.ts

Lines changed: 4 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import {
2-
AWSSignerV4,
3-
decodeXMLEntities,
4-
parseXML,
5-
pooledMap,
6-
} from "../deps.ts";
1+
import { AWSSignerV4, parseXML, pooledMap } from "../deps.ts";
72
import type { S3Config } from "./client.ts";
83
import type {
94
CommonPrefix,
@@ -27,6 +22,8 @@ import { S3Error } from "./error.ts";
2722
import type { Signer } from "../deps.ts";
2823
import { doRequest, encodeURIS3 } from "./utils.ts";
2924
import type { Params } from "./utils.ts";
25+
import type { Document } from "./xml.ts";
26+
import { extractContent, extractField, extractRoot } from "./xml.ts";
3027

3128
export interface S3BucketConfig extends S3Config {
3229
bucket: string;
@@ -330,9 +327,7 @@ export class S3Bucket {
330327
}
331328

332329
const parsed = {
333-
isTruncated: extractContent(root, "IsTruncated") === "true"
334-
? true
335-
: false,
330+
isTruncated: extractContent(root, "IsTruncated") === "true",
336331
contents: root.children
337332
.filter((node) => node.name === "Contents")
338333
.map<S3Object>((s3obj) => {
@@ -625,40 +620,3 @@ export class S3Bucket {
625620
return deleted;
626621
}
627622
}
628-
629-
interface Document {
630-
declaration: {
631-
attributes: Record<string, unknown>;
632-
};
633-
root: Xml | undefined;
634-
}
635-
636-
interface Xml {
637-
name: string;
638-
attributes: unknown;
639-
children: Xml[];
640-
content?: string;
641-
}
642-
643-
function extractRoot(doc: Document, name: string): Xml {
644-
if (!doc.root || doc.root.name !== name) {
645-
throw new S3Error(
646-
`Malformed XML document. Missing ${name} field.`,
647-
JSON.stringify(doc, undefined, 2),
648-
);
649-
}
650-
return doc.root;
651-
}
652-
653-
function extractField(node: Xml, name: string): Xml | undefined {
654-
return node.children.find((node) => node.name === name);
655-
}
656-
657-
function extractContent(node: Xml, name: string): string | undefined {
658-
const field = extractField(node, name);
659-
const content = field?.content;
660-
if (content === undefined) {
661-
return content;
662-
}
663-
return decodeXMLEntities(content);
664-
}

src/client.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
import { AWSSignerV4 } from "../deps.ts";
2-
import type { CreateBucketOptions } from "./types.ts";
1+
import { AWSSignerV4, parseXML } from "../deps.ts";
2+
import type { CreateBucketOptions, ListBucketsResponses } from "./types.ts";
33
import { S3Error } from "./error.ts";
44
import { S3Bucket } from "./bucket.ts";
55
import { doRequest, encoder } from "./utils.ts";
66
import type { Params } from "./utils.ts";
7+
import {
8+
extractContent,
9+
extractField,
10+
extractFields,
11+
extractRoot,
12+
} from "./xml.ts";
13+
import type { Document } from "./xml.ts";
714

815
export interface S3Config {
916
region: string;
@@ -88,4 +95,44 @@ export class S3 {
8895
bucket,
8996
});
9097
}
98+
99+
async listBuckets(): Promise<ListBucketsResponses> {
100+
const resp = await doRequest({
101+
host: this.#host,
102+
signer: this.#signer,
103+
method: "GET",
104+
});
105+
106+
if (resp.status !== 200) {
107+
throw new S3Error(
108+
`Failed to list buckets": ${resp.status} ${resp.statusText}`,
109+
await resp.text(),
110+
);
111+
}
112+
113+
const xml = await resp.text();
114+
return this.#parseListBucketsResponseXml(xml);
115+
}
116+
117+
#parseListBucketsResponseXml(x: string): ListBucketsResponses {
118+
const doc: Document = parseXML(x);
119+
const root = extractRoot(doc, "ListAllMyBucketsResult");
120+
const buckets = extractField(root, "Buckets")!;
121+
const owner = extractField(root, "Owner")!;
122+
123+
return {
124+
buckets: extractFields(buckets, "Bucket")
125+
.map((bucket) => {
126+
const creationDate = extractContent(bucket, "CreationDate");
127+
return {
128+
name: extractContent(bucket, "Name"),
129+
creationDate: creationDate ? new Date(creationDate) : undefined,
130+
};
131+
}),
132+
owner: {
133+
id: extractContent(owner, "ID"),
134+
displayName: extractContent(owner, "DisplayName"),
135+
},
136+
};
137+
}
91138
}

src/client_test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,17 @@ Deno.test({
4242
);
4343
},
4444
});
45+
46+
Deno.test({
47+
name: "[client] should list all buckets",
48+
async fn() {
49+
const { buckets, owner } = await s3.listBuckets();
50+
assert(buckets.length, "no buckets available");
51+
assertEquals(buckets[0].name, "test");
52+
assert(
53+
buckets[0].creationDate instanceof Date,
54+
"creationDate is not of type Date",
55+
);
56+
assertEquals(owner.displayName, "minio");
57+
},
58+
});

src/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,3 +566,18 @@ export interface CreateBucketOptions {
566566
/** Allows grantee to write the ACL for the applicable bucket. */
567567
grantWriteAcp?: string;
568568
}
569+
570+
export interface Bucket {
571+
creationDate?: Date;
572+
name?: string;
573+
}
574+
575+
export interface Owner {
576+
displayName?: string;
577+
id?: string;
578+
}
579+
580+
export interface ListBucketsResponses {
581+
buckets: Array<Bucket>;
582+
owner: Owner;
583+
}

src/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ export const encoder = new TextEncoder();
1010
interface S3RequestOptions {
1111
host: string;
1212
signer: Signer;
13-
path: string;
1413
method: string;
14+
path?: string;
1515
params?: Params;
1616
headers?: Params;
1717
body?: Uint8Array | undefined;
@@ -20,7 +20,7 @@ interface S3RequestOptions {
2020
export async function doRequest({
2121
host,
2222
signer,
23-
path,
23+
path = "/",
2424
params,
2525
method,
2626
headers,

src/xml.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { S3Error } from "./error.ts";
2+
import { decodeXMLEntities } from "../deps.ts";
3+
4+
export interface Document {
5+
declaration: {
6+
attributes: Record<string, unknown>;
7+
};
8+
root: Xml | undefined;
9+
}
10+
11+
export interface Xml {
12+
name: string;
13+
attributes: unknown;
14+
children: Xml[];
15+
content?: string;
16+
}
17+
18+
export function extractRoot(doc: Document, name: string): Xml {
19+
if (!doc.root || doc.root.name !== name) {
20+
throw new S3Error(
21+
`Malformed XML document. Missing ${name} field.`,
22+
JSON.stringify(doc, undefined, 2),
23+
);
24+
}
25+
return doc.root;
26+
}
27+
28+
export function extractField(node: Xml, name: string): Xml | undefined {
29+
return node.children.find((node) => node.name === name);
30+
}
31+
32+
export function extractFields(node: Xml, name: string): Array<Xml> {
33+
return node.children.filter((node) => node.name === name);
34+
}
35+
36+
export function extractContent(node: Xml, name: string): string | undefined {
37+
const field = extractField(node, name);
38+
const content = field?.content;
39+
if (content === undefined) {
40+
return content;
41+
}
42+
return decodeXMLEntities(content);
43+
}

0 commit comments

Comments
 (0)