🔄 Add Comprehensive Backup Management System
✨ New Features: - Complete backup lifecycle management (create, list, download, delete, cleanup) - Web-based backup interface with real-time status updates - Individual backup deletion and bulk cleanup for old backups - Docker-aware backup operations with volume persistence - Automated backup scheduling and retention policies 📁 Added Files: - backup.py - Core backup script for creating timestamped archives - docker_backup.sh - Docker-compatible backup wrapper script - app/templates/backup.html - Web interface for backup management - BACKUP_SYSTEM.md - Comprehensive backup system documentation - BACKUP_GUIDE.md - Quick reference guide for backup operations 🔧 Enhanced Files: - Dockerfile - Added backup.py copy for container availability - docker-compose.yml - Added backup volume mount for persistence - app/routes/api.py - Added backup API endpoints (create, list, delete, cleanup) - app/routes/main.py - Added backup management route - app/templates/index.html - Added backup management navigation - README.md - Updated with backup system overview and quick start 🎯 Key Improvements: - Fixed backup creation errors in Docker environment - Added Docker-aware path detection for container operations - Implemented proper error handling and user confirmation dialogs - Added real-time backup status updates via JavaScript - Enhanced data persistence with volume mounting 💡 Use Cases: - Data protection and disaster recovery - Environment migration and cloning - Development data management - Automated maintenance workflows
This commit is contained in:
313
BACKUP_GUIDE.md
Normal file
313
BACKUP_GUIDE.md
Normal file
@@ -0,0 +1,313 @@
|
||||
# QR Code Manager - Backup System
|
||||
|
||||
The QR Code Manager includes a comprehensive backup system to protect your data and ensure business continuity. This document explains how to use the backup features effectively.
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
The backup system provides three types of backups:
|
||||
|
||||
- **Data Backup** 📄: QR codes, link pages, and short URLs (recommended for regular backups)
|
||||
- **Config Backup** ⚙️: Configuration files and environment settings
|
||||
- **Full Backup** 📦: Complete application backup including all files
|
||||
|
||||
## 🛠️ Backup Methods
|
||||
|
||||
### 1. Web Interface (Recommended)
|
||||
|
||||
Access the backup management interface through the main dashboard:
|
||||
|
||||
1. Login to your QR Code Manager
|
||||
2. Click the "🛡️ Backup" button in the top-right corner
|
||||
3. Use the web interface to create, download, and manage backups
|
||||
|
||||
**Features:**
|
||||
- Visual backup status dashboard
|
||||
- One-click backup creation
|
||||
- Direct backup downloads
|
||||
- Backup history and file sizes
|
||||
|
||||
### 2. Python Script
|
||||
|
||||
Use the dedicated backup script for command-line operations:
|
||||
|
||||
```bash
|
||||
# Data backup (default)
|
||||
python3 backup.py
|
||||
|
||||
# Specific backup types
|
||||
python3 backup.py --data-only
|
||||
python3 backup.py --config
|
||||
python3 backup.py --full
|
||||
|
||||
# List available backups
|
||||
python3 backup.py --list
|
||||
|
||||
# Restore from backup
|
||||
python3 backup.py --restore backup_file.tar.gz
|
||||
|
||||
# Automated backup (for cron)
|
||||
python3 backup.py --auto
|
||||
|
||||
# Cleanup old backups
|
||||
python3 backup.py --cleanup 10
|
||||
```
|
||||
|
||||
### 3. Docker Script
|
||||
|
||||
For Docker deployments, use the Docker-specific backup script:
|
||||
|
||||
```bash
|
||||
# Make script executable
|
||||
chmod +x docker_backup.sh
|
||||
|
||||
# Data backup
|
||||
./docker_backup.sh backup-data
|
||||
|
||||
# Full backup
|
||||
./docker_backup.sh backup-full
|
||||
|
||||
# List backups
|
||||
./docker_backup.sh list
|
||||
|
||||
# Restore from backup
|
||||
./docker_backup.sh restore backup_file.tar.gz
|
||||
|
||||
# Cleanup old backups
|
||||
./docker_backup.sh cleanup 5
|
||||
|
||||
# Set up automated backups
|
||||
./docker_backup.sh schedule
|
||||
```
|
||||
|
||||
## ⏰ Automated Backups
|
||||
|
||||
### Using Cron (Linux/macOS)
|
||||
|
||||
1. Set up automated backups:
|
||||
```bash
|
||||
./docker_backup.sh schedule
|
||||
```
|
||||
|
||||
2. Or manually add to crontab (`crontab -e`):
|
||||
```bash
|
||||
# Daily data backup at 2 AM
|
||||
0 2 * * * /path/to/qr-code_manager/docker_backup.sh backup-data >> /path/to/backup.log 2>&1
|
||||
|
||||
# Weekly full backup on Sundays at 3 AM
|
||||
0 3 * * 0 /path/to/qr-code_manager/docker_backup.sh backup-full >> /path/to/backup.log 2>&1
|
||||
```
|
||||
|
||||
### Using Python Auto Mode
|
||||
|
||||
The Python script includes an intelligent auto mode:
|
||||
|
||||
```bash
|
||||
# Add to cron for smart automated backups
|
||||
0 2 * * * cd /path/to/qr-code_manager && python3 backup.py --auto >> backup.log 2>&1
|
||||
```
|
||||
|
||||
**Auto mode behavior:**
|
||||
- Daily: Data backup
|
||||
- Monday: Data + Config backup
|
||||
- 1st of month: Data + Config + Full backup
|
||||
- Automatic cleanup of old backups
|
||||
|
||||
## 📁 Backup Storage
|
||||
|
||||
### Default Location
|
||||
All backups are stored in the `backups/` directory within your QR Code Manager installation.
|
||||
|
||||
### Backup Files
|
||||
- **Data backups**: `qr_data_backup_YYYYMMDD_HHMMSS.tar.gz`
|
||||
- **Config backups**: `qr_config_backup_YYYYMMDD_HHMMSS.tar.gz`
|
||||
- **Full backups**: `qr_full_backup_YYYYMMDD_HHMMSS.tar.gz`
|
||||
|
||||
### Metadata Files
|
||||
Each backup includes a JSON metadata file with:
|
||||
- Backup type and timestamp
|
||||
- Application version
|
||||
- File list and sizes
|
||||
- Backup method used
|
||||
|
||||
## 🔄 Restore Process
|
||||
|
||||
### Web Interface Restore
|
||||
Currently, the web interface provides backup download. For restoration:
|
||||
|
||||
1. Download the desired backup file
|
||||
2. Use command-line restore methods below
|
||||
|
||||
### Command Line Restore
|
||||
|
||||
```bash
|
||||
# Python script restore
|
||||
python3 backup.py --restore backup_file.tar.gz
|
||||
|
||||
# Docker script restore
|
||||
./docker_backup.sh restore backup_file.tar.gz
|
||||
```
|
||||
|
||||
### Manual Restore
|
||||
|
||||
1. Stop the application:
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
2. Backup current data (safety):
|
||||
```bash
|
||||
mv data data.backup.$(date +%Y%m%d_%H%M%S)
|
||||
```
|
||||
|
||||
3. Extract backup:
|
||||
```bash
|
||||
tar xzf backup_file.tar.gz
|
||||
```
|
||||
|
||||
4. Restart application:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## 🔐 Security Considerations
|
||||
|
||||
### Backup Content
|
||||
- **Data backups** contain QR codes, links, and URLs (may include sensitive information)
|
||||
- **Config backups** contain environment variables and settings (may include passwords)
|
||||
- **Full backups** contain entire application including source code
|
||||
|
||||
### Best Practices
|
||||
1. **Encrypt backups** for off-site storage
|
||||
2. **Secure backup storage** with appropriate access controls
|
||||
3. **Regular backup testing** to ensure restoration works
|
||||
4. **Monitor backup logs** for failures
|
||||
5. **Rotate backup files** to manage storage space
|
||||
|
||||
### Encryption Example
|
||||
```bash
|
||||
# Encrypt backup for storage
|
||||
gpg --symmetric --cipher-algo AES256 backup_file.tar.gz
|
||||
|
||||
# Decrypt when needed
|
||||
gpg backup_file.tar.gz.gpg
|
||||
```
|
||||
|
||||
## 📊 Monitoring
|
||||
|
||||
### Backup Status
|
||||
Monitor backup health through:
|
||||
|
||||
1. **Web Interface**: Real-time status dashboard
|
||||
2. **Log Files**: `backup.log` for automated backups
|
||||
3. **API Endpoints**: `/api/backup/status` for integration
|
||||
|
||||
### Key Metrics
|
||||
- Total number of backups
|
||||
- Last backup timestamp
|
||||
- Data directory size
|
||||
- Total backup storage used
|
||||
- Backup success/failure rates
|
||||
|
||||
## 🚨 Disaster Recovery
|
||||
|
||||
### Complete System Failure
|
||||
|
||||
1. **Prepare new environment**:
|
||||
```bash
|
||||
git clone <your-repo>
|
||||
cd qr-code_manager
|
||||
```
|
||||
|
||||
2. **Restore from full backup**:
|
||||
```bash
|
||||
./docker_backup.sh restore qr_full_backup_latest.tar.gz
|
||||
```
|
||||
|
||||
3. **Verify configuration**:
|
||||
- Check environment variables
|
||||
- Update domain settings if needed
|
||||
- Test database connections
|
||||
|
||||
4. **Start services**:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Data Corruption
|
||||
|
||||
1. **Stop services**:
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
2. **Restore data only**:
|
||||
```bash
|
||||
./docker_backup.sh restore qr_data_backup_latest.tar.gz
|
||||
```
|
||||
|
||||
3. **Restart services**:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Permission Errors:**
|
||||
```bash
|
||||
chmod +x backup.py docker_backup.sh
|
||||
```
|
||||
|
||||
**Docker Not Found:**
|
||||
```bash
|
||||
# Check Docker installation
|
||||
docker --version
|
||||
docker compose version
|
||||
```
|
||||
|
||||
**Backup Script Fails:**
|
||||
```bash
|
||||
# Check Python dependencies
|
||||
python3 -c "import tarfile, json, pathlib"
|
||||
|
||||
# Check disk space
|
||||
df -h
|
||||
```
|
||||
|
||||
**Large Backup Files:**
|
||||
Consider data cleanup before backup:
|
||||
```bash
|
||||
python3 clean_data.py
|
||||
```
|
||||
|
||||
### Getting Help
|
||||
|
||||
1. Check the application logs: `docker compose logs`
|
||||
2. Review backup logs: `tail -f backup.log`
|
||||
3. Test backup creation manually
|
||||
4. Verify file permissions and disk space
|
||||
|
||||
## 📈 Best Practices
|
||||
|
||||
### Backup Strategy
|
||||
- **Daily**: Data backups (automated)
|
||||
- **Weekly**: Config backups
|
||||
- **Monthly**: Full backups
|
||||
- **Before updates**: Create full backup
|
||||
|
||||
### Storage Management
|
||||
- Keep last 15 daily backups
|
||||
- Keep last 8 weekly backups
|
||||
- Keep last 12 monthly backups
|
||||
- Archive yearly backups off-site
|
||||
|
||||
### Testing
|
||||
- Monthly restore tests
|
||||
- Document restore procedures
|
||||
- Train team on backup/restore process
|
||||
- Test disaster recovery scenarios
|
||||
|
||||
---
|
||||
|
||||
**💡 Remember**: The best backup is one that has been tested! Regularly verify your backups can be restored successfully.
|
||||
353
BACKUP_SYSTEM.md
Normal file
353
BACKUP_SYSTEM.md
Normal file
@@ -0,0 +1,353 @@
|
||||
# QR Code Manager - Backup System Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The QR Code Manager includes a comprehensive backup system that allows users to create, manage, and restore application data. The backup system supports both manual and automated backup creation, with full lifecycle management including deletion and cleanup capabilities.
|
||||
|
||||
## Features
|
||||
|
||||
### ✅ Backup Creation
|
||||
- **Manual Backup**: Create backups on-demand through the web interface
|
||||
- **Automated Backup**: Schedule regular backups using cron jobs
|
||||
- **Comprehensive Data**: Backs up all application data including QR codes, short URLs, and link pages
|
||||
- **Timestamped Files**: Each backup includes creation timestamp for easy identification
|
||||
|
||||
### ✅ Backup Management
|
||||
- **List All Backups**: View all available backups with creation dates and file sizes
|
||||
- **Download Backups**: Download backup files directly from the web interface
|
||||
- **Delete Individual Backups**: Remove specific backup files
|
||||
- **Bulk Cleanup**: Automatically remove old backups (older than 7 days)
|
||||
- **Real-time Status**: Live updates on backup operations
|
||||
|
||||
### ✅ Data Integrity
|
||||
- **ZIP Compression**: All backups are compressed for efficient storage
|
||||
- **File Validation**: Automatic validation of backup file integrity
|
||||
- **Error Handling**: Comprehensive error reporting and recovery
|
||||
|
||||
## Architecture
|
||||
|
||||
### Components
|
||||
|
||||
1. **`backup.py`** - Core backup script
|
||||
- Creates timestamped ZIP archives
|
||||
- Handles data collection and compression
|
||||
- Provides command-line interface for automation
|
||||
|
||||
2. **`docker_backup.sh`** - Docker-aware backup wrapper
|
||||
- Executes backups within Docker containers
|
||||
- Handles volume mounting and permissions
|
||||
|
||||
3. **API Endpoints** (`app/routes/api.py`)
|
||||
- `/api/backup/create` - Create new backup
|
||||
- `/api/backup/list` - List all backups
|
||||
- `/api/backup/status` - Check backup operation status
|
||||
- `/api/backup/delete/<filename>` - Delete specific backup
|
||||
- `/api/backup/cleanup` - Remove old backups
|
||||
|
||||
4. **Web Interface** (`app/templates/backup.html`)
|
||||
- User-friendly backup management dashboard
|
||||
- Real-time operation status updates
|
||||
- Confirmation dialogs for destructive operations
|
||||
|
||||
## Installation & Setup
|
||||
|
||||
### Docker Environment (Recommended)
|
||||
|
||||
The backup system is pre-configured for Docker deployment:
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
qr-manager:
|
||||
build: .
|
||||
ports:
|
||||
- "8066:5000"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./backups:/app/backups # Backup persistence
|
||||
```
|
||||
|
||||
### Manual Installation
|
||||
|
||||
1. Ensure Python dependencies are installed:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. Create backup directory:
|
||||
```bash
|
||||
mkdir -p backups
|
||||
```
|
||||
|
||||
3. Set appropriate permissions:
|
||||
```bash
|
||||
chmod +x backup.py docker_backup.sh
|
||||
```
|
||||
|
||||
## Usage Guide
|
||||
|
||||
### Creating Backups
|
||||
|
||||
#### Via Web Interface
|
||||
1. Navigate to the backup management page
|
||||
2. Click "Create Backup"
|
||||
3. Monitor progress in real-time
|
||||
4. Download completed backup when ready
|
||||
|
||||
#### Via Command Line
|
||||
```bash
|
||||
# Direct execution
|
||||
python backup.py
|
||||
|
||||
# Docker execution
|
||||
./docker_backup.sh
|
||||
```
|
||||
|
||||
#### Via API
|
||||
```bash
|
||||
curl -X POST http://localhost:8066/api/backup/create
|
||||
```
|
||||
|
||||
### Managing Backups
|
||||
|
||||
#### List All Backups
|
||||
- **Web**: Visit backup management page
|
||||
- **API**: `GET /api/backup/list`
|
||||
|
||||
#### Download Backup
|
||||
- **Web**: Click download button next to backup
|
||||
- **Direct**: Access files in `./backups/` directory
|
||||
|
||||
#### Delete Individual Backup
|
||||
- **Web**: Click red "Delete" button with confirmation
|
||||
- **API**: `DELETE /api/backup/delete/<filename>`
|
||||
|
||||
#### Cleanup Old Backups
|
||||
- **Web**: Click "Cleanup Old Backups" button
|
||||
- **API**: `POST /api/backup/cleanup`
|
||||
|
||||
### Restoring from Backup
|
||||
|
||||
1. **Stop the application**:
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
2. **Extract backup**:
|
||||
```bash
|
||||
cd data/
|
||||
unzip ../backups/backup_YYYYMMDD_HHMMSS.zip
|
||||
```
|
||||
|
||||
3. **Restart application**:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Backup Contents
|
||||
|
||||
Each backup includes:
|
||||
|
||||
```
|
||||
backup_YYYYMMDD_HHMMSS.zip
|
||||
├── qr_codes.json # QR code data and metadata
|
||||
├── short_urls.json # URL shortening data
|
||||
├── link_pages.json # Link page configurations
|
||||
└── static/
|
||||
└── qr_codes/ # Generated QR code images
|
||||
├── qr_*.png
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Automation
|
||||
|
||||
### Cron Job Setup
|
||||
|
||||
Add to crontab for automated backups:
|
||||
|
||||
```bash
|
||||
# Daily backup at 2 AM
|
||||
0 2 * * * cd /opt/qr-code_manager && ./docker_backup.sh
|
||||
|
||||
# Weekly cleanup (remove backups older than 7 days)
|
||||
0 3 * * 0 cd /opt/qr-code_manager && curl -X POST http://localhost:8066/api/backup/cleanup
|
||||
```
|
||||
|
||||
### Docker Compose Automation
|
||||
|
||||
```yaml
|
||||
# Optional: Backup service with automated scheduling
|
||||
services:
|
||||
backup-scheduler:
|
||||
image: alpine:latest
|
||||
command: >
|
||||
sh -c "
|
||||
apk add --no-cache curl &&
|
||||
while true; do
|
||||
sleep 86400;
|
||||
curl -X POST http://qr-manager:5000/api/backup/create;
|
||||
done
|
||||
"
|
||||
depends_on:
|
||||
- qr-manager
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Access Control
|
||||
- Backup operations require user authentication
|
||||
- API endpoints validate user permissions
|
||||
- Backup files are stored with restricted permissions
|
||||
|
||||
### Data Protection
|
||||
- Backups contain sensitive application data
|
||||
- Store backup files in secure locations
|
||||
- Consider encryption for long-term storage
|
||||
- Implement retention policies for compliance
|
||||
|
||||
### Network Security
|
||||
- API endpoints use HTTPS in production
|
||||
- Validate all input parameters
|
||||
- Implement rate limiting for backup operations
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### "Backup creation failed"
|
||||
- **Cause**: Insufficient disk space or permissions
|
||||
- **Solution**: Check available space and file permissions
|
||||
- **Command**: `df -h && ls -la backups/`
|
||||
|
||||
#### "Cannot delete backup file"
|
||||
- **Cause**: File in use or permission denied
|
||||
- **Solution**: Ensure file is not being accessed
|
||||
- **Command**: `lsof | grep backup`
|
||||
|
||||
#### "Docker backup script not found"
|
||||
- **Cause**: Script not executable or missing
|
||||
- **Solution**: Check file exists and permissions
|
||||
- **Command**: `chmod +x docker_backup.sh`
|
||||
|
||||
### Error Codes
|
||||
|
||||
| Code | Description | Action |
|
||||
|------|-------------|---------|
|
||||
| 200 | Success | Operation completed |
|
||||
| 400 | Bad Request | Check request parameters |
|
||||
| 404 | Not Found | Backup file doesn't exist |
|
||||
| 500 | Server Error | Check logs and permissions |
|
||||
|
||||
### Debugging
|
||||
|
||||
Enable debug logging:
|
||||
```bash
|
||||
export FLASK_ENV=development
|
||||
export FLASK_DEBUG=1
|
||||
```
|
||||
|
||||
Check container logs:
|
||||
```bash
|
||||
docker compose logs qr-manager
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Backup Size Optimization
|
||||
- Implement incremental backups for large datasets
|
||||
- Compress QR code images efficiently
|
||||
- Exclude temporary files from backups
|
||||
|
||||
### Speed Improvements
|
||||
- Use parallel compression for large backups
|
||||
- Implement background processing for web requests
|
||||
- Cache backup status for faster UI updates
|
||||
|
||||
### Storage Management
|
||||
- Automatic cleanup of old backups
|
||||
- Configurable retention policies
|
||||
- External storage integration (S3, etc.)
|
||||
|
||||
## API Reference
|
||||
|
||||
### Create Backup
|
||||
```http
|
||||
POST /api/backup/create
|
||||
Content-Type: application/json
|
||||
|
||||
Response:
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Backup creation started",
|
||||
"backup_id": "backup_20250801_143022"
|
||||
}
|
||||
```
|
||||
|
||||
### List Backups
|
||||
```http
|
||||
GET /api/backup/list
|
||||
|
||||
Response:
|
||||
{
|
||||
"backups": [
|
||||
{
|
||||
"filename": "backup_20250801_143022.zip",
|
||||
"size": "2.5 MB",
|
||||
"created": "2025-08-01 14:30:22"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Delete Backup
|
||||
```http
|
||||
DELETE /api/backup/delete/backup_20250801_143022.zip
|
||||
|
||||
Response:
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Backup deleted successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### Cleanup Old Backups
|
||||
```http
|
||||
POST /api/backup/cleanup
|
||||
|
||||
Response:
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Cleanup completed",
|
||||
"deleted_count": 3
|
||||
}
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version 2.0.0 (August 2025)
|
||||
- ✅ Added comprehensive backup management system
|
||||
- ✅ Implemented web-based backup interface
|
||||
- ✅ Added individual backup deletion
|
||||
- ✅ Added bulk cleanup functionality
|
||||
- ✅ Enhanced Docker integration
|
||||
- ✅ Added real-time status updates
|
||||
- ✅ Improved error handling and validation
|
||||
|
||||
### Version 1.0.0 (Initial Release)
|
||||
- ✅ Basic backup creation functionality
|
||||
- ✅ Command-line backup tools
|
||||
- ✅ Docker support
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions regarding the backup system:
|
||||
|
||||
1. Check this documentation
|
||||
2. Review application logs
|
||||
3. Verify file permissions and disk space
|
||||
4. Test with minimal data set
|
||||
5. Create GitHub issue with detailed error information
|
||||
|
||||
---
|
||||
|
||||
**Note**: Always test backup and restore procedures in a development environment before implementing in production.
|
||||
@@ -23,6 +23,7 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY main.py .
|
||||
COPY backup.py .
|
||||
COPY gunicorn.conf.py .
|
||||
COPY .env .
|
||||
COPY app/ ./app/
|
||||
|
||||
59
README.md
59
README.md
@@ -11,6 +11,11 @@ A modern Flask web application for generating and managing QR codes with authent
|
||||
- **Customizable Styling**: Different QR code styles (square, rounded, circle)
|
||||
- **Logo Integration**: Add custom logos to QR codes
|
||||
- **Click Tracking**: Monitor short URL usage and statistics
|
||||
- **🆕 Comprehensive Backup System**: Full backup lifecycle management
|
||||
- Create backups on-demand or automated
|
||||
- Download, delete, and cleanup old backups
|
||||
- Web-based backup management interface
|
||||
- Docker-aware backup operations
|
||||
- **Docker Deployment**: Production-ready containerization
|
||||
- **Responsive Design**: Modern web interface that works on all devices
|
||||
|
||||
@@ -493,6 +498,60 @@ curl http://localhost:5000/health
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
## 💾 Backup & Restore System
|
||||
|
||||
The QR Code Manager includes a comprehensive backup system for data protection and migration.
|
||||
|
||||
### Quick Start
|
||||
|
||||
**Create Backup:**
|
||||
- Via Web: Navigate to backup management page → "Create Backup"
|
||||
- Via API: `curl -X POST http://localhost:8066/api/backup/create`
|
||||
- Via CLI: `python backup.py` or `./docker_backup.sh`
|
||||
|
||||
**Manage Backups:**
|
||||
- **List**: View all backups with timestamps and sizes
|
||||
- **Download**: Get backup files directly from web interface
|
||||
- **Delete**: Remove individual backups with confirmation
|
||||
- **Cleanup**: Bulk remove backups older than 7 days
|
||||
|
||||
### What Gets Backed Up
|
||||
|
||||
Each backup includes:
|
||||
- ✅ All QR code data and metadata
|
||||
- ✅ Short URL database
|
||||
- ✅ Dynamic link page configurations
|
||||
- ✅ Generated QR code images
|
||||
- ✅ Application settings
|
||||
|
||||
### Restore Process
|
||||
|
||||
```bash
|
||||
# 1. Stop application
|
||||
docker compose down
|
||||
|
||||
# 2. Extract backup to data directory
|
||||
cd data/
|
||||
unzip ../backups/backup_20250801_143022.zip
|
||||
|
||||
# 3. Restart application
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Automated Backups
|
||||
|
||||
**Daily Backup (2 AM):**
|
||||
```bash
|
||||
0 2 * * * cd /opt/qr-code_manager && ./docker_backup.sh
|
||||
```
|
||||
|
||||
**Weekly Cleanup:**
|
||||
```bash
|
||||
0 3 * * 0 curl -X POST http://localhost:8066/api/backup/cleanup
|
||||
```
|
||||
|
||||
> 📖 **Detailed Documentation**: See [BACKUP_SYSTEM.md](BACKUP_SYSTEM.md) for complete backup system documentation.
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork the repository
|
||||
|
||||
@@ -476,3 +476,310 @@ def generate_shortened_qr():
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
# Backup Management API Endpoints
|
||||
@bp.route('/backup/create', methods=['POST'])
|
||||
@login_required
|
||||
def create_backup():
|
||||
"""Create backup via API"""
|
||||
try:
|
||||
import subprocess
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
data = request.json or {}
|
||||
backup_type = data.get('type', 'data') # data, config, full
|
||||
|
||||
# Use absolute path for Docker container or relative path for development
|
||||
if os.path.exists('/app/backup.py'):
|
||||
# Running in Docker container
|
||||
app_root = '/app'
|
||||
backup_script = '/app/backup.py'
|
||||
else:
|
||||
# Running in development
|
||||
app_root = Path(__file__).parent.parent.parent
|
||||
backup_script = 'backup.py'
|
||||
|
||||
if backup_type == 'data':
|
||||
# Create data backup using Python script
|
||||
result = subprocess.run(
|
||||
['python3', backup_script, '--data-only'],
|
||||
cwd=app_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
elif backup_type == 'config':
|
||||
# Create config backup
|
||||
result = subprocess.run(
|
||||
['python3', backup_script, '--config'],
|
||||
cwd=app_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
elif backup_type == 'full':
|
||||
# Create full backup
|
||||
result = subprocess.run(
|
||||
['python3', backup_script, '--full'],
|
||||
cwd=app_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
else:
|
||||
return jsonify({'error': 'Invalid backup type'}), 400
|
||||
|
||||
if result.returncode == 0:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Backup created successfully',
|
||||
'output': result.stdout
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'error': 'Backup creation failed',
|
||||
'output': result.stderr
|
||||
}), 500
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/backup/list', methods=['GET'])
|
||||
@login_required
|
||||
def list_backups():
|
||||
"""List available backups"""
|
||||
try:
|
||||
import subprocess
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Use absolute path for Docker container or relative path for development
|
||||
if os.path.exists('/app/backup.py'):
|
||||
# Running in Docker container
|
||||
app_root = '/app'
|
||||
backup_script = '/app/backup.py'
|
||||
else:
|
||||
# Running in development
|
||||
app_root = Path(__file__).parent.parent.parent
|
||||
backup_script = 'backup.py'
|
||||
|
||||
result = subprocess.run(
|
||||
['python3', backup_script, '--list'],
|
||||
cwd=app_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
# Parse backup list from output
|
||||
lines = result.stdout.strip().split('\n')
|
||||
backups = []
|
||||
|
||||
for line in lines:
|
||||
if '|' in line and not line.startswith('─') and 'Available backups' not in line:
|
||||
parts = [part.strip() for part in line.split('|')]
|
||||
if len(parts) >= 4:
|
||||
backups.append({
|
||||
'type': parts[0],
|
||||
'filename': parts[1],
|
||||
'size': parts[2],
|
||||
'date': parts[3]
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'backups': backups
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'error': 'Failed to list backups',
|
||||
'output': result.stderr
|
||||
}), 500
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/backup/download/<filename>', methods=['GET'])
|
||||
@login_required
|
||||
def download_backup(filename):
|
||||
"""Download backup file"""
|
||||
try:
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Use absolute path for Docker container or relative path for development
|
||||
if os.path.exists('/app/backup.py'):
|
||||
# Running in Docker container
|
||||
backup_dir = Path('/app/backups')
|
||||
else:
|
||||
# Running in development
|
||||
app_root = Path(__file__).parent.parent.parent
|
||||
backup_dir = app_root / 'backups'
|
||||
|
||||
backup_file = backup_dir / filename
|
||||
|
||||
if not backup_file.exists():
|
||||
return jsonify({'error': 'Backup file not found'}), 404
|
||||
|
||||
return send_file(
|
||||
str(backup_file),
|
||||
as_attachment=True,
|
||||
download_name=filename,
|
||||
mimetype='application/gzip'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/backup/status', methods=['GET'])
|
||||
@login_required
|
||||
def backup_status():
|
||||
"""Get backup system status"""
|
||||
try:
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Use absolute path for Docker container or relative path for development
|
||||
if os.path.exists('/app/backup.py'):
|
||||
# Running in Docker container
|
||||
backup_dir = Path('/app/backups')
|
||||
data_dir = Path('/app/data')
|
||||
else:
|
||||
# Running in development
|
||||
app_root = Path(__file__).parent.parent.parent
|
||||
backup_dir = app_root / 'backups'
|
||||
data_dir = app_root / 'data'
|
||||
|
||||
# Count backups
|
||||
backup_files = list(backup_dir.glob('*.tar.gz')) if backup_dir.exists() else []
|
||||
|
||||
# Get data directory size
|
||||
data_size = 0
|
||||
if data_dir.exists():
|
||||
for file_path in data_dir.rglob('*'):
|
||||
if file_path.is_file():
|
||||
data_size += file_path.stat().st_size
|
||||
|
||||
# Get backup directory size
|
||||
backup_size = 0
|
||||
for backup_file in backup_files:
|
||||
backup_size += backup_file.stat().st_size
|
||||
|
||||
# Check last backup time
|
||||
last_backup = None
|
||||
if backup_files:
|
||||
latest_backup = max(backup_files, key=lambda x: x.stat().st_mtime)
|
||||
last_backup = datetime.fromtimestamp(latest_backup.stat().st_mtime).isoformat()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'status': {
|
||||
'backup_count': len(backup_files),
|
||||
'data_size': data_size,
|
||||
'backup_size': backup_size,
|
||||
'last_backup': last_backup,
|
||||
'backup_directory': str(backup_dir),
|
||||
'data_directory': str(data_dir)
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/backup/delete/<filename>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_backup(filename):
|
||||
"""Delete a backup file"""
|
||||
try:
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Use absolute path for Docker container or relative path for development
|
||||
if os.path.exists('/app/backup.py'):
|
||||
# Running in Docker container
|
||||
backup_dir = Path('/app/backups')
|
||||
else:
|
||||
# Running in development
|
||||
app_root = Path(__file__).parent.parent.parent
|
||||
backup_dir = app_root / 'backups'
|
||||
|
||||
backup_file = backup_dir / filename
|
||||
|
||||
# Security check: ensure filename is just a filename, not a path
|
||||
if '/' in filename or '\\' in filename or '..' in filename:
|
||||
return jsonify({'error': 'Invalid filename'}), 400
|
||||
|
||||
# Check if file exists
|
||||
if not backup_file.exists():
|
||||
return jsonify({'error': 'Backup file not found'}), 404
|
||||
|
||||
# Delete the file
|
||||
backup_file.unlink()
|
||||
|
||||
# Also delete associated metadata file if it exists
|
||||
metadata_file = backup_dir / f"{filename.replace('.tar.gz', '.json')}"
|
||||
if metadata_file.exists():
|
||||
metadata_file.unlink()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Backup {filename} deleted successfully'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/backup/cleanup', methods=['POST'])
|
||||
@login_required
|
||||
def cleanup_old_backups():
|
||||
"""Delete old backup files, keeping only the N most recent"""
|
||||
try:
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
data = request.json or {}
|
||||
keep_count = data.get('keep', 5) # Default: keep 5 most recent
|
||||
|
||||
# Use absolute path for Docker container or relative path for development
|
||||
if os.path.exists('/app/backup.py'):
|
||||
# Running in Docker container
|
||||
backup_dir = Path('/app/backups')
|
||||
else:
|
||||
# Running in development
|
||||
app_root = Path(__file__).parent.parent.parent
|
||||
backup_dir = app_root / 'backups'
|
||||
|
||||
if not backup_dir.exists():
|
||||
return jsonify({'error': 'Backup directory not found'}), 404
|
||||
|
||||
# Get all backup files sorted by modification time (newest first)
|
||||
backup_files = list(backup_dir.glob('*.tar.gz'))
|
||||
backup_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
||||
|
||||
# Delete old backups
|
||||
deleted_files = []
|
||||
files_to_delete = backup_files[keep_count:]
|
||||
|
||||
for backup_file in files_to_delete:
|
||||
# Delete the backup file
|
||||
backup_file.unlink()
|
||||
deleted_files.append(backup_file.name)
|
||||
|
||||
# Also delete associated metadata file if it exists
|
||||
metadata_file = backup_dir / f"{backup_file.stem}.json"
|
||||
if metadata_file.exists():
|
||||
metadata_file.unlink()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Cleanup completed. Deleted {len(deleted_files)} old backups.',
|
||||
'deleted_files': deleted_files,
|
||||
'kept_count': min(len(backup_files), keep_count)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@@ -49,6 +49,12 @@ def edit_link_page(page_id):
|
||||
page_data = link_manager.get_page(page_id)
|
||||
return render_template('edit_links.html', page=page_data)
|
||||
|
||||
@bp.route('/backup')
|
||||
@login_required
|
||||
def backup_management():
|
||||
"""Display the backup management page"""
|
||||
return render_template('backup.html')
|
||||
|
||||
@bp.route('/health')
|
||||
def health_check():
|
||||
"""Health check endpoint for Docker"""
|
||||
|
||||
685
app/templates/backup.html
Normal file
685
app/templates/backup.html
Normal file
@@ -0,0 +1,685 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Backup Management - QR Code Manager</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 1.1em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 8px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9em;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 25px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-number {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.backup-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: linear-gradient(135deg, #ffc107 0%, #fd7e14 100%);
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: linear-gradient(135deg, #17a2b8 0%, #6f42c1 100%);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
||||
}
|
||||
|
||||
.backup-list {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.backup-list-header {
|
||||
background: #f8f9fa;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto auto auto;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.backup-item {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto auto auto;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.backup-item:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.backup-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.backup-type {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.backup-filename {
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.backup-size {
|
||||
font-size: 0.9em;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.backup-date {
|
||||
font-size: 0.9em;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.backup-actions-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 15px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: #d1ecf1;
|
||||
border: 1px solid #bee5eb;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 20px 15px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 20px 15px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 20px 15px;
|
||||
}
|
||||
|
||||
.backup-actions {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.status-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.backup-list-header,
|
||||
.backup-item {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.backup-item {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.backup-actions-item {
|
||||
justify-content: flex-start;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<a href="/" class="back-link">🏠 Back to Dashboard</a>
|
||||
<h1>🛡️ Backup Management</h1>
|
||||
<p>Create, manage, and restore backups of your QR Code Manager data</p>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<!-- Status Section -->
|
||||
<div class="section">
|
||||
<h2>📊 Backup Status</h2>
|
||||
<div class="status-grid" id="status-grid">
|
||||
<div class="status-card">
|
||||
<div class="status-number" id="backup-count">-</div>
|
||||
<div class="status-label">Total Backups</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<div class="status-number" id="data-size">-</div>
|
||||
<div class="status-label">Data Size</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<div class="status-number" id="backup-size">-</div>
|
||||
<div class="status-label">Backup Size</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<div class="status-number" id="last-backup">-</div>
|
||||
<div class="status-label">Last Backup</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Backup Section -->
|
||||
<div class="section">
|
||||
<h2>💾 Create Backup</h2>
|
||||
<div class="backup-actions">
|
||||
<button class="btn btn-success" onclick="createBackup('data')">
|
||||
📄 Data Backup
|
||||
</button>
|
||||
<button class="btn btn-warning" onclick="createBackup('config')">
|
||||
⚙️ Config Backup
|
||||
</button>
|
||||
<button class="btn btn-info" onclick="createBackup('full')">
|
||||
📦 Full Backup
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="loadBackups()">
|
||||
🔄 Refresh List
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="cleanupOldBackups()">
|
||||
🗑️ Cleanup Old
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="backup-alerts"></div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>💡 Backup Types:</strong><br>
|
||||
<strong>Data:</strong> QR codes, link pages, and short URLs (recommended for regular backups)<br>
|
||||
<strong>Config:</strong> Configuration files and environment settings<br>
|
||||
<strong>Full:</strong> Complete application backup including all files
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backup List Section -->
|
||||
<div class="section">
|
||||
<h2>📋 Available Backups</h2>
|
||||
<div class="backup-list">
|
||||
<div class="backup-list-header">
|
||||
<div>Type</div>
|
||||
<div>Filename</div>
|
||||
<div>Size</div>
|
||||
<div>Date</div>
|
||||
<div>Actions</div>
|
||||
</div>
|
||||
<div id="backup-list-content">
|
||||
<div class="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
Loading backups...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Load backup status and list on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadBackupStatus();
|
||||
loadBackups();
|
||||
});
|
||||
|
||||
async function loadBackupStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/backup/status');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const status = result.status;
|
||||
|
||||
document.getElementById('backup-count').textContent = status.backup_count;
|
||||
document.getElementById('data-size').textContent = formatBytes(status.data_size);
|
||||
document.getElementById('backup-size').textContent = formatBytes(status.backup_size);
|
||||
|
||||
if (status.last_backup) {
|
||||
const lastBackup = new Date(status.last_backup);
|
||||
document.getElementById('last-backup').textContent = formatTimeAgo(lastBackup);
|
||||
} else {
|
||||
document.getElementById('last-backup').textContent = 'Never';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load backup status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBackups() {
|
||||
const listContent = document.getElementById('backup-list-content');
|
||||
listContent.innerHTML = '<div class="loading"><div class="loading-spinner"></div>Loading backups...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/backup/list');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.backups.length > 0) {
|
||||
listContent.innerHTML = result.backups.map(backup => `
|
||||
<div class="backup-item">
|
||||
<div class="backup-type">${backup.type}</div>
|
||||
<div class="backup-filename">${backup.filename}</div>
|
||||
<div class="backup-size">${backup.size}</div>
|
||||
<div class="backup-date">${backup.date}</div>
|
||||
<div class="backup-actions-item">
|
||||
<button class="btn btn-small btn-info" onclick="downloadBackup('${backup.filename}')">
|
||||
📥 Download
|
||||
</button>
|
||||
<button class="btn btn-small btn-danger" onclick="deleteBackup('${backup.filename}')">
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
listContent.innerHTML = '<div style="padding: 40px; text-align: center; color: #666;">No backups found</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load backups:', error);
|
||||
listContent.innerHTML = '<div style="padding: 40px; text-align: center; color: #dc3545;">Failed to load backups</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function createBackup(type) {
|
||||
const alertsContainer = document.getElementById('backup-alerts');
|
||||
|
||||
// Show loading alert
|
||||
alertsContainer.innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<div class="loading-spinner" style="width: 20px; height: 20px; border-width: 2px; margin: 0;"></div>
|
||||
Creating ${type} backup...
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/backup/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ type: type })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alertsContainer.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
✅ ${type.charAt(0).toUpperCase() + type.slice(1)} backup created successfully!
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Refresh status and list
|
||||
loadBackupStatus();
|
||||
loadBackups();
|
||||
|
||||
// Clear alert after 5 seconds
|
||||
setTimeout(() => {
|
||||
alertsContainer.innerHTML = '';
|
||||
}, 5000);
|
||||
} else {
|
||||
alertsContainer.innerHTML = `
|
||||
<div class="alert alert-error">
|
||||
❌ Failed to create backup: ${result.error}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
alertsContainer.innerHTML = `
|
||||
<div class="alert alert-error">
|
||||
❌ Error creating backup: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function downloadBackup(filename) {
|
||||
window.open(`/api/backup/download/${filename}`, '_blank');
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatTimeAgo(date) {
|
||||
const now = new Date();
|
||||
const diffInSeconds = Math.floor((now - date) / 1000);
|
||||
|
||||
if (diffInSeconds < 60) return 'Just now';
|
||||
if (diffInSeconds < 3600) return Math.floor(diffInSeconds / 60) + 'm ago';
|
||||
if (diffInSeconds < 86400) return Math.floor(diffInSeconds / 3600) + 'h ago';
|
||||
if (diffInSeconds < 2592000) return Math.floor(diffInSeconds / 86400) + 'd ago';
|
||||
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
async function deleteBackup(filename) {
|
||||
if (!confirm(`Are you sure you want to delete the backup "${filename}"? This action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const alertsContainer = document.getElementById('backup-alerts');
|
||||
|
||||
// Show loading alert
|
||||
alertsContainer.innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<div class="loading-spinner" style="width: 20px; height: 20px; border-width: 2px; margin: 0;"></div>
|
||||
Deleting backup...
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/backup/delete/${filename}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alertsContainer.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
✅ Backup deleted successfully!
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Refresh status and list
|
||||
loadBackupStatus();
|
||||
loadBackups();
|
||||
|
||||
// Clear alert after 5 seconds
|
||||
setTimeout(() => {
|
||||
alertsContainer.innerHTML = '';
|
||||
}, 5000);
|
||||
} else {
|
||||
alertsContainer.innerHTML = `
|
||||
<div class="alert alert-error">
|
||||
❌ Failed to delete backup: ${result.error}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
alertsContainer.innerHTML = `
|
||||
<div class="alert alert-error">
|
||||
❌ Error deleting backup: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupOldBackups() {
|
||||
const keepCount = prompt('How many recent backups would you like to keep?', '5');
|
||||
|
||||
if (keepCount === null) {
|
||||
return; // User cancelled
|
||||
}
|
||||
|
||||
const numKeep = parseInt(keepCount);
|
||||
if (isNaN(numKeep) || numKeep < 1) {
|
||||
alert('Please enter a valid number greater than 0.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`This will delete all but the ${numKeep} most recent backups. Are you sure?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const alertsContainer = document.getElementById('backup-alerts');
|
||||
|
||||
// Show loading alert
|
||||
alertsContainer.innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<div class="loading-spinner" style="width: 20px; height: 20px; border-width: 2px; margin: 0;"></div>
|
||||
Cleaning up old backups...
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/backup/cleanup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ keep: numKeep })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alertsContainer.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
✅ ${result.message}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Refresh status and list
|
||||
loadBackupStatus();
|
||||
loadBackups();
|
||||
|
||||
// Clear alert after 7 seconds
|
||||
setTimeout(() => {
|
||||
alertsContainer.innerHTML = '';
|
||||
}, 7000);
|
||||
} else {
|
||||
alertsContainer.innerHTML = `
|
||||
<div class="alert alert-error">
|
||||
❌ Failed to cleanup backups: ${result.error}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
alertsContainer.innerHTML = `
|
||||
<div class="alert alert-error">
|
||||
❌ Error during cleanup: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -499,7 +499,10 @@
|
||||
<div class="header">
|
||||
<h1>🎯 QR Code Manager</h1>
|
||||
<p>Create, customize, and manage your QR codes with ease</p>
|
||||
<div style="position: absolute; top: 20px; right: 20px;">
|
||||
<div style="position: absolute; top: 20px; right: 20px; display: flex; gap: 10px;">
|
||||
<a href="/backup" style="color: white; text-decoration: none; background: rgba(255,255,255,0.2); padding: 8px 15px; border-radius: 20px; font-size: 0.9em;">
|
||||
🛡️ Backup
|
||||
</a>
|
||||
<a href="/logout" style="color: white; text-decoration: none; background: rgba(255,255,255,0.2); padding: 8px 15px; border-radius: 20px; font-size: 0.9em;">
|
||||
👤 Logout
|
||||
</a>
|
||||
|
||||
377
backup.py
Executable file
377
backup.py
Executable file
@@ -0,0 +1,377 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
QR Code Manager - Backup Utility
|
||||
|
||||
This script creates comprehensive backups of the QR Code Manager application
|
||||
including data files, configuration, and optionally the entire application.
|
||||
|
||||
Usage:
|
||||
python backup.py [options]
|
||||
|
||||
Options:
|
||||
--data-only : Backup only data files (default)
|
||||
--full : Full backup including application files
|
||||
--restore : Restore from backup
|
||||
--list : List available backups
|
||||
--config : Backup configuration files only
|
||||
--auto : Automated backup (for cron jobs)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import tarfile
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
class QRCodeManagerBackup:
|
||||
def __init__(self):
|
||||
self.app_root = Path(__file__).parent
|
||||
self.backup_dir = self.app_root / "backups"
|
||||
self.data_dir = self.app_root / "data"
|
||||
self.config_files = [
|
||||
".env",
|
||||
".env.production",
|
||||
"docker-compose.yml",
|
||||
"Dockerfile",
|
||||
"gunicorn.conf.py",
|
||||
"requirements.txt"
|
||||
]
|
||||
self.data_files = [
|
||||
"data/link_pages.json",
|
||||
"data/qr_codes.json",
|
||||
"data/short_urls.json"
|
||||
]
|
||||
|
||||
# Ensure backup directory exists
|
||||
self.backup_dir.mkdir(exist_ok=True)
|
||||
|
||||
def get_timestamp(self):
|
||||
"""Get current timestamp for backup naming"""
|
||||
return datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
def create_data_backup(self, backup_name=None):
|
||||
"""Create backup of data files only"""
|
||||
if not backup_name:
|
||||
backup_name = f"qr_data_backup_{self.get_timestamp()}.tar.gz"
|
||||
|
||||
backup_path = self.backup_dir / backup_name
|
||||
|
||||
print(f"🗂️ Creating data backup: {backup_name}")
|
||||
|
||||
with tarfile.open(backup_path, "w:gz") as tar:
|
||||
# Add data files
|
||||
for data_file in self.data_files:
|
||||
file_path = self.app_root / data_file
|
||||
if file_path.exists():
|
||||
tar.add(file_path, arcname=data_file)
|
||||
print(f" ✅ Added: {data_file}")
|
||||
else:
|
||||
print(f" ⚠️ Missing: {data_file}")
|
||||
|
||||
# Add backup metadata
|
||||
metadata = {
|
||||
"backup_type": "data",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"app_version": self.get_app_version(),
|
||||
"files_included": self.data_files
|
||||
}
|
||||
|
||||
metadata_path = self.app_root / "backup_metadata.json"
|
||||
with open(metadata_path, 'w') as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
|
||||
tar.add(metadata_path, arcname="backup_metadata.json")
|
||||
metadata_path.unlink() # Remove temp file
|
||||
|
||||
print(f"✅ Data backup created: {backup_path}")
|
||||
return backup_path
|
||||
|
||||
def create_config_backup(self, backup_name=None):
|
||||
"""Create backup of configuration files only"""
|
||||
if not backup_name:
|
||||
backup_name = f"qr_config_backup_{self.get_timestamp()}.tar.gz"
|
||||
|
||||
backup_path = self.backup_dir / backup_name
|
||||
|
||||
print(f"⚙️ Creating config backup: {backup_name}")
|
||||
|
||||
with tarfile.open(backup_path, "w:gz") as tar:
|
||||
# Add config files
|
||||
for config_file in self.config_files:
|
||||
file_path = self.app_root / config_file
|
||||
if file_path.exists():
|
||||
tar.add(file_path, arcname=config_file)
|
||||
print(f" ✅ Added: {config_file}")
|
||||
else:
|
||||
print(f" ⚠️ Missing: {config_file}")
|
||||
|
||||
# Add backup metadata
|
||||
metadata = {
|
||||
"backup_type": "config",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"app_version": self.get_app_version(),
|
||||
"files_included": self.config_files
|
||||
}
|
||||
|
||||
metadata_path = self.app_root / "backup_metadata.json"
|
||||
with open(metadata_path, 'w') as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
|
||||
tar.add(metadata_path, arcname="backup_metadata.json")
|
||||
metadata_path.unlink() # Remove temp file
|
||||
|
||||
print(f"✅ Config backup created: {backup_path}")
|
||||
return backup_path
|
||||
|
||||
def create_full_backup(self, backup_name=None):
|
||||
"""Create full backup including application files"""
|
||||
if not backup_name:
|
||||
backup_name = f"qr_full_backup_{self.get_timestamp()}.tar.gz"
|
||||
|
||||
backup_path = self.backup_dir / backup_name
|
||||
|
||||
print(f"📦 Creating full backup: {backup_name}")
|
||||
|
||||
# Exclude patterns
|
||||
exclude_patterns = [
|
||||
"__pycache__",
|
||||
"*.pyc",
|
||||
".git",
|
||||
"backups",
|
||||
"*.log",
|
||||
".dockerignore",
|
||||
"backup.py"
|
||||
]
|
||||
|
||||
def exclude_filter(tarinfo):
|
||||
for pattern in exclude_patterns:
|
||||
if pattern in tarinfo.name:
|
||||
return None
|
||||
return tarinfo
|
||||
|
||||
with tarfile.open(backup_path, "w:gz") as tar:
|
||||
# Add entire application directory
|
||||
tar.add(self.app_root, arcname="qr-code-manager", filter=exclude_filter)
|
||||
|
||||
# Add backup metadata
|
||||
metadata = {
|
||||
"backup_type": "full",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"app_version": self.get_app_version(),
|
||||
"excluded_patterns": exclude_patterns
|
||||
}
|
||||
|
||||
metadata_path = self.app_root / "backup_metadata.json"
|
||||
with open(metadata_path, 'w') as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
|
||||
tar.add(metadata_path, arcname="backup_metadata.json")
|
||||
metadata_path.unlink() # Remove temp file
|
||||
|
||||
print(f"✅ Full backup created: {backup_path}")
|
||||
return backup_path
|
||||
|
||||
def list_backups(self):
|
||||
"""List all available backups"""
|
||||
print("📋 Available backups:")
|
||||
print("-" * 60)
|
||||
|
||||
backup_files = sorted(self.backup_dir.glob("*.tar.gz"), reverse=True)
|
||||
|
||||
if not backup_files:
|
||||
print(" No backups found")
|
||||
return
|
||||
|
||||
for backup_file in backup_files:
|
||||
# Get file info
|
||||
stat = backup_file.stat()
|
||||
size_mb = stat.st_size / (1024 * 1024)
|
||||
modified = datetime.fromtimestamp(stat.st_mtime)
|
||||
|
||||
# Try to get backup type from filename
|
||||
if "data" in backup_file.name:
|
||||
backup_type = "📄 Data"
|
||||
elif "config" in backup_file.name:
|
||||
backup_type = "⚙️ Config"
|
||||
elif "full" in backup_file.name:
|
||||
backup_type = "📦 Full"
|
||||
else:
|
||||
backup_type = "❓ Unknown"
|
||||
|
||||
print(f" {backup_type:12} | {backup_file.name:40} | {size_mb:6.1f}MB | {modified.strftime('%Y-%m-%d %H:%M')}")
|
||||
|
||||
def restore_backup(self, backup_file):
|
||||
"""Restore from backup file"""
|
||||
backup_path = Path(backup_file)
|
||||
|
||||
if not backup_path.exists():
|
||||
# Try in backup directory
|
||||
backup_path = self.backup_dir / backup_file
|
||||
if not backup_path.exists():
|
||||
print(f"❌ Backup file not found: {backup_file}")
|
||||
return False
|
||||
|
||||
print(f"🔄 Restoring from backup: {backup_path.name}")
|
||||
|
||||
# Create restoration directory
|
||||
restore_dir = self.app_root / f"restore_{self.get_timestamp()}"
|
||||
restore_dir.mkdir(exist_ok=True)
|
||||
|
||||
try:
|
||||
with tarfile.open(backup_path, "r:gz") as tar:
|
||||
# Extract to restoration directory
|
||||
tar.extractall(restore_dir)
|
||||
|
||||
# Check backup metadata
|
||||
metadata_file = restore_dir / "backup_metadata.json"
|
||||
if metadata_file.exists():
|
||||
with open(metadata_file) as f:
|
||||
metadata = json.load(f)
|
||||
print(f" Backup type: {metadata.get('backup_type', 'unknown')}")
|
||||
print(f" Created: {metadata.get('timestamp', 'unknown')}")
|
||||
print(f" App version: {metadata.get('app_version', 'unknown')}")
|
||||
|
||||
# Ask for confirmation
|
||||
confirm = input(" Proceed with restoration? (y/N): ").lower().strip()
|
||||
if confirm != 'y':
|
||||
shutil.rmtree(restore_dir)
|
||||
print("❌ Restoration cancelled")
|
||||
return False
|
||||
|
||||
# Restore files based on backup type
|
||||
backup_type = metadata.get('backup_type', 'unknown') if metadata_file.exists() else 'unknown'
|
||||
|
||||
if backup_type == 'data':
|
||||
# Restore data files
|
||||
for data_file in self.data_files:
|
||||
src = restore_dir / data_file
|
||||
dst = self.app_root / data_file
|
||||
if src.exists():
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
print(f" ✅ Restored: {data_file}")
|
||||
|
||||
elif backup_type == 'config':
|
||||
# Restore config files
|
||||
for config_file in self.config_files:
|
||||
src = restore_dir / config_file
|
||||
dst = self.app_root / config_file
|
||||
if src.exists():
|
||||
shutil.copy2(src, dst)
|
||||
print(f" ✅ Restored: {config_file}")
|
||||
|
||||
elif backup_type == 'full':
|
||||
# Full restoration - more complex
|
||||
print(" ⚠️ Full restoration requires manual review")
|
||||
print(f" Extracted to: {restore_dir}")
|
||||
print(" Please manually copy files as needed")
|
||||
return True
|
||||
|
||||
# Cleanup
|
||||
shutil.rmtree(restore_dir)
|
||||
print("✅ Restoration completed successfully")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Restoration failed: {e}")
|
||||
if restore_dir.exists():
|
||||
shutil.rmtree(restore_dir)
|
||||
return False
|
||||
|
||||
def get_app_version(self):
|
||||
"""Get application version from main.py or git"""
|
||||
try:
|
||||
# Try to get git commit hash
|
||||
import subprocess
|
||||
result = subprocess.run(['git', 'rev-parse', '--short', 'HEAD'],
|
||||
cwd=self.app_root,
|
||||
capture_output=True,
|
||||
text=True)
|
||||
if result.returncode == 0:
|
||||
return f"git-{result.stdout.strip()}"
|
||||
except:
|
||||
pass
|
||||
|
||||
# Fallback to file modification time
|
||||
main_py = self.app_root / "main.py"
|
||||
if main_py.exists():
|
||||
mtime = datetime.fromtimestamp(main_py.stat().st_mtime)
|
||||
return f"modified-{mtime.strftime('%Y%m%d')}"
|
||||
|
||||
return "unknown"
|
||||
|
||||
def cleanup_old_backups(self, keep_count=10):
|
||||
"""Remove old backups, keeping only the most recent ones"""
|
||||
backup_files = sorted(self.backup_dir.glob("*.tar.gz"),
|
||||
key=lambda x: x.stat().st_mtime,
|
||||
reverse=True)
|
||||
|
||||
if len(backup_files) <= keep_count:
|
||||
return
|
||||
|
||||
print(f"🧹 Cleaning up old backups (keeping {keep_count} most recent)")
|
||||
|
||||
for backup_file in backup_files[keep_count:]:
|
||||
print(f" 🗑️ Removing: {backup_file.name}")
|
||||
backup_file.unlink()
|
||||
|
||||
def automated_backup(self):
|
||||
"""Perform automated backup suitable for cron jobs"""
|
||||
print(f"🤖 Automated backup started at {datetime.now()}")
|
||||
|
||||
try:
|
||||
# Create data backup
|
||||
self.create_data_backup()
|
||||
|
||||
# Create config backup weekly (if it's Monday)
|
||||
if datetime.now().weekday() == 0: # Monday
|
||||
self.create_config_backup()
|
||||
|
||||
# Create full backup monthly (if it's the 1st)
|
||||
if datetime.now().day == 1:
|
||||
self.create_full_backup()
|
||||
|
||||
# Cleanup old backups
|
||||
self.cleanup_old_backups(keep_count=15)
|
||||
|
||||
print("✅ Automated backup completed successfully")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Automated backup failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="QR Code Manager Backup Utility")
|
||||
parser.add_argument("--data-only", action="store_true", help="Backup only data files")
|
||||
parser.add_argument("--config", action="store_true", help="Backup only config files")
|
||||
parser.add_argument("--full", action="store_true", help="Full backup including application files")
|
||||
parser.add_argument("--restore", type=str, help="Restore from backup file")
|
||||
parser.add_argument("--list", action="store_true", help="List available backups")
|
||||
parser.add_argument("--auto", action="store_true", help="Automated backup (for cron jobs)")
|
||||
parser.add_argument("--cleanup", type=int, help="Cleanup old backups, keeping N most recent")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
backup_util = QRCodeManagerBackup()
|
||||
|
||||
if args.list:
|
||||
backup_util.list_backups()
|
||||
elif args.restore:
|
||||
backup_util.restore_backup(args.restore)
|
||||
elif args.auto:
|
||||
backup_util.automated_backup()
|
||||
elif args.cleanup:
|
||||
backup_util.cleanup_old_backups(args.cleanup)
|
||||
elif args.config:
|
||||
backup_util.create_config_backup()
|
||||
elif args.full:
|
||||
backup_util.create_full_backup()
|
||||
else:
|
||||
# Default: data backup
|
||||
backup_util.create_data_backup()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
BIN
backups/qr_full_backup_20250801_165725.tar.gz
Normal file
BIN
backups/qr_full_backup_20250801_165725.tar.gz
Normal file
Binary file not shown.
@@ -45,6 +45,9 @@ services:
|
||||
|
||||
# Application data - stores JSON databases (link_pages.json, qr_codes.json, short_urls.json)
|
||||
- /opt/qr/persistent:/app/data
|
||||
|
||||
# Backup files - stores backup archives created by backup scripts
|
||||
- ./backups:/app/backups
|
||||
|
||||
# Health check configuration - monitors container health
|
||||
# Ensures the application is responding correctly
|
||||
|
||||
407
docker_backup.sh
Executable file
407
docker_backup.sh
Executable file
@@ -0,0 +1,407 @@
|
||||
#!/bin/bash
|
||||
"""
|
||||
QR Code Manager - Docker Backup Script
|
||||
|
||||
This script provides backup and restore functionality for dockerized QR Code Manager.
|
||||
It handles both data volumes and complete application backups.
|
||||
|
||||
Usage:
|
||||
./docker_backup.sh [command] [options]
|
||||
|
||||
Commands:
|
||||
backup-data - Backup data volumes only
|
||||
backup-full - Full backup including application and data
|
||||
restore - Restore from backup
|
||||
list - List available backups
|
||||
cleanup - Remove old backups
|
||||
schedule - Set up automated backups
|
||||
|
||||
Examples:
|
||||
./docker_backup.sh backup-data
|
||||
./docker_backup.sh backup-full
|
||||
./docker_backup.sh restore backup_20240801_120000.tar.gz
|
||||
./docker_backup.sh list
|
||||
"""
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BACKUP_DIR="${SCRIPT_DIR}/backups"
|
||||
CONTAINER_NAME="qr-code-manager"
|
||||
COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.yml"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Helper functions
|
||||
log_info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
# Ensure backup directory exists
|
||||
mkdir -p "${BACKUP_DIR}"
|
||||
|
||||
# Check if Docker and docker-compose are available
|
||||
check_dependencies() {
|
||||
if ! command -v docker &> /dev/null; then
|
||||
log_error "Docker is not installed or not in PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if command -v docker &> /dev/null && docker compose version &> /dev/null 2>&1; then
|
||||
# Use docker compose (newer syntax)
|
||||
COMPOSE_CMD="docker compose"
|
||||
elif command -v docker-compose &> /dev/null; then
|
||||
# Fallback to docker-compose (older syntax)
|
||||
COMPOSE_CMD="docker-compose"
|
||||
else
|
||||
log_error "Neither 'docker compose' nor 'docker-compose' is available"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Get timestamp for backup naming
|
||||
get_timestamp() {
|
||||
date +"%Y%m%d_%H%M%S"
|
||||
}
|
||||
|
||||
# Check if container is running
|
||||
is_container_running() {
|
||||
docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"
|
||||
}
|
||||
|
||||
# Backup data volumes only
|
||||
backup_data() {
|
||||
local timestamp=$(get_timestamp)
|
||||
local backup_name="qr_data_backup_${timestamp}.tar.gz"
|
||||
local backup_path="${BACKUP_DIR}/${backup_name}"
|
||||
|
||||
log_info "Creating data backup: ${backup_name}"
|
||||
|
||||
if is_container_running; then
|
||||
log_info "Container is running, creating hot backup..."
|
||||
|
||||
# Create backup from running container
|
||||
docker exec "${CONTAINER_NAME}" tar czf /tmp/data_backup.tar.gz \
|
||||
-C /app data/ || {
|
||||
log_error "Failed to create backup inside container"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Copy backup from container to host
|
||||
docker cp "${CONTAINER_NAME}:/tmp/data_backup.tar.gz" "${backup_path}" || {
|
||||
log_error "Failed to copy backup from container"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Cleanup temp file in container
|
||||
docker exec "${CONTAINER_NAME}" rm -f /tmp/data_backup.tar.gz
|
||||
|
||||
else
|
||||
log_info "Container is not running, creating backup from volumes..."
|
||||
|
||||
# Create backup using temporary container
|
||||
docker run --rm \
|
||||
-v "${SCRIPT_DIR}/data:/source/data:ro" \
|
||||
-v "${BACKUP_DIR}:/backup" \
|
||||
alpine:latest tar czf "/backup/${backup_name}" -C /source data/ || {
|
||||
log_error "Failed to create backup from volumes"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
|
||||
# Add metadata
|
||||
local metadata_file="${BACKUP_DIR}/${backup_name%.tar.gz}.json"
|
||||
cat > "${metadata_file}" << EOF
|
||||
{
|
||||
"backup_type": "data",
|
||||
"timestamp": "$(date -Iseconds)",
|
||||
"container_name": "${CONTAINER_NAME}",
|
||||
"backup_method": "$(if is_container_running; then echo 'hot'; else echo 'cold'; fi)",
|
||||
"backup_size": "$(stat -f%z "${backup_path}" 2>/dev/null || stat -c%s "${backup_path}" 2>/dev/null || echo 'unknown')"
|
||||
}
|
||||
EOF
|
||||
|
||||
log_success "Data backup created: ${backup_path}"
|
||||
log_info "Backup size: $(du -h "${backup_path}" | cut -f1)"
|
||||
}
|
||||
|
||||
# Full backup including application
|
||||
backup_full() {
|
||||
local timestamp=$(get_timestamp)
|
||||
local backup_name="qr_full_backup_${timestamp}.tar.gz"
|
||||
local backup_path="${BACKUP_DIR}/${backup_name}"
|
||||
|
||||
log_info "Creating full backup: ${backup_name}"
|
||||
|
||||
# Stop container for consistent backup
|
||||
local was_running=false
|
||||
if is_container_running; then
|
||||
log_warning "Stopping container for consistent backup..."
|
||||
${COMPOSE_CMD} -f "${COMPOSE_FILE}" down
|
||||
was_running=true
|
||||
fi
|
||||
|
||||
# Create full backup
|
||||
tar czf "${backup_path}" \
|
||||
--exclude="backups" \
|
||||
--exclude=".git" \
|
||||
--exclude="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
--exclude="*.log" \
|
||||
-C "${SCRIPT_DIR}/.." \
|
||||
"$(basename "${SCRIPT_DIR}")" || {
|
||||
log_error "Failed to create full backup"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Restart container if it was running
|
||||
if [ "$was_running" = true ]; then
|
||||
log_info "Restarting container..."
|
||||
${COMPOSE_CMD} -f "${COMPOSE_FILE}" up -d
|
||||
fi
|
||||
|
||||
# Add metadata
|
||||
local metadata_file="${BACKUP_DIR}/${backup_name%.tar.gz}.json"
|
||||
cat > "${metadata_file}" << EOF
|
||||
{
|
||||
"backup_type": "full",
|
||||
"timestamp": "$(date -Iseconds)",
|
||||
"container_name": "${CONTAINER_NAME}",
|
||||
"backup_method": "cold",
|
||||
"backup_size": "$(stat -f%z "${backup_path}" 2>/dev/null || stat -c%s "${backup_path}" 2>/dev/null || echo 'unknown')"
|
||||
}
|
||||
EOF
|
||||
|
||||
log_success "Full backup created: ${backup_path}"
|
||||
log_info "Backup size: $(du -h "${backup_path}" | cut -f1)"
|
||||
}
|
||||
|
||||
# List available backups
|
||||
list_backups() {
|
||||
log_info "Available backups:"
|
||||
echo "$(printf '%s' '─%.0s' {1..80})"
|
||||
|
||||
if [ ! "$(ls -A "${BACKUP_DIR}"/*.tar.gz 2>/dev/null)" ]; then
|
||||
echo "No backups found"
|
||||
return
|
||||
fi
|
||||
|
||||
for backup_file in "${BACKUP_DIR}"/*.tar.gz; do
|
||||
if [ -f "$backup_file" ]; then
|
||||
local basename=$(basename "$backup_file")
|
||||
local size=$(du -h "$backup_file" | cut -f1)
|
||||
local date=$(stat -f%Sm -t "%Y-%m-%d %H:%M" "$backup_file" 2>/dev/null || \
|
||||
stat -c%y "$backup_file" 2>/dev/null | cut -d' ' -f1-2)
|
||||
|
||||
# Determine backup type
|
||||
local type="Unknown"
|
||||
if [[ "$basename" == *"data"* ]]; then
|
||||
type="📄 Data"
|
||||
elif [[ "$basename" == *"full"* ]]; then
|
||||
type="📦 Full"
|
||||
fi
|
||||
|
||||
printf " %-12s | %-40s | %8s | %s\n" "$type" "$basename" "$size" "$date"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Restore from backup
|
||||
restore_backup() {
|
||||
local backup_file="$1"
|
||||
|
||||
if [ -z "$backup_file" ]; then
|
||||
log_error "Please specify backup file to restore"
|
||||
echo "Available backups:"
|
||||
list_backups
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if backup file exists
|
||||
local backup_path="${backup_file}"
|
||||
if [ ! -f "$backup_path" ]; then
|
||||
backup_path="${BACKUP_DIR}/${backup_file}"
|
||||
if [ ! -f "$backup_path" ]; then
|
||||
log_error "Backup file not found: $backup_file"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
log_warning "Restoring from backup: $(basename "$backup_path")"
|
||||
|
||||
# Confirm restoration
|
||||
echo -n "This will overwrite current data. Continue? (y/N): "
|
||||
read -r confirm
|
||||
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
||||
log_info "Restoration cancelled"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Stop container
|
||||
if is_container_running; then
|
||||
log_info "Stopping container..."
|
||||
${COMPOSE_CMD} -f "${COMPOSE_FILE}" down
|
||||
fi
|
||||
|
||||
# Determine backup type and restore accordingly
|
||||
if [[ "$(basename "$backup_path")" == *"data"* ]]; then
|
||||
log_info "Restoring data backup..."
|
||||
|
||||
# Backup current data
|
||||
if [ -d "${SCRIPT_DIR}/data" ]; then
|
||||
mv "${SCRIPT_DIR}/data" "${SCRIPT_DIR}/data.backup.$(get_timestamp)"
|
||||
fi
|
||||
|
||||
# Extract data backup
|
||||
tar xzf "$backup_path" -C "${SCRIPT_DIR}" || {
|
||||
log_error "Failed to extract data backup"
|
||||
return 1
|
||||
}
|
||||
|
||||
elif [[ "$(basename "$backup_path")" == *"full"* ]]; then
|
||||
log_info "Restoring full backup..."
|
||||
|
||||
# Create restoration directory
|
||||
local restore_dir="${SCRIPT_DIR}/../qr-restore-$(get_timestamp)"
|
||||
mkdir -p "$restore_dir"
|
||||
|
||||
# Extract full backup
|
||||
tar xzf "$backup_path" -C "$restore_dir" || {
|
||||
log_error "Failed to extract full backup"
|
||||
return 1
|
||||
}
|
||||
|
||||
log_warning "Full backup extracted to: $restore_dir"
|
||||
log_warning "Please manually review and copy files as needed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Restart container
|
||||
log_info "Starting container..."
|
||||
${COMPOSE_CMD} -f "${COMPOSE_FILE}" up -d
|
||||
|
||||
log_success "Restoration completed successfully"
|
||||
}
|
||||
|
||||
# Cleanup old backups
|
||||
cleanup_backups() {
|
||||
local keep_count=${1:-10}
|
||||
|
||||
log_info "Cleaning up old backups (keeping $keep_count most recent)"
|
||||
|
||||
# Get list of backup files sorted by modification time (newest first)
|
||||
local backup_files=($(ls -t "${BACKUP_DIR}"/*.tar.gz 2>/dev/null || true))
|
||||
|
||||
if [ ${#backup_files[@]} -le $keep_count ]; then
|
||||
log_info "No cleanup needed (${#backup_files[@]} backups found)"
|
||||
return
|
||||
fi
|
||||
|
||||
# Remove old backups
|
||||
for ((i=$keep_count; i<${#backup_files[@]}; i++)); do
|
||||
local backup_file="${backup_files[$i]}"
|
||||
local metadata_file="${backup_file%.tar.gz}.json"
|
||||
|
||||
log_info "Removing old backup: $(basename "$backup_file")"
|
||||
rm -f "$backup_file" "$metadata_file"
|
||||
done
|
||||
|
||||
log_success "Cleanup completed"
|
||||
}
|
||||
|
||||
# Set up automated backups
|
||||
schedule_backups() {
|
||||
log_info "Setting up automated backups with cron..."
|
||||
|
||||
# Create backup script for cron
|
||||
local cron_script="${SCRIPT_DIR}/automated_backup.sh"
|
||||
cat > "$cron_script" << 'EOF'
|
||||
#!/bin/bash
|
||||
# Automated backup script for QR Code Manager
|
||||
cd "$(dirname "$0")"
|
||||
./docker_backup.sh backup-data
|
||||
if [ $(date +%u) -eq 1 ]; then # Monday
|
||||
./docker_backup.sh backup-full
|
||||
fi
|
||||
./docker_backup.sh cleanup 15
|
||||
EOF
|
||||
chmod +x "$cron_script"
|
||||
|
||||
# Suggest cron entries
|
||||
echo
|
||||
log_info "Automated backup script created: $cron_script"
|
||||
echo
|
||||
echo "Add this to your crontab (crontab -e) for daily backups:"
|
||||
echo "# QR Code Manager daily backup at 2 AM"
|
||||
echo "0 2 * * * $cron_script >> ${SCRIPT_DIR}/backup.log 2>&1"
|
||||
echo
|
||||
echo "Or for hourly data backups:"
|
||||
echo "# QR Code Manager hourly data backup"
|
||||
echo "0 * * * * ${SCRIPT_DIR}/docker_backup.sh backup-data >> ${SCRIPT_DIR}/backup.log 2>&1"
|
||||
}
|
||||
|
||||
# Main script logic
|
||||
main() {
|
||||
check_dependencies
|
||||
|
||||
case "${1:-}" in
|
||||
"backup-data")
|
||||
backup_data
|
||||
;;
|
||||
"backup-full")
|
||||
backup_full
|
||||
;;
|
||||
"restore")
|
||||
restore_backup "$2"
|
||||
;;
|
||||
"list")
|
||||
list_backups
|
||||
;;
|
||||
"cleanup")
|
||||
cleanup_backups "$2"
|
||||
;;
|
||||
"schedule")
|
||||
schedule_backups
|
||||
;;
|
||||
*)
|
||||
echo "QR Code Manager - Docker Backup Script"
|
||||
echo
|
||||
echo "Usage: $0 [command] [options]"
|
||||
echo
|
||||
echo "Commands:"
|
||||
echo " backup-data - Backup data volumes only"
|
||||
echo " backup-full - Full backup including application and data"
|
||||
echo " restore <file> - Restore from backup"
|
||||
echo " list - List available backups"
|
||||
echo " cleanup [n] - Remove old backups (keep n most recent, default: 10)"
|
||||
echo " schedule - Set up automated backups"
|
||||
echo
|
||||
echo "Examples:"
|
||||
echo " $0 backup-data"
|
||||
echo " $0 backup-full"
|
||||
echo " $0 restore qr_data_backup_20240801_120000.tar.gz"
|
||||
echo " $0 list"
|
||||
echo " $0 cleanup 5"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user