feat: add upload_backup function for validating and saving backup files

This commit is contained in:
Christian Krakau-Louis
2025-06-09 15:58:35 +02:00
parent 9caf9ec791
commit 31e445ab9c
+181
View File
@@ -721,4 +721,185 @@ def apply_retention_policy(retention_days=30):
"message": f"Error applying retention policy: {str(e)}", "message": f"Error applying retention policy: {str(e)}",
"deleted_count": 0, "deleted_count": 0,
"deleted_backups": [] "deleted_backups": []
}
def upload_backup(file):
"""
Upload and validate a backup file to the system
Args:
file: A FileStorage object from Flask's request.files
Returns:
dict: Operation status and information about the uploaded backup
"""
try:
# Ensure backup directory exists
backup_dir = os.path.join('/data', 'backups')
os.makedirs(backup_dir, exist_ok=True)
# Generate a unique filename with timestamp
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
# Save the file with original filename but prefixed with timestamp
# This preserves the original filename but ensures uniqueness
secure_filename = file.filename.replace(' ', '_')
save_filename = f"{timestamp}_{secure_filename}"
save_path = os.path.join(backup_dir, save_filename)
# Save the uploaded file
file.save(save_path)
logger.info(f"Uploaded backup saved to {save_path}")
# Validate the backup file - check if it's a valid zip file
if not zipfile.is_zipfile(save_path):
# Clean up invalid file
if os.path.exists(save_path):
os.remove(save_path)
return {
"status": "error",
"message": "Uploaded file is not a valid backup archive."
}
# Basic validation of backup contents
with zipfile.ZipFile(save_path, 'r') as zip_ref:
file_list = zip_ref.namelist()
# Check for essential files in the backup
if 'database.db' not in file_list and 'song_data.db' not in file_list:
# Clean up invalid backup
if os.path.exists(save_path):
os.remove(save_path)
return {
"status": "error",
"message": "Uploaded file is not a valid Quizzical Beats backup. No database found."
}
# Return success with file info
file_size = os.path.getsize(save_path)
readable_size = f"{file_size / (1024*1024):.2f} MB"
return {
"status": "success",
"message": f"Backup uploaded successfully ({readable_size}).",
"filename": save_filename,
"path": save_path,
"size": file_size,
"upload_time": timestamp
}
except Exception as e:
logger.error(f"Error uploading backup: {str(e)}")
return {
"status": "error",
"message": f"Error uploading backup: {str(e)}"
}
def update_ofelia_config(retention_days=30):
"""
Update the Ofelia scheduler configuration file based on current backup settings.
This creates or updates the config file that Ofelia uses for scheduling backups.
Args:
retention_days: Number of days of backups to keep (0 = keep all)
Returns:
dict: Operation status information and config details
"""
try:
from musicround.models import SystemSetting
import os
# Get backup schedule information
schedule_time = SystemSetting.get('backup_schedule_time', '03:00')
schedule_frequency = SystemSetting.get('backup_schedule_frequency', 'daily')
schedule_enabled = SystemSetting.get('backup_schedule_enabled', 'true') == 'true'
# Map schedule frequency to cron expressions
frequency_map = {
'hourly': '@hourly',
'daily': '@daily',
'weekly': '@weekly'
}
schedule_cron = frequency_map.get(schedule_frequency, '@daily')
# Determine the config file path - use environment variable or default
config_dir = os.environ.get('OFELIA_CONFIG_DIR', '/data/config')
os.makedirs(config_dir, exist_ok=True)
config_path = os.path.join(config_dir, 'ofelia.ini')
# Create the config content
config_content = f"""[global]
save-folder = /var/log/ofelia
[job-exec "backup"]
schedule = {schedule_cron}
command = python /app/run.py backup create --auto
user = root
no-overlap = true
[job-exec "retention"]
schedule = @weekly
command = python /app/run.py backup retention --days {retention_days}
user = root
no-overlap = true
"""
# If backup scheduling is disabled, add a comment at the top
if not schedule_enabled:
config_content = "# AUTOMATED BACKUPS DISABLED - Enable in system settings\n# Remove this comment to enable this configuration\n\n" + config_content
# Write the config file
with open(config_path, 'w') as f:
f.write(config_content)
logger.info(f"Updated Ofelia configuration at {config_path}")
# Also generate a docker-compose labels suggestion for documentation
docker_compose_suggestion = f"""labels:
ofelia.enabled: "true"
ofelia.job-exec.backup.schedule: "{schedule_cron}"
ofelia.job-exec.backup.command: "python /app/run.py backup create --auto"
ofelia.job-exec.backup.no-overlap: "true"
# Retention policy - automatically delete backups older than {retention_days} days
ofelia.job-exec.retention.schedule: "@weekly"
ofelia.job-exec.retention.command: "python /app/run.py backup retention --days {retention_days}"
ofelia.job-exec.retention.no-overlap: "true"
"""
# Generate instructions
instructions = f"""To use this Ofelia configuration:
1. Make sure the ofelia.ini file is accessible to the Ofelia scheduler
2. If using Docker Compose with the Ofelia sidecar pattern, update your docker-compose.yml file
with the labels shown below
3. Restart the application for changes to take effect
For containerized setups:
docker-compose down
docker-compose up -d
"""
return {
"status": "success",
"message": f"Updated Ofelia configuration at {config_path}",
"config_path": config_path,
"config_content": config_content,
"docker_labels": docker_compose_suggestion,
"instructions": instructions,
"schedule": {
"enabled": schedule_enabled,
"frequency": schedule_frequency,
"time": schedule_time,
"retention_days": retention_days
}
}
except Exception as e:
logger.error(f"Error updating Ofelia configuration: {str(e)}")
return {
"status": "error",
"message": f"Failed to update Ofelia configuration: {str(e)}",
"config_path": None,
"config_content": None
} }