opex792 commited on
Commit
684d20b
·
verified ·
1 Parent(s): 212881f

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +0 -11
  2. README.md +65 -10
  3. cleanup.js +26 -20
  4. index.js +153 -140
  5. package.json +5 -14
Dockerfile CHANGED
@@ -1,30 +1,19 @@
1
- # Используем официальный образ Node.js 18.
2
  FROM node:18-slim
3
 
4
- # ИСПРАВЛЕНИЕ: Установка ffmpeg
5
- # Обновляем список пакетов и устанавливаем ffmpeg.
6
- # -y автоматически отвечает 'да' на все запросы.
7
- # && rm -rf /var/lib/apt/lists/* очищает кэш после установки для уменьшения размера образа.
8
  RUN apt-get update && \
9
  apt-get install -y ffmpeg && \
10
  rm -rf /var/lib/apt/lists/*
11
 
12
- # Устанавливаем рабочую директорию внутри контейнера
13
  WORKDIR /usr/src/app
14
 
15
- # Копируем файлы package.json и package-lock.json
16
  COPY package*.json ./
17
 
18
- # Устанавливаем зависимости проекта
19
  RUN npm install --only=production
20
 
21
- # Копируем остальной код приложения в рабочую директорию
22
  COPY . .
23
 
24
- # Открываем порт, на котором будет работать приложение (стандартный для Spaces - 7860)
25
  EXPOSE 7860
26
 
27
- # Указываем команду для запуска приложения при старте контейнера
28
  CMD [ "node", "index.js" ]
29
 
30
 
 
 
1
  FROM node:18-slim
2
 
 
 
 
 
3
  RUN apt-get update && \
4
  apt-get install -y ffmpeg && \
5
  rm -rf /var/lib/apt/lists/*
6
 
 
7
  WORKDIR /usr/src/app
8
 
 
9
  COPY package*.json ./
10
 
 
11
  RUN npm install --only=production
12
 
 
13
  COPY . .
14
 
 
15
  EXPOSE 7860
16
 
 
17
  CMD [ "node", "index.js" ]
18
 
19
 
README.md CHANGED
@@ -1,10 +1,65 @@
1
- ---
2
- title: Ffmpeg Api
3
- emoji: 😻
4
- colorFrom: indigo
5
- colorTo: green
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ API для асинхронного выполнения команд
2
+ Это API позволяет удаленно выполнять консольные команды в асинхронном режиме. Вы отправляете задачу и получаете ее ID, после чего можете отслеживать статус выполнения.
3
+ Новый процесс работы
4
+ Процесс теперь состоит из 3 шагов:
5
+ Шаг 1: Создание задачи
6
+ Отправьте POST-запрос на /api/task/create. Сервер примет файл, создаст задачу и немедленно вернет ответ.
7
+ Пример curl:
8
+ curl -X POST 'https://<your-space-url>/api/task/create' \
9
+ -F 'command=ffmpeg' \
10
+ -F 'args=["-i", "{INPUT_FILE}", "-vcodec", "libx264", "-acodec", "aac", "{OUTPUT_FILE}"]' \
11
+ -F 'file=@"/path/to/your/video.mov"' \
12
+ -F 'output_filename=converted.mp4'
13
+
14
+ Ответ сервера (статус 202 Accepted):
15
+ {
16
+ "message": "Task accepted.",
17
+ "taskId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
18
+ "status_url": "/api/task/status/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
19
+ }
20
+
21
+ Сохраните taskId для следующего шага.
22
+ Шаг 2: Проверка статуса задачи
23
+ Отправляйте GET-запросы на эндпоинт status_url, полученный на предыдущем шаге, чтобы отслеживать выполнение.
24
+ Пример curl:
25
+ curl 'https://<your-space-url>/api/task/status/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
26
+
27
+ Возможные ответы:
28
+ * Задача в очереди или обрабатывается:
29
+ {
30
+ "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
31
+ "status": "processing",
32
+ "elapsedTimeSeconds": 15,
33
+ "progress": 25,
34
+ "estimatedTimeLeftSeconds": 45
35
+ }
36
+
37
+ * Задача успешно завершена:
38
+ {
39
+ "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
40
+ "status": "completed",
41
+ "elapsedTimeSeconds": 60,
42
+ "result": {
43
+ "download_url": "/api/download/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy-converted.mp4"
44
+ }
45
+ }
46
+
47
+ * Задача завершилась с ошибкой:
48
+ {
49
+ "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
50
+ "status": "failed",
51
+ "elapsedTimeSeconds": 10,
52
+ "error": {
53
+ "message": "Process exited with code 1",
54
+ "code": 1
55
+ }
56
+ }
57
+
58
+ Шаг 3: Скачивание результата
59
+ Когда задача получает статус completed, используйте download_url из ответа для скачивания готового файла.
60
+ Пример curl:
61
+ # Используйте download_url из успешного ответа на Шаге 2
62
+ curl 'https://<your-space-url>/api/download/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy-converted.mp4' \
63
+ --output "final_video.mp4"
64
+
65
+ Этот новый подход более надежен для длительных операций и дает вам полный контроль над отслеживанием процесса.
cleanup.js CHANGED
@@ -1,52 +1,58 @@
1
  import cron from 'node-cron';
2
  import fs from 'fs/promises';
3
  import path from 'path';
 
4
 
5
- // ИСПРАВЛЕНИЕ: Используем глобальную временную директорию /tmp.
6
  const TEMP_DIR = '/tmp';
7
- const MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 часа в миллисекундах
8
 
9
  async function cleanupOldFiles() {
10
- console.log('Запуск задачи по очистке старых файлов...');
11
  try {
12
  const files = await fs.readdir(TEMP_DIR);
13
  const now = Date.now();
14
 
15
  for (const file of files) {
16
- // Очищаем только файлы, созданные нашим приложением (у них UUID в имени)
17
- // Это мера предосторожности, чтобы не удалить чужие файлы в /tmp
18
  if (/\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b/i.test(file)) {
19
- const filePath = path.join(TEMP_DIR, file);
20
  try {
21
  const stats = await fs.stat(filePath);
22
- const fileAge = now - stats.mtime.getTime();
23
-
24
- if (fileAge > MAX_AGE_MS) {
25
  await fs.unlink(filePath);
26
- console.log(`Удален старый файл: ${filePath}`);
27
  }
28
  } catch (statError) {
29
  if (statError.code !== 'ENOENT') {
30
- console.error(`Не удалось получить информацию о файле ${filePath}:`, statError);
31
  }
32
  }
33
  }
34
  }
35
  } catch (readDirError) {
36
- if (readDirError.code === 'ENOENT') {
37
- // Этого не должно случиться с /tmp, но на всякий случай.
38
- console.log('Временная директория не существует, очистка не требуется.');
39
- } else {
40
- console.error('Ошибка при чтении временной директории:', readDirError);
 
 
 
 
 
 
 
 
 
 
41
  }
42
  }
43
- console.log('Очистка завершена.');
44
  }
45
 
46
  export function startCleanupJob() {
47
- cleanupOldFiles();
48
- cron.schedule('0 * * * *', cleanupOldFiles);
49
- console.log('Задача по очистке запланирована на запуск каждый час.');
 
 
 
50
  }
51
 
52
 
 
1
  import cron from 'node-cron';
2
  import fs from 'fs/promises';
3
  import path from 'path';
4
+ import { tasks } from './index.js';
5
 
 
6
  const TEMP_DIR = '/tmp';
7
+ const MAX_AGE_MS = 24 * 60 * 60 * 1000;
8
 
9
  async function cleanupOldFiles() {
 
10
  try {
11
  const files = await fs.readdir(TEMP_DIR);
12
  const now = Date.now();
13
 
14
  for (const file of files) {
 
 
15
  if (/\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b/i.test(file)) {
16
+ const filePath = path.join(TEMP_DIR, file);
17
  try {
18
  const stats = await fs.stat(filePath);
19
+ if (now - stats.mtime.getTime() > MAX_AGE_MS) {
 
 
20
  await fs.unlink(filePath);
 
21
  }
22
  } catch (statError) {
23
  if (statError.code !== 'ENOENT') {
24
+ console.error(`Failed to stat file ${filePath}:`, statError);
25
  }
26
  }
27
  }
28
  }
29
  } catch (readDirError) {
30
+ if (readDirError.code !== 'ENOENT') {
31
+ console.error('Error reading temp directory:', readDirError);
32
+ }
33
+ }
34
+ }
35
+
36
+ function cleanupOldTasks() {
37
+ const now = Date.now();
38
+ for (const taskId in tasks) {
39
+ const task = tasks[taskId];
40
+ if (task.submittedAt && (now - task.submittedAt > MAX_AGE_MS)) {
41
+ if (task.outputFilePath) {
42
+ fs.unlink(task.outputFilePath).catch(() => {});
43
+ }
44
+ delete tasks[taskId];
45
  }
46
  }
 
47
  }
48
 
49
  export function startCleanupJob() {
50
+ cleanupOldFiles();
51
+ cleanupOldTasks();
52
+ cron.schedule('0 * * * *', () => {
53
+ cleanupOldFiles();
54
+ cleanupOldTasks();
55
+ });
56
  }
57
 
58
 
index.js CHANGED
@@ -1,130 +1,127 @@
1
  import express from 'express';
2
  import multer from 'multer';
3
  import { spawn } from 'child_process';
4
- import { writeFile, unlink, createReadStream } from 'fs';
5
  import path from 'path';
6
  import { v4 as uuidv4 } from 'uuid';
7
  import fetch from 'node-fetch';
8
  import { startCleanupJob } from './cleanup.js';
9
 
10
- // --- НАСТРОЙКА ---
11
- // ИСПРАВЛЕНИЕ: Используем глобальную временную директорию /tmp, которая всегда доступна для записи.
12
- const TEMP_DIR = '/tmp';
13
-
14
  const app = express();
15
  const PORT = process.env.PORT || 7860;
16
 
17
- console.log(`Используется временная директория: ${TEMP_DIR}`);
18
 
19
- // --- MIDDLEWARE ---
20
  app.use(express.json());
21
  app.use(express.urlencoded({ extended: true }));
22
 
23
  const storage = multer.memoryStorage();
24
  const upload = multer({ storage: storage });
25
 
26
- // --- ЛОГИКА ---
27
-
28
- const executeCommand = (command, args, inputBuffer = null) => {
29
- return new Promise((resolve, reject) => {
30
- const process = spawn(command, args);
31
- let stdoutChunks = [];
32
- let stderrChunks = [];
33
-
34
- process.stdout.on('data', (data) => stdoutChunks.push(data));
35
- process.stderr.on('data', (data) => stderrChunks.push(data));
36
-
37
- process.on('close', (code) => {
38
- const stdout = Buffer.concat(stdoutChunks);
39
- const stderr = Buffer.concat(stderrChunks).toString('utf8');
40
- if (code === 0) {
41
- resolve({ stdout, stderr });
42
- } else {
43
- const error = new Error(`Процесс завершился с кодом ${code}.\nStderr: ${stderr}`);
44
- error.code = code;
45
- error.stderr = stderr;
46
- reject(error);
47
- }
48
- });
49
-
50
- process.on('error', (err) => reject(err));
51
-
52
- if (inputBuffer) {
53
- process.stdin.write(inputBuffer);
54
- process.stdin.end();
55
- }
56
- });
57
- };
58
-
59
  const downloadFile = async (url) => {
60
  const response = await fetch(url);
61
  if (!response.ok) {
62
- throw new Error(`Не удалось скачать файл: ${response.statusText}`);
63
  }
64
  const arrayBuffer = await response.arrayBuffer();
65
  return Buffer.from(arrayBuffer);
66
  };
67
 
68
- // --- МАРШРУТЫ API ---
69
-
70
- app.get('/', (req, res) => {
71
- res.send('Сервер удаленного выполнения команд готов к работе!');
72
- });
73
 
74
- app.post('/api/run/stream', upload.single('file'), async (req, res) => {
75
  try {
76
- const { command, args: argsJson, file_url } = req.body;
77
- const file = req.file;
78
 
79
- if (!command) return res.status(400).send({ error: 'Параметр "command" обязателен.' });
 
 
80
 
81
- let args;
82
- try {
83
- args = argsJson ? JSON.parse(argsJson) : [];
84
- } catch(e) {
85
- return res.status(400).send({ error: 'Параметр "args" должен быть валидным JSON массивом.' });
86
- }
87
 
88
- let inputBuffer;
89
- if (file) {
90
- inputBuffer = file.buffer;
91
- } else if (file_url) {
92
- inputBuffer = await downloadFile(file_url);
93
- } else {
94
- return res.status(400).send({ error: 'Необходимо предоставить файл через "file" или "file_url".' });
95
- }
96
 
97
- const { stdout, stderr } = await executeCommand(command, args, inputBuffer);
98
-
99
- console.log(`Stderr для ${command}: ${stderr}`);
100
-
101
- res.setHeader('Content-Type', 'application/octet-stream');
102
- res.send(stdout);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
  } catch (error) {
105
- console.error('Ошибка в /api/run/stream:', error);
106
- res.status(500).send({
107
- error: 'Ошибка выполнения команды.',
108
  message: error.message,
109
- stderr: error.stderr || 'N/A'
110
- });
 
 
 
111
  }
112
- });
113
 
114
- app.post('/api/run/file', upload.single('file'), async (req, res) => {
115
- const tempFiles = [];
 
116
 
 
117
  try {
118
  const { command, args: argsJson, file_url, output_filename } = req.body;
119
  const file = req.file;
120
 
121
- if (!command) return res.status(400).send({ error: 'Параметр "command" обязателен.' });
122
 
123
  let args;
124
  try {
125
  args = argsJson ? JSON.parse(argsJson) : [];
126
  } catch(e) {
127
- return res.status(400).send({ error: 'Параметр "args" должен быть валидным JSON массивом.' });
128
  }
129
 
130
  let inputBuffer;
@@ -133,88 +130,104 @@ app.post('/api/run/file', upload.single('file'), async (req, res) => {
133
  } else if (file_url) {
134
  inputBuffer = await downloadFile(file_url);
135
  } else {
136
- return res.status(400).send({ error: 'Необходимо предоставить файл через "file" или "file_url".' });
137
  }
138
-
139
- // Получаем имя файла из загрузки, или используем 'input' как запасной вариант
140
- const originalName = file?.originalname?.replace(/[^a-zA-Z0-9._-]/g, '') || 'input';
141
-
142
- const inputFilePath = path.join(TEMP_DIR, `${uuidv4()}-${originalName}`);
143
- const outputFilePath = path.join(TEMP_DIR, `${uuidv4()}-${output_filename || 'output'}`);
144
-
145
- tempFiles.push(inputFilePath, outputFilePath);
146
 
147
- const processedArgs = args.map(arg =>
148
- arg.replace('{INPUT_FILE}', inputFilePath)
149
- .replace('{OUTPUT_FILE}', outputFilePath)
150
- );
 
 
 
 
 
 
 
 
 
 
151
 
152
- await new Promise((resolve, reject) => {
153
- writeFile(inputFilePath, inputBuffer, (err) => {
154
- if (err) {
155
- console.error("Ошибка записи входного файла:", err);
156
- return reject(err);
157
- }
158
- resolve();
159
- });
160
- });
161
 
162
- const { stderr } = await executeCommand(command, processedArgs);
163
- console.log(`Stderr д��я ${command}: ${stderr}`);
164
-
165
- const fileId = path.basename(outputFilePath);
166
- res.status(200).json({
167
- message: 'Команда выполнена успешно.',
168
- download_url: `/api/download/${fileId}`,
169
- stderr: stderr
170
  });
171
 
172
  } catch (error) {
173
- console.error('Ошибка в /api/run/file:', error);
174
-
175
- for (const filePath of tempFiles) {
176
- unlink(filePath, (err) => {
177
- if (err) {
178
- // Не выводим ошибку, если файла просто нет (уже удален или не был создан)
179
- if(err.code !== 'ENOENT') console.error(`Не удалось удалить временный файл ${filePath}:`, err);
180
- }
181
- });
182
- }
183
-
184
- res.status(500).send({
185
- error: 'Ошибка выполнения команды.',
186
  message: error.message,
187
- stderr: error.stderr || 'N/A'
188
  });
189
  }
190
  });
191
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  app.get('/api/download/:fileId', (req, res) => {
193
  const { fileId } = req.params;
194
  if (fileId.includes('..')) {
195
- return res.status(400).send('Неверный ID файла.');
196
  }
197
 
198
  const filePath = path.join(TEMP_DIR, fileId);
199
 
200
- const stream = createReadStream(filePath);
201
- stream.on('error', (err) => {
202
- if (err.code === 'ENOENT') {
203
- res.status(404).send('Файл не найден или был удален.');
204
- } else {
205
- console.error(`Ошибка чтения файла ${filePath}:`, err);
206
- res.status(500).send('Ошибка сервера.');
207
- }
208
- });
209
-
210
- res.setHeader('Content-Type', 'application/octet-stream');
211
- stream.pipe(res);
212
  });
213
 
214
- // --- ЗАПУСК СЕРВЕРА И ОЧИСТКИ ---
215
-
216
  app.listen(PORT, () => {
217
- console.log(`Сервер запущен на порту ${PORT}`);
218
  startCleanupJob();
219
  });
220
 
 
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';
8
  import { startCleanupJob } from './cleanup.js';
9
 
10
+ const TEMP_DIR = '/tmp';
 
 
 
11
  const app = express();
12
  const PORT = process.env.PORT || 7860;
13
 
14
+ export const tasks = {};
15
 
 
16
  app.use(express.json());
17
  app.use(express.urlencoded({ extended: true }));
18
 
19
  const storage = multer.memoryStorage();
20
  const upload = multer({ storage: storage });
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  const downloadFile = async (url) => {
23
  const response = await fetch(url);
24
  if (!response.ok) {
25
+ throw new Error(`Failed to download file: ${response.statusText}`);
26
  }
27
  const arrayBuffer = await response.arrayBuffer();
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;
34
+ const tempFiles = [];
 
35
 
 
36
  try {
37
+ tasks[taskId].status = 'processing';
38
+ tasks[taskId].startTime = Date.now();
39
 
40
+ const originalName = task.payload.originalName?.replace(/[^a-zA-Z0-9._-]/g, '') || 'input';
41
+ const inputFilePath = path.join(TEMP_DIR, `${uuidv4()}-${originalName}`);
42
+ const outputFilePath = path.join(TEMP_DIR, `${uuidv4()}-${outputFilename || 'output'}`);
43
 
44
+ tempFiles.push(inputFilePath, outputFilePath);
45
+ task.outputFilePath = outputFilePath;
 
 
 
 
46
 
47
+ const processedArgs = args.map(arg =>
48
+ arg.replace('{INPUT_FILE}', inputFilePath)
49
+ .replace('{OUTPUT_FILE}', outputFilePath)
50
+ );
 
 
 
 
51
 
52
+ await writeFile(inputFilePath, inputBuffer);
53
+
54
+ const process = spawn(command, processedArgs);
55
+ let stderrOutput = '';
56
+ let totalDuration = 0;
57
+
58
+ process.stderr.on('data', (data) => {
59
+ const stderrLine = data.toString();
60
+ stderrOutput += stderrLine;
61
+
62
+ if (!totalDuration) {
63
+ const durationMatch = stderrLine.match(/Duration: (\d{2}):(\d{2}):(\d{2})\.\d{2}/);
64
+ if (durationMatch) {
65
+ totalDuration = parseInt(durationMatch[1]) * 3600 + parseInt(durationMatch[2]) * 60 + parseInt(durationMatch[3]);
66
+ task.estimatedTotalTime = totalDuration;
67
+ }
68
+ }
69
+
70
+ const timeMatch = stderrLine.match(/time=(\d{2}):(\d{2}):(\d{2})\.\d{2}/);
71
+ if (timeMatch && totalDuration) {
72
+ const currentTime = parseInt(timeMatch[1]) * 3600 + parseInt(timeMatch[2]) * 60 + parseInt(timeMatch[3]);
73
+ task.progress = Math.min(100, Math.round((currentTime / totalDuration) * 100));
74
+ }
75
+ });
76
+
77
+ await new Promise((resolve, reject) => {
78
+ process.on('close', (code) => {
79
+ task.endTime = Date.now();
80
+ task.stderr = stderrOutput;
81
+ if (code === 0) {
82
+ task.status = 'completed';
83
+ task.result = {
84
+ download_url: `/api/download/${path.basename(outputFilePath)}`
85
+ };
86
+ resolve();
87
+ } else {
88
+ const error = new Error(`Process exited with code ${code}`);
89
+ error.code = code;
90
+ reject(error);
91
+ }
92
+ });
93
+ process.on('error', reject);
94
+ });
95
 
96
  } catch (error) {
97
+ tasks[taskId].status = 'failed';
98
+ tasks[taskId].endTime = Date.now();
99
+ tasks[taskId].error = {
100
  message: error.message,
101
+ code: error.code
102
+ };
103
+ for (const filePath of tempFiles) {
104
+ unlink(filePath).catch(() => {});
105
+ }
106
  }
107
+ };
108
 
109
+ app.get('/', (req, res) => {
110
+ res.send('Task-based remote execution server is ready.');
111
+ });
112
 
113
+ app.post('/api/task/create', upload.single('file'), async (req, res) => {
114
  try {
115
  const { command, args: argsJson, file_url, output_filename } = req.body;
116
  const file = req.file;
117
 
118
+ if (!command) return res.status(400).json({ error: 'Parameter "command" is required.' });
119
 
120
  let args;
121
  try {
122
  args = argsJson ? JSON.parse(argsJson) : [];
123
  } catch(e) {
124
+ return res.status(400).json({ error: 'Parameter "args" must be a valid JSON array.' });
125
  }
126
 
127
  let inputBuffer;
 
130
  } else if (file_url) {
131
  inputBuffer = await downloadFile(file_url);
132
  } else {
133
+ return res.status(400).json({ error: 'A file must be provided via "file" or "file_url".' });
134
  }
 
 
 
 
 
 
 
 
135
 
136
+ const taskId = uuidv4();
137
+ tasks[taskId] = {
138
+ id: taskId,
139
+ status: 'queued',
140
+ progress: 0,
141
+ submittedAt: Date.now(),
142
+ payload: {
143
+ command,
144
+ args,
145
+ outputFilename: output_filename,
146
+ originalName: file?.originalname,
147
+ inputBuffer,
148
+ }
149
+ };
150
 
151
+ executeTask(taskId);
 
 
 
 
 
 
 
 
152
 
153
+ res.status(202).json({
154
+ message: "Task accepted.",
155
+ taskId: taskId,
156
+ status_url: `/api/task/status/${taskId}`
 
 
 
 
157
  });
158
 
159
  } catch (error) {
160
+ res.status(500).json({
161
+ error: 'Failed to create task.',
 
 
 
 
 
 
 
 
 
 
 
162
  message: error.message,
 
163
  });
164
  }
165
  });
166
 
167
+ app.get('/api/task/status/:taskId', (req, res) => {
168
+ const { taskId } = req.params;
169
+ const task = tasks[taskId];
170
+
171
+ if (!task) {
172
+ return res.status(404).json({ error: 'Task not found.' });
173
+ }
174
+
175
+ const response = {
176
+ id: task.id,
177
+ status: task.status,
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
+
186
+ if (task.status === 'processing') {
187
+ response.progress = task.progress;
188
+ if (task.estimatedTotalTime && response.elapsedTimeSeconds) {
189
+ const remaining = task.estimatedTotalTime - response.elapsedTimeSeconds;
190
+ response.estimatedTimeLeftSeconds = Math.max(0, remaining);
191
+ }
192
+ }
193
+
194
+ if (task.status === 'completed') {
195
+ response.result = task.result;
196
+ }
197
+
198
+ if (task.status === 'failed') {
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
+
208
  app.get('/api/download/:fileId', (req, res) => {
209
  const { fileId } = req.params;
210
  if (fileId.includes('..')) {
211
+ return res.status(400).send('Invalid file ID.');
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, () => {
 
231
  startCleanupJob();
232
  });
233
 
package.json CHANGED
@@ -1,22 +1,12 @@
1
  {
2
- "name": "huggingface-remote-runner",
3
- "version": "1.0.0",
4
- "description": "API для удаленного выполнения команд в Hugging Face Space",
5
  "main": "index.js",
6
  "type": "module",
7
  "scripts": {
8
- "start": "node index.js",
9
- "test": "echo \"Error: no test specified\" && exit 1"
10
  },
11
- "keywords": [
12
- "huggingface",
13
- "api",
14
- "remote-execution",
15
- "nodejs",
16
- "esm"
17
- ],
18
- "author": "",
19
- "license": "ISC",
20
  "dependencies": {
21
  "express": "^4.18.2",
22
  "multer": "^1.4.5-lts.1",
@@ -26,3 +16,4 @@
26
  }
27
  }
28
 
 
 
1
  {
2
+ "name": "huggingface-ffmpeg-api",
3
+ "version": "2.0.0",
4
+ "description": "ffmpeg rest api",
5
  "main": "index.js",
6
  "type": "module",
7
  "scripts": {
8
+ "start": "node index.js"
 
9
  },
 
 
 
 
 
 
 
 
 
10
  "dependencies": {
11
  "express": "^4.18.2",
12
  "multer": "^1.4.5-lts.1",
 
16
  }
17
  }
18
 
19
+