streamby-core 0.38.0 → 0.40.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 +64 -0
- package/README.md +108 -1
- package/dist/adapters/database/connectionManager.js +55 -0
- package/dist/mcp/tools.js +41 -0
- package/dist/middleware/createRouter.js +11 -1
- package/dist/middleware/routes/ai.js +3 -3
- package/dist/middleware/routes/deliverable.d.ts +4 -2
- package/dist/middleware/routes/deliverable.js +16 -7
- package/dist/middleware/routes/export.js +16 -0
- package/dist/middleware/routes/media.js +7 -6
- package/dist/middleware/routes/project.js +36 -10
- package/dist/middleware/routes/renderFarm.js +4 -77
- package/dist/middleware/routes/storage.js +13 -27
- package/dist/middleware/routes/worker.d.ts +7 -0
- package/dist/middleware/routes/worker.js +125 -0
- package/dist/middleware/routes/workerCallback.d.ts +5 -0
- package/dist/middleware/routes/workerCallback.js +31 -0
- package/dist/services/aiProcessor.js +11 -11
- package/dist/services/apiTokenPermissions.js +8 -7
- package/dist/services/executor.js +10 -10
- package/dist/services/jobQueue.d.ts +6 -4
- package/dist/services/jobQueue.js +28 -16
- package/dist/services/jobStore.d.ts +27 -0
- package/dist/services/jobStore.js +177 -0
- package/dist/services/mediaProcessor.js +16 -16
- package/dist/services/vfxProcessor.js +12 -12
- package/dist/services/workerJobs.d.ts +73 -0
- package/dist/services/workerJobs.js +459 -0
- package/dist/services/workers.d.ts +78 -0
- package/dist/services/workers.js +179 -0
- package/dist/types/index.d.ts +52 -2
- package/dist/utils/assetCategories.d.ts +4 -0
- package/dist/utils/assetCategories.js +21 -0
- package/dist/utils/builtinAccess.d.ts +1 -1
- package/dist/utils/builtinAccess.js +4 -3
- package/dist/utils/privateAddress.d.ts +10 -0
- package/dist/utils/privateAddress.js +85 -0
- package/dist/utils/sanitize.d.ts +1 -0
- package/dist/utils/sanitize.js +14 -0
- package/package.json +1 -1
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.renderFarmRouter = renderFarmRouter;
|
|
7
4
|
const express_1 = require("express");
|
|
@@ -9,80 +6,10 @@ const manager_1 = require("../../models/manager");
|
|
|
9
6
|
const auth_1 = require("../../utils/auth");
|
|
10
7
|
const jobQueue_1 = require("../../services/jobQueue");
|
|
11
8
|
const vfxProcessor_1 = require("../../services/vfxProcessor");
|
|
12
|
-
const sanitize_1 = require("../../utils/sanitize");
|
|
13
|
-
const crypto_1 = __importDefault(require("crypto"));
|
|
14
|
-
const encryption_1 = require("../../utils/encryption");
|
|
15
9
|
function renderFarmRouter(_config) {
|
|
16
10
|
const router = (0, express_1.Router)();
|
|
17
11
|
const Project = (0, manager_1.getModel)('projects');
|
|
18
|
-
//
|
|
19
|
-
router.get('/projects/:projectId/render-farm-connections', async (req, res) => {
|
|
20
|
-
try {
|
|
21
|
-
const auth = req.auth;
|
|
22
|
-
const { projectId } = req.params;
|
|
23
|
-
const project = await Project.findOne({ _id: projectId });
|
|
24
|
-
if (!project)
|
|
25
|
-
return res.status(404).json({ message: 'Project not found' });
|
|
26
|
-
if (!(0, auth_1.isProjectMember)(project, auth.userId))
|
|
27
|
-
return res.status(403).json({ message: 'Unauthorized' });
|
|
28
|
-
res.json({ connections: (0, sanitize_1.sanitizeConnections)(project.renderFarmConnections) });
|
|
29
|
-
}
|
|
30
|
-
catch (err) {
|
|
31
|
-
res.status(500).json({ message: err.message });
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
router.post('/projects/:projectId/render-farm-connections', async (req, res) => {
|
|
35
|
-
try {
|
|
36
|
-
const auth = req.auth;
|
|
37
|
-
const { projectId } = req.params;
|
|
38
|
-
const { name, provider, apiUrl, credential: credentialValue, encryptedCredential: legacyCredential, description } = req.body;
|
|
39
|
-
// The credential, as given (`encryptedCredential` was its old name: it was always the
|
|
40
|
-
// value itself), stored encrypted.
|
|
41
|
-
const credential = credentialValue ?? legacyCredential;
|
|
42
|
-
if (credential && !(0, encryption_1.isEncryptionKeySet)())
|
|
43
|
-
return res.status(500).json({ message: 'Encryption key is not set' });
|
|
44
|
-
const encryptedCredential = credential ? (0, encryption_1.encrypt)(String(credential)) : undefined;
|
|
45
|
-
if (!name || !provider || !apiUrl) {
|
|
46
|
-
return res.status(400).json({ message: 'name, provider and apiUrl are required' });
|
|
47
|
-
}
|
|
48
|
-
const project = await Project.findOne({ _id: projectId });
|
|
49
|
-
if (!project)
|
|
50
|
-
return res.status(404).json({ message: 'Project not found' });
|
|
51
|
-
if (!(0, auth_1.canEditProject)(project, auth.userId))
|
|
52
|
-
return res.status(403).json({ message: 'Unauthorized' });
|
|
53
|
-
const connection = {
|
|
54
|
-
id: crypto_1.default.randomUUID(),
|
|
55
|
-
name,
|
|
56
|
-
provider,
|
|
57
|
-
apiUrl,
|
|
58
|
-
encryptedCredential,
|
|
59
|
-
description,
|
|
60
|
-
projectId,
|
|
61
|
-
createdAt: new Date(),
|
|
62
|
-
};
|
|
63
|
-
await Project.update({ _id: projectId }, { $push: { renderFarmConnections: connection } });
|
|
64
|
-
res.status(201).json({ connection: (0, sanitize_1.sanitizeConnection)(connection) });
|
|
65
|
-
}
|
|
66
|
-
catch (err) {
|
|
67
|
-
res.status(500).json({ message: err.message });
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
router.delete('/projects/:projectId/render-farm-connections/:connectionId', async (req, res) => {
|
|
71
|
-
try {
|
|
72
|
-
const auth = req.auth;
|
|
73
|
-
const { projectId, connectionId } = req.params;
|
|
74
|
-
const project = await Project.findOne({ _id: projectId });
|
|
75
|
-
if (!project)
|
|
76
|
-
return res.status(404).json({ message: 'Project not found' });
|
|
77
|
-
if (!(0, auth_1.canEditProject)(project, auth.userId))
|
|
78
|
-
return res.status(403).json({ message: 'Unauthorized' });
|
|
79
|
-
await Project.update({ _id: projectId }, { $pull: { renderFarmConnections: { id: connectionId } } });
|
|
80
|
-
res.json({ message: 'Connection deleted' });
|
|
81
|
-
}
|
|
82
|
-
catch (err) {
|
|
83
|
-
res.status(500).json({ message: err.message });
|
|
84
|
-
}
|
|
85
|
-
});
|
|
12
|
+
// A project's render farms are its workers: see routes/worker.
|
|
86
13
|
// ─── VFX Jobs ────────────────────────────────────────────────────────────────
|
|
87
14
|
router.post('/projects/:projectId/jobs/render', async (req, res) => {
|
|
88
15
|
try {
|
|
@@ -96,7 +23,7 @@ function renderFarmRouter(_config) {
|
|
|
96
23
|
return res.status(404).json({ message: 'Project not found' });
|
|
97
24
|
if (!(0, auth_1.canEditProject)(project, auth.userId))
|
|
98
25
|
return res.status(403).json({ message: 'Unauthorized' });
|
|
99
|
-
const job = (0, jobQueue_1.createJob)('render', auth.userId, projectId, { fileId, renderer, renderFarmConnectionId, frameRange, resolution, samples, outputFormat });
|
|
26
|
+
const job = await (0, jobQueue_1.createJob)('render', auth.userId, projectId, { fileId, renderer, renderFarmConnectionId, frameRange, resolution, samples, outputFormat });
|
|
100
27
|
setImmediate(() => (0, vfxProcessor_1.runRenderJob)(job.jobId, fileId, projectId, { renderer, renderFarmConnectionId, frameRange, resolution, samples, outputFormat }));
|
|
101
28
|
res.status(202).json({ jobId: job.jobId, status: job.status });
|
|
102
29
|
}
|
|
@@ -116,7 +43,7 @@ function renderFarmRouter(_config) {
|
|
|
116
43
|
return res.status(404).json({ message: 'Project not found' });
|
|
117
44
|
if (!(0, auth_1.canEditProject)(project, auth.userId))
|
|
118
45
|
return res.status(403).json({ message: 'Unauthorized' });
|
|
119
|
-
const job = (0, jobQueue_1.createJob)('format-convert', auth.userId, projectId, { fileId, inputFormat, outputFormat, applyTransforms, embedTextures });
|
|
46
|
+
const job = await (0, jobQueue_1.createJob)('format-convert', auth.userId, projectId, { fileId, inputFormat, outputFormat, applyTransforms, embedTextures });
|
|
120
47
|
setImmediate(() => (0, vfxProcessor_1.runFormatConvertJob)(job.jobId, fileId, projectId, { inputFormat, outputFormat, applyTransforms, embedTextures }));
|
|
121
48
|
res.status(202).json({ jobId: job.jobId, status: job.status });
|
|
122
49
|
}
|
|
@@ -136,7 +63,7 @@ function renderFarmRouter(_config) {
|
|
|
136
63
|
return res.status(404).json({ message: 'Project not found' });
|
|
137
64
|
if (!(0, auth_1.canEditProject)(project, auth.userId))
|
|
138
65
|
return res.status(403).json({ message: 'Unauthorized' });
|
|
139
|
-
const job = (0, jobQueue_1.createJob)('lod', auth.userId, projectId, { fileId, levels, reductionRatios, algorithm, outputFormat });
|
|
66
|
+
const job = await (0, jobQueue_1.createJob)('lod', auth.userId, projectId, { fileId, levels, reductionRatios, algorithm, outputFormat });
|
|
140
67
|
setImmediate(() => (0, vfxProcessor_1.runLodJob)(job.jobId, fileId, projectId, { levels, reductionRatios, algorithm, outputFormat }));
|
|
141
68
|
res.status(202).json({ jobId: job.jobId, status: job.status });
|
|
142
69
|
}
|
|
@@ -18,21 +18,7 @@ const crypto_1 = __importDefault(require("crypto"));
|
|
|
18
18
|
const signedUrls_1 = require("../../services/signedUrls");
|
|
19
19
|
const assetVersions_1 = require("../../services/assetVersions");
|
|
20
20
|
const deliverables_1 = require("../../services/deliverables");
|
|
21
|
-
const
|
|
22
|
-
const VALID_3D_EXTENSIONS = new Set(['.glb', '.gltf', '.obj', '.fbx', '.stl', '.ply']);
|
|
23
|
-
function validateContentType(contentType, filename, category) {
|
|
24
|
-
if (category === 'images')
|
|
25
|
-
return contentType.startsWith('image/');
|
|
26
|
-
if (category === 'audios')
|
|
27
|
-
return contentType.startsWith('audio/');
|
|
28
|
-
if (category === 'videos')
|
|
29
|
-
return contentType.startsWith('video/');
|
|
30
|
-
if (category === '3d-models') {
|
|
31
|
-
const ext = filename.split('.').pop()?.toLowerCase();
|
|
32
|
-
return ext ? VALID_3D_EXTENSIONS.has(`.${ext}`) : false;
|
|
33
|
-
}
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
21
|
+
const assetCategories_1 = require("../../utils/assetCategories");
|
|
36
22
|
function getRawFilesCollection() {
|
|
37
23
|
const model = (0, manager_1.getModel)('storage_files', 'nosql');
|
|
38
24
|
const connectionIds = model.getConnectionIds();
|
|
@@ -113,13 +99,13 @@ function storageRouter(config) {
|
|
|
113
99
|
const auth = req.auth;
|
|
114
100
|
const { projectId, category } = req.params;
|
|
115
101
|
const { fileName, contentType } = req.query;
|
|
116
|
-
if (!VALID_CATEGORIES.includes(category)) {
|
|
117
|
-
return res.status(400).json({ message: `Invalid category. Must be one of: ${VALID_CATEGORIES.join(', ')}` });
|
|
102
|
+
if (!assetCategories_1.VALID_CATEGORIES.includes(category)) {
|
|
103
|
+
return res.status(400).json({ message: `Invalid category. Must be one of: ${assetCategories_1.VALID_CATEGORIES.join(', ')}` });
|
|
118
104
|
}
|
|
119
105
|
if (!fileName || !contentType) {
|
|
120
106
|
return res.status(400).json({ message: 'Query params fileName and contentType are required' });
|
|
121
107
|
}
|
|
122
|
-
if (!validateContentType(contentType, fileName, category)) {
|
|
108
|
+
if (!(0, assetCategories_1.validateContentType)(contentType, fileName, category)) {
|
|
123
109
|
const hint = category === '3d-models'
|
|
124
110
|
? '.glb, .gltf, .obj, .fbx, .stl, or .ply'
|
|
125
111
|
: `${category.slice(0, -1)}/* content type`;
|
|
@@ -179,7 +165,7 @@ function storageRouter(config) {
|
|
|
179
165
|
const fileDoc = await collection.findOne({ fileId, projectId });
|
|
180
166
|
if (!fileDoc)
|
|
181
167
|
return res.status(404).json({ message: 'File not found' });
|
|
182
|
-
if (!validateContentType(contentType, fileName, fileDoc.category)) {
|
|
168
|
+
if (!(0, assetCategories_1.validateContentType)(contentType, fileName, fileDoc.category)) {
|
|
183
169
|
return res.status(400).json({ message: `File does not match category ${fileDoc.category}` });
|
|
184
170
|
}
|
|
185
171
|
if (!adapter.getPresignedUploadUrl) {
|
|
@@ -277,8 +263,8 @@ function storageRouter(config) {
|
|
|
277
263
|
try {
|
|
278
264
|
const auth = req.auth;
|
|
279
265
|
const { projectId, category } = req.params;
|
|
280
|
-
if (!VALID_CATEGORIES.includes(category)) {
|
|
281
|
-
return res.status(400).json({ message: `Invalid category. Must be one of: ${VALID_CATEGORIES.join(', ')}` });
|
|
266
|
+
if (!assetCategories_1.VALID_CATEGORIES.includes(category)) {
|
|
267
|
+
return res.status(400).json({ message: `Invalid category. Must be one of: ${assetCategories_1.VALID_CATEGORIES.join(', ')}` });
|
|
282
268
|
}
|
|
283
269
|
const project = await Project.findOne({ _id: projectId });
|
|
284
270
|
if (!project)
|
|
@@ -350,13 +336,13 @@ function storageRouter(config) {
|
|
|
350
336
|
const auth = req.auth;
|
|
351
337
|
const { projectId, connId } = req.params;
|
|
352
338
|
const { fileName, contentType, category } = req.query;
|
|
353
|
-
if (!category || !VALID_CATEGORIES.includes(category)) {
|
|
354
|
-
return res.status(400).json({ message: `Invalid category. Must be one of: ${VALID_CATEGORIES.join(', ')}` });
|
|
339
|
+
if (!category || !assetCategories_1.VALID_CATEGORIES.includes(category)) {
|
|
340
|
+
return res.status(400).json({ message: `Invalid category. Must be one of: ${assetCategories_1.VALID_CATEGORIES.join(', ')}` });
|
|
355
341
|
}
|
|
356
342
|
if (!fileName || !contentType) {
|
|
357
343
|
return res.status(400).json({ message: 'Query params fileName and contentType are required' });
|
|
358
344
|
}
|
|
359
|
-
if (!validateContentType(contentType, fileName, category)) {
|
|
345
|
+
if (!(0, assetCategories_1.validateContentType)(contentType, fileName, category)) {
|
|
360
346
|
return res.status(400).json({ message: `File does not match category ${category}` });
|
|
361
347
|
}
|
|
362
348
|
const project = await Project.findOne({ _id: projectId });
|
|
@@ -424,8 +410,8 @@ function storageRouter(config) {
|
|
|
424
410
|
try {
|
|
425
411
|
const auth = req.auth;
|
|
426
412
|
const { projectId, connId, category } = req.params;
|
|
427
|
-
if (!VALID_CATEGORIES.includes(category)) {
|
|
428
|
-
return res.status(400).json({ message: `Invalid category. Must be one of: ${VALID_CATEGORIES.join(', ')}` });
|
|
413
|
+
if (!assetCategories_1.VALID_CATEGORIES.includes(category)) {
|
|
414
|
+
return res.status(400).json({ message: `Invalid category. Must be one of: ${assetCategories_1.VALID_CATEGORIES.join(', ')}` });
|
|
429
415
|
}
|
|
430
416
|
const project = await Project.findOne({ _id: projectId });
|
|
431
417
|
if (!project)
|
|
@@ -465,7 +451,7 @@ function storageRouter(config) {
|
|
|
465
451
|
const fileDoc = await collection.findOne(connFilter(projectId, connId, project, { fileId }));
|
|
466
452
|
if (!fileDoc)
|
|
467
453
|
return res.status(404).json({ message: 'File not found' });
|
|
468
|
-
if (!validateContentType(contentType, fileName, fileDoc.category)) {
|
|
454
|
+
if (!(0, assetCategories_1.validateContentType)(contentType, fileName, fileDoc.category)) {
|
|
469
455
|
return res.status(400).json({ message: `File does not match category ${fileDoc.category}` });
|
|
470
456
|
}
|
|
471
457
|
const connAdapter = await resolveConnAdapter(connId, project, auth);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Router, Request } from 'express';
|
|
2
|
+
import { StorageAdapter, StreamByConfig } from '../../types';
|
|
3
|
+
/** Where workers call back to: this router, as the request reached it (unless the host says). */
|
|
4
|
+
export declare const callbackBaseOf: (req: Request, config: StreamByConfig) => string;
|
|
5
|
+
export declare function workerRouter(config: StreamByConfig & {
|
|
6
|
+
adapter?: StorageAdapter;
|
|
7
|
+
}): Router;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.callbackBaseOf = void 0;
|
|
4
|
+
exports.workerRouter = workerRouter;
|
|
5
|
+
const express_1 = require("express");
|
|
6
|
+
const manager_1 = require("../../models/manager");
|
|
7
|
+
const auth_1 = require("../../utils/auth");
|
|
8
|
+
const jobQueue_1 = require("../../services/jobQueue");
|
|
9
|
+
const workers_1 = require("../../services/workers");
|
|
10
|
+
const workerJobs_1 = require("../../services/workerJobs");
|
|
11
|
+
/** Where workers call back to: this router, as the request reached it (unless the host says). */
|
|
12
|
+
const callbackBaseOf = (req, config) => config.workerOptions?.callbackBaseUrl ?? `${req.protocol}://${req.get('host')}${req.baseUrl}`;
|
|
13
|
+
exports.callbackBaseOf = callbackBaseOf;
|
|
14
|
+
// A project's processing workers (its own and the host's), and its jobs: asking a worker to
|
|
15
|
+
// process an asset, following them, and retrying a failed one.
|
|
16
|
+
function workerRouter(config) {
|
|
17
|
+
const router = (0, express_1.Router)();
|
|
18
|
+
const Project = (0, manager_1.getModel)('projects');
|
|
19
|
+
const failed = (res, what, err) => {
|
|
20
|
+
if (err instanceof workers_1.WorkerError)
|
|
21
|
+
return res.status(err.status).json({ message: err.message });
|
|
22
|
+
res.status(500).json({ message: `Failed to ${what}`, details: err?.message });
|
|
23
|
+
};
|
|
24
|
+
const projectFor = async (req, res, edit) => {
|
|
25
|
+
const auth = req.auth;
|
|
26
|
+
const project = await Project.findOne({ _id: req.params.projectId });
|
|
27
|
+
if (!project) {
|
|
28
|
+
res.status(404).json({ message: 'Project not found' });
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (!(edit ? auth_1.canEditProject : auth_1.isProjectMember)(project, auth.userId)) {
|
|
32
|
+
res.status(403).json({ message: 'Unauthorized' });
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return project;
|
|
36
|
+
};
|
|
37
|
+
// ── Workers ──
|
|
38
|
+
router.get('/projects/:projectId/workers', async (req, res) => {
|
|
39
|
+
try {
|
|
40
|
+
const project = await projectFor(req, res, false);
|
|
41
|
+
if (!project)
|
|
42
|
+
return;
|
|
43
|
+
res.json({ data: await (0, workers_1.availableWorkers)(project, req.auth, config) });
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
failed(res, 'list workers', err);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
router.post('/projects/:projectId/workers', async (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
const auth = req.auth;
|
|
52
|
+
const project = await projectFor(req, res, true);
|
|
53
|
+
if (!project)
|
|
54
|
+
return;
|
|
55
|
+
const worker = (0, workers_1.newWorkerConnection)(req.body ?? {}, req.params.projectId, auth.userId, config);
|
|
56
|
+
await (0, workers_1.assertPublicEndpoint)(worker.apiUrl, config);
|
|
57
|
+
await Project.update({ _id: req.params.projectId }, { $push: { renderFarmConnections: worker } });
|
|
58
|
+
res.status(201).json({ data: (0, workers_1.summarizeProjectWorker)(worker) });
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
failed(res, 'connect the worker', err);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
router.delete('/projects/:projectId/workers/:workerId', async (req, res) => {
|
|
65
|
+
try {
|
|
66
|
+
const project = await projectFor(req, res, true);
|
|
67
|
+
if (!project)
|
|
68
|
+
return;
|
|
69
|
+
if (!(0, workers_1.projectWorkers)(project).some(w => w.id === req.params.workerId))
|
|
70
|
+
return res.status(404).json({ message: 'Worker not found' });
|
|
71
|
+
await Project.update({ _id: req.params.projectId }, { $pull: { renderFarmConnections: { id: req.params.workerId } } });
|
|
72
|
+
res.json({ message: 'Worker removed' });
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
failed(res, 'remove the worker', err);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
// ── Jobs ──
|
|
79
|
+
router.get('/projects/:projectId/jobs', async (req, res) => {
|
|
80
|
+
try {
|
|
81
|
+
const project = await projectFor(req, res, false);
|
|
82
|
+
if (!project)
|
|
83
|
+
return;
|
|
84
|
+
const assetId = typeof req.query.assetId === 'string' ? req.query.assetId : undefined;
|
|
85
|
+
res.json({ data: (await (0, jobQueue_1.listJobs)(req.params.projectId, assetId)).map(workerJobs_1.publicJob) });
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
failed(res, 'list jobs', err);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
// A worker processes an asset (or one of its versions): { assetId, versionId?, worker:
|
|
92
|
+
// { source, id }, kind, profile?, resultMode?: 'derived' | 'version' }.
|
|
93
|
+
router.post('/projects/:projectId/jobs/process', async (req, res) => {
|
|
94
|
+
try {
|
|
95
|
+
const project = await projectFor(req, res, true);
|
|
96
|
+
if (!project)
|
|
97
|
+
return;
|
|
98
|
+
const job = await (0, workerJobs_1.enqueueWorkerJob)(project, req.body ?? {}, req.auth, config, (0, exports.callbackBaseOf)(req, config));
|
|
99
|
+
res.status(202).json({ job: (0, workerJobs_1.publicJob)(job) });
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
failed(res, 'start the job', err);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
router.post('/projects/:projectId/jobs/:jobId/retry', async (req, res) => {
|
|
106
|
+
try {
|
|
107
|
+
const project = await projectFor(req, res, true);
|
|
108
|
+
if (!project)
|
|
109
|
+
return;
|
|
110
|
+
const job = await (0, jobQueue_1.getJob)(req.params.jobId);
|
|
111
|
+
if (!job || job.projectId !== req.params.projectId)
|
|
112
|
+
return res.status(404).json({ message: 'Job not found' });
|
|
113
|
+
if (!job.worker)
|
|
114
|
+
return res.status(400).json({ message: 'Only a worker job can be retried' });
|
|
115
|
+
const retried = await (0, workerJobs_1.retryWorkerJob)(job.jobId, config);
|
|
116
|
+
if (!retried)
|
|
117
|
+
return res.status(409).json({ message: `Only a failed job can be retried; this one is ${job.status}` });
|
|
118
|
+
res.status(202).json({ job: (0, workerJobs_1.publicJob)(retried) });
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
failed(res, 'retry the job', err);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
return router;
|
|
125
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.workerCallbackRouter = workerCallbackRouter;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const workers_1 = require("../../services/workers");
|
|
6
|
+
const workerJobs_1 = require("../../services/workerJobs");
|
|
7
|
+
// A worker calling back about the job it was sent: with no session, only the job's own token
|
|
8
|
+
// (`Authorization: Bearer <callback.token>`), which works for that attempt alone.
|
|
9
|
+
function workerCallbackRouter(config) {
|
|
10
|
+
const router = (0, express_1.Router)();
|
|
11
|
+
const handle = (action) => async (req, res) => {
|
|
12
|
+
try {
|
|
13
|
+
const job = await (0, workerJobs_1.authorizeCallback)(req.params.jobId, req.headers.authorization);
|
|
14
|
+
res.json(await action(job, req));
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
if (err instanceof workers_1.WorkerError)
|
|
18
|
+
return res.status(err.status).json({ message: err.message });
|
|
19
|
+
console.error('❌ StreamBy workers: callback failed:', err?.message ?? err);
|
|
20
|
+
res.status(500).json({ message: 'Callback failed' });
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
router.post(`${workerJobs_1.WORKER_CALLBACK_PATH}/:jobId/progress`, handle(async (job, req) => ({ job: (0, workerJobs_1.publicJob)((await (0, workerJobs_1.reportProgress)(job, req.body)) ?? job) })));
|
|
24
|
+
router.post(`${workerJobs_1.WORKER_CALLBACK_PATH}/:jobId/upload-url`, handle((job, req) => (0, workerJobs_1.resultUploadUrl)(job, req.body, config)));
|
|
25
|
+
router.post(`${workerJobs_1.WORKER_CALLBACK_PATH}/:jobId/complete`, handle(async (job, req) => ({ job: (0, workerJobs_1.publicJob)(await (0, workerJobs_1.completeJob)(job, req.body, config)) })));
|
|
26
|
+
router.post(`${workerJobs_1.WORKER_CALLBACK_PATH}/:jobId/fail`, handle(async (job, req) => {
|
|
27
|
+
const after = await (0, workerJobs_1.failWorkerJob)(job, req.body, config);
|
|
28
|
+
return { job: after ? (0, workerJobs_1.publicJob)(after) : null };
|
|
29
|
+
}));
|
|
30
|
+
return router;
|
|
31
|
+
}
|
|
@@ -13,7 +13,7 @@ function getNosqlDb() {
|
|
|
13
13
|
return (0, connectionManager_1.getConnection)(ids[0]).client.db();
|
|
14
14
|
}
|
|
15
15
|
async function runTranscriptionJob(jobId, fileId, projectId, options) {
|
|
16
|
-
(0, jobQueue_1.updateJob)(jobId, { status: 'processing', stage: 'resolving', progress: 10, assetId: fileId });
|
|
16
|
+
await (0, jobQueue_1.updateJob)(jobId, { status: 'processing', stage: 'resolving', progress: 10, assetId: fileId });
|
|
17
17
|
try {
|
|
18
18
|
const db = getNosqlDb();
|
|
19
19
|
if (!db)
|
|
@@ -24,7 +24,7 @@ async function runTranscriptionJob(jobId, fileId, projectId, options) {
|
|
|
24
24
|
if (!fileDoc.contentType?.startsWith('video/') && !fileDoc.contentType?.startsWith('audio/')) {
|
|
25
25
|
throw new Error('Transcription only supports video and audio files');
|
|
26
26
|
}
|
|
27
|
-
(0, jobQueue_1.updateJob)(jobId, { stage: 'queued-provider', progress: 20 });
|
|
27
|
+
await (0, jobQueue_1.updateJob)(jobId, { stage: 'queued-provider', progress: 20 });
|
|
28
28
|
await db.collection('media_metadata').updateOne({ assetId: fileId, projectId }, {
|
|
29
29
|
$set: {
|
|
30
30
|
assetId: fileId,
|
|
@@ -41,14 +41,14 @@ async function runTranscriptionJob(jobId, fileId, projectId, options) {
|
|
|
41
41
|
extractedAt: new Date(),
|
|
42
42
|
},
|
|
43
43
|
}, { upsert: true });
|
|
44
|
-
(0, jobQueue_1.updateJob)(jobId, { stage: 'pending-worker', progress: 30 });
|
|
44
|
+
await (0, jobQueue_1.updateJob)(jobId, { stage: 'pending-worker', progress: 30 });
|
|
45
45
|
}
|
|
46
46
|
catch (err) {
|
|
47
|
-
(0, jobQueue_1.failJob)(jobId, err.message);
|
|
47
|
+
await (0, jobQueue_1.failJob)(jobId, err.message);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
async function runUpscaleJob(jobId, fileId, projectId, options) {
|
|
51
|
-
(0, jobQueue_1.updateJob)(jobId, { status: 'processing', stage: 'resolving', progress: 10, assetId: fileId });
|
|
51
|
+
await (0, jobQueue_1.updateJob)(jobId, { status: 'processing', stage: 'resolving', progress: 10, assetId: fileId });
|
|
52
52
|
try {
|
|
53
53
|
const db = getNosqlDb();
|
|
54
54
|
if (!db)
|
|
@@ -56,7 +56,7 @@ async function runUpscaleJob(jobId, fileId, projectId, options) {
|
|
|
56
56
|
const fileDoc = await db.collection('storage_files').findOne({ fileId, projectId });
|
|
57
57
|
if (!fileDoc)
|
|
58
58
|
throw new Error(`File ${fileId} not found`);
|
|
59
|
-
(0, jobQueue_1.updateJob)(jobId, { stage: 'queued-upscale', progress: 20 });
|
|
59
|
+
await (0, jobQueue_1.updateJob)(jobId, { stage: 'queued-upscale', progress: 20 });
|
|
60
60
|
await db.collection('media_metadata').updateOne({ assetId: fileId, projectId }, {
|
|
61
61
|
$set: {
|
|
62
62
|
assetId: fileId,
|
|
@@ -72,14 +72,14 @@ async function runUpscaleJob(jobId, fileId, projectId, options) {
|
|
|
72
72
|
extractedAt: new Date(),
|
|
73
73
|
},
|
|
74
74
|
}, { upsert: true });
|
|
75
|
-
(0, jobQueue_1.updateJob)(jobId, { stage: 'pending-worker', progress: 30 });
|
|
75
|
+
await (0, jobQueue_1.updateJob)(jobId, { stage: 'pending-worker', progress: 30 });
|
|
76
76
|
}
|
|
77
77
|
catch (err) {
|
|
78
|
-
(0, jobQueue_1.failJob)(jobId, err.message);
|
|
78
|
+
await (0, jobQueue_1.failJob)(jobId, err.message);
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
async function runGenerateAssetJob(jobId, projectId, options) {
|
|
82
|
-
(0, jobQueue_1.updateJob)(jobId, { status: 'processing', stage: 'queued-generation', progress: 10 });
|
|
82
|
+
await (0, jobQueue_1.updateJob)(jobId, { status: 'processing', stage: 'queued-generation', progress: 10 });
|
|
83
83
|
try {
|
|
84
84
|
const db = getNosqlDb();
|
|
85
85
|
if (!db)
|
|
@@ -95,10 +95,10 @@ async function runGenerateAssetJob(jobId, projectId, options) {
|
|
|
95
95
|
status: 'pending',
|
|
96
96
|
requestedAt: new Date(),
|
|
97
97
|
});
|
|
98
|
-
(0, jobQueue_1.updateJob)(jobId, { stage: 'pending-worker', progress: 20 });
|
|
98
|
+
await (0, jobQueue_1.updateJob)(jobId, { stage: 'pending-worker', progress: 20 });
|
|
99
99
|
}
|
|
100
100
|
catch (err) {
|
|
101
|
-
(0, jobQueue_1.failJob)(jobId, err.message);
|
|
101
|
+
await (0, jobQueue_1.failJob)(jobId, err.message);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
async function buildPipelineSuggestion(nodeSchema, availableNodeTypes) {
|
|
@@ -39,7 +39,7 @@ const RULES = [
|
|
|
39
39
|
rule('read', '^/users/search', 'members:manage'),
|
|
40
40
|
rule('read', '^/jobs/[^/]+', 'jobs:read'),
|
|
41
41
|
// The project itself and its settings.
|
|
42
|
-
rule('read', `${P}(/preview)?`, 'project:read'),
|
|
42
|
+
rule('read', `${P}(/preview|/origins|/integrations)?`, 'project:read'),
|
|
43
43
|
rule(['PATCH'], P, 'project:update'),
|
|
44
44
|
rule('delete', P, 'project:delete'),
|
|
45
45
|
rule(['PATCH'], `${P}/(archive|unarchive)`, 'project:update', { userTokenOnly: true }),
|
|
@@ -74,10 +74,10 @@ const RULES = [
|
|
|
74
74
|
// Rights.
|
|
75
75
|
rule('read', `${P}/assets/[^/]+/rights`, 'rights:read'),
|
|
76
76
|
rule(['PUT'], `${P}/assets/[^/]+/rights`, 'rights:write'),
|
|
77
|
-
// Connections (API, database, storage, distribution,
|
|
78
|
-
rule('read', `${P}/(connections|get-connection|distribution-connections|
|
|
79
|
-
rule('write', `${P}/(connections|distribution-connections|
|
|
80
|
-
rule('delete', `${P}/(connections|distribution-connections|
|
|
77
|
+
// Connections (API, database, storage, distribution, workers): the project's settings.
|
|
78
|
+
rule('read', `${P}/(connections|get-connection|distribution-connections|workers)(/.+)?`, 'project:read'),
|
|
79
|
+
rule('write', `${P}/(connections|distribution-connections|workers)(/.+)?`, 'project:update'),
|
|
80
|
+
rule('delete', `${P}/(connections|distribution-connections|workers)/.+`, 'project:update'),
|
|
81
81
|
// Production.
|
|
82
82
|
rule('read', `${P}/production/.+`, 'production:read'),
|
|
83
83
|
rule('write', `${P}/production/.+`, 'production:write'),
|
|
@@ -88,11 +88,12 @@ const RULES = [
|
|
|
88
88
|
rule(['POST'], `${P}/pipelines`, 'workflow:write'),
|
|
89
89
|
rule(['PATCH', 'DELETE'], `${P}/pipelines/[^/]+`, 'workflow:write'),
|
|
90
90
|
// Exports.
|
|
91
|
-
rule('read', `${P}/exports/[^/]
|
|
91
|
+
rule('read', `${P}/exports(/[^/]+)?`, 'exports:read'),
|
|
92
92
|
rule(['POST'], `${P}/exports`, 'exports:write'),
|
|
93
93
|
rule(['PATCH', 'DELETE'], `${P}/exports/[^/]+`, 'exports:write'),
|
|
94
94
|
// Jobs: transcoding, rendering, AI, publishing a deliverable.
|
|
95
|
-
rule(
|
|
95
|
+
rule('read', `${P}/jobs`, 'jobs:read'),
|
|
96
|
+
rule(['POST'], `${P}/(jobs/[^/]+(/retry)?|pipeline-suggest|deliverables/[^/]+/publish)`, 'jobs:run'),
|
|
96
97
|
];
|
|
97
98
|
/** The rule for a request made with an API token, or undefined: not reachable with one. */
|
|
98
99
|
function routeRuleFor(method, path) {
|
|
@@ -216,7 +216,7 @@ async function executeExport(nodeSchema, project, config, { public: isPublic = t
|
|
|
216
216
|
if (isPublic && JOB_NODES.has(processNode.type))
|
|
217
217
|
throw notPublic(processNode);
|
|
218
218
|
if (processNode.type === 'transcodeNode' && fileId) {
|
|
219
|
-
const job = (0, jobQueue_1.createJob)('transcode', systemUserId, projectId, nodeData);
|
|
219
|
+
const job = await (0, jobQueue_1.createJob)('transcode', systemUserId, projectId, nodeData);
|
|
220
220
|
setImmediate(() => (0, mediaProcessor_1.runTranscodeJob)(job.jobId, fileId, projectId, {
|
|
221
221
|
codec: nodeData.codec,
|
|
222
222
|
resolution: nodeData.resolution,
|
|
@@ -227,7 +227,7 @@ async function executeExport(nodeSchema, project, config, { public: isPublic = t
|
|
|
227
227
|
payload = { ...payload, jobId: job.jobId, jobType: 'transcode' };
|
|
228
228
|
}
|
|
229
229
|
else if (processNode.type === 'captionNode' && fileId) {
|
|
230
|
-
const job = (0, jobQueue_1.createJob)('caption', systemUserId, projectId, nodeData);
|
|
230
|
+
const job = await (0, jobQueue_1.createJob)('caption', systemUserId, projectId, nodeData);
|
|
231
231
|
setImmediate(() => (0, mediaProcessor_1.runCaptionJob)(job.jobId, fileId, projectId, {
|
|
232
232
|
sourceLanguage: nodeData.sourceLanguage,
|
|
233
233
|
outputFormat: nodeData.outputFormat,
|
|
@@ -236,7 +236,7 @@ async function executeExport(nodeSchema, project, config, { public: isPublic = t
|
|
|
236
236
|
payload = { ...payload, jobId: job.jobId, jobType: 'caption' };
|
|
237
237
|
}
|
|
238
238
|
else if (processNode.type === 'thumbnailNode' && fileId) {
|
|
239
|
-
const job = (0, jobQueue_1.createJob)('thumbnail', systemUserId, projectId, nodeData);
|
|
239
|
+
const job = await (0, jobQueue_1.createJob)('thumbnail', systemUserId, projectId, nodeData);
|
|
240
240
|
setImmediate(() => (0, mediaProcessor_1.runThumbnailJob)(job.jobId, fileId, projectId, {
|
|
241
241
|
timecode: nodeData.timecode,
|
|
242
242
|
resolution: nodeData.resolution,
|
|
@@ -256,12 +256,12 @@ async function executeExport(nodeSchema, project, config, { public: isPublic = t
|
|
|
256
256
|
if ('error' in storageResolved) {
|
|
257
257
|
throw new NodeExecutionError(processNode.id, processNode.type, `Node '${processNode.id}' (${processNode.type}): ${storageResolved.error}`);
|
|
258
258
|
}
|
|
259
|
-
const job = (0, jobQueue_1.createJob)('ingest', systemUserId, projectId, nodeData);
|
|
259
|
+
const job = await (0, jobQueue_1.createJob)('ingest', systemUserId, projectId, nodeData);
|
|
260
260
|
setImmediate(() => (0, mediaProcessor_1.runIngestJob)(job.jobId, fileId, projectId));
|
|
261
261
|
payload = { ...payload, jobId: job.jobId, jobType: 'ingest' };
|
|
262
262
|
}
|
|
263
263
|
else if (processNode.type === 'renderJobNode' && fileId) {
|
|
264
|
-
const job = (0, jobQueue_1.createJob)('render', systemUserId, projectId, nodeData);
|
|
264
|
+
const job = await (0, jobQueue_1.createJob)('render', systemUserId, projectId, nodeData);
|
|
265
265
|
setImmediate(() => (0, vfxProcessor_1.runRenderJob)(job.jobId, fileId, projectId, {
|
|
266
266
|
renderer: nodeData.renderer,
|
|
267
267
|
renderFarmConnectionId: nodeData.renderFarmConnectionId,
|
|
@@ -273,7 +273,7 @@ async function executeExport(nodeSchema, project, config, { public: isPublic = t
|
|
|
273
273
|
payload = { ...payload, jobId: job.jobId, jobType: 'render' };
|
|
274
274
|
}
|
|
275
275
|
else if (processNode.type === 'formatConvertNode' && fileId) {
|
|
276
|
-
const job = (0, jobQueue_1.createJob)('format-convert', systemUserId, projectId, nodeData);
|
|
276
|
+
const job = await (0, jobQueue_1.createJob)('format-convert', systemUserId, projectId, nodeData);
|
|
277
277
|
setImmediate(() => (0, vfxProcessor_1.runFormatConvertJob)(job.jobId, fileId, projectId, {
|
|
278
278
|
inputFormat: nodeData.inputFormat,
|
|
279
279
|
outputFormat: nodeData.outputFormat,
|
|
@@ -283,7 +283,7 @@ async function executeExport(nodeSchema, project, config, { public: isPublic = t
|
|
|
283
283
|
payload = { ...payload, jobId: job.jobId, jobType: 'format-convert' };
|
|
284
284
|
}
|
|
285
285
|
else if (processNode.type === 'lodNode' && fileId) {
|
|
286
|
-
const job = (0, jobQueue_1.createJob)('lod', systemUserId, projectId, nodeData);
|
|
286
|
+
const job = await (0, jobQueue_1.createJob)('lod', systemUserId, projectId, nodeData);
|
|
287
287
|
setImmediate(() => (0, vfxProcessor_1.runLodJob)(job.jobId, fileId, projectId, {
|
|
288
288
|
levels: nodeData.levels,
|
|
289
289
|
reductionRatios: nodeData.reductionRatios,
|
|
@@ -300,7 +300,7 @@ async function executeExport(nodeSchema, project, config, { public: isPublic = t
|
|
|
300
300
|
payload = { ...payload, qcReport: { checks: checkResults, overallPassed: true } };
|
|
301
301
|
}
|
|
302
302
|
else if (processNode.type === 'transcriptionNode' && fileId) {
|
|
303
|
-
const job = (0, jobQueue_1.createJob)('transcription', systemUserId, projectId, nodeData);
|
|
303
|
+
const job = await (0, jobQueue_1.createJob)('transcription', systemUserId, projectId, nodeData);
|
|
304
304
|
setImmediate(() => (0, aiProcessor_1.runTranscriptionJob)(job.jobId, fileId, projectId, {
|
|
305
305
|
model: nodeData.model,
|
|
306
306
|
sourceLanguage: nodeData.sourceLanguage,
|
|
@@ -311,7 +311,7 @@ async function executeExport(nodeSchema, project, config, { public: isPublic = t
|
|
|
311
311
|
payload = { ...payload, jobId: job.jobId, jobType: 'transcription' };
|
|
312
312
|
}
|
|
313
313
|
else if (processNode.type === 'upscaleNode' && fileId) {
|
|
314
|
-
const job = (0, jobQueue_1.createJob)('upscale', systemUserId, projectId, nodeData);
|
|
314
|
+
const job = await (0, jobQueue_1.createJob)('upscale', systemUserId, projectId, nodeData);
|
|
315
315
|
setImmediate(() => (0, aiProcessor_1.runUpscaleJob)(job.jobId, fileId, projectId, {
|
|
316
316
|
scale: nodeData.scale,
|
|
317
317
|
model: nodeData.model,
|
|
@@ -321,7 +321,7 @@ async function executeExport(nodeSchema, project, config, { public: isPublic = t
|
|
|
321
321
|
payload = { ...payload, jobId: job.jobId, jobType: 'upscale' };
|
|
322
322
|
}
|
|
323
323
|
else if (processNode.type === 'proceduralAssetNode') {
|
|
324
|
-
const job = (0, jobQueue_1.createJob)('generate-asset', systemUserId, projectId, nodeData);
|
|
324
|
+
const job = await (0, jobQueue_1.createJob)('generate-asset', systemUserId, projectId, nodeData);
|
|
325
325
|
setImmediate(() => (0, aiProcessor_1.runGenerateAssetJob)(job.jobId, projectId, {
|
|
326
326
|
assetType: nodeData.assetType,
|
|
327
327
|
provider: nodeData.provider,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { JobRecord, JobType } from '../types';
|
|
2
|
-
export declare function createJob(jobType: JobType, userId: string, projectId: string, payload: Record<string, any>): JobRecord
|
|
3
|
-
export declare function getJob(jobId: string): JobRecord |
|
|
4
|
-
export declare function
|
|
5
|
-
export declare function
|
|
2
|
+
export declare function createJob(jobType: JobType, userId: string, projectId: string, payload: Record<string, any>, extra?: Partial<JobRecord>): Promise<JobRecord>;
|
|
3
|
+
export declare function getJob(jobId: string): Promise<JobRecord | null>;
|
|
4
|
+
export declare function listJobs(projectId: string, assetId?: string): Promise<JobRecord[]>;
|
|
5
|
+
export declare function notify(job: JobRecord): void;
|
|
6
|
+
export declare function updateJob(jobId: string, updates: Partial<JobRecord>): Promise<JobRecord | null>;
|
|
7
|
+
export declare function failJob(jobId: string, error: string): Promise<JobRecord | null>;
|