Skip to content

Commit 5936bd6

Browse files
committed
Added r2 bucket lifecycle command (list, add, remove, set)
1 parent e221401 commit 5936bd6

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]
@@ -136,6 +138,7 @@ describe("r2", () => {
136138
wrangler r2 bucket notification Manage event notification rules for an R2 bucket
137139
wrangler r2 bucket domain Manage custom domains for an R2 bucket
138140
wrangler r2 bucket dev-url Manage public access via the r2.dev URL for an R2 bucket
141+
wrangler r2 bucket lifecycle Manage lifecycle rules for an R2 bucket
139142
140143
GLOBAL FLAGS
141144
-j, --experimental-json-config Experimental: support wrangler.json [boolean]
@@ -1824,6 +1827,240 @@ binding = \\"testBucket\\""
18241827
});
18251828
});
18261829
});
1830+
describe("lifecycle", () => {
1831+
const { setIsTTY } = useMockIsTTY();
1832+
mockAccountId();
1833+
mockApiToken();
1834+
describe("list", () => {
1835+
it("should list lifecycle rules when they exist", async () => {
1836+
const bucketName = "my-bucket";
1837+
const lifecycleRules = [
1838+
{
1839+
id: "rule-1",
1840+
enabled: true,
1841+
conditions: { prefix: "images/" },
1842+
deleteObjectsTransition: {
1843+
condition: {
1844+
type: "Age",
1845+
maxAge: 2592000,
1846+
},
1847+
},
1848+
},
1849+
];
1850+
msw.use(
1851+
http.get(
1852+
"*/accounts/:accountId/r2/buckets/:bucketName/lifecycle",
1853+
async ({ params }) => {
1854+
const { accountId, bucketName: bucketParam } = params;
1855+
expect(accountId).toEqual("some-account-id");
1856+
expect(bucketParam).toEqual(bucketName);
1857+
return HttpResponse.json(
1858+
createFetchResult({
1859+
rules: lifecycleRules,
1860+
})
1861+
);
1862+
},
1863+
{ once: true }
1864+
)
1865+
);
1866+
await runWrangler(`r2 bucket lifecycle list ${bucketName}`);
1867+
expect(std.out).toMatchInlineSnapshot(`
1868+
"Listing lifecycle rules for bucket 'my-bucket'...
1869+
id: rule-1
1870+
enabled: Yes
1871+
prefix: images/
1872+
action: Expire objects after 30 days"
1873+
`);
1874+
});
1875+
});
1876+
describe("add", () => {
1877+
it("it should add a lifecycle rule using command-line arguments", async () => {
1878+
const bucketName = "my-bucket";
1879+
const ruleId = "my-rule";
1880+
const prefix = "images/";
1881+
const conditionType = "Age";
1882+
const conditionValue = "30";
1883+
1884+
msw.use(
1885+
http.get(
1886+
"*/accounts/:accountId/r2/buckets/:bucketName/lifecycle",
1887+
async ({ params }) => {
1888+
const { accountId, bucketName: bucketParam } = params;
1889+
expect(accountId).toEqual("some-account-id");
1890+
expect(bucketParam).toEqual(bucketName);
1891+
return HttpResponse.json(
1892+
createFetchResult({
1893+
rules: [],
1894+
})
1895+
);
1896+
},
1897+
{ once: true }
1898+
),
1899+
http.put(
1900+
"*/accounts/:accountId/r2/buckets/:bucketName/lifecycle",
1901+
async ({ request, params }) => {
1902+
const { accountId, bucketName: bucketParam } = params;
1903+
expect(accountId).toEqual("some-account-id");
1904+
expect(bucketName).toEqual(bucketParam);
1905+
const requestBody = await request.json();
1906+
expect(requestBody).toEqual({
1907+
rules: [
1908+
{
1909+
id: ruleId,
1910+
enabled: true,
1911+
conditions: { prefix: prefix },
1912+
deleteObjectsTransition: {
1913+
condition: {
1914+
type: conditionType,
1915+
maxAge: 2592000,
1916+
},
1917+
},
1918+
},
1919+
],
1920+
});
1921+
return HttpResponse.json(createFetchResult({}));
1922+
},
1923+
{ once: true }
1924+
)
1925+
);
1926+
await runWrangler(
1927+
`r2 bucket lifecycle add ${bucketName} --id ${ruleId} --prefix ${prefix} --expire-days ${conditionValue}`
1928+
);
1929+
expect(std.out).toMatchInlineSnapshot(`
1930+
"Adding lifecycle rule 'my-rule' to bucket 'my-bucket'...
1931+
✨ Added lifecycle rule 'my-rule' to bucket 'my-bucket'."
1932+
`);
1933+
});
1934+
});
1935+
describe("remove", () => {
1936+
it("should remove a lifecycle rule as expected", async () => {
1937+
const bucketName = "my-bucket";
1938+
const ruleId = "my-rule";
1939+
const lifecycleRules = {
1940+
rules: [
1941+
{
1942+
id: ruleId,
1943+
enabled: true,
1944+
conditions: {},
1945+
},
1946+
],
1947+
};
1948+
msw.use(
1949+
http.get(
1950+
"*/accounts/:accountId/r2/buckets/:bucketName/lifecycle",
1951+
async ({ params }) => {
1952+
const { accountId, bucketName: bucketParam } = params;
1953+
expect(accountId).toEqual("some-account-id");
1954+
expect(bucketParam).toEqual(bucketName);
1955+
return HttpResponse.json(createFetchResult(lifecycleRules));
1956+
},
1957+
{ once: true }
1958+
),
1959+
http.put(
1960+
"*/accounts/:accountId/r2/buckets/:bucketName/lifecycle",
1961+
async ({ request, params }) => {
1962+
const { accountId, bucketName: bucketParam } = params;
1963+
expect(accountId).toEqual("some-account-id");
1964+
expect(bucketName).toEqual(bucketParam);
1965+
const requestBody = await request.json();
1966+
expect(requestBody).toEqual({
1967+
rules: [],
1968+
});
1969+
return HttpResponse.json(createFetchResult({}));
1970+
},
1971+
{ once: true }
1972+
)
1973+
);
1974+
await runWrangler(
1975+
`r2 bucket lifecycle remove ${bucketName} --id ${ruleId}`
1976+
);
1977+
expect(std.out).toMatchInlineSnapshot(`
1978+
"Removing lifecycle rule 'my-rule' from bucket 'my-bucket'...
1979+
Lifecycle rule 'my-rule' removed from bucket 'my-bucket'."
1980+
`);
1981+
});
1982+
it("should handle removing non-existant rule ID as expected", async () => {
1983+
const bucketName = "my-bucket";
1984+
const ruleId = "my-rule";
1985+
const lifecycleRules = {
1986+
rules: [],
1987+
};
1988+
msw.use(
1989+
http.get(
1990+
"*/accounts/:accountId/r2/buckets/:bucketName/lifecycle",
1991+
async ({ params }) => {
1992+
const { accountId, bucketName: bucketParam } = params;
1993+
expect(accountId).toEqual("some-account-id");
1994+
expect(bucketParam).toEqual(bucketName);
1995+
return HttpResponse.json(createFetchResult(lifecycleRules));
1996+
},
1997+
{ once: true }
1998+
)
1999+
);
2000+
await expect(() =>
2001+
runWrangler(
2002+
`r2 bucket lifecycle remove ${bucketName} --id ${ruleId}`
2003+
)
2004+
).rejects.toThrowErrorMatchingInlineSnapshot(
2005+
"[Error: Lifecycle rule with ID 'my-rule' not found in configuration for 'my-bucket'.]"
2006+
);
2007+
});
2008+
});
2009+
describe("set", () => {
2010+
it("should set lifecycle configuration from a JSON file", async () => {
2011+
const bucketName = "my-bucket";
2012+
const filePath = "lifecycle-configuration.json";
2013+
const lifecycleRules = {
2014+
rules: [
2015+
{
2016+
id: "rule-1",
2017+
enabled: true,
2018+
conditions: {},
2019+
deleteObjectsTransition: {
2020+
condition: {
2021+
type: "Age",
2022+
maxAge: 2592000,
2023+
},
2024+
},
2025+
},
2026+
],
2027+
};
2028+
2029+
writeFileSync(filePath, JSON.stringify(lifecycleRules));
2030+
2031+
setIsTTY(true);
2032+
mockConfirm({
2033+
text: `Are you sure you want to overwrite all existing lifecycle rules for bucket '${bucketName}'?`,
2034+
result: true,
2035+
});
2036+
2037+
msw.use(
2038+
http.put(
2039+
"*/accounts/:accountId/r2/buckets/:bucketName/lifecycle",
2040+
async ({ request, params }) => {
2041+
const { accountId, bucketName: bucketParam } = params;
2042+
expect(accountId).toEqual("some-account-id");
2043+
expect(bucketName).toEqual(bucketParam);
2044+
const requestBody = await request.json();
2045+
expect(requestBody).toEqual({
2046+
...lifecycleRules,
2047+
});
2048+
return HttpResponse.json(createFetchResult({}));
2049+
},
2050+
{ once: true }
2051+
)
2052+
);
2053+
2054+
await runWrangler(
2055+
`r2 bucket lifecycle set ${bucketName} --file ${filePath}`
2056+
);
2057+
expect(std.out).toMatchInlineSnapshot(`
2058+
"Setting lifecycle configuration (1 rules) for bucket 'my-bucket'...
2059+
✨ Set lifecycle configuration for bucket 'my-bucket'."
2060+
`);
2061+
});
2062+
});
2063+
});
18272064
});
18282065

18292066
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)