Migrate Harbor state and traffic history to SQLite
Build and Deploy Gateway / build-and-push (push) Successful in 1m22s
Build and Deploy Gateway / deploy (push) Successful in 16s

This commit is contained in:
2026-09-10 19:21:21 +03:00
parent ab14fc979e
commit 1ae23d848b
48 changed files with 1703 additions and 446 deletions
+19 -27
View File
@@ -5,6 +5,7 @@ import http from 'node:http';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { DatabaseSync } from 'node:sqlite';
import { buildGatewayPresence } from '../../dist/server/gatewayPresence.js';
import { normalizeSubscriptionConfig } from '../../dist/server/subscription.js';
@@ -151,6 +152,7 @@ async function startClientFixture(t, {
hostNetwork,
gatewayPresencePort,
trafficSource,
expectMigrationError,
}) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-startup-recovery-'));
const { binDirectory, markerPath } = fakeSingbox(directory);
@@ -188,6 +190,15 @@ async function startClientFixture(t, {
await stopChild(child);
fs.rmSync(directory, { recursive: true, force: true });
});
if (expectMigrationError) {
await assert.rejects(waitForState(port, child, () => stderr), expectMigrationError);
assert.notEqual(child.exitCode, 0);
assert.equal(fs.existsSync(markerPath), false);
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(directory, 'state.json'), 'utf8')), state);
assert.equal(fs.readFileSync(path.join(directory, 'subscription-cache.json'), 'utf8'), cacheContents);
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(directory, 'sing-box-config.json'), 'utf8')), config);
return { directory, markerPath };
}
return {
directory,
markerPath,
@@ -257,7 +268,9 @@ test('gateway-direct boot keeps the local proxy and diagnostics but omits every
},
});
const config = JSON.parse(fs.readFileSync(path.join(fixture.directory, 'sing-box-config.json'), 'utf8'));
const stored = JSON.parse(fs.readFileSync(path.join(fixture.directory, 'state.json'), 'utf8'));
const db = new DatabaseSync(path.join(fixture.directory, 'harbor.sqlite'), { readOnly: true });
const stored = JSON.parse(db.prepare("SELECT value FROM documents WHERE key = 'state'").get().value);
db.close();
assert.equal(fixture.state.route.mode, 'gateway-direct');
assert.deepEqual(fixture.state.route.activeLocalRules, []);
@@ -279,23 +292,15 @@ test('corrupt legacy cache with no canonical subscription fails closed instead o
port: 443,
protocol: 'vless',
};
const fixture = await startClientFixture(t, {
await startClientFixture(t, {
state: { schemaVersion: 4, revision: 2, connectionDesired: 'running' },
cacheContents: '{broken',
config: generatedConfig(staleServer),
expectMigrationError: /Cannot migrate subscription-cache.json/,
});
assert.equal(fixture.state.subscription.status, 'missing');
assert.deepEqual(fixture.state.profiles, []);
assert.equal(fixture.state.connection.desired, 'stopped');
assert.equal(fixture.state.connection.process, 'stopped');
assert.equal(fs.existsSync(fixture.markerPath), false);
assert.ok(fs.readdirSync(fixture.directory).some((name) => (
name.startsWith('subscription-cache.json.corrupt-')
)));
});
test('legacy cache owned by another URL is backed up without mixing providers and boots stopped', async (t) => {
test('legacy cache owned by another URL aborts migration without mixing providers or changing originals', async (t) => {
const stateServer = {
id: 'server-a',
label: 'State server',
@@ -314,7 +319,8 @@ test('legacy cache owned by another URL is backed up without mixing providers an
server: 'cache.example',
server_port: 8443,
};
const fixture = await startClientFixture(t, {
await startClientFixture(t, {
expectMigrationError: /owner mismatch/,
state: {
schemaVersion: 4,
revision: 2,
@@ -331,20 +337,6 @@ test('legacy cache owned by another URL is backed up without mixing providers an
}),
config: generatedConfig(stateServer),
});
assert.equal(fixture.state.profiles.length, 1);
assert.equal(fixture.state.profiles[0].subscription.host, 'state.example/…');
assert.deepEqual(fixture.state.profiles[0].servers.map(({ id }) => id), [stateServer.id]);
assert.equal(JSON.stringify(fixture.state).includes('cache.example'), false);
assert.equal(fixture.state.connection.desired, 'stopped');
assert.equal(fixture.state.connection.process, 'stopped');
assert.equal(fixture.state.selection.appliedServerId, '');
assert.equal(fs.existsSync(path.join(fixture.directory, 'subscription-cache.json')), false);
assert.ok(fs.readdirSync(fixture.directory).some((name) => (
name.startsWith('subscription-cache.json.backup-v1-')
)));
assert.equal(fs.existsSync(path.join(fixture.directory, 'sing-box-config.json')), false);
assert.equal(fs.existsSync(fixture.markerPath), false);
});
test('boot rejects an existing config whose route mode disagrees with the current route', async (t) => {