diff --git a/www/api/controllers/backups.php b/www/api/controllers/backups.php index fc302939b..c595fb283 100644 --- a/www/api/controllers/backups.php +++ b/www/api/controllers/backups.php @@ -345,6 +345,7 @@ function process_jsonbackup_file_data_helper($json_config_backup_Data, $source_d //process each of the backups and read out the backup comment, and work out the date it was created foreach ($json_config_backup_Data as $backup_filename => $backup_data) { $backup_data_comment = ''; + $backup_data_trigger_source = null; $backup_alternative = false; $backup_filepath = $source_directory; //Check to see if the source direct is the same as the default or not, if it is't then the the directory is the alternative backup directory (USB or something) @@ -359,6 +360,9 @@ function process_jsonbackup_file_data_helper($json_config_backup_Data, $source_d if (array_key_exists('backup_comment', $decoded_backup_data)) { $backup_data_comment = $decoded_backup_data['backup_comment']; } + if (array_key_exists('backup_trigger_source', $decoded_backup_data)) { + $backup_data_trigger_source = $decoded_backup_data['backup_trigger_source']; + } //Locate the last underscore, this appears before the date/time in the filename $backup_date_time_pos = strrpos($backup_filename_clean, '_'); @@ -372,6 +376,7 @@ function process_jsonbackup_file_data_helper($json_config_backup_Data, $source_d 'backup_filedirectory' => $backup_filepath, 'backup_filename' => $backup_filename, 'backup_comment' => $backup_data_comment, + 'backup_trigger_source' => $backup_data_trigger_source, 'backup_time' => $backup_date_time, 'backup_time_unix' => $backup_date_time_unix ); @@ -397,8 +402,23 @@ function MakeJSONBackup() $fpp_backup_max_age, $fpp_backup_min_number_kept, $fpp_backup_location, $fpp_backup_location_alternate_drive; - //Get the backup comment out of the post data - $backup_comment = file_get_contents('php://input'); + $trigger_source = null; + + //Get data out of the post data + $input_data = file_get_contents('php://input'); + + //Try JSON decode the input in case it's an array with extra data + $input_data_decoded = json_decode($input_data, true); + + if (json_last_error() === JSON_ERROR_NONE) { + // JSON is valid, get comment and trigger source + $backup_comment = $input_data_decoded['backup_comment']; + $trigger_source = $input_data_decoded['trigger_source']; + } else { + //It's just a string so it'll be just a comment + //Get the backup comment out of the post data + $backup_comment = $input_data; + } $toUSBResult = false; @@ -406,7 +426,7 @@ function MakeJSONBackup() require_once "../backup.php"; //Create the backup and return the status - $backup_creation_status = performBackup('all', false, $backup_comment); + $backup_creation_status = performBackup('all', false, $backup_comment, $trigger_source); if (array_key_exists('success', $backup_creation_status) && $backup_creation_status['success'] == true) { $toUSBResult = DoJsonBackupToUSB(); diff --git a/www/api/controllers/configfile.php b/www/api/controllers/configfile.php index 5304ca583..45527d348 100644 --- a/www/api/controllers/configfile.php +++ b/www/api/controllers/configfile.php @@ -89,7 +89,7 @@ function UploadConfigFile() fclose($f); //Trigger a JSON Configuration Backup - GenerateBackupViaAPI('Config File ' . $baseFile . ' was uploaded/modified.'); + GenerateBackupViaAPI('Config File ' . $baseFile . ' was uploaded/modified.', 'config_file/' . $baseFile); $result['Status'] = 'OK'; $result['Message'] = ''; diff --git a/www/backup.php b/www/backup.php index 78dbfe4fb..facd03fd8 100644 --- a/www/backup.php +++ b/www/backup.php @@ -130,10 +130,12 @@ function checkDirectScriptExecution() ///////////////////// /// HOUSE KEEPING /// -//Max age for stored settings backup files, Default: 90 days +//NOT USED - Max age for stored settings backup files, Default: 90 days $fpp_backup_max_age = 90; -//Minimum number of backups we want to keep, this works with max age to ensure we have at least some backups left and not wipe out all of them if they were all old. Default: 45 -$fpp_backup_min_number_kept = 45; +//Max number of backup files we want to keep Default: 45 +$fpp_backup_min_number_kept = 60; +//Max number of backup per plugin or by trigger source (eg. /api/config_file/someconfig all backup generated via this endpoint will have a trigger source of config_file/someconfig we'll limit those backups to the below, default: 5 +$fpp_backup_max_plugin_backups_key = 5; //////////////////// /// DEBUGGING @@ -1793,8 +1795,9 @@ function SavePixelnetDMXFile_F16v2Alpha($restore_data) * @param string $area Backup area to be backed up, refer $system_config_areas for the currently defined list of area * @param bool $allowDownload Toggle to allow or disallow the backup file being sent to the users browser * @param string $backupComment Optionally add a comment to the backup file that may be useful to describe what the backup is for + * @param string $backupTriggerSource Optionally add where/what triggered the backup */ -function performBackup($area = "all", $allowDownload = true, $backupComment = "User Initiated Manual Backup") +function performBackup($area = "all", $allowDownload = true, $backupComment = "User Initiated Manual Backup", $backupTriggerSource = null) { global $fpp_backup_version, $system_config_areas, $protectSensitiveData, $known_json_config_files, $known_ini_config_files; @@ -2076,6 +2079,8 @@ function performBackup($area = "all", $allowDownload = true, $backupComment = "U //Add the backup comment into settings data that has been gathered $tmp_settings_data['backup_comment'] = $backupComment; + //Add the trigger source also, this may be used internally when clearing/manging number of backup files + $tmp_settings_data['backup_trigger_source'] = $backupTriggerSource; //Lastly insert the backup system version (moved from doBackupDownload to here) $tmp_settings_data['fpp_backup_version'] = $fpp_backup_version; //Add the current UNIX epoc time, representing the time the backup as taken, may make it easier in the future to calculate when the backup was taken @@ -2343,15 +2348,18 @@ function changeLocalFPPBackupFileLocation($new_location) */ function pruneOrRemoveAgedBackupFiles() { - global $fpp_backup_max_age, $fpp_backup_min_number_kept, $fpp_backup_location, $backups_verbose_logging; + global $fpp_backup_max_age, $fpp_backup_min_number_kept, $fpp_backup_max_plugin_backups_key, $fpp_backup_location, $backups_verbose_logging; //gets all the files in the configs/backup directory via the API now since it will look in multiple areas $config_dir_files = file_get_contents('http://localhost/api/backups/configuration/list'); $config_dir_files = json_decode($config_dir_files, true); + $config_dir_files_tmp = $config_dir_files; + $backups_to_delete = array(); + $num_backups_deleted = 0; //If the number of backup files that exist IS LESS than what the minimum we want to keep, return and stop processing if (count($config_dir_files) < $fpp_backup_min_number_kept) { - $aged_backup_removal_message = "SETTINGS BACKUP: NOT removing JSON Settings backup files older than $fpp_backup_max_age days. Since there are (" . count($config_dir_files) . ") backups available and this is less than the minimum backups we want to keep ($fpp_backup_min_number_kept) \r\n"; + $aged_backup_removal_message = "SETTINGS BACKUP: NOT removing JSON Settings backup files. Since there are (" . count($config_dir_files) . ") backups available and this is less than the minimum backups we want to keep ($fpp_backup_min_number_kept) \r\n"; error_log($aged_backup_removal_message); return; } @@ -2360,10 +2368,46 @@ function pruneOrRemoveAgedBackupFiles() // $config_dir_files = array_reverse($config_dir_files); //Select the backups that lie outside what we want to keep, because the array is a list of backups with the newest first then we'll be selecting older/dated backups - $backups_to_delete = array_slice($config_dir_files, $fpp_backup_min_number_kept); - $num_backups_deleted = 0; + $plugin_backups_to_delete_tmp = array(); + + //Look through all the backups, select ones with a trigger source, because the backups are newest first, we'll be selecting the file at the position of our max limit through to the end of the list + $backups_by_trigger_source = array(); + foreach ($config_dir_files as $backups_by_trigger_source_index => $backups_by_trigger_source_meta_data) { + $backup_filename = $backups_by_trigger_source_meta_data['backup_filename']; + + if ((stripos(strtolower($backup_filename), "-backup_") !== false) && (stripos(strtolower($backup_filename), ".json") !== false)) { + // + $backup_comment = $backups_by_trigger_source_meta_data['backup_comment']; + if (array_key_exists('backup_trigger_source', $backups_by_trigger_source_meta_data) && $backups_by_trigger_source_meta_data['backup_trigger_source'] !== null) { + //Get the trigger source name & add it and the backup meta data to a temp array, this will group like sources together + //then we'll limit those lists to enforce our backup limit + $backup_trigger_source = $backups_by_trigger_source_meta_data['backup_trigger_source']; + $backups_by_trigger_source[$backup_trigger_source][] = $backups_by_trigger_source_meta_data; + + //Check the number of items for this trigger source, if it's > than our allowed limit then add the backup file to the $backups_to_delete list + if (count($backups_by_trigger_source[$backup_trigger_source]) > $fpp_backup_max_plugin_backups_key) { + $plugin_backups_to_delete_tmp[] = $backups_by_trigger_source_meta_data; + //Remove the entry from the list of backup files, because it will be deleted (we want this list to reflect the list after enforcing plugin backup limits) + //so that we can enforce the global limit after + unset($config_dir_files_tmp[$backups_by_trigger_source_index]); + } + } + } + } + + //Fix indexes + $config_dir_files_tmp = array_values($config_dir_files_tmp); + + if (count($config_dir_files_tmp) > $fpp_backup_min_number_kept) { + $backups_to_delete = array_slice($config_dir_files_tmp, $fpp_backup_min_number_kept); + } + + //tack on the plugin backups to be deleted + foreach ($plugin_backups_to_delete_tmp as $plugin_backups_to_delete_tmp_data) { + $backups_to_delete[] = $plugin_backups_to_delete_tmp_data; + } - //loop over the backup files we've found + //loop over the backup files we've found that are beyond our set limit, all these to be deleted foreach ($backups_to_delete as $backup_file_index => $backup_file_meta_data) { $backup_filename = $backup_file_meta_data['backup_filename']; diff --git a/www/common.php b/www/common.php index fe2ba5848..9f8636fbb 100644 --- a/www/common.php +++ b/www/common.php @@ -2508,12 +2508,17 @@ function read_directory_files($directory, $return_data = true, $sort_by_date = f /** * Makes a POST Call to the api/backups/configuration to generate a JSON Configuration backup with a option comment * @param $backup_comment string Optional Comment that will be inserted into the JSON backup file + * @param $trigger_source string Optional Source that triggered the backup, * @return bool */ -function GenerateBackupViaAPI($backup_comment = "Created via API") +function GenerateBackupViaAPI($backup_comment = "Created via API", $trigger_source = null) { $url = 'http://localhost/api/backups/configuration'; - $data = array($backup_comment); + $data = $backup_comment; + + if (!is_null($trigger_source)) { + $data = json_encode(array('backup_comment' => $backup_comment, 'trigger_source' => $trigger_source)); + } //https://stackoverflow.com/questions/5647461/how-do-i-send-a-post-request-with-php // use key 'http' even if you send the request to https://... @@ -2521,7 +2526,7 @@ function GenerateBackupViaAPI($backup_comment = "Created via API") 'http' => array( 'header' => "Content-type: text/plain", 'method' => 'POST', - 'content' => $backup_comment, + 'content' => $data, ), ); $context = stream_context_create($options);