@hot-updater/cloudflare 1.2.0-rc.7 → 1.2.0-rc.9
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/index.cjs +6 -85
- package/dist/index.d.cts +9 -8
- package/dist/index.d.mts +9 -8
- package/dist/index.mjs +7 -86
- package/dist/init/index.cjs +16 -17
- package/dist/init/index.mjs +17 -18
- package/dist/worker/index.cjs +39 -0
- package/dist/worker/index.d.cts +3 -2
- package/dist/worker/index.d.mts +3 -2
- package/dist/worker/index.mjs +39 -0
- package/package.json +10 -9
- package/src/d1Rest.ts +4 -4
- package/src/r2S3Storage.ts +18 -141
- package/src/r2Storage.ts +2 -3
- package/src/r2WorkerStorage.ts +56 -3
- package/worker/dist/README.md +1 -1
- package/worker/dist/index.js +43 -1
- package/worker/dist/index.js.map +3 -3
package/src/d1Rest.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SqlResult, SqlStatement } from "@hot-updater/plugin-core";
|
|
1
|
+
import type { Lazy, SqlResult, SqlStatement } from "@hot-updater/plugin-core";
|
|
2
2
|
import Cloudflare from "cloudflare";
|
|
3
3
|
|
|
4
4
|
import { D1ExecutionError, type D1ResultLike, toSqlResult } from "./d1Executor";
|
|
@@ -8,10 +8,10 @@ export interface D1RestConfig {
|
|
|
8
8
|
readonly accountId: string;
|
|
9
9
|
/**
|
|
10
10
|
* An API token with D1 edit permission, or a function that returns it,
|
|
11
|
-
* such as
|
|
12
|
-
* so a config loads without the token.
|
|
11
|
+
* such as `secret("<name>")` in `hot-updater.config.ts`: called on the
|
|
12
|
+
* first query, so a config loads without the token.
|
|
13
13
|
*/
|
|
14
|
-
readonly cloudflareApiToken:
|
|
14
|
+
readonly cloudflareApiToken: Lazy<string>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/** The REST API binds text: each value goes as JSON and is read back with `json_extract`. */
|
package/src/r2S3Storage.ts
CHANGED
|
@@ -1,154 +1,31 @@
|
|
|
1
|
+
import type { StorageAdapterWith } from "@hot-updater/plugin-core";
|
|
1
2
|
import {
|
|
2
|
-
|
|
3
|
-
GetObjectCommand,
|
|
4
|
-
HeadObjectCommand,
|
|
5
|
-
S3Client,
|
|
3
|
+
createS3Storage,
|
|
6
4
|
type S3ClientConfig,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
createStorageKeyBuilder,
|
|
12
|
-
createStorageAdapter,
|
|
13
|
-
createStorageUri,
|
|
14
|
-
parseStorageUri,
|
|
15
|
-
type StorageAdapterWith,
|
|
16
|
-
} from "@hot-updater/plugin-core";
|
|
5
|
+
type S3CredentialsOption,
|
|
6
|
+
type S3StorageOperations,
|
|
7
|
+
} from "@hot-updater/storage-s3";
|
|
17
8
|
|
|
18
|
-
export interface R2S3StorageConfig extends S3ClientConfig {
|
|
9
|
+
export interface R2S3StorageConfig extends Omit<S3ClientConfig, "credentials"> {
|
|
19
10
|
accountId: string;
|
|
20
11
|
bucketName: string;
|
|
21
|
-
credentials
|
|
12
|
+
/** The SDK's credentials, or an R2 access key whose fields are lazy. */
|
|
13
|
+
credentials: NonNullable<S3CredentialsOption>;
|
|
22
14
|
/** Base path where bundles will be stored in the bucket. */
|
|
23
15
|
basePath?: string;
|
|
24
16
|
}
|
|
25
17
|
|
|
26
|
-
/** Devices download right after the update check that returns the URL. */
|
|
27
|
-
const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
|
|
28
|
-
|
|
29
|
-
const isObjectNotFoundError = (error: unknown) => {
|
|
30
|
-
if (
|
|
31
|
-
error instanceof Error &&
|
|
32
|
-
(error.name === "NotFound" || error.name === "NoSuchKey")
|
|
33
|
-
) {
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
return (
|
|
37
|
-
typeof error === "object" &&
|
|
38
|
-
error !== null &&
|
|
39
|
-
"$metadata" in error &&
|
|
40
|
-
(error as { $metadata?: { httpStatusCode?: number } }).$metadata
|
|
41
|
-
?.httpStatusCode === 404
|
|
42
|
-
);
|
|
43
|
-
};
|
|
44
|
-
|
|
45
18
|
export const createR2S3Storage = (
|
|
46
19
|
config: R2S3StorageConfig,
|
|
47
|
-
): StorageAdapterWith<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
region,
|
|
57
|
-
...s3Config
|
|
58
|
-
} = config;
|
|
59
|
-
const client = new S3Client({
|
|
60
|
-
...s3Config,
|
|
61
|
-
endpoint: endpoint ?? `https://${accountId}.r2.cloudflarestorage.com`,
|
|
62
|
-
forcePathStyle: forcePathStyle ?? true,
|
|
63
|
-
region: region ?? "auto",
|
|
64
|
-
});
|
|
65
|
-
const getStorageKey = createStorageKeyBuilder(basePath);
|
|
66
|
-
|
|
67
|
-
const parseAndValidate = (storageUri: string) => {
|
|
68
|
-
const parsed = parseStorageUri(storageUri, "r2");
|
|
69
|
-
if (parsed.bucket !== bucketName) {
|
|
70
|
-
throw new Error(
|
|
71
|
-
`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`,
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
return parsed;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
return createStorageAdapter({
|
|
78
|
-
name: "r2Storage",
|
|
79
|
-
protocol: "r2",
|
|
80
|
-
async put({ key, body, contentLength, contentType }) {
|
|
81
|
-
const storageKey = getStorageKey(key);
|
|
82
|
-
await new Upload({
|
|
83
|
-
client,
|
|
84
|
-
params: {
|
|
85
|
-
Body: body,
|
|
86
|
-
Bucket: bucketName,
|
|
87
|
-
CacheControl: "max-age=31536000",
|
|
88
|
-
ContentLength: contentLength,
|
|
89
|
-
ContentType: contentType,
|
|
90
|
-
Key: storageKey,
|
|
91
|
-
},
|
|
92
|
-
}).done();
|
|
93
|
-
return {
|
|
94
|
-
storageUri: createStorageUri({
|
|
95
|
-
protocol: "r2",
|
|
96
|
-
bucket: bucketName,
|
|
97
|
-
key: storageKey,
|
|
98
|
-
}),
|
|
99
|
-
};
|
|
100
|
-
},
|
|
101
|
-
async get({ storageUri }) {
|
|
102
|
-
const { key } = parseAndValidate(storageUri);
|
|
103
|
-
try {
|
|
104
|
-
const response = await client.send(
|
|
105
|
-
new GetObjectCommand({ Bucket: bucketName, Key: key }),
|
|
106
|
-
);
|
|
107
|
-
if (!response.Body) return { response: null };
|
|
108
|
-
const headers = new Headers();
|
|
109
|
-
if (response.ContentType)
|
|
110
|
-
headers.set("content-type", response.ContentType);
|
|
111
|
-
if (response.ContentLength !== undefined) {
|
|
112
|
-
headers.set("content-length", String(response.ContentLength));
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
response: new Response(response.Body.transformToWebStream(), {
|
|
116
|
-
headers,
|
|
117
|
-
}),
|
|
118
|
-
};
|
|
119
|
-
} catch (error) {
|
|
120
|
-
if (isObjectNotFoundError(error)) return { response: null };
|
|
121
|
-
throw error;
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
async getDownloadUrl({ storageUri }) {
|
|
125
|
-
const { key } = parseAndValidate(storageUri);
|
|
126
|
-
return {
|
|
127
|
-
url: await getSignedUrl(
|
|
128
|
-
client,
|
|
129
|
-
new GetObjectCommand({ Bucket: bucketName, Key: key }),
|
|
130
|
-
{ expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS },
|
|
131
|
-
),
|
|
132
|
-
};
|
|
20
|
+
): StorageAdapterWith<S3StorageOperations> => {
|
|
21
|
+
const { accountId, endpoint, forcePathStyle, region, ...s3Config } = config;
|
|
22
|
+
return createS3Storage(
|
|
23
|
+
{ name: "r2Storage", protocol: "r2" },
|
|
24
|
+
{
|
|
25
|
+
...s3Config,
|
|
26
|
+
endpoint: endpoint ?? `https://${accountId}.r2.cloudflarestorage.com`,
|
|
27
|
+
forcePathStyle: forcePathStyle ?? true,
|
|
28
|
+
region: region ?? "auto",
|
|
133
29
|
},
|
|
134
|
-
|
|
135
|
-
const { key } = parseAndValidate(storageUri);
|
|
136
|
-
try {
|
|
137
|
-
await client.send(
|
|
138
|
-
new HeadObjectCommand({ Bucket: bucketName, Key: key }),
|
|
139
|
-
);
|
|
140
|
-
return { exists: true };
|
|
141
|
-
} catch (error) {
|
|
142
|
-
if (isObjectNotFoundError(error)) return { exists: false };
|
|
143
|
-
throw error;
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
async delete({ storageUri }) {
|
|
147
|
-
const { key } = parseAndValidate(storageUri);
|
|
148
|
-
await client.send(
|
|
149
|
-
new DeleteObjectCommand({ Bucket: bucketName, Key: key }),
|
|
150
|
-
);
|
|
151
|
-
return { deleted: true };
|
|
152
|
-
},
|
|
153
|
-
});
|
|
30
|
+
);
|
|
154
31
|
};
|
package/src/r2Storage.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { StorageAdapterWith } from "@hot-updater/plugin-core";
|
|
2
|
+
import type { S3StorageOperations } from "@hot-updater/storage-s3";
|
|
2
3
|
|
|
3
4
|
import { createR2S3Storage, type R2S3StorageConfig } from "./r2S3Storage";
|
|
4
5
|
|
|
@@ -7,9 +8,7 @@ export type { R2S3StorageConfig };
|
|
|
7
8
|
|
|
8
9
|
export const r2Storage = (
|
|
9
10
|
config: R2S3StorageConfig,
|
|
10
|
-
): StorageAdapterWith<
|
|
11
|
-
"put" | "get" | "getDownloadUrl" | "exists" | "delete"
|
|
12
|
-
> => {
|
|
11
|
+
): StorageAdapterWith<S3StorageOperations> => {
|
|
13
12
|
if (!config.credentials) {
|
|
14
13
|
throw new Error(
|
|
15
14
|
"r2Storage requires S3-compatible credentials: pass credentials with an R2 access key ID and secret access key.",
|
package/src/r2WorkerStorage.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createStorageUri,
|
|
5
5
|
parseStorageUri,
|
|
6
6
|
type StorageAdapterWith,
|
|
7
|
+
type StorageObject,
|
|
7
8
|
} from "@hot-updater/plugin-core";
|
|
8
9
|
|
|
9
10
|
import { presignR2GetUrl, type R2Credentials } from "./presignR2Url";
|
|
@@ -28,6 +29,16 @@ export type CloudflareWorkerStorageConfig = {
|
|
|
28
29
|
|
|
29
30
|
/** Devices download right after the update check that returns the URL. */
|
|
30
31
|
const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
|
|
32
|
+
/** Keys the binding deletes in one call. */
|
|
33
|
+
const DELETE_BATCH_SIZE = 1000;
|
|
34
|
+
|
|
35
|
+
type R2WorkerOperations =
|
|
36
|
+
| "put"
|
|
37
|
+
| "get"
|
|
38
|
+
| "exists"
|
|
39
|
+
| "delete"
|
|
40
|
+
| "listObjects"
|
|
41
|
+
| "deleteObjects";
|
|
31
42
|
|
|
32
43
|
/**
|
|
33
44
|
* Storage on the Worker's R2 binding. With R2's S3-compatible credentials,
|
|
@@ -39,14 +50,15 @@ export function r2WorkerStorage(
|
|
|
39
50
|
config: CloudflareWorkerStorageConfig & {
|
|
40
51
|
readonly credentials: R2Credentials;
|
|
41
52
|
},
|
|
42
|
-
): StorageAdapterWith<
|
|
53
|
+
): StorageAdapterWith<R2WorkerOperations | "getDownloadUrl">;
|
|
43
54
|
export function r2WorkerStorage(
|
|
44
55
|
config: CloudflareWorkerStorageConfig,
|
|
45
|
-
): StorageAdapterWith<
|
|
56
|
+
): StorageAdapterWith<R2WorkerOperations>;
|
|
46
57
|
export function r2WorkerStorage(
|
|
47
58
|
config: CloudflareWorkerStorageConfig,
|
|
48
|
-
): StorageAdapterWith<
|
|
59
|
+
): StorageAdapterWith<R2WorkerOperations> {
|
|
49
60
|
const getStorageKey = createStorageKeyBuilder(config.basePath);
|
|
61
|
+
const basePrefix = getStorageKey() ? `${getStorageKey()}/` : "";
|
|
50
62
|
|
|
51
63
|
const parseAndValidate = (storageUri: string) => {
|
|
52
64
|
const parsed = parseStorageUri(storageUri, "r2");
|
|
@@ -128,5 +140,46 @@ export function r2WorkerStorage(
|
|
|
128
140
|
await config.bucket.delete(key);
|
|
129
141
|
return { deleted: true };
|
|
130
142
|
},
|
|
143
|
+
async listObjects(prefix) {
|
|
144
|
+
const folder = getStorageKey(prefix ?? "");
|
|
145
|
+
const objects: StorageObject[] = [];
|
|
146
|
+
let cursor: string | undefined;
|
|
147
|
+
do {
|
|
148
|
+
const listed = await config.bucket.list({
|
|
149
|
+
prefix: folder ? `${folder}/` : "",
|
|
150
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
151
|
+
});
|
|
152
|
+
for (const object of listed.objects) {
|
|
153
|
+
if (!object.key.startsWith(basePrefix)) continue;
|
|
154
|
+
// A key no `put` writes, such as a folder marker, has no URI.
|
|
155
|
+
let storageUri: string;
|
|
156
|
+
try {
|
|
157
|
+
storageUri = createStorageUri({
|
|
158
|
+
protocol: "r2",
|
|
159
|
+
bucket: config.bucketName,
|
|
160
|
+
key: object.key,
|
|
161
|
+
});
|
|
162
|
+
} catch {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
objects.push({
|
|
166
|
+
key: object.key.slice(basePrefix.length),
|
|
167
|
+
lastModifiedAt: object.uploaded,
|
|
168
|
+
size: object.size,
|
|
169
|
+
storageUri,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
cursor = listed.truncated ? listed.cursor : undefined;
|
|
173
|
+
} while (cursor !== undefined);
|
|
174
|
+
return objects;
|
|
175
|
+
},
|
|
176
|
+
async deleteObjects(relativeKeys) {
|
|
177
|
+
const keys = relativeKeys.map((key) => getStorageKey(key));
|
|
178
|
+
for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_SIZE) {
|
|
179
|
+
await config.bucket.delete(
|
|
180
|
+
keys.slice(offset, offset + DELETE_BATCH_SIZE),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
131
184
|
});
|
|
132
185
|
}
|
package/worker/dist/README.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
This folder contains the built output assets for the worker "hot-updater" generated at 2026-10-
|
|
1
|
+
This folder contains the built output assets for the worker "hot-updater" generated at 2026-10-29T15:00:44.535Z.
|
package/worker/dist/index.js
CHANGED
|
@@ -4632,7 +4632,7 @@ var createSqlAdapter = /* @__PURE__ */ __name(({ executor, tablePrefix = "", max
|
|
|
4632
4632
|
}, "createSqlAdapter");
|
|
4633
4633
|
|
|
4634
4634
|
// ../../packages/server/dist/package.mjs
|
|
4635
|
-
var version = "1.2.0-rc.
|
|
4635
|
+
var version = "1.2.0-rc.9";
|
|
4636
4636
|
|
|
4637
4637
|
// ../../packages/server/dist/version.mjs
|
|
4638
4638
|
var HOT_UPDATER_SERVER_VERSION = version;
|
|
@@ -13683,8 +13683,10 @@ var presignR2GetUrl = /* @__PURE__ */ __name(async ({
|
|
|
13683
13683
|
|
|
13684
13684
|
// src/r2WorkerStorage.ts
|
|
13685
13685
|
var PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
|
|
13686
|
+
var DELETE_BATCH_SIZE = 1e3;
|
|
13686
13687
|
function r2WorkerStorage(config) {
|
|
13687
13688
|
const getStorageKey = createStorageKeyBuilder(config.basePath);
|
|
13689
|
+
const basePrefix = getStorageKey() ? `${getStorageKey()}/` : "";
|
|
13688
13690
|
const parseAndValidate = /* @__PURE__ */ __name((storageUri) => {
|
|
13689
13691
|
const parsed = parseStorageUri(storageUri, "r2");
|
|
13690
13692
|
if (parsed.bucket !== config.bucketName) {
|
|
@@ -13761,6 +13763,46 @@ function r2WorkerStorage(config) {
|
|
|
13761
13763
|
const { key } = parseAndValidate(storageUri);
|
|
13762
13764
|
await config.bucket.delete(key);
|
|
13763
13765
|
return { deleted: true };
|
|
13766
|
+
},
|
|
13767
|
+
async listObjects(prefix) {
|
|
13768
|
+
const folder = getStorageKey(prefix ?? "");
|
|
13769
|
+
const objects = [];
|
|
13770
|
+
let cursor;
|
|
13771
|
+
do {
|
|
13772
|
+
const listed2 = await config.bucket.list({
|
|
13773
|
+
prefix: folder ? `${folder}/` : "",
|
|
13774
|
+
...cursor === void 0 ? {} : { cursor }
|
|
13775
|
+
});
|
|
13776
|
+
for (const object of listed2.objects) {
|
|
13777
|
+
if (!object.key.startsWith(basePrefix)) continue;
|
|
13778
|
+
let storageUri;
|
|
13779
|
+
try {
|
|
13780
|
+
storageUri = createStorageUri({
|
|
13781
|
+
protocol: "r2",
|
|
13782
|
+
bucket: config.bucketName,
|
|
13783
|
+
key: object.key
|
|
13784
|
+
});
|
|
13785
|
+
} catch {
|
|
13786
|
+
continue;
|
|
13787
|
+
}
|
|
13788
|
+
objects.push({
|
|
13789
|
+
key: object.key.slice(basePrefix.length),
|
|
13790
|
+
lastModifiedAt: object.uploaded,
|
|
13791
|
+
size: object.size,
|
|
13792
|
+
storageUri
|
|
13793
|
+
});
|
|
13794
|
+
}
|
|
13795
|
+
cursor = listed2.truncated ? listed2.cursor : void 0;
|
|
13796
|
+
} while (cursor !== void 0);
|
|
13797
|
+
return objects;
|
|
13798
|
+
},
|
|
13799
|
+
async deleteObjects(relativeKeys) {
|
|
13800
|
+
const keys = relativeKeys.map((key) => getStorageKey(key));
|
|
13801
|
+
for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_SIZE) {
|
|
13802
|
+
await config.bucket.delete(
|
|
13803
|
+
keys.slice(offset, offset + DELETE_BATCH_SIZE)
|
|
13804
|
+
);
|
|
13805
|
+
}
|
|
13764
13806
|
}
|
|
13765
13807
|
});
|
|
13766
13808
|
}
|