Skip to content

Commit f0ffcd7

Browse files
committed
Added r2 bucket lifecycle command (list, add, remove, set)
1 parent b4a0e74 commit f0ffcd7

File tree

6 files changed

+870
-0
lines changed

6 files changed

+870
-0
lines changed

.changeset/hip-cycles-explain.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 lifecycle command to Wrangler including list, add, remove, set

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

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as fs from "node:fs";
2+
import { writeFileSync } from "node:fs";
23
import { http, HttpResponse } from "msw";
34
import { MAX_UPLOAD_SIZE } from "../r2/constants";
45
import { actionsForEventCategories } from "../r2/helpers";
@@ -100,6 +101,7 @@ describe("r2", () => {
100101
wrangler r2 bucket notification Manage event notification rules for an R2 bucket
101102
wrangler r2 bucket domain Manage custom domains for an R2 bucket
102103
wrangler r2 bucket dev-url Manage public access via the r2.dev URL for an R2 bucket
104+
wrangler r2 bucket lifecycle Manage lifecycle rules for an R2 bucket
103105
104106
GLOBAL FLAGS
105107
-j, --experimental-json-config Experimental: support wrangler.json [boolean]
@@ -137,6 +139,7 @@ describe("r2", () => {
137139
wrangler r2 bucket notification Manage event notification rules for an R2 bucket
138140
wrangler r2 bucket domain Manage custom domains for an R2 bucket
139141
wrangler r2 bucket dev-url Manage public access via the r2.dev URL for an R2 bucket
142+
wrangler r2 bucket lifecycle Manage lifecycle rules for an R2 bucket
140143
141144
GLOBAL FLAGS
142145
-j, --experimental-json-config Experimental: support wrangler.json [boolean]
@@ -1869,6 +1872,240 @@ binding = \\"testBucket\\""
18691872
});
18701873
});
18711874
});
1875+
describe("lifecycle", () => {
1876+
const { setIsTTY } = useMockIsTTY();
1877+
mockAccountId();
1878+
mockApiToken();
1879+
describe("list", () => {
1880+
it("should list lifecycle rules when they exist", async () => {
1881+
const bucketName = "my-bucket";
1882+
const lifecycleRules = [
1883+
{
1884+
id: "rule-1",
1885+
enabled: true,
1886+
conditions: { prefix: "images/" },
1887+
deleteObjectsTransition: {
1888+
condition: {
1889+
type: "Age",
1890+
maxAge: 2592000,
1891+
},
1892+
},
1893+
},
1894+
];
1895+
msw.use(
1896+
http.get(
1897+
"*/accounts/:accountId/r2/buckets/:bucketName/lifecycle",
1898+
async ({ params }) => {
1899+
const { accountId, bucketName: bucketParam } = params;
1900+
expect(accountId).toEqual("some-account-id");
1901+
expect(bucketParam).toEqual(bucketName);
1902+
return HttpResponse.json(
1903+
createFetchResult({
1904+
rules: lifecycleRules,
1905+
})
1906+
);
1907+
},
1908+
{ once: true }
1909+
)
1910+
);
1911+
await runWrangler(`r2 bucket lifecycle list ${bucketName}`);
1912+
expect(std.out).toMatchInlineSnapshot(`
1913+
"Listing lifecycle rules for bucket 'my-bucket'...
1914+
id: rule-1
1915+
enabled: Yes
1916+
prefix: images/
1917+
action: Expire objects after 30 days"
1918+
`);
1919+
});
1920+
});
1921+
describe("add", () => {
1922+
it("it should add a lifecycle rule using command-line arguments", async () => {
1923+
const bucketName = "my-bucket";
1924+
const ruleId = "my-rule";
1925+
const prefix = "images/";
1926+
const conditionType = "Age";
1927+
const conditionValue = "30";
1928+
1929+
msw.use(
1930+
http.get(
1931+
"*/accounts/:accountId/r2/buckets/:bucketName/lifecycle",
1932+
async ({ params }) => {
1933+
const { accountId, bucketName: bucketParam } = params;
1934+
expect(accountId).toEqual("some-account-id");
1935+
expect(bucketParam).toEqual(bucketName);
1936+
return HttpResponse.json(
1937+
createFetchResult({
1938+
rules: [],
1939+
})
1940+
);
1941+
},
1942+
{ once: true }
1943+
),
1944+
http.put(
1945+
"*/accounts/:accountId/r2/buckets/:bucketName/lifecycle",
1946+
async ({ request, params }) => {
1947+
const { accountId, bucketName: bucketParam } = params;
1948+
expect(accountId).toEqual("some-account-id");
1949+
expect(bucketName).toEqual(bucketParam);
1950+
const requestBody = await request.json();
1951+
expect(requestBody).toEqual({
1952+
rules: [
1953+
{
1954+
id: ruleId,
1955+
enabled: true,
1956+
conditions: { prefix: prefix },
1957+
deleteObjectsTransition: {
1958+
condition: {
1959+
type: conditionType,
1960+
maxAge: 2592000,
1961+
},
1962+
},
1963+
},
1964+
],
1965+
});
1966+
return HttpResponse.json(createFetchResult({}));
1967+
},
1968+
{ once: true }
1969+
)
1970+
);
1971+
await runWrangler(
1972+
`r2 bucket lifecycle add ${bucketName} --id ${ruleId} --prefix ${prefix} --expire-days ${conditionValue}`
1973+
);
1974+
expect(std.out).toMatchInlineSnapshot(`
1975+
"Adding lifecycle rule 'my-rule' to bucket 'my-bucket'...
1976+
✨ Added lifecycle rule 'my-rule' to bucket 'my-bucket'."
1977+
`);
1978+
});
1979+
});
1980+
describe("remove", () => {
1981+
it("should remove a lifecycle rule as expected", async () => {
1982+
const bucketName = "my-bucket";
1983+
const ruleId = "my-rule";
1984+
const lifecycleRules = {
1985+
rules: [
1986+
{
1987+
id: ruleId,
1988+
enabled: true,
1989+
conditions: {},
1990+
},
1991+
],
1992+
};
1993+
msw.use(
1994+
http.get(
1995+
"*/accounts/:accountId/r2/buckets/:bucketName/lifecycle",
1996+
async ({ params }) => {
1997+
const { accountId, bucketName: bucketParam } = params;
1998+
expect(accountId).toEqual("some-account-id");
1999+
expect(bucketParam).toEqual(bucketName);
2000+
return HttpResponse.json(createFetchResult(lifecycleRules));
2001+
},
2002+
{ once: true }
2003+
),
2004+
http.put(
2005+
"*/accounts/:accountId/r2/buckets/:bucketName/lifecycle",
2006+
async ({ request, params }) => {
2007+
const { accountId, bucketName: bucketParam } = params;
2008+
expect(accountId).toEqual("some-account-id");
2009+
expect(bucketName).toEqual(bucketParam);
2010+
const requestBody = await request.json();
2011+
expect(requestBody).toEqual({
2012+
rules: [],
2013+
});
2014+
return HttpResponse.json(createFetchResult({}));
2015+
},
2016+
{ once: true }
2017+
)
2018+
);
2019+
await runWrangler(
2020+
`r2 bucket lifecycle remove ${bucketName} --id ${ruleId}`
2021+
);
2022+
expect(std.out).toMatchInlineSnapshot(`
2023+
"Removing lifecycle rule 'my-rule' from bucket 'my-bucket'...
2024+
Lifecycle rule 'my-rule' removed from bucket 'my-bucket'."
2025+
`);
2026+
});
2027+
it("should handle removing non-existant rule ID as expected", async () => {
2028+
const bucketName = "my-bucket";
2029+
const ruleId = "my-rule";
2030+
const lifecycleRules = {
2031+
rules: [],
2032+
};
2033+
msw.use(
2034+
http.get(
2035+
"*/accounts/:accountId/r2/buckets/:bucketName/lifecycle",
2036+
async ({ params }) => {
2037+
const { accountId, bucketName: bucketParam } = params;
2038+
expect(accountId).toEqual("some-account-id");
2039+
expect(bucketParam).toEqual(bucketName);
2040+
return HttpResponse.json(createFetchResult(lifecycleRules));
2041+
},
2042+
{ once: true }
2043+
)
2044+
);
2045+
await expect(() =>
2046+
runWrangler(
2047+
`r2 bucket lifecycle remove ${bucketName} --id ${ruleId}`
2048+
)
2049+
).rejects.toThrowErrorMatchingInlineSnapshot(
2050+
"[Error: Lifecycle rule with ID 'my-rule' not found in configuration for 'my-bucket'.]"
2051+
);
2052+
});
2053+
});
2054+
describe("set", () => {
2055+
it("should set lifecycle configuration from a JSON file", async () => {
2056+
const bucketName = "my-bucket";
2057+
const filePath = "lifecycle-configuration.json";
2058+
const lifecycleRules = {
2059+
rules: [
2060+
{
2061+
id: "rule-1",
2062+
enabled: true,
2063+
conditions: {},
2064+
deleteObjectsTransition: {
2065+
condition: {
2066+
type: "Age",
2067+
maxAge: 2592000,
2068+
},
2069+
},
2070+
},
2071+
],
2072+
};
2073+
2074+
writeFileSync(filePath, JSON.stringify(lifecycleRules));
2075+
2076+
setIsTTY(true);
2077+
mockConfirm({
2078+
text: `Are you sure you want to overwrite all existing lifecycle rules for bucket '${bucketName}'?`,
2079+
result: true,
2080+
});
2081+
2082+
msw.use(
2083+
http.put(
2084+
"*/accounts/:accountId/r2/buckets/:bucketName/lifecycle",
2085+
async ({ request, params }) => {
2086+
const { accountId, bucketName: bucketParam } = params;
2087+
expect(accountId).toEqual("some-account-id");
2088+
expect(bucketName).toEqual(bucketParam);
2089+
const requestBody = await request.json();
2090+
expect(requestBody).toEqual({
2091+
...lifecycleRules,
2092+
});
2093+
return HttpResponse.json(createFetchResult({}));
2094+
},
2095+
{ once: true }
2096+
)
2097+
);
2098+
2099+
await runWrangler(
2100+
`r2 bucket lifecycle set ${bucketName} --file ${filePath}`
2101+
);
2102+
expect(std.out).toMatchInlineSnapshot(`
2103+
"Setting lifecycle configuration (1 rules) for bucket 'my-bucket'...
2104+
✨ Set lifecycle configuration for bucket 'my-bucket'."
2105+
`);
2106+
});
2107+
});
2108+
});
18722109
});
18732110

18742111
describe("r2 object", () => {

packages/wrangler/src/dialogs.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,28 @@ export async function select<Values extends string>(
130130
});
131131
return value;
132132
}
133+
134+
export async function multiselect<Values extends string>(
135+
text: string,
136+
options: SelectOptions<Values>
137+
): Promise<Values[]> {
138+
if (isNonInteractiveOrCI()) {
139+
throw new NoDefaultValueProvided();
140+
}
141+
const { value } = await prompts({
142+
type: "multiselect",
143+
name: "value",
144+
message: text,
145+
choices: options.choices,
146+
instructions: false,
147+
hint: "- Space to select. Return to submit",
148+
onState: (state) => {
149+
if (state.aborted) {
150+
process.nextTick(() => {
151+
process.exit(1);
152+
});
153+
}
154+
},
155+
});
156+
return value;
157+
}

0 commit comments

Comments
 (0)