Skip to content

Commit 038fdd9

Browse files
authored
Added location option for the Wrangler R2 bucket create command (#7092)
1 parent 8ca4b32 commit 038fdd9

File tree

6 files changed

+134
-85
lines changed

6 files changed

+134
-85
lines changed

.changeset/tender-pens-change.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
Added location hint option for the Wrangler R2 bucket create command

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

+47-23
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,9 @@ describe("r2", () => {
204204
-v, --version Show version number [boolean]
205205
206206
OPTIONS
207-
-J, --jurisdiction The jurisdiction where the new bucket will be created [string]
208-
-s, --storage-class The default storage class for objects uploaded to this bucket [string]"
207+
--location The optional location hint that determines geographic placement of the R2 bucket [string] [choices: \\"weur\\", \\"eeur\\", \\"apac\\", \\"wnam\\", \\"enam\\"]
208+
-s, --storage-class The default storage class for objects uploaded to this bucket [string]
209+
-J, --jurisdiction The jurisdiction where the new bucket will be created [string]"
209210
`);
210211
expect(std.err).toMatchInlineSnapshot(`
211212
"X [ERROR] Not enough non-option arguments: got 0, need at least 1
@@ -221,24 +222,25 @@ describe("r2", () => {
221222
`[Error: Unknown arguments: def, ghi]`
222223
);
223224
expect(std.out).toMatchInlineSnapshot(`
224-
"
225-
wrangler r2 bucket create <name>
225+
"
226+
wrangler r2 bucket create <name>
226227
227-
Create a new R2 bucket
228+
Create a new R2 bucket
228229
229-
POSITIONALS
230-
name The name of the new bucket [string] [required]
230+
POSITIONALS
231+
name The name of the new bucket [string] [required]
231232
232-
GLOBAL FLAGS
233-
-j, --experimental-json-config Experimental: support wrangler.json [boolean]
234-
-c, --config Path to .toml configuration file [string]
235-
-e, --env Environment to use for operations and .env files [string]
236-
-h, --help Show help [boolean]
237-
-v, --version Show version number [boolean]
233+
GLOBAL FLAGS
234+
-j, --experimental-json-config Experimental: support wrangler.json [boolean]
235+
-c, --config Path to .toml configuration file [string]
236+
-e, --env Environment to use for operations and .env files [string]
237+
-h, --help Show help [boolean]
238+
-v, --version Show version number [boolean]
238239
239-
OPTIONS
240-
-J, --jurisdiction The jurisdiction where the new bucket will be created [string]
241-
-s, --storage-class The default storage class for objects uploaded to this bucket [string]"
240+
OPTIONS
241+
--location The optional location hint that determines geographic placement of the R2 bucket [string] [choices: \\"weur\\", \\"eeur\\", \\"apac\\", \\"wnam\\", \\"enam\\"]
242+
-s, --storage-class The default storage class for objects uploaded to this bucket [string]
243+
-J, --jurisdiction The jurisdiction where the new bucket will be created [string]"
242244
`);
243245
expect(std.err).toMatchInlineSnapshot(`
244246
"X [ERROR] Unknown arguments: def, ghi
@@ -262,8 +264,8 @@ describe("r2", () => {
262264
);
263265
await runWrangler("r2 bucket create testBucket");
264266
expect(std.out).toMatchInlineSnapshot(`
265-
"Creating bucket testBucket with default storage class set to Standard.
266-
Created bucket testBucket with default storage class set to Standard."
267+
"Creating bucket 'testBucket'...
268+
Created bucket 'testBucket' with default storage class of Standard."
267269
`);
268270
});
269271

@@ -283,16 +285,16 @@ describe("r2", () => {
283285
);
284286
await runWrangler("r2 bucket create testBucket -J eu");
285287
expect(std.out).toMatchInlineSnapshot(`
286-
"Creating bucket testBucket (eu) with default storage class set to Standard.
287-
Created bucket testBucket (eu) with default storage class set to Standard."
288+
"Creating bucket 'testBucket (eu)'...
289+
Created bucket 'testBucket (eu)' with default storage class of Standard."
288290
`);
289291
});
290292

291293
it("should create a bucket with the expected default storage class", async () => {
292294
await runWrangler("r2 bucket create testBucket -s InfrequentAccess");
293295
expect(std.out).toMatchInlineSnapshot(`
294-
"Creating bucket testBucket with default storage class set to InfrequentAccess.
295-
Created bucket testBucket with default storage class set to InfrequentAccess."
296+
"Creating bucket 'testBucket'...
297+
Created bucket 'testBucket' with default storage class of InfrequentAccess."
296298
`);
297299
});
298300

@@ -303,7 +305,7 @@ describe("r2", () => {
303305
`[APIError: A request to the Cloudflare API (/accounts/some-account-id/r2/buckets) failed.]`
304306
);
305307
expect(std.out).toMatchInlineSnapshot(`
306-
"Creating bucket testBucket with default storage class set to Foo.
308+
"Creating bucket 'testBucket'...
307309
308310
X [ERROR] A request to the Cloudflare API (/accounts/some-account-id/r2/buckets) failed.
309311
@@ -315,6 +317,28 @@ describe("r2", () => {
315317
"
316318
`);
317319
});
320+
it("should create a bucket with the expected location hint", async () => {
321+
msw.use(
322+
http.post(
323+
"*/accounts/:accountId/r2/buckets",
324+
async ({ request, params }) => {
325+
const { accountId } = params;
326+
expect(accountId).toEqual("some-account-id");
327+
expect(await request.json()).toEqual({
328+
name: "testBucket",
329+
locationHint: "weur",
330+
});
331+
return HttpResponse.json(createFetchResult({}));
332+
},
333+
{ once: true }
334+
)
335+
);
336+
await runWrangler("r2 bucket create testBucket --location weur");
337+
expect(std.out).toMatchInlineSnapshot(`
338+
"Creating bucket 'testBucket'...
339+
✅ Created bucket 'testBucket' with location hint weur and default storage class of Standard."
340+
`);
341+
});
318342
});
319343

320344
describe("update", () => {

packages/wrangler/src/r2/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
* The maximum file size we can upload using the V4 API.
33
*/
44
export const MAX_UPLOAD_SIZE = 300 * 1024 * 1024;
5+
export const LOCATION_CHOICES = ["weur", "eeur", "apac", "wnam", "enam"];

packages/wrangler/src/r2/create.ts

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { printWranglerBanner } from "..";
2+
import { readConfig } from "../config";
3+
import { UserError } from "../errors";
4+
import { logger } from "../logger";
5+
import * as metrics from "../metrics";
6+
import { requireAuth } from "../user";
7+
import { LOCATION_CHOICES } from "./constants";
8+
import { createR2Bucket, isValidR2BucketName } from "./helpers";
9+
import type {
10+
CommonYargsArgv,
11+
StrictYargsOptionsToInterface,
12+
} from "../yargs-types";
13+
14+
export function Options(yargs: CommonYargsArgv) {
15+
return yargs
16+
.positional("name", {
17+
describe: "The name of the new bucket",
18+
type: "string",
19+
demandOption: true,
20+
})
21+
.option("location", {
22+
describe:
23+
"The optional location hint that determines geographic placement of the R2 bucket",
24+
choices: LOCATION_CHOICES,
25+
requiresArg: true,
26+
type: "string",
27+
})
28+
.option("storage-class", {
29+
describe: "The default storage class for objects uploaded to this bucket",
30+
alias: "s",
31+
requiresArg: false,
32+
type: "string",
33+
})
34+
.option("jurisdiction", {
35+
describe: "The jurisdiction where the new bucket will be created",
36+
alias: "J",
37+
requiresArg: true,
38+
type: "string",
39+
});
40+
}
41+
42+
type HandlerOptions = StrictYargsOptionsToInterface<typeof Options>;
43+
export async function Handler(args: HandlerOptions) {
44+
await printWranglerBanner();
45+
const config = readConfig(args.config, args);
46+
const accountId = await requireAuth(config);
47+
const { name, location, storageClass, jurisdiction } = args;
48+
49+
if (!isValidR2BucketName(name)) {
50+
throw new UserError(
51+
`The bucket name "${name}" is invalid. Bucket names can only have alphanumeric and - characters.`
52+
);
53+
}
54+
55+
if (jurisdiction && location) {
56+
throw new UserError(
57+
"Provide either a jurisdiction or location hint - not both."
58+
);
59+
}
60+
61+
let fullBucketName = `${name}`;
62+
if (jurisdiction !== undefined) {
63+
fullBucketName += ` (${jurisdiction})`;
64+
}
65+
66+
logger.log(`Creating bucket '${fullBucketName}'...`);
67+
await createR2Bucket(accountId, name, location, jurisdiction, storageClass);
68+
logger.log(
69+
`✅ Created bucket '${fullBucketName}' with${
70+
location ? ` location hint ${location} and` : ``
71+
} default storage class of ${storageClass ? storageClass : `Standard`}.`
72+
);
73+
await metrics.sendMetricsEvent("create r2 bucket", {
74+
sendMetrics: config.send_metrics,
75+
});
76+
}

packages/wrangler/src/r2/helpers.ts

+2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export async function listR2Buckets(
4848
export async function createR2Bucket(
4949
accountId: string,
5050
bucketName: string,
51+
location?: string,
5152
jurisdiction?: string,
5253
storageClass?: string
5354
): Promise<void> {
@@ -60,6 +61,7 @@ export async function createR2Bucket(
6061
body: JSON.stringify({
6162
name: bucketName,
6263
...(storageClass !== undefined && { storageClass }),
64+
...(location !== undefined && { locationHint: location }),
6365
}),
6466
headers,
6567
});

packages/wrangler/src/r2/index.ts

+3-62
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@ import { logger } from "../logger";
1111
import * as metrics from "../metrics";
1212
import { requireAuth } from "../user";
1313
import { MAX_UPLOAD_SIZE } from "./constants";
14+
import * as Create from "./create";
1415
import {
1516
bucketAndKeyFromObjectPath,
16-
createR2Bucket,
1717
deleteR2Bucket,
1818
deleteR2Object,
1919
getR2Object,
20-
isValidR2BucketName,
2120
listR2Buckets,
2221
putR2Object,
2322
updateR2BucketStorageClass,
@@ -434,66 +433,8 @@ export function r2(r2Yargs: CommonYargsArgv, subHelp: SubHelp) {
434433
r2BucketYargs.command(
435434
"create <name>",
436435
"Create a new R2 bucket",
437-
(yargs) => {
438-
return yargs
439-
.positional("name", {
440-
describe: "The name of the new bucket",
441-
type: "string",
442-
demandOption: true,
443-
})
444-
.option("jurisdiction", {
445-
describe: "The jurisdiction where the new bucket will be created",
446-
alias: "J",
447-
requiresArg: true,
448-
type: "string",
449-
})
450-
.option("storage-class", {
451-
describe:
452-
"The default storage class for objects uploaded to this bucket",
453-
alias: "s",
454-
requiresArg: false,
455-
type: "string",
456-
});
457-
},
458-
async (args) => {
459-
await printWranglerBanner();
460-
461-
if (!isValidR2BucketName(args.name)) {
462-
throw new CommandLineArgsError(
463-
`The bucket name "${args.name}" is invalid. Bucket names can only have alphanumeric and - characters.`
464-
);
465-
}
466-
467-
const config = readConfig(args.config, args);
468-
469-
const accountId = await requireAuth(config);
470-
471-
let fullBucketName = `${args.name}`;
472-
if (args.jurisdiction !== undefined) {
473-
fullBucketName += ` (${args.jurisdiction})`;
474-
}
475-
476-
let defaultStorageClass = ` with default storage class set to `;
477-
if (args.storageClass !== undefined) {
478-
defaultStorageClass += args.storageClass;
479-
} else {
480-
defaultStorageClass += "Standard";
481-
}
482-
483-
logger.log(
484-
`Creating bucket ${fullBucketName}${defaultStorageClass}.`
485-
);
486-
await createR2Bucket(
487-
accountId,
488-
args.name,
489-
args.jurisdiction,
490-
args.storageClass
491-
);
492-
logger.log(`Created bucket ${fullBucketName}${defaultStorageClass}.`);
493-
await metrics.sendMetricsEvent("create r2 bucket", {
494-
sendMetrics: config.send_metrics,
495-
});
496-
}
436+
Create.Options,
437+
Create.Handler
497438
);
498439

499440
r2BucketYargs.command("update", "Update bucket state", (updateYargs) => {

0 commit comments

Comments
 (0)