diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 0298267..8df030c 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -10,6 +10,7 @@ import { nginxManagerService } from '../services/NginxManagerService' import { phpMyAdminService } from '../services/PhpMyAdminService' import { portMappingService } from '../services/PortMappingService' import path from 'path' +import { mySqlOperationService } from '../services/MySqlOperationService' function findExecutable(dir: string, name: string): string | null { if (!fs.existsSync(dir)) return null @@ -349,6 +350,17 @@ export function registerIpcHandlers(): void { return result.filePaths[0] }) + ipcMain.handle('dialog:select-file', async (_event, filters: any[]) => { + const result = await dialog.showOpenDialog({ + properties: ['openFile'], + filters: filters || [ + { name: 'SQL & Archives', extensions: ['sql', 'zip', 'gz', 'tar'] } + ] + }) + 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 @@ -411,4 +423,75 @@ export function registerIpcHandlers(): void { return { success: false, message: `Sıfırlama hatası: ${error.message}` } } }) + + ipcMain.handle('mysql:import-wizard:check-db', async (_event, { versionId, dbName }) => { + const versions = await mySqlManagerService.getVersions() + const v = versions.find(v => v.id === versionId) + if (!v) return { exists: false, error: 'MySQL version not found' } + + const binDir = path.join(configService.getBasePath(), 'bin', versionId) + const binPath = findExecutable(binDir, 'mysql.exe') + if (!binPath) return { exists: false, error: 'mysql.exe not found' } + + const exists = await mySqlOperationService.checkDatabaseExists({ + binPath, + host: '127.0.0.1', + port: v.port, + rootUser: 'root', + rootPass: '' // Assuming root has no password by default in this setup + }, dbName) + + return { exists } + }) + + ipcMain.handle('mysql:import-wizard:start', async (event, { versionId, filePath, dbName, user, pass, overwrite }) => { + 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 config = { + binPath: mysqlBin, + host: '127.0.0.1', + port: v.port, + rootUser: 'root', + rootPass: '' + } + + const log = (msg: string) => event.sender.send('mysql:import:log', msg) + + try { + if (overwrite) { + log(`Dropping existing database: ${dbName}`) + await mySqlOperationService.dropDatabase(config, dbName) + } + + log(`Creating database and user: ${dbName} / ${user}`) + const createRes = await mySqlOperationService.createDatabaseAndUser(config, dbName, user, pass) + if (!createRes.success) return createRes + + let sqlFiles: string[] = [] + if (filePath.toLowerCase().endsWith('.sql')) { + sqlFiles = [filePath] + } else { + log(`Extracting archive: ${path.basename(filePath)}`) + const extractDir = path.join(app.getPath('temp'), `mysql_import_${Date.now()}`) + sqlFiles = await mySqlOperationService.extractArchive(filePath, extractDir) + if (sqlFiles.length === 0) return { success: false, message: 'No .sql files found in archive.' } + } + + for (const sqlFile of sqlFiles) { + log(`Importing: ${path.basename(sqlFile)}`) + const importRes = await mySqlOperationService.importSql(config, dbName, sqlFile, (msg) => log(msg)) + if (!importRes.success) return importRes + } + + return { success: true, message: 'Import completed successfully.' } + } catch (error: any) { + return { success: false, message: `Import error: ${error.message}` } + } + }) } diff --git a/src/main/services/MySqlOperationService.ts b/src/main/services/MySqlOperationService.ts new file mode 100644 index 0000000..815f4a0 --- /dev/null +++ b/src/main/services/MySqlOperationService.ts @@ -0,0 +1,136 @@ +import fs from 'fs' +import path from 'path' +import { spawn } from 'child_process' +import decompress from 'decompress' + +export interface MySqlConfig { + binPath: string + host: string + port: string + rootUser: string + rootPass: string +} + +export class MySqlOperationService { + async extractArchive(archivePath: string, targetDir: string): Promise { + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }) + } + + await decompress(archivePath, targetDir) + + // Find all .sql files in the extracted directory + const sqlFiles: string[] = [] + const findSqlFiles = (dir: string) => { + const files = fs.readdirSync(dir) + for (const file of files) { + const fullPath = path.join(dir, file) + if (fs.statSync(fullPath).isDirectory()) { + findSqlFiles(fullPath) + } else if (file.toLowerCase().endsWith('.sql')) { + sqlFiles.push(fullPath) + } + } + } + + findSqlFiles(targetDir) + return sqlFiles + } + + async runQuery(config: MySqlConfig, query: string, database?: string): Promise<{ success: boolean; output: string; error?: string }> { + return new Promise((resolve) => { + const args = [ + `--host=${config.host}`, + `--port=${config.port}`, + `--user=${config.rootUser}`, + database ? `--database=${database}` : null + ].filter(Boolean) as string[] + + if (config.rootPass) { + args.push(`--password=${config.rootPass}`) + } + + args.push('-e', query) + + const child = spawn(config.binPath, args, { windowsHide: true }) + let output = '' + let error = '' + + child.stdout.on('data', (data) => output += data.toString()) + child.stderr.on('data', (data) => error += data.toString()) + + child.on('close', (code) => { + if (code === 0) { + resolve({ success: true, output }) + } else { + resolve({ success: false, output, error }) + } + }) + }) + } + + async checkDatabaseExists(config: MySqlConfig, dbName: string): Promise { + const result = await this.runQuery(config, `SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '${dbName}'`) + return result.success && result.output.includes(dbName) + } + + async createDatabaseAndUser(config: MySqlConfig, dbName: string, user: string, pass: string): Promise<{ success: boolean; message: string }> { + const queries = [ + `CREATE DATABASE IF NOT EXISTS \`${dbName}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`, + `CREATE USER IF NOT EXISTS '${user}'@'%' IDENTIFIED BY '${pass}';`, + `GRANT ALL PRIVILEGES ON \`${dbName}\`.* TO '${user}'@'%';`, + `FLUSH PRIVILEGES;` + ] + + for (const query of queries) { + const result = await this.runQuery(config, query) + if (!result.success) { + return { success: false, message: `Query failed: ${query}\nError: ${result.error}` } + } + } + + return { success: true, message: 'Database and user created successfully.' } + } + + async dropDatabase(config: MySqlConfig, dbName: string): Promise<{ success: boolean; message: string }> { + const result = await this.runQuery(config, `DROP DATABASE IF EXISTS \`${dbName}\``) + return { success: result.success, message: result.success ? 'Database dropped' : `Drop failed: ${result.error}` } + } + + async importSql(config: MySqlConfig, dbName: string, sqlFilePath: string, onProgress?: (log: string) => void): Promise<{ success: boolean; message: string }> { + return new Promise((resolve) => { + const mysqlPath = config.binPath + const args = [ + `--host=${config.host}`, + `--port=${config.port}`, + `--user=${config.rootUser}`, + `--database=${dbName}` + ] + + if (config.rootPass) { + args.push(`--password=${config.rootPass}`) + } + + // Using shell true to use redirecting input + const command = `"${mysqlPath}" ${args.join(' ')} < "${sqlFilePath}"` + onProgress?.(`Executing: mysql ${args.join(' ')} < ${path.basename(sqlFilePath)}`) + + const child = spawn('cmd.exe', ['/c', command], { windowsHide: true }) + + child.stdout.on('data', (data) => onProgress?.(data.toString())) + child.stderr.on('data', (data) => onProgress?.(data.toString())) + + child.on('close', (code) => { + if (code === 0) { + onProgress?.('Import completed successfully.') + resolve({ success: true, message: 'Import completed successfully.' }) + } else { + onProgress?.(`Import failed with exit code ${code}`) + resolve({ success: false, message: `Import failed (Exit Code: ${code})` }) + } + }) + }) + } +} + +export const mySqlOperationService = new MySqlOperationService() diff --git a/src/preload/index.ts b/src/preload/index.ts index 4580b09..a8bb2e9 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -27,7 +27,15 @@ const api = { onNginxProgress: (callback: any) => electronAPI.ipcRenderer.on('nginx:progress', (_event, data) => callback(data)), pmaStatus: () => electronAPI.ipcRenderer.invoke('pma:status'), pmaSetup: (mysqlPort: string) => electronAPI.ipcRenderer.invoke('pma:setup', mysqlPort), - onPmaProgress: (callback: any) => electronAPI.ipcRenderer.on('pma:progress', (_event, data) => callback(data)) + onPmaProgress: (callback: any) => electronAPI.ipcRenderer.on('pma:progress', (_event, data) => callback(data)), + selectFile: (filters?: any[]) => electronAPI.ipcRenderer.invoke('dialog:select-file', filters), + checkMySqlDb: (versionId: string, dbName: string) => electronAPI.ipcRenderer.invoke('mysql:import-wizard:check-db', { versionId, dbName }), + startMySqlImport: (data: any) => electronAPI.ipcRenderer.invoke('mysql:import-wizard:start', data), + onMySqlImportLog: (callback: any) => { + const listener = (_event, msg) => callback(msg) + electronAPI.ipcRenderer.on('mysql:import:log', listener) + return () => electronAPI.ipcRenderer.removeListener('mysql:import:log', listener) + } } // Use `contextBridge` APIs to expose Electron APIs to diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 581a3a1..4cd1884 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -71,6 +71,7 @@ import { } from '@mui/icons-material' import Swal from 'sweetalert2' import { useTranslation } from 'react-i18next' +import MySqlImportWizard from './components/MySqlImportWizard' declare global { interface Window { @@ -129,6 +130,7 @@ function App(): JSX.Element { const [pmaInstalled, setPmaInstalled] = useState(false) const [pmaLoading, setPmaLoading] = useState(false) const [pmaProgress, setPmaProgress] = useState(0) + const [isMySqlWizardOpen, setIsMySqlWizardOpen] = useState(false) const fetchStatus = async () => { if (window.api && window.api.getServiceStatus) { @@ -958,9 +960,35 @@ function App(): JSX.Element { )} + + + + + + + + MySQL Dump/Import Sihirbazı + + Seçtiğiniz bir .sql veya arşiv dosyasını hızlıca veritabanına yükleyin. + + + + + + + )} + {activeTab === 'projects' && ( @@ -1085,7 +1113,7 @@ function App(): JSX.Element { )} - + - + + setIsMySqlWizardOpen(false)} + mySqlVersions={mySqlVersions} + /> + ) } export default App + diff --git a/src/renderer/src/components/MySqlImportWizard.tsx b/src/renderer/src/components/MySqlImportWizard.tsx new file mode 100644 index 0000000..f0af2e4 --- /dev/null +++ b/src/renderer/src/components/MySqlImportWizard.tsx @@ -0,0 +1,346 @@ +import React, { useState, useEffect, useRef } from 'react' +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Stepper, + Step, + StepLabel, + Typography, + TextField, + MenuItem, + Select, + FormControl, + InputLabel, + Paper, + IconButton, + InputAdornment, + CircularProgress, + Divider, + Stack +} from '@mui/material' +import { + CloudUpload as FileIcon, + Refresh as RefreshIcon, + CheckCircle as SuccessIcon, + Error as ErrorIcon +} from '@mui/icons-material' +import Swal from 'sweetalert2' + +interface MySqlImportWizardProps { + open: boolean + onClose: () => void + mySqlVersions: any[] +} + +const steps = [ + 'Veri Seçimi', + 'Sunucu Seçimi', + 'Veritabanı Yapılandırması', + 'Özet ve İşlem' +] + +const MySqlImportWizard: React.FC = ({ open, onClose, mySqlVersions }) => { + const [activeStep, setActiveStep] = useState(0) + const [filePath, setFilePath] = useState('') + const [selectedVersion, setSelectedVersion] = useState('') + const [dbConfig, setDbConfig] = useState({ + dbName: '', + user: '', + pass: '' + }) + const [loading, setLoading] = useState(false) + const [logs, setLogs] = useState([]) + const logEndRef = useRef(null) + const [importResult, setImportResult] = useState<{ success: boolean; message: string } | null>(null) + + useEffect(() => { + if (open) { + setActiveStep(0) + setFilePath('') + setLogs([]) + setImportResult(null) + const installedVersion = mySqlVersions.find(v => v.status === 'installed') + if (installedVersion) setSelectedVersion(installedVersion.id) + + // Generate random credentials + const randomStr = Math.random().toString(36).substring(2, 8) + setDbConfig({ + dbName: `db_${randomStr}`, + user: `user_${randomStr}`, + pass: Math.random().toString(36).substring(2, 12) + }) + } + }, [open, mySqlVersions]) + + useEffect(() => { + if (logEndRef.current) { + logEndRef.current.scrollIntoView({ behavior: 'smooth' }) + } + }, [logs]) + + const handleSelectFile = async () => { + const path = await window.api.selectFile([ + { name: 'SQL & Archives', extensions: ['sql', 'zip', 'gz', 'tar'] } + ]) + if (path) { + setFilePath(path) + // Try to guess DB name from file name + const fileName = path.split(/[/\\]/).pop()?.split('.')[0] || '' + if (fileName && !dbConfig.dbName.startsWith('db_')) { + setDbConfig(prev => ({ ...prev, dbName: fileName })) + } + } + } + + const nextStep = () => setActiveStep(prev => prev + 1) + const prevStep = () => setActiveStep(prev => prev - 1) + + const handleStep2Next = () => { + if (!selectedVersion) { + Swal.fire('Hata', 'Lütfen bir MySQL sunucusu seçin.', 'error') + return + } + nextStep() + } + + const handleStep3Next = async () => { + if (!dbConfig.dbName || !dbConfig.user || !dbConfig.pass) { + Swal.fire('Hata', 'Lütfen tüm alanları doldurun.', 'error') + return + } + + setLoading(true) + try { + const { exists } = await window.api.checkMySqlDb(selectedVersion, dbConfig.dbName) + if (exists) { + const result = await Swal.fire({ + title: 'Veritabanı Mevcut!', + text: `'${dbConfig.dbName}' veritabanı zaten var. Üzerine yazmak istiyor musunuz? Mevcut tüm veriler silinecektir!`, + icon: 'warning', + showCancelButton: true, + confirmButtonText: 'Evet, Üzerine Yaz', + cancelButtonText: 'Hayır, Vazgeç', + confirmButtonColor: '#d33' + }) + if (!result.isConfirmed) return + } + nextStep() + } catch (err: any) { + Swal.fire('Hata', err.message, 'error') + } finally { + setLoading(false) + } + } + + const startImport = async () => { + setLoading(true) + setLogs(['[Sistem] İşlem başlatılıyor...']) + + const removeListener = window.api.onMySqlImportLog((msg: string) => { + setLogs(prev => [...prev, msg]) + }) + + try { + const result = await window.api.startMySqlImport({ + versionId: selectedVersion, + filePath, + dbName: dbConfig.dbName, + user: dbConfig.user, + pass: dbConfig.pass, + overwrite: true // Logic handled by checkMySqlDb before + }) + setImportResult(result) + } catch (err: any) { + setImportResult({ success: false, message: err.message }) + } finally { + setLoading(false) + if (removeListener) removeListener() + } + } + + const renderStepContent = (step: number) => { + switch (step) { + case 0: + return ( + + Lütfen yüklenecek olan .sql dosyasını veya arşivini (.zip, .tar.gz) seçin. + + + + + ), + }} + /> + + + + ) + case 1: + return ( + + Dosyanın hangi MySQL sunucusuna yükleneceğini seçin. + + MySQL Sunucusu + + + + ) + case 2: + return ( + + Veritabanı ve kullanıcı bilgilerini yapılandırın. + setDbConfig({ ...dbConfig, dbName: e.target.value })} + /> + setDbConfig({ ...dbConfig, user: e.target.value })} + /> + setDbConfig({ ...dbConfig, pass: e.target.value })} + InputProps={{ + endAdornment: ( + + setDbConfig({ ...dbConfig, pass: Math.random().toString(36).substring(2, 12) })}> + + + + ) + }} + /> + + ) + case 3: + return ( + + {!importResult ? ( + <> + + Özet Bilgiler + + + Dosya: {filePath.split(/[/\\]/).pop()} + Sunucu: {selectedVersion} + Veritabanı: {dbConfig.dbName} + Kullanıcı: {dbConfig.user} + + + {loading && ( + + {logs.map((log, i) => ( + + {log} + + ))} +
+ + )} + + ) : ( + + {importResult.success ? ( + <> + + İşlem Başarılı! + {importResult.message} + + Erişim Bilgileri: + Host: localhost + DB: {dbConfig.dbName} + User: {dbConfig.user} + Pass: {dbConfig.pass} + + + ) : ( + <> + + Hata Oluştu! + {importResult.message} + + {logs.slice(-5).map((l, i) => {l})} + + + )} + + )} + + ) + default: + return 'Bilinmeyen adım' + } + } + + return ( + + MySQL Hızlı Dump/Import Sihirbazı + + + {steps.map((label) => ( + + {label} + + ))} + + + {renderStepContent(activeStep)} + + + + + + {activeStep > 0 && activeStep < 3 && !importResult && ( + + )} + {activeStep === 0 && ( + + )} + {activeStep === 1 && ( + + )} + {activeStep === 2 && ( + + )} + {activeStep === 3 && !importResult && ( + + )} + + + ) +} + +export default MySqlImportWizard