mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 19:01:02 +01:00
358 lines
8.9 KiB
TypeScript
358 lines
8.9 KiB
TypeScript
/**
|
|
* PCD Manager - High-level orchestration for PCD lifecycle
|
|
*/
|
|
|
|
import { Git, clone, type GitStatus, type UpdateInfo } from '$utils/git/index.ts';
|
|
import { databaseInstancesQueries } from '$db/queries/databaseInstances.ts';
|
|
import type { DatabaseInstance } from '$db/queries/databaseInstances.ts';
|
|
import { loadManifest, type Manifest } from './manifest.ts';
|
|
import { getPCDPath } from './paths.ts';
|
|
import { processDependencies, syncDependencies } from './deps.ts';
|
|
import { notify } from '$notifications/builder.ts';
|
|
import { NotificationTypes } from '$notifications/types.ts';
|
|
import { compile, invalidate, startWatch, getCache } from './cache.ts';
|
|
import { logger } from '$logger/logger.ts';
|
|
|
|
export interface LinkOptions {
|
|
repositoryUrl: string;
|
|
name: string;
|
|
branch?: string;
|
|
syncStrategy?: number;
|
|
autoPull?: boolean;
|
|
personalAccessToken?: string;
|
|
}
|
|
|
|
export interface SyncResult {
|
|
success: boolean;
|
|
commitsBehind: number;
|
|
error?: string;
|
|
}
|
|
|
|
/**
|
|
* PCD Manager - Manages the lifecycle of Profilarr Compliant Databases
|
|
*/
|
|
class PCDManager {
|
|
/**
|
|
* Link a new PCD repository
|
|
*/
|
|
async link(options: LinkOptions): Promise<DatabaseInstance> {
|
|
await logger.debug('Starting database link operation', {
|
|
source: 'PCDManager',
|
|
meta: {
|
|
name: options.name,
|
|
repositoryUrl: options.repositoryUrl,
|
|
branch: options.branch
|
|
}
|
|
});
|
|
|
|
// Generate UUID for storage
|
|
const uuid = crypto.randomUUID();
|
|
const localPath = getPCDPath(uuid);
|
|
|
|
try {
|
|
// Clone the repository and detect if it's private
|
|
const isPrivate = await clone(options.repositoryUrl, localPath, options.branch, options.personalAccessToken);
|
|
|
|
// Validate manifest (loadManifest throws if invalid)
|
|
await loadManifest(localPath);
|
|
|
|
// Process dependencies (clone and validate)
|
|
await processDependencies(localPath);
|
|
|
|
// Insert into database
|
|
const id = databaseInstancesQueries.create({
|
|
uuid,
|
|
name: options.name,
|
|
repositoryUrl: options.repositoryUrl,
|
|
localPath,
|
|
syncStrategy: options.syncStrategy,
|
|
autoPull: options.autoPull,
|
|
personalAccessToken: options.personalAccessToken,
|
|
isPrivate
|
|
});
|
|
|
|
// Get and return the created instance
|
|
const instance = databaseInstancesQueries.getById(id);
|
|
if (!instance) {
|
|
throw new Error('Failed to retrieve created database instance');
|
|
}
|
|
|
|
// Compile cache and start watching (only if enabled)
|
|
if (instance.enabled) {
|
|
try {
|
|
await compile(localPath, id);
|
|
await startWatch(localPath, id);
|
|
} catch (error) {
|
|
// Log error but don't fail the link operation
|
|
await logger.error('Failed to compile PCD cache after linking', {
|
|
source: 'PCDManager',
|
|
meta: { error: String(error), databaseId: id }
|
|
});
|
|
}
|
|
}
|
|
|
|
await notify(NotificationTypes.PCD_LINKED)
|
|
.title('Database Linked')
|
|
.message(`Database "${options.name}" has been linked successfully`)
|
|
.meta({ databaseId: id, databaseName: options.name, repositoryUrl: options.repositoryUrl })
|
|
.send();
|
|
|
|
return instance;
|
|
} catch (error) {
|
|
// Cleanup on failure - remove cloned directory
|
|
try {
|
|
await Deno.remove(localPath, { recursive: true });
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unlink a PCD repository
|
|
*/
|
|
async unlink(id: number): Promise<void> {
|
|
const instance = databaseInstancesQueries.getById(id);
|
|
if (!instance) {
|
|
throw new Error(`Database instance ${id} not found`);
|
|
}
|
|
|
|
// Store name and URL for notification
|
|
const { name, repository_url } = instance;
|
|
|
|
// Invalidate cache first
|
|
invalidate(id);
|
|
|
|
// Delete from database
|
|
databaseInstancesQueries.delete(id);
|
|
|
|
// Then cleanup filesystem
|
|
try {
|
|
await Deno.remove(instance.local_path, { recursive: true });
|
|
} catch (error) {
|
|
// Log but don't throw - database entry is already deleted
|
|
console.error(`Failed to remove PCD directory ${instance.local_path}:`, error);
|
|
}
|
|
|
|
await notify(NotificationTypes.PCD_UNLINKED)
|
|
.title('Database Unlinked')
|
|
.message(`Database "${name}" has been removed`)
|
|
.meta({ databaseId: id, databaseName: name, repositoryUrl: repository_url })
|
|
.send();
|
|
}
|
|
|
|
/**
|
|
* Sync a PCD repository (pull updates)
|
|
*/
|
|
async sync(id: number): Promise<SyncResult> {
|
|
const instance = databaseInstancesQueries.getById(id);
|
|
if (!instance) {
|
|
throw new Error(`Database instance ${id} not found`);
|
|
}
|
|
|
|
const git = new Git(instance.local_path);
|
|
|
|
try {
|
|
// Check for updates first
|
|
const updateInfo = await git.checkForUpdates();
|
|
|
|
if (!updateInfo.hasUpdates) {
|
|
// Already up to date
|
|
databaseInstancesQueries.updateSyncedAt(id);
|
|
return {
|
|
success: true,
|
|
commitsBehind: 0
|
|
};
|
|
}
|
|
|
|
// Pull updates
|
|
await git.pull();
|
|
|
|
// Sync dependencies (schema, etc.) if versions changed
|
|
await syncDependencies(instance.local_path);
|
|
|
|
// Update last_synced_at
|
|
databaseInstancesQueries.updateSyncedAt(id);
|
|
|
|
// Recompile cache and restart watching (only if enabled)
|
|
if (instance.enabled) {
|
|
try {
|
|
await compile(instance.local_path, id);
|
|
await startWatch(instance.local_path, id);
|
|
} catch (error) {
|
|
await logger.error('Failed to recompile PCD cache after sync', {
|
|
source: 'PCDManager',
|
|
meta: { error: String(error), databaseId: id }
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
commitsBehind: updateInfo.commitsBehind
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
commitsBehind: 0,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check for available updates without pulling
|
|
*/
|
|
async checkForUpdates(id: number): Promise<UpdateInfo> {
|
|
const instance = databaseInstancesQueries.getById(id);
|
|
if (!instance) {
|
|
throw new Error(`Database instance ${id} not found`);
|
|
}
|
|
|
|
const git = new Git(instance.local_path);
|
|
return await git.checkForUpdates();
|
|
}
|
|
|
|
/**
|
|
* Get parsed manifest for a PCD
|
|
*/
|
|
async getManifest(id: number): Promise<Manifest> {
|
|
const instance = databaseInstancesQueries.getById(id);
|
|
if (!instance) {
|
|
throw new Error(`Database instance ${id} not found`);
|
|
}
|
|
|
|
return await loadManifest(instance.local_path);
|
|
}
|
|
|
|
/**
|
|
* Switch branch for a PCD
|
|
*/
|
|
async switchBranch(id: number, branch: string): Promise<void> {
|
|
const instance = databaseInstancesQueries.getById(id);
|
|
if (!instance) {
|
|
throw new Error(`Database instance ${id} not found`);
|
|
}
|
|
|
|
const git = new Git(instance.local_path);
|
|
await git.checkout(branch);
|
|
await git.pull();
|
|
databaseInstancesQueries.updateSyncedAt(id);
|
|
}
|
|
|
|
/**
|
|
* Get git status for a PCD
|
|
*/
|
|
async getStatus(id: number): Promise<GitStatus> {
|
|
const instance = databaseInstancesQueries.getById(id);
|
|
if (!instance) {
|
|
throw new Error(`Database instance ${id} not found`);
|
|
}
|
|
|
|
const git = new Git(instance.local_path);
|
|
return await git.status();
|
|
}
|
|
|
|
/**
|
|
* Get all PCDs
|
|
*/
|
|
getAll(): DatabaseInstance[] {
|
|
return databaseInstancesQueries.getAll();
|
|
}
|
|
|
|
/**
|
|
* Get PCD by ID
|
|
*/
|
|
getById(id: number): DatabaseInstance | undefined {
|
|
return databaseInstancesQueries.getById(id);
|
|
}
|
|
|
|
/**
|
|
* Get PCDs that need auto-sync
|
|
*/
|
|
getDueForSync(): DatabaseInstance[] {
|
|
return databaseInstancesQueries.getDueForSync();
|
|
}
|
|
|
|
/**
|
|
* Initialize PCD caches for all enabled databases
|
|
* Should be called on application startup
|
|
*/
|
|
async initialize(): Promise<void> {
|
|
const startTime = performance.now();
|
|
|
|
await logger.debug('Initialize caches', { source: 'PCDManager' });
|
|
|
|
const instances = databaseInstancesQueries.getAll();
|
|
const enabledInstances = instances.filter((instance) => instance.enabled);
|
|
|
|
// Collect results for aggregate logging
|
|
const results: Array<{
|
|
name: string;
|
|
schema: number;
|
|
base: number;
|
|
tweaks: number;
|
|
user: number;
|
|
error?: string;
|
|
}> = [];
|
|
|
|
// Compile and watch all enabled instances
|
|
for (const instance of enabledInstances) {
|
|
try {
|
|
const stats = await compile(instance.local_path, instance.id);
|
|
await startWatch(instance.local_path, instance.id);
|
|
|
|
results.push({
|
|
name: instance.name,
|
|
schema: stats.schema,
|
|
base: stats.base,
|
|
tweaks: stats.tweaks,
|
|
user: stats.user
|
|
});
|
|
} catch (error) {
|
|
results.push({
|
|
name: instance.name,
|
|
schema: 0,
|
|
base: 0,
|
|
tweaks: 0,
|
|
user: 0,
|
|
error: String(error)
|
|
});
|
|
|
|
await logger.error(`Cache failed "${instance.name}"`, {
|
|
source: 'PCDManager',
|
|
meta: { error: String(error), databaseId: instance.id }
|
|
});
|
|
}
|
|
}
|
|
|
|
const timing = Math.round(performance.now() - startTime);
|
|
const successful = results.filter((r) => !r.error);
|
|
|
|
await logger.info('Caches ready', {
|
|
source: 'PCDManager',
|
|
meta: {
|
|
databases: successful.map((r) => ({
|
|
name: r.name,
|
|
schema: r.schema,
|
|
base: r.base,
|
|
tweaks: r.tweaks,
|
|
user: r.user
|
|
})),
|
|
timing: `${timing}ms`
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the cache for a database instance
|
|
*/
|
|
getCache(id: number) {
|
|
return getCache(id);
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const pcdManager = new PCDManager();
|