@gentbajko/slopify 0.8.3 → 1.0.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/README.md +28 -6
- package/SUBTITLES.md +2 -0
- package/dist/adapters/alignment/window.js +46 -5
- package/dist/adapters/alignment/worker.js +2 -2
- package/dist/adapters/llm/claude-code.js +6 -3
- package/dist/adapters/llm/codex.js +102 -12
- package/dist/adapters/llm/gemini.js +11 -8
- package/dist/adapters/llm/run-cli.js +61 -6
- package/dist/adapters/tts/inworld-async.js +4 -4
- package/dist/adapters/tts/inworld.js +73 -68
- package/dist/catalog/registry.js +13 -73
- package/dist/edge/cli.js +5 -2
- package/dist/edge/events/hub.js +7 -2
- package/dist/edge/events/preview-cache.js +7 -4
- package/dist/edge/events/visibility.js +33 -0
- package/dist/edge/http/actions.js +74 -79
- package/dist/edge/http/app.js +39 -6
- package/dist/edge/http/audio-preview.js +9 -1
- package/dist/edge/http/checkpoints.js +95 -0
- package/dist/edge/http/diagnostics.js +31 -0
- package/dist/edge/http/draft-files.js +69 -0
- package/dist/edge/http/draft-problem.js +15 -0
- package/dist/edge/http/drafts.js +95 -0
- package/dist/edge/http/multipart.js +86 -0
- package/dist/edge/http/mutations.js +49 -0
- package/dist/edge/http/planning.js +12 -1
- package/dist/edge/http/project-templates.js +74 -0
- package/dist/edge/http/projects.js +19 -2
- package/dist/edge/http/revision-files.js +84 -0
- package/dist/edge/http/revisions.js +103 -0
- package/dist/edge/http/schedules.js +100 -0
- package/dist/edge/http/staging.js +11 -52
- package/dist/edge/http/storage.js +110 -0
- package/dist/edge/http/subtitles.js +9 -79
- package/dist/edge/http/tutorial.js +26 -0
- package/dist/edge/signal-shutdown.js +23 -0
- package/dist/kernel/audio-preview.js +11 -5
- package/dist/kernel/db/migrations/0004-project-revisions.sql +91 -0
- package/dist/kernel/db/migrations/0005-revision-work.sql +103 -0
- package/dist/kernel/db/migrations/0006-play-drafts.sql +38 -0
- package/dist/kernel/db/migrations/0007-review-checkpoints.sql +28 -0
- package/dist/kernel/db/migrations/0008-project-templates.sql +21 -0
- package/dist/kernel/db/migrations/0009-scheduled-jobs.sql +37 -0
- package/dist/kernel/db/migrations/0010-retain-schedule-history.sql +62 -0
- package/dist/kernel/ports/model.js +5 -0
- package/dist/kernel/runner/attempt-repo.js +15 -0
- package/dist/kernel/runner/attempt.js +14 -2
- package/dist/kernel/runner/checkpoint-authority.js +23 -0
- package/dist/kernel/runner/index.js +35 -11
- package/dist/kernel/runner/providers.js +18 -8
- package/dist/kernel/runner/work-attempt.js +47 -0
- package/dist/kernel/runner/work-authority.js +37 -0
- package/dist/kernel/runner/work.js +23 -0
- package/dist/main.js +227 -69
- package/dist/slices/admission/repo.js +5 -56
- package/dist/slices/admission/schema.js +64 -0
- package/dist/slices/admission/start.js +13 -4
- package/dist/slices/article/continuation.js +9 -3
- package/dist/slices/article/provided-entries.js +7 -3
- package/dist/slices/article/run.js +20 -7
- package/dist/slices/article/segments.js +13 -4
- package/dist/slices/batch/index.js +8 -11
- package/dist/slices/cancel/index.js +78 -34
- package/dist/slices/checkpoints/change.js +250 -0
- package/dist/slices/checkpoints/fingerprint.js +33 -0
- package/dist/slices/checkpoints/index.js +2 -0
- package/dist/slices/checkpoints/model.js +1 -0
- package/dist/slices/checkpoints/recovery.js +116 -0
- package/dist/slices/checkpoints/repo.js +132 -0
- package/dist/slices/checkpoints/rules.js +30 -0
- package/dist/slices/checkpoints/schema.js +59 -0
- package/dist/slices/control/index.js +88 -22
- package/dist/slices/control/providers.js +3 -2
- package/dist/slices/control/revision-control-schema.js +7 -0
- package/dist/slices/control/revision-control.js +59 -0
- package/dist/slices/estimate/index.js +77 -59
- package/dist/slices/estimate/requests.js +102 -0
- package/dist/slices/images/run.js +14 -3
- package/dist/slices/library/slots.js +11 -11
- package/dist/slices/library/snapshot.js +33 -0
- package/dist/slices/narration/legacy-plan.js +58 -0
- package/dist/slices/narration/live.js +2 -2
- package/dist/slices/narration/plan.js +48 -54
- package/dist/slices/narration/run.js +21 -8
- package/dist/slices/play-drafts/convert.js +120 -0
- package/dist/slices/play-drafts/model.js +1 -0
- package/dist/slices/play-drafts/readiness.js +80 -0
- package/dist/slices/play-drafts/repo.js +76 -0
- package/dist/slices/play-drafts/review-inputs.js +150 -0
- package/dist/slices/play-drafts/review.js +109 -0
- package/dist/slices/play-drafts/schema.js +218 -0
- package/dist/slices/play-drafts/service.js +242 -0
- package/dist/slices/play-drafts/start-repo.js +113 -0
- package/dist/slices/play-drafts/start.js +148 -0
- package/dist/slices/play-drafts/uploads.js +67 -0
- package/dist/slices/project-templates/from-project.js +159 -0
- package/dist/slices/project-templates/model.js +1 -0
- package/dist/slices/project-templates/repo.js +41 -0
- package/dist/slices/project-templates/schema.js +37 -0
- package/dist/slices/project-templates/service.js +136 -0
- package/dist/slices/project-templates/setup.js +55 -0
- package/dist/slices/rebuild/admission-repo.js +120 -0
- package/dist/slices/rebuild/dependencies.js +33 -0
- package/dist/slices/rebuild/model.js +94 -0
- package/dist/slices/rebuild/narration-history.js +43 -0
- package/dist/slices/rebuild/narration-reuse.js +20 -0
- package/dist/slices/rebuild/preview-details.js +144 -0
- package/dist/slices/rebuild/preview-plan.js +223 -0
- package/dist/slices/rebuild/preview-retained.js +128 -0
- package/dist/slices/rebuild/provided-review.js +32 -0
- package/dist/slices/rebuild/recipe-audio.js +172 -0
- package/dist/slices/rebuild/recipe-build.js +17 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +75 -0
- package/dist/slices/rebuild/recipe-exports.js +65 -0
- package/dist/slices/rebuild/recipe-fixture.js +150 -0
- package/dist/slices/rebuild/recipe-input-schema.js +82 -0
- package/dist/slices/rebuild/recipe-legacy.js +135 -0
- package/dist/slices/rebuild/recipe-model.js +77 -0
- package/dist/slices/rebuild/recipe-provider-choice.js +14 -0
- package/dist/slices/rebuild/recipe-save.js +221 -0
- package/dist/slices/rebuild/recipe-text.js +170 -0
- package/dist/slices/rebuild/recipe-validation.js +86 -0
- package/dist/slices/rebuild/recipe-visual.js +111 -0
- package/dist/slices/rebuild/recipe-work.js +212 -0
- package/dist/slices/rebuild/recipes.js +4 -0
- package/dist/slices/rebuild/repo.js +64 -0
- package/dist/slices/rebuild/runtime-actions.js +76 -0
- package/dist/slices/rebuild/runtime-admission.js +71 -0
- package/dist/slices/rebuild/runtime-article.js +117 -0
- package/dist/slices/rebuild/runtime-checkpoints.js +45 -0
- package/dist/slices/rebuild/runtime-export-inputs.js +109 -0
- package/dist/slices/rebuild/runtime-export.js +139 -0
- package/dist/slices/rebuild/runtime-instructions.js +51 -0
- package/dist/slices/rebuild/runtime-local.js +137 -0
- package/dist/slices/rebuild/runtime-materialize.js +105 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +134 -0
- package/dist/slices/rebuild/runtime-plan.js +103 -0
- package/dist/slices/rebuild/runtime-provider.js +184 -0
- package/dist/slices/rebuild/runtime-publication.js +172 -0
- package/dist/slices/rebuild/runtime-run.js +35 -0
- package/dist/slices/rebuild/runtime-store.js +166 -0
- package/dist/slices/rebuild/runtime-subtitles.js +202 -0
- package/dist/slices/rebuild/service-readiness.js +110 -0
- package/dist/slices/rebuild/service.js +149 -0
- package/dist/slices/rebuild/transition-repo.js +108 -0
- package/dist/slices/rebuild/work-records.js +44 -0
- package/dist/slices/reruns/index.js +5 -0
- package/dist/slices/research/run.js +33 -12
- package/dist/slices/revisions/adopt-assets.js +31 -0
- package/dist/slices/revisions/adopt-content.js +112 -0
- package/dist/slices/revisions/adopt.js +100 -0
- package/dist/slices/revisions/downloads.js +80 -0
- package/dist/slices/revisions/index.js +5 -0
- package/dist/slices/revisions/manifest-repo.js +125 -0
- package/dist/slices/revisions/model.js +1 -0
- package/dist/slices/revisions/mutation-assets.js +143 -0
- package/dist/slices/revisions/mutation-cues.js +95 -0
- package/dist/slices/revisions/mutation-prepare.js +182 -0
- package/dist/slices/revisions/mutation-request.js +58 -0
- package/dist/slices/revisions/mutation-work.js +47 -0
- package/dist/slices/revisions/mutations.js +194 -0
- package/dist/slices/revisions/projection.js +82 -0
- package/dist/slices/revisions/publication-model.js +1 -0
- package/dist/slices/revisions/publication-rules.js +166 -0
- package/dist/slices/revisions/publish.js +115 -0
- package/dist/slices/revisions/repo.js +83 -0
- package/dist/slices/revisions/restore.js +60 -0
- package/dist/slices/revisions/rules.js +99 -0
- package/dist/slices/revisions/schema.js +160 -0
- package/dist/slices/revisions/view.js +44 -0
- package/dist/slices/schedules/calendar.js +47 -0
- package/dist/slices/schedules/model.js +1 -0
- package/dist/slices/schedules/repo.js +281 -0
- package/dist/slices/schedules/scheduler.js +144 -0
- package/dist/slices/schedules/schema.js +80 -0
- package/dist/slices/schedules/service.js +178 -0
- package/dist/slices/settings/cli-paths.js +1 -1
- package/dist/slices/settings/cli-status.js +45 -5
- package/dist/slices/settings/tutorial-schema.js +39 -0
- package/dist/slices/settings/tutorial.js +66 -0
- package/dist/slices/storage/assets.js +108 -0
- package/dist/slices/storage/delete-project.js +2 -1
- package/dist/slices/storage/downloads.js +4 -1
- package/dist/slices/storage/portable.js +871 -0
- package/dist/slices/storage/prepare.js +67 -0
- package/dist/slices/storage/reconcile.js +25 -5
- package/dist/slices/storage/repo.js +2 -15
- package/dist/slices/storage/schema.js +35 -0
- package/dist/slices/storage/staging-refs.js +26 -0
- package/dist/slices/storage/staging.js +16 -11
- package/dist/slices/thumbnail/run.js +26 -9
- package/dist/slices/video/audio-export-args.js +33 -0
- package/dist/slices/video/audio-export.js +13 -36
- package/dist/slices/video/audio-inputs.js +4 -3
- package/dist/slices/video/ffmpeg.js +6 -0
- package/dist/slices/video/reuse-audio.js +11 -4
- package/dist/updater/install-flow.js +27 -2
- package/dist/updater/plan.js +28 -2
- package/dist/updater/service.js +27 -2
- package/dist/updater/worker.js +2 -1
- package/dist/web/assets/index-CO7DnVX9.js +137 -0
- package/dist/web/assets/index-DX1_chKs.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -2
- package/dist/adapters/tts/inworld-text.js +0 -32
- package/dist/web/assets/index-Ce2HApus.css +0 -1
- package/dist/web/assets/index-xbD0MZOY.js +0 -130
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { zValidator } from "@hono/zod-validator";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { createTemplateFromProject, templateFromProjectSchema, } from "../../slices/project-templates/from-project.js";
|
|
6
|
+
import { templateCreateSchema, templateDeleteSchema, templateInstantiateSchema, templateUpdateSchema, } from "../../slices/project-templates/schema.js";
|
|
7
|
+
import { createTemplate, deleteTemplate, instantiateTemplate, listTemplates, readTemplate, updateTemplate, } from "../../slices/project-templates/service.js";
|
|
8
|
+
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
9
|
+
const param = z.object({ id: z.uuid() });
|
|
10
|
+
function refused(c, result) {
|
|
11
|
+
const status = result.reason === "not-found"
|
|
12
|
+
? 404
|
|
13
|
+
: ["conflict", "referenced-by-schedule"].includes(result.reason)
|
|
14
|
+
? 409
|
|
15
|
+
: 400;
|
|
16
|
+
return problem(c, {
|
|
17
|
+
status,
|
|
18
|
+
title: titleOf(status),
|
|
19
|
+
detail: result.reason === "referenced-by-schedule"
|
|
20
|
+
? "Cancel and delete the schedules using this template before deleting it. Their run history will be retained."
|
|
21
|
+
: result.reason === "missing-prompt"
|
|
22
|
+
? "A selected prompt or entry is unavailable. Choose another before saving the template."
|
|
23
|
+
: "Reload the template and check its setup before trying again.",
|
|
24
|
+
extensions: { reason: result.reason },
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function projectTemplateRoutes(deps) {
|
|
28
|
+
return routes(deps);
|
|
29
|
+
}
|
|
30
|
+
function routes(deps) {
|
|
31
|
+
const service = () => {
|
|
32
|
+
if (!deps)
|
|
33
|
+
throw new HTTPException(404);
|
|
34
|
+
return deps;
|
|
35
|
+
};
|
|
36
|
+
return new Hono()
|
|
37
|
+
.get("/", (c) => c.json({ templates: listTemplates(service()) }))
|
|
38
|
+
.post("/from-project/:projectId", zValidator("param", templateFromProjectSchema.unwrap().pick({ projectId: true }), onInvalid), zValidator("json", templateFromProjectSchema.unwrap().omit({ projectId: true }), onInvalid), (c) => {
|
|
39
|
+
const result = createTemplateFromProject(service(), {
|
|
40
|
+
...c.req.valid("json"),
|
|
41
|
+
projectId: c.req.valid("param").projectId,
|
|
42
|
+
});
|
|
43
|
+
return result.ok ? c.json(result.value, 201) : refused(c, result);
|
|
44
|
+
})
|
|
45
|
+
.post("/", zValidator("json", templateCreateSchema, onInvalid), (c) => {
|
|
46
|
+
const result = createTemplate(service(), c.req.valid("json"));
|
|
47
|
+
return result.ok ? c.json(result.value, 201) : refused(c, result);
|
|
48
|
+
})
|
|
49
|
+
.get("/:id", zValidator("param", param, onInvalid), zValidator("query", z.object({ version: z.coerce.number().int().positive().optional() }), onInvalid), (c) => {
|
|
50
|
+
const result = readTemplate(service(), c.req.valid("param").id, c.req.valid("query").version);
|
|
51
|
+
return result.ok ? c.json(result.value) : refused(c, result);
|
|
52
|
+
})
|
|
53
|
+
.put("/:id", zValidator("param", param, onInvalid), zValidator("json", templateUpdateSchema.unwrap().omit({ id: true }), onInvalid), (c) => {
|
|
54
|
+
const result = updateTemplate(service(), {
|
|
55
|
+
...c.req.valid("json"),
|
|
56
|
+
id: c.req.valid("param").id,
|
|
57
|
+
});
|
|
58
|
+
return result.ok ? c.json(result.value) : refused(c, result);
|
|
59
|
+
})
|
|
60
|
+
.delete("/:id", zValidator("param", param, onInvalid), zValidator("json", templateDeleteSchema.unwrap().omit({ id: true }), onInvalid), (c) => {
|
|
61
|
+
const result = deleteTemplate(service(), {
|
|
62
|
+
...c.req.valid("json"),
|
|
63
|
+
id: c.req.valid("param").id,
|
|
64
|
+
});
|
|
65
|
+
return result.ok ? c.json(result.value) : refused(c, result);
|
|
66
|
+
})
|
|
67
|
+
.post("/:id/instantiate", zValidator("param", param, onInvalid), zValidator("json", templateInstantiateSchema.unwrap().omit({ templateId: true }), onInvalid), (c) => {
|
|
68
|
+
const result = instantiateTemplate(service(), {
|
|
69
|
+
...c.req.valid("json"),
|
|
70
|
+
templateId: c.req.valid("param").id,
|
|
71
|
+
});
|
|
72
|
+
return result.ok ? c.json(result.value, 201) : refused(c, result);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -9,6 +9,8 @@ import { startRun } from "../../slices/admission/start.js";
|
|
|
9
9
|
import { withProjectControl } from "../../slices/control/lock.js";
|
|
10
10
|
import { resolveFont } from "../../slices/fonts/index.js";
|
|
11
11
|
import { pickTemplates, renderPicked } from "../../slices/library/slots.js";
|
|
12
|
+
import { adoptBaseline } from "../../slices/revisions/adopt.js";
|
|
13
|
+
import { currentRevisionId } from "../../slices/revisions/repo.js";
|
|
12
14
|
import { deleteProject } from "../../slices/storage/delete-project.js";
|
|
13
15
|
import { outputsOf, stagedFiles } from "../../slices/storage/repo.js";
|
|
14
16
|
import { record } from "../../slices/telemetry/record.js";
|
|
@@ -24,6 +26,7 @@ const idParam = z.object({
|
|
|
24
26
|
// generated from; see stagingRoutes.
|
|
25
27
|
export function projectRoutes(deps) {
|
|
26
28
|
const storage = {
|
|
29
|
+
catalogue: deps.catalogue,
|
|
27
30
|
db: deps.db,
|
|
28
31
|
paths: deps.paths,
|
|
29
32
|
ids: deps.ids,
|
|
@@ -40,13 +43,24 @@ export function projectRoutes(deps) {
|
|
|
40
43
|
log: deps.log,
|
|
41
44
|
appVersion: deps.version,
|
|
42
45
|
};
|
|
43
|
-
const storageForDelete = {
|
|
46
|
+
const storageForDelete = {
|
|
47
|
+
db: deps.db,
|
|
48
|
+
paths: deps.paths,
|
|
49
|
+
log: deps.log,
|
|
50
|
+
hasInflight: deps.runner.hasInflight,
|
|
51
|
+
};
|
|
44
52
|
const summarise = (project) => ({
|
|
45
53
|
...project,
|
|
46
54
|
status: derive(stagesOf(deps.db, project.id), project.paused),
|
|
47
55
|
});
|
|
48
56
|
return (new Hono()
|
|
49
57
|
.post("/", zValidator("json", runDraftSchema, onInvalid), async (c) => {
|
|
58
|
+
if (c.req.valid("json").checkpoints?.length)
|
|
59
|
+
return problem(c, {
|
|
60
|
+
status: 409,
|
|
61
|
+
title: titleOf(409),
|
|
62
|
+
detail: "Start checkpoint-enabled projects through Play Review.",
|
|
63
|
+
});
|
|
50
64
|
const requestedSubtitles = c.req.valid("json").subtitles;
|
|
51
65
|
if (requestedSubtitles !== undefined && requestedSubtitles.mode !== "off") {
|
|
52
66
|
try {
|
|
@@ -89,7 +103,7 @@ export function projectRoutes(deps) {
|
|
|
89
103
|
}
|
|
90
104
|
// Rendered from the values admit() has trimmed, so the stored text carries no
|
|
91
105
|
// padding the user did not intend.
|
|
92
|
-
const { project } = startRun(storage, admitted.draft, renderPicked(picked, admitted.draft.values));
|
|
106
|
+
const { project } = startRun(storage, admitted.draft, renderPicked(picked, admitted.draft.values), false, Object.fromEntries(picked.bodies.map(({ key, body }) => [key, body])));
|
|
93
107
|
// One event per project created. record() swallows its own
|
|
94
108
|
// failures, so a broken telemetry write cannot cost the user the run.
|
|
95
109
|
record(telemetry, "project.created", {});
|
|
@@ -124,7 +138,10 @@ export function projectRoutes(deps) {
|
|
|
124
138
|
detail: "No project has that id.",
|
|
125
139
|
});
|
|
126
140
|
}
|
|
141
|
+
if (deps.catalogue !== undefined)
|
|
142
|
+
adoptBaseline(deps, project.id);
|
|
127
143
|
return c.json({
|
|
144
|
+
revisionId: currentRevisionId(deps.db, project.id) ?? null,
|
|
128
145
|
project: summarise(project),
|
|
129
146
|
stages: stagesOf(deps.db, project.id),
|
|
130
147
|
outputs: outputsOf(deps.db, project.id),
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { zValidator } from "@hono/zod-validator";
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { findRevisionDownload, revisionImagesZip } from "../../slices/revisions/downloads.js";
|
|
8
|
+
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
9
|
+
const id = z
|
|
10
|
+
.string()
|
|
11
|
+
.min(1)
|
|
12
|
+
.max(64)
|
|
13
|
+
.regex(/^[0-9A-Za-z_-]+$/);
|
|
14
|
+
const revisionParam = z.object({ projectId: id, revisionId: id });
|
|
15
|
+
const recordParam = revisionParam.extend({ recordId: id });
|
|
16
|
+
const folderParam = z.object({ id, revisionId: id, recordId: id });
|
|
17
|
+
export function revisionFileRoutes(deps) {
|
|
18
|
+
return new Hono()
|
|
19
|
+
.get("/files/:projectId/revisions/:revisionId/images.zip", zValidator("param", revisionParam, onInvalid), (c) => {
|
|
20
|
+
const { projectId, revisionId } = c.req.valid("param");
|
|
21
|
+
const result = revisionImagesZip(deps, projectId, revisionId);
|
|
22
|
+
if (!result.ok)
|
|
23
|
+
return unavailable(c, result.reason);
|
|
24
|
+
return c.body(result.bytes, 200, {
|
|
25
|
+
"content-type": "application/zip",
|
|
26
|
+
"content-length": String(result.bytes.byteLength),
|
|
27
|
+
"content-disposition": `attachment; filename="${result.filename}"`,
|
|
28
|
+
});
|
|
29
|
+
})
|
|
30
|
+
.get("/files/:projectId/revisions/:revisionId/:recordId", zValidator("param", recordParam, onInvalid), (c) => {
|
|
31
|
+
const { projectId, revisionId, recordId } = c.req.valid("param");
|
|
32
|
+
const result = findRevisionDownload(deps, projectId, revisionId, recordId);
|
|
33
|
+
if (!result.ok)
|
|
34
|
+
return unavailable(c, result.reason);
|
|
35
|
+
const value = result.download;
|
|
36
|
+
return c.body(Readable.toWeb(createReadStream(value.path)), 200, {
|
|
37
|
+
"content-type": value.contentType,
|
|
38
|
+
"content-length": String(value.bytes),
|
|
39
|
+
"content-disposition": `attachment; filename="${value.filename}"`,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export function revisionFolderRoutes(deps) {
|
|
44
|
+
return new Hono().post("/:id/revisions/:revisionId/:recordId/open-folder", zValidator("param", folderParam, onInvalid), async (c) => {
|
|
45
|
+
const origin = c.req.header("origin");
|
|
46
|
+
if (origin !== undefined && origin !== new URL(c.req.url).origin)
|
|
47
|
+
return problem(c, {
|
|
48
|
+
status: 403,
|
|
49
|
+
title: titleOf(403),
|
|
50
|
+
detail: "Open folders from Slopify itself.",
|
|
51
|
+
});
|
|
52
|
+
const { id: projectId, revisionId, recordId } = c.req.valid("param");
|
|
53
|
+
const result = findRevisionDownload(deps, projectId, revisionId, recordId);
|
|
54
|
+
if (!result.ok)
|
|
55
|
+
return unavailable(c, result.reason);
|
|
56
|
+
try {
|
|
57
|
+
if (deps.openFolder === undefined)
|
|
58
|
+
throw new Error("No folder opener configured");
|
|
59
|
+
await deps.openFolder(dirname(result.download.path));
|
|
60
|
+
return c.json({ opened: true });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
deps.log.write("warn", "project.open-folder", {
|
|
64
|
+
projectId,
|
|
65
|
+
detail: "The file manager could not be opened for a retained file.",
|
|
66
|
+
});
|
|
67
|
+
return problem(c, {
|
|
68
|
+
status: 503,
|
|
69
|
+
title: titleOf(503),
|
|
70
|
+
detail: "Could not open the file manager on the machine running Slopify. Make sure a desktop session is available.",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function unavailable(c, reason) {
|
|
76
|
+
return problem(c, {
|
|
77
|
+
status: 404,
|
|
78
|
+
title: titleOf(404),
|
|
79
|
+
detail: reason === "missing-file"
|
|
80
|
+
? "This retained file is no longer on disk. Review the affected rebuild to recreate it."
|
|
81
|
+
: "The retained file is unavailable.",
|
|
82
|
+
extensions: { reason },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { zValidator } from "@hono/zod-validator";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { projectExists } from "../../slices/admission/repo.js";
|
|
5
|
+
import { previewRebuildSchema, startRebuildSchema } from "../../slices/rebuild/model.js";
|
|
6
|
+
import { previewRebuild, startRebuild } from "../../slices/rebuild/service.js";
|
|
7
|
+
import { ensureBaseline, restoreRevision, saveRevision } from "../../slices/revisions/index.js";
|
|
8
|
+
import { currentRevisionId, listRevisionHistory } from "../../slices/revisions/repo.js";
|
|
9
|
+
import { restoreRevisionSchema, saveRevisionSchema } from "../../slices/revisions/schema.js";
|
|
10
|
+
import { getRevisionView } from "../../slices/revisions/view.js";
|
|
11
|
+
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
12
|
+
const id = z
|
|
13
|
+
.string()
|
|
14
|
+
.min(1)
|
|
15
|
+
.max(64)
|
|
16
|
+
.regex(/^[0-9A-Za-z_-]+$/);
|
|
17
|
+
const projectParam = z.object({ id });
|
|
18
|
+
const revisionParam = projectParam.extend({ revisionId: id });
|
|
19
|
+
const saveBody = saveRevisionSchema.extend({ idempotencyKey: z.uuid() });
|
|
20
|
+
const restoreBody = restoreRevisionSchema.extend({ idempotencyKey: z.uuid() });
|
|
21
|
+
const statuses = {
|
|
22
|
+
"no-project": 404,
|
|
23
|
+
"no-revision": 404,
|
|
24
|
+
conflict: 409,
|
|
25
|
+
"idempotency-conflict": 409,
|
|
26
|
+
"invalid-edit": 400,
|
|
27
|
+
"stale-preview": 409,
|
|
28
|
+
"invalid-selection": 400,
|
|
29
|
+
"review-required": 409,
|
|
30
|
+
"cost-ack-required": 409,
|
|
31
|
+
readiness: 409,
|
|
32
|
+
};
|
|
33
|
+
function refused(c, deps, projectId, value) {
|
|
34
|
+
const status = statuses[value.reason];
|
|
35
|
+
return problem(c, {
|
|
36
|
+
status,
|
|
37
|
+
title: titleOf(status),
|
|
38
|
+
detail: value.reason.replaceAll("-", " "),
|
|
39
|
+
extensions: {
|
|
40
|
+
reason: value.reason,
|
|
41
|
+
currentRevisionId: currentRevisionId(deps.db, projectId) ?? null,
|
|
42
|
+
fields: value.fields ?? [],
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function unavailable(c) {
|
|
47
|
+
return problem(c, {
|
|
48
|
+
status: 503,
|
|
49
|
+
title: titleOf(503),
|
|
50
|
+
detail: "Rebuild services are unavailable.",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function revisionRoutes(deps) {
|
|
54
|
+
return new Hono()
|
|
55
|
+
.post("/:id/revisions/prepare", zValidator("param", projectParam, onInvalid), async (c) => {
|
|
56
|
+
const projectId = c.req.valid("param").id;
|
|
57
|
+
const result = await ensureBaseline(deps, projectId);
|
|
58
|
+
return result.ok ? c.json(result) : refused(c, deps, projectId, result);
|
|
59
|
+
})
|
|
60
|
+
.get("/:id/revisions", zValidator("param", projectParam, onInvalid), (c) => {
|
|
61
|
+
const projectId = c.req.valid("param").id;
|
|
62
|
+
return projectExists(deps.db, projectId)
|
|
63
|
+
? c.json({ revisions: listRevisionHistory(deps.db, projectId) })
|
|
64
|
+
: refused(c, deps, projectId, { reason: "no-project" });
|
|
65
|
+
})
|
|
66
|
+
.get("/:id/revisions/:revisionId", zValidator("param", revisionParam, onInvalid), (c) => {
|
|
67
|
+
const { id: projectId, revisionId } = c.req.valid("param");
|
|
68
|
+
const view = getRevisionView(deps, projectId, revisionId);
|
|
69
|
+
return view === undefined
|
|
70
|
+
? refused(c, deps, projectId, { reason: "no-revision" })
|
|
71
|
+
: c.json({ view });
|
|
72
|
+
})
|
|
73
|
+
.post("/:id/revisions", zValidator("param", projectParam, onInvalid), zValidator("json", saveBody, onInvalid), async (c) => {
|
|
74
|
+
const projectId = c.req.valid("param").id;
|
|
75
|
+
const result = await saveRevision(deps, { projectId, ...c.req.valid("json") });
|
|
76
|
+
if (!result.ok)
|
|
77
|
+
return refused(c, deps, projectId, result);
|
|
78
|
+
deps.hub.emit(projectId, { type: "project.updated", projectId });
|
|
79
|
+
return c.json(result);
|
|
80
|
+
})
|
|
81
|
+
.post("/:id/revisions/restore", zValidator("param", projectParam, onInvalid), zValidator("json", restoreBody, onInvalid), async (c) => {
|
|
82
|
+
const projectId = c.req.valid("param").id;
|
|
83
|
+
const result = await restoreRevision(deps, { projectId, ...c.req.valid("json") });
|
|
84
|
+
if (!result.ok)
|
|
85
|
+
return refused(c, deps, projectId, result);
|
|
86
|
+
deps.hub.emit(projectId, { type: "project.updated", projectId });
|
|
87
|
+
return c.json(result);
|
|
88
|
+
})
|
|
89
|
+
.post("/:id/rebuild/preview", zValidator("param", projectParam, onInvalid), zValidator("json", previewRebuildSchema, onInvalid), async (c) => {
|
|
90
|
+
if (deps.rebuild === undefined)
|
|
91
|
+
return unavailable(c);
|
|
92
|
+
const projectId = c.req.valid("param").id;
|
|
93
|
+
const result = await previewRebuild(deps.rebuild, { projectId, ...c.req.valid("json") });
|
|
94
|
+
return result.ok ? c.json(result) : refused(c, deps, projectId, result);
|
|
95
|
+
})
|
|
96
|
+
.post("/:id/rebuild", zValidator("param", projectParam, onInvalid), zValidator("json", startRebuildSchema, onInvalid), async (c) => {
|
|
97
|
+
if (deps.rebuild === undefined)
|
|
98
|
+
return unavailable(c);
|
|
99
|
+
const projectId = c.req.valid("param").id;
|
|
100
|
+
const result = await startRebuild(deps.rebuild, { projectId, ...c.req.valid("json") });
|
|
101
|
+
return result.ok ? c.json(result, 202) : refused(c, deps, projectId, result);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { zValidator } from "@hono/zod-validator";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { scheduleCreateSchema, scheduleRunSchema, scheduleSummarySchema, scheduleUpdateSchema, } from "../../slices/schedules/schema.js";
|
|
6
|
+
import { cancelSchedule, createSchedule, deleteSchedule, listScheduleRuns, listSchedules, pauseSchedule, resumeSchedule, updateSchedule, } from "../../slices/schedules/service.js";
|
|
7
|
+
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
8
|
+
const id = z.object({ id: z.uuid() });
|
|
9
|
+
function refused(c, result) {
|
|
10
|
+
const status = ["not-found", "missing-template"].includes(result.reason)
|
|
11
|
+
? 404
|
|
12
|
+
: ["conflict", "cancel-required", "spend-limit", "unsupported-media"].includes(result.reason)
|
|
13
|
+
? 409
|
|
14
|
+
: 400;
|
|
15
|
+
const detail = {
|
|
16
|
+
"invalid-input": "Check the schedule fields and try again.",
|
|
17
|
+
"not-found": "The schedule was not found.",
|
|
18
|
+
conflict: "The schedule changed elsewhere. Reload it before trying again.",
|
|
19
|
+
"cancel-required": "Cancel this schedule before deleting it. Completed schedules can be deleted directly.",
|
|
20
|
+
"missing-template": "The selected template version is no longer available.",
|
|
21
|
+
"unsupported-media": "This template contains provided media and cannot run unattended.",
|
|
22
|
+
"not-due": "Choose a future one-off time.",
|
|
23
|
+
"spend-limit": "The estimate exceeds the schedule spend limit or contains unknown pricing.",
|
|
24
|
+
readiness: "Provider or local readiness checks refused this scheduled run.",
|
|
25
|
+
};
|
|
26
|
+
return problem(c, {
|
|
27
|
+
status,
|
|
28
|
+
title: titleOf(status),
|
|
29
|
+
detail: detail[result.reason],
|
|
30
|
+
extensions: { reason: result.reason },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function scheduleRoutes(deps) {
|
|
34
|
+
return routes(deps);
|
|
35
|
+
}
|
|
36
|
+
function routes(deps) {
|
|
37
|
+
const service = () => {
|
|
38
|
+
if (deps === undefined)
|
|
39
|
+
throw new HTTPException(404);
|
|
40
|
+
return deps;
|
|
41
|
+
};
|
|
42
|
+
return new Hono()
|
|
43
|
+
.get("/", (c) => c.json({
|
|
44
|
+
schedules: listSchedules(service()).map((schedule) => scheduleSummarySchema.parse(schedule)),
|
|
45
|
+
}))
|
|
46
|
+
.post("/", zValidator("json", scheduleCreateSchema, onInvalid), (c) => {
|
|
47
|
+
const result = createSchedule(service(), c.req.valid("json"));
|
|
48
|
+
return result.ok
|
|
49
|
+
? c.json(scheduleSummarySchema.parse(result.value), 201)
|
|
50
|
+
: refused(c, result);
|
|
51
|
+
})
|
|
52
|
+
.get("/:id", zValidator("param", id, onInvalid), (c) => {
|
|
53
|
+
const input = c.req.valid("param");
|
|
54
|
+
const schedule = listSchedules(service()).find((item) => item.id === input.id);
|
|
55
|
+
if (!schedule)
|
|
56
|
+
return refused(c, { ok: false, reason: "not-found" });
|
|
57
|
+
const runs = listScheduleRuns(service(), input.id);
|
|
58
|
+
return runs.ok
|
|
59
|
+
? c.json({
|
|
60
|
+
schedule: scheduleSummarySchema.parse(schedule),
|
|
61
|
+
runs: runs.value.map((run) => scheduleRunSchema.parse(run)),
|
|
62
|
+
})
|
|
63
|
+
: refused(c, runs);
|
|
64
|
+
})
|
|
65
|
+
.put("/:id", zValidator("param", id, onInvalid), zValidator("json", scheduleUpdateSchema.unwrap().omit({ id: true }), onInvalid), (c) => {
|
|
66
|
+
const result = updateSchedule(service(), {
|
|
67
|
+
...c.req.valid("json"),
|
|
68
|
+
id: c.req.valid("param").id,
|
|
69
|
+
});
|
|
70
|
+
return result.ok ? c.json(scheduleSummarySchema.parse(result.value)) : refused(c, result);
|
|
71
|
+
})
|
|
72
|
+
.post("/:id/pause", zValidator("param", id, onInvalid), zValidator("json", z.object({ baseVersion: z.number().int().positive() }).strict(), onInvalid), (c) => {
|
|
73
|
+
const result = pauseSchedule(service(), {
|
|
74
|
+
...c.req.valid("json"),
|
|
75
|
+
id: c.req.valid("param").id,
|
|
76
|
+
});
|
|
77
|
+
return result.ok ? c.json(scheduleSummarySchema.parse(result.value)) : refused(c, result);
|
|
78
|
+
})
|
|
79
|
+
.post("/:id/resume", zValidator("param", id, onInvalid), zValidator("json", z.object({ baseVersion: z.number().int().positive() }).strict(), onInvalid), (c) => {
|
|
80
|
+
const result = resumeSchedule(service(), {
|
|
81
|
+
...c.req.valid("json"),
|
|
82
|
+
id: c.req.valid("param").id,
|
|
83
|
+
});
|
|
84
|
+
return result.ok ? c.json(scheduleSummarySchema.parse(result.value)) : refused(c, result);
|
|
85
|
+
})
|
|
86
|
+
.post("/:id/cancel", zValidator("param", id, onInvalid), zValidator("json", z.object({ baseVersion: z.number().int().positive() }).strict(), onInvalid), (c) => {
|
|
87
|
+
const result = cancelSchedule(service(), {
|
|
88
|
+
...c.req.valid("json"),
|
|
89
|
+
id: c.req.valid("param").id,
|
|
90
|
+
});
|
|
91
|
+
return result.ok ? c.json(scheduleSummarySchema.parse(result.value)) : refused(c, result);
|
|
92
|
+
})
|
|
93
|
+
.delete("/:id", zValidator("param", id, onInvalid), zValidator("json", z.object({ baseVersion: z.number().int().positive() }).strict(), onInvalid), (c) => {
|
|
94
|
+
const result = deleteSchedule(service(), {
|
|
95
|
+
...c.req.valid("json"),
|
|
96
|
+
id: c.req.valid("param").id,
|
|
97
|
+
});
|
|
98
|
+
return result.ok ? c.json(result.value) : refused(c, result);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { Readable } from "node:stream";
|
|
2
|
-
import { Busboy } from "@fastify/busboy";
|
|
3
1
|
import { zValidator } from "@hono/zod-validator";
|
|
4
2
|
import { Hono } from "hono";
|
|
5
3
|
import { z } from "zod";
|
|
6
4
|
import { uploadableStageKinds } from "../../slices/storage/model.js";
|
|
7
5
|
import { stagedFiles } from "../../slices/storage/repo.js";
|
|
8
6
|
import { discardStagedFile, stageUpload } from "../../slices/storage/staging.js";
|
|
7
|
+
import { readMultipart } from "./multipart.js";
|
|
9
8
|
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
10
9
|
const kindParam = z.object({ kind: z.enum(uploadableStageKinds) });
|
|
11
10
|
const idParam = z.object({ id: z.string().min(1).max(64) });
|
|
@@ -26,23 +25,11 @@ export function stagingRoutes(deps) {
|
|
|
26
25
|
return new Hono()
|
|
27
26
|
.get("/", (c) => c.json({ files: stagedFiles(deps.db) }))
|
|
28
27
|
.post("/:kind", zValidator("param", kindParam, onInvalid), async (c) => {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
detail: "An upload is sent as multipart/form-data with the file in one part.",
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
const body = c.req.raw.body;
|
|
38
|
-
if (body === null) {
|
|
39
|
-
return problem(c, {
|
|
40
|
-
status: 400,
|
|
41
|
-
title: titleOf(400),
|
|
42
|
-
detail: "The request carried no body.",
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
const result = await readUpload(storage, c.req.valid("param").kind, contentType, body);
|
|
28
|
+
const result = await readMultipart(c.req.raw, (content, originalFilename) => stageUpload(storage, {
|
|
29
|
+
stageKind: c.req.valid("param").kind,
|
|
30
|
+
originalFilename,
|
|
31
|
+
content,
|
|
32
|
+
}));
|
|
46
33
|
if (result === undefined) {
|
|
47
34
|
return problem(c, {
|
|
48
35
|
status: 400,
|
|
@@ -59,44 +46,16 @@ export function stagingRoutes(deps) {
|
|
|
59
46
|
const result = discardStagedFile(storage, c.req.valid("param").id);
|
|
60
47
|
if (!result.ok) {
|
|
61
48
|
return problem(c, {
|
|
62
|
-
status: 404,
|
|
63
|
-
title: titleOf(404),
|
|
64
|
-
detail:
|
|
49
|
+
status: result.reason === "in-use" ? 409 : 404,
|
|
50
|
+
title: titleOf(result.reason === "in-use" ? 409 : 404),
|
|
51
|
+
detail: result.reason === "in-use"
|
|
52
|
+
? "This file is used by a saved draft."
|
|
53
|
+
: "No staged file has that id.",
|
|
65
54
|
});
|
|
66
55
|
}
|
|
67
56
|
return c.body(null, 204);
|
|
68
57
|
});
|
|
69
58
|
}
|
|
70
|
-
// The bytes go from the socket to the parser to the disk. Nothing collects them: the
|
|
71
|
-
// platform's own formData() materialises a whole part in memory first, which an
|
|
72
|
-
// uncapped audio or video upload cannot afford.
|
|
73
|
-
function readUpload(storage, stageKind, contentType, body) {
|
|
74
|
-
return new Promise((resolve, reject) => {
|
|
75
|
-
const parser = new Busboy({
|
|
76
|
-
headers: { "content-type": contentType },
|
|
77
|
-
limits: { files: 1, fields: 8 },
|
|
78
|
-
// The parser hands the name through untouched: silently renaming a file the user
|
|
79
|
-
// picked would record a name nobody chose, so the slice rejects instead.
|
|
80
|
-
preservePath: true,
|
|
81
|
-
});
|
|
82
|
-
let started = false;
|
|
83
|
-
parser.on("file", (_field, stream, filename) => {
|
|
84
|
-
started = true;
|
|
85
|
-
stageUpload(storage, {
|
|
86
|
-
stageKind,
|
|
87
|
-
originalFilename: typeof filename === "string" ? filename : "",
|
|
88
|
-
content: stream,
|
|
89
|
-
}).then(resolve, reject);
|
|
90
|
-
});
|
|
91
|
-
parser.on("error", reject);
|
|
92
|
-
parser.on("finish", () => {
|
|
93
|
-
if (!started) {
|
|
94
|
-
resolve(undefined);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
Readable.fromWeb(body).pipe(parser);
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
59
|
function detailOf(reason) {
|
|
101
60
|
return reason === "empty-file"
|
|
102
61
|
? "The uploaded file is empty."
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { exportPortable, importPortable, portableMaxArchiveBytes, storageUsage, } from "../../slices/storage/portable.js";
|
|
3
|
+
import { reconcileStorage } from "../../slices/storage/reconcile.js";
|
|
4
|
+
import { problem, titleOf } from "./problem.js";
|
|
5
|
+
export function storageRoutes(deps) {
|
|
6
|
+
return new Hono()
|
|
7
|
+
.get("/", (c) => c.json(storageUsage({ db: deps.db, paths: deps.paths })))
|
|
8
|
+
.get("/export", (c) => {
|
|
9
|
+
try {
|
|
10
|
+
const bytes = exportPortable({
|
|
11
|
+
db: deps.db,
|
|
12
|
+
paths: deps.paths,
|
|
13
|
+
ids: deps.ids,
|
|
14
|
+
now: () => deps.clock.now().toISOString(),
|
|
15
|
+
});
|
|
16
|
+
return c.body(bytes, 200, {
|
|
17
|
+
"content-type": "application/zip",
|
|
18
|
+
"content-length": String(bytes.byteLength),
|
|
19
|
+
"content-disposition": 'attachment; filename="slopify-backup.zip"',
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return problem(c, {
|
|
24
|
+
status: 500,
|
|
25
|
+
title: titleOf(500),
|
|
26
|
+
detail: "The backup could not be created.",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
.put("/import", async (c) => {
|
|
31
|
+
const contentType = c.req.header("content-type") ?? "";
|
|
32
|
+
if (!contentType.includes("application/zip"))
|
|
33
|
+
return problem(c, {
|
|
34
|
+
status: 415,
|
|
35
|
+
title: titleOf(415),
|
|
36
|
+
detail: "Upload a Slopify backup ZIP.",
|
|
37
|
+
});
|
|
38
|
+
const upload = await readPortableUpload(c.req.raw);
|
|
39
|
+
if (!upload.ok)
|
|
40
|
+
return problem(c, {
|
|
41
|
+
status: 413,
|
|
42
|
+
title: titleOf(413),
|
|
43
|
+
detail: "The backup must be between 1 byte and 100 MB.",
|
|
44
|
+
});
|
|
45
|
+
try {
|
|
46
|
+
const imported = importPortable({
|
|
47
|
+
db: deps.db,
|
|
48
|
+
paths: deps.paths,
|
|
49
|
+
ids: deps.ids,
|
|
50
|
+
now: () => deps.clock.now().toISOString(),
|
|
51
|
+
}, upload.bytes);
|
|
52
|
+
return c.json(imported);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return problem(c, {
|
|
56
|
+
status: 400,
|
|
57
|
+
title: titleOf(400),
|
|
58
|
+
detail: "The backup is invalid or incomplete.",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
.post("/cleanup", (c) => c.json(reconcileStorage(deps.db, deps.paths)));
|
|
63
|
+
}
|
|
64
|
+
async function readPortableUpload(request) {
|
|
65
|
+
const declared = request.headers.get("content-length");
|
|
66
|
+
if (declared !== null && /^\d+$/.test(declared) && Number(declared) > portableMaxArchiveBytes) {
|
|
67
|
+
try {
|
|
68
|
+
await request.body?.cancel();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// The request is already rejected; cancellation only releases transport resources sooner.
|
|
72
|
+
}
|
|
73
|
+
return { ok: false };
|
|
74
|
+
}
|
|
75
|
+
if (request.body === null)
|
|
76
|
+
return { ok: false };
|
|
77
|
+
const reader = request.body.getReader();
|
|
78
|
+
const chunks = [];
|
|
79
|
+
let total = 0;
|
|
80
|
+
try {
|
|
81
|
+
while (true) {
|
|
82
|
+
const next = await reader.read();
|
|
83
|
+
if (next.done)
|
|
84
|
+
break;
|
|
85
|
+
if (next.value.byteLength > portableMaxArchiveBytes - total) {
|
|
86
|
+
try {
|
|
87
|
+
await reader.cancel();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// The response is already decided; an upload transport failing cancellation is harmless.
|
|
91
|
+
}
|
|
92
|
+
return { ok: false };
|
|
93
|
+
}
|
|
94
|
+
total += next.value.byteLength;
|
|
95
|
+
chunks.push(next.value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
reader.releaseLock();
|
|
100
|
+
}
|
|
101
|
+
if (total === 0)
|
|
102
|
+
return { ok: false };
|
|
103
|
+
const bytes = new Uint8Array(total);
|
|
104
|
+
let offset = 0;
|
|
105
|
+
for (const chunk of chunks) {
|
|
106
|
+
bytes.set(chunk, offset);
|
|
107
|
+
offset += chunk.byteLength;
|
|
108
|
+
}
|
|
109
|
+
return { ok: true, bytes };
|
|
110
|
+
}
|