feat: implement PHP and MySQL version management services with automated download and cataloging capabilities

This commit is contained in:
Ümit Tunç
2026-03-28 09:47:56 +03:00
parent b38e3d98bd
commit 430476416d
12 changed files with 791 additions and 95 deletions
+164 -1
View File
@@ -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",
+1
View File
@@ -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",
+37 -1
View File
@@ -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
+61
View File
@@ -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/"
}
}
}
+2 -1
View File
@@ -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
}
})
+119
View File
@@ -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<MySqlVersion[]> {
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<MySqlVersion[]> {
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()
+57 -20
View File
@@ -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<string, PhpVersion> = 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<string, PhpVersion>()
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)
}
+5 -1
View File
@@ -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
+181 -30
View File
@@ -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<any[]>([])
const [phpVersions, setPhpVersions] = useState<any[]>([])
const [mySqlVersions, setMySqlVersions] = useState<any[]>([])
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,22 +90,50 @@ function App(): JSX.Element {
})
const fetchStatus = async () => {
// @ts-ignore
if (window.api && window.api.getServiceStatus) {
const currentStatus = await window.api.getServiceStatus()
if (currentStatus) setStatus(currentStatus)
}
}
const fetchProjects = async () => {
// @ts-ignore
if (window.api && window.api.getProjects) {
const list = await window.api.getProjects()
setProjects(list || [])
}
}
const fetchPhpVersions = async () => {
// @ts-ignore
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
@@ -104,28 +141,45 @@ function App(): JSX.Element {
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 (
<Box sx={{ display: 'flex', minHeight: '100vh', backgroundImage: 'radial-gradient(circle at 50% 0%, #153E5E 0%, #121212 100%)', bgcolor: 'transparent' }}>
<AppBar position="fixed" sx={{ zIndex: (theme) => theme.zIndex.drawer + 1, bgcolor: 'rgba(21, 62, 94, 0.85)', backdropFilter: 'blur(10px)' }}>
@@ -208,7 +275,7 @@ function App(): JSX.Element {
<LanguageIcon />
</IconButton>
</Tooltip>
<IconButton color="inherit">
<IconButton color="inherit" onClick={() => setActiveTab('settings')}>
<SettingsIcon />
</IconButton>
</Toolbar>
@@ -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)' },
}}
>
<Toolbar />
@@ -255,16 +322,32 @@ function App(): JSX.Element {
<Container maxWidth="md">
{activeTab === 'settings' && (
<Box>
<Typography variant="h5" sx={{ fontWeight: 'medium', mb: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h5" sx={{ fontWeight: 'medium' }}>
Genel Ayarlar
</Typography>
<Button startIcon={loadingVersions ? <CircularProgress size={20} /> : <RefreshIcon />} onClick={handleRefreshVersions} disabled={loadingVersions}>
Listeyi Yenile
</Button>
</Box>
<Paper sx={{ mb: 4, bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2 }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs value={settingsTab} onChange={(_e, v) => setSettingsTab(v)} textColor="primary" indicatorColor="primary">
<Tab label="PHP Sürümleri" icon={<PhpIcon />} iconPosition="start" />
<Tab label="MySQL Sürümleri" icon={<DbIcon />} iconPosition="start" />
</Tabs>
</Box>
{settingsTab === 0 && (
<Paper sx={{ bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2 }}>
<Box sx={{ p: 2, borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>PHP Sürümleri</Typography>
<Typography variant="body2" color="text.secondary">Gereken PHP sürümlerini buradan indirebilir ve yönetebilirsiniz.</Typography>
</Box>
<List>
<List sx={{ maxHeight: 500, overflow: 'auto' }}>
{phpVersions.length === 0 && !loadingVersions && (
<ListItem><ListItemText primary="Yükleniyor veya liste boş..." /></ListItem>
)}
{phpVersions.map((v) => (
<ListItem key={v.id} sx={{ py: 2 }}>
<ListItemIcon>
@@ -294,6 +377,55 @@ function App(): JSX.Element {
))}
</List>
</Paper>
)}
{settingsTab === 1 && (
<Paper sx={{ bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2 }}>
<Box sx={{ p: 2, borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>MySQL Sürümleri</Typography>
<Typography variant="body2" color="text.secondary">Farklı MySQL sürümlerini indirebilir ve sunucunuzda kullanabilirsiniz.</Typography>
</Box>
<List sx={{ maxHeight: 500, overflow: 'auto' }}>
{mySqlVersions.length === 0 && !loadingVersions && (
<ListItem><ListItemText primary="Yükleniyor veya liste boş..." /></ListItem>
)}
{mySqlVersions.map((v) => (
<ListItem key={v.id} sx={{ py: 2 }}>
<ListItemIcon>
{v.status === 'installed' ? <InstalledIcon color="success" /> : <AvailableIcon color="action" />}
</ListItemIcon>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{`MySQL ${v.version} (${v.cycle})`}
{v.description && (
<Chip label={v.description} size="small" variant="outlined" sx={{ fontSize: '0.7rem', height: 20 }} />
)}
</Box>
}
secondary={v.status === 'downloading' ? `İndiriliyor... %${Math.round(v.progress)}` : v.status === 'installed' ? 'Kurulu' : 'İndirilebilir'}
sx={{ flexGrow: 1 }}
/>
<Box sx={{ minWidth: 120, textAlign: 'right' }}>
{v.status === 'available' && (
<Button variant="outlined" size="small" startIcon={<DownloadIcon />} onClick={() => handleDownloadMySql(v.id)}>
İNDİR
</Button>
)}
{v.status === 'downloading' && (
<Box sx={{ width: '100%', mt: 1 }}>
<LinearProgress variant="determinate" value={v.progress} sx={{ height: 8, borderRadius: 5 }} />
</Box>
)}
{v.status === 'installed' && (
<Chip label="HAZIR" color="success" variant="outlined" size="small" />
)}
</Box>
</ListItem>
))}
</List>
</Paper>
)}
</Box>
)}
{activeTab === 'dashboard' && (
@@ -405,9 +537,18 @@ function App(): JSX.Element {
onChange={(e) => setNewProject({ ...newProject, name: e.target.value })}
/>
<TextField
label="Dosya Yolu (Absolute path)"
label="Dosya Yolu"
fullWidth
value={newProject.path}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={handleSelectPath} edge="end">
<FolderOpenIcon />
</IconButton>
</InputAdornment>
),
}}
onChange={(e) => setNewProject({ ...newProject, path: e.target.value })}
/>
<TextField
@@ -416,24 +557,34 @@ function App(): JSX.Element {
value={newProject.host}
onChange={(e) => setNewProject({ ...newProject, host: e.target.value })}
/>
<FormControl fullWidth>
<InputLabel>PHP Sürümü</InputLabel>
<FormControl fullWidth disabled={installedPhpVersions.length === 0}>
<InputLabel id="php-version-label">PHP Sürümü</InputLabel>
<Select
labelId="php-version-label"
value={newProject.phpVersion}
label="PHP Sürümü"
onChange={(e) => setNewProject({ ...newProject, phpVersion: e.target.value })}
>
<MenuItem value="8.0">PHP 8.0</MenuItem>
<MenuItem value="8.1">PHP 8.1</MenuItem>
<MenuItem value="8.2">PHP 8.2</MenuItem>
<MenuItem value="8.3">PHP 8.3</MenuItem>
{installedPhpVersions.length > 0 ? (
installedPhpVersions.map(v => (
<MenuItem key={v.id} value={v.version}>PHP {v.version}</MenuItem>
))
) : (
<MenuItem disabled value="">Önce PHP sürümü indirmelisiniz</MenuItem>
)}
</Select>
</FormControl>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setIsProjectDialogOpen(false)}>İptal</Button>
<Button onClick={handleAddProject} variant="contained" disabled={!newProject.name || !newProject.path}>Ekle</Button>
<Button
onClick={handleAddProject}
variant="contained"
disabled={!newProject.name || !newProject.path || installedPhpVersions.length === 0}
>
Ekle
</Button>
</DialogActions>
</Dialog>
</Box>
+38
View File
@@ -0,0 +1,38 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>fw_error_www</title>
</head>
<body>
<div align="center">
<div align="center">
<p>&nbsp;</p>
</div>
<table width="449" align="center" border="0">
<tbody>
<tr>
<td width="68%" valign="top">
<div align="center">
<p><img alt="" src="https://www.oracle.com/splash/oralogo_small.gif" width="154" height="19" /></p>
<p align="justify"><font face="Arial, Helvetica, sans-serif">This site </font><font face="Arial, Helvetica, sans-serif"> is experiencing technical difficulty. We are aware of the issue and are working as quickly as possible to correct the issue. <br />
<br />
We apologize for any inconvenience this may have caused. <br />
<br />
To speak with an Oracle sales representative: 1.800.ORACLE1.<br />
<br />
To contact Oracle Corporate Headquarters from anywhere in the world: 1.650.506.7000.<br />
<br />
To get technical support in the United States: 1.800.633.0738.
<br><br><br><p style="font-size: 18px" align="left"><b>Incident Number: 0.64e1202.1774680040.2bfa9102</b></p></font><br />
&nbsp;</p>
</div>
</td>
</tr>
</tbody>
</table>
</p>
</body>
+48
View File
@@ -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();
+37
View File
@@ -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();