@hot-updater/cloudflare 0.31.4 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/iac/index.cjs +206 -92
- package/dist/iac/index.mjs +198 -84
- package/dist/index.cjs +223 -63
- package/dist/index.d.cts +46 -6
- package/dist/index.d.mts +46 -6
- package/dist/index.mjs +218 -59
- package/dist/worker/index.cjs +9 -12
- package/dist/worker/index.d.cts +4 -3
- package/dist/worker/index.d.mts +3 -2
- package/dist/worker/index.mjs +9 -12
- package/package.json +10 -7
- package/src/cloudflareWorkerDatabase.spec.ts +260 -0
- package/src/cloudflareWorkerDatabase.ts +23 -19
- package/src/d1Database.spec.ts +16 -2
- package/src/d1Database.ts +23 -19
- package/src/r2S3Storage.ts +197 -0
- package/src/r2Storage.spec.ts +316 -2
- package/src/r2Storage.ts +50 -110
- package/src/r2WranglerStorage.ts +193 -0
- package/worker/dist/README.md +1 -1
- package/worker/dist/index.js +249 -58
- package/worker/dist/index.js.map +4 -4
- package/worker/src/index.ts +0 -1
- package/worker/src/getUpdateInfo.ts +0 -194
package/src/r2Storage.spec.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
import { Buffer } from "buffer";
|
|
1
2
|
import fs from "fs/promises";
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
DeleteObjectCommand,
|
|
6
|
+
GetObjectCommand,
|
|
7
|
+
HeadObjectCommand,
|
|
8
|
+
S3Client,
|
|
9
|
+
} from "@aws-sdk/client-s3";
|
|
10
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
11
|
+
import { ExecaError } from "execa";
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
13
|
|
|
5
14
|
import { r2Storage } from "./r2Storage";
|
|
6
15
|
|
|
@@ -8,16 +17,269 @@ const { wrangler } = vi.hoisted(() => ({
|
|
|
8
17
|
wrangler: vi.fn(),
|
|
9
18
|
}));
|
|
10
19
|
|
|
20
|
+
let fakeStore: Record<string, Buffer> = {};
|
|
21
|
+
let deletedKeys: string[] = [];
|
|
22
|
+
|
|
23
|
+
vi.mock("@aws-sdk/lib-storage", () => ({
|
|
24
|
+
Upload: class {
|
|
25
|
+
params: {
|
|
26
|
+
Body: Buffer;
|
|
27
|
+
Bucket: string;
|
|
28
|
+
CacheControl?: string;
|
|
29
|
+
ContentType?: string;
|
|
30
|
+
Key: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
constructor({
|
|
34
|
+
params,
|
|
35
|
+
}: {
|
|
36
|
+
params: {
|
|
37
|
+
Body: Buffer;
|
|
38
|
+
Bucket: string;
|
|
39
|
+
CacheControl?: string;
|
|
40
|
+
ContentType?: string;
|
|
41
|
+
Key: string;
|
|
42
|
+
};
|
|
43
|
+
}) {
|
|
44
|
+
this.params = params;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async done() {
|
|
48
|
+
fakeStore[this.params.Key] = this.params.Body;
|
|
49
|
+
return {
|
|
50
|
+
Bucket: this.params.Bucket,
|
|
51
|
+
Key: this.params.Key,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
vi.mock("@aws-sdk/s3-request-presigner", () => ({
|
|
58
|
+
getSignedUrl: vi.fn(async () => "https://signed-r2.example.com/object"),
|
|
59
|
+
}));
|
|
60
|
+
|
|
11
61
|
vi.mock("./utils/createWrangler", () => ({
|
|
12
62
|
createWrangler: vi.fn(() => wrangler),
|
|
13
63
|
}));
|
|
14
64
|
|
|
65
|
+
const createExecaError = (message: string) =>
|
|
66
|
+
Object.assign(Object.create(ExecaError.prototype), {
|
|
67
|
+
message,
|
|
68
|
+
shortMessage: message,
|
|
69
|
+
stderr: message,
|
|
70
|
+
stdout: "",
|
|
71
|
+
}) as ExecaError;
|
|
72
|
+
|
|
15
73
|
describe("r2Storage", () => {
|
|
16
74
|
beforeEach(() => {
|
|
75
|
+
fakeStore = {};
|
|
76
|
+
deletedKeys = [];
|
|
17
77
|
wrangler.mockReset();
|
|
18
78
|
});
|
|
19
79
|
|
|
20
|
-
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
vi.restoreAllMocks();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const mockS3Client = () => {
|
|
85
|
+
return vi
|
|
86
|
+
.spyOn(S3Client.prototype, "send")
|
|
87
|
+
.mockImplementation(async (command: any) => {
|
|
88
|
+
if (command instanceof HeadObjectCommand) {
|
|
89
|
+
const key = command.input.Key!;
|
|
90
|
+
if (fakeStore[key]) {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const error = new Error("Not found");
|
|
95
|
+
error.name = "NotFound";
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (command instanceof GetObjectCommand) {
|
|
100
|
+
const key = command.input.Key!;
|
|
101
|
+
const object = fakeStore[key];
|
|
102
|
+
if (!object) {
|
|
103
|
+
const error = new Error("No such key");
|
|
104
|
+
error.name = "NoSuchKey";
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
Body: {
|
|
110
|
+
transformToByteArray: async () => new Uint8Array(object),
|
|
111
|
+
transformToString: async () => object.toString("utf8"),
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (command instanceof DeleteObjectCommand) {
|
|
117
|
+
deletedKeys.push(command.input.Key!);
|
|
118
|
+
delete fakeStore[command.input.Key!];
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
throw new Error("Unsupported command");
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
it("uploads R2 objects through the S3 API when credentials are provided", async () => {
|
|
127
|
+
mockS3Client();
|
|
128
|
+
|
|
129
|
+
const storage = r2Storage({
|
|
130
|
+
accountId: "account-id",
|
|
131
|
+
bucketName: "test-bucket",
|
|
132
|
+
credentials: {
|
|
133
|
+
accessKeyId: "access-key-id",
|
|
134
|
+
secretAccessKey: "secret-access-key",
|
|
135
|
+
},
|
|
136
|
+
})();
|
|
137
|
+
|
|
138
|
+
const filePath = "/tmp/hot-updater-r2-upload.txt";
|
|
139
|
+
await fs.writeFile(filePath, "hello r2");
|
|
140
|
+
|
|
141
|
+
await expect(
|
|
142
|
+
storage.profiles.node.upload("releases/bundle-1", filePath),
|
|
143
|
+
).resolves.toEqual({
|
|
144
|
+
storageUri:
|
|
145
|
+
"r2://test-bucket/releases/bundle-1/hot-updater-r2-upload.txt",
|
|
146
|
+
});
|
|
147
|
+
expect(fakeStore["releases/bundle-1/hot-updater-r2-upload.txt"]).toEqual(
|
|
148
|
+
Buffer.from("hello r2"),
|
|
149
|
+
);
|
|
150
|
+
expect(wrangler).not.toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("downloads R2 objects through the S3 API when credentials are provided", async () => {
|
|
154
|
+
mockS3Client();
|
|
155
|
+
fakeStore["releases/bundle-1/manifest.json"] = Buffer.from(
|
|
156
|
+
JSON.stringify({
|
|
157
|
+
assets: {},
|
|
158
|
+
bundleId: "bundle-1",
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const storage = r2Storage({
|
|
163
|
+
accountId: "account-id",
|
|
164
|
+
bucketName: "test-bucket",
|
|
165
|
+
credentials: {
|
|
166
|
+
accessKeyId: "access-key-id",
|
|
167
|
+
secretAccessKey: "secret-access-key",
|
|
168
|
+
},
|
|
169
|
+
})();
|
|
170
|
+
|
|
171
|
+
const downloadPath = "/tmp/hot-updater-test-manifest.json";
|
|
172
|
+
await fs.rm(downloadPath, { force: true });
|
|
173
|
+
|
|
174
|
+
await storage.profiles.node.downloadFile(
|
|
175
|
+
"r2://test-bucket/releases/bundle-1/manifest.json",
|
|
176
|
+
downloadPath,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
expect(JSON.parse(await fs.readFile(downloadPath, "utf8"))).toEqual({
|
|
180
|
+
assets: {},
|
|
181
|
+
bundleId: "bundle-1",
|
|
182
|
+
});
|
|
183
|
+
expect(wrangler).not.toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("checks R2 object existence through the S3 API", async () => {
|
|
187
|
+
mockS3Client();
|
|
188
|
+
fakeStore["releases/logo.png"] = Buffer.from("logo");
|
|
189
|
+
|
|
190
|
+
const storage = r2Storage({
|
|
191
|
+
accountId: "account-id",
|
|
192
|
+
bucketName: "test-bucket",
|
|
193
|
+
credentials: {
|
|
194
|
+
accessKeyId: "access-key-id",
|
|
195
|
+
secretAccessKey: "secret-access-key",
|
|
196
|
+
},
|
|
197
|
+
})();
|
|
198
|
+
|
|
199
|
+
await expect(
|
|
200
|
+
storage.profiles.node.exists("r2://test-bucket/releases/logo.png"),
|
|
201
|
+
).resolves.toBe(true);
|
|
202
|
+
await expect(
|
|
203
|
+
storage.profiles.node.exists("r2://test-bucket/releases/missing.png"),
|
|
204
|
+
).resolves.toBe(false);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("deletes R2 objects through the S3 API", async () => {
|
|
208
|
+
mockS3Client();
|
|
209
|
+
fakeStore["releases/logo.png"] = Buffer.from("logo");
|
|
210
|
+
|
|
211
|
+
const storage = r2Storage({
|
|
212
|
+
accountId: "account-id",
|
|
213
|
+
bucketName: "test-bucket",
|
|
214
|
+
credentials: {
|
|
215
|
+
accessKeyId: "access-key-id",
|
|
216
|
+
secretAccessKey: "secret-access-key",
|
|
217
|
+
},
|
|
218
|
+
})();
|
|
219
|
+
|
|
220
|
+
await storage.profiles.node.delete("r2://test-bucket/releases/logo.png");
|
|
221
|
+
|
|
222
|
+
expect(deletedKeys).toEqual(["releases/logo.png"]);
|
|
223
|
+
expect(fakeStore["releases/logo.png"]).toBeUndefined();
|
|
224
|
+
expect(wrangler).not.toHaveBeenCalled();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("reads R2 text through the runtime S3 API when credentials are provided", async () => {
|
|
228
|
+
mockS3Client();
|
|
229
|
+
fakeStore["releases/bundle-1/manifest.json"] = Buffer.from(
|
|
230
|
+
JSON.stringify({
|
|
231
|
+
assets: {},
|
|
232
|
+
bundleId: "bundle-1",
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const storage = r2Storage({
|
|
237
|
+
accountId: "account-id",
|
|
238
|
+
bucketName: "test-bucket",
|
|
239
|
+
credentials: {
|
|
240
|
+
accessKeyId: "access-key-id",
|
|
241
|
+
secretAccessKey: "secret-access-key",
|
|
242
|
+
},
|
|
243
|
+
})();
|
|
244
|
+
|
|
245
|
+
await expect(
|
|
246
|
+
storage.profiles.runtime.readText(
|
|
247
|
+
"r2://test-bucket/releases/bundle-1/manifest.json",
|
|
248
|
+
),
|
|
249
|
+
).resolves.toBe('{"assets":{},"bundleId":"bundle-1"}');
|
|
250
|
+
await expect(
|
|
251
|
+
storage.profiles.runtime.readText(
|
|
252
|
+
"r2://test-bucket/releases/missing.json",
|
|
253
|
+
),
|
|
254
|
+
).resolves.toBeNull();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("creates signed R2 download URLs through the runtime S3 API", async () => {
|
|
258
|
+
mockS3Client();
|
|
259
|
+
const storage = r2Storage({
|
|
260
|
+
accountId: "account-id",
|
|
261
|
+
bucketName: "test-bucket",
|
|
262
|
+
credentials: {
|
|
263
|
+
accessKeyId: "access-key-id",
|
|
264
|
+
secretAccessKey: "secret-access-key",
|
|
265
|
+
},
|
|
266
|
+
})();
|
|
267
|
+
|
|
268
|
+
await expect(
|
|
269
|
+
storage.profiles.runtime.getDownloadUrl(
|
|
270
|
+
"r2://test-bucket/releases/bundle-1/index.bundle",
|
|
271
|
+
),
|
|
272
|
+
).resolves.toEqual({
|
|
273
|
+
fileUrl: "https://signed-r2.example.com/object",
|
|
274
|
+
});
|
|
275
|
+
expect(getSignedUrl).toHaveBeenCalledWith(
|
|
276
|
+
expect.any(S3Client),
|
|
277
|
+
expect.any(GetObjectCommand),
|
|
278
|
+
{ expiresIn: 3600 },
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("falls back to wrangler without S3 credentials", async () => {
|
|
21
283
|
wrangler.mockImplementation(async (...args: string[]) => {
|
|
22
284
|
const fileIndex = args.indexOf("--file");
|
|
23
285
|
const downloadPath = args[fileIndex + 1];
|
|
@@ -65,6 +327,20 @@ describe("r2Storage", () => {
|
|
|
65
327
|
);
|
|
66
328
|
});
|
|
67
329
|
|
|
330
|
+
it("keeps the deprecated wrangler path node-only at runtime", async () => {
|
|
331
|
+
const storage = r2Storage({
|
|
332
|
+
accountId: "account-id",
|
|
333
|
+
bucketName: "test-bucket",
|
|
334
|
+
cloudflareApiToken: "api-token",
|
|
335
|
+
})();
|
|
336
|
+
|
|
337
|
+
await expect(
|
|
338
|
+
storage.profiles.runtime.readText(
|
|
339
|
+
"r2://test-bucket/releases/bundle-1/manifest.json",
|
|
340
|
+
),
|
|
341
|
+
).rejects.toThrow("r2Storage runtime profile requires R2 S3 credentials.");
|
|
342
|
+
});
|
|
343
|
+
|
|
68
344
|
it("rejects downloads from a different bucket", async () => {
|
|
69
345
|
const storage = r2Storage({
|
|
70
346
|
accountId: "account-id",
|
|
@@ -82,4 +358,42 @@ describe("r2Storage", () => {
|
|
|
82
358
|
);
|
|
83
359
|
expect(wrangler).not.toHaveBeenCalled();
|
|
84
360
|
});
|
|
361
|
+
|
|
362
|
+
it("returns false when the R2 object does not exist", async () => {
|
|
363
|
+
wrangler.mockRejectedValueOnce(createExecaError("object not found"));
|
|
364
|
+
|
|
365
|
+
const storage = r2Storage({
|
|
366
|
+
accountId: "account-id",
|
|
367
|
+
bucketName: "test-bucket",
|
|
368
|
+
cloudflareApiToken: "api-token",
|
|
369
|
+
})();
|
|
370
|
+
|
|
371
|
+
await expect(
|
|
372
|
+
storage.profiles.node.exists("r2://test-bucket/releases/logo.png"),
|
|
373
|
+
).resolves.toBe(false);
|
|
374
|
+
expect(wrangler).toHaveBeenCalledWith(
|
|
375
|
+
"r2",
|
|
376
|
+
"object",
|
|
377
|
+
"get",
|
|
378
|
+
"test-bucket/releases/logo.png",
|
|
379
|
+
"--file",
|
|
380
|
+
expect.any(String),
|
|
381
|
+
"--remote",
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("rethrows non-missing R2 existence errors", async () => {
|
|
386
|
+
const error = createExecaError("Authentication failed");
|
|
387
|
+
wrangler.mockRejectedValueOnce(error);
|
|
388
|
+
|
|
389
|
+
const storage = r2Storage({
|
|
390
|
+
accountId: "account-id",
|
|
391
|
+
bucketName: "test-bucket",
|
|
392
|
+
cloudflareApiToken: "api-token",
|
|
393
|
+
})();
|
|
394
|
+
|
|
395
|
+
await expect(
|
|
396
|
+
storage.profiles.node.exists("r2://test-bucket/releases/logo.png"),
|
|
397
|
+
).rejects.toBe(error);
|
|
398
|
+
});
|
|
85
399
|
});
|
package/src/r2Storage.ts
CHANGED
|
@@ -1,126 +1,66 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
1
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
parseStorageUri,
|
|
2
|
+
createUniversalStoragePlugin,
|
|
3
|
+
type StoragePluginHooks,
|
|
4
|
+
type UniversalStoragePlugin,
|
|
9
5
|
} from "@hot-updater/plugin-core";
|
|
10
|
-
import { ExecaError } from "execa";
|
|
11
6
|
|
|
12
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
createS3RuntimeStorageProfile,
|
|
9
|
+
createS3StorageProfile,
|
|
10
|
+
type R2S3StorageConfig,
|
|
11
|
+
} from "./r2S3Storage";
|
|
12
|
+
import {
|
|
13
|
+
createWranglerRuntimeStorageProfile,
|
|
14
|
+
createWranglerStorageProfile,
|
|
15
|
+
type R2WranglerStorageConfig,
|
|
16
|
+
} from "./r2WranglerStorage";
|
|
13
17
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
18
|
+
export type R2StorageConfig = R2S3StorageConfig | R2WranglerStorageConfig;
|
|
19
|
+
|
|
20
|
+
export type { R2S3StorageConfig, R2WranglerStorageConfig };
|
|
21
|
+
|
|
22
|
+
const hasS3Credentials = (
|
|
23
|
+
config: R2StorageConfig,
|
|
24
|
+
): config is R2S3StorageConfig => {
|
|
25
|
+
return Boolean(config.credentials);
|
|
26
|
+
};
|
|
23
27
|
|
|
24
28
|
/**
|
|
25
29
|
* Cloudflare R2 storage plugin for Hot Updater.
|
|
26
30
|
*/
|
|
27
|
-
|
|
31
|
+
interface R2Storage {
|
|
32
|
+
(
|
|
33
|
+
config: R2S3StorageConfig,
|
|
34
|
+
hooks?: StoragePluginHooks,
|
|
35
|
+
): () => UniversalStoragePlugin;
|
|
36
|
+
/**
|
|
37
|
+
* @deprecated `cloudflareApiToken` uses the Wrangler CLI for R2 operations,
|
|
38
|
+
* which is slower than direct S3-compatible API access. Create R2
|
|
39
|
+
* S3-compatible credentials in the Cloudflare dashboard and pass them with
|
|
40
|
+
* `r2Storage({ credentials })` instead.
|
|
41
|
+
*/
|
|
42
|
+
(
|
|
43
|
+
config: R2WranglerStorageConfig,
|
|
44
|
+
hooks?: StoragePluginHooks,
|
|
45
|
+
): () => UniversalStoragePlugin;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const createR2StoragePlugin = createUniversalStoragePlugin<R2StorageConfig>({
|
|
28
49
|
name: "r2Storage",
|
|
29
50
|
supportedProtocol: "r2",
|
|
30
51
|
factory: (config) => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const getStorageKey = createStorageKeyBuilder(config.basePath);
|
|
52
|
+
if (hasS3Credentials(config)) {
|
|
53
|
+
return {
|
|
54
|
+
node: createS3StorageProfile(config),
|
|
55
|
+
runtime: createS3RuntimeStorageProfile(config),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
39
58
|
|
|
40
59
|
return {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (bucket !== bucketName) {
|
|
44
|
-
throw new Error(
|
|
45
|
-
`Bucket name mismatch: expected "${bucketName}", but found "${bucket}".`,
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
await wrangler(
|
|
51
|
-
"r2",
|
|
52
|
-
"object",
|
|
53
|
-
"delete",
|
|
54
|
-
[bucketName, key].join("/"),
|
|
55
|
-
"--remote",
|
|
56
|
-
);
|
|
57
|
-
} catch {
|
|
58
|
-
throw new Error("Can not delete bundle");
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
async upload(key, filePath) {
|
|
62
|
-
const contentType = getContentType(filePath);
|
|
63
|
-
|
|
64
|
-
const filename = path.basename(filePath);
|
|
65
|
-
|
|
66
|
-
const Key = getStorageKey(key, filename);
|
|
67
|
-
try {
|
|
68
|
-
const { stderr, exitCode } = await wrangler(
|
|
69
|
-
"r2",
|
|
70
|
-
"object",
|
|
71
|
-
"put",
|
|
72
|
-
[bucketName, Key].join("/"),
|
|
73
|
-
"--file",
|
|
74
|
-
filePath,
|
|
75
|
-
"--content-type",
|
|
76
|
-
contentType,
|
|
77
|
-
"--remote",
|
|
78
|
-
);
|
|
79
|
-
if (exitCode !== 0 && stderr) {
|
|
80
|
-
throw new Error(stderr);
|
|
81
|
-
}
|
|
82
|
-
} catch (error) {
|
|
83
|
-
if (error instanceof ExecaError) {
|
|
84
|
-
throw new Error(error.stderr || error.stdout);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
throw error;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
storageUri: `r2://${bucketName}/${Key}`,
|
|
92
|
-
};
|
|
93
|
-
},
|
|
94
|
-
async downloadFile(storageUri, filePath) {
|
|
95
|
-
const { bucket, key } = parseStorageUri(storageUri, "r2");
|
|
96
|
-
if (bucket !== bucketName) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
`Bucket name mismatch: expected "${bucketName}", but found "${bucket}".`,
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
104
|
-
const { stderr, exitCode } = await wrangler(
|
|
105
|
-
"r2",
|
|
106
|
-
"object",
|
|
107
|
-
"get",
|
|
108
|
-
[bucketName, key].join("/"),
|
|
109
|
-
"--file",
|
|
110
|
-
filePath,
|
|
111
|
-
"--remote",
|
|
112
|
-
);
|
|
113
|
-
if (exitCode !== 0 && stderr) {
|
|
114
|
-
throw new Error(stderr);
|
|
115
|
-
}
|
|
116
|
-
} catch (error) {
|
|
117
|
-
if (error instanceof ExecaError) {
|
|
118
|
-
throw new Error(error.stderr || error.stdout);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
throw error;
|
|
122
|
-
}
|
|
123
|
-
},
|
|
60
|
+
node: createWranglerStorageProfile(config),
|
|
61
|
+
runtime: createWranglerRuntimeStorageProfile(),
|
|
124
62
|
};
|
|
125
63
|
},
|
|
126
64
|
});
|
|
65
|
+
|
|
66
|
+
export const r2Storage: R2Storage = createR2StoragePlugin;
|