@hot-updater/vercel 0.0.0-stage → 1.0.2
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/LICENSE +33 -0
- package/dist/function/index.mjs +53589 -0
- package/dist/index.cjs +208 -0
- package/dist/index.d.cts +26 -0
- package/dist/index.d.mts +26 -0
- package/dist/index.mjs +207 -0
- package/dist/init/index.cjs +7174 -0
- package/dist/init/index.d.cts +101 -0
- package/dist/init/index.d.mts +101 -0
- package/dist/init/index.mjs +7166 -0
- package/package.json +60 -3
- package/README.md +0 -3
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
3
|
+
let _vercel_blob = require("@vercel/blob");
|
|
4
|
+
//#region src/vercelBlobStorage.ts
|
|
5
|
+
const PROTOCOL = "vercel-blob";
|
|
6
|
+
const ONE_YEAR_IN_SECONDS = 31536e3;
|
|
7
|
+
/** Above this, `put` uploads in parts. */
|
|
8
|
+
const MULTIPART_THRESHOLD_BYTES = 104857600;
|
|
9
|
+
/** One delegation presigns every private download URL for a day. */
|
|
10
|
+
const DELEGATION_LIFETIME_MS = 864e5;
|
|
11
|
+
/** Vercel's limit on a delegation, and so on a presigned URL. */
|
|
12
|
+
const MAX_SIGNED_LIFETIME_SECONDS = 604800;
|
|
13
|
+
const DELETE_BATCH_SIZE = 100;
|
|
14
|
+
/**
|
|
15
|
+
* A character @vercel/blob drops from a pathname's URL, such as `#`, or
|
|
16
|
+
* reads as an escape, such as `%`, so `get` and presigned URLs miss the
|
|
17
|
+
* object; control characters; and the escape character `!`.
|
|
18
|
+
*/
|
|
19
|
+
const ESCAPED_CHARACTER = /[#?%\\!\p{Cc}]/gu;
|
|
20
|
+
const ESCAPE_SEQUENCE = /!([0-9A-F]{2})/gu;
|
|
21
|
+
const utf8 = new TextEncoder();
|
|
22
|
+
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
23
|
+
/**
|
|
24
|
+
* The pathname Blob keeps a key under: each escaped character as `!` and
|
|
25
|
+
* the two hex digits of each of its UTF-8 bytes, such as `#` as `!23`.
|
|
26
|
+
* Distinct keys keep distinct pathnames, and deploy's keys, such as
|
|
27
|
+
* `<bundleId>/manifest.json`, are stored as they are.
|
|
28
|
+
*/
|
|
29
|
+
const toPathname = (key) => key.replace(ESCAPED_CHARACTER, (character) => Array.from(utf8.encode(character), (byte) => `!${byte.toString(16).toUpperCase().padStart(2, "0")}`).join(""));
|
|
30
|
+
/** The key `toPathname` stored under `pathname`, or null for a pathname it never makes. */
|
|
31
|
+
const fromPathname = (pathname) => {
|
|
32
|
+
const bytes = [];
|
|
33
|
+
const flush = (parts) => {
|
|
34
|
+
if (bytes.length === 0) return true;
|
|
35
|
+
try {
|
|
36
|
+
parts.push(utf8Decoder.decode(new Uint8Array(bytes)));
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
bytes.length = 0;
|
|
41
|
+
return true;
|
|
42
|
+
};
|
|
43
|
+
const parts = [];
|
|
44
|
+
let index = 0;
|
|
45
|
+
for (const match of pathname.matchAll(ESCAPE_SEQUENCE)) {
|
|
46
|
+
if (match.index !== index) {
|
|
47
|
+
if (!flush(parts)) return null;
|
|
48
|
+
parts.push(pathname.slice(index, match.index));
|
|
49
|
+
}
|
|
50
|
+
bytes.push(Number.parseInt(match[1], 16));
|
|
51
|
+
index = match.index + match[0].length;
|
|
52
|
+
}
|
|
53
|
+
if (!flush(parts)) return null;
|
|
54
|
+
parts.push(pathname.slice(index));
|
|
55
|
+
const key = parts.join("");
|
|
56
|
+
return toPathname(key) === pathname ? key : null;
|
|
57
|
+
};
|
|
58
|
+
/** The store ID a read-write token belongs to, as Vercel's CLI reads it. */
|
|
59
|
+
const storeIdOf = (token) => {
|
|
60
|
+
const [prefix, kind, rw, id] = token.split("_");
|
|
61
|
+
if (prefix !== "vercel" || kind !== "blob" || rw !== "rw" || !id) throw new Error("vercelBlobStorage needs the store's read-write token (vercel_blob_rw_...).");
|
|
62
|
+
return `store_${id}`;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Bundles in a Vercel Blob store. A public store's objects download from
|
|
66
|
+
* their Blob URL; a private store's from a presigned URL that expires.
|
|
67
|
+
*/
|
|
68
|
+
const vercelBlobStorage = (config) => {
|
|
69
|
+
const { token, access } = config;
|
|
70
|
+
const storeId = storeIdOf(token);
|
|
71
|
+
const host = `${storeId.slice(6).toLowerCase()}.${access}.blob.vercel-storage.com`;
|
|
72
|
+
const normalizedBasePath = config.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
|
|
73
|
+
const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(normalizedBasePath);
|
|
74
|
+
const expiresIn = config.signedUrlExpiresIn ?? 3600;
|
|
75
|
+
if (!Number.isFinite(expiresIn) || expiresIn <= 0 || expiresIn > MAX_SIGNED_LIFETIME_SECONDS) throw new Error(`vercelBlobStorage signedUrlExpiresIn must be between 1 and ${MAX_SIGNED_LIFETIME_SECONDS} seconds (seven days).`);
|
|
76
|
+
const expiresInMs = expiresIn * 1e3;
|
|
77
|
+
let delegation;
|
|
78
|
+
/** A read delegation that outlives `validUntil`, shared by every URL it presigns. */
|
|
79
|
+
const delegationUntil = async (validUntil) => {
|
|
80
|
+
const current = await delegation?.catch(() => void 0);
|
|
81
|
+
if (current !== void 0 && current.validUntil >= validUntil) return current;
|
|
82
|
+
const issued = (0, _vercel_blob.issueSignedToken)({
|
|
83
|
+
token,
|
|
84
|
+
operations: ["get"],
|
|
85
|
+
validUntil: Date.now() + Math.min(MAX_SIGNED_LIFETIME_SECONDS * 1e3, Math.max(DELEGATION_LIFETIME_MS, expiresInMs * 2))
|
|
86
|
+
});
|
|
87
|
+
delegation = issued;
|
|
88
|
+
return issued;
|
|
89
|
+
};
|
|
90
|
+
const parseAndValidate = (storageUri) => {
|
|
91
|
+
const parsed = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, PROTOCOL);
|
|
92
|
+
if (parsed.bucket !== storeId) throw new Error(`Blob store mismatch: expected "${storeId}", but found "${parsed.bucket}".`);
|
|
93
|
+
return parsed;
|
|
94
|
+
};
|
|
95
|
+
const getListPrefix = (prefix = "") => {
|
|
96
|
+
const value = [normalizedBasePath, prefix.replace(/^\/+|\/+$/g, "")].filter(Boolean).join("/");
|
|
97
|
+
return value ? `${toPathname(value)}/` : "";
|
|
98
|
+
};
|
|
99
|
+
const getRelativeKey = (key) => {
|
|
100
|
+
if (!normalizedBasePath) return key;
|
|
101
|
+
const basePrefix = `${normalizedBasePath}/`;
|
|
102
|
+
return key.startsWith(basePrefix) ? key.slice(basePrefix.length) : key;
|
|
103
|
+
};
|
|
104
|
+
return (0, _hot_updater_plugin_core.createStorageAdapter)({
|
|
105
|
+
name: "vercelBlobStorage",
|
|
106
|
+
protocol: PROTOCOL,
|
|
107
|
+
async put({ key, body, contentLength, contentType }) {
|
|
108
|
+
const storageKey = getStorageKey(key);
|
|
109
|
+
const storageUri = (0, _hot_updater_plugin_core.createStorageUri)({
|
|
110
|
+
protocol: PROTOCOL,
|
|
111
|
+
bucket: storeId,
|
|
112
|
+
key: storageKey
|
|
113
|
+
});
|
|
114
|
+
await (0, _vercel_blob.put)(toPathname(storageKey), body, {
|
|
115
|
+
access,
|
|
116
|
+
token,
|
|
117
|
+
addRandomSuffix: false,
|
|
118
|
+
allowOverwrite: true,
|
|
119
|
+
cacheControlMaxAge: ONE_YEAR_IN_SECONDS,
|
|
120
|
+
contentType,
|
|
121
|
+
multipart: contentLength === void 0 || contentLength > MULTIPART_THRESHOLD_BYTES
|
|
122
|
+
});
|
|
123
|
+
return { storageUri };
|
|
124
|
+
},
|
|
125
|
+
async get({ storageUri }) {
|
|
126
|
+
const { key } = parseAndValidate(storageUri);
|
|
127
|
+
const result = await (0, _vercel_blob.get)(toPathname(key), {
|
|
128
|
+
access,
|
|
129
|
+
token,
|
|
130
|
+
useCache: false
|
|
131
|
+
});
|
|
132
|
+
if (result?.statusCode !== 200) return { response: null };
|
|
133
|
+
const contentLength = result.headers.get("content-length");
|
|
134
|
+
return { response: new Response(result.stream, { headers: {
|
|
135
|
+
"content-type": result.blob.contentType,
|
|
136
|
+
...contentLength && !result.headers.has("content-encoding") ? { "content-length": contentLength } : {}
|
|
137
|
+
} }) };
|
|
138
|
+
},
|
|
139
|
+
async getDownloadUrl({ storageUri }) {
|
|
140
|
+
const { key } = parseAndValidate(storageUri);
|
|
141
|
+
const pathname = toPathname(key);
|
|
142
|
+
if (access === "public") return { url: `https://${host}/${pathname.split("/").map(encodeURIComponent).join("/")}` };
|
|
143
|
+
const validUntil = Date.now() + expiresInMs;
|
|
144
|
+
const { presignedUrl } = await (0, _vercel_blob.presignUrl)(await delegationUntil(validUntil), {
|
|
145
|
+
access,
|
|
146
|
+
operation: "get",
|
|
147
|
+
pathname,
|
|
148
|
+
validUntil
|
|
149
|
+
});
|
|
150
|
+
return { url: presignedUrl };
|
|
151
|
+
},
|
|
152
|
+
async exists({ storageUri }) {
|
|
153
|
+
const { key } = parseAndValidate(storageUri);
|
|
154
|
+
try {
|
|
155
|
+
await (0, _vercel_blob.head)(toPathname(key), { token });
|
|
156
|
+
return { exists: true };
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error instanceof _vercel_blob.BlobNotFoundError) return { exists: false };
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
async delete({ storageUri }) {
|
|
163
|
+
const { key } = parseAndValidate(storageUri);
|
|
164
|
+
await (0, _vercel_blob.del)(toPathname(key), { token });
|
|
165
|
+
return { deleted: true };
|
|
166
|
+
},
|
|
167
|
+
async listObjects(prefix) {
|
|
168
|
+
const objects = [];
|
|
169
|
+
let cursor;
|
|
170
|
+
do {
|
|
171
|
+
const page = await (0, _vercel_blob.list)({
|
|
172
|
+
token,
|
|
173
|
+
prefix: getListPrefix(prefix),
|
|
174
|
+
limit: 1e3,
|
|
175
|
+
...cursor === void 0 ? {} : { cursor }
|
|
176
|
+
});
|
|
177
|
+
for (const blob of page.blobs) {
|
|
178
|
+
const key = fromPathname(blob.pathname);
|
|
179
|
+
if (key === null) continue;
|
|
180
|
+
let storageUri;
|
|
181
|
+
try {
|
|
182
|
+
storageUri = (0, _hot_updater_plugin_core.createStorageUri)({
|
|
183
|
+
protocol: PROTOCOL,
|
|
184
|
+
bucket: storeId,
|
|
185
|
+
key
|
|
186
|
+
});
|
|
187
|
+
} catch {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
objects.push({
|
|
191
|
+
key: getRelativeKey(key),
|
|
192
|
+
lastModifiedAt: blob.uploadedAt,
|
|
193
|
+
size: blob.size,
|
|
194
|
+
storageUri
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
cursor = page.hasMore ? page.cursor : void 0;
|
|
198
|
+
} while (cursor !== void 0);
|
|
199
|
+
return objects;
|
|
200
|
+
},
|
|
201
|
+
async deleteObjects(relativeKeys) {
|
|
202
|
+
const pathnames = relativeKeys.map((key) => toPathname(getStorageKey(key)));
|
|
203
|
+
for (let offset = 0; offset < pathnames.length; offset += DELETE_BATCH_SIZE) await (0, _vercel_blob.del)(pathnames.slice(offset, offset + DELETE_BATCH_SIZE), { token });
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
//#endregion
|
|
208
|
+
exports.vercelBlobStorage = vercelBlobStorage;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { StorageAdapterWith } from "@hot-updater/plugin-core";
|
|
2
|
+
//#region src/vercelBlobStorage.d.ts
|
|
3
|
+
interface VercelBlobStorageConfig {
|
|
4
|
+
/**
|
|
5
|
+
* The Blob store's read-write token, `vercel_blob_rw_<storeId>_<secret>`:
|
|
6
|
+
* the `BLOB_READ_WRITE_TOKEN` Vercel adds to a project the store connects to.
|
|
7
|
+
*/
|
|
8
|
+
token: string;
|
|
9
|
+
/** The store's access, which Vercel fixes when it creates the store. */
|
|
10
|
+
access: "public" | "private";
|
|
11
|
+
/** Base path where bundles will be stored in the store. */
|
|
12
|
+
basePath?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Lifetime of a private store's download URLs in seconds, at most seven
|
|
15
|
+
* days, Vercel's limit. @default 3600
|
|
16
|
+
*/
|
|
17
|
+
signedUrlExpiresIn?: number;
|
|
18
|
+
}
|
|
19
|
+
type VercelBlobStorageOperations = "put" | "get" | "getDownloadUrl" | "exists" | "delete" | "listObjects" | "deleteObjects";
|
|
20
|
+
/**
|
|
21
|
+
* Bundles in a Vercel Blob store. A public store's objects download from
|
|
22
|
+
* their Blob URL; a private store's from a presigned URL that expires.
|
|
23
|
+
*/
|
|
24
|
+
declare const vercelBlobStorage: (config: VercelBlobStorageConfig) => StorageAdapterWith<VercelBlobStorageOperations>;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { VercelBlobStorageConfig, vercelBlobStorage };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { StorageAdapterWith } from "@hot-updater/plugin-core";
|
|
2
|
+
//#region src/vercelBlobStorage.d.ts
|
|
3
|
+
interface VercelBlobStorageConfig {
|
|
4
|
+
/**
|
|
5
|
+
* The Blob store's read-write token, `vercel_blob_rw_<storeId>_<secret>`:
|
|
6
|
+
* the `BLOB_READ_WRITE_TOKEN` Vercel adds to a project the store connects to.
|
|
7
|
+
*/
|
|
8
|
+
token: string;
|
|
9
|
+
/** The store's access, which Vercel fixes when it creates the store. */
|
|
10
|
+
access: "public" | "private";
|
|
11
|
+
/** Base path where bundles will be stored in the store. */
|
|
12
|
+
basePath?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Lifetime of a private store's download URLs in seconds, at most seven
|
|
15
|
+
* days, Vercel's limit. @default 3600
|
|
16
|
+
*/
|
|
17
|
+
signedUrlExpiresIn?: number;
|
|
18
|
+
}
|
|
19
|
+
type VercelBlobStorageOperations = "put" | "get" | "getDownloadUrl" | "exists" | "delete" | "listObjects" | "deleteObjects";
|
|
20
|
+
/**
|
|
21
|
+
* Bundles in a Vercel Blob store. A public store's objects download from
|
|
22
|
+
* their Blob URL; a private store's from a presigned URL that expires.
|
|
23
|
+
*/
|
|
24
|
+
declare const vercelBlobStorage: (config: VercelBlobStorageConfig) => StorageAdapterWith<VercelBlobStorageOperations>;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { VercelBlobStorageConfig, vercelBlobStorage };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { createStorageAdapter, createStorageKeyBuilder, createStorageUri, parseStorageUri } from "@hot-updater/plugin-core";
|
|
2
|
+
import { BlobNotFoundError, del, get, head, issueSignedToken, list, presignUrl, put } from "@vercel/blob";
|
|
3
|
+
//#region src/vercelBlobStorage.ts
|
|
4
|
+
const PROTOCOL = "vercel-blob";
|
|
5
|
+
const ONE_YEAR_IN_SECONDS = 31536e3;
|
|
6
|
+
/** Above this, `put` uploads in parts. */
|
|
7
|
+
const MULTIPART_THRESHOLD_BYTES = 104857600;
|
|
8
|
+
/** One delegation presigns every private download URL for a day. */
|
|
9
|
+
const DELEGATION_LIFETIME_MS = 864e5;
|
|
10
|
+
/** Vercel's limit on a delegation, and so on a presigned URL. */
|
|
11
|
+
const MAX_SIGNED_LIFETIME_SECONDS = 604800;
|
|
12
|
+
const DELETE_BATCH_SIZE = 100;
|
|
13
|
+
/**
|
|
14
|
+
* A character @vercel/blob drops from a pathname's URL, such as `#`, or
|
|
15
|
+
* reads as an escape, such as `%`, so `get` and presigned URLs miss the
|
|
16
|
+
* object; control characters; and the escape character `!`.
|
|
17
|
+
*/
|
|
18
|
+
const ESCAPED_CHARACTER = /[#?%\\!\p{Cc}]/gu;
|
|
19
|
+
const ESCAPE_SEQUENCE = /!([0-9A-F]{2})/gu;
|
|
20
|
+
const utf8 = new TextEncoder();
|
|
21
|
+
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
22
|
+
/**
|
|
23
|
+
* The pathname Blob keeps a key under: each escaped character as `!` and
|
|
24
|
+
* the two hex digits of each of its UTF-8 bytes, such as `#` as `!23`.
|
|
25
|
+
* Distinct keys keep distinct pathnames, and deploy's keys, such as
|
|
26
|
+
* `<bundleId>/manifest.json`, are stored as they are.
|
|
27
|
+
*/
|
|
28
|
+
const toPathname = (key) => key.replace(ESCAPED_CHARACTER, (character) => Array.from(utf8.encode(character), (byte) => `!${byte.toString(16).toUpperCase().padStart(2, "0")}`).join(""));
|
|
29
|
+
/** The key `toPathname` stored under `pathname`, or null for a pathname it never makes. */
|
|
30
|
+
const fromPathname = (pathname) => {
|
|
31
|
+
const bytes = [];
|
|
32
|
+
const flush = (parts) => {
|
|
33
|
+
if (bytes.length === 0) return true;
|
|
34
|
+
try {
|
|
35
|
+
parts.push(utf8Decoder.decode(new Uint8Array(bytes)));
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
bytes.length = 0;
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
42
|
+
const parts = [];
|
|
43
|
+
let index = 0;
|
|
44
|
+
for (const match of pathname.matchAll(ESCAPE_SEQUENCE)) {
|
|
45
|
+
if (match.index !== index) {
|
|
46
|
+
if (!flush(parts)) return null;
|
|
47
|
+
parts.push(pathname.slice(index, match.index));
|
|
48
|
+
}
|
|
49
|
+
bytes.push(Number.parseInt(match[1], 16));
|
|
50
|
+
index = match.index + match[0].length;
|
|
51
|
+
}
|
|
52
|
+
if (!flush(parts)) return null;
|
|
53
|
+
parts.push(pathname.slice(index));
|
|
54
|
+
const key = parts.join("");
|
|
55
|
+
return toPathname(key) === pathname ? key : null;
|
|
56
|
+
};
|
|
57
|
+
/** The store ID a read-write token belongs to, as Vercel's CLI reads it. */
|
|
58
|
+
const storeIdOf = (token) => {
|
|
59
|
+
const [prefix, kind, rw, id] = token.split("_");
|
|
60
|
+
if (prefix !== "vercel" || kind !== "blob" || rw !== "rw" || !id) throw new Error("vercelBlobStorage needs the store's read-write token (vercel_blob_rw_...).");
|
|
61
|
+
return `store_${id}`;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Bundles in a Vercel Blob store. A public store's objects download from
|
|
65
|
+
* their Blob URL; a private store's from a presigned URL that expires.
|
|
66
|
+
*/
|
|
67
|
+
const vercelBlobStorage = (config) => {
|
|
68
|
+
const { token, access } = config;
|
|
69
|
+
const storeId = storeIdOf(token);
|
|
70
|
+
const host = `${storeId.slice(6).toLowerCase()}.${access}.blob.vercel-storage.com`;
|
|
71
|
+
const normalizedBasePath = config.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
|
|
72
|
+
const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
|
|
73
|
+
const expiresIn = config.signedUrlExpiresIn ?? 3600;
|
|
74
|
+
if (!Number.isFinite(expiresIn) || expiresIn <= 0 || expiresIn > MAX_SIGNED_LIFETIME_SECONDS) throw new Error(`vercelBlobStorage signedUrlExpiresIn must be between 1 and ${MAX_SIGNED_LIFETIME_SECONDS} seconds (seven days).`);
|
|
75
|
+
const expiresInMs = expiresIn * 1e3;
|
|
76
|
+
let delegation;
|
|
77
|
+
/** A read delegation that outlives `validUntil`, shared by every URL it presigns. */
|
|
78
|
+
const delegationUntil = async (validUntil) => {
|
|
79
|
+
const current = await delegation?.catch(() => void 0);
|
|
80
|
+
if (current !== void 0 && current.validUntil >= validUntil) return current;
|
|
81
|
+
const issued = issueSignedToken({
|
|
82
|
+
token,
|
|
83
|
+
operations: ["get"],
|
|
84
|
+
validUntil: Date.now() + Math.min(MAX_SIGNED_LIFETIME_SECONDS * 1e3, Math.max(DELEGATION_LIFETIME_MS, expiresInMs * 2))
|
|
85
|
+
});
|
|
86
|
+
delegation = issued;
|
|
87
|
+
return issued;
|
|
88
|
+
};
|
|
89
|
+
const parseAndValidate = (storageUri) => {
|
|
90
|
+
const parsed = parseStorageUri(storageUri, PROTOCOL);
|
|
91
|
+
if (parsed.bucket !== storeId) throw new Error(`Blob store mismatch: expected "${storeId}", but found "${parsed.bucket}".`);
|
|
92
|
+
return parsed;
|
|
93
|
+
};
|
|
94
|
+
const getListPrefix = (prefix = "") => {
|
|
95
|
+
const value = [normalizedBasePath, prefix.replace(/^\/+|\/+$/g, "")].filter(Boolean).join("/");
|
|
96
|
+
return value ? `${toPathname(value)}/` : "";
|
|
97
|
+
};
|
|
98
|
+
const getRelativeKey = (key) => {
|
|
99
|
+
if (!normalizedBasePath) return key;
|
|
100
|
+
const basePrefix = `${normalizedBasePath}/`;
|
|
101
|
+
return key.startsWith(basePrefix) ? key.slice(basePrefix.length) : key;
|
|
102
|
+
};
|
|
103
|
+
return createStorageAdapter({
|
|
104
|
+
name: "vercelBlobStorage",
|
|
105
|
+
protocol: PROTOCOL,
|
|
106
|
+
async put({ key, body, contentLength, contentType }) {
|
|
107
|
+
const storageKey = getStorageKey(key);
|
|
108
|
+
const storageUri = createStorageUri({
|
|
109
|
+
protocol: PROTOCOL,
|
|
110
|
+
bucket: storeId,
|
|
111
|
+
key: storageKey
|
|
112
|
+
});
|
|
113
|
+
await put(toPathname(storageKey), body, {
|
|
114
|
+
access,
|
|
115
|
+
token,
|
|
116
|
+
addRandomSuffix: false,
|
|
117
|
+
allowOverwrite: true,
|
|
118
|
+
cacheControlMaxAge: ONE_YEAR_IN_SECONDS,
|
|
119
|
+
contentType,
|
|
120
|
+
multipart: contentLength === void 0 || contentLength > MULTIPART_THRESHOLD_BYTES
|
|
121
|
+
});
|
|
122
|
+
return { storageUri };
|
|
123
|
+
},
|
|
124
|
+
async get({ storageUri }) {
|
|
125
|
+
const { key } = parseAndValidate(storageUri);
|
|
126
|
+
const result = await get(toPathname(key), {
|
|
127
|
+
access,
|
|
128
|
+
token,
|
|
129
|
+
useCache: false
|
|
130
|
+
});
|
|
131
|
+
if (result?.statusCode !== 200) return { response: null };
|
|
132
|
+
const contentLength = result.headers.get("content-length");
|
|
133
|
+
return { response: new Response(result.stream, { headers: {
|
|
134
|
+
"content-type": result.blob.contentType,
|
|
135
|
+
...contentLength && !result.headers.has("content-encoding") ? { "content-length": contentLength } : {}
|
|
136
|
+
} }) };
|
|
137
|
+
},
|
|
138
|
+
async getDownloadUrl({ storageUri }) {
|
|
139
|
+
const { key } = parseAndValidate(storageUri);
|
|
140
|
+
const pathname = toPathname(key);
|
|
141
|
+
if (access === "public") return { url: `https://${host}/${pathname.split("/").map(encodeURIComponent).join("/")}` };
|
|
142
|
+
const validUntil = Date.now() + expiresInMs;
|
|
143
|
+
const { presignedUrl } = await presignUrl(await delegationUntil(validUntil), {
|
|
144
|
+
access,
|
|
145
|
+
operation: "get",
|
|
146
|
+
pathname,
|
|
147
|
+
validUntil
|
|
148
|
+
});
|
|
149
|
+
return { url: presignedUrl };
|
|
150
|
+
},
|
|
151
|
+
async exists({ storageUri }) {
|
|
152
|
+
const { key } = parseAndValidate(storageUri);
|
|
153
|
+
try {
|
|
154
|
+
await head(toPathname(key), { token });
|
|
155
|
+
return { exists: true };
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error instanceof BlobNotFoundError) return { exists: false };
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
async delete({ storageUri }) {
|
|
162
|
+
const { key } = parseAndValidate(storageUri);
|
|
163
|
+
await del(toPathname(key), { token });
|
|
164
|
+
return { deleted: true };
|
|
165
|
+
},
|
|
166
|
+
async listObjects(prefix) {
|
|
167
|
+
const objects = [];
|
|
168
|
+
let cursor;
|
|
169
|
+
do {
|
|
170
|
+
const page = await list({
|
|
171
|
+
token,
|
|
172
|
+
prefix: getListPrefix(prefix),
|
|
173
|
+
limit: 1e3,
|
|
174
|
+
...cursor === void 0 ? {} : { cursor }
|
|
175
|
+
});
|
|
176
|
+
for (const blob of page.blobs) {
|
|
177
|
+
const key = fromPathname(blob.pathname);
|
|
178
|
+
if (key === null) continue;
|
|
179
|
+
let storageUri;
|
|
180
|
+
try {
|
|
181
|
+
storageUri = createStorageUri({
|
|
182
|
+
protocol: PROTOCOL,
|
|
183
|
+
bucket: storeId,
|
|
184
|
+
key
|
|
185
|
+
});
|
|
186
|
+
} catch {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
objects.push({
|
|
190
|
+
key: getRelativeKey(key),
|
|
191
|
+
lastModifiedAt: blob.uploadedAt,
|
|
192
|
+
size: blob.size,
|
|
193
|
+
storageUri
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
cursor = page.hasMore ? page.cursor : void 0;
|
|
197
|
+
} while (cursor !== void 0);
|
|
198
|
+
return objects;
|
|
199
|
+
},
|
|
200
|
+
async deleteObjects(relativeKeys) {
|
|
201
|
+
const pathnames = relativeKeys.map((key) => toPathname(getStorageKey(key)));
|
|
202
|
+
for (let offset = 0; offset < pathnames.length; offset += DELETE_BATCH_SIZE) await del(pathnames.slice(offset, offset + DELETE_BATCH_SIZE), { token });
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
//#endregion
|
|
207
|
+
export { vercelBlobStorage };
|