ffmpeg-api / index.js
opex792's picture
Upload 4 files
ebf7adb verified
raw
history blame
7.95 kB
import express from 'express';
import multer from 'multer';
import { spawn } from 'child_process';
import { writeFile, unlink, createReadStream } from 'fs';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import fetch from 'node-fetch';
import { startCleanupJob } from './cleanup.js';
// --- НАСТРОЙКА ---
// ИСПРАВЛЕНИЕ: Используем глобальную временную директорию /tmp, которая всегда доступна для записи.
const TEMP_DIR = '/tmp';
const app = express();
const PORT = process.env.PORT || 7860;
console.log(`Используется временная директория: ${TEMP_DIR}`);
// --- MIDDLEWARE ---
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
// --- ЛОГИКА ---
const executeCommand = (command, args, inputBuffer = null) => {
return new Promise((resolve, reject) => {
const process = spawn(command, args);
let stdoutChunks = [];
let stderrChunks = [];
process.stdout.on('data', (data) => stdoutChunks.push(data));
process.stderr.on('data', (data) => stderrChunks.push(data));
process.on('close', (code) => {
const stdout = Buffer.concat(stdoutChunks);
const stderr = Buffer.concat(stderrChunks).toString('utf8');
if (code === 0) {
resolve({ stdout, stderr });
} else {
const error = new Error(`Процесс завершился с кодом ${code}.\nStderr: ${stderr}`);
error.code = code;
error.stderr = stderr;
reject(error);
}
});
process.on('error', (err) => reject(err));
if (inputBuffer) {
process.stdin.write(inputBuffer);
process.stdin.end();
}
});
};
const downloadFile = async (url) => {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Не удалось скачать файл: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
};
// --- МАРШРУТЫ API ---
app.get('/', (req, res) => {
res.send('Сервер удаленного выполнения команд готов к работе!');
});
app.post('/api/run/stream', upload.single('file'), async (req, res) => {
try {
const { command, args: argsJson, file_url } = req.body;
const file = req.file;
if (!command) return res.status(400).send({ error: 'Параметр "command" обязателен.' });
let args;
try {
args = argsJson ? JSON.parse(argsJson) : [];
} catch(e) {
return res.status(400).send({ error: 'Параметр "args" должен быть валидным JSON массивом.' });
}
let inputBuffer;
if (file) {
inputBuffer = file.buffer;
} else if (file_url) {
inputBuffer = await downloadFile(file_url);
} else {
return res.status(400).send({ error: 'Необходимо предоставить файл через "file" или "file_url".' });
}
const { stdout, stderr } = await executeCommand(command, args, inputBuffer);
console.log(`Stderr для ${command}: ${stderr}`);
res.setHeader('Content-Type', 'application/octet-stream');
res.send(stdout);
} catch (error) {
console.error('Ошибка в /api/run/stream:', error);
res.status(500).send({
error: 'Ошибка выполнения команды.',
message: error.message,
stderr: error.stderr || 'N/A'
});
}
});
app.post('/api/run/file', upload.single('file'), async (req, res) => {
const tempFiles = [];
try {
const { command, args: argsJson, file_url, output_filename } = req.body;
const file = req.file;
if (!command) return res.status(400).send({ error: 'Параметр "command" обязателен.' });
let args;
try {
args = argsJson ? JSON.parse(argsJson) : [];
} catch(e) {
return res.status(400).send({ error: 'Параметр "args" должен быть валидным JSON массивом.' });
}
let inputBuffer;
if (file) {
inputBuffer = file.buffer;
} else if (file_url) {
inputBuffer = await downloadFile(file_url);
} else {
return res.status(400).send({ error: 'Необходимо предоставить файл через "file" или "file_url".' });
}
// Получаем имя файла из загрузки, или используем 'input' как запасной вариант
const originalName = file?.originalname?.replace(/[^a-zA-Z0-9._-]/g, '') || 'input';
const inputFilePath = path.join(TEMP_DIR, `${uuidv4()}-${originalName}`);
const outputFilePath = path.join(TEMP_DIR, `${uuidv4()}-${output_filename || 'output'}`);
tempFiles.push(inputFilePath, outputFilePath);
const processedArgs = args.map(arg =>
arg.replace('{INPUT_FILE}', inputFilePath)
.replace('{OUTPUT_FILE}', outputFilePath)
);
await new Promise((resolve, reject) => {
writeFile(inputFilePath, inputBuffer, (err) => {
if (err) {
console.error("Ошибка записи входного файла:", err);
return reject(err);
}
resolve();
});
});
const { stderr } = await executeCommand(command, processedArgs);
console.log(`Stderr для ${command}: ${stderr}`);
const fileId = path.basename(outputFilePath);
res.status(200).json({
message: 'Команда выполнена успешно.',
download_url: `/api/download/${fileId}`,
stderr: stderr
});
} catch (error) {
console.error('Ошибка в /api/run/file:', error);
for (const filePath of tempFiles) {
unlink(filePath, (err) => {
if (err) {
// Не выводим ошибку, если файла просто нет (уже удален или не был создан)
if(err.code !== 'ENOENT') console.error(`Не удалось удалить временный файл ${filePath}:`, err);
}
});
}
res.status(500).send({
error: 'Ошибка выполнения команды.',
message: error.message,
stderr: error.stderr || 'N/A'
});
}
});
app.get('/api/download/:fileId', (req, res) => {
const { fileId } = req.params;
if (fileId.includes('..')) {
return res.status(400).send('Неверный ID файла.');
}
const filePath = path.join(TEMP_DIR, fileId);
const stream = createReadStream(filePath);
stream.on('error', (err) => {
if (err.code === 'ENOENT') {
res.status(404).send('Файл не найден или был удален.');
} else {
console.error(`Ошибка чтения файла ${filePath}:`, err);
res.status(500).send('Ошибка сервера.');
}
});
res.setHeader('Content-Type', 'application/octet-stream');
stream.pipe(res);
});
// --- ЗАПУСК СЕРВЕРА И ОЧИСТКИ ---
app.listen(PORT, () => {
console.log(`Сервер запущен на порту ${PORT}`);
startCleanupJob();
});