Skip to content

Commit

Permalink
Added wrangler r2 bucket info command, improved formatting of output …
Browse files Browse the repository at this point in the history
…for r2 bucket list command (#7212)
  • Loading branch information
jonesphillip authored Nov 13, 2024
1 parent c12c0fe commit 837f2f5
Show file tree
Hide file tree
Showing 6 changed files with 320 additions and 68 deletions.
5 changes: 5 additions & 0 deletions .changeset/tame-bobcats-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": minor
---

Added r2 bucket info command to Wrangler. Improved formatting of r2 bucket list output
121 changes: 83 additions & 38 deletions packages/wrangler/src/__tests__/r2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { runInTempDir } from "./helpers/run-in-tmp";
import { runWrangler } from "./helpers/run-wrangler";
import type {
PutNotificationRequestBody,
R2BucketInfo,
R2EventableOperation,
R2EventType,
} from "../r2/helpers";
Expand Down Expand Up @@ -92,14 +91,15 @@ describe("r2", () => {
Manage R2 buckets
COMMANDS
wrangler r2 bucket create <name> Create a new R2 bucket
wrangler r2 bucket update Update bucket state
wrangler r2 bucket list List R2 buckets
wrangler r2 bucket delete <name> Delete an R2 bucket
wrangler r2 bucket sippy Manage Sippy incremental migration on an R2 bucket
wrangler r2 bucket notification Manage event notification rules for an R2 bucket
wrangler r2 bucket domain Manage custom domains for an R2 bucket
wrangler r2 bucket dev-url Manage public access via the r2.dev URL for an R2 bucket
wrangler r2 bucket create <name> Create a new R2 bucket
wrangler r2 bucket update Update bucket state
wrangler r2 bucket list List R2 buckets
wrangler r2 bucket info <bucket> Get information about an R2 bucket
wrangler r2 bucket delete <bucket> Delete an R2 bucket
wrangler r2 bucket sippy Manage Sippy incremental migration on an R2 bucket
wrangler r2 bucket notification Manage event notification rules for an R2 bucket
wrangler r2 bucket domain Manage custom domains for an R2 bucket
wrangler r2 bucket dev-url Manage public access via the r2.dev URL for an R2 bucket
GLOBAL FLAGS
-j, --experimental-json-config Experimental: support wrangler.json [boolean]
Expand Down Expand Up @@ -128,14 +128,15 @@ describe("r2", () => {
Manage R2 buckets
COMMANDS
wrangler r2 bucket create <name> Create a new R2 bucket
wrangler r2 bucket update Update bucket state
wrangler r2 bucket list List R2 buckets
wrangler r2 bucket delete <name> Delete an R2 bucket
wrangler r2 bucket sippy Manage Sippy incremental migration on an R2 bucket
wrangler r2 bucket notification Manage event notification rules for an R2 bucket
wrangler r2 bucket domain Manage custom domains for an R2 bucket
wrangler r2 bucket dev-url Manage public access via the r2.dev URL for an R2 bucket
wrangler r2 bucket create <name> Create a new R2 bucket
wrangler r2 bucket update Update bucket state
wrangler r2 bucket list List R2 buckets
wrangler r2 bucket info <bucket> Get information about an R2 bucket
wrangler r2 bucket delete <bucket> Delete an R2 bucket
wrangler r2 bucket sippy Manage Sippy incremental migration on an R2 bucket
wrangler r2 bucket notification Manage event notification rules for an R2 bucket
wrangler r2 bucket domain Manage custom domains for an R2 bucket
wrangler r2 bucket dev-url Manage public access via the r2.dev URL for an R2 bucket
GLOBAL FLAGS
-j, --experimental-json-config Experimental: support wrangler.json [boolean]
Expand All @@ -148,9 +149,15 @@ describe("r2", () => {

describe("list", () => {
it("should list buckets & check request inputs", async () => {
const expectedBuckets: R2BucketInfo[] = [
{ name: "bucket-1-local-once", creation_date: "01-01-2001" },
{ name: "bucket-2-local-once", creation_date: "01-01-2001" },
const mockBuckets = [
{
name: "bucket-1-local-once",
creation_date: "01-01-2001",
},
{
name: "bucket-2-local-once",
creation_date: "01-01-2001",
},
];
msw.use(
http.get(
Expand All @@ -161,27 +168,65 @@ describe("r2", () => {
expect(await request.text()).toEqual("");
return HttpResponse.json(
createFetchResult({
buckets: [
{
name: "bucket-1-local-once",
creation_date: "01-01-2001",
},
{
name: "bucket-2-local-once",
creation_date: "01-01-2001",
},
],
buckets: mockBuckets,
})
);
},
{ once: true }
)
);
await runWrangler("r2 bucket list");

expect(std.err).toMatchInlineSnapshot(`""`);
const buckets = JSON.parse(std.out);
expect(buckets).toEqual(expectedBuckets);
await runWrangler(`r2 bucket list`);
expect(std.out).toMatchInlineSnapshot(`
"Listing buckets...
name: bucket-1-local-once
creation_date: 01-01-2001
name: bucket-2-local-once
creation_date: 01-01-2001"
`);
});
});

describe("info", () => {
it("should get information for the given bucket", async () => {
const bucketName = "my-bucket";
const bucketInfo = {
name: bucketName,
creation_date: "01-01-2001",
location: "WNAM",
storage_class: "Standard",
};

msw.use(
http.get(
"*/accounts/:accountId/r2/buckets/:bucketName",
async ({ params }) => {
const { accountId, bucketName: bucketParam } = params;
expect(accountId).toEqual("some-account-id");
expect(bucketParam).toEqual(bucketName);
return HttpResponse.json(
createFetchResult({
...bucketInfo,
})
);
},
{ once: true }
),
http.post("*/graphql", async () => {
return HttpResponse.json(createFetchResult({}));
})
);
await runWrangler(`r2 bucket info ${bucketName}`);
expect(std.out).toMatchInlineSnapshot(`
"Getting info for 'my-bucket'...
name: my-bucket
created: 01-01-2001
location: WNAM
default_storage_class: Standard
object_count: 0
bucket_size: 0 B"
`);
});
});

Expand Down Expand Up @@ -475,12 +520,12 @@ binding = \\"testBucket\\""
);
expect(std.out).toMatchInlineSnapshot(`
"
wrangler r2 bucket delete <name>
wrangler r2 bucket delete <bucket>
Delete an R2 bucket
POSITIONALS
name The name of the bucket to delete [string] [required]
bucket The name of the bucket to delete [string] [required]
GLOBAL FLAGS
-j, --experimental-json-config Experimental: support wrangler.json [boolean]
Expand Down Expand Up @@ -515,12 +560,12 @@ binding = \\"testBucket\\""
);
expect(std.out).toMatchInlineSnapshot(`
"
wrangler r2 bucket delete <name>
wrangler r2 bucket delete <bucket>
Delete an R2 bucket
POSITIONALS
name The name of the bucket to delete [string] [required]
bucket The name of the bucket to delete [string] [required]
GLOBAL FLAGS
-j, --experimental-json-config Experimental: support wrangler.json [boolean]
Expand Down
134 changes: 133 additions & 1 deletion packages/wrangler/src/r2/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Miniflare } from "miniflare";
import { fetchResult } from "../cfetch";
import prettyBytes from "pretty-bytes";
import { fetchGraphqlResult, fetchResult } from "../cfetch";
import { fetchR2Objects } from "../cfetch/internal";
import { getLocalPersistencePath } from "../dev/get-local-persistence-path";
import { buildPersistOptions } from "../dev/miniflare";
Expand All @@ -20,6 +21,29 @@ import type { HeadersInit } from "undici";
export interface R2BucketInfo {
name: string;
creation_date: string;
location?: string;
storage_class?: string;
}

export interface R2BucketMetrics {
max?: {
objectCount?: number;
payloadSize?: number;
metadataSize?: number;
};
dimensions: {
datetime?: string;
};
}

export interface R2BucketMetricsGraphQLResponse {
data: {
viewer: {
accounts: {
r2StorageAdaptiveGroups?: R2BucketMetrics[];
}[];
};
};
}

/**
Expand All @@ -39,6 +63,114 @@ export async function listR2Buckets(
return results.buckets;
}

export function tablefromR2BucketsListResponse(buckets: R2BucketInfo[]): {
name: string;
creation_date: string;
}[] {
const rows = [];
for (const bucket of buckets) {
rows.push({
name: bucket.name,
creation_date: bucket.creation_date,
});
}
return rows;
}

export async function getR2Bucket(
accountId: string,
bucketName: string,
jurisdiction?: string
): Promise<R2BucketInfo> {
const headers: HeadersInit = {};
if (jurisdiction !== undefined) {
headers["cf-r2-jurisdiction"] = jurisdiction;
}
const result = await fetchResult<R2BucketInfo>(
`/accounts/${accountId}/r2/buckets/${bucketName}`,
{
method: "GET",
headers,
}
);
return result;
}

export async function getR2BucketMetrics(
accountId: string,
bucketName: string,
jurisdiction?: string
): Promise<{ objectCount: number; totalSize: string }> {
const today = new Date();
const yesterday = new Date(new Date(today).setDate(today.getDate() - 1));

let fullBucketName = bucketName;
if (jurisdiction) {
fullBucketName = `${jurisdiction}_${bucketName}`;
}

const storageMetricsQuery = `
query getR2StorageMetrics($accountTag: String, $filter: R2StorageAdaptiveGroupsFilter_InputObject) {
viewer {
accounts(filter: { accountTag: $accountTag }) {
r2StorageAdaptiveGroups(
limit: 1
filter: $filter
orderBy: [datetime_DESC]
) {
max {
objectCount
payloadSize
metadataSize
}
dimensions {
datetime
}
}
}
}
}
`;

const variables = {
accountTag: accountId,
filter: {
datetime_geq: yesterday.toISOString(),
datetime_leq: today.toISOString(),
bucketName: fullBucketName,
},
};
const storageMetricsResult =
await fetchGraphqlResult<R2BucketMetricsGraphQLResponse>({
method: "POST",
body: JSON.stringify({
query: storageMetricsQuery,
operationName: "getR2StorageMetrics",
variables,
}),
headers: {
"Content-Type": "application/json",
},
});

if (storageMetricsResult) {
const metricsData =
storageMetricsResult.data?.viewer?.accounts[0]
?.r2StorageAdaptiveGroups?.[0];
if (metricsData && metricsData.max) {
const objectCount = metricsData.max.objectCount || 0;
const totalSize =
(metricsData.max.payloadSize || 0) +
(metricsData.max.metadataSize || 0);
return {
objectCount,
totalSize: prettyBytes(totalSize),
};
}
}
return { objectCount: 0, totalSize: "0 B" };
}

/**
* Create a bucket with the given `bucketName` within the account given by `accountId`.
*
Expand Down
Loading

0 comments on commit 837f2f5

Please sign in to comment.