opex792 commited on
Commit
7dc09d2
·
verified ·
1 Parent(s): 82970de

Upload 2 files

Browse files
Files changed (2) hide show
  1. README.md +27 -40
  2. index.js +71 -19
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: FFMpeg Command Runner
3
  emoji: 🏃
4
  colorFrom: indigo
5
  colorTo: blue
@@ -7,20 +7,34 @@ sdk: docker
7
  pinned: false
8
  ---
9
 
10
- # API для асинхронного выполнения команд
11
 
12
- Это API позволяет удаленно выполнять консольные команды в асинхронном режиме. Вы отправляете задачу и получаете ее ID, после чего можете отслеживать статус выполнения.
13
 
14
- ## Новый процесс работы
 
15
 
16
- Процесс теперь состоит из 3 шагов:
 
 
17
 
18
- ### Шаг 1: Создание задачи
19
 
20
- Отправьте POST-запрос на `/api/task/create`. Сервер примет файл, создаст задачу и немедленно вернет ответ.
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
- Отправляйте GET-запросы на эндпоинт status_url, полученный на предыдущем шаге, чтобы отслеживать выполнение.
40
- Пример curl:
41
- curl 'https://<your-space-url>/api/task/status/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
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
- Пример curl:
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, createReadStream } from 'fs/promises';
 
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(filePath)
217
- .then(stream => {
218
- res.setHeader('Content-Type', 'application/octet-stream');
219
- stream.pipe(res);
220
- })
221
- .catch(err => {
222
- if (err.code === 'ENOENT') {
223
- res.status(404).send('File not found or has been cleaned up.');
224
- } else {
225
- res.status(500).send('Server error.');
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, () => {