opex792 commited on
Commit
ebf7adb
·
verified ·
1 Parent(s): 0d25770

Upload 4 files

Browse files
Files changed (3) hide show
  1. Dockerfile +0 -6
  2. cleanup.js +20 -29
  3. index.js +20 -73
Dockerfile CHANGED
@@ -1,5 +1,4 @@
1
  # Используем официальный образ Node.js 18.
2
- # ESM-модули и fetch API стабильны в этой версии.
3
  FROM node:18-slim
4
 
5
  # Устанавливаем рабочую директорию внутри контейнера
@@ -9,20 +8,15 @@ WORKDIR /usr/src/app
9
  COPY package*.json ./
10
 
11
  # Устанавливаем зависимости проекта
12
- # --only=production гарантирует, что установятся только производственные зависимости
13
  RUN npm install --only=production
14
 
15
  # Копируем остальной код приложения в рабочую директорию
16
  COPY . .
17
 
18
- # ИСПРАВЛЕНИЕ: Строка ниже удалена, т.к. приложение само создает нужную директорию
19
- # RUN mkdir -p /tmp/huggingface-runner
20
-
21
  # Открываем порт, на котором будет работать приложение (стандартный для Spaces - 7860)
22
  EXPOSE 7860
23
 
24
  # Указываем команду для запуска приложения при старте контейнера
25
- # Используем массив для корректной обработки сигналов
26
  CMD [ "node", "index.js" ]
27
 
28
 
 
1
  # Используем официальный образ Node.js 18.
 
2
  FROM node:18-slim
3
 
4
  # Устанавливаем рабочую директорию внутри контейнера
 
8
  COPY package*.json ./
9
 
10
  # Устанавливаем зависимости проекта
 
11
  RUN npm install --only=production
12
 
13
  # Копируем остальной код приложения в рабочую директорию
14
  COPY . .
15
 
 
 
 
16
  # Открываем порт, на котором будет работать приложение (стандартный для Spaces - 7860)
17
  EXPOSE 7860
18
 
19
  # Указываем команду для запуска приложения при старте контейнера
 
20
  CMD [ "node", "index.js" ]
21
 
22
 
cleanup.js CHANGED
@@ -1,18 +1,11 @@
1
  import cron from 'node-cron';
2
  import fs from 'fs/promises';
3
  import path from 'path';
4
- import { fileURLToPath } from 'url';
5
-
6
- // ИСПРАВЛЕНИЕ: Получаем путь к директории аналогично index.js
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
9
- const TEMP_DIR = path.join(__dirname, 'tmp');
10
 
 
 
11
  const MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 часа в миллисекундах
12
 
13
- /**
14
- * Удаляет старые файлы из временной директории.
15
- */
16
  async function cleanupOldFiles() {
17
  console.log('Запуск задачи по очистке старых файлов...');
18
  try {
@@ -20,24 +13,28 @@ async function cleanupOldFiles() {
20
  const now = Date.now();
21
 
22
  for (const file of files) {
23
- const filePath = path.join(TEMP_DIR, file);
24
- try {
25
- const stats = await fs.stat(filePath);
26
- const fileAge = now - stats.mtime.getTime();
27
-
28
- if (fileAge > MAX_AGE_MS) {
29
- await fs.unlink(filePath);
30
- console.log(`Удален старый файл: ${filePath}`);
31
- }
32
- } catch (statError) {
33
- // Если файл был удален между readdir и stat, просто игнорируем ошибку
34
- if (statError.code !== 'ENOENT') {
35
- console.error(`Не удалось получить информацию о файле ${filePath}:`, statError);
 
 
 
36
  }
37
  }
38
  }
39
  } catch (readDirError) {
40
  if (readDirError.code === 'ENOENT') {
 
41
  console.log('Временная директория не существует, очистка не требуется.');
42
  } else {
43
  console.error('Ошибка при чтении временной директории:', readDirError);
@@ -46,15 +43,9 @@ async function cleanupOldFiles() {
46
  console.log('Очистка завершена.');
47
  }
48
 
49
- /**
50
- * Запускает периодическую задачу (cron job) для очистки.
51
- * Задача будет выполняться каждый час.
52
- */
53
  export function startCleanupJob() {
54
- // Запускаем сразу при старте, а затем каждый час
55
  cleanupOldFiles();
56
-
57
- cron.schedule('0 * * * *', cleanupOldFiles); // '0 * * * *' - каждую 0-ю минуту каждого часа
58
  console.log('Задача по очистке запланирована на запуск каждый час.');
59
  }
60
 
 
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 {
 
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);
 
43
  console.log('Очистка завершена.');
44
  }
45
 
 
 
 
 
46
  export function startCleanupJob() {
 
47
  cleanupOldFiles();
48
+ cron.schedule('0 * * * *', cleanupOldFiles);
 
49
  console.log('Задача по очистке запланирована на запуск каждый час.');
50
  }
51
 
index.js CHANGED
@@ -1,50 +1,30 @@
1
  import express from 'express';
2
  import multer from 'multer';
3
  import { spawn } from 'child_process';
4
- import { writeFile, unlink, createReadStream, mkdirSync } from 'fs';
5
- import { fileURLToPath } from 'url';
6
- import path, { dirname } from 'path';
7
  import { v4 as uuidv4 } from 'uuid';
8
  import fetch from 'node-fetch';
9
  import { startCleanupJob } from './cleanup.js';
10
 
11
  // --- НАСТРОЙКА ---
12
- const __filename = fileURLToPath(import.meta.url);
13
- const __dirname = dirname(__filename);
14
-
15
- // ИСПРАВЛЕНИЕ: Создаем временную директорию внутри проекта, чтобы избежать проблем с правами.
16
- const TEMP_DIR = path.join(__dirname, 'tmp');
17
-
18
- // Создаем директорию синхронно при запуске, если она не существует.
19
- try {
20
- mkdirSync(TEMP_DIR, { recursive: true });
21
- console.log(`Временная директория готова для использования: ${TEMP_DIR}`);
22
- } catch (e) {
23
- console.error(`Критическая ошибка: не удалось создать временную директорию ${TEMP_DIR}.`, e);
24
- process.exit(1); // Завершаем работу, если не можем создать папку.
25
- }
26
-
27
 
28
  const app = express();
29
- const PORT = process.env.PORT || 7860; // Hugging Face Spaces использует порт 7860
 
 
30
 
31
  // --- MIDDLEWARE ---
32
  app.use(express.json());
33
  app.use(express.urlencoded({ extended: true }));
34
 
35
- // Настройка Multer для загрузки файлов в память
36
  const storage = multer.memoryStorage();
37
  const upload = multer({ storage: storage });
38
 
39
  // --- ЛОГИКА ---
40
 
41
- /**
42
- * Функция для выполнения внешней команды.
43
- * @param {string} command - Команда для выполнения (например, 'ffmpeg').
44
- * @param {Array<string>} args - Аргументы для команды.
45
- * @param {Buffer | null} inputBuffer - Входные данные для stdin.
46
- * @returns {Promise<{stdout: Buffer, stderr: string}>} - Промис с результатом.
47
- */
48
  const executeCommand = (command, args, inputBuffer = null) => {
49
  return new Promise((resolve, reject) => {
50
  const process = spawn(command, args);
@@ -76,12 +56,6 @@ const executeCommand = (command, args, inputBuffer = null) => {
76
  });
77
  };
78
 
79
-
80
- /**
81
- * Скачивает файл по URL.
82
- * @param {string} url - URL файла.
83
- * @returns {Promise<Buffer>} - Промис с буфером файла.
84
- */
85
  const downloadFile = async (url) => {
86
  const response = await fetch(url);
87
  if (!response.ok) {
@@ -97,22 +71,12 @@ app.get('/', (req, res) => {
97
  res.send('Сервер удаленного выполнения команд готов к работе!');
98
  });
99
 
100
-
101
- /**
102
- * МАРШРУТ 1: Потоковая обработка (без сохранения)
103
- * Принимает файл (загрузка или URL) и команду, возвращает результат потоком.
104
- * POST /api/run/stream
105
- * Body: { command: "ваша команда", args: ["аргумент1", "аргумент2"], file_url?: "URL файла" }
106
- * или multipart/form-data с полями command, args и file.
107
- */
108
  app.post('/api/run/stream', upload.single('file'), async (req, res) => {
109
  try {
110
  const { command, args: argsJson, file_url } = req.body;
111
  const file = req.file;
112
 
113
- if (!command) {
114
- return res.status(400).send({ error: 'Параметр "command" обязателен.' });
115
- }
116
 
117
  let args;
118
  try {
@@ -134,7 +98,6 @@ app.post('/api/run/stream', upload.single('file'), async (req, res) => {
134
 
135
  console.log(`Stderr для ${command}: ${stderr}`);
136
 
137
- // Отправляем результат как бинарные данные
138
  res.setHeader('Content-Type', 'application/octet-stream');
139
  res.send(stdout);
140
 
@@ -148,22 +111,14 @@ app.post('/api/run/stream', upload.single('file'), async (req, res) => {
148
  }
149
  });
150
 
151
-
152
- /**
153
- * МАРШРУТ 2: Обработка с сохранением файла
154
- * Сохраняет файл, выполняет команду, сохраняет результат и отдает ссылку.
155
- * POST /api/run/file
156
- */
157
  app.post('/api/run/file', upload.single('file'), async (req, res) => {
158
- const tempFiles = []; // Массив для отслеживания созданных файлов
159
 
160
  try {
161
  const { command, args: argsJson, file_url, output_filename } = req.body;
162
  const file = req.file;
163
 
164
- if (!command) {
165
- return res.status(400).send({ error: 'Параметр "command" обязателен.' });
166
- }
167
 
168
  let args;
169
  try {
@@ -180,9 +135,11 @@ app.post('/api/run/file', upload.single('file'), async (req, res) => {
180
  } else {
181
  return res.status(400).send({ error: 'Необходимо предоставить файл через "file" или "file_url".' });
182
  }
183
-
184
- // Заменяем плейсхолдеры в аргументах
185
- const inputFilePath = path.join(TEMP_DIR, `${uuidv4()}-${file?.originalname || 'input'}`);
 
 
186
  const outputFilePath = path.join(TEMP_DIR, `${uuidv4()}-${output_filename || 'output'}`);
187
 
188
  tempFiles.push(inputFilePath, outputFilePath);
@@ -192,7 +149,6 @@ app.post('/api/run/file', upload.single('file'), async (req, res) => {
192
  .replace('{OUTPUT_FILE}', outputFilePath)
193
  );
194
 
195
- // Сохраняем входной файл
196
  await new Promise((resolve, reject) => {
197
  writeFile(inputFilePath, inputBuffer, (err) => {
198
  if (err) {
@@ -203,12 +159,9 @@ app.post('/api/run/file', upload.single('file'), async (req, res) => {
203
  });
204
  });
205
 
206
- // Выполняем команду
207
- // Здесь stdin не используется, так как команда работает с файлами на диске
208
  const { stderr } = await executeCommand(command, processedArgs);
209
  console.log(`Stderr для ${command}: ${stderr}`);
210
 
211
- // Отправляем ссылку на скачивание файла
212
  const fileId = path.basename(outputFilePath);
213
  res.status(200).json({
214
  message: 'Команда выполнена успешно.',
@@ -219,10 +172,12 @@ app.post('/api/run/file', upload.single('file'), async (req, res) => {
219
  } catch (error) {
220
  console.error('Ошибка в /api/run/file:', error);
221
 
222
- // Удаляем временные файлы в случае ошибки
223
  for (const filePath of tempFiles) {
224
  unlink(filePath, (err) => {
225
- if (err) console.error(`Не удалось удалить временный файл ${filePath}:`, err);
 
 
 
226
  });
227
  }
228
 
@@ -234,26 +189,20 @@ app.post('/api/run/file', upload.single('file'), async (req, res) => {
234
  }
235
  });
236
 
237
-
238
- /**
239
- * МАРШРУТ 3: Скачивание файла по ID
240
- * GET /api/download/:fileId
241
- */
242
  app.get('/api/download/:fileId', (req, res) => {
243
  const { fileId } = req.params;
244
- // Важно: проверяем, что fileId не содержит ".." для предотвращения выхода из директории
245
  if (fileId.includes('..')) {
246
  return res.status(400).send('Неверный ID файла.');
247
  }
248
 
249
  const filePath = path.join(TEMP_DIR, fileId);
250
 
251
- // Проверяем, существует ли файл, и отправляем его
252
  const stream = createReadStream(filePath);
253
  stream.on('error', (err) => {
254
  if (err.code === 'ENOENT') {
255
  res.status(404).send('Файл не найден или был удален.');
256
  } else {
 
257
  res.status(500).send('Ошибка сервера.');
258
  }
259
  });
@@ -262,12 +211,10 @@ app.get('/api/download/:fileId', (req, res) => {
262
  stream.pipe(res);
263
  });
264
 
265
-
266
  // --- ЗАПУСК СЕРВЕРА И ОЧИСТКИ ---
267
 
268
  app.listen(PORT, () => {
269
  console.log(`Сервер запущен на порту ${PORT}`);
270
- // Запускаем задачу по очистке старых файлов
271
  startCleanupJob();
272
  });
273
 
 
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);
 
56
  });
57
  };
58
 
 
 
 
 
 
 
59
  const downloadFile = async (url) => {
60
  const response = await fetch(url);
61
  if (!response.ok) {
 
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 {
 
98
 
99
  console.log(`Stderr для ${command}: ${stderr}`);
100
 
 
101
  res.setHeader('Content-Type', 'application/octet-stream');
102
  res.send(stdout);
103
 
 
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 {
 
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);
 
149
  .replace('{OUTPUT_FILE}', outputFilePath)
150
  );
151
 
 
152
  await new Promise((resolve, reject) => {
153
  writeFile(inputFilePath, inputBuffer, (err) => {
154
  if (err) {
 
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: 'Команда выполнена успешно.',
 
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
 
 
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
  });
 
211
  stream.pipe(res);
212
  });
213
 
 
214
  // --- ЗАПУСК СЕРВЕРА И ОЧИСТКИ ---
215
 
216
  app.listen(PORT, () => {
217
  console.log(`Сервер запущен на порту ${PORT}`);
 
218
  startCleanupJob();
219
  });
220