1.3.0-beta.6: пульт «Файлы» — текст пользователя (что здесь хранится и сколько), файлы, которые ещё сохраняются из MAX, кнопка «Обновить»

This commit is contained in:
Folist 2026-10-09 23:13:07 +03:00
parent 94a988fc96
commit 9bdf4340dc
4 changed files with 29 additions and 7 deletions

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "telemax",
"version": "1.3.0-beta.5",
"version": "1.3.0-beta.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "telemax",
"version": "1.3.0-beta.5",
"version": "1.3.0-beta.6",
"license": "AGPL-3.0-only",
"dependencies": {
"dotenv": "^17.2.3",

View file

@ -2,7 +2,7 @@
"name": "telemax",
"private": true,
"license": "AGPL-3.0-only",
"version": "1.3.0-beta.5",
"version": "1.3.0-beta.6",
"type": "module",
"scripts": {
"dev": "tsx watch src/server/app.ts",

View file

@ -123,6 +123,8 @@ export class FileShare {
private chain: Promise<unknown> = Promise.resolve();
private uploadHandler: ((t: UploadTicket, filePath: string, size: number, name: string) => Promise<void>) | null = null;
private busyTokens = new Set<string>();
/** Files being saved from MAX right now — the panel lists them before they're done. */
private saving = new Map<string, { name: string; size: number; written: number }>();
constructor(
dataDir: string,
@ -200,6 +202,11 @@ export class FileShare {
return this.index.tickets.find((t) => t.token === token);
}
/** Files still being saved from MAX: name, declared size, bytes so far. */
inProgress(): Array<{ name: string; size: number; written: number }> {
return [...this.saving.values()];
}
pendingUploads(): number {
return this.index.tickets.length;
}
@ -286,9 +293,12 @@ export class FileShare {
const part = path.join(dir, `${safe}.part`);
let written = 0;
const limit = Math.max(size, 0) + 1024 * 1024; // the declared size, a little slack
const progress = { name: safe, size, written: 0 };
this.saving.set(id, progress);
const guard = new Transform({
transform(chunk: Buffer, _enc, cb) {
written += chunk.length;
progress.written = written;
onProgress?.(written);
if (written > limit) cb(new Error(`file grew past its declared ${size} bytes`));
else cb(null, chunk);
@ -300,6 +310,8 @@ export class FileShare {
} catch (err) {
await rm(dir, { recursive: true, force: true });
throw err;
} finally {
this.saving.delete(id);
}
return this.locked(() => this.addFile(id, safe, written, 'max2tg'));
}

View file

@ -161,21 +161,31 @@ function filesView(files: FileShare): View {
return { text: '📁 Пересылка больших файлов отключена: FILES=off в .env.', markup: Markup.inlineKeyboard([back]).reply_markup };
}
const list = files.list();
const saving = files.inProgress();
const pending = files.pendingUploads();
const pendingLine = pending ? `\n\n⏳ Ждут загрузки по ссылке: ${pending}.` : '';
const ttl = ttlText(files.opts.ttlMs);
const savingLines = saving.map((x) => `⏳ Сохраняется: ${x.name} — ${formatBytes(x.written)} из ${formatBytes(x.size)}`);
const pendingLine = pending ? `⏳ Ждут загрузки по ссылке: ${pending}` : '';
const footer =
'Здесь хранятся файлы, которые мост держит на сервере для скачивания по ссылке:\n' +
'• файлы из MAX больше 50 МБ — Telegram не принимает от ботов такие файлы напрямую;\n' +
'• загруженные для отправки в MAX, но не ушедшие из-за ошибки — их можно отправить ещё раз.\n\n' +
'Файлы, которые уже ушли в MAX, на сервере не остаются.\n' +
`Файлы со ссылкой хранятся ${ttl} от последней выданной ссылки, потом удаляются.`;
const extra = [...savingLines, pendingLine].filter(Boolean).join('\n');
if (list.length === 0) {
return {
text: `📁 Файлов нет.\n\nСюда попадают большие файлы, которые мост пересылает по ссылке: из MAX — больше 50 МБ, в MAX — больше 4 ГБ. Файл живёт ${ttl} от последней выданной ссылки, потом удаляется.${pendingLine}`,
markup: Markup.inlineKeyboard([back]).reply_markup,
text: `📁 Файлов нет.${extra ? `\n\n${extra}` : ''}\n\n${footer}`,
markup: Markup.inlineKeyboard([[Markup.button.callback('🔄 Обновить', 'tlmx_panel:files')], back]).reply_markup,
};
}
const total = list.reduce((s, f) => s + f.size, 0);
const lines = list.map((f, i) => `${i + 1}. ${f.direction === 'max2tg' ? '⬇️' : '⬆️'} ${f.name} — ${formatBytes(f.size)}, до ${formatExpiry(f.expiresAt)}`);
return {
text: `📁 Файлы на сервере: ${list.length}, всего ${formatBytes(total)}.\n⬇️ — из MAX, ⬆️ — для MAX. Ссылка живёт ${ttl}, потом файл удаляется.\n\n${lines.join('\n')}${pendingLine}`,
text: `📁 Файлы на сервере: ${list.length}, всего ${formatBytes(total)}.\n⬇️ — из MAX, ⬆️ — не ушли в MAX.\n\n${lines.join('\n')}${extra ? `\n\n${extra}` : ''}\n\n${footer}`,
markup: Markup.inlineKeyboard([
...list.slice(0, 20).map((f, i) => [Markup.button.callback(`${i + 1}. ${truncateUtf16(f.name, 48)}`, `tlmx_panel:file:${f.id}`)]),
[Markup.button.callback('🔄 Обновить', 'tlmx_panel:files')],
back,
]).reply_markup,
};