@mieweb/pulsevault 0.0.1

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.
Files changed (43) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +386 -0
  3. package/dist/app.d.ts +114 -0
  4. package/dist/app.d.ts.map +1 -0
  5. package/dist/app.js +54 -0
  6. package/dist/app.js.map +1 -0
  7. package/dist/augment.d.ts +45 -0
  8. package/dist/augment.d.ts.map +1 -0
  9. package/dist/augment.js +2 -0
  10. package/dist/augment.js.map +1 -0
  11. package/dist/lib/deeplinks.d.ts +32 -0
  12. package/dist/lib/deeplinks.d.ts.map +1 -0
  13. package/dist/lib/deeplinks.js +24 -0
  14. package/dist/lib/deeplinks.js.map +1 -0
  15. package/dist/lib/errors.d.ts +6 -0
  16. package/dist/lib/errors.d.ts.map +1 -0
  17. package/dist/lib/errors.js +4 -0
  18. package/dist/lib/errors.js.map +1 -0
  19. package/dist/lib/magic.d.ts +50 -0
  20. package/dist/lib/magic.d.ts.map +1 -0
  21. package/dist/lib/magic.js +66 -0
  22. package/dist/lib/magic.js.map +1 -0
  23. package/dist/lib/pulsevaultTus.d.ts +56 -0
  24. package/dist/lib/pulsevaultTus.d.ts.map +1 -0
  25. package/dist/lib/pulsevaultTus.js +146 -0
  26. package/dist/lib/pulsevaultTus.js.map +1 -0
  27. package/dist/lib/uuid.d.ts +2 -0
  28. package/dist/lib/uuid.d.ts.map +1 -0
  29. package/dist/lib/uuid.js +5 -0
  30. package/dist/lib/uuid.js.map +1 -0
  31. package/dist/routes/pulsevault.d.ts +30 -0
  32. package/dist/routes/pulsevault.d.ts.map +1 -0
  33. package/dist/routes/pulsevault.js +315 -0
  34. package/dist/routes/pulsevault.js.map +1 -0
  35. package/dist/storage/local.d.ts +34 -0
  36. package/dist/storage/local.d.ts.map +1 -0
  37. package/dist/storage/local.js +167 -0
  38. package/dist/storage/local.js.map +1 -0
  39. package/dist/storage/types.d.ts +72 -0
  40. package/dist/storage/types.d.ts.map +1 -0
  41. package/dist/storage/types.js +2 -0
  42. package/dist/storage/types.js.map +1 -0
  43. package/package.json +75 -0
@@ -0,0 +1,315 @@
1
+ import send from "@fastify/send";
2
+ import { createPulsevaultTusServer, pulseVaultTusContext, } from "../lib/pulsevaultTus.js";
3
+ import { pulseVaultError } from "../lib/errors.js";
4
+ import { isUuid } from "../lib/uuid.js";
5
+ /**
6
+ * Pull `videoid` out of a raw `Upload-Metadata` header. Format is a
7
+ * comma-separated list of `<key> <base64-value>` pairs (tus v1 creation
8
+ * extension). We only care about `videoid`; everything else is left to the
9
+ * tus server proper.
10
+ */
11
+ function parseVideoidFromMetadata(header) {
12
+ for (const pair of header.split(",")) {
13
+ const trimmed = pair.trim();
14
+ if (!trimmed)
15
+ continue;
16
+ const sep = trimmed.indexOf(" ");
17
+ if (sep < 0)
18
+ continue;
19
+ const key = trimmed.slice(0, sep);
20
+ if (key !== "videoid")
21
+ continue;
22
+ const value = trimmed.slice(sep + 1).trim();
23
+ if (!value)
24
+ return undefined;
25
+ try {
26
+ const decoded = Buffer.from(value, "base64").toString("utf8");
27
+ return isUuid(decoded) ? decoded : undefined;
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ }
33
+ return undefined;
34
+ }
35
+ /**
36
+ * Decode the last URL segment of a tus PATCH/HEAD/DELETE (base64url-encoded
37
+ * id) and extract the first path component, which the plugin always shapes as
38
+ * the videoid (see `pulseVaultTus.ts`).
39
+ */
40
+ function videoidFromTusUrl(url) {
41
+ const match = url.match(/\/upload\/([^/?#]+)/);
42
+ if (!match?.[1])
43
+ return undefined;
44
+ let decoded;
45
+ try {
46
+ decoded = Buffer.from(match[1], "base64url").toString("utf8");
47
+ }
48
+ catch {
49
+ return undefined;
50
+ }
51
+ const first = decoded.split("/", 1)[0];
52
+ return isUuid(first) ? first : undefined;
53
+ }
54
+ function extractAuthzStatus(err) {
55
+ const e = err;
56
+ if (typeof e?.statusCode === "number")
57
+ return e.statusCode;
58
+ if (typeof e?.status_code === "number")
59
+ return e.status_code;
60
+ return 403;
61
+ }
62
+ function extractAuthzMessage(err) {
63
+ if (err instanceof Error && err.message)
64
+ return err.message;
65
+ return "Forbidden";
66
+ }
67
+ const pulseVaultErrorResponse = {
68
+ type: "object",
69
+ properties: {
70
+ ok: { type: "boolean" },
71
+ error: { type: "string" },
72
+ },
73
+ required: ["ok", "error"],
74
+ };
75
+ const tusRouteSchema = {
76
+ tags: ["pulsevault"],
77
+ summary: "TUS resumable upload endpoint",
78
+ description: "TUS v1 resumable upload protocol.\n\n" +
79
+ "- `POST` creates a new upload. The `Upload-Metadata` header must include a base64-encoded `videoid` pair.\n" +
80
+ "- `PATCH` appends a chunk at the offset given by `Upload-Offset`, with `Content-Type: application/offset+octet-stream`.\n" +
81
+ "- `HEAD` returns the current offset for a resumable upload.\n" +
82
+ "- `DELETE` cancels an in-flight upload.\n\n" +
83
+ "See https://tus.io/protocols/resumable-upload for the full protocol.",
84
+ response: {
85
+ 400: { description: "Invalid request.", ...pulseVaultErrorResponse },
86
+ 403: {
87
+ description: "Authorize hook rejected the request.",
88
+ ...pulseVaultErrorResponse,
89
+ },
90
+ },
91
+ };
92
+ const videoDeleteSchema = {
93
+ tags: ["pulsevault"],
94
+ summary: "Delete an uploaded video",
95
+ description: "Deletes all storage for a videoid (bytes + sidecar metadata). Runs the `authorize` hook with `phase: \"delete\"` before the adapter's `remove` is called. Returns 204 on success, 404 if the videoid was unknown, 501 if the adapter does not implement `remove`.",
96
+ params: {
97
+ type: "object",
98
+ properties: {
99
+ videoid: {
100
+ type: "string",
101
+ format: "uuid",
102
+ description: "UUID of the upload to delete.",
103
+ },
104
+ },
105
+ required: ["videoid"],
106
+ },
107
+ response: {
108
+ 400: {
109
+ description: "`videoid` is not a valid UUID.",
110
+ ...pulseVaultErrorResponse,
111
+ },
112
+ 403: {
113
+ description: "Authorize hook rejected the request.",
114
+ ...pulseVaultErrorResponse,
115
+ },
116
+ 404: { description: "Video not found.", ...pulseVaultErrorResponse },
117
+ 501: {
118
+ description: "Storage adapter does not implement delete.",
119
+ ...pulseVaultErrorResponse,
120
+ },
121
+ },
122
+ };
123
+ const videoGetSchema = {
124
+ tags: ["pulsevault"],
125
+ summary: "Serve a previously uploaded video",
126
+ description: "Resolves the `videoid` through the configured storage adapter and either streams the bytes or redirects (for CDN-backed adapters). Runs the `authorize` hook before resolve.",
127
+ params: {
128
+ type: "object",
129
+ properties: {
130
+ videoid: {
131
+ type: "string",
132
+ format: "uuid",
133
+ description: "UUID returned from the reserve/TUS-create flow.",
134
+ },
135
+ },
136
+ required: ["videoid"],
137
+ },
138
+ response: {
139
+ 400: {
140
+ description: "`videoid` is not a valid UUID.",
141
+ ...pulseVaultErrorResponse,
142
+ },
143
+ 403: {
144
+ description: "Authorize hook rejected the request.",
145
+ ...pulseVaultErrorResponse,
146
+ },
147
+ 404: { description: "Video not found.", ...pulseVaultErrorResponse },
148
+ },
149
+ };
150
+ const pulseVaultRoutes = async (fastify, opts) => {
151
+ const { storage, maxUploadSize, allowedExtensions, cache, authorize, validatePayload, onUploadComplete, } = opts;
152
+ // `fastify.prefix` is `""` when the plugin is mounted at the root.
153
+ const tusPath = `${fastify.prefix}/upload`;
154
+ const tusServer = createPulsevaultTusServer({
155
+ storage,
156
+ tusPath,
157
+ maxSize: maxUploadSize,
158
+ allowedExtensions,
159
+ validatePayload,
160
+ onUploadComplete,
161
+ });
162
+ fastify.addContentTypeParser("application/offset+octet-stream", (_request, _payload, done) => {
163
+ done(null);
164
+ });
165
+ /**
166
+ * Run the consumer's `authorize` hook (if any) for a TUS request. Returns
167
+ * `true` iff the request may proceed; on rejection, this function already
168
+ * wrote the response.
169
+ */
170
+ const runAuthorize = async (request, reply, phase) => {
171
+ let videoid;
172
+ if (phase === "create") {
173
+ const meta = request.headers["upload-metadata"];
174
+ if (typeof meta === "string") {
175
+ videoid = parseVideoidFromMetadata(meta);
176
+ }
177
+ }
178
+ else {
179
+ videoid = videoidFromTusUrl(request.url);
180
+ }
181
+ if (videoid) {
182
+ request.pulseVault = { videoid };
183
+ }
184
+ if (!authorize) {
185
+ return { ok: true, videoid };
186
+ }
187
+ // If we can't extract a videoid, let tus produce its own 4xx for malformed
188
+ // input rather than synthesize a fake authorize failure.
189
+ if (!videoid) {
190
+ return { ok: true, videoid };
191
+ }
192
+ try {
193
+ await authorize(request, { phase, videoid });
194
+ return { ok: true, videoid };
195
+ }
196
+ catch (err) {
197
+ const statusCode = extractAuthzStatus(err);
198
+ const message = extractAuthzMessage(err);
199
+ request.log.info({ err, videoid, phase, statusCode }, "pulsevault authorize rejected");
200
+ await reply.code(statusCode).send(pulseVaultError(message));
201
+ return { ok: false };
202
+ }
203
+ };
204
+ const tusHandler = async (request, reply) => {
205
+ const phase = request.method === "POST" ? "create" : "patch";
206
+ const authz = await runAuthorize(request, reply, phase);
207
+ if (!authz.ok)
208
+ return;
209
+ // Once hijacked, Fastify will not write to this reply on our behalf, so we
210
+ // must translate any unexpected throw from `@tus/server` into a response
211
+ // ourselves — otherwise the socket stays open until the client times out.
212
+ reply.hijack();
213
+ try {
214
+ await pulseVaultTusContext.run({ request, videoid: authz.videoid }, () => tusServer.handle(request.raw, reply.raw));
215
+ }
216
+ catch (err) {
217
+ request.log.error({ err }, "pulsevault tus handler failed");
218
+ if (reply.raw.headersSent || reply.raw.writableEnded) {
219
+ reply.raw.destroy();
220
+ return;
221
+ }
222
+ reply.raw.statusCode = 500;
223
+ reply.raw.setHeader("content-type", "text/plain; charset=utf-8");
224
+ reply.raw.end("Internal Server Error");
225
+ }
226
+ };
227
+ const tusMethods = ["POST", "PATCH", "HEAD", "DELETE", "OPTIONS"];
228
+ fastify.route({
229
+ method: [...tusMethods],
230
+ url: "/upload",
231
+ schema: tusRouteSchema,
232
+ handler: tusHandler,
233
+ });
234
+ fastify.route({
235
+ method: [...tusMethods],
236
+ url: "/upload/*",
237
+ schema: tusRouteSchema,
238
+ handler: tusHandler,
239
+ });
240
+ fastify.delete("/:videoid", { schema: videoDeleteSchema }, async (request, reply) => {
241
+ const videoid = request.params?.videoid;
242
+ if (!isUuid(videoid)) {
243
+ return reply
244
+ .code(400)
245
+ .send(pulseVaultError("`videoid` must be a valid UUID"));
246
+ }
247
+ request.pulseVault = { videoid };
248
+ if (authorize) {
249
+ try {
250
+ await authorize(request, { phase: "delete", videoid });
251
+ }
252
+ catch (err) {
253
+ const statusCode = extractAuthzStatus(err);
254
+ const message = extractAuthzMessage(err);
255
+ request.log.info({ err, videoid, phase: "delete", statusCode }, "pulsevault authorize rejected");
256
+ return reply.code(statusCode).send(pulseVaultError(message));
257
+ }
258
+ }
259
+ if (typeof storage.remove !== "function") {
260
+ return reply
261
+ .code(501)
262
+ .send(pulseVaultError("Storage adapter does not support delete"));
263
+ }
264
+ const removed = await storage.remove(videoid);
265
+ if (!removed) {
266
+ return reply.code(404).send(pulseVaultError("Video not found"));
267
+ }
268
+ return reply.code(204).send();
269
+ });
270
+ fastify.get("/:videoid", { schema: videoGetSchema }, async (request, reply) => {
271
+ const videoid = request.params?.videoid;
272
+ if (!isUuid(videoid)) {
273
+ return reply
274
+ .code(400)
275
+ .send(pulseVaultError("`videoid` must be a valid UUID"));
276
+ }
277
+ request.pulseVault = { videoid };
278
+ // Run authorize *before* resolve so consumers can reject without the
279
+ // response leaking "this videoid exists but you don't own it" vs. "no such
280
+ // videoid".
281
+ if (authorize) {
282
+ try {
283
+ await authorize(request, { phase: "resolve", videoid });
284
+ }
285
+ catch (err) {
286
+ const statusCode = extractAuthzStatus(err);
287
+ const message = extractAuthzMessage(err);
288
+ request.log.info({ err, videoid, phase: "resolve", statusCode }, "pulsevault authorize rejected");
289
+ return reply.code(statusCode).send(pulseVaultError(message));
290
+ }
291
+ }
292
+ const resolved = await storage.resolve(videoid);
293
+ if (!resolved) {
294
+ return reply.code(404).send(pulseVaultError("Video not found"));
295
+ }
296
+ if (resolved.kind === "redirect") {
297
+ return reply.redirect(resolved.url, resolved.statusCode ?? 302);
298
+ }
299
+ const result = await send(request.raw, resolved.filename, {
300
+ root: resolved.root,
301
+ ...cache,
302
+ });
303
+ if (result.type === "error") {
304
+ return reply
305
+ .code(result.statusCode)
306
+ .send(pulseVaultError(result.metadata.error.message));
307
+ }
308
+ for (const [name, value] of Object.entries(result.headers)) {
309
+ reply.header(name, value);
310
+ }
311
+ return reply.code(result.statusCode).send(result.stream);
312
+ });
313
+ };
314
+ export default pulseVaultRoutes;
315
+ //# sourceMappingURL=pulsevault.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pulsevault.js","sourceRoot":"","sources":["../../src/routes/pulsevault.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import type { PulseVaultStorage } from "./types.js";
2
+ export type LocalStorageOptions = {
3
+ /** Directory where per-video subdirectories are stored. Resolved against CWD if relative. */
4
+ workspaceDir: string;
5
+ };
6
+ /**
7
+ * Local adapter storage. Layout contract (stable; downstream tools may rely
8
+ * on it):
9
+ *
10
+ * ```text
11
+ * <workspaceRoot>/<videoid>/
12
+ * .pulsevault.json # our sidecar: { version, ext, filename, status }
13
+ * video/<videoid><ext> # finalized upload bytes
14
+ * video/<videoid><ext>.json # @tus/file-store's offset/metadata sidecar
15
+ * ```
16
+ *
17
+ * `workspaceRoot` is exposed so consumers can layer post-processing (e.g.
18
+ * hydrate an ArtiPod with `video/`, `transcripts/`, `frames/` mounts) against
19
+ * the same on-disk tree from an `onUploadComplete` hook. `getLocalPath` is
20
+ * exposed for `validatePayload` helpers that need to sniff the bytes before
21
+ * the video is marked ready.
22
+ */
23
+ export type LocalStorage = PulseVaultStorage & {
24
+ readonly workspaceRoot: string;
25
+ /**
26
+ * Return the absolute path to the upload bytes for `videoid`, regardless
27
+ * of ready state. Falls back to reading the sidecar if the in-memory cache
28
+ * is cold (so it works even after a server restart mid-upload). Returns
29
+ * `null` if the videoid is unknown.
30
+ */
31
+ getLocalPath(videoid: string): Promise<string | null>;
32
+ };
33
+ export declare function createLocalStorage(opts: LocalStorageOptions): LocalStorage;
34
+ //# sourceMappingURL=local.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/storage/local.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAEV,iBAAiB,EAElB,MAAM,YAAY,CAAC;AA6BpB,MAAM,MAAM,mBAAmB,GAAG;IAChC,6FAA6F;IAC7F,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG;IAC7C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;;;;OAKG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACvD,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,mBAAmB,GAAG,YAAY,CAkL1E"}
@@ -0,0 +1,167 @@
1
+ import { FileStore } from "@tus/file-store";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ const SIDECAR_NAME = ".pulsevault.json";
5
+ const SIDECAR_VERSION = 1;
6
+ export function createLocalStorage(opts) {
7
+ const workspaceRoot = path.resolve(opts.workspaceDir);
8
+ const datastore = new FileStore({ directory: workspaceRoot });
9
+ // Metadata cache keyed by videoid. Populated eagerly on reserve and lazily
10
+ // from the sidecar on cache-miss — so we never do a workspace-wide scan at
11
+ // boot and never do a per-request readdir on the GET hot path.
12
+ const metaCache = new Map();
13
+ const videoDir = (videoid) => path.join(workspaceRoot, videoid);
14
+ const sidecarPath = (videoid) => path.join(videoDir(videoid), SIDECAR_NAME);
15
+ const videoRelPath = (videoid, ext) => `video/${videoid}${ext}`;
16
+ const writeSidecar = async (videoid, sidecar) => {
17
+ // Atomic tmp + rename so a crash mid-write can never leave a truncated
18
+ // JSON blob that `loadMeta` would then treat as corrupt.
19
+ const finalPath = sidecarPath(videoid);
20
+ const tmpPath = `${finalPath}.tmp`;
21
+ await fs.writeFile(tmpPath, JSON.stringify(sidecar), "utf8");
22
+ await fs.rename(tmpPath, finalPath);
23
+ };
24
+ const readSidecar = async (videoid) => {
25
+ let raw;
26
+ try {
27
+ raw = await fs.readFile(sidecarPath(videoid), "utf8");
28
+ }
29
+ catch (err) {
30
+ if (err?.code === "ENOENT")
31
+ return null;
32
+ throw err;
33
+ }
34
+ try {
35
+ const parsed = JSON.parse(raw);
36
+ if (typeof parsed.ext !== "string")
37
+ return null;
38
+ if (typeof parsed.filename !== "string")
39
+ return null;
40
+ // Older sidecars (pre-status) are treated as ready so an in-place
41
+ // upgrade doesn't hide finalized uploads. New uploads always write a
42
+ // `status` field explicitly.
43
+ const status = parsed.status === "uploading" ? "uploading" : "ready";
44
+ return {
45
+ version: SIDECAR_VERSION,
46
+ ext: parsed.ext,
47
+ filename: parsed.filename,
48
+ status,
49
+ };
50
+ }
51
+ catch {
52
+ // Malformed sidecar — treat as absent. `reserveUpload` will rewrite
53
+ // it on the next create.
54
+ return null;
55
+ }
56
+ };
57
+ const loadMeta = async (videoid) => {
58
+ const cached = metaCache.get(videoid);
59
+ if (cached)
60
+ return cached;
61
+ const sidecar = await readSidecar(videoid);
62
+ if (!sidecar)
63
+ return null;
64
+ const meta = {
65
+ ext: sidecar.ext,
66
+ ready: sidecar.status === "ready",
67
+ };
68
+ metaCache.set(videoid, meta);
69
+ return meta;
70
+ };
71
+ const initialize = async () => {
72
+ await fs.mkdir(workspaceRoot, { recursive: true });
73
+ };
74
+ const reserveUpload = async ({ videoid, filename, ext, }) => {
75
+ // Collision guard: if an upload (in-progress or ready) already exists
76
+ // for this videoid, refuse the new upload rather than letting
77
+ // @tus/file-store silently reset the metadata sidecar to offset 0 and
78
+ // overwrite the file chunk-by-chunk on subsequent PATCHes. Translates
79
+ // to HTTP 409 via @tus/server's error path.
80
+ const meta = await loadMeta(videoid);
81
+ if (meta) {
82
+ throw Object.assign(new Error(`videoid ${videoid} already has an upload`), { statusCode: 409, status_code: 409 });
83
+ }
84
+ const dir = videoDir(videoid);
85
+ await fs.mkdir(path.join(dir, "video"), { recursive: true });
86
+ await writeSidecar(videoid, {
87
+ version: SIDECAR_VERSION,
88
+ ext,
89
+ filename,
90
+ status: "uploading",
91
+ });
92
+ metaCache.set(videoid, { ext, ready: false });
93
+ // @tus/file-store joins this onto its configured `directory`, so the
94
+ // actual file lands at `<workspaceRoot>/<videoid>/video/<videoid><ext>`.
95
+ return `${videoid}/${videoRelPath(videoid, ext)}`;
96
+ };
97
+ const resolve = async (videoid) => {
98
+ const meta = await loadMeta(videoid);
99
+ // Only serve ready uploads. In-progress uploads stay hidden — a client
100
+ // GETting mid-upload would otherwise receive a truncated file.
101
+ if (!meta || !meta.ready)
102
+ return null;
103
+ const root = videoDir(videoid);
104
+ const relFile = videoRelPath(videoid, meta.ext);
105
+ try {
106
+ const stat = await fs.stat(path.join(root, relFile));
107
+ if (!stat.isFile())
108
+ return null;
109
+ }
110
+ catch (err) {
111
+ if (err?.code === "ENOENT")
112
+ return null;
113
+ throw err;
114
+ }
115
+ return { kind: "stream", root, filename: relFile };
116
+ };
117
+ const markReady = async (videoid) => {
118
+ const sidecar = await readSidecar(videoid);
119
+ if (!sidecar) {
120
+ // No sidecar means no `reserveUpload` happened for this videoid — this
121
+ // is a contract violation by the caller, not a recoverable state.
122
+ throw new Error(`markReady: no sidecar for videoid ${videoid} (was reserveUpload called?)`);
123
+ }
124
+ if (sidecar.status === "ready") {
125
+ // Idempotent: already ready is fine, keep the cache consistent.
126
+ metaCache.set(videoid, { ext: sidecar.ext, ready: true });
127
+ return;
128
+ }
129
+ await writeSidecar(videoid, { ...sidecar, status: "ready" });
130
+ metaCache.set(videoid, { ext: sidecar.ext, ready: true });
131
+ };
132
+ const remove = async (videoid) => {
133
+ const dir = videoDir(videoid);
134
+ // Drop from cache before the rm so a racing `resolve` that arrives after
135
+ // the rm but before the cache eviction can't hand back a stale path.
136
+ metaCache.delete(videoid);
137
+ try {
138
+ // Note: no `force: true` — we want ENOENT to surface so the caller
139
+ // (the DELETE route) can distinguish "removed something" from "nothing
140
+ // to remove" and return 204 vs 404 accordingly.
141
+ await fs.rm(dir, { recursive: true });
142
+ return true;
143
+ }
144
+ catch (err) {
145
+ if (err?.code === "ENOENT")
146
+ return false;
147
+ throw err;
148
+ }
149
+ };
150
+ const getLocalPath = async (videoid) => {
151
+ const meta = await loadMeta(videoid);
152
+ if (!meta)
153
+ return null;
154
+ return path.join(workspaceRoot, videoid, "video", `${videoid}${meta.ext}`);
155
+ };
156
+ return {
157
+ datastore,
158
+ workspaceRoot,
159
+ initialize,
160
+ reserveUpload,
161
+ resolve,
162
+ markReady,
163
+ remove,
164
+ getLocalPath,
165
+ };
166
+ }
167
+ //# sourceMappingURL=local.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local.js","sourceRoot":"","sources":["../../src/storage/local.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ import type { DataStore } from "@tus/server";
2
+ /**
3
+ * How the GET route should serve a resolved video. Stream means `@fastify/send`
4
+ * reads directly from a local path; Redirect issues an HTTP 3xx to a URL the
5
+ * adapter produced (e.g. a pre-signed object storage URL).
6
+ */
7
+ export type PulseVaultResolution = {
8
+ kind: "stream";
9
+ /** Root directory `@fastify/send` should jail to. */
10
+ root: string;
11
+ /** Filename relative to `root`. */
12
+ filename: string;
13
+ } | {
14
+ kind: "redirect";
15
+ url: string;
16
+ /** Defaults to 302. */
17
+ statusCode?: number;
18
+ };
19
+ export type ReserveUploadParams = {
20
+ /** UUID from `Upload-Metadata.videoid`. */
21
+ videoid: string;
22
+ /** Raw filename from `Upload-Metadata.filename`. */
23
+ filename: string;
24
+ /** Lowercase extension including the leading dot, validated upstream. */
25
+ ext: string;
26
+ };
27
+ /**
28
+ * Storage backend contract. Keep the surface small: one write hook, one read
29
+ * hook, plus optional one-time init. Adapters own their own configuration.
30
+ */
31
+ export interface PulseVaultStorage {
32
+ /** TUS datastore used for resumable uploads. */
33
+ readonly datastore: DataStore;
34
+ /** One-time setup. Called once during plugin boot. */
35
+ initialize?(): Promise<void>;
36
+ /**
37
+ * One-time teardown. Called from Fastify's `onClose` hook so adapters can
38
+ * flush state, close connections, etc. The local adapter has nothing to
39
+ * release, so it omits this method.
40
+ */
41
+ shutdown?(): Promise<void>;
42
+ /**
43
+ * Called by TUS's `namingFunction` after core validation. Returns the file
44
+ * id (the string the datastore will use as its key/path). Adapters may also
45
+ * perform per-upload bookkeeping here (e.g. creating local mount dirs).
46
+ */
47
+ reserveUpload(params: ReserveUploadParams): Promise<string>;
48
+ /**
49
+ * Called by the GET route. Returns how to serve the video, or `null` if
50
+ * the videoid is unknown *or* the upload is still in progress — adapters
51
+ * that implement `markReady` should only return non-null once the bytes
52
+ * have been promoted to "ready."
53
+ */
54
+ resolve(videoid: string): Promise<PulseVaultResolution | null>;
55
+ /**
56
+ * Mark an upload as fully written and safe to serve. Called by the plugin
57
+ * after the final byte lands and any `validatePayload` + `onUploadComplete`
58
+ * hooks succeed. Adapters that do not distinguish uploading from ready
59
+ * (e.g. S3 multipart, which only materializes on CompleteMultipartUpload)
60
+ * may omit this method — `resolve` will be trusted to only return finished
61
+ * uploads in that case.
62
+ */
63
+ markReady?(videoid: string): Promise<void>;
64
+ /**
65
+ * Delete all storage associated with a videoid. Returns `true` if
66
+ * something was removed, `false` if the videoid was already absent.
67
+ * Called both from the `DELETE /:videoid` route and from the plugin's
68
+ * cleanup path when `validatePayload` rejects a completed upload.
69
+ */
70
+ remove?(videoid: string): Promise<boolean>;
71
+ }
72
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/storage/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAC5B;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAC;CAClB,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,uBAAuB;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEN,MAAM,MAAM,mBAAmB,GAAG;IAChC,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,gDAAgD;IAChD,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAE9B,sDAAsD;IACtD,UAAU,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7B;;;;OAIG;IACH,QAAQ,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3B;;;;OAIG;IACH,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE5D;;;;;OAKG;IACH,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;IAE/D;;;;;;;OAOG;IACH,SAAS,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3C;;;;;OAKG;IACH,MAAM,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5C"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/storage/types.ts"],"names":[],"mappings":""}