@velajs/storage 0.3.0 → 0.3.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/CHANGELOG.md +6 -1
- package/dist/body-BMUcBeQ0.js +93 -0
- package/dist/body-BMUcBeQ0.js.map +1 -0
- package/dist/client/index.d.ts +49 -46
- package/dist/client/index.js +207 -228
- package/dist/client/index.js.map +1 -0
- package/dist/drivers/memory/index.d.ts +17 -13
- package/dist/drivers/memory/index.js +190 -197
- package/dist/drivers/memory/index.js.map +1 -0
- package/dist/drivers/r2/index.d.ts +7 -4
- package/dist/drivers/r2/index.js +162 -160
- package/dist/drivers/r2/index.js.map +1 -0
- package/dist/drivers/r2-http/index.d.ts +23 -19
- package/dist/drivers/r2-http/index.js +52 -45
- package/dist/drivers/r2-http/index.js.map +1 -0
- package/dist/drivers/s3/index.d.ts +79 -5
- package/dist/drivers/s3/index.js +2 -3
- package/dist/index.d.ts +271 -17
- package/dist/index.js +1078 -16
- package/dist/index.js.map +1 -0
- package/dist/middleware/index.d.ts +177 -15
- package/dist/middleware/index.js +553 -8
- package/dist/middleware/index.js.map +1 -0
- package/dist/protocol.types-C5PPBD12.d.ts +80 -0
- package/dist/r2.types-Bw4Ft83t.d.ts +71 -0
- package/dist/retry-DXtO6R9D.js +119 -0
- package/dist/retry-DXtO6R9D.js.map +1 -0
- package/dist/s3-DrE9NrnD.js +557 -0
- package/dist/s3-DrE9NrnD.js.map +1 -0
- package/dist/storage.error-DVCOkJhc.js +49 -0
- package/dist/storage.error-DVCOkJhc.js.map +1 -0
- package/dist/storage.types-BJN7Yp1J.d.ts +288 -0
- package/dist/storagesdk/index.d.ts +73 -69
- package/dist/storagesdk/index.js +117 -130
- package/dist/storagesdk/index.js.map +1 -0
- package/dist/stored-file-2Yl07eRc.js +105 -0
- package/dist/stored-file-2Yl07eRc.js.map +1 -0
- package/dist/testing/index.d.ts +12 -8
- package/dist/testing/index.js +36 -53
- package/dist/testing/index.js.map +1 -0
- package/package.json +62 -48
- package/dist/base64.d.ts +0 -8
- package/dist/base64.js +0 -25
- package/dist/decorators/inject-storage.decorator.d.ts +0 -17
- package/dist/decorators/inject-storage.decorator.js +0 -21
- package/dist/drivers/r2/r2.types.d.ts +0 -67
- package/dist/drivers/r2/r2.types.js +0 -5
- package/dist/drivers/s3/s3-client.d.ts +0 -33
- package/dist/drivers/s3/s3-client.js +0 -154
- package/dist/drivers/s3/s3.driver.d.ts +0 -4
- package/dist/drivers/s3/s3.driver.js +0 -404
- package/dist/drivers/s3/s3.types.d.ts +0 -28
- package/dist/drivers/s3/s3.types.js +0 -1
- package/dist/drivers/s3/xml.d.ts +0 -6
- package/dist/drivers/s3/xml.js +0 -52
- package/dist/http/authorizer.types.d.ts +0 -59
- package/dist/http/authorizer.types.js +0 -1
- package/dist/http/http-helpers.d.ts +0 -22
- package/dist/http/http-helpers.js +0 -80
- package/dist/http/protocol.types.d.ts +0 -76
- package/dist/http/protocol.types.js +0 -1
- package/dist/internal/body.d.ts +0 -18
- package/dist/internal/body.js +0 -90
- package/dist/internal/retry.d.ts +0 -41
- package/dist/internal/retry.js +0 -127
- package/dist/internal/stored-file.d.ts +0 -27
- package/dist/internal/stored-file.js +0 -114
- package/dist/middleware/cache.d.ts +0 -50
- package/dist/middleware/cache.js +0 -101
- package/dist/middleware/compose.d.ts +0 -12
- package/dist/middleware/compose.js +0 -11
- package/dist/middleware/compression.d.ts +0 -16
- package/dist/middleware/compression.js +0 -101
- package/dist/middleware/encryption.d.ts +0 -14
- package/dist/middleware/encryption.js +0 -134
- package/dist/middleware/failover.d.ts +0 -18
- package/dist/middleware/failover.js +0 -52
- package/dist/middleware/retry.d.ts +0 -14
- package/dist/middleware/retry.js +0 -47
- package/dist/middleware/versioning.d.ts +0 -36
- package/dist/middleware/versioning.js +0 -89
- package/dist/middleware/wrap.d.ts +0 -11
- package/dist/middleware/wrap.js +0 -46
- package/dist/object-key.d.ts +0 -17
- package/dist/object-key.js +0 -42
- package/dist/storage.controller.d.ts +0 -21
- package/dist/storage.controller.js +0 -380
- package/dist/storage.error.d.ts +0 -28
- package/dist/storage.error.js +0 -46
- package/dist/storage.facade.d.ts +0 -47
- package/dist/storage.facade.js +0 -443
- package/dist/storage.module.d.ts +0 -46
- package/dist/storage.module.js +0 -113
- package/dist/storage.service.d.ts +0 -42
- package/dist/storage.service.js +0 -86
- package/dist/storage.tokens.d.ts +0 -13
- package/dist/storage.tokens.js +0 -26
- package/dist/storage.types.d.ts +0 -245
- package/dist/storage.types.js +0 -1
package/dist/index.js
CHANGED
|
@@ -1,16 +1,1078 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
import { a as isStream, i as countingStream, n as chunkStream, s as toStream, t as byteLengthOf } from "./body-BMUcBeQ0.js";
|
|
2
|
+
import { n as isAbort, t as StorageError } from "./storage.error-DVCOkJhc.js";
|
|
3
|
+
import { n as runWithRetry, t as normalizeRetry } from "./retry-DXtO6R9D.js";
|
|
4
|
+
import { Controller, Get, Inject, Injectable, InjectionToken, Post, Req, defineModule, lazyProvider, stableHash } from "@velajs/vela";
|
|
5
|
+
//#region src/object-key.ts
|
|
6
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
7
|
+
/**
|
|
8
|
+
* True when `key` is a safe object key:
|
|
9
|
+
* - non-empty, not longer than 1024 bytes (S3's limit)
|
|
10
|
+
* - no leading `/`
|
|
11
|
+
* - no `.` / `..` path segments (traversal)
|
|
12
|
+
* - no control characters
|
|
13
|
+
* - no backslashes (Windows-style separators)
|
|
14
|
+
*/
|
|
15
|
+
function isSafeKey(key) {
|
|
16
|
+
if (typeof key !== "string" || key.length === 0 || key.length > 1024) return false;
|
|
17
|
+
if (key.startsWith("/")) return false;
|
|
18
|
+
if (key.includes("\\")) return false;
|
|
19
|
+
if (CONTROL_CHARS.test(key)) return false;
|
|
20
|
+
for (const segment of key.split("/")) if (segment === "." || segment === "..") return false;
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
/** Return `key` if safe, else throw `StorageError('InvalidKey')`. */
|
|
24
|
+
function sanitizeKey(key) {
|
|
25
|
+
if (!isSafeKey(key)) throw new StorageError("InvalidKey", `unsafe object key: ${JSON.stringify(key)}`);
|
|
26
|
+
return key;
|
|
27
|
+
}
|
|
28
|
+
/** Normalize a prefix: drop leading/trailing slashes, collapse to '' when empty. */
|
|
29
|
+
function normalizePrefix(prefix) {
|
|
30
|
+
if (!prefix) return "";
|
|
31
|
+
return prefix.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
32
|
+
}
|
|
33
|
+
/** Join a normalized prefix and a key with a single '/'. */
|
|
34
|
+
function joinKey(prefix, key) {
|
|
35
|
+
return prefix ? `${prefix}/${key}` : key;
|
|
36
|
+
}
|
|
37
|
+
/** Strip a normalized prefix from a stored key (for surfacing to callers). */
|
|
38
|
+
function stripPrefix(prefix, key) {
|
|
39
|
+
if (!prefix) return key;
|
|
40
|
+
const withSlash = `${prefix}/`;
|
|
41
|
+
return key.startsWith(withSlash) ? key.slice(withSlash.length) : key;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/storage.facade.ts
|
|
45
|
+
const DEFAULT_PART_SIZE = 5 * 1024 * 1024;
|
|
46
|
+
const DEFAULT_CONCURRENCY = 4;
|
|
47
|
+
const MULTIPART_THRESHOLD = 16 * 1024 * 1024;
|
|
48
|
+
/**
|
|
49
|
+
* The consumer-facing storage handle. Wraps a {@link StorageDriver} with
|
|
50
|
+
* capability-gating (throws before any driver call), retry/timeout/abort,
|
|
51
|
+
* prefix scoping, generic `move`/`deleteMany` fallbacks, multipart
|
|
52
|
+
* orchestration, progress synthesis, and observability hooks.
|
|
53
|
+
*
|
|
54
|
+
* Framework-free — `StorageService` builds and caches one of these per bucket.
|
|
55
|
+
*/
|
|
56
|
+
var Storage = class Storage {
|
|
57
|
+
#driver;
|
|
58
|
+
#prefix;
|
|
59
|
+
#readonly;
|
|
60
|
+
#opts;
|
|
61
|
+
constructor(opts) {
|
|
62
|
+
this.#opts = opts;
|
|
63
|
+
this.#driver = opts.driver;
|
|
64
|
+
this.#prefix = normalizePrefix(opts.prefix);
|
|
65
|
+
this.#readonly = opts.readonly ?? false;
|
|
66
|
+
}
|
|
67
|
+
get raw() {
|
|
68
|
+
return this.#driver.raw;
|
|
69
|
+
}
|
|
70
|
+
get driver() {
|
|
71
|
+
return this.#driver;
|
|
72
|
+
}
|
|
73
|
+
get capabilities() {
|
|
74
|
+
const d = this.#driver;
|
|
75
|
+
return {
|
|
76
|
+
rangeRead: !!d.supportsRange,
|
|
77
|
+
uploadProgress: !!d.reportsUploadProgress,
|
|
78
|
+
delimiter: !!d.supportsDelimiter,
|
|
79
|
+
metadata: !!d.supportsMetadata,
|
|
80
|
+
cacheControl: !!d.supportsCacheControl,
|
|
81
|
+
serverSideCopy: !!d.supportsServerSideCopy,
|
|
82
|
+
multipart: typeof d.createMultipartUpload === "function",
|
|
83
|
+
signedMultipart: d.signedMultipart != null,
|
|
84
|
+
signedUrl: d.signedUrl ?? {
|
|
85
|
+
supported: false,
|
|
86
|
+
upload: false
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async upload(key, body, opts) {
|
|
91
|
+
this.#assertWritable();
|
|
92
|
+
this.#assertUploadSupported(opts);
|
|
93
|
+
const d = this.#driver;
|
|
94
|
+
const path = this.#path(key);
|
|
95
|
+
const len = byteLengthOf(body);
|
|
96
|
+
const wantsMultipart = opts?.multipart != null && opts.multipart !== false;
|
|
97
|
+
const unknownStream = isStream(body) && len === void 0;
|
|
98
|
+
const bigKnown = len != null && len > MULTIPART_THRESHOLD;
|
|
99
|
+
const canMultipart = typeof d.createMultipartUpload === "function";
|
|
100
|
+
if (wantsMultipart && !canMultipart) throw new StorageError("Unsupported", `${d.name}: multipart not supported`);
|
|
101
|
+
if ((wantsMultipart || unknownStream || bigKnown) && canMultipart) return {
|
|
102
|
+
...await this.#exec("upload", key, false, opts, () => this.#putMultipart(path, body, opts)),
|
|
103
|
+
key
|
|
104
|
+
};
|
|
105
|
+
const streaming = isStream(body);
|
|
106
|
+
let outBody = body;
|
|
107
|
+
if (opts?.onProgress && !d.reportsUploadProgress && streaming) outBody = countingStream(body, (loaded) => opts.onProgress({
|
|
108
|
+
loaded,
|
|
109
|
+
total: len
|
|
110
|
+
}));
|
|
111
|
+
const result = await this.#exec("upload", key, !streaming, opts, (signal) => d.upload(path, outBody, {
|
|
112
|
+
...opts,
|
|
113
|
+
signal
|
|
114
|
+
}));
|
|
115
|
+
if (opts?.onProgress && !d.reportsUploadProgress && !streaming && len != null) opts.onProgress({
|
|
116
|
+
loaded: len,
|
|
117
|
+
total: len
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
...result,
|
|
121
|
+
key
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async #putMultipart(path, body, opts) {
|
|
125
|
+
const mp = typeof opts?.multipart === "object" ? opts.multipart : {};
|
|
126
|
+
const partSize = mp.partSize ?? DEFAULT_PART_SIZE;
|
|
127
|
+
const concurrency = Math.max(1, mp.concurrency ?? DEFAULT_CONCURRENCY);
|
|
128
|
+
const total = byteLengthOf(body);
|
|
129
|
+
const upload = await this.#driver.createMultipartUpload(path, {
|
|
130
|
+
contentType: opts?.contentType,
|
|
131
|
+
cacheControl: opts?.cacheControl,
|
|
132
|
+
metadata: opts?.metadata,
|
|
133
|
+
signal: opts?.signal
|
|
134
|
+
});
|
|
135
|
+
try {
|
|
136
|
+
const parts = [];
|
|
137
|
+
let uploadedBytes = 0;
|
|
138
|
+
let partNumber = 0;
|
|
139
|
+
const inflight = /* @__PURE__ */ new Set();
|
|
140
|
+
for await (const chunk of chunkStream(toStream(body), partSize)) {
|
|
141
|
+
partNumber += 1;
|
|
142
|
+
const n = partNumber;
|
|
143
|
+
const size = chunk.byteLength;
|
|
144
|
+
const tracked = (async () => {
|
|
145
|
+
const up = await upload.uploadPart(n, chunk, { signal: opts?.signal });
|
|
146
|
+
parts[n - 1] = {
|
|
147
|
+
partNumber: n,
|
|
148
|
+
etag: up.etag,
|
|
149
|
+
size: up.size ?? size
|
|
150
|
+
};
|
|
151
|
+
uploadedBytes += size;
|
|
152
|
+
opts?.onProgress?.({
|
|
153
|
+
loaded: uploadedBytes,
|
|
154
|
+
total
|
|
155
|
+
});
|
|
156
|
+
})().finally(() => inflight.delete(tracked));
|
|
157
|
+
inflight.add(tracked);
|
|
158
|
+
if (inflight.size >= concurrency) await Promise.race(inflight);
|
|
159
|
+
}
|
|
160
|
+
await Promise.all(inflight);
|
|
161
|
+
return await upload.complete(parts, { signal: opts?.signal });
|
|
162
|
+
} catch (e) {
|
|
163
|
+
await upload.abort().catch(() => {});
|
|
164
|
+
throw StorageError.wrap(e);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async download(key, opts) {
|
|
168
|
+
if (opts?.range) this.#assertRange();
|
|
169
|
+
const file = await this.#exec("download", key, true, opts, (signal) => this.#driver.download(this.#path(key), {
|
|
170
|
+
...opts,
|
|
171
|
+
signal
|
|
172
|
+
}));
|
|
173
|
+
return this.#relabel(file, key);
|
|
174
|
+
}
|
|
175
|
+
async head(key, opts) {
|
|
176
|
+
const file = await this.#exec("head", key, true, opts, (signal) => this.#driver.head(this.#path(key), {
|
|
177
|
+
...opts,
|
|
178
|
+
signal
|
|
179
|
+
}));
|
|
180
|
+
return this.#relabel(file, key);
|
|
181
|
+
}
|
|
182
|
+
exists(key, opts) {
|
|
183
|
+
return this.#exec("exists", key, true, opts, (signal) => this.#driver.exists(this.#path(key), {
|
|
184
|
+
...opts,
|
|
185
|
+
signal
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
async delete(keyOrKeys, opts) {
|
|
189
|
+
this.#assertWritable();
|
|
190
|
+
if (Array.isArray(keyOrKeys)) return this.#deleteMany(keyOrKeys, opts);
|
|
191
|
+
const key = keyOrKeys;
|
|
192
|
+
return this.#exec("delete", key, true, opts, (signal) => this.#driver.delete(this.#path(key), {
|
|
193
|
+
...opts,
|
|
194
|
+
signal
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
async #deleteMany(keys, opts) {
|
|
198
|
+
const d = this.#driver;
|
|
199
|
+
if (d.deleteMany) {
|
|
200
|
+
const res = await this.#exec("deleteMany", void 0, true, opts, (signal) => d.deleteMany(keys.map((k) => this.#path(k)), {
|
|
201
|
+
...opts,
|
|
202
|
+
signal
|
|
203
|
+
}));
|
|
204
|
+
return {
|
|
205
|
+
deleted: res.deleted.map((k) => this.#strip(k)),
|
|
206
|
+
errors: res.errors?.map((e) => ({
|
|
207
|
+
key: this.#strip(e.key),
|
|
208
|
+
error: e.error
|
|
209
|
+
}))
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const concurrency = Math.max(1, opts?.concurrency ?? 8);
|
|
213
|
+
const stopOnError = opts?.stopOnError ?? false;
|
|
214
|
+
const deleted = [];
|
|
215
|
+
const errors = [];
|
|
216
|
+
let index = 0;
|
|
217
|
+
let stopped = false;
|
|
218
|
+
const worker = async () => {
|
|
219
|
+
while (index < keys.length && !stopped) {
|
|
220
|
+
const key = keys[index++];
|
|
221
|
+
try {
|
|
222
|
+
await d.delete(this.#path(key), { signal: opts?.signal });
|
|
223
|
+
deleted.push(key);
|
|
224
|
+
} catch (e) {
|
|
225
|
+
const error = StorageError.wrap(e);
|
|
226
|
+
errors.push({
|
|
227
|
+
key,
|
|
228
|
+
error
|
|
229
|
+
});
|
|
230
|
+
if (stopOnError) {
|
|
231
|
+
stopped = true;
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const runners = Array.from({ length: Math.min(concurrency, keys.length) }, worker);
|
|
238
|
+
if (stopOnError) await Promise.all(runners);
|
|
239
|
+
else await Promise.allSettled(runners);
|
|
240
|
+
return {
|
|
241
|
+
deleted,
|
|
242
|
+
errors: errors.length ? errors : void 0
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
async copy(from, to, opts) {
|
|
246
|
+
this.#assertWritable();
|
|
247
|
+
return this.#exec("copy", to, true, opts, (signal) => this.#driver.copy(this.#path(from), this.#path(to), {
|
|
248
|
+
...opts,
|
|
249
|
+
signal
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
async move(from, to, opts) {
|
|
253
|
+
this.#assertWritable();
|
|
254
|
+
const f = this.#path(from);
|
|
255
|
+
const t = this.#path(to);
|
|
256
|
+
if (f === t) return;
|
|
257
|
+
if (this.#driver.move) {
|
|
258
|
+
await this.#exec("move", to, true, opts, (signal) => this.#driver.move(f, t, {
|
|
259
|
+
...opts,
|
|
260
|
+
signal
|
|
261
|
+
}));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
await this.#exec("move", to, true, opts, async (signal) => {
|
|
265
|
+
await this.#driver.copy(f, t, {
|
|
266
|
+
...opts,
|
|
267
|
+
signal
|
|
268
|
+
});
|
|
269
|
+
await this.#driver.delete(f, {
|
|
270
|
+
...opts,
|
|
271
|
+
signal
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
async list(opts) {
|
|
276
|
+
if (opts?.delimiter) this.#assertDelimiter();
|
|
277
|
+
const scopedPrefix = this.#listPrefix(opts?.prefix);
|
|
278
|
+
const res = await this.#exec("list", void 0, true, opts, (signal) => this.#driver.list({
|
|
279
|
+
...opts,
|
|
280
|
+
prefix: scopedPrefix,
|
|
281
|
+
signal
|
|
282
|
+
}));
|
|
283
|
+
return {
|
|
284
|
+
items: res.items.map((f) => this.#relabel(f, this.#strip(f.key))),
|
|
285
|
+
prefixes: res.prefixes?.map((p) => this.#strip(p)),
|
|
286
|
+
cursor: res.cursor
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
async *listAll(opts) {
|
|
290
|
+
let cursor = opts?.cursor;
|
|
291
|
+
do {
|
|
292
|
+
const page = await this.list({
|
|
293
|
+
...opts,
|
|
294
|
+
cursor
|
|
295
|
+
});
|
|
296
|
+
for (const item of page.items) yield item;
|
|
297
|
+
cursor = page.cursor;
|
|
298
|
+
} while (cursor);
|
|
299
|
+
}
|
|
300
|
+
async url(key, opts) {
|
|
301
|
+
if (!this.#driver.signedUrl?.supported) throw new StorageError("Unsupported", `${this.#driver.name}: url() not supported`);
|
|
302
|
+
return this.#exec("url", key, true, opts, (signal) => this.#driver.url(this.#path(key), {
|
|
303
|
+
...opts,
|
|
304
|
+
signal
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
async signedUploadUrl(key, opts) {
|
|
308
|
+
this.#assertWritable();
|
|
309
|
+
if (!this.#driver.signedUrl?.upload) throw new StorageError("Unsupported", `${this.#driver.name}: signed uploads not supported`);
|
|
310
|
+
return this.#exec("signedUploadUrl", key, true, opts, (signal) => this.#driver.signedUploadUrl(this.#path(key), {
|
|
311
|
+
...opts,
|
|
312
|
+
signal
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
/** Prefix-bound view of the driver's presigned-multipart capability. */
|
|
316
|
+
get signedMultipart() {
|
|
317
|
+
const cap = this.#driver.signedMultipart;
|
|
318
|
+
if (!cap) return void 0;
|
|
319
|
+
const path = (k) => this.#path(k);
|
|
320
|
+
return {
|
|
321
|
+
create: (key, o) => cap.create(path(key), o),
|
|
322
|
+
signPart: (key, uploadId, partNumber, o) => cap.signPart(path(key), uploadId, partNumber, o),
|
|
323
|
+
complete: (key, uploadId, parts) => cap.complete(path(key), uploadId, parts),
|
|
324
|
+
abort: (key, uploadId) => cap.abort(path(key), uploadId)
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
async createMultipartUpload(key, opts) {
|
|
328
|
+
this.#assertWritable();
|
|
329
|
+
if (typeof this.#driver.createMultipartUpload !== "function") throw new StorageError("Unsupported", `${this.#driver.name}: multipart not supported`);
|
|
330
|
+
return this.#driver.createMultipartUpload(this.#path(key), opts);
|
|
331
|
+
}
|
|
332
|
+
resumeMultipartUpload(key, uploadId) {
|
|
333
|
+
this.#assertWritable();
|
|
334
|
+
if (typeof this.#driver.resumeMultipartUpload !== "function") throw new StorageError("Unsupported", `${this.#driver.name}: multipart resume not supported`);
|
|
335
|
+
return this.#driver.resumeMultipartUpload(this.#path(key), uploadId);
|
|
336
|
+
}
|
|
337
|
+
/** A key-bound handle over the common single-object operations. */
|
|
338
|
+
file(key) {
|
|
339
|
+
return {
|
|
340
|
+
key,
|
|
341
|
+
upload: (body, o) => this.upload(key, body, o),
|
|
342
|
+
download: (o) => this.download(key, o),
|
|
343
|
+
head: (o) => this.head(key, o),
|
|
344
|
+
exists: (o) => this.exists(key, o),
|
|
345
|
+
delete: (o) => this.delete(key, o),
|
|
346
|
+
url: (o) => this.url(key, o),
|
|
347
|
+
signedUploadUrl: (o) => this.signedUploadUrl(key, o)
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
/** A read-only clone of this handle (mutations throw `ReadOnly`). */
|
|
351
|
+
readonly() {
|
|
352
|
+
return new Storage({
|
|
353
|
+
...this.#opts,
|
|
354
|
+
readonly: true
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
#path(key) {
|
|
358
|
+
return joinKey(this.#prefix, sanitizeKey(key));
|
|
359
|
+
}
|
|
360
|
+
#strip(storedKey) {
|
|
361
|
+
return stripPrefix(this.#prefix, storedKey);
|
|
362
|
+
}
|
|
363
|
+
#listPrefix(prefix) {
|
|
364
|
+
if (!this.#prefix) return prefix;
|
|
365
|
+
return prefix ? `${this.#prefix}/${prefix}` : `${this.#prefix}/`;
|
|
366
|
+
}
|
|
367
|
+
#relabel(file, logicalKey) {
|
|
368
|
+
if (file.key === logicalKey) return file;
|
|
369
|
+
return {
|
|
370
|
+
key: logicalKey,
|
|
371
|
+
name: logicalKey.slice(logicalKey.lastIndexOf("/") + 1),
|
|
372
|
+
size: file.size,
|
|
373
|
+
type: file.type,
|
|
374
|
+
lastModified: file.lastModified,
|
|
375
|
+
etag: file.etag,
|
|
376
|
+
metadata: file.metadata,
|
|
377
|
+
arrayBuffer: () => file.arrayBuffer(),
|
|
378
|
+
text: () => file.text(),
|
|
379
|
+
blob: () => file.blob(),
|
|
380
|
+
stream: () => file.stream()
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
#assertWritable() {
|
|
384
|
+
if (this.#readonly) throw new StorageError("ReadOnly", "storage handle is read-only");
|
|
385
|
+
}
|
|
386
|
+
#assertUploadSupported(opts) {
|
|
387
|
+
const d = this.#driver;
|
|
388
|
+
if (opts?.metadata && Object.keys(opts.metadata).length > 0 && !d.supportsMetadata) throw new StorageError("Unsupported", `${d.name}: metadata not supported`);
|
|
389
|
+
if (opts?.cacheControl && !d.supportsCacheControl) throw new StorageError("Unsupported", `${d.name}: cacheControl not supported`);
|
|
390
|
+
}
|
|
391
|
+
#assertRange() {
|
|
392
|
+
if (!this.#driver.supportsRange) throw new StorageError("Unsupported", `${this.#driver.name}: range reads not supported`);
|
|
393
|
+
}
|
|
394
|
+
#assertDelimiter() {
|
|
395
|
+
if (!this.#driver.supportsDelimiter) throw new StorageError("Unsupported", `${this.#driver.name}: delimiter not supported`);
|
|
396
|
+
}
|
|
397
|
+
async #exec(type, key, replayable, opts, fn) {
|
|
398
|
+
const started = Date.now();
|
|
399
|
+
const retries = replayable ? opts?.retries ?? this.#opts.retries : 0;
|
|
400
|
+
const maxRetries = normalizeRetry(retries).max;
|
|
401
|
+
const hooks = this.#opts.hooks;
|
|
402
|
+
try {
|
|
403
|
+
const result = await runWithRetry(fn, {
|
|
404
|
+
retries,
|
|
405
|
+
timeout: opts?.timeout ?? this.#opts.timeout,
|
|
406
|
+
signal: opts?.signal ?? this.#opts.signal
|
|
407
|
+
}, (info) => hooks?.onRetry?.({
|
|
408
|
+
type,
|
|
409
|
+
key,
|
|
410
|
+
attempt: info.attempt,
|
|
411
|
+
maxRetries,
|
|
412
|
+
delayMs: info.delayMs,
|
|
413
|
+
error: info.error
|
|
414
|
+
}));
|
|
415
|
+
hooks?.onOperation?.({
|
|
416
|
+
type,
|
|
417
|
+
key,
|
|
418
|
+
status: "success",
|
|
419
|
+
durationMs: Date.now() - started
|
|
420
|
+
});
|
|
421
|
+
return result;
|
|
422
|
+
} catch (e) {
|
|
423
|
+
const error = StorageError.wrap(e);
|
|
424
|
+
hooks?.onError?.({
|
|
425
|
+
type,
|
|
426
|
+
key,
|
|
427
|
+
error
|
|
428
|
+
});
|
|
429
|
+
hooks?.onOperation?.({
|
|
430
|
+
type,
|
|
431
|
+
key,
|
|
432
|
+
status: "error",
|
|
433
|
+
durationMs: Date.now() - started,
|
|
434
|
+
error
|
|
435
|
+
});
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
function createStorage(opts) {
|
|
441
|
+
return new Storage(opts);
|
|
442
|
+
}
|
|
443
|
+
//#endregion
|
|
444
|
+
//#region src/http/http-helpers.ts
|
|
445
|
+
/** Map a core error code to (wire code, HTTP status). */
|
|
446
|
+
function mapError(code) {
|
|
447
|
+
switch (code) {
|
|
448
|
+
case "NotFound": return {
|
|
449
|
+
wire: "not_found",
|
|
450
|
+
status: 404
|
|
451
|
+
};
|
|
452
|
+
case "AccessDenied":
|
|
453
|
+
case "ReadOnly": return {
|
|
454
|
+
wire: "forbidden",
|
|
455
|
+
status: 403
|
|
456
|
+
};
|
|
457
|
+
case "Unsupported": return {
|
|
458
|
+
wire: "capability_unsupported",
|
|
459
|
+
status: 400
|
|
460
|
+
};
|
|
461
|
+
case "InvalidKey": return {
|
|
462
|
+
wire: "invalid_key",
|
|
463
|
+
status: 400
|
|
464
|
+
};
|
|
465
|
+
case "InvalidRequest":
|
|
466
|
+
case "Aborted": return {
|
|
467
|
+
wire: "invalid_request",
|
|
468
|
+
status: 400
|
|
469
|
+
};
|
|
470
|
+
case "Conflict": return {
|
|
471
|
+
wire: "conflict",
|
|
472
|
+
status: 409
|
|
473
|
+
};
|
|
474
|
+
case "RateLimited": return {
|
|
475
|
+
wire: "rate_limited",
|
|
476
|
+
status: 429
|
|
477
|
+
};
|
|
478
|
+
case "Timeout": return {
|
|
479
|
+
wire: "upstream_error",
|
|
480
|
+
status: 504
|
|
481
|
+
};
|
|
482
|
+
default: return {
|
|
483
|
+
wire: "upstream_error",
|
|
484
|
+
status: 502
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function serializeStored(file) {
|
|
489
|
+
return {
|
|
490
|
+
key: file.key,
|
|
491
|
+
name: file.name,
|
|
492
|
+
size: file.size,
|
|
493
|
+
type: file.type,
|
|
494
|
+
lastModified: file.lastModified,
|
|
495
|
+
etag: file.etag,
|
|
496
|
+
metadata: file.metadata
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
function clampExpiry(requested, def, max) {
|
|
500
|
+
return Math.min(requested ?? def, max);
|
|
501
|
+
}
|
|
502
|
+
/** Parse an HTTP `Range` header (single range) into a {@link ByteRange}. */
|
|
503
|
+
function parseRange(header) {
|
|
504
|
+
if (!header) return void 0;
|
|
505
|
+
const m = /^bytes=(\d+)-(\d*)$/.exec(header.trim());
|
|
506
|
+
if (!m) return void 0;
|
|
507
|
+
return {
|
|
508
|
+
start: Number(m[1]),
|
|
509
|
+
end: m[2] === "" ? void 0 : Number(m[2])
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
/** Content-Disposition header — defaults to `attachment` (anti stored-XSS). */
|
|
513
|
+
function dispositionHeader(disposition, name) {
|
|
514
|
+
const filename = `filename*=UTF-8''${encodeURIComponent(name)}`;
|
|
515
|
+
return disposition === "inline" ? `inline; ${filename}` : `attachment; ${filename}`;
|
|
516
|
+
}
|
|
517
|
+
//#endregion
|
|
518
|
+
//#region \0@oxc-project+runtime@0.139.0/helpers/esm/decorateMetadata.js
|
|
519
|
+
function __decorateMetadata(k, v) {
|
|
520
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
521
|
+
}
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region \0@oxc-project+runtime@0.139.0/helpers/esm/decorateParam.js
|
|
524
|
+
function __decorateParam(paramIndex, decorator) {
|
|
525
|
+
return function(target, key) {
|
|
526
|
+
decorator(target, key, paramIndex);
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region \0@oxc-project+runtime@0.139.0/helpers/esm/decorate.js
|
|
531
|
+
function __decorate(decorators, target, key, desc) {
|
|
532
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
533
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
534
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
535
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
536
|
+
}
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region src/storage.controller.ts
|
|
539
|
+
/**
|
|
540
|
+
* Build a vela controller exposing presigned/multipart upload + download
|
|
541
|
+
* endpoints for one bucket. Factory-decorated (like better-auth's catch-all)
|
|
542
|
+
* so `basePath` bakes in at decoration time. Consumes the {@link StorageService}
|
|
543
|
+
* facade so gating/retry/prefix apply uniformly on the HTTP path.
|
|
544
|
+
*/
|
|
545
|
+
function createStorageController(basePath, serviceToken, http) {
|
|
546
|
+
let StorageController = class StorageController {
|
|
547
|
+
svc;
|
|
548
|
+
constructor(svc) {
|
|
549
|
+
this.svc = svc;
|
|
550
|
+
}
|
|
551
|
+
get storage() {
|
|
552
|
+
return this.svc.storage;
|
|
553
|
+
}
|
|
554
|
+
async authorize(c, action) {
|
|
555
|
+
if (!http.authorize) {
|
|
556
|
+
if (http.defaultPolicy === "allow") return {};
|
|
557
|
+
throw new StorageError("AccessDenied", "storage: no authorizer configured (default deny)");
|
|
558
|
+
}
|
|
559
|
+
const res = await http.authorize(action, {
|
|
560
|
+
req: c.req.raw,
|
|
561
|
+
ctx: c,
|
|
562
|
+
driver: http.driverName
|
|
563
|
+
});
|
|
564
|
+
if (res === false) throw new StorageError("AccessDenied", "not allowed");
|
|
565
|
+
if (res === true) return {};
|
|
566
|
+
return res;
|
|
567
|
+
}
|
|
568
|
+
fail(c, e) {
|
|
569
|
+
const err = e instanceof StorageError ? e : StorageError.wrap(e);
|
|
570
|
+
const { wire, status } = mapError(err.code);
|
|
571
|
+
const message = err.internal || status >= 500 ? "storage backend error" : err.message;
|
|
572
|
+
return c.json({ error: {
|
|
573
|
+
code: wire,
|
|
574
|
+
message
|
|
575
|
+
} }, status);
|
|
576
|
+
}
|
|
577
|
+
async signUpload(c) {
|
|
578
|
+
try {
|
|
579
|
+
const b = await c.req.json();
|
|
580
|
+
const ov = await this.authorize(c, {
|
|
581
|
+
type: "sign-upload",
|
|
582
|
+
key: b.key,
|
|
583
|
+
contentType: b.contentType,
|
|
584
|
+
size: b.size
|
|
585
|
+
});
|
|
586
|
+
const key = sanitizeKey(ov.key ?? b.key);
|
|
587
|
+
const upload = await this.storage.signedUploadUrl(key, {
|
|
588
|
+
expiresIn: clampExpiry(ov.expiresIn ?? b.expiresIn, http.defaultExpiresIn, http.maxExpiresIn),
|
|
589
|
+
contentType: b.contentType,
|
|
590
|
+
maxSize: ov.maxSize ?? http.maxUploadSize
|
|
591
|
+
});
|
|
592
|
+
return c.json({
|
|
593
|
+
key,
|
|
594
|
+
upload
|
|
595
|
+
});
|
|
596
|
+
} catch (e) {
|
|
597
|
+
return this.fail(c, e);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
async multipartCreate(c) {
|
|
601
|
+
try {
|
|
602
|
+
const b = await c.req.json();
|
|
603
|
+
const ov = await this.authorize(c, {
|
|
604
|
+
type: "multipart-create",
|
|
605
|
+
key: b.key,
|
|
606
|
+
contentType: b.contentType
|
|
607
|
+
});
|
|
608
|
+
const key = sanitizeKey(ov.key ?? b.key);
|
|
609
|
+
const sm = this.storage.signedMultipart;
|
|
610
|
+
if (!sm) throw new StorageError("Unsupported", "presigned multipart not supported");
|
|
611
|
+
const created = await sm.create(key, {
|
|
612
|
+
contentType: b.contentType,
|
|
613
|
+
metadata: ov.metadata ?? b.metadata,
|
|
614
|
+
partSize: b.partSize
|
|
615
|
+
});
|
|
616
|
+
return c.json({
|
|
617
|
+
key,
|
|
618
|
+
uploadId: created.uploadId,
|
|
619
|
+
partSize: created.partSize
|
|
620
|
+
});
|
|
621
|
+
} catch (e) {
|
|
622
|
+
return this.fail(c, e);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
async multipartSignPart(c) {
|
|
626
|
+
try {
|
|
627
|
+
const b = await c.req.json();
|
|
628
|
+
const ov = await this.authorize(c, {
|
|
629
|
+
type: "multipart-sign-part",
|
|
630
|
+
key: b.key,
|
|
631
|
+
uploadId: b.uploadId,
|
|
632
|
+
partNumber: b.partNumber
|
|
633
|
+
});
|
|
634
|
+
const key = sanitizeKey(ov.key ?? b.key);
|
|
635
|
+
const sm = this.storage.signedMultipart;
|
|
636
|
+
if (!sm) throw new StorageError("Unsupported", "presigned multipart not supported");
|
|
637
|
+
const part = await sm.signPart(key, b.uploadId, b.partNumber, { expiresIn: clampExpiry(ov.expiresIn, http.defaultExpiresIn, http.maxExpiresIn) });
|
|
638
|
+
return c.json({
|
|
639
|
+
partNumber: b.partNumber,
|
|
640
|
+
method: "PUT",
|
|
641
|
+
url: part.url,
|
|
642
|
+
headers: part.headers
|
|
643
|
+
});
|
|
644
|
+
} catch (e) {
|
|
645
|
+
return this.fail(c, e);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async multipartComplete(c) {
|
|
649
|
+
try {
|
|
650
|
+
const b = await c.req.json();
|
|
651
|
+
const key = sanitizeKey((await this.authorize(c, {
|
|
652
|
+
type: "multipart-complete",
|
|
653
|
+
key: b.key,
|
|
654
|
+
uploadId: b.uploadId
|
|
655
|
+
})).key ?? b.key);
|
|
656
|
+
const sm = this.storage.signedMultipart;
|
|
657
|
+
if (!sm) throw new StorageError("Unsupported", "presigned multipart not supported");
|
|
658
|
+
return c.json(await sm.complete(key, b.uploadId, b.parts));
|
|
659
|
+
} catch (e) {
|
|
660
|
+
return this.fail(c, e);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async multipartAbort(c) {
|
|
664
|
+
try {
|
|
665
|
+
const b = await c.req.json();
|
|
666
|
+
const key = sanitizeKey((await this.authorize(c, {
|
|
667
|
+
type: "multipart-abort",
|
|
668
|
+
key: b.key,
|
|
669
|
+
uploadId: b.uploadId
|
|
670
|
+
})).key ?? b.key);
|
|
671
|
+
const sm = this.storage.signedMultipart;
|
|
672
|
+
if (!sm) throw new StorageError("Unsupported", "presigned multipart not supported");
|
|
673
|
+
await sm.abort(key, b.uploadId);
|
|
674
|
+
return c.json({ ok: true });
|
|
675
|
+
} catch (e) {
|
|
676
|
+
return this.fail(c, e);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async list(c) {
|
|
680
|
+
try {
|
|
681
|
+
const prefix = c.req.query("prefix");
|
|
682
|
+
const ov = await this.authorize(c, {
|
|
683
|
+
type: "list",
|
|
684
|
+
prefix
|
|
685
|
+
});
|
|
686
|
+
const limit = c.req.query("limit");
|
|
687
|
+
const res = await this.storage.list({
|
|
688
|
+
prefix: ov.prefix ?? prefix,
|
|
689
|
+
cursor: c.req.query("cursor"),
|
|
690
|
+
delimiter: c.req.query("delimiter"),
|
|
691
|
+
limit: limit ? Math.min(Number(limit), http.maxListLimit) : void 0
|
|
692
|
+
});
|
|
693
|
+
return c.json({
|
|
694
|
+
items: res.items.map(serializeStored),
|
|
695
|
+
prefixes: res.prefixes,
|
|
696
|
+
cursor: res.cursor
|
|
697
|
+
});
|
|
698
|
+
} catch (e) {
|
|
699
|
+
return this.fail(c, e);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
async head(c) {
|
|
703
|
+
try {
|
|
704
|
+
const key = sanitizeKey(c.req.query("key") ?? "");
|
|
705
|
+
const ov = await this.authorize(c, {
|
|
706
|
+
type: "head",
|
|
707
|
+
key
|
|
708
|
+
});
|
|
709
|
+
const file = await this.storage.head(sanitizeKey(ov.key ?? key));
|
|
710
|
+
return c.json(serializeStored(file));
|
|
711
|
+
} catch (e) {
|
|
712
|
+
return this.fail(c, e);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
async delete(c) {
|
|
716
|
+
try {
|
|
717
|
+
const keys = (await c.req.json()).keys.map((k) => sanitizeKey(k));
|
|
718
|
+
const effective = ((await this.authorize(c, {
|
|
719
|
+
type: "delete",
|
|
720
|
+
keys
|
|
721
|
+
})).keys ?? keys).map((k) => sanitizeKey(k));
|
|
722
|
+
return c.json(await this.storage.delete(effective, { concurrency: http.deleteConcurrency }));
|
|
723
|
+
} catch (e) {
|
|
724
|
+
return this.fail(c, e);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
async download(c) {
|
|
728
|
+
try {
|
|
729
|
+
const key = sanitizeKey(c.req.query("key") ?? "");
|
|
730
|
+
const disp = c.req.query("disposition");
|
|
731
|
+
const ov = await this.authorize(c, {
|
|
732
|
+
type: "download",
|
|
733
|
+
key
|
|
734
|
+
});
|
|
735
|
+
const ekey = sanitizeKey(ov.key ?? key);
|
|
736
|
+
const name = ekey.slice(ekey.lastIndexOf("/") + 1);
|
|
737
|
+
if (http.download === "redirect" && this.storage.capabilities.signedUrl.supported) {
|
|
738
|
+
const url = await this.storage.url(ekey, {
|
|
739
|
+
expiresIn: clampExpiry(ov.expiresIn, http.defaultExpiresIn, http.maxExpiresIn),
|
|
740
|
+
responseContentDisposition: dispositionHeader(disp, name)
|
|
741
|
+
});
|
|
742
|
+
return c.redirect(url, 302);
|
|
743
|
+
}
|
|
744
|
+
const range = parseRange(c.req.header("range"));
|
|
745
|
+
const file = await this.storage.download(ekey, range ? { range } : void 0);
|
|
746
|
+
const headers = new Headers({
|
|
747
|
+
"content-type": file.type,
|
|
748
|
+
"content-length": String(file.size),
|
|
749
|
+
"content-disposition": dispositionHeader(disp, file.name)
|
|
750
|
+
});
|
|
751
|
+
if (file.etag) headers.set("etag", file.etag);
|
|
752
|
+
if (range) {
|
|
753
|
+
headers.set("content-range", `bytes ${range.start}-${range.start + file.size - 1}/*`);
|
|
754
|
+
return new Response(file.stream(), {
|
|
755
|
+
status: 206,
|
|
756
|
+
headers
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
return new Response(file.stream(), {
|
|
760
|
+
status: 200,
|
|
761
|
+
headers
|
|
762
|
+
});
|
|
763
|
+
} catch (e) {
|
|
764
|
+
return this.fail(c, e);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
async signDownload(c) {
|
|
768
|
+
try {
|
|
769
|
+
const b = await c.req.json();
|
|
770
|
+
const ov = await this.authorize(c, {
|
|
771
|
+
type: "download",
|
|
772
|
+
key: b.key
|
|
773
|
+
});
|
|
774
|
+
const key = sanitizeKey(ov.key ?? b.key);
|
|
775
|
+
const expiresIn = clampExpiry(ov.expiresIn ?? b.expiresIn, http.defaultExpiresIn, http.maxExpiresIn);
|
|
776
|
+
const url = await this.storage.url(key, { expiresIn });
|
|
777
|
+
return c.json({
|
|
778
|
+
url,
|
|
779
|
+
expiresAt: Date.now() + expiresIn * 1e3
|
|
780
|
+
});
|
|
781
|
+
} catch (e) {
|
|
782
|
+
return this.fail(c, e);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
__decorate([
|
|
787
|
+
Post("/sign-upload"),
|
|
788
|
+
__decorateParam(0, Req()),
|
|
789
|
+
__decorateMetadata("design:type", Function),
|
|
790
|
+
__decorateMetadata("design:paramtypes", [Object]),
|
|
791
|
+
__decorateMetadata("design:returntype", Promise)
|
|
792
|
+
], StorageController.prototype, "signUpload", null);
|
|
793
|
+
__decorate([
|
|
794
|
+
Post("/multipart/create"),
|
|
795
|
+
__decorateParam(0, Req()),
|
|
796
|
+
__decorateMetadata("design:type", Function),
|
|
797
|
+
__decorateMetadata("design:paramtypes", [Object]),
|
|
798
|
+
__decorateMetadata("design:returntype", Promise)
|
|
799
|
+
], StorageController.prototype, "multipartCreate", null);
|
|
800
|
+
__decorate([
|
|
801
|
+
Post("/multipart/sign-part"),
|
|
802
|
+
__decorateParam(0, Req()),
|
|
803
|
+
__decorateMetadata("design:type", Function),
|
|
804
|
+
__decorateMetadata("design:paramtypes", [Object]),
|
|
805
|
+
__decorateMetadata("design:returntype", Promise)
|
|
806
|
+
], StorageController.prototype, "multipartSignPart", null);
|
|
807
|
+
__decorate([
|
|
808
|
+
Post("/multipart/complete"),
|
|
809
|
+
__decorateParam(0, Req()),
|
|
810
|
+
__decorateMetadata("design:type", Function),
|
|
811
|
+
__decorateMetadata("design:paramtypes", [Object]),
|
|
812
|
+
__decorateMetadata("design:returntype", Promise)
|
|
813
|
+
], StorageController.prototype, "multipartComplete", null);
|
|
814
|
+
__decorate([
|
|
815
|
+
Post("/multipart/abort"),
|
|
816
|
+
__decorateParam(0, Req()),
|
|
817
|
+
__decorateMetadata("design:type", Function),
|
|
818
|
+
__decorateMetadata("design:paramtypes", [Object]),
|
|
819
|
+
__decorateMetadata("design:returntype", Promise)
|
|
820
|
+
], StorageController.prototype, "multipartAbort", null);
|
|
821
|
+
__decorate([
|
|
822
|
+
Get("/list"),
|
|
823
|
+
__decorateParam(0, Req()),
|
|
824
|
+
__decorateMetadata("design:type", Function),
|
|
825
|
+
__decorateMetadata("design:paramtypes", [Object]),
|
|
826
|
+
__decorateMetadata("design:returntype", Promise)
|
|
827
|
+
], StorageController.prototype, "list", null);
|
|
828
|
+
__decorate([
|
|
829
|
+
Get("/head"),
|
|
830
|
+
__decorateParam(0, Req()),
|
|
831
|
+
__decorateMetadata("design:type", Function),
|
|
832
|
+
__decorateMetadata("design:paramtypes", [Object]),
|
|
833
|
+
__decorateMetadata("design:returntype", Promise)
|
|
834
|
+
], StorageController.prototype, "head", null);
|
|
835
|
+
__decorate([
|
|
836
|
+
Post("/delete"),
|
|
837
|
+
__decorateParam(0, Req()),
|
|
838
|
+
__decorateMetadata("design:type", Function),
|
|
839
|
+
__decorateMetadata("design:paramtypes", [Object]),
|
|
840
|
+
__decorateMetadata("design:returntype", Promise)
|
|
841
|
+
], StorageController.prototype, "delete", null);
|
|
842
|
+
__decorate([
|
|
843
|
+
Get("/download"),
|
|
844
|
+
__decorateParam(0, Req()),
|
|
845
|
+
__decorateMetadata("design:type", Function),
|
|
846
|
+
__decorateMetadata("design:paramtypes", [Object]),
|
|
847
|
+
__decorateMetadata("design:returntype", Promise)
|
|
848
|
+
], StorageController.prototype, "download", null);
|
|
849
|
+
__decorate([
|
|
850
|
+
Post("/sign-download"),
|
|
851
|
+
__decorateParam(0, Req()),
|
|
852
|
+
__decorateMetadata("design:type", Function),
|
|
853
|
+
__decorateMetadata("design:paramtypes", [Object]),
|
|
854
|
+
__decorateMetadata("design:returntype", Promise)
|
|
855
|
+
], StorageController.prototype, "signDownload", null);
|
|
856
|
+
StorageController = __decorate([
|
|
857
|
+
Controller(basePath),
|
|
858
|
+
Injectable(),
|
|
859
|
+
__decorateParam(0, Inject(serviceToken)),
|
|
860
|
+
__decorateMetadata("design:paramtypes", [Object])
|
|
861
|
+
], StorageController);
|
|
862
|
+
return StorageController;
|
|
863
|
+
}
|
|
864
|
+
//#endregion
|
|
865
|
+
//#region src/storage.service.ts
|
|
866
|
+
/**
|
|
867
|
+
* Build the real driver on first property access and delegate to it. This is
|
|
868
|
+
* the edge-binding-safe seam: the driver factory (which may read Cloudflare
|
|
869
|
+
* bindings / `c.env` secrets) does not run at module load — only on the first
|
|
870
|
+
* storage operation, which happens at request time.
|
|
871
|
+
*
|
|
872
|
+
* Functions are bound to the built driver so class-based drivers keep `this`.
|
|
873
|
+
*/
|
|
874
|
+
function lazyDriver(build) {
|
|
875
|
+
let built;
|
|
876
|
+
const ensure = () => built ??= build();
|
|
877
|
+
return new Proxy({}, {
|
|
878
|
+
get(_target, prop) {
|
|
879
|
+
const driver = ensure();
|
|
880
|
+
const value = driver[prop];
|
|
881
|
+
return typeof value === "function" ? value.bind(driver) : value;
|
|
882
|
+
},
|
|
883
|
+
has(_target, prop) {
|
|
884
|
+
return prop in ensure();
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
let StorageService = class StorageService {
|
|
889
|
+
storage;
|
|
890
|
+
name;
|
|
891
|
+
constructor(build, options) {
|
|
892
|
+
this.name = options.name;
|
|
893
|
+
this.storage = createStorage({
|
|
894
|
+
driver: lazyDriver(build),
|
|
895
|
+
prefix: options.prefix,
|
|
896
|
+
readonly: options.readonly,
|
|
897
|
+
hooks: options.hooks
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
upload(key, body, opts) {
|
|
901
|
+
return this.storage.upload(key, body, opts);
|
|
902
|
+
}
|
|
903
|
+
download(key, opts) {
|
|
904
|
+
return this.storage.download(key, opts);
|
|
905
|
+
}
|
|
906
|
+
head(key, opts) {
|
|
907
|
+
return this.storage.head(key, opts);
|
|
908
|
+
}
|
|
909
|
+
exists(key, opts) {
|
|
910
|
+
return this.storage.exists(key, opts);
|
|
911
|
+
}
|
|
912
|
+
delete(key, opts) {
|
|
913
|
+
return this.storage.delete(key, opts);
|
|
914
|
+
}
|
|
915
|
+
deleteMany(keys, opts) {
|
|
916
|
+
return this.storage.delete(keys, opts);
|
|
917
|
+
}
|
|
918
|
+
copy(from, to, opts) {
|
|
919
|
+
return this.storage.copy(from, to, opts);
|
|
920
|
+
}
|
|
921
|
+
move(from, to, opts) {
|
|
922
|
+
return this.storage.move(from, to, opts);
|
|
923
|
+
}
|
|
924
|
+
list(opts) {
|
|
925
|
+
return this.storage.list(opts);
|
|
926
|
+
}
|
|
927
|
+
url(key, opts) {
|
|
928
|
+
return this.storage.url(key, opts);
|
|
929
|
+
}
|
|
930
|
+
signedUploadUrl(key, opts) {
|
|
931
|
+
return this.storage.signedUploadUrl(key, opts);
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
StorageService = __decorate([Injectable(), __decorateMetadata("design:paramtypes", [Function, Object])], StorageService);
|
|
935
|
+
//#endregion
|
|
936
|
+
//#region src/storage.tokens.ts
|
|
937
|
+
/** Bucket name used when a registration omits `name`. */
|
|
938
|
+
const DEFAULT_STORAGE_NAME = "default";
|
|
939
|
+
const builderTokens = /* @__PURE__ */ new Map();
|
|
940
|
+
const serviceTokens = /* @__PURE__ */ new Map();
|
|
941
|
+
/** Internal token holding the lazy driver-construction thunk for `name`. */
|
|
942
|
+
function storageDriverBuilder(name) {
|
|
943
|
+
let token = builderTokens.get(name);
|
|
944
|
+
if (!token) {
|
|
945
|
+
token = new InjectionToken(`vela.storage.Builder:${name}`);
|
|
946
|
+
builderTokens.set(name, token);
|
|
947
|
+
}
|
|
948
|
+
return token;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Token that resolves to the {@link StorageService} for a named bucket.
|
|
952
|
+
* The default bucket uses the `StorageService` class token directly (see
|
|
953
|
+
* `@InjectStorage()`); named buckets use this.
|
|
954
|
+
*/
|
|
955
|
+
function storageToken(name) {
|
|
956
|
+
let token = serviceTokens.get(name);
|
|
957
|
+
if (!token) {
|
|
958
|
+
token = new InjectionToken(`vela.storage.Service:${name}`);
|
|
959
|
+
serviceTokens.set(name, token);
|
|
960
|
+
}
|
|
961
|
+
return token;
|
|
962
|
+
}
|
|
963
|
+
//#endregion
|
|
964
|
+
//#region src/storage.module.ts
|
|
965
|
+
/**
|
|
966
|
+
* The stable, identifying subset of the HTTP options folded into the instance
|
|
967
|
+
* key — matches what `buildControllers` bakes into the mounted controller so
|
|
968
|
+
* two registrations that differ only in HTTP behavior get distinct keys.
|
|
969
|
+
*/
|
|
970
|
+
function httpKeyPart(http) {
|
|
971
|
+
if (!http || http.mountController === false) return {};
|
|
972
|
+
return {
|
|
973
|
+
basePath: http.basePath ?? "/api/storage",
|
|
974
|
+
download: http.download ?? "redirect",
|
|
975
|
+
defaultPolicy: http.defaultPolicy ?? "deny"
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
function buildControllers(name, serviceToken, http) {
|
|
979
|
+
if (!http || http.mountController === false) return [];
|
|
980
|
+
const resolved = {
|
|
981
|
+
driverName: name,
|
|
982
|
+
authorize: http.authorize,
|
|
983
|
+
defaultPolicy: http.defaultPolicy ?? "deny",
|
|
984
|
+
download: http.download ?? "redirect",
|
|
985
|
+
defaultExpiresIn: http.defaultExpiresIn ?? 900,
|
|
986
|
+
maxExpiresIn: http.maxExpiresIn ?? 3600,
|
|
987
|
+
maxUploadSize: http.maxUploadSize,
|
|
988
|
+
maxListLimit: http.maxListLimit ?? 1e3,
|
|
989
|
+
deleteConcurrency: http.deleteConcurrency ?? 8
|
|
990
|
+
};
|
|
991
|
+
return [createStorageController(http.basePath ?? "/api/storage", serviceToken, resolved)];
|
|
992
|
+
}
|
|
993
|
+
const { ConfigurableModuleClass } = defineModule({
|
|
994
|
+
name: "Storage",
|
|
995
|
+
key: (o) => stableHash({
|
|
996
|
+
name: o.name ?? "default",
|
|
997
|
+
prefix: o.prefix ?? "",
|
|
998
|
+
readonly: !!o.readonly,
|
|
999
|
+
hooks: !!o.hooks,
|
|
1000
|
+
...httpKeyPart(o.http)
|
|
1001
|
+
}),
|
|
1002
|
+
setup: ({ OPTIONS, options }) => {
|
|
1003
|
+
const name = options.name ?? "default";
|
|
1004
|
+
const builderToken = storageDriverBuilder(name);
|
|
1005
|
+
const serviceToken = name === "default" ? StorageService : storageToken(name);
|
|
1006
|
+
const svcOptions = {
|
|
1007
|
+
name,
|
|
1008
|
+
prefix: options.prefix,
|
|
1009
|
+
readonly: options.readonly,
|
|
1010
|
+
hooks: options.hooks
|
|
1011
|
+
};
|
|
1012
|
+
return {
|
|
1013
|
+
providers: [lazyProvider({
|
|
1014
|
+
provide: builderToken,
|
|
1015
|
+
inject: [OPTIONS],
|
|
1016
|
+
useFactory: (o) => o.driver()
|
|
1017
|
+
}), {
|
|
1018
|
+
provide: serviceToken,
|
|
1019
|
+
useFactory: (build) => new StorageService(build, svcOptions),
|
|
1020
|
+
inject: [builderToken]
|
|
1021
|
+
}],
|
|
1022
|
+
controllers: buildControllers(name, serviceToken, options.http),
|
|
1023
|
+
exports: [serviceToken, builderToken]
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
var StorageModule = class StorageModule {
|
|
1028
|
+
/** Synchronous registration — the driver is provided directly. */
|
|
1029
|
+
static forRoot(options) {
|
|
1030
|
+
const { driver, ...rest } = options;
|
|
1031
|
+
return {
|
|
1032
|
+
...ConfigurableModuleClass.forRoot({
|
|
1033
|
+
...rest,
|
|
1034
|
+
driver: () => driver
|
|
1035
|
+
}),
|
|
1036
|
+
module: StorageModule
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
/** Deferred / DI-driven registration — `useFactory` runs lazily on first use. */
|
|
1040
|
+
static forRootAsync(options) {
|
|
1041
|
+
const { useFactory, inject, imports, key, ...structural } = options;
|
|
1042
|
+
return {
|
|
1043
|
+
...ConfigurableModuleClass.forRootAsync({
|
|
1044
|
+
imports,
|
|
1045
|
+
inject,
|
|
1046
|
+
key,
|
|
1047
|
+
...structural,
|
|
1048
|
+
useFactory: (...deps) => ({ driver: () => useFactory(...deps) })
|
|
1049
|
+
}),
|
|
1050
|
+
module: StorageModule
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
//#endregion
|
|
1055
|
+
//#region src/decorators/inject-storage.decorator.ts
|
|
1056
|
+
/**
|
|
1057
|
+
* Inject the {@link StorageService} for a named bucket.
|
|
1058
|
+
*
|
|
1059
|
+
* ```ts
|
|
1060
|
+
* class UploadService {
|
|
1061
|
+
* constructor(
|
|
1062
|
+
* @InjectStorage() private readonly uploads: StorageService, // default bucket
|
|
1063
|
+
* @InjectStorage('backups') private readonly backups: StorageService, // named bucket
|
|
1064
|
+
* ) {}
|
|
1065
|
+
* }
|
|
1066
|
+
* ```
|
|
1067
|
+
*
|
|
1068
|
+
* The default bucket resolves the `StorageService` class token directly (so
|
|
1069
|
+
* plain `@Inject(StorageService)` and `Test.overrideProvider(StorageService)`
|
|
1070
|
+
* keep working); named buckets resolve their per-name token.
|
|
1071
|
+
*/
|
|
1072
|
+
function InjectStorage(name = DEFAULT_STORAGE_NAME) {
|
|
1073
|
+
return Inject(name === "default" ? StorageService : storageToken(name));
|
|
1074
|
+
}
|
|
1075
|
+
//#endregion
|
|
1076
|
+
export { DEFAULT_STORAGE_NAME, InjectStorage, Storage, StorageError, StorageModule, StorageService, createStorage, createStorageController, isAbort, isSafeKey, joinKey, lazyDriver, normalizePrefix, sanitizeKey, storageDriverBuilder, storageToken, stripPrefix };
|
|
1077
|
+
|
|
1078
|
+
//# sourceMappingURL=index.js.map
|