From 430476416d6946f67bd127b729e9040de9551f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Sat, 28 Mar 2026 09:47:56 +0300 Subject: [PATCH] feat: implement PHP and MySQL version management services with automated download and cataloging capabilities --- package-lock.json | 165 ++++++++++++- package.json | 1 + src/main/ipc/index.ts | 38 ++- src/main/resources/mysql_catalog.json | 61 +++++ src/main/services/DownloadService.ts | 3 +- src/main/services/MySqlManagerService.ts | 119 +++++++++ src/main/services/PhpManagerService.ts | 77 ++++-- src/preload/index.ts | 6 +- src/renderer/src/App.tsx | 293 +++++++++++++++++------ test_dl.zip | 38 +++ test_dl_v2.js | 48 ++++ test_mysql_api.js | 37 +++ 12 files changed, 791 insertions(+), 95 deletions(-) create mode 100644 src/main/resources/mysql_catalog.json create mode 100644 src/main/services/MySqlManagerService.ts create mode 100644 test_dl.zip create mode 100644 test_dl_v2.js create mode 100644 test_mysql_api.js diff --git a/package-lock.json b/package-lock.json index cc729ab..2985967 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "@vitejs/plugin-react": "^4.3.1", "electron": "^31.7.7", "electron-builder": "^24.13.3", + "electron-reload": "^2.0.0-alpha.1", "electron-vite": "^2.3.0", "eslint": "^8.57.0", "eslint-plugin-react": "^7.34.3", @@ -2672,6 +2673,20 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/app-builder-bin": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz", @@ -3151,6 +3166,19 @@ "node": ">=6.0.0" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -3199,6 +3227,19 @@ "balanced-match": "^1.0.0" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -3516,6 +3557,44 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -4552,6 +4631,16 @@ "node": ">= 10.0.0" } }, + "node_modules/electron-reload": { + "version": "2.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/electron-reload/-/electron-reload-2.0.0-alpha.1.tgz", + "integrity": "sha512-hTde7gv0TEqxbxlB3pj2CwoyCQ9sdiQrcP8GkpzhosxyVeYM3mZbMEVKCZK3L0fED7Mz5A9IWmK7zEvi4H3P1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.328", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.328.tgz", @@ -5295,6 +5384,19 @@ "minimatch": "^5.0.1" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -6194,6 +6296,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", @@ -6387,6 +6502,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-number-object": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", @@ -7190,7 +7315,6 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7518,6 +7642,19 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -7872,6 +8009,19 @@ "minimatch": "^5.1.0" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -8946,6 +9096,19 @@ "node": ">= 0.4" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", diff --git a/package.json b/package.json index 9760966..4d532b8 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@vitejs/plugin-react": "^4.3.1", "electron": "^31.7.7", "electron-builder": "^24.13.3", + "electron-reload": "^2.0.0-alpha.1", "electron-vite": "^2.3.0", "eslint": "^8.57.0", "eslint-plugin-react": "^7.34.3", diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 94a72d5..77fe0c4 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -1,9 +1,10 @@ -import { ipcMain, app } from 'electron' +import { ipcMain, app, dialog } from 'electron' import { processManager } from '../services/ProcessManager' import { configService } from '../services/ConfigService' import { downloadService } from '../services/DownloadService' import { projectService } from '../services/ProjectService' import { phpManagerService } from '../services/PhpManagerService' +import { mySqlManagerService } from '../services/MySqlManagerService' import path from 'path' export function registerIpcHandlers(): void { @@ -83,6 +84,41 @@ export function registerIpcHandlers(): void { } }) + // MySQL Versions + ipcMain.handle('mysql:versions', async () => { + return await mySqlManagerService.getVersions() + }) + + ipcMain.handle('mysql:download', async (event, id: string) => { + const versions = await mySqlManagerService.getVersions() + const version = versions.find(v => v.id === id) + if (!version) return { success: false, message: 'Geçersiz MySQL sürümü.' } + + try { + mySqlManagerService.updateProgress(id, 0, 'downloading') + event.sender.send('mysql:progress', { id, progress: 0 }) + + await downloadService.downloadAndExtract(version.url, id, (p) => { + mySqlManagerService.updateProgress(id, p) + event.sender.send('mysql:progress', { id, progress: p }) + }) + + mySqlManagerService.updateProgress(id, 100, 'installed') + return { success: true, message: 'MySQL indirme tamamlandı.' } + } catch (error: any) { + mySqlManagerService.updateProgress(id, 0, 'available') + return { success: false, message: `MySQL hatası: ${error.message}` } + } + }) + + ipcMain.handle('dialog:select-directory', async () => { + const result = await dialog.showOpenDialog({ + properties: ['openDirectory'] + }) + if (result.canceled) return null + return result.filePaths[0] + }) + ipcMain.handle('binaries:download', async (_event, { name, url }) => { return downloadService.downloadAndExtract(url, name, (p) => { // TODO: Emit progress to renderer via webContents.send diff --git a/src/main/resources/mysql_catalog.json b/src/main/resources/mysql_catalog.json new file mode 100644 index 0000000..34d2ad1 --- /dev/null +++ b/src/main/resources/mysql_catalog.json @@ -0,0 +1,61 @@ +{ + "mysql_release_catalog": { + "generated_date": "2026-03-28", + "provider": "Oracle MySQL", + "releases": [ + { + "series": "9.x (Innovation)", + "version": "9.2.0", + "status": "Latest Innovation", + "php_min_recommended": "PHP 8.2+", + "download_url": "https://dev.mysql.com/downloads/mysql/", + "is_archive": false + }, + { + "series": "8.4 (LTS)", + "version": "8.4.2", + "status": "Current LTS", + "php_min_recommended": "PHP 8.1+", + "download_url": "https://dev.mysql.com/downloads/mysql/8.4.html", + "is_archive": false + }, + { + "series": "8.0", + "version": "8.0.40", + "status": "End of Life (April 2026)", + "php_min_recommended": "PHP 7.4 - 8.3", + "download_url": "https://downloads.mysql.com/archives/community/?product=community&version=8.0.40", + "is_archive": true + }, + { + "series": "5.7", + "version": "5.7.44", + "status": "End of Life (2023)", + "php_min_recommended": "PHP 5.6 - 7.4", + "download_url": "https://downloads.mysql.com/archives/community/?product=community&version=5.7.44", + "is_archive": true + }, + { + "series": "5.6", + "version": "5.6.51", + "status": "Legacy / End of Life", + "php_min_recommended": "PHP 5.5 - 5.6", + "download_url": "https://downloads.mysql.com/archives/community/?product=community&version=5.6.51", + "is_archive": true + }, + { + "series": "5.5", + "version": "5.5.62", + "status": "Legacy / End of Life", + "php_min_recommended": "PHP 5.4 - 5.6", + "download_url": "https://downloads.mysql.com/archives/community/?product=community&version=5.5.62", + "is_archive": true + } + ], + "useful_links": { + "full_archive": "https://downloads.mysql.com/archives/community/", + "yum_repo": "https://dev.mysql.com/downloads/repo/yum/", + "apt_repo": "https://dev.mysql.com/downloads/repo/apt/" + } + } +} \ No newline at end of file diff --git a/src/main/services/DownloadService.ts b/src/main/services/DownloadService.ts index 390b779..81b3567 100644 --- a/src/main/services/DownloadService.ts +++ b/src/main/services/DownloadService.ts @@ -38,7 +38,8 @@ export class DownloadService { method: 'GET', responseType: 'stream', headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Referer': url.includes('mysql.com') ? 'https://dev.mysql.com/downloads/mysql/' : undefined } }) diff --git a/src/main/services/MySqlManagerService.ts b/src/main/services/MySqlManagerService.ts new file mode 100644 index 0000000..693b4e7 --- /dev/null +++ b/src/main/services/MySqlManagerService.ts @@ -0,0 +1,119 @@ +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() diff --git a/src/main/services/PhpManagerService.ts b/src/main/services/PhpManagerService.ts index 871a0b6..254bb49 100644 --- a/src/main/services/PhpManagerService.ts +++ b/src/main/services/PhpManagerService.ts @@ -15,6 +15,7 @@ export class PhpManagerService { private versions: PhpVersion[] = [] private binDir: string private winPhpUrl = 'https://windows.php.net/downloads/releases/' + private winArchiveUrl = 'https://windows.php.net/downloads/releases/archives/' private winReleasesJsonUrl = 'https://windows.php.net/downloads/releases/releases.json' private mainReleasesJsonUrl = 'https://www.php.net/releases/index.php?json' @@ -32,28 +33,29 @@ export class PhpManagerService { private async fetchAvailableVersions() { try { - // Fetch from windows.php.net for actual paths + // 1. Fetch from windows.php.net for current releases const winResponse = await axios.get(this.winReleasesJsonUrl, { headers: { 'User-Agent': 'Mozilla/5.0' } - }) + }).catch(() => ({ data: {} })) const winData = winResponse.data - // Fetch from php.net for supported versions list + // 2. Fetch from main php.net for the comprehensive version list const mainResponse = await axios.get(this.mainReleasesJsonUrl, { headers: { 'User-Agent': 'Mozilla/5.0' } }) const mainData = mainResponse.data - const dynamicVersions: PhpVersion[] = [] + const dynamicVersions: Map = new Map() - // Method 1: Versions explicitly available in windows releases.json - for (const majorMinor in winData) { - const release = winData[majorMinor] + // Strategy A: Direct matches from windows releases.json (current & stable) + for (const branch in winData) { + const release = winData[branch] const version = release.version - const tsBuild = release['ts-vs16-x64'] || release['ts-vs17-x64'] || release['ts-vc15-x64'] + // prioritize ts-vs16/17-x64 + const tsBuild = release['ts-vs16-x64'] || release['ts-vs17-x64'] || release['ts-vc15-x64'] || release['ts-vc14-x64'] if (tsBuild && tsBuild.zip) { - dynamicVersions.push({ + dynamicVersions.set(version, { id: `php-${version}`, version: version, url: this.winPhpUrl + tsBuild.zip.path, @@ -63,21 +65,56 @@ export class PhpManagerService { } } - // Method 2: Check supported_versions from main php.net - // This helps if windows releases.json doesn't group some newer versions yet - if (mainData['8'] && mainData['8'].supported_versions) { - for (const ver of mainData['8'].supported_versions) { - // ver is "8.2", "8.3" etc. - // If we already have a version for this branch, we are good. - // Otherwise, we could try to guess or find more. + // Strategy B: Legacy versions from main php.net JSON + for (const majorKey in mainData) { + const majorEntry = mainData[majorKey] + const latestVersion = majorEntry.version + + if (!dynamicVersions.has(latestVersion)) { + // Construct archive URL for older versions like 5.6.40, 7.4.33 etc if not in winData + // Pattern: php-VERSION-Win32-COMPILER-ARCH.zip + let arch = 'x64' + let compiler = 'vc15' + + if (latestVersion.startsWith('5.6')) compiler = 'vc11' + else if (latestVersion.startsWith('7.0') || latestVersion.startsWith('7.1')) compiler = 'vc14' + else if (latestVersion.startsWith('7.2') || latestVersion.startsWith('7.3') || latestVersion.startsWith('7.4')) compiler = 'vc15' + else compiler = 'vs16' + + const url = `${this.winArchiveUrl}php-${latestVersion}-Win32-${compiler}-${arch}.zip` + + // We add it, noting that older versions might have different naming conventions + // But for 5.6 and up, this pattern is generally consistent for x64 TS. + dynamicVersions.set(latestVersion, { + id: `php-${latestVersion}`, + version: latestVersion, + url: url, + status: 'available', + progress: 0 + }) + } + + // Also check for supported versions list (e.g. older 8.x) + if (majorEntry.supported_versions) { + for (const branch of majorEntry.supported_versions) { + // This JSON doesn't give specific patch versions for these branches, + // but winData usually covers the latest of each branch anyway. + } } } - // De-duplicate by version string - const uniqueVersionsMap = new Map() - dynamicVersions.forEach(v => uniqueVersionsMap.set(v.version, v)) + // De-duplicate and sort + this.versions = Array.from(dynamicVersions.values()).sort((a, b) => { + // Semi-numeric sort to handle versions correctly + const partsA = a.version.split('.').map(Number) + const partsB = b.version.split('.').map(Number) + for (let i = 0; i < 3; i++) { + if ((partsA[i] || 0) > (partsB[i] || 0)) return -1 + if ((partsA[i] || 0) < (partsB[i] || 0)) return 1 + } + return 0 + }) - this.versions = Array.from(uniqueVersionsMap.values()).sort((a, b) => b.version.localeCompare(a.version)) } catch (e) { console.error('Failed to fetch PHP versions:', e) } diff --git a/src/preload/index.ts b/src/preload/index.ts index 734b19c..eb3743e 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -13,7 +13,11 @@ const api = { removeProject: (id: string) => electronAPI.ipcRenderer.invoke('projects:remove', id), getPhpVersions: () => electronAPI.ipcRenderer.invoke('binaries:list-php'), downloadPhpVersion: (id: string) => electronAPI.ipcRenderer.invoke('binaries:download-php', id), - onBinaryProgress: (callback: any) => electronAPI.ipcRenderer.on('binaries:progress', (_event, data) => callback(data)) + onBinaryProgress: (callback: any) => electronAPI.ipcRenderer.on('binaries:progress', (_event, data) => callback(data)), + getMySqlVersions: () => electronAPI.ipcRenderer.invoke('mysql:versions'), + downloadMySqlVersion: (id: string) => electronAPI.ipcRenderer.invoke('mysql:download', id), + onMySqlProgress: (callback: any) => electronAPI.ipcRenderer.on('mysql:progress', (_event, data) => callback(data)), + selectDirectory: () => electronAPI.ipcRenderer.invoke('dialog:select-directory') } // Use `contextBridge` APIs to expose Electron APIs to diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 3c4d167..3d81fa5 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -29,7 +29,11 @@ import { MenuItem, FormControl, InputLabel, - LinearProgress + LinearProgress, + Tabs, + Tab, + InputAdornment, + CircularProgress } from '@mui/material' import { Dashboard as DashboardIcon, @@ -44,7 +48,9 @@ import { Delete as DeleteIcon, Download as DownloadIcon, CheckCircle as InstalledIcon, - CloudDownload as AvailableIcon + CloudDownload as AvailableIcon, + Refresh as RefreshIcon, + FolderOpen as FolderOpenIcon } from '@mui/icons-material' import { useTranslation } from 'react-i18next' @@ -70,10 +76,13 @@ function App(): JSX.Element { mysql: 'stopped' }) const [activeTab, setActiveTab] = useState('dashboard') + const [settingsTab, setSettingsTab] = useState(0) const [projects, setProjects] = useState([]) const [phpVersions, setPhpVersions] = useState([]) + const [mySqlVersions, setMySqlVersions] = useState([]) + const [loadingVersions, setLoadingVersions] = useState(false) const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false) - const [newProject, setNewProject] = useState({ name: '', path: '', phpVersion: '8.2', host: 'localhost' }) + const [newProject, setNewProject] = useState({ name: '', path: '', phpVersion: '', host: 'localhost' }) const [notification, setNotification] = useState<{ open: boolean, message: string, severity: 'success' | 'error' }>({ open: false, message: '', @@ -81,51 +90,96 @@ function App(): JSX.Element { }) const fetchStatus = async () => { - // @ts-ignore - const currentStatus = await window.api.getServiceStatus() - if (currentStatus) setStatus(currentStatus) + if (window.api && window.api.getServiceStatus) { + const currentStatus = await window.api.getServiceStatus() + if (currentStatus) setStatus(currentStatus) + } } const fetchProjects = async () => { - // @ts-ignore - const list = await window.api.getProjects() - setProjects(list || []) + if (window.api && window.api.getProjects) { + const list = await window.api.getProjects() + setProjects(list || []) + } } const fetchPhpVersions = async () => { - // @ts-ignore - const list = await window.api.getPhpVersions() - setPhpVersions(list || []) + if (window.api && window.api.getPhpVersions) { + const list = await window.api.getPhpVersions() + setPhpVersions(list || []) + } } + const fetchMySqlVersions = async () => { + if (window.api && window.api.getMySqlVersions) { + const list = await window.api.getMySqlVersions() + setMySqlVersions(list || []) + } + } + + const handleRefreshVersions = async () => { + setLoadingVersions(true) + try { + await Promise.all([fetchPhpVersions(), fetchMySqlVersions()]) + } finally { + setLoadingVersions(false) + } + } + + useEffect(() => { + if (isProjectDialogOpen && !newProject.phpVersion && phpVersions.length > 0) { + const installed = phpVersions.find(v => v.status === 'installed') + if (installed) { + setNewProject(prev => ({ ...prev, phpVersion: installed.version })) + } + } + }, [isProjectDialogOpen, phpVersions]) + useEffect(() => { if (!window.api) return fetchStatus() fetchProjects() fetchPhpVersions() + fetchMySqlVersions() - // @ts-ignore - const removeListener = window.api.onBinaryProgress((data) => { + const removePhpListener = window.api.onBinaryProgress ? window.api.onBinaryProgress((data) => { if (!data) return const { id, progress } = data setPhpVersions(prev => prev.map(v => v.id === id ? { ...v, progress, status: progress === 100 ? 'installed' : 'downloading' } : v)) - }) + }) : null + + const removeMySqlListener = window.api.onMySqlProgress ? window.api.onMySqlProgress((data) => { + if (!data) return + const { id, progress } = data + setMySqlVersions(prev => prev.map(v => v.id === id ? { ...v, progress, status: progress === 100 ? 'installed' : 'downloading' } : v)) + }) : null const interval = setInterval(() => { fetchStatus() fetchProjects() fetchPhpVersions() + fetchMySqlVersions() }, 3000) return () => { clearInterval(interval) - if (typeof removeListener === 'function') removeListener() + if (typeof removePhpListener === 'function') removePhpListener() + if (typeof removeMySqlListener === 'function') removeMySqlListener() } }, []) + const handleSelectPath = async () => { + if (window.api && window.api.selectDirectory) { + const path = await window.api.selectDirectory() + if (path) { + setNewProject(prev => ({ ...prev, path })) + } + } + } + const handleToggleService = async (service: keyof ServiceStatus) => { - // @ts-ignore + if (!window.api) return const result = status[service] === 'running' ? await window.api.stopService(service) : await window.api.startService(service) @@ -143,10 +197,10 @@ function App(): JSX.Element { } const handleStartAll = async () => { + if (!window.api) return const services: (keyof ServiceStatus)[] = ['nginx', 'php', 'mysql'] for (const s of services) { if (status[s] !== 'running') { - // @ts-ignore await window.api.startService(s) } } @@ -154,10 +208,10 @@ function App(): JSX.Element { } const handleStopAll = async () => { + if (!window.api) return const services: (keyof ServiceStatus)[] = ['nginx', 'php', 'mysql'] for (const s of services) { if (status[s] === 'running') { - // @ts-ignore await window.api.stopService(s) } } @@ -170,23 +224,23 @@ function App(): JSX.Element { } const handleAddProject = async () => { - // @ts-ignore + if (!window.api) return await window.api.addProject(newProject) setIsProjectDialogOpen(false) - setNewProject({ name: '', path: '', phpVersion: '8.2', host: 'localhost' }) + setNewProject({ name: '', path: '', phpVersion: '', host: 'localhost' }) setNotification({ open: true, message: 'Proje başarıyla eklendi.', severity: 'success' }) fetchProjects() } const handleRemoveProject = async (id: string) => { - // @ts-ignore + if (!window.api) return await window.api.removeProject(id) setNotification({ open: true, message: 'Proje silindi.', severity: 'success' }) fetchProjects() } const handleDownloadPhp = async (id: string) => { - // @ts-ignore + if (!window.api) return const result = await window.api.downloadPhpVersion(id) setNotification({ open: true, @@ -196,6 +250,19 @@ function App(): JSX.Element { fetchPhpVersions() } + const handleDownloadMySql = async (id: string) => { + if (!window.api) return + const result = await window.api.downloadMySqlVersion(id) + setNotification({ + open: true, + message: result.message, + severity: result.success ? 'success' : 'error' + }) + fetchMySqlVersions() + } + + const installedPhpVersions = phpVersions.filter(v => v.status === 'installed') + return ( theme.zIndex.drawer + 1, bgcolor: 'rgba(21, 62, 94, 0.85)', backdropFilter: 'blur(10px)' }}> @@ -208,7 +275,7 @@ function App(): JSX.Element { - + setActiveTab('settings')}> @@ -219,7 +286,7 @@ function App(): JSX.Element { sx={{ width: drawerWidth, flexShrink: 0, - [`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box', bgcolor: 'rgba(30, 30, 30, 0.95)', borderRight: '1px solid rgba(255, 255, 255, 0.05)' }, + [`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box', bgcolor: 'rgba(18, 18, 18, 0.95)', borderRight: '1px solid rgba(255, 255, 255, 0.05)' }, }} > @@ -255,45 +322,110 @@ function App(): JSX.Element { {activeTab === 'settings' && ( - - Genel Ayarlar - + + + Genel Ayarlar + + + - - - PHP Sürümleri - Gereken PHP sürümlerini buradan indirebilir ve yönetebilirsiniz. - - - {phpVersions.map((v) => ( - - - {v.status === 'installed' ? : } - - - - {v.status === 'available' && ( - - )} - {v.status === 'downloading' && ( - - - - )} - {v.status === 'installed' && ( - - )} - - - ))} - - + + setSettingsTab(v)} textColor="primary" indicatorColor="primary"> + } iconPosition="start" /> + } iconPosition="start" /> + + + + {settingsTab === 0 && ( + + + PHP Sürümleri + Gereken PHP sürümlerini buradan indirebilir ve yönetebilirsiniz. + + + {phpVersions.length === 0 && !loadingVersions && ( + + )} + {phpVersions.map((v) => ( + + + {v.status === 'installed' ? : } + + + + {v.status === 'available' && ( + + )} + {v.status === 'downloading' && ( + + + + )} + {v.status === 'installed' && ( + + )} + + + ))} + + + )} + + {settingsTab === 1 && ( + + + MySQL Sürümleri + Farklı MySQL sürümlerini indirebilir ve sunucunuzda kullanabilirsiniz. + + + {mySqlVersions.length === 0 && !loadingVersions && ( + + )} + {mySqlVersions.map((v) => ( + + + {v.status === 'installed' ? : } + + + {`MySQL ${v.version} (${v.cycle})`} + {v.description && ( + + )} + + } + secondary={v.status === 'downloading' ? `İndiriliyor... %${Math.round(v.progress)}` : v.status === 'installed' ? 'Kurulu' : 'İndirilebilir'} + sx={{ flexGrow: 1 }} + /> + + {v.status === 'available' && ( + + )} + {v.status === 'downloading' && ( + + + + )} + {v.status === 'installed' && ( + + )} + + + ))} + + + )} )} {activeTab === 'dashboard' && ( @@ -405,9 +537,18 @@ function App(): JSX.Element { onChange={(e) => setNewProject({ ...newProject, name: e.target.value })} /> + + + + + ), + }} onChange={(e) => setNewProject({ ...newProject, path: e.target.value })} /> setNewProject({ ...newProject, host: e.target.value })} /> - - PHP Sürümü + + PHP Sürümü - + diff --git a/test_dl.zip b/test_dl.zip new file mode 100644 index 0000000..5b4ecd1 --- /dev/null +++ b/test_dl.zip @@ -0,0 +1,38 @@ + + + + + +fw_error_www + + + + +
+
+

 

+
+ + + + + + +
+
+

+

This site is experiencing technical difficulty. We are aware of the issue and are working as quickly as possible to correct the issue.
+
+ We apologize for any inconvenience this may have caused.
+
+ To speak with an Oracle sales representative: 1.800.ORACLE1.
+
+ To contact Oracle Corporate Headquarters from anywhere in the world: 1.650.506.7000.
+
+ To get technical support in the United States: 1.800.633.0738. +


Incident Number: 0.64e1202.1774680040.2bfa9102


+  

+
+
+

+ \ No newline at end of file diff --git a/test_dl_v2.js b/test_dl_v2.js new file mode 100644 index 0000000..ec7dfa0 --- /dev/null +++ b/test_dl_v2.js @@ -0,0 +1,48 @@ +const axios = require('axios'); +const fs = require('fs'); + +async function test() { + console.log('Testing two-step download...'); + const url = 'https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.40-winx64.zip'; + const landing = 'https://dev.mysql.com/downloads/mysql/'; + + try { + console.log('1. Fetching landing page for cookies...'); + const landingRes = await axios.get(landing, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + } + }); + + const cookies = landingRes.headers['set-cookie']; + console.log('Cookies received:', cookies ? cookies.length : 0); + + console.log('2. Downloading ZIP with cookies...'); + const downloadRes = await axios({ + url, + method: 'GET', + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Referer': landing, + 'Cookie': cookies ? cookies.join('; ') : '' + }, + responseType: 'arraybuffer', + timeout: 10000 + }); + + console.log('Download Status:', downloadRes.status); + console.log('Content Length:', downloadRes.data.length); + if (downloadRes.data.length < 5000) { + console.log('Content Preview:', downloadRes.data.toString().substring(0, 500)); + } else { + console.log('Download SUCCESS (likely binary)'); + } + } catch (e) { + console.error('ERROR:', e.response ? e.response.status : e.message); + if (e.response && e.response.data) { + console.log('Error Data:', e.response.data.toString().substring(0, 200)); + } + } +} + +test(); diff --git a/test_mysql_api.js b/test_mysql_api.js new file mode 100644 index 0000000..cbed01a --- /dev/null +++ b/test_mysql_api.js @@ -0,0 +1,37 @@ +const axios = require('axios'); + +async function test() { + console.log('Testing official MySQL endpoints...'); + const urls = [ + 'https://downloads.mysql.com/archives/community/?json=1', + 'https://downloads.mysql.com/archives/community/?tpl=version&os=3&json=1', + 'https://dev.mysql.com/downloads/mysql/?json=1', + 'https://dev.mysql.com/downloads/mysql/?tpl=version&os=3&json=1' + ]; + + for (const url of urls) { + try { + const response = await axios.get(url, { + headers: { + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + }, + timeout: 5000 + }); + console.log(`URL: ${url}`); + console.log(`STATUS: ${response.status}`); + console.log(`CONTENT-TYPE: ${response.headers['content-type']}`); + console.log(`IS JSON: ${typeof response.data === 'object'}`); + if (typeof response.data === 'object') { + console.log('DATA PREVIEW:', JSON.stringify(response.data).substring(0, 200)); + } else { + console.log('DATA PREVIEW (100 chars):', String(response.data).substring(0, 100)); + } + console.log('---'); + } catch (e) { + console.log(`URL: ${url} failed: ${e.message}`); + } + } +} + +test();