1
+ import json
2
+ import os
3
+ from datetime import datetime
4
+ from tkinter import filedialog , messagebox
5
+ from logger import logger
6
+ from constants import CONFIG_FILE , SCHEDULE_FILE , CONFIG_VERSION
7
+
8
+ class BackupRestoreManager :
9
+ """处理应用程序数据的备份和还原"""
10
+
11
+ def __init__ (self , main_app ):
12
+ """
13
+ 初始化备份还原管理器。
14
+ Args:
15
+ main_app: CourseScheduler 主应用实例。
16
+ """
17
+ self .main_app = main_app
18
+ self .config_handler = main_app .config_handler
19
+
20
+ def export_data (self , selected_configs , include_schedule ):
21
+ """
22
+ 导出选定的配置和课表数据。
23
+ Args:
24
+ selected_configs (list): 要导出的配置名称列表。
25
+ include_schedule (bool): 是否包含课表数据。
26
+ """
27
+ backup_data = {
28
+ "metadata" : {
29
+ "export_date" : datetime .now ().isoformat (),
30
+ "config_version" : CONFIG_VERSION ,
31
+ "current_config_in_backup" : self .config_handler .config .get ("current_config" )
32
+ },
33
+ "configs" : None ,
34
+ "schedule" : None
35
+ }
36
+
37
+ # 1. 处理配置数据
38
+ if selected_configs :
39
+ all_configs = self .config_handler .config .get ("configs" , {})
40
+ configs_to_export = {name : all_configs [name ] for name in selected_configs if name in all_configs }
41
+ if configs_to_export :
42
+ backup_data ["configs" ] = configs_to_export
43
+
44
+ # 2. 处理课表数据
45
+ if include_schedule :
46
+ if os .path .exists (SCHEDULE_FILE ):
47
+ with open (SCHEDULE_FILE , 'r' , encoding = 'utf-8' ) as f :
48
+ backup_data ["schedule" ] = json .load (f )
49
+
50
+ # 3. 检查是否有效数据被导出
51
+ if not backup_data ["configs" ] and not backup_data ["schedule" ]:
52
+ messagebox .showwarning ("无数据" , "没有选择任何要导出的数据。" )
53
+ return
54
+
55
+ # 4. 弹出文件保存对话框
56
+ file_path = filedialog .asksaveasfilename (
57
+ title = "保存备份文件" ,
58
+ defaultextension = ".json" ,
59
+ filetypes = [("JSON files" , "*.json" ), ("All files" , "*.*" )],
60
+ initialfile = f"CourseScheduler_Backup_{ datetime .now ().strftime ('%Y%m%d_%H%M%S' )} .json"
61
+ )
62
+
63
+ if not file_path :
64
+ return # 用户取消
65
+
66
+ # 5. 写入文件
67
+ try :
68
+ with open (file_path , 'w' , encoding = 'utf-8' ) as f :
69
+ json .dump (backup_data , f , ensure_ascii = False , indent = 4 )
70
+ messagebox .showinfo ("成功" , f"数据已成功导出到:\n { file_path } " )
71
+ except Exception as e :
72
+ logger .log_error (f"导出数据失败: { e } " )
73
+ messagebox .showerror ("错误" , f"导出失败: { e } " )
74
+
75
+ def import_data (self , mode ):
76
+ """
77
+ 导入数据。
78
+ Args:
79
+ mode (str): 'incremental' 或 'overwrite'。
80
+ """
81
+ file_path = filedialog .askopenfilename (
82
+ title = "选择备份文件进行导入" ,
83
+ filetypes = [("JSON files" , "*.json" ), ("All files" , "*.*" )]
84
+ )
85
+
86
+ if not file_path :
87
+ return # 用户取消
88
+
89
+ try :
90
+ with open (file_path , 'r' , encoding = 'utf-8' ) as f :
91
+ backup_data = json .load (f )
92
+ except Exception as e :
93
+ logger .log_error (f"读取备份文件失败: { e } " )
94
+ messagebox .showerror ("错误" , f"无法读取或解析备份文件: { e } " )
95
+ return
96
+
97
+ # 验证备份文件
98
+ if "metadata" not in backup_data or "configs" not in backup_data or "schedule" not in backup_data :
99
+ messagebox .showerror ("文件无效" , "选择的文件不是一个有效的备份文件。" )
100
+ return
101
+
102
+ # 版本兼容性检查
103
+ backup_version = backup_data .get ("metadata" , {}).get ("config_version" )
104
+ if backup_version and backup_version > CONFIG_VERSION :
105
+ if not messagebox .askyesno ("版本不兼容警告" ,
106
+ f"备份文件版本 ({ backup_version } ) 高于当前应用版本 ({ CONFIG_VERSION } )。\n "
107
+ "导入可能会导致未知问题。\n \n "
108
+ "您确定要继续吗?" ):
109
+ return
110
+
111
+ # 确认操作
112
+ if not messagebox .askyesno ("确认导入" , f"确定要以 '{ mode } ' 模式导入数据吗?\n 这将修改您当前的配置和课表,建议操作前先进行备份。" ):
113
+ return
114
+
115
+ # 根据模式执行导入
116
+ try :
117
+ if mode == 'overwrite' :
118
+ self ._overwrite_import (backup_data )
119
+ elif mode == 'incremental' :
120
+ self ._incremental_import (backup_data )
121
+
122
+ # 刷新运行时状态
123
+ self .config_handler ._load_attributes_from_config ()
124
+
125
+ # 如果编辑器窗口是打开的,警告并关闭它以防止数据冲突
126
+ if self .main_app .editor_window and self .main_app .editor_window .window .winfo_exists ():
127
+ messagebox .showwarning ("编辑器已关闭" , "为防止数据冲突,课表编辑器窗口已关闭。请重新打开以查看更新后的课表。" )
128
+ self .main_app .editor_window .window .destroy ()
129
+
130
+ # 提示重启
131
+ messagebox .showinfo ("成功" , "数据导入成功!\n 为了使所有更改完全生效,建议您重启应用程序。" )
132
+
133
+ except Exception as e :
134
+ logger .log_error (f"导入数据时发生错误: { e } " )
135
+ messagebox .showerror ("导入失败" , f"处理导入数据时发生错误: { e } " )
136
+
137
+ def _overwrite_import (self , backup_data ):
138
+ """执行覆盖导入(使用临时文件保证原子性)"""
139
+ config_to_write = None
140
+ schedule_to_write = None
141
+
142
+ # 准备要写入的数据
143
+ if backup_data .get ("configs" ):
144
+ new_config_data = self .config_handler .config .copy ()
145
+ new_config_data ["configs" ] = backup_data ["configs" ]
146
+ backup_current = backup_data .get ("metadata" , {}).get ("current_config_in_backup" )
147
+ if backup_current and backup_current in backup_data ["configs" ]:
148
+ new_config_data ["current_config" ] = backup_current
149
+ else :
150
+ new_config_data ["current_config" ] = next (iter (backup_data ["configs" ]))
151
+ config_to_write = new_config_data
152
+
153
+ if backup_data .get ("schedule" ):
154
+ schedule_to_write = backup_data ["schedule" ]
155
+
156
+ # 原子化写入
157
+ self ._atomic_write (config_to_write , schedule_to_write )
158
+
159
+ # 更新内存状态
160
+ if config_to_write :
161
+ self .config_handler .config = config_to_write
162
+ if schedule_to_write :
163
+ self .main_app .schedule = schedule_to_write
164
+
165
+ def _incremental_import (self , backup_data ):
166
+ """执行增量导入"""
167
+ """执行增量导入(使用临时文件保证原子性)"""
168
+ config_to_write = None
169
+ schedule_to_write = None
170
+
171
+ # 准备要写入的配置数据
172
+ if backup_data .get ("configs" ):
173
+ new_config_data = self .config_handler .config .copy ()
174
+ for name , config_data in backup_data ["configs" ].items ():
175
+ new_config_data ["configs" ][name ] = config_data
176
+ config_to_write = new_config_data
177
+
178
+ # 准备要写入的课表数据
179
+ if backup_data .get ("schedule" ):
180
+ if os .path .exists (SCHEDULE_FILE ):
181
+ with open (SCHEDULE_FILE , 'r' , encoding = 'utf-8' ) as f :
182
+ current_schedule = json .load (f )
183
+ else :
184
+ current_schedule = {"schedules" : {}}
185
+
186
+ for name , schedule_data in backup_data ["schedule" ]["schedules" ].items ():
187
+ current_schedule ["schedules" ][name ] = schedule_data
188
+ schedule_to_write = current_schedule
189
+
190
+ # 原子化写入
191
+ self ._atomic_write (config_to_write , schedule_to_write )
192
+
193
+ # 更新内存状态
194
+ if config_to_write :
195
+ self .config_handler .config = config_to_write
196
+ if schedule_to_write :
197
+ self .main_app .schedule = schedule_to_write
198
+
199
+ def _atomic_write (self , config_data , schedule_data ):
200
+ """
201
+ 将数据原子化地写入配置文件。
202
+ 先写入.tmp文件,成功后再替换原文件。
203
+ """
204
+ config_tmp_file = CONFIG_FILE + ".tmp"
205
+ schedule_tmp_file = SCHEDULE_FILE + ".tmp"
206
+
207
+ try :
208
+ # 写入临时文件
209
+ if config_data :
210
+ with open (config_tmp_file , 'w' , encoding = 'utf-8' ) as f :
211
+ json .dump (config_data , f , ensure_ascii = False , indent = 4 )
212
+ if schedule_data :
213
+ with open (schedule_tmp_file , 'w' , encoding = 'utf-8' ) as f :
214
+ json .dump (schedule_data , f , ensure_ascii = False , indent = 4 )
215
+
216
+ # 替换原文件
217
+ if config_data :
218
+ os .replace (config_tmp_file , CONFIG_FILE )
219
+ if schedule_data :
220
+ os .replace (schedule_tmp_file , SCHEDULE_FILE )
221
+
222
+ except Exception as e :
223
+ # 如果出错,清理临时文件
224
+ if os .path .exists (config_tmp_file ):
225
+ os .remove (config_tmp_file )
226
+ if os .path .exists (schedule_tmp_file ):
227
+ os .remove (schedule_tmp_file )
228
+ raise e # 将异常向上抛出,由调用者处理
0 commit comments