feat: add upload_backup function for validating and saving backup files
This commit is contained in:
@@ -721,4 +721,185 @@ def apply_retention_policy(retention_days=30):
|
||||
"message": f"Error applying retention policy: {str(e)}",
|
||||
"deleted_count": 0,
|
||||
"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
|
||||
}
|
||||
Reference in New Issue
Block a user