Skip to content

Commit 68d05db

Browse files
author
GeiserX
committed
Fix automatic backups and ZIP download
- Fix BACKUP_DIR to use fixed path /.way-cms-backups (matches docker mount) - Add auto backups to backup listing API (shown with [Auto] prefix) - Simplify ZIP download using iframe (fixes hanging issue) - Add API endpoint to manually trigger auto backup - Add detailed backup logging for debugging - Add BACKUP_DIR env var support
1 parent 5ecf79f commit 68d05db

File tree

3 files changed

+96
-76
lines changed

3 files changed

+96
-76
lines changed
7.76 MB
Binary file not shown.

cms/app.py

Lines changed: 84 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
CMS_USERNAME = os.environ.get('CMS_USERNAME', 'admin')
2626
CMS_PASSWORD_HASH = os.environ.get('CMS_PASSWORD_HASH', '') # bcrypt hash
2727
CMS_PASSWORD = os.environ.get('CMS_PASSWORD', '') # Plain password (legacy, will hash it)
28-
BACKUP_DIR = os.path.join(os.path.dirname(CMS_BASE_DIR), '.way-cms-backups')
28+
BACKUP_DIR = os.environ.get('BACKUP_DIR', '/.way-cms-backups') # Fixed path for docker mount
2929
ALLOWED_EXTENSIONS = {'html', 'htm', 'css', 'js', 'txt', 'xml', 'json', 'md', 'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'zip', 'ico', 'woff', 'woff2', 'ttf', 'eot'}
3030
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB max file size
3131
READ_ONLY_MODE = os.environ.get('READ_ONLY_MODE', 'false').lower() == 'true'
@@ -1304,36 +1304,60 @@ def create_manual_backup():
13041304
@app.route('/api/folder-backups', methods=['GET'])
13051305
@login_required
13061306
def list_folder_backups():
1307-
"""List all folder backups (ZIP files)."""
1307+
"""List all folder backups (ZIP files) including automatic backups."""
13081308
folder_path = request.args.get('path', '')
1309+
1310+
backups = []
1311+
1312+
# Check manual backups directory
13091313
folder_backup_dir = os.path.join(BACKUP_DIR, 'folders', folder_path if folder_path else 'root')
1314+
if os.path.exists(folder_backup_dir):
1315+
try:
1316+
for item in os.listdir(folder_backup_dir):
1317+
if item.endswith('.zip'):
1318+
item_path = os.path.join(folder_backup_dir, item)
1319+
if os.path.isfile(item_path):
1320+
stat = os.stat(item_path)
1321+
backup_name = item[:-4] if item.endswith('.zip') else item
1322+
1323+
backups.append({
1324+
'name': backup_name,
1325+
'filename': item,
1326+
'path': item_path.replace(BACKUP_DIR, '').lstrip('/'),
1327+
'size': stat.st_size,
1328+
'modified': datetime.fromtimestamp(stat.st_mtime).isoformat(),
1329+
'formatted_date': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
1330+
'type': 'manual'
1331+
})
1332+
except Exception as e:
1333+
print(f"Error reading manual backups: {e}")
13101334

1311-
if not os.path.exists(folder_backup_dir):
1312-
return jsonify({'backups': []})
1335+
# Check automatic backups directory (only for root path)
1336+
if not folder_path:
1337+
auto_backup_dir = os.path.join(BACKUP_DIR, 'auto')
1338+
if os.path.exists(auto_backup_dir):
1339+
try:
1340+
for item in os.listdir(auto_backup_dir):
1341+
if item.endswith('.zip'):
1342+
item_path = os.path.join(auto_backup_dir, item)
1343+
if os.path.isfile(item_path):
1344+
stat = os.stat(item_path)
1345+
backup_name = item[:-4] if item.endswith('.zip') else item
1346+
1347+
backups.append({
1348+
'name': f"[Auto] {backup_name}",
1349+
'filename': item,
1350+
'path': f"auto/{item}",
1351+
'size': stat.st_size,
1352+
'modified': datetime.fromtimestamp(stat.st_mtime).isoformat(),
1353+
'formatted_date': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
1354+
'type': 'auto'
1355+
})
1356+
except Exception as e:
1357+
print(f"Error reading automatic backups: {e}")
13131358

1314-
backups = []
1315-
try:
1316-
for item in os.listdir(folder_backup_dir):
1317-
if item.endswith('.zip'):
1318-
item_path = os.path.join(folder_backup_dir, item)
1319-
if os.path.isfile(item_path):
1320-
stat = os.stat(item_path)
1321-
# Extract backup name (without .zip)
1322-
backup_name = item[:-4] if item.endswith('.zip') else item
1323-
1324-
backups.append({
1325-
'name': backup_name,
1326-
'filename': item,
1327-
'path': item_path.replace(BACKUP_DIR, '').lstrip('/'),
1328-
'size': stat.st_size,
1329-
'modified': datetime.fromtimestamp(stat.st_mtime).isoformat(),
1330-
'formatted_date': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S')
1331-
})
1332-
1333-
backups.sort(key=lambda x: x['modified'], reverse=True)
1334-
return jsonify({'backups': backups})
1335-
except Exception as e:
1336-
return jsonify({'error': str(e)}), 500
1359+
backups.sort(key=lambda x: x['modified'], reverse=True)
1360+
return jsonify({'backups': backups})
13371361

13381362

13391363
@app.route('/api/create-folder-backup', methods=['POST'])
@@ -1435,6 +1459,29 @@ def restore_folder_backup():
14351459
return jsonify({'error': str(e)}), 500
14361460

14371461

1462+
@app.route('/api/trigger-auto-backup', methods=['POST'])
1463+
@login_required
1464+
@read_only_check
1465+
def trigger_auto_backup():
1466+
"""Manually trigger an automatic backup."""
1467+
try:
1468+
backup_path = create_automatic_backup()
1469+
if backup_path:
1470+
stat = os.stat(backup_path)
1471+
return jsonify({
1472+
'success': True,
1473+
'backup': {
1474+
'path': backup_path.replace(BACKUP_DIR, '').lstrip('/'),
1475+
'size': stat.st_size,
1476+
'formatted_date': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S')
1477+
}
1478+
})
1479+
else:
1480+
return jsonify({'error': 'Failed to create backup'}), 500
1481+
except Exception as e:
1482+
return jsonify({'error': str(e)}), 500
1483+
1484+
14381485
@app.route('/api/delete-folder-backup', methods=['DELETE'])
14391486
@login_required
14401487
@read_only_check
@@ -1771,20 +1818,24 @@ def get_website_name_for_backup():
17711818
def create_automatic_backup():
17721819
"""Create an automatic backup of the entire website directory."""
17731820
if not AUTO_BACKUP_ENABLED:
1821+
print("[AutoBackup] Automatic backups disabled")
17741822
return None
17751823

17761824
try:
17771825
import zipfile
1778-
import io
1826+
1827+
print(f"[AutoBackup] Creating backup... BACKUP_DIR={BACKUP_DIR}, AUTO_BACKUP_DIR={AUTO_BACKUP_DIR}")
17791828

17801829
# Create auto backup directory
17811830
os.makedirs(AUTO_BACKUP_DIR, exist_ok=True)
1831+
print(f"[AutoBackup] Directory created/exists: {AUTO_BACKUP_DIR}")
17821832

17831833
# Generate backup filename with timestamp
17841834
website_name = get_website_name_for_backup()
17851835
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
17861836
backup_filename = f"{website_name}_{timestamp}.zip"
17871837
backup_path = os.path.join(AUTO_BACKUP_DIR, backup_filename)
1838+
print(f"[AutoBackup] Creating backup at: {backup_path}")
17881839

17891840
# Create ZIP backup
17901841
with zipfile.ZipFile(backup_path, 'w', zipfile.ZIP_DEFLATED) as zip_file:
@@ -1801,11 +1852,14 @@ def create_automatic_backup():
18011852
zip_file.write(file_path, arcname)
18021853
except (OSError, IOError) as e:
18031854
# Skip files that can't be read (permissions, etc.)
1804-
print(f"Warning: Could not backup {file_path}: {e}")
1855+
print(f"[AutoBackup] Warning: Could not backup {file_path}: {e}")
18051856

1857+
print(f"[AutoBackup] Backup created successfully: {backup_path}")
18061858
return backup_path
18071859
except Exception as e:
1808-
print(f"Error creating automatic backup: {e}")
1860+
print(f"[AutoBackup] Error creating automatic backup: {e}")
1861+
import traceback
1862+
traceback.print_exc()
18091863
return None
18101864

18111865
def manage_backup_retention():

cms/static/js/main.js

Lines changed: 12 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1917,52 +1917,18 @@ function downloadCurrentFolder() {
19171917
const url = `${API_BASE}/api/download-zip?path=${encodeURIComponent(path)}`;
19181918

19191919
console.log('Downloading ZIP from root folder:', url);
1920-
1921-
// Show loading indicator
1922-
showToast('Preparing ZIP download...');
1923-
1924-
// Use fetch to get the blob and download it properly
1925-
fetch(url, {
1926-
method: 'GET',
1927-
credentials: 'include' // Include cookies for authentication
1928-
})
1929-
.then(response => {
1930-
if (!response.ok) {
1931-
throw new Error(`HTTP error! status: ${response.status}`);
1932-
}
1933-
// Get the filename from Content-Disposition header or use default
1934-
const contentDisposition = response.headers.get('Content-Disposition');
1935-
let filename = 'website.zip';
1936-
if (contentDisposition) {
1937-
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
1938-
if (filenameMatch && filenameMatch[1]) {
1939-
filename = filenameMatch[1].replace(/['"]/g, '');
1940-
}
1941-
}
1942-
1943-
// Convert response to blob
1944-
return response.blob().then(blob => ({ blob, filename }));
1945-
})
1946-
.then(({ blob, filename }) => {
1947-
// Create blob URL and trigger download
1948-
const blobUrl = URL.createObjectURL(blob);
1949-
const link = document.createElement('a');
1950-
link.href = blobUrl;
1951-
link.download = filename;
1952-
document.body.appendChild(link);
1953-
link.click();
1954-
document.body.removeChild(link);
1955-
1956-
// Clean up blob URL after a delay
1957-
setTimeout(() => URL.revokeObjectURL(blobUrl), 100);
1958-
1959-
showToast('Download started');
1960-
})
1961-
.catch(err => {
1962-
console.error('Download error:', err);
1963-
showToast('Error downloading ZIP file. Check console for details.');
1964-
alert('Error downloading ZIP file: ' + err.message);
1965-
});
1920+
showToast('Starting ZIP download...');
1921+
1922+
// Simple approach: use an iframe to trigger download
1923+
// This avoids fetch blob issues with large files
1924+
let iframe = document.getElementById('download-iframe');
1925+
if (!iframe) {
1926+
iframe = document.createElement('iframe');
1927+
iframe.id = 'download-iframe';
1928+
iframe.style.display = 'none';
1929+
document.body.appendChild(iframe);
1930+
}
1931+
iframe.src = url;
19661932
}
19671933

19681934
// Upload ZIP functionality

0 commit comments

Comments
 (0)