0.10.2 Download Files
This commit is contained in:
978
backend/package-lock.json
generated
978
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ripster-backend",
|
||||
"version": "0.10.1-1",
|
||||
"version": "0.10.2",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
@@ -8,6 +8,7 @@
|
||||
"dev": "nodemon src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
const express = require('express');
|
||||
const archiver = require('archiver');
|
||||
const asyncHandler = require('../middleware/asyncHandler');
|
||||
const historyService = require('../services/historyService');
|
||||
const pipelineService = require('../services/pipelineService');
|
||||
const logger = require('../services/logger').child('HISTORY_ROUTE');
|
||||
const { errorToMeta } = require('../utils/errorMeta');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -180,6 +182,72 @@ router.post(
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:id/download',
|
||||
asyncHandler(async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const target = String(req.query.target || '').trim();
|
||||
const descriptor = await historyService.getJobArchiveDescriptor(id, target);
|
||||
|
||||
logger.info('get:job:download', {
|
||||
reqId: req.reqId,
|
||||
id,
|
||||
target: descriptor.target,
|
||||
sourceType: descriptor.sourceType,
|
||||
sourcePath: descriptor.sourcePath
|
||||
});
|
||||
|
||||
const archive = archiver('zip', { zlib: { level: 9 } });
|
||||
|
||||
const handleArchiveError = (error) => {
|
||||
logger.error('get:job:download:failed', {
|
||||
reqId: req.reqId,
|
||||
id,
|
||||
target: descriptor.target,
|
||||
error: errorToMeta(error)
|
||||
});
|
||||
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
error: {
|
||||
message: 'ZIP-Download fehlgeschlagen.'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.destroy(error);
|
||||
};
|
||||
|
||||
archive.on('warning', handleArchiveError);
|
||||
archive.on('error', handleArchiveError);
|
||||
res.on('close', () => {
|
||||
if (!res.writableEnded) {
|
||||
archive.abort();
|
||||
}
|
||||
});
|
||||
|
||||
res.setHeader('Content-Type', 'application/zip');
|
||||
res.attachment(descriptor.archiveName);
|
||||
archive.pipe(res);
|
||||
|
||||
if (descriptor.sourceType === 'directory') {
|
||||
archive.directory(descriptor.sourcePath, descriptor.entryName);
|
||||
} else {
|
||||
archive.file(descriptor.sourcePath, { name: descriptor.entryName });
|
||||
}
|
||||
|
||||
try {
|
||||
const finalizeResult = archive.finalize();
|
||||
if (finalizeResult && typeof finalizeResult.catch === 'function') {
|
||||
finalizeResult.catch(handleArchiveError);
|
||||
}
|
||||
} catch (error) {
|
||||
handleArchiveError(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:id',
|
||||
asyncHandler(async (req, res) => {
|
||||
|
||||
@@ -764,6 +764,36 @@ function normalizeJobIdValue(value) {
|
||||
return Math.trunc(parsed);
|
||||
}
|
||||
|
||||
function normalizeArchiveTarget(value) {
|
||||
const raw = String(value || '').trim().toLowerCase();
|
||||
if (raw === 'raw') {
|
||||
return 'raw';
|
||||
}
|
||||
if (raw === 'output' || raw === 'movie' || raw === 'encode') {
|
||||
return 'output';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function sanitizeArchiveNamePart(value, fallback = 'job') {
|
||||
const normalized = String(value || '')
|
||||
.normalize('NFKD')
|
||||
.replace(/[^\x00-\x7F]+/g, '');
|
||||
const safe = normalized
|
||||
.replace(/[^A-Za-z0-9._-]+/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^[_-]+|[_-]+$/g, '')
|
||||
.slice(0, 80);
|
||||
return safe || fallback;
|
||||
}
|
||||
|
||||
function buildJobArchiveName(job, target) {
|
||||
const jobId = normalizeJobIdValue(job?.id) || 'unknown';
|
||||
const titlePart = sanitizeArchiveNamePart(job?.title || job?.detected_title || '', 'job');
|
||||
const targetPart = target === 'raw' ? 'raw' : 'encode';
|
||||
return `job-${jobId}-${titlePart}-${targetPart}.zip`;
|
||||
}
|
||||
|
||||
function parseSourceJobIdFromPlan(encodePlanRaw) {
|
||||
const plan = parseInfoFromValue(encodePlanRaw, null);
|
||||
const sourceJobId = normalizeJobIdValue(plan?.sourceJobId);
|
||||
@@ -1427,6 +1457,76 @@ class HistoryService {
|
||||
};
|
||||
}
|
||||
|
||||
async getJobArchiveDescriptor(jobId, target) {
|
||||
const normalizedJobId = normalizeJobIdValue(jobId);
|
||||
if (!normalizedJobId) {
|
||||
const error = new Error('Ungültige Job-ID.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const normalizedTarget = normalizeArchiveTarget(target);
|
||||
if (!normalizedTarget) {
|
||||
const error = new Error('Ungültiges Download-Ziel. Erlaubt sind raw und output.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const [job, settings] = await Promise.all([
|
||||
this.getJobById(normalizedJobId),
|
||||
settingsService.getSettingsMap()
|
||||
]);
|
||||
|
||||
if (!job) {
|
||||
const error = new Error('Job nicht gefunden.');
|
||||
error.statusCode = 404;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const resolvedPaths = resolveEffectiveStoragePathsForJob(settings, job);
|
||||
const sourcePath = normalizedTarget === 'raw'
|
||||
? resolvedPaths.effectiveRawPath
|
||||
: resolvedPaths.effectiveOutputPath;
|
||||
|
||||
if (!sourcePath) {
|
||||
const error = new Error(
|
||||
normalizedTarget === 'raw'
|
||||
? 'Kein RAW-Pfad für diesen Job vorhanden.'
|
||||
: 'Kein Output-Pfad für diesen Job vorhanden.'
|
||||
);
|
||||
error.statusCode = 404;
|
||||
throw error;
|
||||
}
|
||||
|
||||
let sourceStat;
|
||||
try {
|
||||
sourceStat = await fs.promises.stat(sourcePath);
|
||||
} catch (_error) {
|
||||
const error = new Error(
|
||||
normalizedTarget === 'raw'
|
||||
? 'RAW-Pfad wurde nicht gefunden.'
|
||||
: 'Output-Pfad wurde nicht gefunden.'
|
||||
);
|
||||
error.statusCode = 404;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!sourceStat.isDirectory() && !sourceStat.isFile()) {
|
||||
const error = new Error('Nur Dateien oder Verzeichnisse können als ZIP heruntergeladen werden.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
jobId: normalizedJobId,
|
||||
target: normalizedTarget,
|
||||
sourcePath,
|
||||
sourceType: sourceStat.isDirectory() ? 'directory' : 'file',
|
||||
entryName: path.basename(sourcePath) || (normalizedTarget === 'raw' ? 'raw' : 'output'),
|
||||
archiveName: buildJobArchiveName(job, normalizedTarget)
|
||||
};
|
||||
}
|
||||
|
||||
async getDatabaseRows(filters = {}) {
|
||||
const jobs = await this.getJobs(filters);
|
||||
return jobs.map((job) => ({
|
||||
|
||||
Reference in New Issue
Block a user