From 91327b245c724fc3b8c6620b14227c362926684d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Mon, 30 Mar 2026 08:40:57 +0300 Subject: [PATCH] feat: implement MySQL operation service and add export wizard UI with localization support --- src/main/ipc/index.ts | 55 +++- src/main/services/MySqlOperationService.ts | 62 ++++ src/renderer/src/App.tsx | 40 ++- .../src/components/MySqlExportWizard.tsx | 309 ++++++++++++++++++ src/renderer/src/locales/tr.json | 4 +- 5 files changed, 465 insertions(+), 5 deletions(-) create mode 100644 src/renderer/src/components/MySqlExportWizard.tsx diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 6ad77c4..b3ba1aa 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -1,4 +1,4 @@ -import { ipcMain, app, dialog } from 'electron' +import { ipcMain, app, dialog, shell } from 'electron' import fs from 'fs' import { processManager } from '../services/ProcessManager' import { configService } from '../services/ConfigService' @@ -393,6 +393,21 @@ export function registerIpcHandlers(): void { return result.filePaths[0] }) + ipcMain.handle('dialog:save-file', async (_event, filters: any[]) => { + const result = await dialog.showSaveDialog({ + filters: filters || [ + { name: 'SQL File', extensions: ['sql'] }, + { name: 'Compressed SQL', extensions: ['sql.gz'] } + ] + }) + if (result.canceled) return null + return result.filePath + }) + + ipcMain.handle('shell:open-path', async (_event, pathStr: string) => { + return await shell.openPath(pathStr) + }) + ipcMain.handle('binaries:download', async (_event, { name, url }) => { return downloadService.downloadAndExtract(url, name, (p) => { // TODO: Emit progress to renderer via webContents.send @@ -646,4 +661,42 @@ export function registerIpcHandlers(): void { return { success: false, message: `Import error: ${error.message}` } } }) + + ipcMain.handle('mysql:export-wizard:list-dbs', async (_event, { versionId, rootPass }) => { + const versions = await mySqlManagerService.getVersions() + const v = versions.find(v => v.id === versionId) + if (!v) return [] + + const binDir = path.join(configService.getBasePath(), 'bin', versionId) + const mysqlBin = findExecutable(binDir, 'mysql.exe') + if (!mysqlBin) return [] + + return await mySqlOperationService.listDatabases({ + binPath: mysqlBin, + host: '127.0.0.1', + port: v.port, + rootUser: 'root', + rootPass: rootPass || '' + }) + }) + + ipcMain.handle('mysql:export-wizard:start', async (event, { versionId, dbName, savePath, compress, rootPass }) => { + const versions = await mySqlManagerService.getVersions() + const v = versions.find(v => v.id === versionId) + if (!v) return { success: false, message: 'MySQL version not found' } + + const binDir = path.join(configService.getBasePath(), 'bin', versionId) + const mysqlBin = findExecutable(binDir, 'mysql.exe') + if (!mysqlBin) return { success: false, message: 'mysql.exe not found' } + + const log = (msg: string) => event.sender.send('mysql:export:log', msg) + + return await mySqlOperationService.exportSql({ + binPath: mysqlBin, + host: '127.0.0.1', + port: v.port, + rootUser: 'root', + rootPass: rootPass || '' + }, dbName, savePath, compress, (msg) => log(msg)) + }) } diff --git a/src/main/services/MySqlOperationService.ts b/src/main/services/MySqlOperationService.ts index cf128c8..adc6ae9 100644 --- a/src/main/services/MySqlOperationService.ts +++ b/src/main/services/MySqlOperationService.ts @@ -162,6 +162,68 @@ export class MySqlOperationService { }) }) } + + async listDatabases(config: MySqlConfig): Promise { + const result = await this.runQuery(config, 'SHOW DATABASES') + if (!result.success) return [] + + return result.output + .split('\n') + .map(line => line.trim()) + .filter(line => line && line !== 'Database' && !line.match(/^information_schema|performance_schema|mysql|sys$/i)) + } + + async exportSql(config: MySqlConfig, dbName: string, targetPath: string, compress: boolean, onProgress?: (log: string) => void): Promise<{ success: boolean; message: string }> { + return new Promise((resolve) => { + const dumpPath = config.binPath.replace(/mysql\.exe$/i, 'mysqldump.exe') + const args = [ + `--host=${config.host}`, + `--port=${config.port}`, + `--user=${config.rootUser}`, + dbName, + '--routines', + '--triggers' + ] + + if (config.rootPass) { + args.push(`--password=${config.rootPass}`) + } + + onProgress?.(`Executing: "${dumpPath}" ${args.join(' ')} > "${targetPath}"`) + + const child = spawn(dumpPath, args, { windowsHide: true }) + const writeStream = fs.createWriteStream(targetPath) + + if (compress) { + const gzip = zlib.createGzip() + child.stdout.pipe(gzip).pipe(writeStream) + } else { + child.stdout.pipe(writeStream) + } + + let errorOutput = '' + child.stderr.on('data', (data) => { + errorOutput += data.toString() + onProgress?.(data.toString()) + }) + + child.on('error', (err) => { + onProgress?.(`Process error: ${err.message}`) + resolve({ success: false, message: `Process error: ${err.message}` }) + }) + + child.on('close', (code) => { + writeStream.end() + if (code === 0) { + onProgress?.('Export completed successfully.') + resolve({ success: true, message: 'Export completed successfully.' }) + } else { + onProgress?.(`Export failed with exit code ${code}`) + resolve({ success: false, message: `Export failed: ${errorOutput}` }) + } + }) + }) + } } export const mySqlOperationService = new MySqlOperationService() diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 9b4ce32..09a7d84 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -72,6 +72,7 @@ import { import Swal from 'sweetalert2' import { useTranslation } from 'react-i18next' import MySqlImportWizard from './components/MySqlImportWizard' +import MySqlExportWizard from './components/MySqlExportWizard' import NginxCodeEditor from './components/NginxCodeEditor' declare global { @@ -131,6 +132,7 @@ function App(): JSX.Element { const [pmaLoading, setPmaLoading] = useState(false) const [pmaProgress, setPmaProgress] = useState(0) const [isMySqlWizardOpen, setIsMySqlWizardOpen] = useState(false) + const [isMySqlExportWizardOpen, setIsMySqlExportWizardOpen] = useState(false) const [isNginxConfigOpen, setIsNginxConfigOpen] = useState(false) const [nginxConfig, setNginxConfig] = useState('') const [isProjectNginxConfigOpen, setIsProjectNginxConfigOpen] = useState(false) @@ -1085,13 +1087,13 @@ function App(): JSX.Element { )} - + - MySQL Dump/Import Sihirbazı + MySQL Veri İçe Aktar Seçtiğiniz bir .sql veya arşiv dosyasını hızlıca veritabanına yükleyin. @@ -1104,7 +1106,33 @@ function App(): JSX.Element { startIcon={} sx={{ borderRadius: 2 }} > - SİHİRBAZI BAŞLAT + İÇE AKTAR + + + + + + + + + + + + MySQL Veri Dışa Aktar + + Veritabanlarını .sql veya .sql.gz olarak güvenle yedekleyin. + + + + @@ -1432,6 +1460,12 @@ function App(): JSX.Element { mySqlVersions={mySqlVersions} /> + setIsMySqlExportWizardOpen(false)} + mySqlVersions={mySqlVersions} + /> + setIsNginxConfigOpen(false)} diff --git a/src/renderer/src/components/MySqlExportWizard.tsx b/src/renderer/src/components/MySqlExportWizard.tsx new file mode 100644 index 0000000..ea056e6 --- /dev/null +++ b/src/renderer/src/components/MySqlExportWizard.tsx @@ -0,0 +1,309 @@ +import React, { useState, useEffect, useRef } from 'react' +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Stepper, + Step, + StepLabel, + Typography, + TextField, + MenuItem, + Select, + FormControl, + InputLabel, + Paper, + InputAdornment, + CircularProgress, + Divider, + Stack, + FormControlLabel, + Checkbox +} from '@mui/material' +import { + CheckCircle as SuccessIcon, + Error as ErrorIcon, + Save as SaveIcon +} from '@mui/icons-material' +import Swal from 'sweetalert2' + +interface MySqlExportWizardProps { + open: boolean + onClose: () => void + mySqlVersions: any[] +} + +const steps = [ + 'Sunucu Seçimi', + 'Veritabanı Seçimi', + 'Dışa Aktarma Ayarları', + 'Özet ve İşlem' +] + +const MySqlExportWizard: React.FC = ({ open, onClose, mySqlVersions }) => { + const [activeStep, setActiveStep] = useState(0) + const [selectedVersion, setSelectedVersion] = useState('') + const [databases, setDatabases] = useState([]) + const [selectedDb, setSelectedDb] = useState('') + const [savePath, setSavePath] = useState('') + const [compress, setCompress] = useState(true) + const [rootPass, setRootPass] = useState('') + const [loading, setLoading] = useState(false) + const [logs, setLogs] = useState([]) + const logEndRef = useRef(null) + const [exportResult, setExportResult] = useState<{ success: boolean; message: string } | null>(null) + + useEffect(() => { + if (open) { + setActiveStep(0) + setSelectedDb('') + setSavePath('') + setLogs([]) + setExportResult(null) + const installedVersion = mySqlVersions.find(v => v.status === 'installed') + if (installedVersion) setSelectedVersion(installedVersion.id) + } + }, [open, mySqlVersions]) + + useEffect(() => { + if (logEndRef.current) { + logEndRef.current.scrollIntoView({ behavior: 'smooth' }) + } + }, [logs]) + + const nextStep = () => setActiveStep(prev => prev + 1) + const prevStep = () => setActiveStep(prev => prev - 1) + + const handleStep1Next = async () => { + if (!selectedVersion) { + Swal.fire('Hata', 'Lütfen bir MySQL sunucusu seçin.', 'error') + return + } + setLoading(true) + try { + const dbs = await window.api.invoke('mysql:export-wizard:list-dbs', { versionId: selectedVersion, rootPass }) + setDatabases(dbs) + nextStep() + } catch (err: any) { + Swal.fire('Hata', 'Veritabanları listelenemedi: ' + err.message, 'error') + } finally { + setLoading(false) + } + } + + const handleSelectSavePath = async () => { + const path = await window.api.invoke('dialog:save-file', [ + { name: compress ? 'Compressed SQL' : 'SQL File', extensions: [compress ? 'sql.gz' : 'sql'] } + ]) + if (path) setSavePath(path) + } + + const startExport = async () => { + setLoading(true) + setLogs(['[Sistem] Dışa aktarma işlemi başlatılıyor...']) + + const removeListener = window.api.onMySqlExportLog((msg: string) => { + setLogs(prev => [...prev, msg]) + }) + + try { + const result = await window.api.invoke('mysql:export-wizard:start', { + versionId: selectedVersion, + dbName: selectedDb, + savePath, + compress, + rootPass + }) + setExportResult(result) + } catch (err: any) { + setExportResult({ success: false, message: err.message }) + } finally { + setLoading(false) + if (removeListener) removeListener() + } + } + + const renderStepContent = (step: number) => { + switch (step) { + case 0: + return ( + + Veritabanın hangi MySQL sunucusunda olduğunu seçin. + + MySQL Sunucusu + + + setRootPass(e.target.value)} + helperText="MySQL 'root' kullanıcısının şifresi. Boşsa boş bırakın." + /> + + ) + case 1: + return ( + + Dışa aktarmak istediğiniz veritabanını seçin. + + Veritabanı + + + {databases.length === 0 && ( + + Seçili sunucuda erişilebilir veritabanı bulunamadı. + + )} + + ) + case 2: + return ( + + Dosyanın kaydedileceği yeri ve formatını belirleyin. + setCompress(e.target.checked)} />} + label="Gzip ile Sıkıştır (.sql.gz)" + /> + + + + + ), + }} + /> + + + + ) + case 3: + return ( + + {!exportResult ? ( + <> + + Özet Bilgiler + + + Sunucu: {selectedVersion} + Veritabanı: {selectedDb} + Hedef: {savePath} + Sıkıştırma: {compress ? 'Açık (.gz)' : 'Kapalı (.sql)'} + + + {loading && ( + + {logs.map((log, i) => ( + + {log} + + ))} +
+ + )} + + ) : ( + + {exportResult.success ? ( + <> + + İşlem Başarılı! + {exportResult.message} + + + ) : ( + <> + + Hata Oluştu! + {exportResult.message} + + )} + + )} + + ) + default: + return 'Bilinmeyen adım' + } + } + + return ( + + MySQL Veritabanı Dışa Aktarma Sihirbazı + + + {steps.map((label) => ( + + {label} + + ))} + + + {renderStepContent(activeStep)} + + + + + + {activeStep > 0 && activeStep < 3 && !exportResult && ( + + )} + {activeStep === 0 && ( + + )} + {activeStep === 1 && ( + + )} + {activeStep === 2 && ( + + )} + {activeStep === 3 && !exportResult && ( + + )} + + + ) +} + +export default MySqlExportWizard diff --git a/src/renderer/src/locales/tr.json b/src/renderer/src/locales/tr.json index e0b6df4..f93fa5d 100644 --- a/src/renderer/src/locales/tr.json +++ b/src/renderer/src/locales/tr.json @@ -9,7 +9,9 @@ "quit": "Çıkış", "save": "Kaydet", "port": "Port", - "all": "Tümü" + "all": "Tümü", + "export": "Dışa Aktar", + "import": "İçe Aktar" }, "dashboard": { "title": "Trunçgil Multi-PHP Server",