"""备份服务:一键备份网站数据和资源""" import os import shutil import zipfile from datetime import datetime from flask import current_app class BackupService: """备份服务类""" # 备份目录(项目根目录下的 backups 文件夹) BACKUP_DIR = 'backups' @classmethod def get_backup_dir(cls): """获取备份目录路径""" # 相对于项目根目录 root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) backup_dir = os.path.join(root_dir, cls.BACKUP_DIR) # 确保备份目录存在 if not os.path.exists(backup_dir): os.makedirs(backup_dir) return backup_dir @classmethod def create_backup(cls, include_uploads=True): """ 创建完整备份 Args: include_uploads: 是否包含上传的图片附件 Returns: tuple: (backup_file_path, error_message) """ try: # 获取项目根目录 root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) # 备份文件名:techblog_backup_YYYYMMDD_HHMMSS.zip timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') backup_filename = f'techblog_backup_{timestamp}.zip' backup_filepath = os.path.join(cls.get_backup_dir(), backup_filename) # 数据库文件路径 db_path = os.path.join(root_dir, 'data', 'techblog.db') # 创建数据库备份文件(带时间戳) db_backup_filename = f'techblog_backup_{timestamp}.db' db_backup_path = os.path.join(root_dir, 'data', db_backup_filename) # 复制数据库文件 if os.path.exists(db_path): shutil.copy2(db_path, db_backup_path) # 上传目录路径 uploads_path = os.path.join(root_dir, 'uploads') # 创建 ZIP 备份文件 with zipfile.ZipFile(backup_filepath, 'w', zipfile.ZIP_DEFLATED) as zipf: # 1. 备份数据库(备份文件) if os.path.exists(db_backup_path): zipf.write(db_backup_path, f'data/{db_backup_filename}') # 删除临时备份文件 os.remove(db_backup_path) # 2. 备份上传的图片和附件(保持目录结构) if include_uploads and os.path.exists(uploads_path): for root, dirs, files in os.walk(uploads_path): for file in files: file_path = os.path.join(root, file) # 计算相对路径,保持目录结构 rel_path = os.path.relpath(file_path, uploads_path) arcname = os.path.join('uploads', rel_path) zipf.write(file_path, arcname) # 3. 写入备份信息 info_content = f"""tech-blog 备份信息 ======================== 备份时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} 数据库备份: {db_backup_filename} 上传文件: {'已包含' if include_uploads else '未包含'} 恢复说明: 1. 解压备份文件 2. 将 data/{db_backup_filename} 复制为 data/techblog.db 3. 将 uploads/ 目录复制到项目根目录 """ zipf.writestr('backup_info.txt', info_content) # 获取备份文件大小 file_size = os.path.getsize(backup_filepath) return { 'success': True, 'filepath': backup_filepath, 'filename': backup_filename, 'size': file_size, 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'db_backup_name': db_backup_filename }, None except Exception as e: # 清理临时文件 if 'db_backup_path' in locals() and os.path.exists(db_backup_path): os.remove(db_backup_path) return None, str(e) @classmethod def list_backups(cls): """ 列出所有备份文件 Returns: list: 备份文件列表,每个元素包含文件名、大小、时间 """ backup_dir = cls.get_backup_dir() backups = [] if not os.path.exists(backup_dir): return backups for filename in os.listdir(backup_dir): if filename.endswith('.zip') and filename.startswith('techblog_backup_'): filepath = os.path.join(backup_dir, filename) stat = os.stat(filepath) backups.append({ 'filename': filename, 'filepath': filepath, 'size': stat.st_size, 'created_at': datetime.fromtimestamp(stat.st_mtime) }) # 按时间倒序排列 backups.sort(key=lambda x: x['created_at'], reverse=True) return backups @classmethod def delete_backup(cls, filename): """ 删除备份文件 Args: filename: 备份文件名 Returns: tuple: (success, error_message) """ try: backup_dir = cls.get_backup_dir() filepath = os.path.join(backup_dir, filename) if not os.path.exists(filepath): return False, '备份文件不存在' os.remove(filepath) return True, None except Exception as e: return False, str(e) @classmethod def get_backup_path(cls, filename): """获取备份文件的完整路径""" return os.path.join(cls.get_backup_dir(), filename) @classmethod def format_filesize(cls, size_bytes): """格式化文件大小""" for unit in ['B', 'KB', 'MB', 'GB']: if size_bytes < 1024.0: return f"{size_bytes:.2f} {unit}" size_bytes /= 1024.0 return f"{size_bytes:.2f} TB"