import express from 'express'; import multer from 'multer'; import { spawn } from 'child_process'; import { writeFile, unlink, readFile } from 'fs/promises'; import { createReadStream } from 'fs'; import path from 'path'; import { v4 as uuidv4 } from 'uuid'; import fetch from 'node-fetch'; import { startCleanupJob } from './cleanup.js'; const TEMP_DIR = '/tmp'; const app = express(); const PORT = process.env.PORT || 7860; export const tasks = {}; app.use(express.json()); app.use(express.urlencoded({ extended: true })); const storage = multer.memoryStorage(); const upload = multer({ storage: storage }); const downloadFile = async (url) => { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to download file: ${response.statusText}`); } const arrayBuffer = await response.arrayBuffer(); return Buffer.from(arrayBuffer); }; // --- "УМНЫЙ" ЭНДПОИНТ ДЛЯ ПОТОКОВОЙ ОБРАБОТКИ --- 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).json({ error: 'Parameter "command" is required.' }); let args; try { args = argsJson ? JSON.parse(argsJson) : []; } catch(e) { return res.status(400).json({ error: 'Parameter "args" must be a valid JSON array.' }); } let inputBuffer; if (file) { inputBuffer = file.buffer; } else if (file_url) { inputBuffer = await downloadFile(file_url); } else { return res.status(400).json({ error: 'A file must be provided via "file" or "file_url".' }); } // --- СПЕЦИАЛЬНАЯ ЛОГИКА ДЛЯ FFMPEG --- if (command === 'ffmpeg') { const inputFilePath = path.join(TEMP_DIR, `${uuidv4()}-input`); const outputFilePath = path.join(TEMP_DIR, `${uuidv4()}-output`); try { await writeFile(inputFilePath, inputBuffer); const processedArgs = args.map(arg => arg.replace('{INPUT_FILE}', inputFilePath) .replace('{OUTPUT_FILE}', outputFilePath) ); const process = spawn(command, processedArgs); let stderrChunks = []; process.stderr.on('data', (data) => stderrChunks.push(data)); process.on('close', async (code) => { if (code === 0) { try { const outputBuffer = await readFile(outputFilePath); res.setHeader('Content-Type', 'application/octet-stream'); res.send(outputBuffer); } catch (readError) { res.status(500).json({ error: "Command succeeded, but failed to read output file.", message: readError.message }); } } else { const stderr = Buffer.concat(stderrChunks).toString('utf8'); res.status(500).json({ error: 'Command execution failed.', code: code, stderr: stderr }); } // Очистка await unlink(inputFilePath).catch(()=>{}); await unlink(outputFilePath).catch(()=>{}); }); } catch (execError) { res.status(500).json({ error: "Failed during ffmpeg file operations.", message: execError.message }); await unlink(inputFilePath).catch(()=>{}); await unlink(outputFilePath).catch(()=>{}); } // --- ОБЫЧНАЯ ЛОГИКА ДЛЯ ДРУГИХ КОМАНД --- } else { 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) => { if (code === 0) { res.setHeader('Content-Type', 'application/octet-stream'); res.send(Buffer.concat(stdoutChunks)); } else { const stderr = Buffer.concat(stderrChunks).toString('utf8'); res.status(500).json({ error: 'Command execution failed.', code: code, stderr: stderr }); } }); process.stdin.write(inputBuffer); process.stdin.end(); } } catch (error) { res.status(500).json({ error: 'Server error during stream processing.', message: error.message }); } }); // --- СИСТЕМА АСИНХРОННЫХ ЗАДАЧ (ОСТАЕТСЯ КАК АЛЬТЕРНАТИВА) --- // ... (остальной код для /api/task/* и /api/download/* без изменений) ... const executeTask = async (taskId) => { const task = tasks[taskId]; const { command, args, inputBuffer, outputFilename } = task.payload; const tempFiles = []; try { tasks[taskId].status = 'processing'; tasks[taskId].startTime = Date.now(); const originalName = task.payload.originalName?.replace(/[^a-zA-Z0-9._-]/g, '') || 'input'; const inputFilePath = path.join(TEMP_DIR, `${uuidv4()}-${originalName}`); const outputFilePath = path.join(TEMP_DIR, `${uuidv4()}-${outputFilename || 'output'}`); tempFiles.push(inputFilePath, outputFilePath); task.outputFilePath = outputFilePath; const processedArgs = args.map(arg => arg.replace('{INPUT_FILE}', inputFilePath).replace('{OUTPUT_FILE}', outputFilePath)); await writeFile(inputFilePath, inputBuffer); const process = spawn(command, processedArgs); let stderrOutput = ''; let totalDuration = 0; process.stderr.on('data', (data) => { const stderrLine = data.toString(); stderrOutput += stderrLine; if (!totalDuration) { const durationMatch = stderrLine.match(/Duration: (\d{2}):(\d{2}):(\d{2})\.\d{2}/); if (durationMatch) { totalDuration = parseInt(durationMatch[1]) * 3600 + parseInt(durationMatch[2]) * 60 + parseInt(durationMatch[3]); task.estimatedTotalTime = totalDuration; } } const timeMatch = stderrLine.match(/time=(\d{2}):(\d{2}):(\d{2})\.\d{2}/); if (timeMatch && totalDuration) { const currentTime = parseInt(timeMatch[1]) * 3600 + parseInt(timeMatch[2]) * 60 + parseInt(timeMatch[3]); task.progress = Math.min(100, Math.round((currentTime / totalDuration) * 100)); } }); await new Promise((resolve, reject) => { process.on('close', (code) => { task.endTime = Date.now(); task.stderr = stderrOutput; if (code === 0) { task.status = 'completed'; task.result = { download_url: `/api/download/${path.basename(outputFilePath)}` }; resolve(); } else { const error = new Error(`Process exited with code ${code}`); error.code = code; reject(error); } }); process.on('error', reject); }); } catch (error) { tasks[taskId].status = 'failed'; tasks[taskId].endTime = Date.now(); tasks[taskId].error = { message: error.message, code: error.code }; for (const filePath of tempFiles) { unlink(filePath).catch(() => {}); } } }; app.get('/', (req, res) => { res.send('Task-based remote execution server is ready.'); }); app.post('/api/task/create', upload.single('file'), async (req, res) => { try { const { command, args: argsJson, file_url, output_filename } = req.body; const file = req.file; if (!command) return res.status(400).json({ error: 'Parameter "command" is required.' }); let args; try { args = argsJson ? JSON.parse(argsJson) : []; } catch(e) { return res.status(400).json({ error: 'Parameter "args" must be a valid JSON array.' }); } let inputBuffer; if (file) { inputBuffer = file.buffer; } else if (file_url) { inputBuffer = await downloadFile(file_url); } else { return res.status(400).json({ error: 'A file must be provided via "file" or "file_url".' }); } const taskId = uuidv4(); tasks[taskId] = { id: taskId, status: 'queued', progress: 0, submittedAt: Date.now(), payload: { command, args, outputFilename: output_filename, originalName: file?.originalname, inputBuffer, } }; executeTask(taskId); res.status(202).json({ message: "Task accepted.", taskId: taskId, status_url: `/api/task/status/${taskId}` }); } catch (error) { res.status(500).json({ error: 'Failed to create task.', message: error.message }); } }); app.get('/api/task/status/:taskId', (req, res) => { const { taskId } = req.params; const task = tasks[taskId]; if (!task) { return res.status(404).json({ error: 'Task not found.' }); } const response = { id: task.id, status: task.status }; if (task.startTime) { const endTime = task.endTime || Date.now(); response.elapsedTimeSeconds = Math.round((endTime - task.startTime) / 1000); } if (task.status === 'processing') { response.progress = task.progress; if (task.estimatedTotalTime && response.elapsedTimeSeconds) { const remaining = task.estimatedTotalTime - response.elapsedTimeSeconds; response.estimatedTimeLeftSeconds = Math.max(0, remaining); } } if (task.status === 'completed') { response.result = task.result; } if (task.status === 'failed') { response.error = task.error; } res.status(200).json(response); }); app.get('/api/download/:fileId', (req, res) => { const { fileId } = req.params; if (fileId.includes('..')) { return res.status(400).send('Invalid file ID.'); } const filePath = path.join(TEMP_DIR, fileId); const stream = createReadStream(filePath); stream.on('error', (err) => { if (err.code === 'ENOENT') { res.status(404).send('File not found or has been cleaned up.'); } else { res.status(500).send('Server error.'); } }); res.setHeader('Content-Type', 'application/octet-stream'); stream.pipe(res); }); app.listen(PORT, () => { startCleanupJob(); });