Spaces:
Sleeping
Sleeping
Upload 2 files
Browse files
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 🏃
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: blue
|
|
@@ -7,20 +7,34 @@ sdk: docker
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
# API для
|
| 11 |
|
| 12 |
-
Это API позволяет удаленно выполнять консольные
|
| 13 |
|
| 14 |
-
|
|
|
|
| 15 |
|
| 16 |
-
|
|
|
|
|
|
|
| 17 |
|
| 18 |
-
|
| 19 |
|
| 20 |
-
|
| 21 |
|
| 22 |
**Пример `curl`:**
|
| 23 |
```bash
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
curl -X POST 'https://<your-space-url>/api/task/create' \
|
| 25 |
-F 'command=ffmpeg' \
|
| 26 |
-F 'args=["-i", "{INPUT_FILE}", "-vcodec", "libx264", "-acodec", "aac", "{OUTPUT_FILE}"]' \
|
|
@@ -34,24 +48,11 @@ curl -X POST 'https://<your-space-url>/api/task/create' \
|
|
| 34 |
"status_url": "/api/task/status/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
| 35 |
}
|
| 36 |
|
| 37 |
-
Сохраните taskId для следующего шага.
|
| 38 |
Шаг 2: Проверка статуса задачи
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
Возможные ответы:
|
| 44 |
-
* Задача в очереди или обрабатывается:
|
| 45 |
-
{
|
| 46 |
-
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
| 47 |
-
"status": "processing",
|
| 48 |
-
"elapsedTimeSeconds": 15,
|
| 49 |
-
"progress": 25,
|
| 50 |
-
"estimatedTimeLeftSeconds": 45
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
* Задача успешно завершена:
|
| 54 |
-
{
|
| 55 |
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
| 56 |
"status": "completed",
|
| 57 |
"elapsedTimeSeconds": 60,
|
|
@@ -60,24 +61,10 @@ curl 'https://<your-space-url>/api/task/status/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx
|
|
| 60 |
}
|
| 61 |
}
|
| 62 |
|
| 63 |
-
* Задача завершилась с ошибкой:
|
| 64 |
-
{
|
| 65 |
-
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
| 66 |
-
"status": "failed",
|
| 67 |
-
"elapsedTimeSeconds": 10,
|
| 68 |
-
"error": {
|
| 69 |
-
"message": "Process exited with code 1",
|
| 70 |
-
"code": 1
|
| 71 |
-
}
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
Шаг 3: Скачивание результата
|
| 75 |
-
Когда задача получает статус completed, используйте download_url
|
| 76 |
-
|
| 77 |
-
# Используйте download_url из успешного ответа на Шаге 2
|
| 78 |
curl 'https://<your-space-url>/api/download/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy-converted.mp4' \
|
| 79 |
--output "final_video.mp4"
|
| 80 |
|
| 81 |
-
Этот новый подход более надежен для длительных операций и дает вам полный контроль над отслеживанием процесса.
|
| 82 |
-
|
| 83 |
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Async Command Runner
|
| 3 |
emoji: 🏃
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: blue
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
+
# API для выполнения команд
|
| 11 |
|
| 12 |
+
Это API позволяет удаленно выполнять консольные команды. Поддерживается два режима:
|
| 13 |
|
| 14 |
+
1. **Потоковый (Stream)**: Быстрая обработка "на лету" для простых команд.
|
| 15 |
+
2. **Асинхронные задачи (Tasks)**: Для длительных операций, с возможностью отслеживания прогресса.
|
| 16 |
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
## Режим 1: Потоковая обработка
|
| 20 |
|
| 21 |
+
Идеально для быстрых команд. Файл передается в `stdin` команды, а результат из `stdout` возвращается клиенту. Файлы не сохраняются на диске.
|
| 22 |
|
| 23 |
+
**Эндпоинт:** `POST /api/run/stream`
|
| 24 |
|
| 25 |
**Пример `curl`:**
|
| 26 |
```bash
|
| 27 |
+
curl -X POST 'https://<your-space-url>/api/run/stream' \
|
| 28 |
+
-F 'file=@/path/to/your/image.jpg' \
|
| 29 |
+
-F 'command=magick' \
|
| 30 |
+
-F 'args=["convert", "-", "-grayscale", "average", "jpg:-"]' \
|
| 31 |
+
--output grayscale_image.jpg
|
| 32 |
+
|
| 33 |
+
Режим 2: Асинхронные задачи
|
| 34 |
+
Используйте, когда команде нужно работать с файлами на диске или когда результат нужно сохранить.
|
| 35 |
+
Шаг 1: Создание задачи
|
| 36 |
+
Эндпоинт: POST /api/task/create
|
| 37 |
+
Пример curl:
|
| 38 |
curl -X POST 'https://<your-space-url>/api/task/create' \
|
| 39 |
-F 'command=ffmpeg' \
|
| 40 |
-F 'args=["-i", "{INPUT_FILE}", "-vcodec", "libx264", "-acodec", "aac", "{OUTPUT_FILE}"]' \
|
|
|
|
| 48 |
"status_url": "/api/task/status/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
| 49 |
}
|
| 50 |
|
|
|
|
| 51 |
Шаг 2: Проверка статуса задачи
|
| 52 |
+
Эндпоинт: GET /api/task/status/:taskId
|
| 53 |
+
Отправляйте GET-запросы на status_url, чтобы отслеживать выполнение.
|
| 54 |
+
Пример ответа (завершено):
|
| 55 |
+
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
| 57 |
"status": "completed",
|
| 58 |
"elapsedTimeSeconds": 60,
|
|
|
|
| 61 |
}
|
| 62 |
}
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
Шаг 3: Скачивание результата
|
| 65 |
+
Когда задача получает статус completed, используйте download_url для скачивания файла.
|
| 66 |
+
Эндпоинт: GET /api/download/:fileId
|
|
|
|
| 67 |
curl 'https://<your-space-url>/api/download/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy-converted.mp4' \
|
| 68 |
--output "final_video.mp4"
|
| 69 |
|
|
|
|
|
|
|
| 70 |
|
index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
import express from 'express';
|
| 2 |
import multer from 'multer';
|
| 3 |
import { spawn } from 'child_process';
|
| 4 |
-
import { writeFile, unlink
|
|
|
|
| 5 |
import path from 'path';
|
| 6 |
import { v4 as uuidv4 } from 'uuid';
|
| 7 |
import fetch from 'node-fetch';
|
|
@@ -28,6 +29,62 @@ const downloadFile = async (url) => {
|
|
| 28 |
return Buffer.from(arrayBuffer);
|
| 29 |
};
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
const executeTask = async (taskId) => {
|
| 32 |
const task = tasks[taskId];
|
| 33 |
const { command, args, inputBuffer, outputFilename } = task.payload;
|
|
@@ -178,8 +235,7 @@ app.get('/api/task/status/:taskId', (req, res) => {
|
|
| 178 |
};
|
| 179 |
|
| 180 |
if (task.startTime) {
|
| 181 |
-
const
|
| 182 |
-
endTime = task.endTime || Date.now();
|
| 183 |
response.elapsedTimeSeconds = Math.round((endTime - task.startTime) / 1000);
|
| 184 |
}
|
| 185 |
|
|
@@ -199,9 +255,6 @@ app.get('/api/task/status/:taskId', (req, res) => {
|
|
| 199 |
response.error = task.error;
|
| 200 |
}
|
| 201 |
|
| 202 |
-
// For debugging, optionally include stderr
|
| 203 |
-
// response.stderr = task.stderr;
|
| 204 |
-
|
| 205 |
res.status(200).json(response);
|
| 206 |
});
|
| 207 |
|
|
@@ -212,19 +265,18 @@ app.get('/api/download/:fileId', (req, res) => {
|
|
| 212 |
}
|
| 213 |
|
| 214 |
const filePath = path.join(TEMP_DIR, fileId);
|
| 215 |
-
|
| 216 |
-
createReadStream
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
});
|
| 228 |
});
|
| 229 |
|
| 230 |
app.listen(PORT, () => {
|
|
|
|
| 1 |
import express from 'express';
|
| 2 |
import multer from 'multer';
|
| 3 |
import { spawn } from 'child_process';
|
| 4 |
+
import { writeFile, unlink } from 'fs/promises';
|
| 5 |
+
import { createReadStream } from 'fs'; // <<< ИСПРАВЛЕНИЕ: Правильный импорт
|
| 6 |
import path from 'path';
|
| 7 |
import { v4 as uuidv4 } from 'uuid';
|
| 8 |
import fetch from 'node-fetch';
|
|
|
|
| 29 |
return Buffer.from(arrayBuffer);
|
| 30 |
};
|
| 31 |
|
| 32 |
+
|
| 33 |
+
// --- СТАРЫЙ ЭНДПОИНТ ДЛЯ ПОТОКОВОЙ ОБРАБОТКИ (ВОЗВРАЩЕН) ---
|
| 34 |
+
app.post('/api/run/stream', upload.single('file'), async (req, res) => {
|
| 35 |
+
try {
|
| 36 |
+
const { command, args: argsJson, file_url } = req.body;
|
| 37 |
+
const file = req.file;
|
| 38 |
+
|
| 39 |
+
if (!command) return res.status(400).send({ error: 'Parameter "command" is required.' });
|
| 40 |
+
|
| 41 |
+
let args;
|
| 42 |
+
try {
|
| 43 |
+
args = argsJson ? JSON.parse(argsJson) : [];
|
| 44 |
+
} catch(e) {
|
| 45 |
+
return res.status(400).send({ error: 'Parameter "args" must be a valid JSON array.' });
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
let inputBuffer;
|
| 49 |
+
if (file) {
|
| 50 |
+
inputBuffer = file.buffer;
|
| 51 |
+
} else if (file_url) {
|
| 52 |
+
inputBuffer = await downloadFile(file_url);
|
| 53 |
+
} else {
|
| 54 |
+
return res.status(400).send({ error: 'A file must be provided via "file" or "file_url".' });
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
const process = spawn(command, args);
|
| 58 |
+
let stdoutChunks = [];
|
| 59 |
+
|
| 60 |
+
process.stdout.on('data', (data) => stdoutChunks.push(data));
|
| 61 |
+
|
| 62 |
+
process.on('close', (code) => {
|
| 63 |
+
if (code === 0) {
|
| 64 |
+
res.setHeader('Content-Type', 'application/octet-stream');
|
| 65 |
+
res.send(Buffer.concat(stdoutChunks));
|
| 66 |
+
} else {
|
| 67 |
+
res.status(500).send({
|
| 68 |
+
error: 'Command execution failed.',
|
| 69 |
+
code: code
|
| 70 |
+
});
|
| 71 |
+
}
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
+
process.stdin.write(inputBuffer);
|
| 75 |
+
process.stdin.end();
|
| 76 |
+
|
| 77 |
+
} catch (error) {
|
| 78 |
+
res.status(500).send({
|
| 79 |
+
error: 'Server error during stream processing.',
|
| 80 |
+
message: error.message
|
| 81 |
+
});
|
| 82 |
+
}
|
| 83 |
+
});
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
// --- НОВАЯ СИСТЕМА АСИНХРОННЫХ ЗАДАЧ ---
|
| 87 |
+
|
| 88 |
const executeTask = async (taskId) => {
|
| 89 |
const task = tasks[taskId];
|
| 90 |
const { command, args, inputBuffer, outputFilename } = task.payload;
|
|
|
|
| 235 |
};
|
| 236 |
|
| 237 |
if (task.startTime) {
|
| 238 |
+
const endTime = task.endTime || Date.now();
|
|
|
|
| 239 |
response.elapsedTimeSeconds = Math.round((endTime - task.startTime) / 1000);
|
| 240 |
}
|
| 241 |
|
|
|
|
| 255 |
response.error = task.error;
|
| 256 |
}
|
| 257 |
|
|
|
|
|
|
|
|
|
|
| 258 |
res.status(200).json(response);
|
| 259 |
});
|
| 260 |
|
|
|
|
| 265 |
}
|
| 266 |
|
| 267 |
const filePath = path.join(TEMP_DIR, fileId);
|
| 268 |
+
|
| 269 |
+
// ИСПРАВЛЕНИЕ: Используем createReadStream без промиса
|
| 270 |
+
const stream = createReadStream(filePath);
|
| 271 |
+
stream.on('error', (err) => {
|
| 272 |
+
if (err.code === 'ENOENT') {
|
| 273 |
+
res.status(404).send('File not found or has been cleaned up.');
|
| 274 |
+
} else {
|
| 275 |
+
res.status(500).send('Server error.');
|
| 276 |
+
}
|
| 277 |
+
});
|
| 278 |
+
res.setHeader('Content-Type', 'application/octet-stream');
|
| 279 |
+
stream.pipe(res);
|
|
|
|
| 280 |
});
|
| 281 |
|
| 282 |
app.listen(PORT, () => {
|