feat(storage): add StorageManager and cache subsystem; integrate storage into ConnectionManager and GitopsApp, migrate legacy connections, and add tests

This commit is contained in:
2026-02-24 15:22:56 +00:00
parent e8e45d5371
commit 43321c35d6
19 changed files with 706 additions and 25 deletions

View File

@@ -2,41 +2,74 @@ import * as plugins from '../plugins.ts';
import { logger } from '../logging.ts';
import type * as interfaces from '../../ts_interfaces/index.ts';
import { BaseProvider, GiteaProvider, GitLabProvider } from '../providers/index.ts';
import type { StorageManager } from '../storage/index.ts';
const CONNECTIONS_FILE = './.nogit/connections.json';
const LEGACY_CONNECTIONS_FILE = './.nogit/connections.json';
const CONNECTIONS_PREFIX = '/connections/';
/**
* Manages provider connections - persists to .nogit/connections.json
* and creates provider instances on demand.
* Manages provider connections persists each connection as an
* individual JSON file via StorageManager.
*/
export class ConnectionManager {
private connections: interfaces.data.IProviderConnection[] = [];
private storageManager: StorageManager;
constructor(storageManager: StorageManager) {
this.storageManager = storageManager;
}
async init(): Promise<void> {
await this.migrateLegacyFile();
await this.loadConnections();
}
private async loadConnections(): Promise<void> {
/**
* One-time migration from the legacy .nogit/connections.json file.
*/
private async migrateLegacyFile(): Promise<void> {
try {
const text = await Deno.readTextFile(CONNECTIONS_FILE);
this.connections = JSON.parse(text);
logger.info(`Loaded ${this.connections.length} connection(s)`);
const text = await Deno.readTextFile(LEGACY_CONNECTIONS_FILE);
const legacy: interfaces.data.IProviderConnection[] = JSON.parse(text);
if (legacy.length > 0) {
logger.info(`Migrating ${legacy.length} connection(s) from legacy file...`);
for (const conn of legacy) {
await this.storageManager.setJSON(`${CONNECTIONS_PREFIX}${conn.id}.json`, conn);
}
// Rename legacy file so migration doesn't repeat
await Deno.rename(LEGACY_CONNECTIONS_FILE, LEGACY_CONNECTIONS_FILE + '.migrated');
logger.success('Legacy connections migrated successfully');
}
} catch {
this.connections = [];
logger.debug('No existing connections file found, starting fresh');
// No legacy file or already migrated — nothing to do
}
}
private async saveConnections(): Promise<void> {
// Ensure .nogit directory exists
try {
await Deno.mkdir('./.nogit', { recursive: true });
} catch { /* already exists */ }
await Deno.writeTextFile(CONNECTIONS_FILE, JSON.stringify(this.connections, null, 2));
private async loadConnections(): Promise<void> {
const keys = await this.storageManager.list(CONNECTIONS_PREFIX);
this.connections = [];
for (const key of keys) {
const conn = await this.storageManager.getJSON<interfaces.data.IProviderConnection>(key);
if (conn) {
this.connections.push(conn);
}
}
if (this.connections.length > 0) {
logger.info(`Loaded ${this.connections.length} connection(s)`);
} else {
logger.debug('No existing connections found, starting fresh');
}
}
private async persistConnection(conn: interfaces.data.IProviderConnection): Promise<void> {
await this.storageManager.setJSON(`${CONNECTIONS_PREFIX}${conn.id}.json`, conn);
}
private async removeConnection(id: string): Promise<void> {
await this.storageManager.delete(`${CONNECTIONS_PREFIX}${id}.json`);
}
getConnections(): interfaces.data.IProviderConnection[] {
// Return connections without exposing tokens
return this.connections.map((c) => ({ ...c, token: '***' }));
}
@@ -60,7 +93,7 @@ export class ConnectionManager {
status: 'disconnected',
};
this.connections.push(connection);
await this.saveConnections();
await this.persistConnection(connection);
logger.success(`Connection created: ${name} (${providerType})`);
return { ...connection, token: '***' };
}
@@ -74,7 +107,7 @@ export class ConnectionManager {
if (updates.name) conn.name = updates.name;
if (updates.baseUrl) conn.baseUrl = updates.baseUrl.replace(/\/+$/, '');
if (updates.token) conn.token = updates.token;
await this.saveConnections();
await this.persistConnection(conn);
return { ...conn, token: '***' };
}
@@ -82,7 +115,7 @@ export class ConnectionManager {
const idx = this.connections.findIndex((c) => c.id === id);
if (idx === -1) throw new Error(`Connection not found: ${id}`);
this.connections.splice(idx, 1);
await this.saveConnections();
await this.removeConnection(id);
logger.info(`Connection deleted: ${id}`);
}
@@ -91,7 +124,7 @@ export class ConnectionManager {
const result = await provider.testConnection();
const conn = this.connections.find((c) => c.id === id)!;
conn.status = result.ok ? 'connected' : 'error';
await this.saveConnections();
await this.persistConnection(conn);
return result;
}

View File

@@ -1,25 +1,50 @@
import { logger } from '../logging.ts';
import { ConnectionManager } from './connectionmanager.ts';
import { OpsServer } from '../opsserver/index.ts';
import { StorageManager } from '../storage/index.ts';
import { CacheDb, CacheCleaner, CachedProject } from '../cache/index.ts';
import { resolvePaths } from '../paths.ts';
/**
* Main GitOps application orchestrator
*/
export class GitopsApp {
public storageManager: StorageManager;
public connectionManager: ConnectionManager;
public opsServer: OpsServer;
public cacheDb: CacheDb;
public cacheCleaner: CacheCleaner;
constructor() {
this.connectionManager = new ConnectionManager();
const paths = resolvePaths();
this.storageManager = new StorageManager({
backend: 'filesystem',
fsPath: paths.defaultStoragePath,
});
this.connectionManager = new ConnectionManager(this.storageManager);
this.cacheDb = CacheDb.getInstance({
storagePath: paths.defaultTsmDbPath,
dbName: 'gitops_cache',
});
this.cacheCleaner = new CacheCleaner(this.cacheDb);
this.cacheCleaner.registerClass(CachedProject);
this.opsServer = new OpsServer(this);
}
async start(port = 3000): Promise<void> {
logger.info('Initializing GitOps...');
// Start CacheDb
await this.cacheDb.start();
// Initialize connection manager (loads saved connections)
await this.connectionManager.init();
// Start CacheCleaner
this.cacheCleaner.start();
// Start OpsServer
await this.opsServer.start(port);
@@ -29,6 +54,8 @@ export class GitopsApp {
async stop(): Promise<void> {
logger.info('Shutting down GitOps...');
await this.opsServer.stop();
this.cacheCleaner.stop();
await this.cacheDb.stop();
logger.success('GitOps shutdown complete');
}
}