@hot-updater/cloudflare 1.1.3 → 1.1.5
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 +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +7 -86
- 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/r2S3Storage.ts +14 -139
- 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/dist/index.cjs
CHANGED
|
@@ -24,9 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
25
25
|
let cloudflare = require("cloudflare");
|
|
26
26
|
cloudflare = __toESM(cloudflare, 1);
|
|
27
|
-
let
|
|
28
|
-
let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
|
|
29
|
-
let _aws_sdk_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
|
|
27
|
+
let _hot_updater_storage_s3 = require("@hot-updater/storage-s3");
|
|
30
28
|
//#region src/d1Executor.ts
|
|
31
29
|
var D1ExecutionError = class extends Error {
|
|
32
30
|
name = "D1ExecutionError";
|
|
@@ -132,94 +130,17 @@ const d1Database = (config) => {
|
|
|
132
130
|
};
|
|
133
131
|
//#endregion
|
|
134
132
|
//#region src/r2S3Storage.ts
|
|
135
|
-
/** Devices download right after the update check that returns the URL. */
|
|
136
|
-
const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
|
|
137
|
-
const isObjectNotFoundError = (error) => {
|
|
138
|
-
if (error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey")) return true;
|
|
139
|
-
return typeof error === "object" && error !== null && "$metadata" in error && error.$metadata?.httpStatusCode === 404;
|
|
140
|
-
};
|
|
141
133
|
const createR2S3Storage = (config) => {
|
|
142
|
-
const { accountId,
|
|
143
|
-
|
|
134
|
+
const { accountId, endpoint, forcePathStyle, region, ...s3Config } = config;
|
|
135
|
+
return (0, _hot_updater_storage_s3.createS3Storage)({
|
|
136
|
+
name: "r2Storage",
|
|
137
|
+
protocol: "r2"
|
|
138
|
+
}, {
|
|
144
139
|
...s3Config,
|
|
145
140
|
endpoint: endpoint ?? `https://${accountId}.r2.cloudflarestorage.com`,
|
|
146
141
|
forcePathStyle: forcePathStyle ?? true,
|
|
147
142
|
region: region ?? "auto"
|
|
148
143
|
});
|
|
149
|
-
const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(basePath);
|
|
150
|
-
const parseAndValidate = (storageUri) => {
|
|
151
|
-
const parsed = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "r2");
|
|
152
|
-
if (parsed.bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`);
|
|
153
|
-
return parsed;
|
|
154
|
-
};
|
|
155
|
-
return (0, _hot_updater_plugin_core.createStorageAdapter)({
|
|
156
|
-
name: "r2Storage",
|
|
157
|
-
protocol: "r2",
|
|
158
|
-
async put({ key, body, contentLength, contentType }) {
|
|
159
|
-
const storageKey = getStorageKey(key);
|
|
160
|
-
await new _aws_sdk_lib_storage.Upload({
|
|
161
|
-
client,
|
|
162
|
-
params: {
|
|
163
|
-
Body: body,
|
|
164
|
-
Bucket: bucketName,
|
|
165
|
-
CacheControl: "max-age=31536000",
|
|
166
|
-
ContentLength: contentLength,
|
|
167
|
-
ContentType: contentType,
|
|
168
|
-
Key: storageKey
|
|
169
|
-
}
|
|
170
|
-
}).done();
|
|
171
|
-
return { storageUri: (0, _hot_updater_plugin_core.createStorageUri)({
|
|
172
|
-
protocol: "r2",
|
|
173
|
-
bucket: bucketName,
|
|
174
|
-
key: storageKey
|
|
175
|
-
}) };
|
|
176
|
-
},
|
|
177
|
-
async get({ storageUri }) {
|
|
178
|
-
const { key } = parseAndValidate(storageUri);
|
|
179
|
-
try {
|
|
180
|
-
const response = await client.send(new _aws_sdk_client_s3.GetObjectCommand({
|
|
181
|
-
Bucket: bucketName,
|
|
182
|
-
Key: key
|
|
183
|
-
}));
|
|
184
|
-
if (!response.Body) return { response: null };
|
|
185
|
-
const headers = new Headers();
|
|
186
|
-
if (response.ContentType) headers.set("content-type", response.ContentType);
|
|
187
|
-
if (response.ContentLength !== void 0) headers.set("content-length", String(response.ContentLength));
|
|
188
|
-
return { response: new Response(response.Body.transformToWebStream(), { headers }) };
|
|
189
|
-
} catch (error) {
|
|
190
|
-
if (isObjectNotFoundError(error)) return { response: null };
|
|
191
|
-
throw error;
|
|
192
|
-
}
|
|
193
|
-
},
|
|
194
|
-
async getDownloadUrl({ storageUri }) {
|
|
195
|
-
const { key } = parseAndValidate(storageUri);
|
|
196
|
-
return { url: await (0, _aws_sdk_s3_request_presigner.getSignedUrl)(client, new _aws_sdk_client_s3.GetObjectCommand({
|
|
197
|
-
Bucket: bucketName,
|
|
198
|
-
Key: key
|
|
199
|
-
}), { expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS }) };
|
|
200
|
-
},
|
|
201
|
-
async exists({ storageUri }) {
|
|
202
|
-
const { key } = parseAndValidate(storageUri);
|
|
203
|
-
try {
|
|
204
|
-
await client.send(new _aws_sdk_client_s3.HeadObjectCommand({
|
|
205
|
-
Bucket: bucketName,
|
|
206
|
-
Key: key
|
|
207
|
-
}));
|
|
208
|
-
return { exists: true };
|
|
209
|
-
} catch (error) {
|
|
210
|
-
if (isObjectNotFoundError(error)) return { exists: false };
|
|
211
|
-
throw error;
|
|
212
|
-
}
|
|
213
|
-
},
|
|
214
|
-
async delete({ storageUri }) {
|
|
215
|
-
const { key } = parseAndValidate(storageUri);
|
|
216
|
-
await client.send(new _aws_sdk_client_s3.DeleteObjectCommand({
|
|
217
|
-
Bucket: bucketName,
|
|
218
|
-
Key: key
|
|
219
|
-
}));
|
|
220
|
-
return { deleted: true };
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
144
|
};
|
|
224
145
|
//#endregion
|
|
225
146
|
//#region src/r2Storage.ts
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StorageAdapterWith, ToolingDatabase } from "@hot-updater/plugin-core";
|
|
2
|
-
import { S3ClientConfig } from "@
|
|
2
|
+
import { S3ClientConfig, S3StorageOperations } from "@hot-updater/storage-s3";
|
|
3
3
|
//#region src/d1Executor.d.ts
|
|
4
4
|
declare class D1ExecutionError extends Error {
|
|
5
5
|
readonly name = "D1ExecutionError";
|
|
@@ -31,6 +31,6 @@ interface R2S3StorageConfig extends S3ClientConfig {
|
|
|
31
31
|
//#endregion
|
|
32
32
|
//#region src/r2Storage.d.ts
|
|
33
33
|
type R2StorageConfig = R2S3StorageConfig;
|
|
34
|
-
declare const r2Storage: (config: R2S3StorageConfig) => StorageAdapterWith<
|
|
34
|
+
declare const r2Storage: (config: R2S3StorageConfig) => StorageAdapterWith<S3StorageOperations>;
|
|
35
35
|
//#endregion
|
|
36
36
|
export { D1DatabaseConfig, D1ExecutionError, type R2S3StorageConfig, R2StorageConfig, d1Database, r2Storage };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StorageAdapterWith, ToolingDatabase } from "@hot-updater/plugin-core";
|
|
2
|
-
import { S3ClientConfig } from "@
|
|
2
|
+
import { S3ClientConfig, S3StorageOperations } from "@hot-updater/storage-s3";
|
|
3
3
|
//#region src/d1Executor.d.ts
|
|
4
4
|
declare class D1ExecutionError extends Error {
|
|
5
5
|
readonly name = "D1ExecutionError";
|
|
@@ -31,6 +31,6 @@ interface R2S3StorageConfig extends S3ClientConfig {
|
|
|
31
31
|
//#endregion
|
|
32
32
|
//#region src/r2Storage.d.ts
|
|
33
33
|
type R2StorageConfig = R2S3StorageConfig;
|
|
34
|
-
declare const r2Storage: (config: R2S3StorageConfig) => StorageAdapterWith<
|
|
34
|
+
declare const r2Storage: (config: R2S3StorageConfig) => StorageAdapterWith<S3StorageOperations>;
|
|
35
35
|
//#endregion
|
|
36
36
|
export { D1DatabaseConfig, D1ExecutionError, type R2S3StorageConfig, R2StorageConfig, d1Database, r2Storage };
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { WRITE_GUARD_TABLE, coreSettings, coreTarget, createEngineDatabase, createSqlAdapter,
|
|
1
|
+
import { WRITE_GUARD_TABLE, coreSettings, coreTarget, createEngineDatabase, createSqlAdapter, createTableStatements, generateEngineSql } from "@hot-updater/plugin-core";
|
|
2
2
|
import Cloudflare from "cloudflare";
|
|
3
|
-
import {
|
|
4
|
-
import { Upload } from "@aws-sdk/lib-storage";
|
|
5
|
-
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
3
|
+
import { createS3Storage } from "@hot-updater/storage-s3";
|
|
6
4
|
//#region src/d1Executor.ts
|
|
7
5
|
var D1ExecutionError = class extends Error {
|
|
8
6
|
name = "D1ExecutionError";
|
|
@@ -108,94 +106,17 @@ const d1Database = (config) => {
|
|
|
108
106
|
};
|
|
109
107
|
//#endregion
|
|
110
108
|
//#region src/r2S3Storage.ts
|
|
111
|
-
/** Devices download right after the update check that returns the URL. */
|
|
112
|
-
const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
|
|
113
|
-
const isObjectNotFoundError = (error) => {
|
|
114
|
-
if (error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey")) return true;
|
|
115
|
-
return typeof error === "object" && error !== null && "$metadata" in error && error.$metadata?.httpStatusCode === 404;
|
|
116
|
-
};
|
|
117
109
|
const createR2S3Storage = (config) => {
|
|
118
|
-
const { accountId,
|
|
119
|
-
|
|
110
|
+
const { accountId, endpoint, forcePathStyle, region, ...s3Config } = config;
|
|
111
|
+
return createS3Storage({
|
|
112
|
+
name: "r2Storage",
|
|
113
|
+
protocol: "r2"
|
|
114
|
+
}, {
|
|
120
115
|
...s3Config,
|
|
121
116
|
endpoint: endpoint ?? `https://${accountId}.r2.cloudflarestorage.com`,
|
|
122
117
|
forcePathStyle: forcePathStyle ?? true,
|
|
123
118
|
region: region ?? "auto"
|
|
124
119
|
});
|
|
125
|
-
const getStorageKey = createStorageKeyBuilder(basePath);
|
|
126
|
-
const parseAndValidate = (storageUri) => {
|
|
127
|
-
const parsed = parseStorageUri(storageUri, "r2");
|
|
128
|
-
if (parsed.bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`);
|
|
129
|
-
return parsed;
|
|
130
|
-
};
|
|
131
|
-
return createStorageAdapter({
|
|
132
|
-
name: "r2Storage",
|
|
133
|
-
protocol: "r2",
|
|
134
|
-
async put({ key, body, contentLength, contentType }) {
|
|
135
|
-
const storageKey = getStorageKey(key);
|
|
136
|
-
await new Upload({
|
|
137
|
-
client,
|
|
138
|
-
params: {
|
|
139
|
-
Body: body,
|
|
140
|
-
Bucket: bucketName,
|
|
141
|
-
CacheControl: "max-age=31536000",
|
|
142
|
-
ContentLength: contentLength,
|
|
143
|
-
ContentType: contentType,
|
|
144
|
-
Key: storageKey
|
|
145
|
-
}
|
|
146
|
-
}).done();
|
|
147
|
-
return { storageUri: createStorageUri({
|
|
148
|
-
protocol: "r2",
|
|
149
|
-
bucket: bucketName,
|
|
150
|
-
key: storageKey
|
|
151
|
-
}) };
|
|
152
|
-
},
|
|
153
|
-
async get({ storageUri }) {
|
|
154
|
-
const { key } = parseAndValidate(storageUri);
|
|
155
|
-
try {
|
|
156
|
-
const response = await client.send(new GetObjectCommand({
|
|
157
|
-
Bucket: bucketName,
|
|
158
|
-
Key: key
|
|
159
|
-
}));
|
|
160
|
-
if (!response.Body) return { response: null };
|
|
161
|
-
const headers = new Headers();
|
|
162
|
-
if (response.ContentType) headers.set("content-type", response.ContentType);
|
|
163
|
-
if (response.ContentLength !== void 0) headers.set("content-length", String(response.ContentLength));
|
|
164
|
-
return { response: new Response(response.Body.transformToWebStream(), { headers }) };
|
|
165
|
-
} catch (error) {
|
|
166
|
-
if (isObjectNotFoundError(error)) return { response: null };
|
|
167
|
-
throw error;
|
|
168
|
-
}
|
|
169
|
-
},
|
|
170
|
-
async getDownloadUrl({ storageUri }) {
|
|
171
|
-
const { key } = parseAndValidate(storageUri);
|
|
172
|
-
return { url: await getSignedUrl(client, new GetObjectCommand({
|
|
173
|
-
Bucket: bucketName,
|
|
174
|
-
Key: key
|
|
175
|
-
}), { expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS }) };
|
|
176
|
-
},
|
|
177
|
-
async exists({ storageUri }) {
|
|
178
|
-
const { key } = parseAndValidate(storageUri);
|
|
179
|
-
try {
|
|
180
|
-
await client.send(new HeadObjectCommand({
|
|
181
|
-
Bucket: bucketName,
|
|
182
|
-
Key: key
|
|
183
|
-
}));
|
|
184
|
-
return { exists: true };
|
|
185
|
-
} catch (error) {
|
|
186
|
-
if (isObjectNotFoundError(error)) return { exists: false };
|
|
187
|
-
throw error;
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
async delete({ storageUri }) {
|
|
191
|
-
const { key } = parseAndValidate(storageUri);
|
|
192
|
-
await client.send(new DeleteObjectCommand({
|
|
193
|
-
Bucket: bucketName,
|
|
194
|
-
Key: key
|
|
195
|
-
}));
|
|
196
|
-
return { deleted: true };
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
120
|
};
|
|
200
121
|
//#endregion
|
|
201
122
|
//#region src/r2Storage.ts
|
package/dist/worker/index.cjs
CHANGED
|
@@ -121,8 +121,11 @@ const presignR2GetUrl = async ({ accountId, bucketName, credentials, expiresInSe
|
|
|
121
121
|
//#region src/r2WorkerStorage.ts
|
|
122
122
|
/** Devices download right after the update check that returns the URL. */
|
|
123
123
|
const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
|
|
124
|
+
/** Keys the binding deletes in one call. */
|
|
125
|
+
const DELETE_BATCH_SIZE = 1e3;
|
|
124
126
|
function r2WorkerStorage(config) {
|
|
125
127
|
const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(config.basePath);
|
|
128
|
+
const basePrefix = getStorageKey() ? `${getStorageKey()}/` : "";
|
|
126
129
|
const parseAndValidate = (storageUri) => {
|
|
127
130
|
const parsed = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "r2");
|
|
128
131
|
if (parsed.bucket !== config.bucketName) throw new Error(`Bucket name mismatch: expected "${config.bucketName}", but found "${parsed.bucket}".`);
|
|
@@ -178,6 +181,42 @@ function r2WorkerStorage(config) {
|
|
|
178
181
|
const { key } = parseAndValidate(storageUri);
|
|
179
182
|
await config.bucket.delete(key);
|
|
180
183
|
return { deleted: true };
|
|
184
|
+
},
|
|
185
|
+
async listObjects(prefix) {
|
|
186
|
+
const folder = getStorageKey(prefix ?? "");
|
|
187
|
+
const objects = [];
|
|
188
|
+
let cursor;
|
|
189
|
+
do {
|
|
190
|
+
const listed = await config.bucket.list({
|
|
191
|
+
prefix: folder ? `${folder}/` : "",
|
|
192
|
+
...cursor === void 0 ? {} : { cursor }
|
|
193
|
+
});
|
|
194
|
+
for (const object of listed.objects) {
|
|
195
|
+
if (!object.key.startsWith(basePrefix)) continue;
|
|
196
|
+
let storageUri;
|
|
197
|
+
try {
|
|
198
|
+
storageUri = (0, _hot_updater_plugin_core.createStorageUri)({
|
|
199
|
+
protocol: "r2",
|
|
200
|
+
bucket: config.bucketName,
|
|
201
|
+
key: object.key
|
|
202
|
+
});
|
|
203
|
+
} catch {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
objects.push({
|
|
207
|
+
key: object.key.slice(basePrefix.length),
|
|
208
|
+
lastModifiedAt: object.uploaded,
|
|
209
|
+
size: object.size,
|
|
210
|
+
storageUri
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
cursor = listed.truncated ? listed.cursor : void 0;
|
|
214
|
+
} while (cursor !== void 0);
|
|
215
|
+
return objects;
|
|
216
|
+
},
|
|
217
|
+
async deleteObjects(relativeKeys) {
|
|
218
|
+
const keys = relativeKeys.map((key) => getStorageKey(key));
|
|
219
|
+
for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_SIZE) await config.bucket.delete(keys.slice(offset, offset + DELETE_BATCH_SIZE));
|
|
181
220
|
}
|
|
182
221
|
});
|
|
183
222
|
}
|
package/dist/worker/index.d.cts
CHANGED
|
@@ -48,6 +48,7 @@ type CloudflareWorkerStorageConfig = {
|
|
|
48
48
|
readonly accountId?: undefined;
|
|
49
49
|
readonly credentials?: undefined;
|
|
50
50
|
});
|
|
51
|
+
type R2WorkerOperations = "put" | "get" | "exists" | "delete" | "listObjects" | "deleteObjects";
|
|
51
52
|
/**
|
|
52
53
|
* Storage on the Worker's R2 binding. With R2's S3-compatible credentials,
|
|
53
54
|
* its download URLs are presigned and devices download from the bucket;
|
|
@@ -56,7 +57,7 @@ type CloudflareWorkerStorageConfig = {
|
|
|
56
57
|
*/
|
|
57
58
|
declare function r2WorkerStorage(config: CloudflareWorkerStorageConfig & {
|
|
58
59
|
readonly credentials: R2Credentials;
|
|
59
|
-
}): StorageAdapterWith<
|
|
60
|
-
declare function r2WorkerStorage(config: CloudflareWorkerStorageConfig): StorageAdapterWith<
|
|
60
|
+
}): StorageAdapterWith<R2WorkerOperations | "getDownloadUrl">;
|
|
61
|
+
declare function r2WorkerStorage(config: CloudflareWorkerStorageConfig): StorageAdapterWith<R2WorkerOperations>;
|
|
61
62
|
//#endregion
|
|
62
63
|
export { type CloudflareWorkerStorageConfig, type D1Like, d1Database, r2WorkerStorage as r2Storage };
|
package/dist/worker/index.d.mts
CHANGED
|
@@ -48,6 +48,7 @@ type CloudflareWorkerStorageConfig = {
|
|
|
48
48
|
readonly accountId?: undefined;
|
|
49
49
|
readonly credentials?: undefined;
|
|
50
50
|
});
|
|
51
|
+
type R2WorkerOperations = "put" | "get" | "exists" | "delete" | "listObjects" | "deleteObjects";
|
|
51
52
|
/**
|
|
52
53
|
* Storage on the Worker's R2 binding. With R2's S3-compatible credentials,
|
|
53
54
|
* its download URLs are presigned and devices download from the bucket;
|
|
@@ -56,7 +57,7 @@ type CloudflareWorkerStorageConfig = {
|
|
|
56
57
|
*/
|
|
57
58
|
declare function r2WorkerStorage(config: CloudflareWorkerStorageConfig & {
|
|
58
59
|
readonly credentials: R2Credentials;
|
|
59
|
-
}): StorageAdapterWith<
|
|
60
|
-
declare function r2WorkerStorage(config: CloudflareWorkerStorageConfig): StorageAdapterWith<
|
|
60
|
+
}): StorageAdapterWith<R2WorkerOperations | "getDownloadUrl">;
|
|
61
|
+
declare function r2WorkerStorage(config: CloudflareWorkerStorageConfig): StorageAdapterWith<R2WorkerOperations>;
|
|
61
62
|
//#endregion
|
|
62
63
|
export { type CloudflareWorkerStorageConfig, type D1Like, d1Database, r2WorkerStorage as r2Storage };
|
package/dist/worker/index.mjs
CHANGED
|
@@ -120,8 +120,11 @@ const presignR2GetUrl = async ({ accountId, bucketName, credentials, expiresInSe
|
|
|
120
120
|
//#region src/r2WorkerStorage.ts
|
|
121
121
|
/** Devices download right after the update check that returns the URL. */
|
|
122
122
|
const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
|
|
123
|
+
/** Keys the binding deletes in one call. */
|
|
124
|
+
const DELETE_BATCH_SIZE = 1e3;
|
|
123
125
|
function r2WorkerStorage(config) {
|
|
124
126
|
const getStorageKey = createStorageKeyBuilder(config.basePath);
|
|
127
|
+
const basePrefix = getStorageKey() ? `${getStorageKey()}/` : "";
|
|
125
128
|
const parseAndValidate = (storageUri) => {
|
|
126
129
|
const parsed = parseStorageUri(storageUri, "r2");
|
|
127
130
|
if (parsed.bucket !== config.bucketName) throw new Error(`Bucket name mismatch: expected "${config.bucketName}", but found "${parsed.bucket}".`);
|
|
@@ -177,6 +180,42 @@ function r2WorkerStorage(config) {
|
|
|
177
180
|
const { key } = parseAndValidate(storageUri);
|
|
178
181
|
await config.bucket.delete(key);
|
|
179
182
|
return { deleted: true };
|
|
183
|
+
},
|
|
184
|
+
async listObjects(prefix) {
|
|
185
|
+
const folder = getStorageKey(prefix ?? "");
|
|
186
|
+
const objects = [];
|
|
187
|
+
let cursor;
|
|
188
|
+
do {
|
|
189
|
+
const listed = await config.bucket.list({
|
|
190
|
+
prefix: folder ? `${folder}/` : "",
|
|
191
|
+
...cursor === void 0 ? {} : { cursor }
|
|
192
|
+
});
|
|
193
|
+
for (const object of listed.objects) {
|
|
194
|
+
if (!object.key.startsWith(basePrefix)) continue;
|
|
195
|
+
let storageUri;
|
|
196
|
+
try {
|
|
197
|
+
storageUri = createStorageUri({
|
|
198
|
+
protocol: "r2",
|
|
199
|
+
bucket: config.bucketName,
|
|
200
|
+
key: object.key
|
|
201
|
+
});
|
|
202
|
+
} catch {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
objects.push({
|
|
206
|
+
key: object.key.slice(basePrefix.length),
|
|
207
|
+
lastModifiedAt: object.uploaded,
|
|
208
|
+
size: object.size,
|
|
209
|
+
storageUri
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
cursor = listed.truncated ? listed.cursor : void 0;
|
|
213
|
+
} while (cursor !== void 0);
|
|
214
|
+
return objects;
|
|
215
|
+
},
|
|
216
|
+
async deleteObjects(relativeKeys) {
|
|
217
|
+
const keys = relativeKeys.map((key) => getStorageKey(key));
|
|
218
|
+
for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_SIZE) await config.bucket.delete(keys.slice(offset, offset + DELETE_BATCH_SIZE));
|
|
180
219
|
}
|
|
181
220
|
});
|
|
182
221
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/cloudflare",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.5",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
@@ -49,18 +49,19 @@
|
|
|
49
49
|
"package.json"
|
|
50
50
|
],
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@aws-sdk/client-s3": "3.1066.0",
|
|
53
|
-
"@aws-sdk/lib-storage": "3.1008.0",
|
|
54
|
-
"@aws-sdk/s3-request-presigner": "3.1066.0",
|
|
55
52
|
"cloudflare": "4.2.0",
|
|
56
53
|
"hono": "4.12.34",
|
|
57
54
|
"uuidv7": "^1.0.2",
|
|
58
|
-
"@hot-updater/cli-tools": "1.1.
|
|
59
|
-
"@hot-updater/
|
|
60
|
-
"@hot-updater/
|
|
61
|
-
"@hot-updater/
|
|
55
|
+
"@hot-updater/cli-tools": "1.1.5",
|
|
56
|
+
"@hot-updater/server": "1.1.5",
|
|
57
|
+
"@hot-updater/storage-s3": "1.1.5",
|
|
58
|
+
"@hot-updater/plugin-core": "1.1.5",
|
|
59
|
+
"@hot-updater/protocol": "1.1.5"
|
|
62
60
|
},
|
|
63
61
|
"devDependencies": {
|
|
62
|
+
"@aws-sdk/client-s3": "3.1066.0",
|
|
63
|
+
"@aws-sdk/lib-storage": "3.1066.0",
|
|
64
|
+
"@aws-sdk/s3-request-presigner": "3.1066.0",
|
|
64
65
|
"@cloudflare/vitest-pool-workers": "0.13.0",
|
|
65
66
|
"@cloudflare/workers-types": "^4.20260312.1",
|
|
66
67
|
"@types/node": "^20",
|
|
@@ -74,7 +75,7 @@
|
|
|
74
75
|
"vitest": "4.1.4",
|
|
75
76
|
"wrangler": "^4.69.0",
|
|
76
77
|
"xdg-app-paths": "^8.3.0",
|
|
77
|
-
"@hot-updater/test-utils": "1.1.
|
|
78
|
+
"@hot-updater/test-utils": "1.1.5"
|
|
78
79
|
},
|
|
79
80
|
"scripts": {
|
|
80
81
|
"build": "tsdown && pnpm build:worker",
|
package/src/r2S3Storage.ts
CHANGED
|
@@ -1,19 +1,9 @@
|
|
|
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
|
-
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
10
|
-
import {
|
|
11
|
-
createStorageKeyBuilder,
|
|
12
|
-
createStorageAdapter,
|
|
13
|
-
createStorageUri,
|
|
14
|
-
parseStorageUri,
|
|
15
|
-
type StorageAdapterWith,
|
|
16
|
-
} from "@hot-updater/plugin-core";
|
|
5
|
+
type S3StorageOperations,
|
|
6
|
+
} from "@hot-updater/storage-s3";
|
|
17
7
|
|
|
18
8
|
export interface R2S3StorageConfig extends S3ClientConfig {
|
|
19
9
|
accountId: string;
|
|
@@ -23,132 +13,17 @@ export interface R2S3StorageConfig extends S3ClientConfig {
|
|
|
23
13
|
basePath?: string;
|
|
24
14
|
}
|
|
25
15
|
|
|
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
16
|
export const createR2S3Storage = (
|
|
46
17
|
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
|
-
};
|
|
18
|
+
): StorageAdapterWith<S3StorageOperations> => {
|
|
19
|
+
const { accountId, endpoint, forcePathStyle, region, ...s3Config } = config;
|
|
20
|
+
return createS3Storage(
|
|
21
|
+
{ name: "r2Storage", protocol: "r2" },
|
|
22
|
+
{
|
|
23
|
+
...s3Config,
|
|
24
|
+
endpoint: endpoint ?? `https://${accountId}.r2.cloudflarestorage.com`,
|
|
25
|
+
forcePathStyle: forcePathStyle ?? true,
|
|
26
|
+
region: region ?? "auto",
|
|
133
27
|
},
|
|
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
|
-
});
|
|
28
|
+
);
|
|
154
29
|
};
|
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.",
|