import fs from 'fs' import path from 'path' import { app } from 'electron' export interface MySqlVersion { id: string version: string cycle: string url: string status: 'available' | 'downloading' | 'installed' progress: number description?: string } export class MySqlManagerService { private versions: MySqlVersion[] = [] private binDir: string private catalogPath: string constructor() { this.binDir = path.join(app.getPath('userData'), 'bin') // In development, it's in src/main/resources, but we should handle both this.catalogPath = path.join(app.getAppPath(), 'src/main/resources/mysql_catalog.json') if (!fs.existsSync(this.catalogPath)) { // Fallback for production or different root this.catalogPath = path.join(process.cwd(), 'src/main/resources/mysql_catalog.json') } this.init() } private async init() { if (!fs.existsSync(this.binDir)) { fs.mkdirSync(this.binDir, { recursive: true }) } } async fetchAvailableVersions(): Promise { console.log('Loading MySQL versions from local catalog:', this.catalogPath) try { if (!fs.existsSync(this.catalogPath)) { console.error('MySQL Catalog not found at:', this.catalogPath) return this.versions } const rawData = fs.readFileSync(this.catalogPath, 'utf8') const catalog = JSON.parse(rawData) const releases = catalog.mysql_release_catalog.releases as any[] const dynamicVersions: MySqlVersion[] = releases.map(v => { const version = v.version const cycle = v.series // Pattern: https://downloads.mysql.com/archives/get/p/23/file/mysql-VERSION-winx64.zip const url = `https://downloads.mysql.com/archives/get/p/23/file/mysql-${version}-winx64.zip` return { id: `mysql-${version}`, version, cycle, url, status: 'available', progress: 0, description: v.status } }) this.updateVersionsList(dynamicVersions) return this.versions } catch (e: any) { console.error('Failed to load MySQL catalog:', e.message) return this.versions } } private updateVersionsList(newList: MySqlVersion[]) { this.versions = newList.map(dv => { const current = this.versions.find(cv => cv.id === dv.id) if (current && current.status === 'downloading') return current return dv }).sort((a, b) => b.version.localeCompare(a.version, undefined, { numeric: true, sensitivity: 'base' })) this.refreshInstalledStatus() } private refreshInstalledStatus() { this.versions = this.versions.map(v => { const targetDir = path.join(this.binDir, v.id) const possiblePaths = [ path.join(targetDir, 'bin', 'mysqld.exe'), path.join(targetDir, `mysql-${v.version}-winx64`, 'bin', 'mysqld.exe') ] const isInstalled = possiblePaths.some(p => fs.existsSync(p)) if (isInstalled) { return { ...v, status: 'installed', progress: 100 } } return v }) } async getVersions(): Promise { if (this.versions.length === 0) { await this.fetchAvailableVersions() } else { this.refreshInstalledStatus() } return this.versions } updateProgress(id: string, progress: number, status: MySqlVersion['status'] = 'downloading') { const version = this.versions.find(v => v.id === id) if (version) { version.progress = progress version.status = status } } } export const mySqlManagerService = new MySqlManagerService()