updated dependencies
This commit is contained in:
parent
d081be19a1
commit
7bfdae0519
43
backend/package-lock.json
generated
43
backend/package-lock.json
generated
@ -20,7 +20,6 @@
|
|||||||
"express": "^4.22.2",
|
"express": "^4.22.2",
|
||||||
"ffmpeg-static": "^5.2.0",
|
"ffmpeg-static": "^5.2.0",
|
||||||
"ffprobe-static": "^3.1.0",
|
"ffprobe-static": "^3.1.0",
|
||||||
"fluent-ffmpeg": "^2.1.3",
|
|
||||||
"helmet": "^8.0.0",
|
"helmet": "^8.0.0",
|
||||||
"joi": "^17.13.0",
|
"joi": "^17.13.0",
|
||||||
"json2csv": "^5.0.7",
|
"json2csv": "^5.0.7",
|
||||||
@ -47,7 +46,6 @@
|
|||||||
"@types/express": "^4.17.25",
|
"@types/express": "^4.17.25",
|
||||||
"@types/express-serve-static-core": "^4.19.8",
|
"@types/express-serve-static-core": "^4.19.8",
|
||||||
"@types/ffprobe-static": "^2.0.3",
|
"@types/ffprobe-static": "^2.0.3",
|
||||||
"@types/fluent-ffmpeg": "^2.1.28",
|
|
||||||
"@types/json2csv": "^5.0.7",
|
"@types/json2csv": "^5.0.7",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/multer": "^2.1.0",
|
"@types/multer": "^2.1.0",
|
||||||
@ -2300,16 +2298,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/fluent-ffmpeg": {
|
|
||||||
"version": "2.1.28",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.28.tgz",
|
|
||||||
"integrity": "sha512-5ovxsDwBcPfJ+eYs1I/ZpcYCnkce7pvH9AHSvrZllAp1ZPpTRDZAFjF3TRFbukxSgIYTTNYePbS0rKUmaxVbXw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/http-errors": {
|
"node_modules/@types/http-errors": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
||||||
@ -3517,11 +3505,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/async": {
|
|
||||||
"version": "0.2.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
|
|
||||||
"integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
|
|
||||||
},
|
|
||||||
"node_modules/async-function": {
|
"node_modules/async-function": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
|
||||||
@ -5538,19 +5521,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/fluent-ffmpeg": {
|
|
||||||
"version": "2.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz",
|
|
||||||
"integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"async": "^0.2.9",
|
|
||||||
"which": "^1.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/for-each": {
|
"node_modules/for-each": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||||
@ -6667,6 +6637,7 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/jju": {
|
"node_modules/jju": {
|
||||||
@ -9831,18 +9802,6 @@
|
|||||||
"webidl-conversions": "^3.0.0"
|
"webidl-conversions": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/which": {
|
|
||||||
"version": "1.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
|
||||||
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"isexe": "^2.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"which": "bin/which"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/which-boxed-primitive": {
|
"node_modules/which-boxed-primitive": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
|
||||||
|
|||||||
@ -43,7 +43,6 @@
|
|||||||
"express": "^4.22.2",
|
"express": "^4.22.2",
|
||||||
"ffmpeg-static": "^5.2.0",
|
"ffmpeg-static": "^5.2.0",
|
||||||
"ffprobe-static": "^3.1.0",
|
"ffprobe-static": "^3.1.0",
|
||||||
"fluent-ffmpeg": "^2.1.3",
|
|
||||||
"helmet": "^8.0.0",
|
"helmet": "^8.0.0",
|
||||||
"joi": "^17.13.0",
|
"joi": "^17.13.0",
|
||||||
"json2csv": "^5.0.7",
|
"json2csv": "^5.0.7",
|
||||||
@ -85,7 +84,6 @@
|
|||||||
"@types/express": "^4.17.25",
|
"@types/express": "^4.17.25",
|
||||||
"@types/express-serve-static-core": "^4.19.8",
|
"@types/express-serve-static-core": "^4.19.8",
|
||||||
"@types/ffprobe-static": "^2.0.3",
|
"@types/ffprobe-static": "^2.0.3",
|
||||||
"@types/fluent-ffmpeg": "^2.1.28",
|
|
||||||
"@types/json2csv": "^5.0.7",
|
"@types/json2csv": "^5.0.7",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/multer": "^2.1.0",
|
"@types/multer": "^2.1.0",
|
||||||
|
|||||||
@ -6,11 +6,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
import { createRequire } from 'node:module';
|
import { createRequire } from 'node:module';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import ffmpeg from 'fluent-ffmpeg';
|
|
||||||
import { path as ffprobePath } from 'ffprobe-static';
|
import { path as ffprobePath } from 'ffprobe-static';
|
||||||
|
|
||||||
import config from '../config.ts';
|
import config from '../config.ts';
|
||||||
@ -22,11 +22,6 @@ const loadCommonJsModule = createRequire(import.meta.url);
|
|||||||
const ffmpegStaticValue: unknown = loadCommonJsModule('ffmpeg-static');
|
const ffmpegStaticValue: unknown = loadCommonJsModule('ffmpeg-static');
|
||||||
const ffmpegPath = typeof ffmpegStaticValue === 'string' ? ffmpegStaticValue : null;
|
const ffmpegPath = typeof ffmpegStaticValue === 'string' ? ffmpegStaticValue : null;
|
||||||
|
|
||||||
if (ffmpegPath) {
|
|
||||||
ffmpeg.setFfmpegPath(ffmpegPath);
|
|
||||||
}
|
|
||||||
ffmpeg.setFfprobePath(ffprobePath);
|
|
||||||
|
|
||||||
let ffmpegQueueTail: Promise<unknown> = Promise.resolve();
|
let ffmpegQueueTail: Promise<unknown> = Promise.resolve();
|
||||||
let queuedFfmpegJobs = 0;
|
let queuedFfmpegJobs = 0;
|
||||||
let ffmpegJobSequence = 0;
|
let ffmpegJobSequence = 0;
|
||||||
@ -41,6 +36,27 @@ const reverseVideoBreaker = new CircuitBreaker({
|
|||||||
const FFMPEG_REVERSE_TIMEOUT_MS = config.resilience.ffmpeg.reverseTimeoutMs;
|
const FFMPEG_REVERSE_TIMEOUT_MS = config.resilience.ffmpeg.reverseTimeoutMs;
|
||||||
const FFPROBE_TIMEOUT_MS = config.resilience.ffmpeg.ffprobeTimeoutMs;
|
const FFPROBE_TIMEOUT_MS = config.resilience.ffmpeg.ffprobeTimeoutMs;
|
||||||
|
|
||||||
|
interface MediaProbeStream {
|
||||||
|
codec_type?: string | undefined;
|
||||||
|
duration?: string | number | undefined;
|
||||||
|
width?: string | number | undefined;
|
||||||
|
height?: string | number | undefined;
|
||||||
|
avg_frame_rate?: string | number | undefined;
|
||||||
|
r_frame_rate?: string | number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MediaProbeOutput {
|
||||||
|
streams?: MediaProbeStream[] | undefined;
|
||||||
|
format?: {
|
||||||
|
duration?: string | number | undefined;
|
||||||
|
} | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProcessResult {
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
}
|
||||||
|
|
||||||
function parseFrameRate(value: unknown): number | null {
|
function parseFrameRate(value: unknown): number | null {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return null;
|
return null;
|
||||||
@ -86,6 +102,75 @@ function toError(value: unknown): Error {
|
|||||||
return value instanceof Error ? value : new Error(String(value));
|
return value instanceof Error ? value : new Error(String(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isMediaProbeOutput(value: unknown): value is MediaProbeOutput {
|
||||||
|
return Boolean(value) && typeof value === 'object';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFfmpegExecutablePath(): string {
|
||||||
|
if (!ffmpegPath) {
|
||||||
|
throw new Error('Bundled FFmpeg binary is unavailable');
|
||||||
|
}
|
||||||
|
return ffmpegPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runProcess(
|
||||||
|
executablePath: string,
|
||||||
|
args: string[],
|
||||||
|
options: { timeoutMs: number; timeoutMessage: string },
|
||||||
|
): Promise<ProcessResult> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn(executablePath, args, {
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
let settled = false;
|
||||||
|
|
||||||
|
const finish = (error: Error | null, result?: ProcessResult): void => {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settled = true;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(result ?? { stdout, stderr });
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
const error = new Error(options.timeoutMessage);
|
||||||
|
child.kill('SIGKILL');
|
||||||
|
finish(error);
|
||||||
|
}, options.timeoutMs);
|
||||||
|
timeout.unref();
|
||||||
|
|
||||||
|
child.stdout.setEncoding('utf8');
|
||||||
|
child.stderr.setEncoding('utf8');
|
||||||
|
child.stdout.on('data', (chunk: string) => {
|
||||||
|
stdout += chunk;
|
||||||
|
});
|
||||||
|
child.stderr.on('data', (chunk: string) => {
|
||||||
|
stderr += chunk;
|
||||||
|
});
|
||||||
|
child.on('error', (error) => finish(error));
|
||||||
|
child.on('close', (code, signal) => {
|
||||||
|
if (code === 0) {
|
||||||
|
finish(null, { stdout, stderr });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
finish(
|
||||||
|
new Error(
|
||||||
|
`Process failed: ${executablePath} exited with code ${code ?? 'null'} and signal ${signal ?? 'null'}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function enqueueFfmpegJob<TResult>(
|
async function enqueueFfmpegJob<TResult>(
|
||||||
jobName: string,
|
jobName: string,
|
||||||
runJob: FfmpegJobRunner<TResult>,
|
runJob: FfmpegJobRunner<TResult>,
|
||||||
@ -167,59 +252,44 @@ async function reverseVideoWithoutQueue(
|
|||||||
'Starting video reversal',
|
'Starting video reversal',
|
||||||
);
|
);
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
const ffmpegArgs = [
|
||||||
const command = ffmpeg(inputPath)
|
'-y',
|
||||||
.outputOptions([
|
'-i',
|
||||||
'-vf',
|
inputPath,
|
||||||
'reverse',
|
'-vf',
|
||||||
'-af',
|
'reverse',
|
||||||
'areverse',
|
'-af',
|
||||||
'-c:v',
|
'areverse',
|
||||||
'libx264',
|
'-c:v',
|
||||||
'-preset',
|
'libx264',
|
||||||
'fast',
|
'-preset',
|
||||||
'-crf',
|
'fast',
|
||||||
'23',
|
'-crf',
|
||||||
'-c:a',
|
'23',
|
||||||
'aac',
|
'-c:a',
|
||||||
'-threads',
|
'aac',
|
||||||
'1',
|
'-threads',
|
||||||
'-movflags',
|
'1',
|
||||||
'+faststart',
|
'-movflags',
|
||||||
])
|
'+faststart',
|
||||||
.output(outputPath)
|
outputPath,
|
||||||
.on('start', (cmd) => logger.debug({ cmd }, 'FFmpeg command'))
|
];
|
||||||
.on('progress', (progress) => {
|
|
||||||
if (progress.percent) {
|
|
||||||
logger.debug({ percent: progress.percent }, 'FFmpeg progress');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on('end', () => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
logger.info({ outputPath }, 'Video reversal complete');
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.on('error', (err, stdout, stderr) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
logger.error({ err, stdout, stderr }, 'FFmpeg error');
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
logger.debug(
|
||||||
const error = new Error(
|
{ cmd: [getFfmpegExecutablePath(), ...ffmpegArgs].join(' ') },
|
||||||
`FFmpeg reverse video timed out after ${FFMPEG_REVERSE_TIMEOUT_MS}ms`,
|
'FFmpeg command',
|
||||||
);
|
);
|
||||||
logger.error(
|
|
||||||
{ err: error, inputPath, outputPath },
|
|
||||||
'FFmpeg reverse video timeout',
|
|
||||||
);
|
|
||||||
command.kill('SIGKILL');
|
|
||||||
reject(error);
|
|
||||||
}, FFMPEG_REVERSE_TIMEOUT_MS);
|
|
||||||
timeout.unref();
|
|
||||||
|
|
||||||
command.run();
|
try {
|
||||||
});
|
await runProcess(getFfmpegExecutablePath(), ffmpegArgs, {
|
||||||
|
timeoutMs: FFMPEG_REVERSE_TIMEOUT_MS,
|
||||||
|
timeoutMessage: `FFmpeg reverse video timed out after ${FFMPEG_REVERSE_TIMEOUT_MS}ms`,
|
||||||
|
});
|
||||||
|
logger.info({ outputPath }, 'Video reversal complete');
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err, inputPath, outputPath }, 'FFmpeg error');
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
const reversedBuffer = await fs.readFile(outputPath);
|
const reversedBuffer = await fs.readFile(outputPath);
|
||||||
let outputMetadata: MediaMetadata | null = null;
|
let outputMetadata: MediaMetadata | null = null;
|
||||||
@ -253,59 +323,74 @@ async function reverseVideoWithoutQueue(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function probeMediaMetadata(filePath: string): Promise<MediaMetadata> {
|
async function probeMediaMetadata(filePath: string): Promise<MediaMetadata> {
|
||||||
return new Promise((resolve, reject) => {
|
const result = await runProcess(
|
||||||
const timeout = setTimeout(() => {
|
ffprobePath,
|
||||||
reject(
|
[
|
||||||
new Error(`FFprobe timed out after ${FFPROBE_TIMEOUT_MS}ms: ${filePath}`),
|
'-v',
|
||||||
);
|
'error',
|
||||||
}, FFPROBE_TIMEOUT_MS);
|
'-print_format',
|
||||||
timeout.unref();
|
'json',
|
||||||
|
'-show_format',
|
||||||
|
'-show_streams',
|
||||||
|
filePath,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
timeoutMs: FFPROBE_TIMEOUT_MS,
|
||||||
|
timeoutMessage: `FFprobe timed out after ${FFPROBE_TIMEOUT_MS}ms: ${filePath}`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
ffmpeg.ffprobe(filePath, (err: unknown, metadata) => {
|
let metadata: MediaProbeOutput;
|
||||||
clearTimeout(timeout);
|
try {
|
||||||
if (err) {
|
const parsedMetadata: unknown = JSON.parse(result.stdout);
|
||||||
reject(toError(err));
|
if (!isMediaProbeOutput(parsedMetadata)) {
|
||||||
return;
|
throw new Error('FFprobe output is not a JSON object');
|
||||||
}
|
}
|
||||||
|
metadata = parsedMetadata;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to parse FFprobe metadata: ${toError(error).message}`);
|
||||||
|
}
|
||||||
|
|
||||||
const videoStream =
|
const streams = metadata.streams ?? [];
|
||||||
metadata.streams.find((stream) => stream.codec_type === 'video') ??
|
const videoStream =
|
||||||
null;
|
streams.find((stream) => stream.codec_type === 'video') ?? null;
|
||||||
const audioStream =
|
const audioStream =
|
||||||
metadata.streams.find((stream) => stream.codec_type === 'audio') ??
|
streams.find((stream) => stream.codec_type === 'audio') ?? null;
|
||||||
null;
|
const primaryStream = videoStream ?? audioStream;
|
||||||
const primaryStream = videoStream ?? audioStream;
|
const formatDuration = Number(metadata.format?.duration);
|
||||||
const formatDuration = Number(metadata.format.duration);
|
const streamDuration = Number(primaryStream?.duration);
|
||||||
const streamDuration = Number(primaryStream?.duration);
|
const durationSec =
|
||||||
const durationSec =
|
Number.isFinite(formatDuration) && formatDuration > 0
|
||||||
Number.isFinite(formatDuration) && formatDuration > 0
|
? formatDuration
|
||||||
? formatDuration
|
: Number.isFinite(streamDuration) && streamDuration > 0
|
||||||
: Number.isFinite(streamDuration) && streamDuration > 0
|
? streamDuration
|
||||||
? streamDuration
|
: null;
|
||||||
: null;
|
|
||||||
|
|
||||||
const widthPx = Number(videoStream?.width);
|
const widthPx = Number(videoStream?.width);
|
||||||
const heightPx = Number(videoStream?.height);
|
const heightPx = Number(videoStream?.height);
|
||||||
const frameRate =
|
const frameRate =
|
||||||
parseFrameRate(videoStream?.avg_frame_rate) ??
|
parseFrameRate(videoStream?.avg_frame_rate) ??
|
||||||
parseFrameRate(videoStream?.r_frame_rate);
|
parseFrameRate(videoStream?.r_frame_rate);
|
||||||
|
|
||||||
resolve({
|
return {
|
||||||
durationSec,
|
durationSec,
|
||||||
widthPx: Number.isFinite(widthPx) && widthPx > 0 ? widthPx : null,
|
widthPx: Number.isFinite(widthPx) && widthPx > 0 ? widthPx : null,
|
||||||
heightPx: Number.isFinite(heightPx) && heightPx > 0 ? heightPx : null,
|
heightPx: Number.isFinite(heightPx) && heightPx > 0 ? heightPx : null,
|
||||||
frameRate,
|
frameRate,
|
||||||
});
|
};
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isFFmpegAvailable(): Promise<boolean> {
|
async function isFFmpegAvailable(): Promise<boolean> {
|
||||||
return new Promise((resolve) => {
|
try {
|
||||||
ffmpeg.getAvailableFormats((err: unknown) => {
|
await runProcess(getFfmpegExecutablePath(), ['-hide_banner', '-formats'], {
|
||||||
resolve(!err);
|
timeoutMs: FFPROBE_TIMEOUT_MS,
|
||||||
|
timeoutMessage: `FFmpeg availability check timed out after ${FFPROBE_TIMEOUT_MS}ms`,
|
||||||
});
|
});
|
||||||
});
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn({ err }, 'FFmpeg availability check failed');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { isFFmpegAvailable, probeMediaMetadata, reverseVideo };
|
export { isFFmpegAvailable, probeMediaMetadata, reverseVideo };
|
||||||
|
|||||||
@ -294,8 +294,8 @@ goal is only to restore the web app. Use PM2/systemd for process recovery.
|
|||||||
|
|
||||||
## Node Version
|
## Node Version
|
||||||
|
|
||||||
The project requirement is Node.js 20.x LTS. Some standard VMs may report
|
The project requirement is Node.js 24.x LTS. Some standard VMs may report
|
||||||
`/usr/bin/node` as Node 22 in PM2. If startup fails after a system update,
|
a different `/usr/bin/node` in PM2. If startup fails after a system update,
|
||||||
verify:
|
verify:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -42,53 +42,6 @@ TODO:
|
|||||||
- Если нужен, оставить `localStorage`, но осознанно и с коротким TTL/refresh policy.
|
- Если нужен, оставить `localStorage`, но осознанно и с коротким TTL/refresh policy.
|
||||||
- Заменить frontend `jsonwebtoken` decode на `jwt-decode` или `/auth/me`.
|
- Заменить frontend `jsonwebtoken` decode на `jwt-decode` или `/auth/me`.
|
||||||
|
|
||||||
## P1 - Build и зависимости
|
|
||||||
|
|
||||||
### Package manager
|
|
||||||
|
|
||||||
Решение: для root/frontend/backend используется только `npm`.
|
|
||||||
Источник истины для dependency graph: `package-lock.json` в `frontend/` и `backend/`.
|
|
||||||
Команды установки/deploy остаются на `npm ci` / `npm install` и `npm run ...`.
|
|
||||||
|
|
||||||
TODO:
|
|
||||||
|
|
||||||
- [x] Выбрать один package manager для frontend/backend: `npm`.
|
|
||||||
- [x] Удалить лишний lockfile после проверки: лишних `yarn.lock` / `pnpm-lock.yaml` нет, остаются только `package-lock.json`.
|
|
||||||
- [x] Обновить docs/deployment commands: текущие docs уже используют `npm`.
|
|
||||||
|
|
||||||
### Dependencies LTS alignment и obsolete package replacement
|
|
||||||
|
|
||||||
Зависимости нужно обновлять осознанно, чтобы не закреплять старые toolchain limitations и устаревшие библиотеки.
|
|
||||||
|
|
||||||
TODO:
|
|
||||||
|
|
||||||
- Проверить текущие frontend/backend dependencies и devDependencies на совместимость с выбранным Node LTS.
|
|
||||||
- Обновить TypeScript, ESLint, `@typescript-eslint/*`, Next.js/React tooling и backend runtime tooling до актуальных LTS/stable versions.
|
|
||||||
- Найти deprecated, unsupported и obsolete packages через package manager audit/outdated/deprecation warnings, npm metadata, release notes и project documentation.
|
|
||||||
- Для deprecated/unsupported/obsolete packages выбрать actively maintained mainstream alternatives с широкой adoption, свежими releases, нормальной TypeScript/ESM support и понятным migration path.
|
|
||||||
- Для каждой замены зафиксировать причину: что именно deprecated/unsupported/obsolete, какой replacement выбран, какие breaking changes ожидаются.
|
|
||||||
- Проверить DB scripts, migrations/seeders, test runner и production start после dependency updates.
|
|
||||||
- Не делать blind major upgrades без changelog/release notes и rollback plan.
|
|
||||||
|
|
||||||
### Node version
|
|
||||||
|
|
||||||
TODO:
|
|
||||||
|
|
||||||
- Зафиксировать выбранный Node LTS через `backend/package.json` `engines.node`.
|
|
||||||
- Согласовать `engines.node`.
|
|
||||||
- Обновить `@types/node` во frontend, когда будет удобно.
|
|
||||||
|
|
||||||
### CI/minimal checks
|
|
||||||
|
|
||||||
Для небольшого проекта достаточно минимального gate:
|
|
||||||
|
|
||||||
- frontend build;
|
|
||||||
- frontend lint/typecheck, если проходит;
|
|
||||||
- backend lint;
|
|
||||||
- smoke checklist перед deploy.
|
|
||||||
|
|
||||||
Audit/outdated/license checks делать периодически, не обязательно blocking на каждый PR.
|
|
||||||
|
|
||||||
## P2 - Security hardening
|
## P2 - Security hardening
|
||||||
|
|
||||||
### CSP
|
### CSP
|
||||||
@ -101,12 +54,6 @@ TODO:
|
|||||||
- Если будет время, включить report-only CSP на stage.
|
- Если будет время, включить report-only CSP на stage.
|
||||||
- Enforced CSP делать только после проверки runtime presentations.
|
- Enforced CSP делать только после проверки runtime presentations.
|
||||||
|
|
||||||
## P2 - Документация
|
|
||||||
|
|
||||||
TODO:
|
|
||||||
|
|
||||||
- Обновить docs только для реально изменённых workflows/API/schema.
|
|
||||||
|
|
||||||
## Рекомендуемый порядок
|
## Рекомендуемый порядок
|
||||||
|
|
||||||
1. Добавить validation для самых рискованных endpoints и запретить новые routes без validation.
|
1. Добавить validation для самых рискованных endpoints и запретить новые routes без validation.
|
||||||
|
|||||||
20
frontend/package-lock.json
generated
20
frontend/package-lock.json
generated
@ -47,7 +47,7 @@
|
|||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/lodash": "^4.17.24",
|
"@types/lodash": "^4.17.24",
|
||||||
"@types/node": "18.7.16",
|
"@types/node": "^24.13.2",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.62.1",
|
"@typescript-eslint/eslint-plugin": "^8.62.1",
|
||||||
"@typescript-eslint/parser": "^8.62.1",
|
"@typescript-eslint/parser": "^8.62.1",
|
||||||
@ -2610,11 +2610,14 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "18.7.16",
|
"version": "24.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.16.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
|
||||||
"integrity": "sha512-EQHhixfu+mkqHMZl1R2Ovuvn47PUw18azMJOTwSZr9/fhzHNGXAJ0ma0dayRVchprpCj0Kc1K1xKoWaATWF1qg==",
|
"integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~7.18.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/parse-json": {
|
"node_modules/@types/parse-json": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
@ -9065,6 +9068,13 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "7.18.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||||
|
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/unrs-resolver": {
|
"node_modules/unrs-resolver": {
|
||||||
"version": "1.12.2",
|
"version": "1.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz",
|
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz",
|
||||||
|
|||||||
@ -60,7 +60,7 @@
|
|||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/lodash": "^4.17.24",
|
"@types/lodash": "^4.17.24",
|
||||||
"@types/node": "18.7.16",
|
"@types/node": "^24.13.2",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.62.1",
|
"@typescript-eslint/eslint-plugin": "^8.62.1",
|
||||||
"@typescript-eslint/parser": "^8.62.1",
|
"@typescript-eslint/parser": "^8.62.1",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user