| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import 'server-only'; |
|
|
| import Database from 'better-sqlite3'; |
| import path from 'path'; |
| import fs from 'fs'; |
| import { |
| getSystemDatabase, |
| createWorkspace, |
| setDefaultWorkspace, |
| getUserDefaultWorkspace, |
| getUserById, |
| updateWorkspace, |
| getWorkspaceById, |
| getDeploymentBySlug, |
| } from './system-database'; |
| import { generateUniqueSlug } from '@/lib/publishing/slug-generator'; |
| import { logger } from '@/lib/utils'; |
|
|
| function openReadonlyDb(dbPath: string): Database.Database { |
| const db = new Database(dbPath, { readonly: true }); |
| const key = process.env.DB_ENCRYPTION_KEY; |
| if (key) db.pragma(`key='${key}'`); |
| return db; |
| } |
|
|
| const DEFAULT_WORKSPACE_NAME = 'Local Workspace'; |
|
|
| function getDataDir(): string { |
| return process.env.DATA_DIR || path.join(process.cwd(), 'data'); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function ensureDefaultWorkspace(userId: string): Promise<string> { |
| |
| |
| const isBalancerManaged = !!process.env.WEBHOOK_URL; |
|
|
| |
| |
| |
| const existing = getUserDefaultWorkspace(userId); |
| if (existing && getWorkspaceById(existing)) { |
| if (!isBalancerManaged) { |
| migrateLegacyData(existing); |
| } |
| return existing; |
| } |
|
|
| |
| const user = getUserById(userId); |
| if (!user) { |
| const { randomBytes } = await import('crypto'); |
| |
| |
| |
| const hash = `nologin:${randomBytes(32).toString('hex')}`; |
| const db = getSystemDatabase(); |
| db.prepare(` |
| INSERT OR IGNORE INTO users (id, email, password_hash, display_name, is_admin, active) |
| VALUES (?, ?, ?, ?, 1, 1) |
| `).run(userId, `${userId}@localhost`, hash, userId === 'desktop' ? 'Desktop' : 'Admin'); |
| } |
|
|
| |
| const workspaceId = createWorkspace(DEFAULT_WORKSPACE_NAME, userId); |
| updateWorkspace(workspaceId, { |
| max_projects: 9999, |
| max_deployments: 9999, |
| max_storage_mb: 99999, |
| }); |
| setDefaultWorkspace(userId, workspaceId); |
|
|
| |
| if (!isBalancerManaged) { |
| migrateLegacyData(workspaceId); |
| } |
|
|
| return workspaceId; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| function migrateLegacyData(workspaceId: string): void { |
| const dataDir = getDataDir(); |
| const legacyDbPath = path.join(dataDir, 'osws.sqlite'); |
| const workspaceDir = path.join(dataDir, 'workspaces', workspaceId); |
| const workspaceDbPath = path.join(workspaceDir, 'osws.sqlite'); |
|
|
| if (!fs.existsSync(legacyDbPath)) return; |
|
|
| |
| let legacyProjectCount = 0; |
| try { |
| const db = openReadonlyDb(legacyDbPath); |
| const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='projects'").get(); |
| if (row) { |
| legacyProjectCount = (db.prepare('SELECT COUNT(*) as c FROM projects').get() as { c: number }).c; |
| } |
| db.close(); |
| } catch { return; } |
|
|
| if (legacyProjectCount === 0) return; |
|
|
| |
| if (fs.existsSync(workspaceDbPath)) { |
| try { |
| const db = openReadonlyDb(workspaceDbPath); |
| const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='projects'").get(); |
| if (row) { |
| const count = (db.prepare('SELECT COUNT(*) as c FROM projects').get() as { c: number }).c; |
| db.close(); |
| if (count > 0) return; |
| } else { |
| db.close(); |
| } |
| } catch { } |
| } |
|
|
| try { |
| fs.mkdirSync(workspaceDir, { recursive: true }); |
|
|
| |
| fs.copyFileSync(legacyDbPath, workspaceDbPath); |
|
|
| |
| for (const ext of ['-wal', '-shm']) { |
| const walPath = legacyDbPath + ext; |
| if (fs.existsSync(walPath)) { |
| fs.copyFileSync(walPath, workspaceDbPath + ext); |
| } |
| } |
|
|
| |
| const legacyProjectsDir = path.join(dataDir, 'projects'); |
| if (fs.existsSync(legacyProjectsDir)) { |
| const workspaceProjectsDir = path.join(workspaceDir, 'projects'); |
| copyDirRecursive(legacyProjectsDir, workspaceProjectsDir); |
| } |
| } catch (err) { |
| logger.error('[DefaultWorkspace] Failed to migrate legacy data:', err); |
| } |
| } |
|
|
| function copyDirRecursive(src: string, dest: string): void { |
| if (!fs.existsSync(src)) return; |
| fs.mkdirSync(dest, { recursive: true }); |
|
|
| const entries = fs.readdirSync(src, { withFileTypes: true }); |
| for (const entry of entries) { |
| const srcPath = path.join(src, entry.name); |
| const destPath = path.join(dest, entry.name); |
| if (entry.isDirectory()) { |
| copyDirRecursive(srcPath, destPath); |
| } else { |
| fs.copyFileSync(srcPath, destPath); |
| } |
| } |
| } |
|
|
| |
| |
| |
|
|
| export interface RepairResult { |
| legacyDbMigrated: boolean; |
| legacyProjectsMigrated: number; |
| deploymentRoutesCreated: number; |
| errors: string[]; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function repairWorkspace(workspaceId: string): RepairResult { |
| const dataDir = getDataDir(); |
| const workspaceDir = path.join(dataDir, 'workspaces', workspaceId); |
| const workspaceDbPath = path.join(workspaceDir, 'osws.sqlite'); |
| const legacyDbPath = path.join(dataDir, 'osws.sqlite'); |
| const result: RepairResult = { |
| legacyDbMigrated: false, |
| legacyProjectsMigrated: 0, |
| deploymentRoutesCreated: 0, |
| errors: [], |
| }; |
|
|
| |
| if (fs.existsSync(legacyDbPath)) { |
| const legacyHasData = (() => { |
| try { |
| const db = openReadonlyDb(legacyDbPath); |
| const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='projects'").get(); |
| if (!tableExists) { db.close(); return false; } |
| const count = (db.prepare('SELECT COUNT(*) as count FROM projects').get() as { count: number }).count; |
| db.close(); |
| return count > 0; |
| } catch { return false; } |
| })(); |
|
|
| if (legacyHasData) { |
| const workspaceHasData = (() => { |
| if (!fs.existsSync(workspaceDbPath)) return false; |
| try { |
| const db = openReadonlyDb(workspaceDbPath); |
| const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='projects'").get(); |
| if (!tableExists) { db.close(); return false; } |
| const count = (db.prepare('SELECT COUNT(*) as count FROM projects').get() as { count: number }).count; |
| db.close(); |
| return count > 0; |
| } catch { return false; } |
| })(); |
|
|
| if (!workspaceHasData) { |
| try { |
| fs.mkdirSync(workspaceDir, { recursive: true }); |
| fs.copyFileSync(legacyDbPath, workspaceDbPath); |
| for (const ext of ['-wal', '-shm']) { |
| const walPath = legacyDbPath + ext; |
| if (fs.existsSync(walPath)) { |
| fs.copyFileSync(walPath, workspaceDbPath + ext); |
| } |
| } |
| result.legacyDbMigrated = true; |
| } catch (err) { |
| result.errors.push(`Failed to copy legacy DB: ${err}`); |
| } |
| } |
| } |
| } |
|
|
| |
| const legacyProjectsDir = path.join(dataDir, 'projects'); |
| const workspaceProjectsDir = path.join(workspaceDir, 'projects'); |
| if (fs.existsSync(legacyProjectsDir) && fs.existsSync(workspaceDbPath)) { |
| try { |
| |
| const db = openReadonlyDb(workspaceDbPath); |
| const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='projects'").get(); |
| const projectIds: string[] = []; |
| if (tableExists) { |
| const rows = db.prepare('SELECT id FROM projects').all() as { id: string }[]; |
| projectIds.push(...rows.map(r => r.id)); |
| } |
| db.close(); |
|
|
| |
| for (const projectId of projectIds) { |
| const legacyProjectDir = path.join(legacyProjectsDir, projectId); |
| const workspaceProjectDir = path.join(workspaceProjectsDir, projectId); |
| if (fs.existsSync(legacyProjectDir) && !fs.existsSync(workspaceProjectDir)) { |
| copyDirRecursive(legacyProjectDir, workspaceProjectDir); |
| result.legacyProjectsMigrated++; |
| } |
| } |
| } catch (err) { |
| result.errors.push(`Failed to migrate project databases: ${err}`); |
| } |
| } |
|
|
| |
| if (fs.existsSync(workspaceDbPath)) { |
| try { |
| const db = openReadonlyDb(workspaceDbPath); |
| const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='deployments'").get(); |
| if (tableExists) { |
| const deployments = db.prepare('SELECT id, slug FROM deployments').all() as { id: string; slug: string | null }[]; |
| db.close(); |
|
|
| const sysDb = getSystemDatabase(); |
| for (const deployment of deployments) { |
| const existing = sysDb.prepare('SELECT deployment_id FROM deployment_routing WHERE deployment_id = ?') |
| .get(deployment.id); |
| if (!existing) { |
| |
| |
| |
| const slug = deployment.slug || generateUniqueSlug(s => !!getDeploymentBySlug(s)); |
| sysDb.prepare(` |
| INSERT OR IGNORE INTO deployment_routing (deployment_id, workspace_id, slug) |
| VALUES (?, ?, ?) |
| `).run(deployment.id, workspaceId, slug); |
| result.deploymentRoutesCreated++; |
| } |
| } |
| } else { |
| db.close(); |
| } |
| } catch (err) { |
| result.errors.push(`Failed to repair deployment routes: ${err}`); |
| } |
| } |
|
|
| return result; |
| } |
|
|