feat(storage): add StorageManager and cache subsystem; integrate storage into ConnectionManager and GitopsApp, migrate legacy connections, and add tests
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user