Skip to content

Commit 837f2f5

Browse files
authored
Added wrangler r2 bucket info command, improved formatting of output for r2 bucket list command (#7212)
1 parent c12c0fe commit 837f2f5

File tree

6 files changed

+320
-68
lines changed

6 files changed

+320
-68
lines changed

.changeset/tame-bobcats-suffer.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
Added r2 bucket info command to Wrangler. Improved formatting of r2 bucket list output

packages/wrangler/src/__tests__/r2.test.ts

Lines changed: 83 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { runInTempDir } from "./helpers/run-in-tmp";
1212
import { runWrangler } from "./helpers/run-wrangler";
1313
import type {
1414
PutNotificationRequestBody,
15-
R2BucketInfo,
1615
R2EventableOperation,
1716
R2EventType,
1817
} from "../r2/helpers";
@@ -92,14 +91,15 @@ describe("r2", () => {
9291
Manage R2 buckets
9392
9493
COMMANDS
95-
wrangler r2 bucket create <name> Create a new R2 bucket
96-
wrangler r2 bucket update Update bucket state
97-
wrangler r2 bucket list List R2 buckets
98-
wrangler r2 bucket delete <name> Delete an R2 bucket
99-
wrangler r2 bucket sippy Manage Sippy incremental migration on an R2 bucket
100-
wrangler r2 bucket notification Manage event notification rules for an R2 bucket
101-
wrangler r2 bucket domain Manage custom domains for an R2 bucket
102-
wrangler r2 bucket dev-url Manage public access via the r2.dev URL for an R2 bucket
94+
wrangler r2 bucket create <name> Create a new R2 bucket
95+
wrangler r2 bucket update Update bucket state
96+
wrangler r2 bucket list List R2 buckets
97+
wrangler r2 bucket info <bucket> Get information about an R2 bucket
98+
wrangler r2 bucket delete <bucket> Delete an R2 bucket
99+
wrangler r2 bucket sippy Manage Sippy incremental migration on an R2 bucket
100+
wrangler r2 bucket notification Manage event notification rules for an R2 bucket
101+
wrangler r2 bucket domain Manage custom domains for an R2 bucket
102+
wrangler r2 bucket dev-url Manage public access via the r2.dev URL for an R2 bucket
103103
104104
GLOBAL FLAGS
105105
-j, --experimental-json-config Experimental: support wrangler.json [boolean]
@@ -128,14 +128,15 @@ describe("r2", () => {
128128
Manage R2 buckets
129129
130130
COMMANDS
131-
wrangler r2 bucket create <name> Create a new R2 bucket
132-
wrangler r2 bucket update Update bucket state
133-
wrangler r2 bucket list List R2 buckets
134-
wrangler r2 bucket delete <name> Delete an R2 bucket
135-
wrangler r2 bucket sippy Manage Sippy incremental migration on an R2 bucket
136-
wrangler r2 bucket notification Manage event notification rules for an R2 bucket
137-
wrangler r2 bucket domain Manage custom domains for an R2 bucket
138-
wrangler r2 bucket dev-url Manage public access via the r2.dev URL for an R2 bucket
131+
wrangler r2 bucket create <name> Create a new R2 bucket
132+
wrangler r2 bucket update Update bucket state
133+
wrangler r2 bucket list List R2 buckets
134+
wrangler r2 bucket info <bucket> Get information about an R2 bucket
135+
wrangler r2 bucket delete <bucket> Delete an R2 bucket
136+
wrangler r2 bucket sippy Manage Sippy incremental migration on an R2 bucket
137+
wrangler r2 bucket notification Manage event notification rules for an R2 bucket
138+
wrangler r2 bucket domain Manage custom domains for an R2 bucket
139+
wrangler r2 bucket dev-url Manage public access via the r2.dev URL for an R2 bucket
139140
140141
GLOBAL FLAGS
141142
-j, --experimental-json-config Experimental: support wrangler.json [boolean]
@@ -148,9 +149,15 @@ describe("r2", () => {
148149

149150
describe("list", () => {
150151
it("should list buckets & check request inputs", async () => {
151-
const expectedBuckets: R2BucketInfo[] = [
152-
{ name: "bucket-1-local-once", creation_date: "01-01-2001" },
153-
{ name: "bucket-2-local-once", creation_date: "01-01-2001" },
152+
const mockBuckets = [
153+
{
154+
name: "bucket-1-local-once",
155+
creation_date: "01-01-2001",
156+
},
157+
{
158+
name: "bucket-2-local-once",
159+
creation_date: "01-01-2001",
160+
},
154161
];
155162
msw.use(
156163
http.get(
@@ -161,27 +168,65 @@ describe("r2", () => {
161168
expect(await request.text()).toEqual("");
162169
return HttpResponse.json(
163170
createFetchResult({
164-
buckets: [
165-
{
166-
name: "bucket-1-local-once",
167-
creation_date: "01-01-2001",
168-
},
169-
{
170-
name: "bucket-2-local-once",
171-
creation_date: "01-01-2001",
172-
},
173-
],
171+
buckets: mockBuckets,
174172
})
175173
);
176174
},
177175
{ once: true }
178176
)
179177
);
180-
await runWrangler("r2 bucket list");
181178

182-
expect(std.err).toMatchInlineSnapshot(`""`);
183-
const buckets = JSON.parse(std.out);
184-
expect(buckets).toEqual(expectedBuckets);
179+
await runWrangler(`r2 bucket list`);
180+
expect(std.out).toMatchInlineSnapshot(`
181+
"Listing buckets...
182+
name: bucket-1-local-once
183+
creation_date: 01-01-2001
184+
185+
name: bucket-2-local-once
186+
creation_date: 01-01-2001"
187+
`);
188+
});
189+
});
190+
191+
describe("info", () => {
192+
it("should get information for the given bucket", async () => {
193+
const bucketName = "my-bucket";
194+
const bucketInfo = {
195+
name: bucketName,
196+
creation_date: "01-01-2001",
197+
location: "WNAM",
198+
storage_class: "Standard",
199+
};
200+
201+
msw.use(
202+
http.get(
203+
"*/accounts/:accountId/r2/buckets/:bucketName",
204+
async ({ params }) => {
205+
const { accountId, bucketName: bucketParam } = params;
206+
expect(accountId).toEqual("some-account-id");
207+
expect(bucketParam).toEqual(bucketName);
208+
return HttpResponse.json(
209+
createFetchResult({
210+
...bucketInfo,
211+
})
212+
);
213+
},
214+
{ once: true }
215+
),
216+
http.post("*/graphql", async () => {
217+
return HttpResponse.json(createFetchResult({}));
218+
})
219+
);
220+
await runWrangler(`r2 bucket info ${bucketName}`);
221+
expect(std.out).toMatchInlineSnapshot(`
222+
"Getting info for 'my-bucket'...
223+
name: my-bucket
224+
created: 01-01-2001
225+
location: WNAM
226+
default_storage_class: Standard
227+
object_count: 0
228+
bucket_size: 0 B"
229+
`);
185230
});
186231
});
187232

@@ -475,12 +520,12 @@ binding = \\"testBucket\\""
475520
);
476521
expect(std.out).toMatchInlineSnapshot(`
477522
"
478-
wrangler r2 bucket delete <name>
523+
wrangler r2 bucket delete <bucket>
479524
480525
Delete an R2 bucket
481526
482527
POSITIONALS
483-
name The name of the bucket to delete [string] [required]
528+
bucket The name of the bucket to delete [string] [required]
484529
485530
GLOBAL FLAGS
486531
-j, --experimental-json-config Experimental: support wrangler.json [boolean]
@@ -515,12 +560,12 @@ binding = \\"testBucket\\""
515560
);
516561
expect(std.out).toMatchInlineSnapshot(`
517562
"
518-
wrangler r2 bucket delete <name>
563+
wrangler r2 bucket delete <bucket>
519564
520565
Delete an R2 bucket
521566
522567
POSITIONALS
523-
name The name of the bucket to delete [string] [required]
568+
bucket The name of the bucket to delete [string] [required]
524569
525570
GLOBAL FLAGS
526571
-j, --experimental-json-config Experimental: support wrangler.json [boolean]

packages/wrangler/src/r2/helpers.ts

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Miniflare } from "miniflare";
2-
import { fetchResult } from "../cfetch";
2+
import prettyBytes from "pretty-bytes";
3+
import { fetchGraphqlResult, fetchResult } from "../cfetch";
34
import { fetchR2Objects } from "../cfetch/internal";
45
import { getLocalPersistencePath } from "../dev/get-local-persistence-path";
56
import { buildPersistOptions } from "../dev/miniflare";
@@ -20,6 +21,29 @@ import type { HeadersInit } from "undici";
2021
export interface R2BucketInfo {
2122
name: string;
2223
creation_date: string;
24+
location?: string;
25+
storage_class?: string;
26+
}
27+
28+
export interface R2BucketMetrics {
29+
max?: {
30+
objectCount?: number;
31+
payloadSize?: number;
32+
metadataSize?: number;
33+
};
34+
dimensions: {
35+
datetime?: string;
36+
};
37+
}
38+
39+
export interface R2BucketMetricsGraphQLResponse {
40+
data: {
41+
viewer: {
42+
accounts: {
43+
r2StorageAdaptiveGroups?: R2BucketMetrics[];
44+
}[];
45+
};
46+
};
2347
}
2448

2549
/**
@@ -39,6 +63,114 @@ export async function listR2Buckets(
3963
return results.buckets;
4064
}
4165

66+
export function tablefromR2BucketsListResponse(buckets: R2BucketInfo[]): {
67+
name: string;
68+
creation_date: string;
69+
}[] {
70+
const rows = [];
71+
for (const bucket of buckets) {
72+
rows.push({
73+
name: bucket.name,
74+
creation_date: bucket.creation_date,
75+
});
76+
}
77+
return rows;
78+
}
79+
80+
export async function getR2Bucket(
81+
accountId: string,
82+
bucketName: string,
83+
jurisdiction?: string
84+
): Promise<R2BucketInfo> {
85+
const headers: HeadersInit = {};
86+
if (jurisdiction !== undefined) {
87+
headers["cf-r2-jurisdiction"] = jurisdiction;
88+
}
89+
const result = await fetchResult<R2BucketInfo>(
90+
`/accounts/${accountId}/r2/buckets/${bucketName}`,
91+
{
92+
method: "GET",
93+
headers,
94+
}
95+
);
96+
return result;
97+
}
98+
99+
export async function getR2BucketMetrics(
100+
accountId: string,
101+
bucketName: string,
102+
jurisdiction?: string
103+
): Promise<{ objectCount: number; totalSize: string }> {
104+
const today = new Date();
105+
const yesterday = new Date(new Date(today).setDate(today.getDate() - 1));
106+
107+
let fullBucketName = bucketName;
108+
if (jurisdiction) {
109+
fullBucketName = `${jurisdiction}_${bucketName}`;
110+
}
111+
112+
const storageMetricsQuery = `
113+
query getR2StorageMetrics($accountTag: String, $filter: R2StorageAdaptiveGroupsFilter_InputObject) {
114+
viewer {
115+
accounts(filter: { accountTag: $accountTag }) {
116+
r2StorageAdaptiveGroups(
117+
limit: 1
118+
filter: $filter
119+
orderBy: [datetime_DESC]
120+
) {
121+
max {
122+
objectCount
123+
payloadSize
124+
metadataSize
125+
}
126+
dimensions {
127+
datetime
128+
}
129+
}
130+
}
131+
}
132+
}
133+
`;
134+
135+
const variables = {
136+
accountTag: accountId,
137+
filter: {
138+
datetime_geq: yesterday.toISOString(),
139+
datetime_leq: today.toISOString(),
140+
bucketName: fullBucketName,
141+
},
142+
};
143+
const storageMetricsResult =
144+
await fetchGraphqlResult<R2BucketMetricsGraphQLResponse>({
145+
method: "POST",
146+
body: JSON.stringify({
147+
query: storageMetricsQuery,
148+
operationName: "getR2StorageMetrics",
149+
variables,
150+
}),
151+
headers: {
152+
"Content-Type": "application/json",
153+
},
154+
});
155+
156+
if (storageMetricsResult) {
157+
const metricsData =
158+
storageMetricsResult.data?.viewer?.accounts[0]
159+
?.r2StorageAdaptiveGroups?.[0];
160+
if (metricsData && metricsData.max) {
161+
const objectCount = metricsData.max.objectCount || 0;
162+
const totalSize =
163+
(metricsData.max.payloadSize || 0) +
164+
(metricsData.max.metadataSize || 0);
165+
return {
166+
objectCount,
167+
totalSize: prettyBytes(totalSize),
168+
};
169+
}
170+
}
171+
return { objectCount: 0, totalSize: "0 B" };
172+
}
173+
42174
/**
43175
* Create a bucket with the given `bucketName` within the account given by `accountId`.
44176
*

0 commit comments

Comments
 (0)