Spaces:
Sleeping
Sleeping
| 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(); | |
| }); | |