@hot-updater/cloudflare 0.36.6 → 1.0.0-rc.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 +989 -11
- package/dist/iac/index.mjs +992 -15
- package/dist/index.cjs +919 -6989
- package/dist/index.d.cts +11 -47
- package/dist/index.d.mts +11 -47
- package/dist/index.mjs +917 -7006
- package/dist/worker/index.cjs +898 -336
- package/dist/worker/index.d.cts +18 -33
- package/dist/worker/index.d.mts +18 -33
- package/dist/worker/index.mjs +899 -331
- package/package.json +7 -10
- package/sql/bundles.sql +183 -35
- package/src/d1Database.spec.ts +143 -392
- package/src/d1Database.ts +55 -650
- package/src/d1Implementation.spec.ts +252 -0
- package/src/d1Implementation.ts +742 -0
- package/src/d1Query.spec.ts +46 -0
- package/src/d1Query.ts +89 -0
- package/src/d1Rows.spec.ts +111 -0
- package/src/d1Rows.ts +326 -0
- package/src/d1Sql.spec.ts +30 -0
- package/src/d1Sql.ts +175 -0
- package/src/r2S3Storage.ts +93 -137
- package/src/r2Storage.spec.ts +120 -218
- package/src/r2Storage.ts +13 -61
- package/src/r2WorkerStorage.spec.ts +161 -50
- package/src/r2WorkerStorage.ts +85 -118
- package/src/worker/d1Database.spec.ts +33 -0
- package/src/worker/d1Database.ts +52 -0
- package/src/worker/index.ts +3 -31
- package/worker/dist/README.md +1 -1
- package/worker/dist/index.js +5968 -4701
- package/worker/dist/index.js.map +4 -4
- package/worker/migrations/0001_hot-updater_1.0.0.sql +197 -0
- package/worker/migrations/0002_hot-updater_artifact_byte_sizes.sql +11 -0
- package/worker/src/databaseMigration.integration.spec.ts +114 -0
- package/worker/src/databasePlugins.integration.spec.ts +292 -0
- package/worker/src/index.integration.spec.ts +102 -294
- package/worker/src/index.ts +14 -66
- package/worker/src/storagePlugin.integration.spec.ts +59 -0
- package/worker/wrangler.json +5 -2
- package/src/cloudflareWorkerDatabase.spec.ts +0 -260
- package/src/cloudflareWorkerDatabase.ts +0 -651
- package/src/r2WranglerStorage.ts +0 -193
- package/worker/migrations/0001_hot-updater_init.sql +0 -16
- package/worker/migrations/0002_hot-updater_0.13.0.sql +0 -8
- package/worker/migrations/0003_hot-updater_0.18.0.sql +0 -38
- package/worker/migrations/0004_hot-updater_0.29.0.sql +0 -11
- package/worker/migrations/0005_hot-updater_0.31.0.sql +0 -24
package/src/r2Storage.spec.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Buffer } from "buffer";
|
|
2
|
-
import fs from "fs/promises";
|
|
3
2
|
|
|
4
3
|
import {
|
|
5
4
|
DeleteObjectCommand,
|
|
@@ -7,25 +6,32 @@ import {
|
|
|
7
6
|
HeadObjectCommand,
|
|
8
7
|
S3Client,
|
|
9
8
|
} from "@aws-sdk/client-s3";
|
|
10
|
-
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
11
|
-
import { ExecaError } from "execa";
|
|
12
9
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
13
10
|
|
|
14
11
|
import { r2Storage } from "./r2Storage";
|
|
15
12
|
|
|
16
|
-
const { wrangler } = vi.hoisted(() => ({
|
|
17
|
-
wrangler: vi.fn(),
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
13
|
let fakeStore: Record<string, Buffer> = {};
|
|
21
14
|
let deletedKeys: string[] = [];
|
|
15
|
+
let uploadedParams:
|
|
16
|
+
| {
|
|
17
|
+
Body: ReadableStream<Uint8Array>;
|
|
18
|
+
Bucket: string;
|
|
19
|
+
CacheControl?: string;
|
|
20
|
+
ContentLength?: number;
|
|
21
|
+
ContentType?: string;
|
|
22
|
+
Key: string;
|
|
23
|
+
}
|
|
24
|
+
| undefined;
|
|
25
|
+
|
|
26
|
+
const createBody = (value: string) => new Response(value).body!;
|
|
22
27
|
|
|
23
28
|
vi.mock("@aws-sdk/lib-storage", () => ({
|
|
24
29
|
Upload: class {
|
|
25
30
|
params: {
|
|
26
|
-
Body:
|
|
31
|
+
Body: ReadableStream<Uint8Array>;
|
|
27
32
|
Bucket: string;
|
|
28
33
|
CacheControl?: string;
|
|
34
|
+
ContentLength?: number;
|
|
29
35
|
ContentType?: string;
|
|
30
36
|
Key: string;
|
|
31
37
|
};
|
|
@@ -34,18 +40,22 @@ vi.mock("@aws-sdk/lib-storage", () => ({
|
|
|
34
40
|
params,
|
|
35
41
|
}: {
|
|
36
42
|
params: {
|
|
37
|
-
Body:
|
|
43
|
+
Body: ReadableStream<Uint8Array>;
|
|
38
44
|
Bucket: string;
|
|
39
45
|
CacheControl?: string;
|
|
46
|
+
ContentLength?: number;
|
|
40
47
|
ContentType?: string;
|
|
41
48
|
Key: string;
|
|
42
49
|
};
|
|
43
50
|
}) {
|
|
44
51
|
this.params = params;
|
|
52
|
+
uploadedParams = params;
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
async done() {
|
|
48
|
-
fakeStore[this.params.Key] =
|
|
56
|
+
fakeStore[this.params.Key] = Buffer.from(
|
|
57
|
+
await new Response(this.params.Body).arrayBuffer(),
|
|
58
|
+
);
|
|
49
59
|
return {
|
|
50
60
|
Bucket: this.params.Bucket,
|
|
51
61
|
Key: this.params.Key,
|
|
@@ -54,27 +64,11 @@ vi.mock("@aws-sdk/lib-storage", () => ({
|
|
|
54
64
|
},
|
|
55
65
|
}));
|
|
56
66
|
|
|
57
|
-
vi.mock("@aws-sdk/s3-request-presigner", () => ({
|
|
58
|
-
getSignedUrl: vi.fn(async () => "https://signed-r2.example.com/object"),
|
|
59
|
-
}));
|
|
60
|
-
|
|
61
|
-
vi.mock("./utils/createWrangler", () => ({
|
|
62
|
-
createWrangler: vi.fn(() => wrangler),
|
|
63
|
-
}));
|
|
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
|
-
|
|
73
67
|
describe("r2Storage", () => {
|
|
74
68
|
beforeEach(() => {
|
|
75
69
|
fakeStore = {};
|
|
76
70
|
deletedKeys = [];
|
|
77
|
-
|
|
71
|
+
uploadedParams = undefined;
|
|
78
72
|
});
|
|
79
73
|
|
|
80
74
|
afterEach(() => {
|
|
@@ -109,6 +103,7 @@ describe("r2Storage", () => {
|
|
|
109
103
|
Body: {
|
|
110
104
|
transformToByteArray: async () => new Uint8Array(object),
|
|
111
105
|
transformToString: async () => object.toString("utf8"),
|
|
106
|
+
transformToWebStream: () => new Response(object).body!,
|
|
112
107
|
},
|
|
113
108
|
};
|
|
114
109
|
}
|
|
@@ -133,21 +128,55 @@ describe("r2Storage", () => {
|
|
|
133
128
|
accessKeyId: "access-key-id",
|
|
134
129
|
secretAccessKey: "secret-access-key",
|
|
135
130
|
},
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
const filePath = "/tmp/hot-updater-r2-upload.txt";
|
|
139
|
-
await fs.writeFile(filePath, "hello r2");
|
|
131
|
+
});
|
|
140
132
|
|
|
141
133
|
await expect(
|
|
142
|
-
storage.
|
|
134
|
+
storage.put?.({
|
|
135
|
+
key: "releases/bundle-1/hot-updater-r2-upload.txt",
|
|
136
|
+
body: createBody("hello r2"),
|
|
137
|
+
contentLength: 8,
|
|
138
|
+
contentType: "text/plain",
|
|
139
|
+
}),
|
|
143
140
|
).resolves.toEqual({
|
|
144
141
|
storageUri:
|
|
145
142
|
"r2://test-bucket/releases/bundle-1/hot-updater-r2-upload.txt",
|
|
146
143
|
});
|
|
147
|
-
expect(
|
|
148
|
-
|
|
149
|
-
);
|
|
150
|
-
expect(
|
|
144
|
+
expect(
|
|
145
|
+
fakeStore["releases/bundle-1/hot-updater-r2-upload.txt"].toString(),
|
|
146
|
+
).toBe("hello r2");
|
|
147
|
+
expect(uploadedParams).toMatchObject({
|
|
148
|
+
ContentLength: 8,
|
|
149
|
+
ContentType: "text/plain",
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("round-trips spaces, Unicode, fragments, and percent signs in S3 keys", async () => {
|
|
154
|
+
mockS3Client();
|
|
155
|
+
const storage = r2Storage({
|
|
156
|
+
accountId: "account-id",
|
|
157
|
+
bucketName: "test-bucket",
|
|
158
|
+
credentials: {
|
|
159
|
+
accessKeyId: "access-key-id",
|
|
160
|
+
secretAccessKey: "secret-access-key",
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
const key = "릴리스 folder/logo@2x #100%/bundle.zip";
|
|
164
|
+
|
|
165
|
+
const uploaded = await storage.put?.({
|
|
166
|
+
key,
|
|
167
|
+
body: createBody("bundle"),
|
|
168
|
+
contentLength: 6,
|
|
169
|
+
contentType: "application/zip",
|
|
170
|
+
});
|
|
171
|
+
expect(uploaded?.storageUri).toContain("logo%402x");
|
|
172
|
+
expect(uploaded?.storageUri).not.toContain("#100%");
|
|
173
|
+
await expect(
|
|
174
|
+
storage.get?.({ storageUri: uploaded!.storageUri }),
|
|
175
|
+
).resolves.toEqual({ response: expect.any(Response) });
|
|
176
|
+
await storage.delete?.({ storageUri: uploaded!.storageUri });
|
|
177
|
+
await storage.delete?.({ storageUri: uploaded!.storageUri });
|
|
178
|
+
|
|
179
|
+
expect(deletedKeys).toEqual([key, key]);
|
|
151
180
|
});
|
|
152
181
|
|
|
153
182
|
it("downloads R2 objects through the S3 API when credentials are provided", async () => {
|
|
@@ -166,21 +195,16 @@ describe("r2Storage", () => {
|
|
|
166
195
|
accessKeyId: "access-key-id",
|
|
167
196
|
secretAccessKey: "secret-access-key",
|
|
168
197
|
},
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
const downloadPath = "/tmp/hot-updater-test-manifest.json";
|
|
172
|
-
await fs.rm(downloadPath, { force: true });
|
|
198
|
+
});
|
|
173
199
|
|
|
174
|
-
await storage.
|
|
175
|
-
"r2://test-bucket/releases/bundle-1/manifest.json",
|
|
176
|
-
|
|
177
|
-
);
|
|
200
|
+
const { response } = (await storage.get?.({
|
|
201
|
+
storageUri: "r2://test-bucket/releases/bundle-1/manifest.json",
|
|
202
|
+
})) ?? { response: null };
|
|
178
203
|
|
|
179
|
-
expect(JSON.parse(await
|
|
204
|
+
expect(JSON.parse((await response?.text()) ?? "")).toEqual({
|
|
180
205
|
assets: {},
|
|
181
206
|
bundleId: "bundle-1",
|
|
182
207
|
});
|
|
183
|
-
expect(wrangler).not.toHaveBeenCalled();
|
|
184
208
|
});
|
|
185
209
|
|
|
186
210
|
it("checks R2 object existence through the S3 API", async () => {
|
|
@@ -194,14 +218,14 @@ describe("r2Storage", () => {
|
|
|
194
218
|
accessKeyId: "access-key-id",
|
|
195
219
|
secretAccessKey: "secret-access-key",
|
|
196
220
|
},
|
|
197
|
-
})
|
|
221
|
+
});
|
|
198
222
|
|
|
199
223
|
await expect(
|
|
200
|
-
storage.
|
|
201
|
-
).resolves.
|
|
224
|
+
storage.exists?.({ storageUri: "r2://test-bucket/releases/logo.png" }),
|
|
225
|
+
).resolves.toEqual({ exists: true });
|
|
202
226
|
await expect(
|
|
203
|
-
storage.
|
|
204
|
-
).resolves.
|
|
227
|
+
storage.exists?.({ storageUri: "r2://test-bucket/releases/missing.png" }),
|
|
228
|
+
).resolves.toEqual({ exists: false });
|
|
205
229
|
});
|
|
206
230
|
|
|
207
231
|
it("deletes R2 objects through the S3 API", async () => {
|
|
@@ -215,13 +239,18 @@ describe("r2Storage", () => {
|
|
|
215
239
|
accessKeyId: "access-key-id",
|
|
216
240
|
secretAccessKey: "secret-access-key",
|
|
217
241
|
},
|
|
218
|
-
})
|
|
242
|
+
});
|
|
219
243
|
|
|
220
|
-
await
|
|
244
|
+
await expect(
|
|
245
|
+
storage.delete?.({
|
|
246
|
+
storageUri: "r2://test-bucket/releases/logo.png",
|
|
247
|
+
}),
|
|
248
|
+
).resolves.toEqual({
|
|
249
|
+
deleted: true,
|
|
250
|
+
});
|
|
221
251
|
|
|
222
252
|
expect(deletedKeys).toEqual(["releases/logo.png"]);
|
|
223
253
|
expect(fakeStore["releases/logo.png"]).toBeUndefined();
|
|
224
|
-
expect(wrangler).not.toHaveBeenCalled();
|
|
225
254
|
});
|
|
226
255
|
|
|
227
256
|
it("reads R2 text through the runtime S3 API when credentials are provided", async () => {
|
|
@@ -240,22 +269,29 @@ describe("r2Storage", () => {
|
|
|
240
269
|
accessKeyId: "access-key-id",
|
|
241
270
|
secretAccessKey: "secret-access-key",
|
|
242
271
|
},
|
|
243
|
-
})
|
|
272
|
+
});
|
|
244
273
|
|
|
274
|
+
const { response } = (await storage.get?.({
|
|
275
|
+
storageUri: "r2://test-bucket/releases/bundle-1/manifest.json",
|
|
276
|
+
})) ?? { response: null };
|
|
277
|
+
expect(await response?.text()).toBe('{"assets":{},"bundleId":"bundle-1"}');
|
|
245
278
|
await expect(
|
|
246
|
-
storage.
|
|
247
|
-
"r2://test-bucket/releases/
|
|
248
|
-
),
|
|
249
|
-
).resolves.
|
|
250
|
-
await expect(
|
|
251
|
-
storage.profiles.runtime.readText(
|
|
252
|
-
"r2://test-bucket/releases/missing.json",
|
|
253
|
-
),
|
|
254
|
-
).resolves.toBeNull();
|
|
279
|
+
storage.get?.({
|
|
280
|
+
storageUri: "r2://test-bucket/releases/missing.json",
|
|
281
|
+
}),
|
|
282
|
+
).resolves.toEqual({ response: null });
|
|
255
283
|
});
|
|
256
284
|
|
|
257
|
-
it("
|
|
258
|
-
|
|
285
|
+
it("rejects configs without S3 credentials", () => {
|
|
286
|
+
expect(() =>
|
|
287
|
+
r2Storage({
|
|
288
|
+
accountId: "account-id",
|
|
289
|
+
bucketName: "test-bucket",
|
|
290
|
+
} as never),
|
|
291
|
+
).toThrow("r2Storage requires S3-compatible credentials");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("does not add getDownloadUrl to a deploy-only configuration", () => {
|
|
259
295
|
const storage = r2Storage({
|
|
260
296
|
accountId: "account-id",
|
|
261
297
|
bucketName: "test-bucket",
|
|
@@ -263,23 +299,12 @@ describe("r2Storage", () => {
|
|
|
263
299
|
accessKeyId: "access-key-id",
|
|
264
300
|
secretAccessKey: "secret-access-key",
|
|
265
301
|
},
|
|
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
302
|
});
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
expect.any(GetObjectCommand),
|
|
278
|
-
{ expiresIn: 3600 },
|
|
279
|
-
);
|
|
303
|
+
|
|
304
|
+
expect(Reflect.has(storage, "getDownloadUrl")).toBe(false);
|
|
280
305
|
});
|
|
281
306
|
|
|
282
|
-
it("
|
|
307
|
+
it("adds a signed download URL operation when configured for a server", async () => {
|
|
283
308
|
mockS3Client();
|
|
284
309
|
const storage = r2Storage({
|
|
285
310
|
accountId: "account-id",
|
|
@@ -288,157 +313,34 @@ describe("r2Storage", () => {
|
|
|
288
313
|
accessKeyId: "access-key-id",
|
|
289
314
|
secretAccessKey: "secret-access-key",
|
|
290
315
|
},
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
await storage.profiles.runtime.getDownloadUrl(
|
|
294
|
-
"r2://test-bucket/releases/logo%402x.png",
|
|
295
|
-
);
|
|
296
|
-
|
|
297
|
-
const command = vi.mocked(getSignedUrl).mock.calls.at(-1)?.[1];
|
|
298
|
-
expect(command).toBeInstanceOf(GetObjectCommand);
|
|
299
|
-
expect((command as GetObjectCommand).input.Key).toBe(
|
|
300
|
-
"releases/logo@2x.png",
|
|
301
|
-
);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it("falls back to wrangler without S3 credentials", async () => {
|
|
305
|
-
wrangler.mockImplementation(async (...args: string[]) => {
|
|
306
|
-
const fileIndex = args.indexOf("--file");
|
|
307
|
-
const downloadPath = args[fileIndex + 1];
|
|
308
|
-
|
|
309
|
-
await fs.writeFile(
|
|
310
|
-
downloadPath,
|
|
311
|
-
JSON.stringify({
|
|
312
|
-
bundleId: "bundle-1",
|
|
313
|
-
assets: {},
|
|
314
|
-
}),
|
|
315
|
-
);
|
|
316
|
-
|
|
317
|
-
return {
|
|
318
|
-
exitCode: 0,
|
|
319
|
-
stderr: "",
|
|
320
|
-
};
|
|
316
|
+
downloadUrlSigningKey: "test-signing-key",
|
|
321
317
|
});
|
|
322
318
|
|
|
323
|
-
const storage = r2Storage({
|
|
324
|
-
accountId: "account-id",
|
|
325
|
-
bucketName: "test-bucket",
|
|
326
|
-
cloudflareApiToken: "api-token",
|
|
327
|
-
})();
|
|
328
|
-
|
|
329
|
-
const downloadPath = "/tmp/hot-updater-test-manifest.json";
|
|
330
|
-
await fs.rm(downloadPath, { force: true });
|
|
331
|
-
|
|
332
|
-
await storage.profiles.node.downloadFile(
|
|
333
|
-
"r2://test-bucket/releases/bundle-1/manifest.json",
|
|
334
|
-
downloadPath,
|
|
335
|
-
);
|
|
336
|
-
|
|
337
|
-
expect(JSON.parse(await fs.readFile(downloadPath, "utf8"))).toEqual({
|
|
338
|
-
bundleId: "bundle-1",
|
|
339
|
-
assets: {},
|
|
340
|
-
});
|
|
341
|
-
expect(wrangler).toHaveBeenCalledWith(
|
|
342
|
-
"r2",
|
|
343
|
-
"object",
|
|
344
|
-
"get",
|
|
345
|
-
"test-bucket/releases/bundle-1/manifest.json",
|
|
346
|
-
"--file",
|
|
347
|
-
downloadPath,
|
|
348
|
-
"--remote",
|
|
349
|
-
);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it("keeps the deprecated wrangler path node-only at runtime", async () => {
|
|
353
|
-
const storage = r2Storage({
|
|
354
|
-
accountId: "account-id",
|
|
355
|
-
bucketName: "test-bucket",
|
|
356
|
-
cloudflareApiToken: "api-token",
|
|
357
|
-
})();
|
|
358
|
-
|
|
359
319
|
await expect(
|
|
360
|
-
storage.
|
|
361
|
-
"r2://test-bucket/releases/bundle
|
|
362
|
-
),
|
|
363
|
-
).
|
|
320
|
+
storage.getDownloadUrl?.({
|
|
321
|
+
storageUri: "r2://test-bucket/releases/bundle.zip",
|
|
322
|
+
}),
|
|
323
|
+
).resolves.toEqual({
|
|
324
|
+
url: expect.stringMatching(/^\/storage\//),
|
|
325
|
+
});
|
|
364
326
|
});
|
|
365
327
|
|
|
366
328
|
it("rejects downloads from a different bucket", async () => {
|
|
367
329
|
const storage = r2Storage({
|
|
368
330
|
accountId: "account-id",
|
|
369
331
|
bucketName: "test-bucket",
|
|
370
|
-
|
|
371
|
-
|
|
332
|
+
credentials: {
|
|
333
|
+
accessKeyId: "access-key-id",
|
|
334
|
+
secretAccessKey: "secret-access-key",
|
|
335
|
+
},
|
|
336
|
+
});
|
|
372
337
|
|
|
373
338
|
await expect(
|
|
374
|
-
storage.
|
|
375
|
-
"r2://other-bucket/releases/bundle-1/manifest.json",
|
|
376
|
-
|
|
377
|
-
),
|
|
339
|
+
storage.get?.({
|
|
340
|
+
storageUri: "r2://other-bucket/releases/bundle-1/manifest.json",
|
|
341
|
+
}),
|
|
378
342
|
).rejects.toThrow(
|
|
379
343
|
'Bucket name mismatch: expected "test-bucket", but found "other-bucket".',
|
|
380
344
|
);
|
|
381
|
-
expect(wrangler).not.toHaveBeenCalled();
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
it("returns false when the R2 object does not exist", async () => {
|
|
385
|
-
wrangler.mockRejectedValueOnce(createExecaError("object not found"));
|
|
386
|
-
|
|
387
|
-
const storage = r2Storage({
|
|
388
|
-
accountId: "account-id",
|
|
389
|
-
bucketName: "test-bucket",
|
|
390
|
-
cloudflareApiToken: "api-token",
|
|
391
|
-
})();
|
|
392
|
-
|
|
393
|
-
await expect(
|
|
394
|
-
storage.profiles.node.exists("r2://test-bucket/releases/logo.png"),
|
|
395
|
-
).resolves.toBe(false);
|
|
396
|
-
expect(wrangler).toHaveBeenCalledWith(
|
|
397
|
-
"r2",
|
|
398
|
-
"object",
|
|
399
|
-
"get",
|
|
400
|
-
"test-bucket/releases/logo.png",
|
|
401
|
-
"--file",
|
|
402
|
-
expect.any(String),
|
|
403
|
-
"--remote",
|
|
404
|
-
);
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it("decodes percent-encoded keys before using Wrangler", async () => {
|
|
408
|
-
wrangler.mockResolvedValueOnce({ exitCode: 0, stderr: "" });
|
|
409
|
-
const storage = r2Storage({
|
|
410
|
-
accountId: "account-id",
|
|
411
|
-
bucketName: "test-bucket",
|
|
412
|
-
cloudflareApiToken: "api-token",
|
|
413
|
-
})();
|
|
414
|
-
|
|
415
|
-
await storage.profiles.node.exists(
|
|
416
|
-
"r2://test-bucket/releases/logo%402x.png",
|
|
417
|
-
);
|
|
418
|
-
|
|
419
|
-
expect(wrangler).toHaveBeenCalledWith(
|
|
420
|
-
"r2",
|
|
421
|
-
"object",
|
|
422
|
-
"get",
|
|
423
|
-
"test-bucket/releases/logo@2x.png",
|
|
424
|
-
"--file",
|
|
425
|
-
expect.any(String),
|
|
426
|
-
"--remote",
|
|
427
|
-
);
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
it("rethrows non-missing R2 existence errors", async () => {
|
|
431
|
-
const error = createExecaError("Authentication failed");
|
|
432
|
-
wrangler.mockRejectedValueOnce(error);
|
|
433
|
-
|
|
434
|
-
const storage = r2Storage({
|
|
435
|
-
accountId: "account-id",
|
|
436
|
-
bucketName: "test-bucket",
|
|
437
|
-
cloudflareApiToken: "api-token",
|
|
438
|
-
})();
|
|
439
|
-
|
|
440
|
-
await expect(
|
|
441
|
-
storage.profiles.node.exists("r2://test-bucket/releases/logo.png"),
|
|
442
|
-
).rejects.toBe(error);
|
|
443
345
|
});
|
|
444
346
|
});
|
package/src/r2Storage.ts
CHANGED
|
@@ -1,66 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createUniversalStoragePlugin,
|
|
3
|
-
type StoragePluginHooks,
|
|
4
|
-
type UniversalStoragePlugin,
|
|
5
|
-
} from "@hot-updater/plugin-core";
|
|
1
|
+
import type { StoragePluginWith } from "@hot-updater/plugin-core";
|
|
6
2
|
|
|
7
|
-
import {
|
|
8
|
-
createS3RuntimeStorageProfile,
|
|
9
|
-
createS3StorageProfile,
|
|
10
|
-
type R2S3StorageConfig,
|
|
11
|
-
} from "./r2S3Storage";
|
|
12
|
-
import {
|
|
13
|
-
createWranglerRuntimeStorageProfile,
|
|
14
|
-
createWranglerStorageProfile,
|
|
15
|
-
type R2WranglerStorageConfig,
|
|
16
|
-
} from "./r2WranglerStorage";
|
|
3
|
+
import { createR2S3Storage, type R2S3StorageConfig } from "./r2S3Storage";
|
|
17
4
|
|
|
18
|
-
export type R2StorageConfig = R2S3StorageConfig
|
|
5
|
+
export type R2StorageConfig = R2S3StorageConfig;
|
|
6
|
+
export type { R2S3StorageConfig };
|
|
19
7
|
|
|
20
|
-
export
|
|
8
|
+
export const r2Storage = (
|
|
9
|
+
config: R2S3StorageConfig,
|
|
10
|
+
): StoragePluginWith<"put" | "get" | "exists" | "delete"> => {
|
|
11
|
+
if (!config.credentials) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
"r2Storage requires S3-compatible credentials. The Wrangler fallback was removed.",
|
|
14
|
+
);
|
|
15
|
+
}
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
config: R2StorageConfig,
|
|
24
|
-
): config is R2S3StorageConfig => {
|
|
25
|
-
return Boolean(config.credentials);
|
|
17
|
+
return createR2S3Storage(config);
|
|
26
18
|
};
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Cloudflare R2 storage plugin for Hot Updater.
|
|
30
|
-
*/
|
|
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>({
|
|
49
|
-
name: "r2Storage",
|
|
50
|
-
supportedProtocol: "r2",
|
|
51
|
-
factory: (config) => {
|
|
52
|
-
if (hasS3Credentials(config)) {
|
|
53
|
-
return {
|
|
54
|
-
node: createS3StorageProfile(config),
|
|
55
|
-
runtime: createS3RuntimeStorageProfile(config),
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
node: createWranglerStorageProfile(config),
|
|
61
|
-
runtime: createWranglerRuntimeStorageProfile(),
|
|
62
|
-
};
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
export const r2Storage: R2Storage = createR2StoragePlugin;
|