- 移除 VMess 协议 - VLESS/SS2022 按用户流量统计(AnyTLS 日志解析,SS 节点级) - sing-box v2ray_api stats 支持 - 内存优化:面板 128M,agent 64M,sing-box 64M
111 lines
3.3 KiB
JavaScript
111 lines
3.3 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const COLLECTIONS = [
|
|
'users', 'servers', 'agents', 'enrollmentTokens', 'customers', 'nodes',
|
|
'nodeClients', 'jobs', 'activity', 'observations', 'subscriptionAccess', 'subscriptionClients'
|
|
];
|
|
|
|
function emptyData() {
|
|
return {
|
|
schemaVersion: 1,
|
|
createdAt: new Date().toISOString(),
|
|
settings: {
|
|
brandName: 'NexusGate',
|
|
sessionHours: 12,
|
|
observationTtlMinutes: 10,
|
|
activityRetentionDays: 30,
|
|
completedJobRetentionDays: 7
|
|
},
|
|
users: [], servers: [], agents: [], enrollmentTokens: [], customers: [],
|
|
nodes: [], nodeClients: [], jobs: [], activity: [], observations: [], subscriptionAccess: [], subscriptionClients: []
|
|
};
|
|
}
|
|
|
|
class Store {
|
|
static SKIP = Symbol('no data changes');
|
|
|
|
constructor(file) {
|
|
this.file = path.resolve(file);
|
|
this.data = null;
|
|
this.writeQueue = Promise.resolve();
|
|
}
|
|
|
|
async init() {
|
|
await fs.promises.mkdir(path.dirname(this.file), { recursive: true, mode: 0o700 });
|
|
try {
|
|
const raw = await fs.promises.readFile(this.file, 'utf8');
|
|
this.data = JSON.parse(raw);
|
|
this.data.subscriptionAccess ||= [];
|
|
this.data.subscriptionClients ||= [];
|
|
this.#validate(this.data);
|
|
} catch (error) {
|
|
if (error.code !== 'ENOENT') throw error;
|
|
this.data = emptyData();
|
|
await this.#persist(this.data);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
snapshot() {
|
|
return structuredClone(this.data);
|
|
}
|
|
|
|
async transaction(mutator) {
|
|
const operation = async () => {
|
|
const draft = structuredClone(this.data);
|
|
const result = await mutator(draft);
|
|
if (result === Store.SKIP) return null;
|
|
this.#validate(draft);
|
|
await this.#persist(draft);
|
|
this.data = draft;
|
|
return result;
|
|
};
|
|
this.writeQueue = this.writeQueue.then(operation, operation);
|
|
return this.writeQueue;
|
|
}
|
|
|
|
// Status/diagnostic updates are useful immediately but need not force a
|
|
// whole-database rewrite. Serialize them with durable transactions so a
|
|
// pending file write cannot replace a newer in-memory heartbeat.
|
|
async transient(mutator) {
|
|
const operation = () => mutator(this.data);
|
|
this.writeQueue = this.writeQueue.then(operation, operation);
|
|
return this.writeQueue;
|
|
}
|
|
|
|
async replace(next) {
|
|
return this.transaction((draft) => {
|
|
const copy = structuredClone(next);
|
|
copy.subscriptionAccess ||= [];
|
|
copy.subscriptionClients ||= [];
|
|
this.#validate(copy);
|
|
for (const key of Object.keys(draft)) delete draft[key];
|
|
Object.assign(draft, copy);
|
|
});
|
|
}
|
|
|
|
#validate(data) {
|
|
if (!data || data.schemaVersion !== 1 || !data.settings) {
|
|
throw new Error('Unsupported or corrupt NexusGate data file');
|
|
}
|
|
for (const name of COLLECTIONS) {
|
|
if (!Array.isArray(data[name])) throw new Error(`Invalid collection: ${name}`);
|
|
}
|
|
}
|
|
|
|
async #persist(data) {
|
|
const temp = `${this.file}.${process.pid}.tmp`;
|
|
// The entire database is written for each durable transaction. Compact
|
|
// JSON avoids multiplying disk I/O and retained backup size by whitespace.
|
|
const body = `${JSON.stringify(data)}\n`;
|
|
await fs.promises.writeFile(temp, body, { mode: 0o600 });
|
|
await fs.promises.rename(temp, this.file);
|
|
await fs.promises.chmod(this.file, 0o600);
|
|
}
|
|
}
|
|
|
|
module.exports = { Store, emptyData };
|