WalleGriffkinder commited on
Commit
d87d7b2
·
verified ·
1 Parent(s): e8a66ab

Create server.js

Browse files
Files changed (1) hide show
  1. server.js +268 -0
server.js ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import fs from 'fs/promises';
3
+ import fssync from 'fs';
4
+ import path from 'path';
5
+ import { spawn } from 'child_process';
6
+ import fetch from 'node-fetch';
7
+ import { glob } from 'glob';
8
+
9
+ // --- Конфигурация ---
10
+ const EXPRESS_PORT = parseInt(process.env.EXPRESS_PORT || '3001', 10);
11
+ const TELEGRAM_DATA_DIR = process.env.TELEGRAM_DATA_DIR || '/var/lib/telegram-bot-api';
12
+ const FILES_TTL_HOURS = parseInt(process.env.FILES_TTL || '-1', 10);
13
+
14
+ const GITHUB_USERNAME = process.env.GITHUB_USERNAME || '';
15
+ const GITHUB_TOKEN = process.env.GITHUB_TOKEN || '';
16
+ const ENV_GIST_ID = process.env.ENV_GIST_ID || '';
17
+
18
+ let currentTunnelUrl = ''; // Будет обновляться при запуске туннеля
19
+
20
+ const app = express();
21
+
22
+ // --- Вспомогательные функции ---
23
+ function formatBytes(bytes, decimals = 2) {
24
+ if (bytes === 0) return '0 Bytes';
25
+ const k = 1024;
26
+ const dm = decimals < 0 ? 0 : decimals;
27
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
28
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
29
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
30
+ }
31
+
32
+ async function getDirectoryStats(dirPath) {
33
+ try {
34
+ const files = await glob(`${dirPath}/**/*`, { nodir: true, dot: true });
35
+ let totalSize = 0;
36
+ for (const file of files) {
37
+ try {
38
+ const stats = await fs.stat(file);
39
+ totalSize += stats.size;
40
+ } catch (e) {
41
+ // Игнорируем ошибки для отдельных файлов (например, если файл удален во время сканирования)
42
+ }
43
+ }
44
+ return {
45
+ fileCount: files.length,
46
+ totalSizeBytes: totalSize,
47
+ totalSizeHuman: formatBytes(totalSize),
48
+ };
49
+ } catch (error) {
50
+ console.error(`Error getting directory stats for ${dirPath}:`, error);
51
+ return { fileCount: 0, totalSizeBytes: 0, totalSizeHuman: '0 Bytes', error: error.message };
52
+ }
53
+ }
54
+
55
+ async function cleanupOldFiles(dirPath, ttlHours) {
56
+ if (ttlHours <= 0) {
57
+ return { processed: 0, deleted: 0, errors: 0, message: 'Cleanup disabled (FILES_TTL <= 0)' };
58
+ }
59
+ console.log(`[TTL] Starting cleanup for files older than ${ttlHours} hours in ${dirPath}`);
60
+ const now = Date.now();
61
+ const ttlMs = ttlHours * 60 * 60 * 1000;
62
+ let processed = 0;
63
+ let deleted = 0;
64
+ let errors = 0;
65
+
66
+ try {
67
+ const files = await glob(`${dirPath}/**/*`, { nodir: true, dot: true, stat: true, withFileTypes: false }); // stat:true для mtime
68
+
69
+ for (const file of files) {
70
+ processed++;
71
+ try {
72
+ // glob с { stat: true } возвращает объекты с путем и fs.Stats, но mtime может быть не в том формате
73
+ // поэтому перепроверяем stat для каждого файла
74
+ const stats = await fs.stat(file); // file здесь это строка пути
75
+ if (stats.isFile()) {
76
+ const fileAge = now - stats.mtimeMs;
77
+ if (fileAge > ttlMs) {
78
+ await fs.unlink(file);
79
+ deleted++;
80
+ if (deleted % 100 === 0) console.log(`[TTL] Deleted ${deleted} old files so far...`);
81
+ }
82
+ }
83
+ } catch (e) {
84
+ console.error(`[TTL] Error processing file ${file}:`, e.message);
85
+ errors++;
86
+ }
87
+ }
88
+ // Попытка удалить пустые директории (опционально, может быть сложно и рискованно)
89
+ // Для простоты пока не реализуем удаление пустых директорий после очистки файлов.
90
+ } catch (globError) {
91
+ console.error(`[TTL] Error during glob search:`, globError);
92
+ return { processed, deleted, errors: errors + 1, message: `Glob error: ${globError.message}` };
93
+ }
94
+
95
+ const result = { processed, deleted, errors, message: `Cleanup completed. Processed: ${processed}, Deleted: ${deleted}, Errors: ${errors}` };
96
+ console.log(`[TTL] ${result.message}`);
97
+ return result;
98
+ }
99
+
100
+ // --- Обновление Gist ---
101
+ async function updateEnvGistInGithub(tunnelUrlToSave) {
102
+ if (!GITHUB_USERNAME || !GITHUB_TOKEN || !ENV_GIST_ID) {
103
+ console.warn('Gist update skipped: GITHUB_USERNAME, GITHUB_TOKEN, or ENV_GIST_ID is not set.');
104
+ return;
105
+ }
106
+ try {
107
+ const spaceId = process.env.SPACE_ID || 'N/A';
108
+ const spaceHost = process.env.SPACE_HOST || 'N/A';
109
+ const content = {
110
+ last_updated: new Date().toISOString(),
111
+ space_id: spaceId,
112
+ space_host: spaceHost,
113
+ tools_tunnel_url: tunnelUrlToSave || 'pending...',
114
+ telegram_api_main_port: 7860,
115
+ tools_app_internal_port: EXPRESS_PORT,
116
+ files_ttl_hours: FILES_TTL_HOURS > 0 ? FILES_TTL_HOURS : 'disabled',
117
+ };
118
+ const gistData = {
119
+ description: `Hugging Face Space Info - ${spaceId}`,
120
+ files: {
121
+ [`hf_space_env_${spaceId}.json`]: {
122
+ content: JSON.stringify(content, null, 2),
123
+ },
124
+ },
125
+ };
126
+ const response = await fetch(`https://api.github.com/gists/${ENV_GIST_ID}`, {
127
+ method: 'PATCH',
128
+ headers: {
129
+ 'Authorization': `token ${GITHUB_TOKEN}`,
130
+ 'Accept': 'application/vnd.github.v3+json',
131
+ 'User-Agent': 'HFSpaceTgAPITools',
132
+ 'Content-Type': 'application/json',
133
+ },
134
+ body: JSON.stringify(gistData),
135
+ });
136
+ if (!response.ok) {
137
+ throw new Error(`GitHub API error: ${response.status} ${await response.text()}`);
138
+ }
139
+ console.log(`Gist ${ENV_GIST_ID} updated successfully with tunnel URL: ${tunnelUrlToSave}`);
140
+ } catch (error) {
141
+ console.error('Error updating Gist:', error);
142
+ }
143
+ }
144
+
145
+
146
+ // --- Маршруты Express ---
147
+ app.get('/', (req, res) => {
148
+ res.send(`Telegram API Tools. Tunnel: ${currentTunnelUrl || 'pending...'}. Stats: ${currentTunnelUrl}/stats. File base: ${currentTunnelUrl}/file/`);
149
+ });
150
+
151
+ app.get('/stats', async (req, res) => {
152
+ const stats = await getDirectoryStats(TELEGRAM_DATA_DIR);
153
+ let ttlCleanupResult = { message: "TTL cleanup not run or disabled." };
154
+ if (req.query.run_ttl_now === 'true' && FILES_TTL_HOURS > 0) {
155
+ ttlCleanupResult = await cleanupOldFiles(TELEGRAM_DATA_DIR, FILES_TTL_HOURS);
156
+ }
157
+ res.json({
158
+ directory: TELEGRAM_DATA_DIR,
159
+ ...stats,
160
+ files_ttl_hours: FILES_TTL_HOURS > 0 ? FILES_TTL_HOURS : 'disabled',
161
+ ttl_cleanup_on_this_request: ttlCleanupResult
162
+ });
163
+ });
164
+
165
+ app.get('/file/:filepath(*)', (req, res) => {
166
+ const relativePath = req.params.filepath;
167
+ if (!relativePath || relativePath.includes('..')) { // Простая проверка на '..'
168
+ return res.status(400).send('Invalid file path.');
169
+ }
170
+
171
+ // Нормализуем путь и убеждаемся, что он внутри TELEGRAM_DATA_DIR
172
+ const absoluteRequestedPath = path.normalize(path.join(TELEGRAM_DATA_DIR, relativePath));
173
+
174
+ if (!absoluteRequestedPath.startsWith(path.resolve(TELEGRAM_DATA_DIR))) {
175
+ return res.status(403).send('Forbidden: Access outside designated directory is not allowed.');
176
+ }
177
+
178
+ if (fssync.existsSync(absoluteRequestedPath)) {
179
+ const stats = fssync.statSync(absoluteRequestedPath);
180
+ if (stats.isFile()) {
181
+ res.sendFile(absoluteRequestedPath, (err) => {
182
+ if (err) {
183
+ console.error(`Error sending file ${absoluteRequestedPath}:`, err);
184
+ if (!res.headersSent) {
185
+ res.status(500).send('Error sending file.');
186
+ }
187
+ }
188
+ });
189
+ } else {
190
+ res.status(404).send('Path is not a file.');
191
+ }
192
+ } else {
193
+ res.status(404).send('File not found.');
194
+ }
195
+ });
196
+
197
+ // --- Запуск сервера и туннеля ---
198
+ app.listen(EXPRESS_PORT, () => {
199
+ console.log(`Express server (for tools) listening on port ${EXPRESS_PORT}`);
200
+ console.log(`Attempting to start localhost.run tunnel for port ${EXPRESS_PORT}...`);
201
+
202
+ const tunnelProcess = spawn('ssh', [
203
+ '-R', `80:localhost:${EXPRESS_PORT}`,
204
+ '-o', 'StrictHostKeyChecking=no',
205
+ '-o', 'UserKnownHostsFile=/dev/null',
206
+ '-o', 'ServerAliveInterval=60',
207
+ '-o', 'ExitOnForwardFailure=yes',
208
+ '-o', 'LogLevel=ERROR', // Меньше логов от ssh, если все ок
209
+ 'nokey@localhost.run'
210
+ ]);
211
+
212
+ tunnelProcess.stdout.on('data', (data) => {
213
+ const output = data.toString();
214
+ // Ищем URL в формате https://*.lhr.life или https://*.lhr.run
215
+ const urlMatch = output.match(/https?:\/\/[a-zA-Z0-9-]+\.(lhr\.life|lhr\.run)/);
216
+ if (urlMatch && urlMatch[0] !== currentTunnelUrl) {
217
+ currentTunnelUrl = urlMatch[0];
218
+ console.log(`>>> Tools Tunnel active: ${currentTunnelUrl}`);
219
+ updateEnvGistInGithub(currentTunnelUrl).catch(console.error);
220
+ }
221
+ // Выводим весь stdout для отладки, если URL не найден сразу
222
+ if (!urlMatch) {
223
+ console.log(`localhost.run stdout: ${output}`);
224
+ }
225
+ });
226
+
227
+ tunnelProcess.stderr.on('data', (data) => {
228
+ console.error(`localhost.run stderr: ${data.toString()}`);
229
+ });
230
+
231
+ tunnelProcess.on('close', (code) => {
232
+ console.log(`localhost.run tunnel process exited with code ${code}`);
233
+ currentTunnelUrl = ''; // Сбрасываем URL, если туннель упал
234
+ updateEnvGistInGithub('Tunnel closed or failed.').catch(console.error);
235
+ });
236
+
237
+ tunnelProcess.on('error', (err) => {
238
+ console.error('Failed to start localhost.run tunnel process:', err);
239
+ currentTunnelUrl = '';
240
+ updateEnvGistInGithub('Tunnel failed to start.').catch(console.error);
241
+ });
242
+
243
+ // Первоначальное обновление Gist
244
+ updateEnvGistInGithub('Tunnel URL pending...').catch(console.error);
245
+
246
+ // Периодическая очистка файлов, если TTL настроен
247
+ if (FILES_TTL_HOURS > 0) {
248
+ const ttlIntervalMs = FILES_TTL_HOURS * 60 * 60 * 1000;
249
+ // const ttlIntervalMs = 60 * 1000; // Для теста - каждую минуту
250
+ console.log(`[TTL] Scheduling cleanup every ${FILES_TTL_HOURS} hours.`);
251
+ // Запуск первой очистки через некоторое время после старта, чтобы дать системе "успокоиться"
252
+ setTimeout(() => {
253
+ cleanupOldFiles(TELEGRAM_DATA_DIR, FILES_TTL_HOURS).catch(console.error);
254
+ setInterval(() => {
255
+ cleanupOldFiles(TELEGRAM_DATA_DIR, FILES_TTL_HOURS).catch(console.error);
256
+ }, ttlIntervalMs);
257
+ }, 5 * 60 * 1000); // Первая очистка через 5 минут
258
+ }
259
+ });
260
+
261
+ // Обработка сигналов для корректного завершения
262
+ function gracefulShutdown() {
263
+ console.log('Received shutdown signal. Closing server...');
264
+ // Здесь можно добавить закрытие сервера Express, если нужно дождаться завершения запросов
265
+ process.exit(0);
266
+ }
267
+ process.on('SIGINT', gracefulShutdown);
268
+ process.on('SIGTERM', gracefulShutdown);