Files
amnezia_web-PRO/scripts/clients-meta.mjs
Andrey 34415a4b8a fix: keep disconnect dates in the clients metadata snapshot
Даты «Задать дату» (lastDisconnectedAt) и отключение по расписанию
(scheduledTunnelDisconnectAt) тоже живут в clientsTable и терялись при
затирании. Снимок хранит и их, но возвращает только когда запись реально
затёрли (пропал creationDate) — иначе воскресало бы снятое в панели расписание.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 11:40:14 +03:00

88 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Страховка от затирания clientsTable приложением AmneziaVPN.
*
* Приложение при переподключении/перезагрузке сервера пересобирает clientsTable
* из awg0.conf: остаются только clientId и дефолтные имена «Client N», а имена,
* даты создания и last_config, которые ведёт панель, пропадают.
*
* Панель держит свою копию этих полей в DATA_DIR (том /data переживает
* пересоздание контейнера) и возвращает их на место при следующем чтении.
*/
import fs from "fs";
import path from "path";
/** Поля, которые панель сама никогда не стирает: пустое значение = потеря. */
export const STABLE_FIELDS = ["clientName", "creationDate", "last_config", "allowedIps"];
/**
* Даты, которые панель штатно снимает («Задать дату», отключение по расписанию).
* Их возвращаем только когда запись действительно затёрли, иначе воскресим
* снятое расписание.
*/
export const DATE_FIELDS = ["lastDisconnectedAt", "scheduledTunnelDisconnectAt"];
export const META_FIELDS = [...STABLE_FIELDS, ...DATE_FIELDS];
/** «Client 17» — дефолтное имя от приложения, оно не ценнее сохранённого. */
export function isPlaceholderName(name) {
return /^Client\s*\d+$/i.test(String(name || "").trim());
}
function isMeaningful(field, value) {
if (value == null || value === "") return false;
return field === "clientName" ? !isPlaceholderName(value) : true;
}
function metaFile(dataDir, profileId) {
const safe = String(profileId || "default").replace(/[^\w.-]/g, "_");
return path.join(dataDir, "clients-meta", `${safe}.json`);
}
export function readClientsMeta(dataDir, profileId) {
try {
const data = JSON.parse(fs.readFileSync(metaFile(dataDir, profileId), "utf8"));
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
} catch {
return {};
}
}
export function writeClientsMeta(dataDir, profileId, meta) {
const file = metaFile(dataDir, profileId);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, `${JSON.stringify(meta, null, 2)}\n`);
}
/**
* Возвращает клиентов с восстановленными полями и число восстановленных значений.
* Заодно обновляет снимок метаданных теми значениями, что сейчас на сервере;
* metaChanged=false — снимок писать не нужно (панель читает состояние часто).
*/
export function healClientsMeta(clients, meta) {
let restored = 0;
const before = JSON.stringify(meta);
const nextMeta = { ...meta };
const healed = clients.map((row) => {
const id = row?.clientId;
if (!id) return row;
const userData = { ...(row.userData || {}) };
const saved = nextMeta[id] || {};
// Приложение сносит creationDate целиком — по нему и опознаём затирание.
const wiped = !isMeaningful("creationDate", userData.creationDate) && isMeaningful("creationDate", saved.creationDate);
for (const field of wiped ? META_FIELDS : STABLE_FIELDS) {
if (!isMeaningful(field, userData[field]) && isMeaningful(field, saved[field])) {
userData[field] = saved[field];
restored++;
}
}
const keep = {};
for (const field of META_FIELDS) {
if (isMeaningful(field, userData[field])) keep[field] = userData[field];
}
if (Object.keys(keep).length) nextMeta[id] = keep;
else delete nextMeta[id];
return { ...row, userData };
});
return { clients: healed, restored, meta: nextMeta, metaChanged: JSON.stringify(nextMeta) !== before };
}