@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/storage.facade.js
DELETED
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
import { byteLengthOf, chunkStream, countingStream, isStream, toStream } from "./internal/body.js";
|
|
2
|
-
import { normalizeRetry, runWithRetry } from "./internal/retry.js";
|
|
3
|
-
import { joinKey, normalizePrefix, sanitizeKey, stripPrefix } from "./object-key.js";
|
|
4
|
-
import { StorageError } from "./storage.error.js";
|
|
5
|
-
const DEFAULT_PART_SIZE = 5 * 1024 * 1024; // 5 MiB (S3 minimum non-final part)
|
|
6
|
-
const DEFAULT_CONCURRENCY = 4;
|
|
7
|
-
// Known-length bodies larger than this are routed through multipart so we never
|
|
8
|
-
// buffer a whole large object in memory (aws4fetch must hash the payload).
|
|
9
|
-
const MULTIPART_THRESHOLD = 16 * 1024 * 1024;
|
|
10
|
-
/**
|
|
11
|
-
* The consumer-facing storage handle. Wraps a {@link StorageDriver} with
|
|
12
|
-
* capability-gating (throws before any driver call), retry/timeout/abort,
|
|
13
|
-
* prefix scoping, generic `move`/`deleteMany` fallbacks, multipart
|
|
14
|
-
* orchestration, progress synthesis, and observability hooks.
|
|
15
|
-
*
|
|
16
|
-
* Framework-free — `StorageService` builds and caches one of these per bucket.
|
|
17
|
-
*/ export class Storage {
|
|
18
|
-
#driver;
|
|
19
|
-
#prefix;
|
|
20
|
-
#readonly;
|
|
21
|
-
#opts;
|
|
22
|
-
constructor(opts){
|
|
23
|
-
this.#opts = opts;
|
|
24
|
-
this.#driver = opts.driver;
|
|
25
|
-
this.#prefix = normalizePrefix(opts.prefix);
|
|
26
|
-
this.#readonly = opts.readonly ?? false;
|
|
27
|
-
}
|
|
28
|
-
get raw() {
|
|
29
|
-
return this.#driver.raw;
|
|
30
|
-
}
|
|
31
|
-
get driver() {
|
|
32
|
-
return this.#driver;
|
|
33
|
-
}
|
|
34
|
-
get capabilities() {
|
|
35
|
-
const d = this.#driver;
|
|
36
|
-
return {
|
|
37
|
-
rangeRead: !!d.supportsRange,
|
|
38
|
-
uploadProgress: !!d.reportsUploadProgress,
|
|
39
|
-
delimiter: !!d.supportsDelimiter,
|
|
40
|
-
metadata: !!d.supportsMetadata,
|
|
41
|
-
cacheControl: !!d.supportsCacheControl,
|
|
42
|
-
serverSideCopy: !!d.supportsServerSideCopy,
|
|
43
|
-
multipart: typeof d.createMultipartUpload === 'function',
|
|
44
|
-
signedMultipart: d.signedMultipart != null,
|
|
45
|
-
signedUrl: d.signedUrl ?? {
|
|
46
|
-
supported: false,
|
|
47
|
-
upload: false
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
// ---- upload ------------------------------------------------------------
|
|
52
|
-
async upload(key, body, opts) {
|
|
53
|
-
this.#assertWritable();
|
|
54
|
-
this.#assertUploadSupported(opts);
|
|
55
|
-
const d = this.#driver;
|
|
56
|
-
const path = this.#path(key);
|
|
57
|
-
const len = byteLengthOf(body);
|
|
58
|
-
const wantsMultipart = opts?.multipart != null && opts.multipart !== false;
|
|
59
|
-
const unknownStream = isStream(body) && len === undefined;
|
|
60
|
-
const bigKnown = len != null && len > MULTIPART_THRESHOLD;
|
|
61
|
-
const canMultipart = typeof d.createMultipartUpload === 'function';
|
|
62
|
-
if (wantsMultipart && !canMultipart) {
|
|
63
|
-
throw new StorageError('Unsupported', `${d.name}: multipart not supported`);
|
|
64
|
-
}
|
|
65
|
-
if ((wantsMultipart || unknownStream || bigKnown) && canMultipart) {
|
|
66
|
-
const result = await this.#exec('upload', key, false, opts, ()=>this.#putMultipart(path, body, opts));
|
|
67
|
-
return {
|
|
68
|
-
...result,
|
|
69
|
-
key
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
const streaming = isStream(body);
|
|
73
|
-
let outBody = body;
|
|
74
|
-
if (opts?.onProgress && !d.reportsUploadProgress && streaming) {
|
|
75
|
-
outBody = countingStream(body, (loaded)=>opts.onProgress({
|
|
76
|
-
loaded,
|
|
77
|
-
total: len
|
|
78
|
-
}));
|
|
79
|
-
}
|
|
80
|
-
const result = await this.#exec('upload', key, !streaming, opts, (signal)=>d.upload(path, outBody, {
|
|
81
|
-
...opts,
|
|
82
|
-
signal
|
|
83
|
-
}));
|
|
84
|
-
if (opts?.onProgress && !d.reportsUploadProgress && !streaming && len != null) {
|
|
85
|
-
opts.onProgress({
|
|
86
|
-
loaded: len,
|
|
87
|
-
total: len
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
return {
|
|
91
|
-
...result,
|
|
92
|
-
key
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
async #putMultipart(path, body, opts) {
|
|
96
|
-
const mp = typeof opts?.multipart === 'object' ? opts.multipart : {};
|
|
97
|
-
const partSize = mp.partSize ?? DEFAULT_PART_SIZE;
|
|
98
|
-
const concurrency = Math.max(1, mp.concurrency ?? DEFAULT_CONCURRENCY);
|
|
99
|
-
const total = byteLengthOf(body);
|
|
100
|
-
const upload = await this.#driver.createMultipartUpload(path, {
|
|
101
|
-
contentType: opts?.contentType,
|
|
102
|
-
cacheControl: opts?.cacheControl,
|
|
103
|
-
metadata: opts?.metadata,
|
|
104
|
-
signal: opts?.signal
|
|
105
|
-
});
|
|
106
|
-
try {
|
|
107
|
-
const parts = [];
|
|
108
|
-
let uploadedBytes = 0;
|
|
109
|
-
let partNumber = 0;
|
|
110
|
-
const inflight = new Set();
|
|
111
|
-
for await (const chunk of chunkStream(toStream(body), partSize)){
|
|
112
|
-
partNumber += 1;
|
|
113
|
-
const n = partNumber;
|
|
114
|
-
const size = chunk.byteLength;
|
|
115
|
-
const task = (async ()=>{
|
|
116
|
-
const up = await upload.uploadPart(n, chunk, {
|
|
117
|
-
signal: opts?.signal
|
|
118
|
-
});
|
|
119
|
-
parts[n - 1] = {
|
|
120
|
-
partNumber: n,
|
|
121
|
-
etag: up.etag,
|
|
122
|
-
size: up.size ?? size
|
|
123
|
-
};
|
|
124
|
-
uploadedBytes += size;
|
|
125
|
-
opts?.onProgress?.({
|
|
126
|
-
loaded: uploadedBytes,
|
|
127
|
-
total
|
|
128
|
-
});
|
|
129
|
-
})();
|
|
130
|
-
const tracked = task.finally(()=>inflight.delete(tracked));
|
|
131
|
-
inflight.add(tracked);
|
|
132
|
-
if (inflight.size >= concurrency) await Promise.race(inflight);
|
|
133
|
-
}
|
|
134
|
-
await Promise.all(inflight);
|
|
135
|
-
return await upload.complete(parts, {
|
|
136
|
-
signal: opts?.signal
|
|
137
|
-
});
|
|
138
|
-
} catch (e) {
|
|
139
|
-
await upload.abort().catch(()=>{});
|
|
140
|
-
throw StorageError.wrap(e);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
// ---- read / metadata ---------------------------------------------------
|
|
144
|
-
async download(key, opts) {
|
|
145
|
-
if (opts?.range) this.#assertRange();
|
|
146
|
-
const file = await this.#exec('download', key, true, opts, (signal)=>this.#driver.download(this.#path(key), {
|
|
147
|
-
...opts,
|
|
148
|
-
signal
|
|
149
|
-
}));
|
|
150
|
-
return this.#relabel(file, key);
|
|
151
|
-
}
|
|
152
|
-
async head(key, opts) {
|
|
153
|
-
const file = await this.#exec('head', key, true, opts, (signal)=>this.#driver.head(this.#path(key), {
|
|
154
|
-
...opts,
|
|
155
|
-
signal
|
|
156
|
-
}));
|
|
157
|
-
return this.#relabel(file, key);
|
|
158
|
-
}
|
|
159
|
-
exists(key, opts) {
|
|
160
|
-
return this.#exec('exists', key, true, opts, (signal)=>this.#driver.exists(this.#path(key), {
|
|
161
|
-
...opts,
|
|
162
|
-
signal
|
|
163
|
-
}));
|
|
164
|
-
}
|
|
165
|
-
async delete(keyOrKeys, opts) {
|
|
166
|
-
this.#assertWritable();
|
|
167
|
-
if (Array.isArray(keyOrKeys)) return this.#deleteMany(keyOrKeys, opts);
|
|
168
|
-
const key = keyOrKeys;
|
|
169
|
-
return this.#exec('delete', key, true, opts, (signal)=>this.#driver.delete(this.#path(key), {
|
|
170
|
-
...opts,
|
|
171
|
-
signal
|
|
172
|
-
}));
|
|
173
|
-
}
|
|
174
|
-
async #deleteMany(keys, opts) {
|
|
175
|
-
const d = this.#driver;
|
|
176
|
-
if (d.deleteMany) {
|
|
177
|
-
const res = await this.#exec('deleteMany', undefined, true, opts, (signal)=>d.deleteMany(keys.map((k)=>this.#path(k)), {
|
|
178
|
-
...opts,
|
|
179
|
-
signal
|
|
180
|
-
}));
|
|
181
|
-
return {
|
|
182
|
-
deleted: res.deleted.map((k)=>this.#strip(k)),
|
|
183
|
-
errors: res.errors?.map((e)=>({
|
|
184
|
-
key: this.#strip(e.key),
|
|
185
|
-
error: e.error
|
|
186
|
-
}))
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
// Generic bounded fan-out fallback.
|
|
190
|
-
const concurrency = Math.max(1, opts?.concurrency ?? 8);
|
|
191
|
-
const stopOnError = opts?.stopOnError ?? false;
|
|
192
|
-
const deleted = [];
|
|
193
|
-
const errors = [];
|
|
194
|
-
let index = 0;
|
|
195
|
-
let stopped = false;
|
|
196
|
-
const worker = async ()=>{
|
|
197
|
-
while(index < keys.length && !stopped){
|
|
198
|
-
const key = keys[index++];
|
|
199
|
-
try {
|
|
200
|
-
await d.delete(this.#path(key), {
|
|
201
|
-
signal: opts?.signal
|
|
202
|
-
});
|
|
203
|
-
deleted.push(key);
|
|
204
|
-
} catch (e) {
|
|
205
|
-
const error = StorageError.wrap(e);
|
|
206
|
-
errors.push({
|
|
207
|
-
key,
|
|
208
|
-
error
|
|
209
|
-
});
|
|
210
|
-
if (stopOnError) {
|
|
211
|
-
stopped = true;
|
|
212
|
-
throw error;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
const runners = Array.from({
|
|
218
|
-
length: Math.min(concurrency, keys.length)
|
|
219
|
-
}, worker);
|
|
220
|
-
if (stopOnError) await Promise.all(runners);
|
|
221
|
-
else await Promise.allSettled(runners);
|
|
222
|
-
return {
|
|
223
|
-
deleted,
|
|
224
|
-
errors: errors.length ? errors : undefined
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
async copy(from, to, opts) {
|
|
228
|
-
this.#assertWritable();
|
|
229
|
-
return this.#exec('copy', to, true, opts, (signal)=>this.#driver.copy(this.#path(from), this.#path(to), {
|
|
230
|
-
...opts,
|
|
231
|
-
signal
|
|
232
|
-
}));
|
|
233
|
-
}
|
|
234
|
-
async move(from, to, opts) {
|
|
235
|
-
this.#assertWritable();
|
|
236
|
-
const f = this.#path(from);
|
|
237
|
-
const t = this.#path(to);
|
|
238
|
-
if (f === t) return;
|
|
239
|
-
if (this.#driver.move) {
|
|
240
|
-
await this.#exec('move', to, true, opts, (signal)=>this.#driver.move(f, t, {
|
|
241
|
-
...opts,
|
|
242
|
-
signal
|
|
243
|
-
}));
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
await this.#exec('move', to, true, opts, async (signal)=>{
|
|
247
|
-
await this.#driver.copy(f, t, {
|
|
248
|
-
...opts,
|
|
249
|
-
signal
|
|
250
|
-
});
|
|
251
|
-
await this.#driver.delete(f, {
|
|
252
|
-
...opts,
|
|
253
|
-
signal
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
// ---- list --------------------------------------------------------------
|
|
258
|
-
async list(opts) {
|
|
259
|
-
if (opts?.delimiter) this.#assertDelimiter();
|
|
260
|
-
const scopedPrefix = this.#listPrefix(opts?.prefix);
|
|
261
|
-
const res = await this.#exec('list', undefined, true, opts, (signal)=>this.#driver.list({
|
|
262
|
-
...opts,
|
|
263
|
-
prefix: scopedPrefix,
|
|
264
|
-
signal
|
|
265
|
-
}));
|
|
266
|
-
return {
|
|
267
|
-
items: res.items.map((f)=>this.#relabel(f, this.#strip(f.key))),
|
|
268
|
-
prefixes: res.prefixes?.map((p)=>this.#strip(p)),
|
|
269
|
-
cursor: res.cursor
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
async *listAll(opts) {
|
|
273
|
-
let cursor = opts?.cursor;
|
|
274
|
-
do {
|
|
275
|
-
const page = await this.list({
|
|
276
|
-
...opts,
|
|
277
|
-
cursor
|
|
278
|
-
});
|
|
279
|
-
for (const item of page.items)yield item;
|
|
280
|
-
cursor = page.cursor;
|
|
281
|
-
}while (cursor)
|
|
282
|
-
}
|
|
283
|
-
// ---- URLs / presign ----------------------------------------------------
|
|
284
|
-
async url(key, opts) {
|
|
285
|
-
if (!this.#driver.signedUrl?.supported) {
|
|
286
|
-
throw new StorageError('Unsupported', `${this.#driver.name}: url() not supported`);
|
|
287
|
-
}
|
|
288
|
-
return this.#exec('url', key, true, opts, (signal)=>this.#driver.url(this.#path(key), {
|
|
289
|
-
...opts,
|
|
290
|
-
signal
|
|
291
|
-
}));
|
|
292
|
-
}
|
|
293
|
-
async signedUploadUrl(key, opts) {
|
|
294
|
-
this.#assertWritable();
|
|
295
|
-
if (!this.#driver.signedUrl?.upload) {
|
|
296
|
-
throw new StorageError('Unsupported', `${this.#driver.name}: signed uploads not supported`);
|
|
297
|
-
}
|
|
298
|
-
return this.#exec('signedUploadUrl', key, true, opts, (signal)=>this.#driver.signedUploadUrl(this.#path(key), {
|
|
299
|
-
...opts,
|
|
300
|
-
signal
|
|
301
|
-
}));
|
|
302
|
-
}
|
|
303
|
-
/** Prefix-bound view of the driver's presigned-multipart capability. */ get signedMultipart() {
|
|
304
|
-
const cap = this.#driver.signedMultipart;
|
|
305
|
-
if (!cap) return undefined;
|
|
306
|
-
const path = (k)=>this.#path(k);
|
|
307
|
-
return {
|
|
308
|
-
create: (key, o)=>cap.create(path(key), o),
|
|
309
|
-
signPart: (key, uploadId, partNumber, o)=>cap.signPart(path(key), uploadId, partNumber, o),
|
|
310
|
-
complete: (key, uploadId, parts)=>cap.complete(path(key), uploadId, parts),
|
|
311
|
-
abort: (key, uploadId)=>cap.abort(path(key), uploadId)
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
// ---- low-level multipart ----------------------------------------------
|
|
315
|
-
async createMultipartUpload(key, opts) {
|
|
316
|
-
this.#assertWritable();
|
|
317
|
-
if (typeof this.#driver.createMultipartUpload !== 'function') {
|
|
318
|
-
throw new StorageError('Unsupported', `${this.#driver.name}: multipart not supported`);
|
|
319
|
-
}
|
|
320
|
-
return this.#driver.createMultipartUpload(this.#path(key), opts);
|
|
321
|
-
}
|
|
322
|
-
resumeMultipartUpload(key, uploadId) {
|
|
323
|
-
this.#assertWritable();
|
|
324
|
-
if (typeof this.#driver.resumeMultipartUpload !== 'function') {
|
|
325
|
-
throw new StorageError('Unsupported', `${this.#driver.name}: multipart resume not supported`);
|
|
326
|
-
}
|
|
327
|
-
return this.#driver.resumeMultipartUpload(this.#path(key), uploadId);
|
|
328
|
-
}
|
|
329
|
-
// ---- ergonomics --------------------------------------------------------
|
|
330
|
-
/** A key-bound handle over the common single-object operations. */ file(key) {
|
|
331
|
-
return {
|
|
332
|
-
key,
|
|
333
|
-
upload: (body, o)=>this.upload(key, body, o),
|
|
334
|
-
download: (o)=>this.download(key, o),
|
|
335
|
-
head: (o)=>this.head(key, o),
|
|
336
|
-
exists: (o)=>this.exists(key, o),
|
|
337
|
-
delete: (o)=>this.delete(key, o),
|
|
338
|
-
url: (o)=>this.url(key, o),
|
|
339
|
-
signedUploadUrl: (o)=>this.signedUploadUrl(key, o)
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
/** A read-only clone of this handle (mutations throw `ReadOnly`). */ readonly() {
|
|
343
|
-
return new Storage({
|
|
344
|
-
...this.#opts,
|
|
345
|
-
readonly: true
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
// ---- internals ---------------------------------------------------------
|
|
349
|
-
#path(key) {
|
|
350
|
-
return joinKey(this.#prefix, sanitizeKey(key));
|
|
351
|
-
}
|
|
352
|
-
#strip(storedKey) {
|
|
353
|
-
return stripPrefix(this.#prefix, storedKey);
|
|
354
|
-
}
|
|
355
|
-
#listPrefix(prefix) {
|
|
356
|
-
if (!this.#prefix) return prefix;
|
|
357
|
-
return prefix ? `${this.#prefix}/${prefix}` : `${this.#prefix}/`;
|
|
358
|
-
}
|
|
359
|
-
#relabel(file, logicalKey) {
|
|
360
|
-
if (file.key === logicalKey) return file;
|
|
361
|
-
const name = logicalKey.slice(logicalKey.lastIndexOf('/') + 1);
|
|
362
|
-
return {
|
|
363
|
-
key: logicalKey,
|
|
364
|
-
name,
|
|
365
|
-
size: file.size,
|
|
366
|
-
type: file.type,
|
|
367
|
-
lastModified: file.lastModified,
|
|
368
|
-
etag: file.etag,
|
|
369
|
-
metadata: file.metadata,
|
|
370
|
-
arrayBuffer: ()=>file.arrayBuffer(),
|
|
371
|
-
text: ()=>file.text(),
|
|
372
|
-
blob: ()=>file.blob(),
|
|
373
|
-
stream: ()=>file.stream()
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
#assertWritable() {
|
|
377
|
-
if (this.#readonly) throw new StorageError('ReadOnly', 'storage handle is read-only');
|
|
378
|
-
}
|
|
379
|
-
#assertUploadSupported(opts) {
|
|
380
|
-
const d = this.#driver;
|
|
381
|
-
if (opts?.metadata && Object.keys(opts.metadata).length > 0 && !d.supportsMetadata) {
|
|
382
|
-
throw new StorageError('Unsupported', `${d.name}: metadata not supported`);
|
|
383
|
-
}
|
|
384
|
-
if (opts?.cacheControl && !d.supportsCacheControl) {
|
|
385
|
-
throw new StorageError('Unsupported', `${d.name}: cacheControl not supported`);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
#assertRange() {
|
|
389
|
-
if (!this.#driver.supportsRange) {
|
|
390
|
-
throw new StorageError('Unsupported', `${this.#driver.name}: range reads not supported`);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
#assertDelimiter() {
|
|
394
|
-
if (!this.#driver.supportsDelimiter) {
|
|
395
|
-
throw new StorageError('Unsupported', `${this.#driver.name}: delimiter not supported`);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
async #exec(type, key, replayable, opts, fn) {
|
|
399
|
-
const started = Date.now();
|
|
400
|
-
const retries = replayable ? opts?.retries ?? this.#opts.retries : 0;
|
|
401
|
-
const maxRetries = normalizeRetry(retries).max;
|
|
402
|
-
const hooks = this.#opts.hooks;
|
|
403
|
-
try {
|
|
404
|
-
const result = await runWithRetry(fn, {
|
|
405
|
-
retries,
|
|
406
|
-
timeout: opts?.timeout ?? this.#opts.timeout,
|
|
407
|
-
signal: opts?.signal ?? this.#opts.signal
|
|
408
|
-
}, (info)=>hooks?.onRetry?.({
|
|
409
|
-
type,
|
|
410
|
-
key,
|
|
411
|
-
attempt: info.attempt,
|
|
412
|
-
maxRetries,
|
|
413
|
-
delayMs: info.delayMs,
|
|
414
|
-
error: info.error
|
|
415
|
-
}));
|
|
416
|
-
hooks?.onOperation?.({
|
|
417
|
-
type,
|
|
418
|
-
key,
|
|
419
|
-
status: 'success',
|
|
420
|
-
durationMs: Date.now() - started
|
|
421
|
-
});
|
|
422
|
-
return result;
|
|
423
|
-
} catch (e) {
|
|
424
|
-
const error = StorageError.wrap(e);
|
|
425
|
-
hooks?.onError?.({
|
|
426
|
-
type,
|
|
427
|
-
key,
|
|
428
|
-
error
|
|
429
|
-
});
|
|
430
|
-
hooks?.onOperation?.({
|
|
431
|
-
type,
|
|
432
|
-
key,
|
|
433
|
-
status: 'error',
|
|
434
|
-
durationMs: Date.now() - started,
|
|
435
|
-
error
|
|
436
|
-
});
|
|
437
|
-
throw error;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
export function createStorage(opts) {
|
|
442
|
-
return new Storage(opts);
|
|
443
|
-
}
|
package/dist/storage.module.d.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { type DynamicModule, type InferTokens, type Token } from '@velajs/vela';
|
|
2
|
-
import type { StorageDriver, StorageHooks } from './storage.types';
|
|
3
|
-
import type { StorageAuthorizer } from './http/authorizer.types';
|
|
4
|
-
/** Options for mounting the HTTP upload/download controller for a bucket. */
|
|
5
|
-
export interface StorageHttpOptions {
|
|
6
|
-
/** Mount path (relative to vela's globalPrefix). Default `/api/storage`. */
|
|
7
|
-
basePath?: string;
|
|
8
|
-
/** Fine-grained per-action authorizer (deny / allow / allow-with-overrides). */
|
|
9
|
-
authorize?: StorageAuthorizer;
|
|
10
|
-
/** Behavior when no `authorize` is set. Default `'deny'`. */
|
|
11
|
-
defaultPolicy?: 'deny' | 'allow';
|
|
12
|
-
/** `redirect` to a signed URL (default) or `proxy` bytes through the Worker. */
|
|
13
|
-
download?: 'redirect' | 'proxy';
|
|
14
|
-
/** Set `false` to configure options without mounting the controller. Default mounts. */
|
|
15
|
-
mountController?: boolean;
|
|
16
|
-
defaultExpiresIn?: number;
|
|
17
|
-
maxExpiresIn?: number;
|
|
18
|
-
maxUploadSize?: number;
|
|
19
|
-
maxListLimit?: number;
|
|
20
|
-
deleteConcurrency?: number;
|
|
21
|
-
}
|
|
22
|
-
export interface StorageModuleOptions {
|
|
23
|
-
driver: StorageDriver;
|
|
24
|
-
name?: string;
|
|
25
|
-
prefix?: string;
|
|
26
|
-
readonly?: boolean;
|
|
27
|
-
hooks?: StorageHooks;
|
|
28
|
-
http?: StorageHttpOptions;
|
|
29
|
-
}
|
|
30
|
-
export interface StorageModuleAsyncOptions<Inject extends readonly Token<unknown>[] = readonly Token<unknown>[]> {
|
|
31
|
-
inject?: Inject;
|
|
32
|
-
imports?: DynamicModule['imports'];
|
|
33
|
-
useFactory: (...deps: InferTokens<Inject>) => StorageDriver;
|
|
34
|
-
name?: string;
|
|
35
|
-
prefix?: string;
|
|
36
|
-
readonly?: boolean;
|
|
37
|
-
hooks?: StorageHooks;
|
|
38
|
-
http?: StorageHttpOptions;
|
|
39
|
-
key?: string;
|
|
40
|
-
}
|
|
41
|
-
export declare class StorageModule {
|
|
42
|
-
/** Synchronous registration — the driver is provided directly. */
|
|
43
|
-
static forRoot(options: StorageModuleOptions): DynamicModule;
|
|
44
|
-
/** Deferred / DI-driven registration — `useFactory` runs lazily on first use. */
|
|
45
|
-
static forRootAsync<const Inject extends readonly Token<unknown>[] = readonly Token<unknown>[]>(options: StorageModuleAsyncOptions<Inject>): DynamicModule;
|
|
46
|
-
}
|
package/dist/storage.module.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { defineModule, lazyProvider, stableHash } from "@velajs/vela";
|
|
2
|
-
import { createStorageController } from "./storage.controller.js";
|
|
3
|
-
import { StorageService } from "./storage.service.js";
|
|
4
|
-
import { DEFAULT_STORAGE_NAME, storageDriverBuilder, storageToken } from "./storage.tokens.js";
|
|
5
|
-
/**
|
|
6
|
-
* The stable, identifying subset of the HTTP options folded into the instance
|
|
7
|
-
* key — matches what `buildControllers` bakes into the mounted controller so
|
|
8
|
-
* two registrations that differ only in HTTP behavior get distinct keys.
|
|
9
|
-
*/ function httpKeyPart(http) {
|
|
10
|
-
if (!http || http.mountController === false) return {};
|
|
11
|
-
return {
|
|
12
|
-
basePath: http.basePath ?? '/api/storage',
|
|
13
|
-
download: http.download ?? 'redirect',
|
|
14
|
-
defaultPolicy: http.defaultPolicy ?? 'deny'
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
function buildControllers(name, serviceToken, http) {
|
|
18
|
-
if (!http || http.mountController === false) return [];
|
|
19
|
-
const resolved = {
|
|
20
|
-
driverName: name,
|
|
21
|
-
authorize: http.authorize,
|
|
22
|
-
defaultPolicy: http.defaultPolicy ?? 'deny',
|
|
23
|
-
download: http.download ?? 'redirect',
|
|
24
|
-
defaultExpiresIn: http.defaultExpiresIn ?? 900,
|
|
25
|
-
maxExpiresIn: http.maxExpiresIn ?? 3600,
|
|
26
|
-
maxUploadSize: http.maxUploadSize,
|
|
27
|
-
maxListLimit: http.maxListLimit ?? 1000,
|
|
28
|
-
deleteConcurrency: http.deleteConcurrency ?? 8
|
|
29
|
-
};
|
|
30
|
-
const basePath = http.basePath ?? '/api/storage';
|
|
31
|
-
return [
|
|
32
|
-
createStorageController(basePath, serviceToken, resolved)
|
|
33
|
-
];
|
|
34
|
-
}
|
|
35
|
-
const { ConfigurableModuleClass } = defineModule({
|
|
36
|
-
name: 'Storage',
|
|
37
|
-
// Value-shaped identity only: never the (stateful) driver thunk. Same
|
|
38
|
-
// name+prefix+readonly+hooks+http dedups (HMR-idempotent); a distinct bucket
|
|
39
|
-
// name coexists.
|
|
40
|
-
key: (o)=>stableHash({
|
|
41
|
-
name: o.name ?? DEFAULT_STORAGE_NAME,
|
|
42
|
-
prefix: o.prefix ?? '',
|
|
43
|
-
readonly: !!o.readonly,
|
|
44
|
-
hooks: !!o.hooks,
|
|
45
|
-
...httpKeyPart(o.http)
|
|
46
|
-
}),
|
|
47
|
-
setup: ({ OPTIONS, options })=>{
|
|
48
|
-
const name = options.name ?? DEFAULT_STORAGE_NAME;
|
|
49
|
-
const builderToken = storageDriverBuilder(name);
|
|
50
|
-
const serviceToken = name === DEFAULT_STORAGE_NAME ? StorageService : storageToken(name);
|
|
51
|
-
const svcOptions = {
|
|
52
|
-
name,
|
|
53
|
-
prefix: options.prefix,
|
|
54
|
-
readonly: options.readonly,
|
|
55
|
-
hooks: options.hooks
|
|
56
|
-
};
|
|
57
|
-
return {
|
|
58
|
-
providers: [
|
|
59
|
-
// Deferred, memoized driver construction — the seam that keeps
|
|
60
|
-
// Cloudflare bindings / `c.env` reads out of module load.
|
|
61
|
-
lazyProvider({
|
|
62
|
-
provide: builderToken,
|
|
63
|
-
inject: [
|
|
64
|
-
OPTIONS
|
|
65
|
-
],
|
|
66
|
-
useFactory: (o)=>o.driver()
|
|
67
|
-
}),
|
|
68
|
-
{
|
|
69
|
-
provide: serviceToken,
|
|
70
|
-
useFactory: (build)=>new StorageService(build, svcOptions),
|
|
71
|
-
inject: [
|
|
72
|
-
builderToken
|
|
73
|
-
]
|
|
74
|
-
}
|
|
75
|
-
],
|
|
76
|
-
controllers: buildControllers(name, serviceToken, options.http),
|
|
77
|
-
exports: [
|
|
78
|
-
serviceToken,
|
|
79
|
-
builderToken
|
|
80
|
-
]
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
export class StorageModule {
|
|
85
|
-
/** Synchronous registration — the driver is provided directly. */ static forRoot(options) {
|
|
86
|
-
const { driver, ...rest } = options;
|
|
87
|
-
return {
|
|
88
|
-
...ConfigurableModuleClass.forRoot({
|
|
89
|
-
...rest,
|
|
90
|
-
driver: ()=>driver
|
|
91
|
-
}),
|
|
92
|
-
module: StorageModule
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
/** Deferred / DI-driven registration — `useFactory` runs lazily on first use. */ static forRootAsync(options) {
|
|
96
|
-
const { useFactory, inject, imports, key, ...structural } = options;
|
|
97
|
-
return {
|
|
98
|
-
...ConfigurableModuleClass.forRootAsync({
|
|
99
|
-
imports,
|
|
100
|
-
inject,
|
|
101
|
-
key,
|
|
102
|
-
...structural,
|
|
103
|
-
// Wrap the caller's driver factory in a thunk so resolving the options
|
|
104
|
-
// token (at bootstrap) does NOT build the driver — only the first
|
|
105
|
-
// storage operation does.
|
|
106
|
-
useFactory: (...deps)=>({
|
|
107
|
-
driver: ()=>useFactory(...deps)
|
|
108
|
-
})
|
|
109
|
-
}),
|
|
110
|
-
module: StorageModule
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { Storage } from './storage.facade';
|
|
2
|
-
import type { Body, DeleteManyOptions, DeleteManyResult, DownloadOptions, ListOptions, ListResult, OperationOptions, SignedUpload, SignUploadOptions, StorageDriver, StorageHooks, StoredFile, UploadOptions, UploadResult, UrlOptions } from './storage.types';
|
|
3
|
-
/**
|
|
4
|
-
* Build the real driver on first property access and delegate to it. This is
|
|
5
|
-
* the edge-binding-safe seam: the driver factory (which may read Cloudflare
|
|
6
|
-
* bindings / `c.env` secrets) does not run at module load — only on the first
|
|
7
|
-
* storage operation, which happens at request time.
|
|
8
|
-
*
|
|
9
|
-
* Functions are bound to the built driver so class-based drivers keep `this`.
|
|
10
|
-
*/
|
|
11
|
-
export declare function lazyDriver(build: () => StorageDriver): StorageDriver;
|
|
12
|
-
export interface StorageServiceOptions {
|
|
13
|
-
name: string;
|
|
14
|
-
prefix?: string;
|
|
15
|
-
readonly?: boolean;
|
|
16
|
-
hooks?: StorageHooks;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* The injectable consumers reach for. Wraps one bucket's {@link Storage}
|
|
20
|
-
* facade (lazy driver construction), plus thin forwarders for the common
|
|
21
|
-
* single-object operations so `service.upload(...)` works without reaching
|
|
22
|
-
* through `.storage`.
|
|
23
|
-
*
|
|
24
|
-
* For multiple buckets, register `StorageModule.forRoot({ name })` per bucket
|
|
25
|
-
* and inject with `@InjectStorage(name)`.
|
|
26
|
-
*/
|
|
27
|
-
export declare class StorageService {
|
|
28
|
-
readonly storage: Storage;
|
|
29
|
-
readonly name: string;
|
|
30
|
-
constructor(build: () => StorageDriver, options: StorageServiceOptions);
|
|
31
|
-
upload(key: string, body: Body, opts?: UploadOptions): Promise<UploadResult>;
|
|
32
|
-
download(key: string, opts?: DownloadOptions): Promise<StoredFile>;
|
|
33
|
-
head(key: string, opts?: OperationOptions): Promise<StoredFile>;
|
|
34
|
-
exists(key: string, opts?: OperationOptions): Promise<boolean>;
|
|
35
|
-
delete(key: string, opts?: OperationOptions): Promise<void>;
|
|
36
|
-
deleteMany(keys: string[], opts?: DeleteManyOptions): Promise<DeleteManyResult>;
|
|
37
|
-
copy(from: string, to: string, opts?: OperationOptions): Promise<void>;
|
|
38
|
-
move(from: string, to: string, opts?: OperationOptions): Promise<void>;
|
|
39
|
-
list(opts?: ListOptions): Promise<ListResult>;
|
|
40
|
-
url(key: string, opts?: UrlOptions): Promise<string>;
|
|
41
|
-
signedUploadUrl(key: string, opts: SignUploadOptions): Promise<SignedUpload>;
|
|
42
|
-
}
|