@velajs/storage 0.2.0
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 +14 -0
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/base64.d.ts +8 -0
- package/dist/base64.js +25 -0
- package/dist/client/index.d.ts +50 -0
- package/dist/client/index.js +243 -0
- package/dist/decorators/inject-storage.decorator.d.ts +17 -0
- package/dist/decorators/inject-storage.decorator.js +21 -0
- package/dist/drivers/memory/index.d.ts +16 -0
- package/dist/drivers/memory/index.js +202 -0
- package/dist/drivers/r2/index.d.ts +11 -0
- package/dist/drivers/r2/index.js +166 -0
- package/dist/drivers/r2/r2.types.d.ts +67 -0
- package/dist/drivers/r2/r2.types.js +5 -0
- package/dist/drivers/r2-http/index.d.ts +33 -0
- package/dist/drivers/r2-http/index.js +52 -0
- package/dist/drivers/s3/index.d.ts +5 -0
- package/dist/drivers/s3/index.js +3 -0
- package/dist/drivers/s3/s3-client.d.ts +33 -0
- package/dist/drivers/s3/s3-client.js +154 -0
- package/dist/drivers/s3/s3.driver.d.ts +4 -0
- package/dist/drivers/s3/s3.driver.js +404 -0
- package/dist/drivers/s3/s3.types.d.ts +28 -0
- package/dist/drivers/s3/s3.types.js +1 -0
- package/dist/drivers/s3/xml.d.ts +6 -0
- package/dist/drivers/s3/xml.js +52 -0
- package/dist/http/authorizer.types.d.ts +59 -0
- package/dist/http/authorizer.types.js +1 -0
- package/dist/http/http-helpers.d.ts +22 -0
- package/dist/http/http-helpers.js +80 -0
- package/dist/http/protocol.types.d.ts +76 -0
- package/dist/http/protocol.types.js +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +16 -0
- package/dist/internal/body.d.ts +18 -0
- package/dist/internal/body.js +90 -0
- package/dist/internal/retry.d.ts +41 -0
- package/dist/internal/retry.js +127 -0
- package/dist/internal/stored-file.d.ts +27 -0
- package/dist/internal/stored-file.js +114 -0
- package/dist/middleware/cache.d.ts +50 -0
- package/dist/middleware/cache.js +101 -0
- package/dist/middleware/compose.d.ts +12 -0
- package/dist/middleware/compose.js +11 -0
- package/dist/middleware/compression.d.ts +16 -0
- package/dist/middleware/compression.js +101 -0
- package/dist/middleware/encryption.d.ts +14 -0
- package/dist/middleware/encryption.js +134 -0
- package/dist/middleware/failover.d.ts +18 -0
- package/dist/middleware/failover.js +52 -0
- package/dist/middleware/index.d.ts +15 -0
- package/dist/middleware/index.js +8 -0
- package/dist/middleware/retry.d.ts +14 -0
- package/dist/middleware/retry.js +47 -0
- package/dist/middleware/versioning.d.ts +36 -0
- package/dist/middleware/versioning.js +89 -0
- package/dist/middleware/wrap.d.ts +11 -0
- package/dist/middleware/wrap.js +46 -0
- package/dist/object-key.d.ts +17 -0
- package/dist/object-key.js +42 -0
- package/dist/storage.controller.d.ts +21 -0
- package/dist/storage.controller.js +380 -0
- package/dist/storage.error.d.ts +28 -0
- package/dist/storage.error.js +46 -0
- package/dist/storage.facade.d.ts +47 -0
- package/dist/storage.facade.js +443 -0
- package/dist/storage.module.d.ts +46 -0
- package/dist/storage.module.js +113 -0
- package/dist/storage.service.d.ts +42 -0
- package/dist/storage.service.js +86 -0
- package/dist/storage.tokens.d.ts +13 -0
- package/dist/storage.tokens.js +26 -0
- package/dist/storage.types.d.ts +245 -0
- package/dist/storage.types.js +1 -0
- package/dist/storagesdk/index.d.ts +85 -0
- package/dist/storagesdk/index.js +136 -0
- package/package.json +116 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { StorageError } from "../storage.error.js";
|
|
2
|
+
import { passthrough } from "./wrap.js";
|
|
3
|
+
export const VERSIONED = Symbol.for('vela.storage.versioned');
|
|
4
|
+
/**
|
|
5
|
+
* Keeps prior versions on overwrite via key-suffixing (no external manifest →
|
|
6
|
+
* no read-modify-write race). Snapshots are cheap when the base driver
|
|
7
|
+
* `supportsServerSideCopy`. Direct presigned uploads bypass snapshotting, so
|
|
8
|
+
* `signedUploadUrl` throws. Reach `listVersions`/`restore`/`deleteVersion` via
|
|
9
|
+
* {@link unwrapVersioning}.
|
|
10
|
+
*/ export function versioning(opts = {}) {
|
|
11
|
+
const prefix = opts.prefix ?? '.vela-versions/';
|
|
12
|
+
let seq = 0;
|
|
13
|
+
const vid = opts.versionId ?? (()=>`${Date.now().toString(36)}-${(seq++).toString(36)}`);
|
|
14
|
+
const vkey = (key, id)=>`${prefix}${key}/${id}`;
|
|
15
|
+
const isVersion = (key)=>key.startsWith(prefix);
|
|
16
|
+
const listVersions = async (inner, key)=>{
|
|
17
|
+
const res = await inner.list({
|
|
18
|
+
prefix: `${prefix}${key}/`
|
|
19
|
+
});
|
|
20
|
+
return res.items.map((item)=>({
|
|
21
|
+
versionId: item.key.slice(item.key.lastIndexOf('/') + 1),
|
|
22
|
+
key: item.key,
|
|
23
|
+
size: item.size,
|
|
24
|
+
lastModified: item.lastModified
|
|
25
|
+
}));
|
|
26
|
+
};
|
|
27
|
+
const snapshot = async (inner, key)=>{
|
|
28
|
+
if (!await inner.exists(key)) return;
|
|
29
|
+
const id = vid();
|
|
30
|
+
if (inner.supportsServerSideCopy) {
|
|
31
|
+
await inner.copy(key, vkey(key, id));
|
|
32
|
+
} else {
|
|
33
|
+
const current = await inner.download(key);
|
|
34
|
+
await inner.upload(vkey(key, id), current.stream(), {
|
|
35
|
+
contentType: current.type
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (opts.maxVersions) {
|
|
39
|
+
const versions = (await listVersions(inner, key)).sort((a, b)=>a.versionId < b.versionId ? -1 : 1);
|
|
40
|
+
for (const v of versions.slice(0, Math.max(0, versions.length - opts.maxVersions))){
|
|
41
|
+
await inner.delete(v.key);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
return (inner)=>{
|
|
46
|
+
const driver = passthrough(inner, {
|
|
47
|
+
async upload (k, b, o) {
|
|
48
|
+
await snapshot(inner, k);
|
|
49
|
+
return inner.upload(k, b, o);
|
|
50
|
+
},
|
|
51
|
+
async copy (f, t, o) {
|
|
52
|
+
await snapshot(inner, t);
|
|
53
|
+
return inner.copy(f, t, o);
|
|
54
|
+
},
|
|
55
|
+
async delete (k, o) {
|
|
56
|
+
if (opts.onDelete !== 'purge') await snapshot(inner, k);
|
|
57
|
+
await inner.delete(k, o);
|
|
58
|
+
if (opts.onDelete === 'purge') {
|
|
59
|
+
for (const v of (await listVersions(inner, k)))await inner.delete(v.key);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
list: opts.hideFromList === false ? undefined : async (o)=>{
|
|
63
|
+
const res = await inner.list(o);
|
|
64
|
+
return {
|
|
65
|
+
items: res.items.filter((i)=>!isVersion(i.key)),
|
|
66
|
+
prefixes: res.prefixes?.filter((p)=>!isVersion(p)),
|
|
67
|
+
cursor: res.cursor
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
signedUploadUrl () {
|
|
71
|
+
throw new StorageError('Unsupported', 'versioning: direct uploads bypass snapshots');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
const versioned = {
|
|
75
|
+
listVersions: (key)=>listVersions(inner, key),
|
|
76
|
+
async restore (key, versionId) {
|
|
77
|
+
await snapshot(inner, key);
|
|
78
|
+
await inner.copy(vkey(key, versionId), key);
|
|
79
|
+
},
|
|
80
|
+
deleteVersion: (key, versionId)=>inner.delete(vkey(key, versionId))
|
|
81
|
+
};
|
|
82
|
+
return Object.assign(driver, {
|
|
83
|
+
[VERSIONED]: versioned
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** Retrieve the {@link Versioned} surface from a versioning-wrapped driver (or `undefined`). */ export function unwrapVersioning(driver) {
|
|
88
|
+
return driver[VERSIONED];
|
|
89
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { StorageDriver } from '../storage.types';
|
|
2
|
+
/** A driver middleware: wraps a driver, returns a driver. */
|
|
3
|
+
export type Middleware = (inner: StorageDriver) => StorageDriver;
|
|
4
|
+
/**
|
|
5
|
+
* Build a driver that delegates to `inner`, applying `over` overrides and
|
|
6
|
+
* omitting the methods in `omit`. Explicit delegation (not spread) keeps
|
|
7
|
+
* class-based drivers' `this`. Optional methods are forwarded only when the
|
|
8
|
+
* inner driver has them (so `presence == capability` is preserved), unless
|
|
9
|
+
* omitted (e.g. a body-transforming middleware suppresses multipart/presign).
|
|
10
|
+
*/
|
|
11
|
+
export declare function passthrough(inner: StorageDriver, over?: Partial<StorageDriver>, omit?: ReadonlyArray<keyof StorageDriver>): StorageDriver;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a driver that delegates to `inner`, applying `over` overrides and
|
|
3
|
+
* omitting the methods in `omit`. Explicit delegation (not spread) keeps
|
|
4
|
+
* class-based drivers' `this`. Optional methods are forwarded only when the
|
|
5
|
+
* inner driver has them (so `presence == capability` is preserved), unless
|
|
6
|
+
* omitted (e.g. a body-transforming middleware suppresses multipart/presign).
|
|
7
|
+
*/ export function passthrough(inner, over = {}, omit = []) {
|
|
8
|
+
const omitted = new Set(omit);
|
|
9
|
+
const has = (k)=>!omitted.has(k);
|
|
10
|
+
const d = {
|
|
11
|
+
name: over.name ?? inner.name,
|
|
12
|
+
raw: inner.raw,
|
|
13
|
+
reportsUploadProgress: over.reportsUploadProgress ?? inner.reportsUploadProgress,
|
|
14
|
+
supportsRange: over.supportsRange ?? inner.supportsRange,
|
|
15
|
+
supportsDelimiter: over.supportsDelimiter ?? inner.supportsDelimiter,
|
|
16
|
+
supportsMetadata: over.supportsMetadata ?? inner.supportsMetadata,
|
|
17
|
+
supportsCacheControl: over.supportsCacheControl ?? inner.supportsCacheControl,
|
|
18
|
+
supportsServerSideCopy: over.supportsServerSideCopy ?? inner.supportsServerSideCopy,
|
|
19
|
+
signedUrl: over.signedUrl ?? inner.signedUrl,
|
|
20
|
+
upload: over.upload ?? ((k, b, o)=>inner.upload(k, b, o)),
|
|
21
|
+
download: over.download ?? ((k, o)=>inner.download(k, o)),
|
|
22
|
+
head: over.head ?? ((k, o)=>inner.head(k, o)),
|
|
23
|
+
exists: over.exists ?? ((k, o)=>inner.exists(k, o)),
|
|
24
|
+
delete: over.delete ?? ((k, o)=>inner.delete(k, o)),
|
|
25
|
+
copy: over.copy ?? ((f, t, o)=>inner.copy(f, t, o)),
|
|
26
|
+
list: over.list ?? ((o)=>inner.list(o)),
|
|
27
|
+
url: over.url ?? ((k, o)=>inner.url(k, o)),
|
|
28
|
+
signedUploadUrl: over.signedUploadUrl ?? ((k, o)=>inner.signedUploadUrl(k, o))
|
|
29
|
+
};
|
|
30
|
+
if (has('deleteMany') && (over.deleteMany || inner.deleteMany)) {
|
|
31
|
+
d.deleteMany = over.deleteMany ?? ((ks, o)=>inner.deleteMany(ks, o));
|
|
32
|
+
}
|
|
33
|
+
if (has('move') && (over.move || inner.move)) {
|
|
34
|
+
d.move = over.move ?? ((f, t, o)=>inner.move(f, t, o));
|
|
35
|
+
}
|
|
36
|
+
if (has('createMultipartUpload') && (over.createMultipartUpload || inner.createMultipartUpload)) {
|
|
37
|
+
d.createMultipartUpload = over.createMultipartUpload ?? ((k, o)=>inner.createMultipartUpload(k, o));
|
|
38
|
+
}
|
|
39
|
+
if (has('resumeMultipartUpload') && (over.resumeMultipartUpload || inner.resumeMultipartUpload)) {
|
|
40
|
+
d.resumeMultipartUpload = over.resumeMultipartUpload ?? ((k, u)=>inner.resumeMultipartUpload(k, u));
|
|
41
|
+
}
|
|
42
|
+
if (has('signedMultipart') && (over.signedMultipart || inner.signedMultipart)) {
|
|
43
|
+
d.signedMultipart = over.signedMultipart ?? inner.signedMultipart;
|
|
44
|
+
}
|
|
45
|
+
return d;
|
|
46
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when `key` is a safe object key:
|
|
3
|
+
* - non-empty, not longer than 1024 bytes (S3's limit)
|
|
4
|
+
* - no leading `/`
|
|
5
|
+
* - no `.` / `..` path segments (traversal)
|
|
6
|
+
* - no control characters
|
|
7
|
+
* - no backslashes (Windows-style separators)
|
|
8
|
+
*/
|
|
9
|
+
export declare function isSafeKey(key: string): boolean;
|
|
10
|
+
/** Return `key` if safe, else throw `StorageError('InvalidKey')`. */
|
|
11
|
+
export declare function sanitizeKey(key: string): string;
|
|
12
|
+
/** Normalize a prefix: drop leading/trailing slashes, collapse to '' when empty. */
|
|
13
|
+
export declare function normalizePrefix(prefix: string | undefined): string;
|
|
14
|
+
/** Join a normalized prefix and a key with a single '/'. */
|
|
15
|
+
export declare function joinKey(prefix: string, key: string): string;
|
|
16
|
+
/** Strip a normalized prefix from a stored key (for surfacing to callers). */
|
|
17
|
+
export declare function stripPrefix(prefix: string, key: string): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { StorageError } from "./storage.error.js";
|
|
2
|
+
// Object-key hygiene shared by the facade (prefix scoping) and the HTTP
|
|
3
|
+
// controller (post-authorize re-sanitization). The rules block the classic
|
|
4
|
+
// traversal / injection footguns without being so strict that legitimate
|
|
5
|
+
// keys (nested "folders", dots, spaces) are rejected.
|
|
6
|
+
// Control characters: C0 range (0x00-0x1F) plus DEL (0x7F).
|
|
7
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
8
|
+
/**
|
|
9
|
+
* True when `key` is a safe object key:
|
|
10
|
+
* - non-empty, not longer than 1024 bytes (S3's limit)
|
|
11
|
+
* - no leading `/`
|
|
12
|
+
* - no `.` / `..` path segments (traversal)
|
|
13
|
+
* - no control characters
|
|
14
|
+
* - no backslashes (Windows-style separators)
|
|
15
|
+
*/ export 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('/')){
|
|
21
|
+
if (segment === '.' || segment === '..') return false;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
/** Return `key` if safe, else throw `StorageError('InvalidKey')`. */ export function sanitizeKey(key) {
|
|
26
|
+
if (!isSafeKey(key)) {
|
|
27
|
+
throw new StorageError('InvalidKey', `unsafe object key: ${JSON.stringify(key)}`);
|
|
28
|
+
}
|
|
29
|
+
return key;
|
|
30
|
+
}
|
|
31
|
+
/** Normalize a prefix: drop leading/trailing slashes, collapse to '' when empty. */ export function normalizePrefix(prefix) {
|
|
32
|
+
if (!prefix) return '';
|
|
33
|
+
return prefix.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
34
|
+
}
|
|
35
|
+
/** Join a normalized prefix and a key with a single '/'. */ export function joinKey(prefix, key) {
|
|
36
|
+
return prefix ? `${prefix}/${key}` : key;
|
|
37
|
+
}
|
|
38
|
+
/** Strip a normalized prefix from a stored key (for surfacing to callers). */ export function stripPrefix(prefix, key) {
|
|
39
|
+
if (!prefix) return key;
|
|
40
|
+
const withSlash = `${prefix}/`;
|
|
41
|
+
return key.startsWith(withSlash) ? key.slice(withSlash.length) : key;
|
|
42
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Token, type Type } from '@velajs/vela';
|
|
2
|
+
import type { StorageService } from './storage.service';
|
|
3
|
+
import type { StorageAuthorizer } from './http/authorizer.types';
|
|
4
|
+
export interface ResolvedHttpOptions {
|
|
5
|
+
driverName: string;
|
|
6
|
+
authorize?: StorageAuthorizer;
|
|
7
|
+
defaultPolicy: 'deny' | 'allow';
|
|
8
|
+
download: 'redirect' | 'proxy';
|
|
9
|
+
defaultExpiresIn: number;
|
|
10
|
+
maxExpiresIn: number;
|
|
11
|
+
maxUploadSize?: number;
|
|
12
|
+
maxListLimit: number;
|
|
13
|
+
deleteConcurrency: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Build a vela controller exposing presigned/multipart upload + download
|
|
17
|
+
* endpoints for one bucket. Factory-decorated (like better-auth's catch-all)
|
|
18
|
+
* so `basePath` bakes in at decoration time. Consumes the {@link StorageService}
|
|
19
|
+
* facade so gating/retry/prefix apply uniformly on the HTTP path.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createStorageController(basePath: string, serviceToken: Token<StorageService>, http: ResolvedHttpOptions): Type;
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
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;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
function _ts_metadata(k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
}
|
|
10
|
+
function _ts_param(paramIndex, decorator) {
|
|
11
|
+
return function(target, key) {
|
|
12
|
+
decorator(target, key, paramIndex);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
import { Controller, Get, Inject, Injectable, Post, Req } from "@velajs/vela";
|
|
16
|
+
import { sanitizeKey } from "./object-key.js";
|
|
17
|
+
import { StorageError } from "./storage.error.js";
|
|
18
|
+
import { clampExpiry, dispositionHeader, mapError, parseRange, serializeStored } from "./http/http-helpers.js";
|
|
19
|
+
/**
|
|
20
|
+
* Build a vela controller exposing presigned/multipart upload + download
|
|
21
|
+
* endpoints for one bucket. Factory-decorated (like better-auth's catch-all)
|
|
22
|
+
* so `basePath` bakes in at decoration time. Consumes the {@link StorageService}
|
|
23
|
+
* facade so gating/retry/prefix apply uniformly on the HTTP path.
|
|
24
|
+
*/ export function createStorageController(basePath, serviceToken, http) {
|
|
25
|
+
let StorageController = class StorageController {
|
|
26
|
+
svc;
|
|
27
|
+
constructor(svc){
|
|
28
|
+
this.svc = svc;
|
|
29
|
+
}
|
|
30
|
+
get storage() {
|
|
31
|
+
return this.svc.storage;
|
|
32
|
+
}
|
|
33
|
+
async authorize(c, action) {
|
|
34
|
+
if (!http.authorize) {
|
|
35
|
+
if (http.defaultPolicy === 'allow') return {};
|
|
36
|
+
throw new StorageError('AccessDenied', 'storage: no authorizer configured (default deny)');
|
|
37
|
+
}
|
|
38
|
+
const res = await http.authorize(action, {
|
|
39
|
+
req: c.req.raw,
|
|
40
|
+
ctx: c,
|
|
41
|
+
driver: http.driverName
|
|
42
|
+
});
|
|
43
|
+
if (res === false) throw new StorageError('AccessDenied', 'not allowed');
|
|
44
|
+
if (res === true) return {};
|
|
45
|
+
return res;
|
|
46
|
+
}
|
|
47
|
+
fail(c, e) {
|
|
48
|
+
const err = e instanceof StorageError ? e : StorageError.wrap(e);
|
|
49
|
+
const { wire, status } = mapError(err.code);
|
|
50
|
+
return c.json({
|
|
51
|
+
error: {
|
|
52
|
+
code: wire,
|
|
53
|
+
message: err.message
|
|
54
|
+
}
|
|
55
|
+
}, status);
|
|
56
|
+
}
|
|
57
|
+
async signUpload(c) {
|
|
58
|
+
try {
|
|
59
|
+
const b = await c.req.json();
|
|
60
|
+
const ov = await this.authorize(c, {
|
|
61
|
+
type: 'sign-upload',
|
|
62
|
+
key: b.key,
|
|
63
|
+
contentType: b.contentType,
|
|
64
|
+
size: b.size
|
|
65
|
+
});
|
|
66
|
+
const key = sanitizeKey(ov.key ?? b.key);
|
|
67
|
+
const upload = await this.storage.signedUploadUrl(key, {
|
|
68
|
+
expiresIn: clampExpiry(ov.expiresIn ?? b.expiresIn, http.defaultExpiresIn, http.maxExpiresIn),
|
|
69
|
+
contentType: b.contentType,
|
|
70
|
+
maxSize: ov.maxSize ?? http.maxUploadSize
|
|
71
|
+
});
|
|
72
|
+
return c.json({
|
|
73
|
+
key,
|
|
74
|
+
upload
|
|
75
|
+
});
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return this.fail(c, e);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async multipartCreate(c) {
|
|
81
|
+
try {
|
|
82
|
+
const b = await c.req.json();
|
|
83
|
+
const ov = await this.authorize(c, {
|
|
84
|
+
type: 'multipart-create',
|
|
85
|
+
key: b.key,
|
|
86
|
+
contentType: b.contentType
|
|
87
|
+
});
|
|
88
|
+
const key = sanitizeKey(ov.key ?? b.key);
|
|
89
|
+
const sm = this.storage.signedMultipart;
|
|
90
|
+
if (!sm) throw new StorageError('Unsupported', 'presigned multipart not supported');
|
|
91
|
+
const created = await sm.create(key, {
|
|
92
|
+
contentType: b.contentType,
|
|
93
|
+
metadata: ov.metadata ?? b.metadata,
|
|
94
|
+
partSize: b.partSize
|
|
95
|
+
});
|
|
96
|
+
return c.json({
|
|
97
|
+
key,
|
|
98
|
+
uploadId: created.uploadId,
|
|
99
|
+
partSize: created.partSize
|
|
100
|
+
});
|
|
101
|
+
} catch (e) {
|
|
102
|
+
return this.fail(c, e);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async multipartSignPart(c) {
|
|
106
|
+
try {
|
|
107
|
+
const b = await c.req.json();
|
|
108
|
+
const ov = await this.authorize(c, {
|
|
109
|
+
type: 'multipart-sign-part',
|
|
110
|
+
key: b.key,
|
|
111
|
+
uploadId: b.uploadId,
|
|
112
|
+
partNumber: b.partNumber
|
|
113
|
+
});
|
|
114
|
+
const key = sanitizeKey(ov.key ?? b.key);
|
|
115
|
+
const sm = this.storage.signedMultipart;
|
|
116
|
+
if (!sm) throw new StorageError('Unsupported', 'presigned multipart not supported');
|
|
117
|
+
const part = await sm.signPart(key, b.uploadId, b.partNumber, {
|
|
118
|
+
expiresIn: clampExpiry(ov.expiresIn, http.defaultExpiresIn, http.maxExpiresIn)
|
|
119
|
+
});
|
|
120
|
+
return c.json({
|
|
121
|
+
partNumber: b.partNumber,
|
|
122
|
+
method: 'PUT',
|
|
123
|
+
url: part.url,
|
|
124
|
+
headers: part.headers
|
|
125
|
+
});
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return this.fail(c, e);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async multipartComplete(c) {
|
|
131
|
+
try {
|
|
132
|
+
const b = await c.req.json();
|
|
133
|
+
const ov = await this.authorize(c, {
|
|
134
|
+
type: 'multipart-complete',
|
|
135
|
+
key: b.key,
|
|
136
|
+
uploadId: b.uploadId
|
|
137
|
+
});
|
|
138
|
+
const key = sanitizeKey(ov.key ?? b.key);
|
|
139
|
+
const sm = this.storage.signedMultipart;
|
|
140
|
+
if (!sm) throw new StorageError('Unsupported', 'presigned multipart not supported');
|
|
141
|
+
return c.json(await sm.complete(key, b.uploadId, b.parts));
|
|
142
|
+
} catch (e) {
|
|
143
|
+
return this.fail(c, e);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async multipartAbort(c) {
|
|
147
|
+
try {
|
|
148
|
+
const b = await c.req.json();
|
|
149
|
+
const ov = await this.authorize(c, {
|
|
150
|
+
type: 'multipart-abort',
|
|
151
|
+
key: b.key,
|
|
152
|
+
uploadId: b.uploadId
|
|
153
|
+
});
|
|
154
|
+
const key = sanitizeKey(ov.key ?? b.key);
|
|
155
|
+
const sm = this.storage.signedMultipart;
|
|
156
|
+
if (!sm) throw new StorageError('Unsupported', 'presigned multipart not supported');
|
|
157
|
+
await sm.abort(key, b.uploadId);
|
|
158
|
+
return c.json({
|
|
159
|
+
ok: true
|
|
160
|
+
});
|
|
161
|
+
} catch (e) {
|
|
162
|
+
return this.fail(c, e);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async list(c) {
|
|
166
|
+
try {
|
|
167
|
+
const prefix = c.req.query('prefix');
|
|
168
|
+
const ov = await this.authorize(c, {
|
|
169
|
+
type: 'list',
|
|
170
|
+
prefix
|
|
171
|
+
});
|
|
172
|
+
const limit = c.req.query('limit');
|
|
173
|
+
const res = await this.storage.list({
|
|
174
|
+
prefix: ov.prefix ?? prefix,
|
|
175
|
+
cursor: c.req.query('cursor'),
|
|
176
|
+
delimiter: c.req.query('delimiter'),
|
|
177
|
+
limit: limit ? Math.min(Number(limit), http.maxListLimit) : undefined
|
|
178
|
+
});
|
|
179
|
+
return c.json({
|
|
180
|
+
items: res.items.map(serializeStored),
|
|
181
|
+
prefixes: res.prefixes,
|
|
182
|
+
cursor: res.cursor
|
|
183
|
+
});
|
|
184
|
+
} catch (e) {
|
|
185
|
+
return this.fail(c, e);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async head(c) {
|
|
189
|
+
try {
|
|
190
|
+
const key = sanitizeKey(c.req.query('key') ?? '');
|
|
191
|
+
const ov = await this.authorize(c, {
|
|
192
|
+
type: 'head',
|
|
193
|
+
key
|
|
194
|
+
});
|
|
195
|
+
const file = await this.storage.head(sanitizeKey(ov.key ?? key));
|
|
196
|
+
return c.json(serializeStored(file));
|
|
197
|
+
} catch (e) {
|
|
198
|
+
return this.fail(c, e);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async delete(c) {
|
|
202
|
+
try {
|
|
203
|
+
const b = await c.req.json();
|
|
204
|
+
const keys = b.keys.map((k)=>sanitizeKey(k));
|
|
205
|
+
const ov = await this.authorize(c, {
|
|
206
|
+
type: 'delete',
|
|
207
|
+
keys
|
|
208
|
+
});
|
|
209
|
+
const effective = (ov.keys ?? keys).map((k)=>sanitizeKey(k));
|
|
210
|
+
return c.json(await this.storage.delete(effective, {
|
|
211
|
+
concurrency: http.deleteConcurrency
|
|
212
|
+
}));
|
|
213
|
+
} catch (e) {
|
|
214
|
+
return this.fail(c, e);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async download(c) {
|
|
218
|
+
try {
|
|
219
|
+
const key = sanitizeKey(c.req.query('key') ?? '');
|
|
220
|
+
const disp = c.req.query('disposition');
|
|
221
|
+
const ov = await this.authorize(c, {
|
|
222
|
+
type: 'download',
|
|
223
|
+
key
|
|
224
|
+
});
|
|
225
|
+
const ekey = sanitizeKey(ov.key ?? key);
|
|
226
|
+
const name = ekey.slice(ekey.lastIndexOf('/') + 1);
|
|
227
|
+
if (http.download === 'redirect' && this.storage.capabilities.signedUrl.supported) {
|
|
228
|
+
const url = await this.storage.url(ekey, {
|
|
229
|
+
expiresIn: clampExpiry(ov.expiresIn, http.defaultExpiresIn, http.maxExpiresIn),
|
|
230
|
+
responseContentDisposition: dispositionHeader(disp, name)
|
|
231
|
+
});
|
|
232
|
+
return c.redirect(url, 302);
|
|
233
|
+
}
|
|
234
|
+
const range = parseRange(c.req.header('range'));
|
|
235
|
+
const file = await this.storage.download(ekey, range ? {
|
|
236
|
+
range
|
|
237
|
+
} : undefined);
|
|
238
|
+
const headers = new Headers({
|
|
239
|
+
'content-type': file.type,
|
|
240
|
+
'content-length': String(file.size),
|
|
241
|
+
'content-disposition': dispositionHeader(disp, file.name)
|
|
242
|
+
});
|
|
243
|
+
if (file.etag) headers.set('etag', file.etag);
|
|
244
|
+
if (range) {
|
|
245
|
+
headers.set('content-range', `bytes ${range.start}-${range.start + file.size - 1}/*`);
|
|
246
|
+
return new Response(file.stream(), {
|
|
247
|
+
status: 206,
|
|
248
|
+
headers
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return new Response(file.stream(), {
|
|
252
|
+
status: 200,
|
|
253
|
+
headers
|
|
254
|
+
});
|
|
255
|
+
} catch (e) {
|
|
256
|
+
return this.fail(c, e);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async signDownload(c) {
|
|
260
|
+
try {
|
|
261
|
+
const b = await c.req.json();
|
|
262
|
+
const ov = await this.authorize(c, {
|
|
263
|
+
type: 'download',
|
|
264
|
+
key: b.key
|
|
265
|
+
});
|
|
266
|
+
const key = sanitizeKey(ov.key ?? b.key);
|
|
267
|
+
const expiresIn = clampExpiry(ov.expiresIn ?? b.expiresIn, http.defaultExpiresIn, http.maxExpiresIn);
|
|
268
|
+
const url = await this.storage.url(key, {
|
|
269
|
+
expiresIn
|
|
270
|
+
});
|
|
271
|
+
return c.json({
|
|
272
|
+
url,
|
|
273
|
+
expiresAt: Date.now() + expiresIn * 1000
|
|
274
|
+
});
|
|
275
|
+
} catch (e) {
|
|
276
|
+
return this.fail(c, e);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
_ts_decorate([
|
|
281
|
+
Post('/sign-upload'),
|
|
282
|
+
_ts_param(0, Req()),
|
|
283
|
+
_ts_metadata("design:type", Function),
|
|
284
|
+
_ts_metadata("design:paramtypes", [
|
|
285
|
+
typeof Context === "undefined" ? Object : Context
|
|
286
|
+
]),
|
|
287
|
+
_ts_metadata("design:returntype", Promise)
|
|
288
|
+
], StorageController.prototype, "signUpload", null);
|
|
289
|
+
_ts_decorate([
|
|
290
|
+
Post('/multipart/create'),
|
|
291
|
+
_ts_param(0, Req()),
|
|
292
|
+
_ts_metadata("design:type", Function),
|
|
293
|
+
_ts_metadata("design:paramtypes", [
|
|
294
|
+
typeof Context === "undefined" ? Object : Context
|
|
295
|
+
]),
|
|
296
|
+
_ts_metadata("design:returntype", Promise)
|
|
297
|
+
], StorageController.prototype, "multipartCreate", null);
|
|
298
|
+
_ts_decorate([
|
|
299
|
+
Post('/multipart/sign-part'),
|
|
300
|
+
_ts_param(0, Req()),
|
|
301
|
+
_ts_metadata("design:type", Function),
|
|
302
|
+
_ts_metadata("design:paramtypes", [
|
|
303
|
+
typeof Context === "undefined" ? Object : Context
|
|
304
|
+
]),
|
|
305
|
+
_ts_metadata("design:returntype", Promise)
|
|
306
|
+
], StorageController.prototype, "multipartSignPart", null);
|
|
307
|
+
_ts_decorate([
|
|
308
|
+
Post('/multipart/complete'),
|
|
309
|
+
_ts_param(0, Req()),
|
|
310
|
+
_ts_metadata("design:type", Function),
|
|
311
|
+
_ts_metadata("design:paramtypes", [
|
|
312
|
+
typeof Context === "undefined" ? Object : Context
|
|
313
|
+
]),
|
|
314
|
+
_ts_metadata("design:returntype", Promise)
|
|
315
|
+
], StorageController.prototype, "multipartComplete", null);
|
|
316
|
+
_ts_decorate([
|
|
317
|
+
Post('/multipart/abort'),
|
|
318
|
+
_ts_param(0, Req()),
|
|
319
|
+
_ts_metadata("design:type", Function),
|
|
320
|
+
_ts_metadata("design:paramtypes", [
|
|
321
|
+
typeof Context === "undefined" ? Object : Context
|
|
322
|
+
]),
|
|
323
|
+
_ts_metadata("design:returntype", Promise)
|
|
324
|
+
], StorageController.prototype, "multipartAbort", null);
|
|
325
|
+
_ts_decorate([
|
|
326
|
+
Get('/list'),
|
|
327
|
+
_ts_param(0, Req()),
|
|
328
|
+
_ts_metadata("design:type", Function),
|
|
329
|
+
_ts_metadata("design:paramtypes", [
|
|
330
|
+
typeof Context === "undefined" ? Object : Context
|
|
331
|
+
]),
|
|
332
|
+
_ts_metadata("design:returntype", Promise)
|
|
333
|
+
], StorageController.prototype, "list", null);
|
|
334
|
+
_ts_decorate([
|
|
335
|
+
Get('/head'),
|
|
336
|
+
_ts_param(0, Req()),
|
|
337
|
+
_ts_metadata("design:type", Function),
|
|
338
|
+
_ts_metadata("design:paramtypes", [
|
|
339
|
+
typeof Context === "undefined" ? Object : Context
|
|
340
|
+
]),
|
|
341
|
+
_ts_metadata("design:returntype", Promise)
|
|
342
|
+
], StorageController.prototype, "head", null);
|
|
343
|
+
_ts_decorate([
|
|
344
|
+
Post('/delete'),
|
|
345
|
+
_ts_param(0, Req()),
|
|
346
|
+
_ts_metadata("design:type", Function),
|
|
347
|
+
_ts_metadata("design:paramtypes", [
|
|
348
|
+
typeof Context === "undefined" ? Object : Context
|
|
349
|
+
]),
|
|
350
|
+
_ts_metadata("design:returntype", Promise)
|
|
351
|
+
], StorageController.prototype, "delete", null);
|
|
352
|
+
_ts_decorate([
|
|
353
|
+
Get('/download'),
|
|
354
|
+
_ts_param(0, Req()),
|
|
355
|
+
_ts_metadata("design:type", Function),
|
|
356
|
+
_ts_metadata("design:paramtypes", [
|
|
357
|
+
typeof Context === "undefined" ? Object : Context
|
|
358
|
+
]),
|
|
359
|
+
_ts_metadata("design:returntype", Promise)
|
|
360
|
+
], StorageController.prototype, "download", null);
|
|
361
|
+
_ts_decorate([
|
|
362
|
+
Post('/sign-download'),
|
|
363
|
+
_ts_param(0, Req()),
|
|
364
|
+
_ts_metadata("design:type", Function),
|
|
365
|
+
_ts_metadata("design:paramtypes", [
|
|
366
|
+
typeof Context === "undefined" ? Object : Context
|
|
367
|
+
]),
|
|
368
|
+
_ts_metadata("design:returntype", Promise)
|
|
369
|
+
], StorageController.prototype, "signDownload", null);
|
|
370
|
+
StorageController = _ts_decorate([
|
|
371
|
+
Controller(basePath),
|
|
372
|
+
Injectable(),
|
|
373
|
+
_ts_param(0, Inject(serviceToken)),
|
|
374
|
+
_ts_metadata("design:type", Function),
|
|
375
|
+
_ts_metadata("design:paramtypes", [
|
|
376
|
+
typeof StorageService === "undefined" ? Object : StorageService
|
|
377
|
+
])
|
|
378
|
+
], StorageController);
|
|
379
|
+
return StorageController;
|
|
380
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single error taxonomy for `@velajs/storage`. Every driver maps its
|
|
3
|
+
* provider/transport failures onto a {@link StorageErrorCode}; the facade's
|
|
4
|
+
* capability gates throw `Unsupported`; the retry loop keys off
|
|
5
|
+
* {@link StorageError.retryable}; and the HTTP controller maps codes to
|
|
6
|
+
* status via its own `codeToHttp()` table.
|
|
7
|
+
*/
|
|
8
|
+
export type StorageErrorCode = 'NotFound' | 'AccessDenied' | 'Unsupported' | 'ReadOnly' | 'InvalidKey' | 'InvalidRequest' | 'Conflict' | 'RateLimited' | 'Network' | 'Timeout' | 'Aborted' | 'Provider' | 'Parse';
|
|
9
|
+
export interface StorageErrorOptions {
|
|
10
|
+
cause?: unknown;
|
|
11
|
+
/** Underlying HTTP status, when the failure came from a transport response. */
|
|
12
|
+
status?: number;
|
|
13
|
+
/** Override the default retryability implied by {@link StorageErrorCode}. */
|
|
14
|
+
retryable?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare class StorageError extends Error {
|
|
17
|
+
name: string;
|
|
18
|
+
readonly code: StorageErrorCode;
|
|
19
|
+
readonly retryable: boolean;
|
|
20
|
+
readonly status: number | undefined;
|
|
21
|
+
constructor(code: StorageErrorCode, message: string, options?: StorageErrorOptions);
|
|
22
|
+
/** Wrap an arbitrary thrown value as a `StorageError` (idempotent). */
|
|
23
|
+
static wrap(e: unknown): StorageError;
|
|
24
|
+
/** Map a transport status code to a `StorageError` (5xx/429 retryable). */
|
|
25
|
+
static fromStatus(status: number, message: string, cause?: unknown): StorageError;
|
|
26
|
+
}
|
|
27
|
+
/** True for `AbortError`-shaped values (DOMException or `{ name: 'AbortError' }`). */
|
|
28
|
+
export declare function isAbort(e: unknown): boolean;
|