@@ -721,7 +721,7 @@ protected static function clearBackups($server_id, $web_id, $max_backup_copies,
721721 }
722722
723723 /**
724- * Garbage collection: deletes records from database about files that do not exist and deletes untracked files.
724+ * Garbage collection: deletes records from database about files that do not exist and deletes untracked files and cleans up backup download directories .
725725 * The backup directory must be mounted before calling this method.
726726 * @param int $server_id
727727 * @param string|null $backup_type if defined then process only backups of this type
@@ -734,33 +734,38 @@ protected static function backups_garbage_collection($server_id, $backup_type =
734734 global $ app ;
735735
736736 //First check that all records in database have related files and delete records without files on disk
737- $ args = array ();
738- $ args_domains = array ();
737+ $ args_sql = array ();
738+ $ args_sql_domains = array ();
739+ $ args_sql_domains_with_backups = array ();
739740 $ server_config = $ app ->getconf ->get_server_config ($ server_id , 'server ' );
740741 $ backup_dir = trim ($ server_config ['backup_dir ' ]);
741742 $ sql = "SELECT * FROM web_backup WHERE server_id = ? " ;
742743 $ sql_domains = "SELECT domain_id,document_root,system_user,system_group,backup_interval FROM web_domain WHERE server_id = ? AND (type = 'vhost' OR type = 'vhostsubdomain' OR type = 'vhostalias') " ;
743- array_push ($ args , $ server_id );
744- array_push ($ args_domains , $ server_id );
744+ $ sql_domains_with_backups = "SELECT domain_id,document_root,system_user,system_group,backup_interval FROM web_domain WHERE domain_id in (SELECT parent_domain_id FROM web_backup WHERE server_id = ? " . ((!empty ($ backup_type )) ? " AND backup_type = ? " : "" ) . ") AND (type = 'vhost' OR type = 'vhostsubdomain' OR type = 'vhostalias') " ;
745+ array_push ($ args_sql , $ server_id );
746+ array_push ($ args_sql_domains , $ server_id );
747+ array_push ($ args_sql_domains_with_backups , $ server_id );
745748 if (!empty ($ backup_type )) {
746749 $ sql .= " AND backup_type = ? " ;
747- array_push ($ args , $ backup_type );
750+ array_push ($ args_sql , $ backup_type );
751+ array_push ($ args_sql_domains_with_backups , $ backup_type );
748752 }
749753 if (!empty ($ domain_id )) {
750754 $ sql .= " AND parent_domain_id = ? " ;
751755 $ sql_domains .= " AND domain_id = ? " ;
752- array_push ($ args , $ domain_id );
753- array_push ($ args_domains , $ domain_id );
756+ $ sql_domains_with_backups .= " AND domain_id = ? " ;
757+ array_push ($ args_sql , $ domain_id );
758+ array_push ($ args_sql_domains , $ domain_id );
759+ array_push ($ args_sql_domains_with_backups , $ domain_id );
754760 }
755- array_unshift ($ args , $ sql );
756- array_unshift ($ args_domains , $ sql_domains );
757761
758762 $ db_list = array ($ app ->db );
759763 if ($ app ->db ->dbHost != $ app ->dbmaster ->dbHost )
760764 array_push ($ db_list , $ app ->dbmaster );
761765
766+ // Cleanup web_backup entries for non-existent backup files
762767 foreach ($ db_list as $ db ) {
763- $ backups = call_user_func_array ( array ( $ db , " queryAllRecords " ) , $ args );
768+ $ backups = $ app -> db -> queryAllRecords ( $ sql , true , $ args_sql );
764769 foreach ($ backups as $ backup ) {
765770 $ backup_file = $ backup_dir . '/web ' . $ backup ['parent_domain_id ' ] . '/ ' . $ backup ['filename ' ];
766771 if (!is_file ($ backup_file )) {
@@ -771,27 +776,42 @@ protected static function backups_garbage_collection($server_id, $backup_type =
771776 }
772777 }
773778
774- foreach ($ db_list as $ db ) {
775- $ domains = call_user_func_array (array ($ db , "queryAllRecords " ), $ args_domains );
776- foreach ($ domains as $ rec ) {
777- $ domain_id = $ rec ['domain_id ' ];
778- $ domain_backup_dir = $ backup_dir . '/web ' . $ domain_id ;
779- $ files = self ::get_files ($ domain_backup_dir );
780-
781- //Delete files that are in backup directory, but do not exist in database
782- if (!empty ($ files )) {
783- $ sql = "SELECT backup_id,filename FROM web_backup WHERE server_id = ? AND parent_domain_id = ? " ;
784- $ backups = $ db ->queryAllRecords ($ sql , $ server_id , $ domain_id );
779+ // Cleanup backup files with missing web_backup entries (runs on all servers)
780+ $ domains = $ app ->dbmaster ->queryAllRecords ($ sql_domains_with_backups , true , $ args_sql_domains_with_backups );
781+ foreach ($ domains as $ rec ) {
782+ $ domain_id = $ rec ['domain_id ' ];
783+ $ domain_backup_dir = $ backup_dir . '/web ' . $ domain_id ;
784+ $ files = self ::get_files ($ domain_backup_dir );
785+
786+ if (!empty ($ files )) {
787+ // leave out server_id here, in case backup storage is shared between servers
788+ $ sql = "SELECT backup_id, filename FROM web_backup WHERE parent_domain_id = ? " ;
789+ $ untracked_backup_files = array ();
790+ foreach ($ db_list as $ db ) {
791+ $ backups = $ db ->queryAllRecords ($ sql , $ domain_id );
785792 foreach ($ backups as $ backup ) {
786793 if (!in_array ($ backup ['filename ' ],$ files )) {
787- $ backup_file = $ backup_dir . '/web ' . $ domain_id . '/ ' . $ backup ['filename ' ];
788- $ app ->log ('Backup file ' . $ backup_file . ' is not contained in database, deleting this file from disk ' , LOGLEVEL_DEBUG );
789- @unlink ($ backup_file );
794+ $ untracked_backup_files [] = $ backup ['filename ' ];
790795 }
791796 }
792797 }
798+ array_unique ( $ untracked_backup_files );
799+ foreach ($ untracked_backup_files as $ f ) {
800+ $ backup_file = $ backup_dir . '/web ' . $ domain_id . '/ ' . $ f ;
801+ $ app ->log ('Backup file ' . $ backup_file . ' is not contained in database, deleting this file from disk ' , LOGLEVEL_DEBUG );
802+ @unlink ($ backup_file );
803+ }
804+ }
805+ }
806+
807+ // This cleanup only runs on web servers
808+ $ domains = $ app ->db ->queryAllRecords ($ sql_domains , true , $ args_sql_domains );
809+ foreach ($ domains as $ rec ) {
810+ $ domain_id = $ rec ['domain_id ' ];
811+ $ domain_backup_dir = $ backup_dir . '/web ' . $ domain_id ;
793812
794- //Remove backupdir symlink and create as directory instead
813+ // Remove backupdir symlink and create as directory instead
814+ if (is_link ($ backup_download_dir ) || !is_dir ($ backup_download_dir )) {
795815 $ web_path = $ rec ['document_root ' ];
796816 $ app ->system ->web_folder_protection ($ web_path , false );
797817
@@ -806,23 +826,23 @@ protected static function backups_garbage_collection($server_id, $backup_type =
806826 }
807827
808828 $ app ->system ->web_folder_protection ($ web_path , true );
829+ }
809830
810- // delete old files from backup download dir (/var/www/example.com/backup)
811- if (is_dir ($ backup_download_dir )) {
812- $ dir_handle = dir ($ backup_download_dir );
813- $ now = time ();
814- while (false !== ($ entry = $ dir_handle ->read ())) {
815- $ full_filename = $ backup_download_dir . '/ ' . $ entry ;
816- if ($ entry != '. ' && $ entry != '.. ' && is_file ($ full_filename )) {
817- // delete files older than 3 days
818- if ($ now - filemtime ($ full_filename ) >= 60 * 60 * 24 * 3 ) {
819- $ app ->log ('Backup file ' . $ full_filename . ' is too old, deleting this file from disk ' , LOGLEVEL_DEBUG );
820- @unlink ($ full_filename );
821- }
831+ // delete old files from backup download dir (/var/www/example.com/backup)
832+ if (is_dir ($ backup_download_dir )) {
833+ $ dir_handle = dir ($ backup_download_dir );
834+ $ now = time ();
835+ while (false !== ($ entry = $ dir_handle ->read ())) {
836+ $ full_filename = $ backup_download_dir . '/ ' . $ entry ;
837+ if ($ entry != '. ' && $ entry != '.. ' && is_file ($ full_filename ) && ! is_link ($ full_filename )) {
838+ // delete files older than 3 days
839+ if ($ now - filemtime ($ full_filename ) >= 60 * 60 * 24 * 3 ) {
840+ $ app ->log ('Backup file ' . $ full_filename . ' is too old, deleting this file from disk ' , LOGLEVEL_DEBUG );
841+ @unlink ($ full_filename );
822842 }
823843 }
824- $ dir_handle ->close ();
825844 }
845+ $ dir_handle ->close ();
826846 }
827847 }
828848 }
@@ -897,16 +917,24 @@ protected static function get_files($directory, $prefix_list = null, $endings_li
897917 * Generates excludes list for compressors
898918 * @param string[] $backup_excludes
899919 * @param string $arg
920+ * @param string $pre
921+ * @param string $post
900922 * @return string
901923 * @author Ramil Valitov <ramilvalitov@gmail.com>
902924 */
903- protected static function generateExcludeList ($ backup_excludes , $ arg )
925+ protected static function generateExcludeList ($ backup_excludes , $ arg, $ pre = '' , $ post = '' )
904926 {
905- $ excludes = implode (" " . $ arg , $ backup_excludes );
906- if (!empty ($ excludes )) {
907- $ excludes = $ arg . $ excludes ;
927+ $ excludes = "" ;
928+ foreach ($ backup_excludes as $ ex ) {
929+ # pass through escapeshellarg if not already done
930+ if ( preg_match ( "/^'.+'$/ " , $ ex ) ) {
931+ $ excludes .= "$ {arg}$ {pre}$ {ex}$ {post} " ;
932+ } else {
933+ $ excludes .= "$ {arg}" . escapeshellarg ("$ {pre}$ {ex}$ {post}" ) . " " ;
934+ }
908935 }
909- return $ excludes ;
936+
937+ return trim ( $ excludes );
910938 }
911939
912940 /**
@@ -965,12 +993,14 @@ protected static function runWebCompression($format, $backup_excludes, $backup_m
965993 if (!empty ($ password )) {
966994 $ zip_options .= ' --password ' . escapeshellarg ($ password );
967995 }
996+ $ excludes = self ::generateExcludeList ($ backup_excludes , '-x ' );
997+ $ excludes .= " " . self ::generateExcludeList ($ backup_excludes , '-x ' , '' , '/* ' );
968998 if ($ backup_mode == 'user_zip ' ) {
969999 //Standard casual behaviour of ISPConfig
970- $ app ->system ->exec_safe ($ find_user_files . ' | zip ' . $ zip_options . ' -b ? ' . $ excludes . ' --symlinks ? -@' , $ web_path , $ web_user , $ web_group , $ http_server_user , $ backup_tmp , $ web_backup_dir . '/ ' . $ web_backup_file );
1000+ $ app ->system ->exec_safe ($ find_user_files . ' | zip ' . $ zip_options . ' -b ? --symlinks ? -@ ' . $ excludes , $ web_path , $ web_user , $ web_group , $ http_server_user , $ backup_tmp , $ web_backup_dir . '/ ' . $ web_backup_file );
9711001 } else {
972- //Use cd to have a correct directory structure inside the archive, extra options to zip hidden (dot) files
973- $ app ->system ->exec_safe ('cd ? && zip ' . $ zip_options . ' -b ? ' . $ excludes . ' --symlinks -r ? * .* -x "../*" ' , $ web_path , $ backup_tmp , $ web_backup_dir . '/ ' . $ web_backup_file );
1002+ //Use cd to have a correct directory structure inside the archive, zip current directory "." to include hidden (dot) files
1003+ $ app ->system ->exec_safe ('cd ? && zip ' . $ zip_options . ' -b ? --symlinks -r ? . ' . $ excludes , $ web_path , $ backup_tmp , $ web_backup_dir . '/ ' . $ web_backup_file );
9741004 }
9751005 $ exit_code = $ app ->system ->last_exec_retcode ();
9761006 // zip can return 12(due to harmless warnings) and still create valid backups
@@ -1215,8 +1245,8 @@ protected static function make_database_backup($web_domain, $backup_job)
12151245 //* Remove old backups
12161246 self ::backups_garbage_collection ($ server_id , 'mysql ' , $ domain_id );
12171247 $ prefix_list = array (
1218- ' db_ ' . escapeshellarg ( $ db_name). ' _ ' ,
1219- ' manual-db_ ' . escapeshellarg ( $ db_name). ' _ ' ,
1248+ " db_ $ { db_name} _ " ,
1249+ " manual-db_ $ { db_name} _ " ,
12201250 );
12211251 self ::clearBackups ($ server_id , $ domain_id , intval ($ rec ['backup_copies ' ]), $ db_backup_dir , $ prefix_list );
12221252 } else {
@@ -1291,9 +1321,8 @@ protected static function make_web_backup($web_domain, $backup_job)
12911321
12921322 # default exclusions
12931323 $ backup_excludes = array (
1294- escapeshellarg ( './backup\* ' ) ,
1324+ './backup* ' ,
12951325 './bin ' , './dev ' , './etc ' , './lib ' , './lib32 ' , './lib64 ' , './opt ' , './sys ' , './usr ' , './var ' , './proc ' , './run ' , './tmp ' ,
1296- './log ' ,
12971326 );
12981327
12991328 $ b_excludes = explode (', ' , trim ($ web_domain ['backup_excludes ' ]));
@@ -1419,7 +1448,7 @@ public static function run_backup($domain_id, $type, $backup_job, $mount = true)
14191448 $ ok = self ::make_web_backup ($ rec , $ backup_job );
14201449 break ;
14211450 case 'mysql ' :
1422- $ rec ['server_id ' ] = $ server_id ;
1451+ $ rec ['server_id ' ] = $ server_id ;
14231452 $ ok = self ::make_database_backup ($ rec , $ backup_job );
14241453 break ;
14251454 default :
@@ -1460,7 +1489,7 @@ public static function run_all_backups($server_id, $backup_job = "auto")
14601489 }
14611490 }
14621491
1463- $ sql = "SELECT DISTINCT d.*, db.server_id as `server_id` FROM web_database as db INNER JOIN web_domain as d ON (d.domain_id = db.parent_domain_id) WHERE db.server_id = ? AND db.active = 'y' AND d.backup_interval != 'none' AND d.backup_interval != '' " ;
1492+ $ sql = "SELECT DISTINCT d.*, db.server_id as `server_id` FROM web_database as db INNER JOIN web_domain as d ON (d.domain_id = db.parent_domain_id) WHERE db.server_id = ? AND db.active = 'y' AND d.backup_interval != 'none' AND d.backup_interval != '' " ;
14641493 $ databases = $ app ->dbmaster ->queryAllRecords ($ sql , $ server_id );
14651494
14661495 foreach ($ databases as $ database ) {
0 commit comments