@@ -249,8 +249,24 @@ def deleteDatabase(dbname, dbuser):
249249 return str (msg )
250250
251251 @staticmethod
252- def createDatabaseBackup (databaseName , tempStoragePath , rustic = 0 , RusticRepoName = None , externalApp = None ):
252+ def createDatabaseBackup (databaseName , tempStoragePath , rustic = 0 , RusticRepoName = None ,
253+ externalApp = None , use_compression = None , use_new_features = None ):
254+ """
255+ Enhanced database backup with backward compatibility
256+
257+ Parameters:
258+ - use_compression: None (auto-detect), True (force compression), False (no compression)
259+ - use_new_features: None (auto-detect based on config), True/False (force)
260+ """
253261 try :
262+ # Check if new features are enabled (via config file or parameter)
263+ if use_new_features is None :
264+ use_new_features = mysqlUtilities .checkNewBackupFeatures ()
265+
266+ # Determine compression based on config or parameter
267+ if use_compression is None :
268+ use_compression = mysqlUtilities .shouldUseCompression ()
269+
254270 passFile = "/etc/cyberpanel/mysqlPassword"
255271
256272 try :
@@ -291,53 +307,57 @@ def createDatabaseBackup(databaseName, tempStoragePath, rustic=0, RusticRepoName
291307 SHELL = False
292308
293309 if rustic == 0 :
310+ # Determine backup file extension based on compression
311+ if use_compression :
312+ backup_extension = '.sql.gz'
313+ backup_file = f"{ tempStoragePath } /{ databaseName } { backup_extension } "
314+ else :
315+ backup_extension = '.sql'
316+ backup_file = f"{ tempStoragePath } /{ databaseName } { backup_extension } "
294317
295- command = 'rm -f ' + tempStoragePath + "/" + databaseName + '.sql'
318+ # Remove old backup if exists
319+ command = f'rm -f { backup_file } '
296320 ProcessUtilities .executioner (command )
297321
298- command = 'mysqldump --defaults-file=/home/cyberpanel/.my.cnf -u %s --host=%s --port %s %s' % (mysqluser , mysqlhost , mysqlport , databaseName )
299-
300- # if os.path.exists(ProcessUtilities.debugPath):
301- # logging.CyberCPLogFileWriter.writeToFile(command)
302- #
303- # logging.CyberCPLogFileWriter.writeToFile(f'Get current executing uid {os.getuid()}')
304- #
305- # cmd = shlex.split(command)
306- #
307- # try:
308- # errorPath = '/home/cyberpanel/error-logs.txt'
309- # errorLog = open(errorPath, 'a')
310- # with open(tempStoragePath + "/" + databaseName + '.sql', 'w') as f:
311- # res = subprocess.call(cmd, stdout=f, stderr=errorLog, shell=SHELL)
312- # if res != 0:
313- # logging.CyberCPLogFileWriter.writeToFile(
314- # "Database: " + databaseName + "could not be backed! [createDatabaseBackup]")
315- # return 0
316- # except subprocess.CalledProcessError as msg:
317- # logging.CyberCPLogFileWriter.writeToFile(
318- # "Database: " + databaseName + "could not be backed! Error: %s. [createDatabaseBackup]" % (
319- # str(msg)))
320- # return 0
321-
322- cmd = shlex .split (command )
323-
324- with open (tempStoragePath + "/" + databaseName + '.sql' , 'w' ) as f :
325- # Using subprocess.run to capture stdout and stderr
326- result = subprocess .run (
327- cmd ,
328- stdout = f ,
329- stderr = subprocess .PIPE ,
330- shell = SHELL
331- )
322+ # Build mysqldump command with new features
323+ dump_cmd = mysqlUtilities .buildMysqldumpCommand (
324+ mysqluser , mysqlhost , mysqlport , databaseName ,
325+ use_new_features , use_compression
326+ )
332327
333- # Check if the command was successful
334- if result .returncode != 0 :
328+ if use_compression :
329+ # New method: Stream directly to compressed file
330+ full_command = f"{ dump_cmd } | gzip -c > { backup_file } "
331+ result = ProcessUtilities .executioner (full_command , shell = True )
332+
333+ if result != 0 :
335334 logging .CyberCPLogFileWriter .writeToFile (
336- "Database: " + databaseName + " could not be backed up! [createDatabaseBackup]"
335+ f "Database: { databaseName } could not be backed up (compressed) ! [createDatabaseBackup]"
337336 )
338- # Log stderr
339- logging .CyberCPLogFileWriter .writeToFile (result .stderr .decode ('utf-8' ))
340337 return 0
338+ else :
339+ # Legacy method: Direct dump to file (backward compatible)
340+ cmd = shlex .split (dump_cmd )
341+
342+ with open (backup_file , 'w' ) as f :
343+ result = subprocess .run (
344+ cmd ,
345+ stdout = f ,
346+ stderr = subprocess .PIPE ,
347+ shell = SHELL
348+ )
349+
350+ if result .returncode != 0 :
351+ logging .CyberCPLogFileWriter .writeToFile (
352+ "Database: " + databaseName + " could not be backed up! [createDatabaseBackup]"
353+ )
354+ logging .CyberCPLogFileWriter .writeToFile (result .stderr .decode ('utf-8' ))
355+ return 0
356+
357+ # Store metadata about backup format for restore
358+ mysqlUtilities .saveBackupMetadata (
359+ databaseName , tempStoragePath , use_compression , use_new_features
360+ )
341361
342362 else :
343363 SHELL = True
@@ -369,6 +389,9 @@ def createDatabaseBackup(databaseName, tempStoragePath, rustic=0, RusticRepoName
369389
370390 @staticmethod
371391 def restoreDatabaseBackup (databaseName , tempStoragePath , dbPassword , passwordCheck = None , additionalName = None , rustic = 0 , RusticRepoName = None , externalApp = None , snapshotid = None ):
392+ """
393+ Enhanced restore with automatic format detection
394+ """
372395 try :
373396 passFile = "/etc/cyberpanel/mysqlPassword"
374397
@@ -409,24 +432,60 @@ def restoreDatabaseBackup(databaseName, tempStoragePath, dbPassword, passwordChe
409432 subprocess .call (shlex .split (command ))
410433
411434 if rustic == 0 :
435+ # Auto-detect backup format
436+ backup_format = mysqlUtilities .detectBackupFormat (
437+ tempStoragePath , databaseName , additionalName
438+ )
412439
413- command = 'mysql --defaults-file=/home/cyberpanel/.my.cnf -u %s --host=%s --port %s %s' % (mysqluser , mysqlhost , mysqlport , databaseName )
414- if os .path .exists (ProcessUtilities .debugPath ):
415- logging .CyberCPLogFileWriter .writeToFile (f'{ command } { tempStoragePath } /{ databaseName } ' )
416- cmd = shlex .split (command )
417-
418- if additionalName == None :
419- with open (tempStoragePath + "/" + databaseName + '.sql' , 'r' ) as f :
420- res = subprocess .call (cmd , stdin = f )
421- if res != 0 :
422- logging .CyberCPLogFileWriter .writeToFile ("Could not restore MYSQL database: " + databaseName + "! [restoreDatabaseBackup]" )
440+ if additionalName :
441+ base_name = additionalName
442+ else :
443+ base_name = databaseName
444+
445+ # Determine actual backup file based on detected format
446+ if backup_format ['compressed' ]:
447+ backup_file = f"{ tempStoragePath } /{ base_name } .sql.gz"
448+ if not os .path .exists (backup_file ):
449+ # Fallback to uncompressed for backward compatibility
450+ backup_file = f"{ tempStoragePath } /{ base_name } .sql"
451+ backup_format ['compressed' ] = False
452+ else :
453+ backup_file = f"{ tempStoragePath } /{ base_name } .sql"
454+ if not os .path .exists (backup_file ):
455+ # Try compressed version
456+ backup_file = f"{ tempStoragePath } /{ base_name } .sql.gz"
457+ if os .path .exists (backup_file ):
458+ backup_format ['compressed' ] = True
459+
460+ if not os .path .exists (backup_file ):
461+ logging .CyberCPLogFileWriter .writeToFile (
462+ f"Backup file not found: { backup_file } "
463+ )
464+ return 0
465+
466+ # Build restore command
467+ mysql_cmd = f'mysql --defaults-file=/home/cyberpanel/.my.cnf -u { mysqluser } --host={ mysqlhost } --port { mysqlport } { databaseName } '
468+
469+ if backup_format ['compressed' ]:
470+ # Handle compressed backup
471+ restore_cmd = f"gunzip -c { backup_file } | { mysql_cmd } "
472+ result = ProcessUtilities .executioner (restore_cmd , shell = True )
473+
474+ if result != 0 :
475+ logging .CyberCPLogFileWriter .writeToFile (
476+ f"Could not restore database: { databaseName } from compressed backup"
477+ )
423478 return 0
424479 else :
425- with open (tempStoragePath + "/" + additionalName + '.sql' , 'r' ) as f :
426- res = subprocess .call (cmd , stdin = f )
480+ # Handle uncompressed backup (legacy)
481+ cmd = shlex .split (mysql_cmd )
482+ with open (backup_file , 'r' ) as f :
483+ result = subprocess .call (cmd , stdin = f )
427484
428- if res != 0 :
429- logging .CyberCPLogFileWriter .writeToFile ("Could not restore MYSQL database: " + additionalName + "! [restoreDatabaseBackup]" )
485+ if result != 0 :
486+ logging .CyberCPLogFileWriter .writeToFile (
487+ f"Could not restore database: { databaseName } "
488+ )
430489 return 0
431490
432491 if passwordCheck == None :
@@ -449,6 +508,8 @@ def restoreDatabaseBackup(databaseName, tempStoragePath, dbPassword, passwordChe
449508 logging .CyberCPLogFileWriter .writeToFile (f'{ command } { tempStoragePath } /{ databaseName } ' )
450509 ProcessUtilities .outputExecutioner (command , None , True )
451510
511+ return 1
512+
452513 except BaseException as msg :
453514 logging .CyberCPLogFileWriter .writeToFile (str (msg ) + "[restoreDatabaseBackup]" )
454515 return 0
@@ -1220,6 +1281,153 @@ def UpgradeMariaDB(versionToInstall, tempStatusPath):
12201281
12211282 logging .CyberCPLogFileWriter .statusWriter (tempStatusPath , 'Completed [200]' )
12221283
1284+ @staticmethod
1285+ def buildMysqldumpCommand (user , host , port , database , use_new_features , use_compression ):
1286+ """Build mysqldump command with appropriate options"""
1287+
1288+ base_cmd = f"mysqldump --defaults-file=/home/cyberpanel/.my.cnf -u { user } --host={ host } --port { port } "
1289+
1290+ # Add new performance features if enabled
1291+ if use_new_features :
1292+ # Add single-transaction for InnoDB consistency
1293+ base_cmd += " --single-transaction"
1294+
1295+ # Add extended insert for better performance
1296+ base_cmd += " --extended-insert"
1297+
1298+ # Add order by primary for consistent dumps
1299+ base_cmd += " --order-by-primary"
1300+
1301+ # Add quick option to avoid loading entire result set
1302+ base_cmd += " --quick"
1303+
1304+ # Add lock tables option
1305+ base_cmd += " --lock-tables=false"
1306+
1307+ # Check MySQL version for parallel support
1308+ if mysqlUtilities .supportParallelDump ():
1309+ # Get number of threads (max 4 for safety)
1310+ threads = min (4 , ProcessUtilities .getNumberOfCores () if hasattr (ProcessUtilities , 'getNumberOfCores' ) else 2 )
1311+ base_cmd += f" --parallel={ threads } "
1312+
1313+ base_cmd += f" { database } "
1314+ return base_cmd
1315+
1316+ @staticmethod
1317+ def saveBackupMetadata (database , path , compressed , new_features ):
1318+ """Save metadata about backup format for restore compatibility"""
1319+ import time
1320+
1321+ metadata = {
1322+ 'database' : database ,
1323+ 'compressed' : compressed ,
1324+ 'new_features' : new_features ,
1325+ 'backup_version' : '2.0' if new_features else '1.0' ,
1326+ 'timestamp' : time .time ()
1327+ }
1328+
1329+ metadata_file = f"{ path } /{ database } .backup.json"
1330+ with open (metadata_file , 'w' ) as f :
1331+ json .dump (metadata , f )
1332+
1333+ @staticmethod
1334+ def detectBackupFormat (path , database , additional_name = None ):
1335+ """
1336+ Detect backup format from metadata or file extension
1337+ """
1338+ base_name = additional_name if additional_name else database
1339+
1340+ # First try to read metadata file (new backups will have this)
1341+ metadata_file = f"{ path } /{ base_name } .backup.json"
1342+ if os .path .exists (metadata_file ):
1343+ try :
1344+ with open (metadata_file , 'r' ) as f :
1345+ return json .load (f )
1346+ except :
1347+ pass
1348+
1349+ # Fallback: detect by file existence and extension
1350+ format_info = {
1351+ 'compressed' : False ,
1352+ 'new_features' : False ,
1353+ 'backup_version' : '1.0'
1354+ }
1355+
1356+ # Check for compressed file
1357+ if os .path .exists (f"{ path } /{ base_name } .sql.gz" ):
1358+ format_info ['compressed' ] = True
1359+ # Compressed backups likely use new features
1360+ format_info ['new_features' ] = True
1361+ format_info ['backup_version' ] = '2.0'
1362+ elif os .path .exists (f"{ path } /{ base_name } .sql" ):
1363+ format_info ['compressed' ] = False
1364+ # Check file content for new features indicators
1365+ format_info ['new_features' ] = mysqlUtilities .checkSQLFileFeatures (
1366+ f"{ path } /{ base_name } .sql"
1367+ )
1368+
1369+ return format_info
1370+
1371+ @staticmethod
1372+ def checkNewBackupFeatures ():
1373+ """Check if new backup features are enabled"""
1374+ try :
1375+ config_file = '/usr/local/CyberCP/plogical/backup_config.json'
1376+ if not os .path .exists (config_file ):
1377+ # Try alternate location
1378+ config_file = '/etc/cyberpanel/backup_config.json'
1379+
1380+ if os .path .exists (config_file ):
1381+ with open (config_file , 'r' ) as f :
1382+ config = json .load (f )
1383+ return config .get ('database_backup' , {}).get ('use_new_features' , False )
1384+ except :
1385+ pass
1386+ return False # Default to legacy mode for safety
1387+
1388+ @staticmethod
1389+ def shouldUseCompression ():
1390+ """Check if compression should be used"""
1391+ try :
1392+ config_file = '/usr/local/CyberCP/plogical/backup_config.json'
1393+ if not os .path .exists (config_file ):
1394+ # Try alternate location
1395+ config_file = '/etc/cyberpanel/backup_config.json'
1396+
1397+ if os .path .exists (config_file ):
1398+ with open (config_file , 'r' ) as f :
1399+ config = json .load (f )
1400+ return config .get ('database_backup' , {}).get ('use_compression' , False )
1401+ except :
1402+ pass
1403+ return False # Default to no compression for compatibility
1404+
1405+ @staticmethod
1406+ def supportParallelDump ():
1407+ """Check if MySQL version supports parallel dump"""
1408+ try :
1409+ result = ProcessUtilities .outputExecutioner ("mysql --version" )
1410+ # MySQL 8.0+ and MariaDB 10.3+ support parallel dump
1411+ if "8.0" in result or "8.1" in result or "10.3" in result or "10.4" in result or "10.5" in result or "10.6" in result :
1412+ return True
1413+ except :
1414+ pass
1415+ return False
1416+
1417+ @staticmethod
1418+ def checkSQLFileFeatures (file_path ):
1419+ """Check SQL file for new feature indicators"""
1420+ try :
1421+ # Read first few lines to check for new features
1422+ with open (file_path , 'r' ) as f :
1423+ head = f .read (2048 ) # Read first 2KB
1424+ # Check for indicators of new features
1425+ if "--single-transaction" in head or "--extended-insert" in head or "-- Dump completed" in head :
1426+ return True
1427+ except :
1428+ pass
1429+ return False
1430+
12231431
12241432def main ():
12251433 parser = argparse .ArgumentParser (description = 'CyberPanel' )
0 commit comments