ErisPulse 2.1.14.dev2__py3-none-any.whl → 2.2.0.dev0__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.
- ErisPulse/Core/Event/__init__.py +49 -0
- ErisPulse/Core/Event/base.py +118 -0
- ErisPulse/Core/Event/cmd.py +212 -0
- ErisPulse/Core/Event/exceptions.py +37 -0
- ErisPulse/Core/Event/manager.py +128 -0
- ErisPulse/Core/Event/message.py +91 -0
- ErisPulse/Core/Event/meta.py +82 -0
- ErisPulse/Core/Event/notice.py +97 -0
- ErisPulse/Core/Event/request.py +67 -0
- ErisPulse/Core/__init__.py +6 -0
- ErisPulse/Core/adapter.py +7 -1
- ErisPulse/Core/config.py +4 -0
- ErisPulse/Core/env.py +8 -535
- ErisPulse/Core/logger.py +78 -1
- ErisPulse/Core/mods.py +22 -18
- ErisPulse/Core/router.py +6 -1
- ErisPulse/Core/storage.py +547 -0
- ErisPulse/__init__.py +29 -18
- ErisPulse/__main__.py +991 -71
- {erispulse-2.1.14.dev2.dist-info → erispulse-2.2.0.dev0.dist-info}/METADATA +40 -19
- erispulse-2.2.0.dev0.dist-info/RECORD +26 -0
- {erispulse-2.1.14.dev2.dist-info → erispulse-2.2.0.dev0.dist-info}/licenses/LICENSE +7 -1
- erispulse-2.1.14.dev2.dist-info/RECORD +0 -16
- {erispulse-2.1.14.dev2.dist-info → erispulse-2.2.0.dev0.dist-info}/WHEEL +0 -0
- {erispulse-2.1.14.dev2.dist-info → erispulse-2.2.0.dev0.dist-info}/entry_points.txt +0 -0
ErisPulse/Core/mods.py
CHANGED
|
@@ -30,8 +30,8 @@ class ModuleManager:
|
|
|
30
30
|
DEFAULT_STATUS_PREFIX = "erispulse.data.modules.status:"
|
|
31
31
|
|
|
32
32
|
def __init__(self):
|
|
33
|
-
from .
|
|
34
|
-
self.
|
|
33
|
+
from .storage import storage
|
|
34
|
+
self.storage = storage
|
|
35
35
|
self._ensure_prefixes()
|
|
36
36
|
|
|
37
37
|
def _ensure_prefixes(self) -> None:
|
|
@@ -39,10 +39,10 @@ class ModuleManager:
|
|
|
39
39
|
{!--< internal-use >!--}
|
|
40
40
|
确保模块前缀配置存在
|
|
41
41
|
"""
|
|
42
|
-
if not self.
|
|
43
|
-
self.
|
|
44
|
-
if not self.
|
|
45
|
-
self.
|
|
42
|
+
if not self.storage.get("erispulse.system.module_prefix"):
|
|
43
|
+
self.storage.set("erispulse.system.module_prefix", self.DEFAULT_MODULE_PREFIX)
|
|
44
|
+
if not self.storage.get("erispulse.system.status_prefix"):
|
|
45
|
+
self.storage.set("erispulse.system.status_prefix", self.DEFAULT_STATUS_PREFIX)
|
|
46
46
|
|
|
47
47
|
@property
|
|
48
48
|
def module_prefix(self) -> str:
|
|
@@ -51,7 +51,7 @@ class ModuleManager:
|
|
|
51
51
|
|
|
52
52
|
:return: 模块数据前缀字符串
|
|
53
53
|
"""
|
|
54
|
-
return self.
|
|
54
|
+
return self.storage.get("erispulse.system.module_prefix")
|
|
55
55
|
|
|
56
56
|
@property
|
|
57
57
|
def status_prefix(self) -> str:
|
|
@@ -60,7 +60,7 @@ class ModuleManager:
|
|
|
60
60
|
|
|
61
61
|
:return: 模块状态前缀字符串
|
|
62
62
|
"""
|
|
63
|
-
return self.
|
|
63
|
+
return self.storage.get("erispulse.system.status_prefix")
|
|
64
64
|
|
|
65
65
|
def set_module_status(self, module_name: str, status: bool) -> None:
|
|
66
66
|
"""
|
|
@@ -76,7 +76,7 @@ class ModuleManager:
|
|
|
76
76
|
>>> mods.set_module_status("MyModule", False)
|
|
77
77
|
"""
|
|
78
78
|
from .logger import logger
|
|
79
|
-
self.
|
|
79
|
+
self.storage.set(f"{self.status_prefix}{module_name}", bool(status))
|
|
80
80
|
logger.debug(f"模块 {module_name} 状态已设置为 {status}")
|
|
81
81
|
|
|
82
82
|
def get_module_status(self, module_name: str) -> bool:
|
|
@@ -90,7 +90,7 @@ class ModuleManager:
|
|
|
90
90
|
>>> if mods.get_module_status("MyModule"):
|
|
91
91
|
>>> print("模块已启用")
|
|
92
92
|
"""
|
|
93
|
-
status = self.
|
|
93
|
+
status = self.storage.get(f"{self.status_prefix}{module_name}", True)
|
|
94
94
|
if isinstance(status, str):
|
|
95
95
|
return status.lower() not in ('false', '0', 'no', 'off')
|
|
96
96
|
return bool(status)
|
|
@@ -108,7 +108,7 @@ class ModuleManager:
|
|
|
108
108
|
>>> "description": "我的模块",
|
|
109
109
|
>>> })
|
|
110
110
|
"""
|
|
111
|
-
self.
|
|
111
|
+
self.storage.set(f"{self.module_prefix}{module_name}", module_info)
|
|
112
112
|
|
|
113
113
|
def get_module(self, module_name: str) -> Optional[Dict[str, Any]]:
|
|
114
114
|
"""
|
|
@@ -122,7 +122,7 @@ class ModuleManager:
|
|
|
122
122
|
>>> if module_info:
|
|
123
123
|
>>> print(f"模块版本: {module_info.get('version')}")
|
|
124
124
|
"""
|
|
125
|
-
return self.
|
|
125
|
+
return self.storage.get(f"{self.module_prefix}{module_name}")
|
|
126
126
|
|
|
127
127
|
def set_all_modules(self, modules_info: Dict[str, Dict[str, Any]]) -> None:
|
|
128
128
|
"""
|
|
@@ -151,7 +151,7 @@ class ModuleManager:
|
|
|
151
151
|
>>> print(f"{name}: {info.get('status')}")
|
|
152
152
|
"""
|
|
153
153
|
modules_info = {}
|
|
154
|
-
all_keys = self.
|
|
154
|
+
all_keys = self.storage.get_all_keys()
|
|
155
155
|
prefix_len = len(self.module_prefix)
|
|
156
156
|
|
|
157
157
|
for key in all_keys:
|
|
@@ -185,9 +185,9 @@ class ModuleManager:
|
|
|
185
185
|
module_key = f"{self.module_prefix}{module_name}"
|
|
186
186
|
status_key = f"{self.status_prefix}{module_name}"
|
|
187
187
|
|
|
188
|
-
if self.
|
|
189
|
-
self.
|
|
190
|
-
self.
|
|
188
|
+
if self.storage.get(module_key) is not None:
|
|
189
|
+
self.storage.delete(module_key)
|
|
190
|
+
self.storage.delete(status_key)
|
|
191
191
|
return True
|
|
192
192
|
return False
|
|
193
193
|
|
|
@@ -208,12 +208,16 @@ class ModuleManager:
|
|
|
208
208
|
if module_prefix:
|
|
209
209
|
if not module_prefix.endswith(':'):
|
|
210
210
|
module_prefix += ':'
|
|
211
|
-
self.
|
|
211
|
+
self.storage.set("erispulse.system.module_prefix", module_prefix)
|
|
212
212
|
|
|
213
213
|
if status_prefix:
|
|
214
214
|
if not status_prefix.endswith(':'):
|
|
215
215
|
status_prefix += ':'
|
|
216
|
-
self.
|
|
216
|
+
self.storage.set("erispulse.system.status_prefix", status_prefix)
|
|
217
217
|
|
|
218
218
|
|
|
219
219
|
mods = ModuleManager()
|
|
220
|
+
|
|
221
|
+
__all__ = [
|
|
222
|
+
"mods",
|
|
223
|
+
]
|
ErisPulse/Core/router.py
CHANGED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ErisPulse 存储管理模块
|
|
3
|
+
|
|
4
|
+
提供键值存储、事务支持、快照和恢复功能,用于管理框架运行时数据。
|
|
5
|
+
基于SQLite实现持久化存储,支持复杂数据类型和原子操作。
|
|
6
|
+
|
|
7
|
+
{!--< tips >!--}
|
|
8
|
+
1. 支持JSON序列化存储复杂数据类型
|
|
9
|
+
2. 提供事务支持确保数据一致性
|
|
10
|
+
3. 自动快照功能防止数据丢失
|
|
11
|
+
{!--< /tips >!--}
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import json
|
|
16
|
+
import sqlite3
|
|
17
|
+
import shutil
|
|
18
|
+
import time
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from typing import List, Dict, Optional, Any, Tuple, Type
|
|
21
|
+
|
|
22
|
+
class StorageManager:
|
|
23
|
+
"""
|
|
24
|
+
存储管理器
|
|
25
|
+
|
|
26
|
+
单例模式实现,提供键值存储的增删改查、事务和快照管理
|
|
27
|
+
|
|
28
|
+
{!--< tips >!--}
|
|
29
|
+
1. 使用get/set方法操作存储项
|
|
30
|
+
2. 使用transaction上下文管理事务
|
|
31
|
+
3. 使用snapshot/restore管理数据快照
|
|
32
|
+
{!--< /tips >!--}
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
_instance = None
|
|
36
|
+
db_path = os.path.join(os.path.dirname(__file__), "../data/config.db")
|
|
37
|
+
SNAPSHOT_DIR = os.path.join(os.path.dirname(__file__), "../data/snapshots")
|
|
38
|
+
|
|
39
|
+
def __new__(cls, *args, **kwargs):
|
|
40
|
+
if not cls._instance:
|
|
41
|
+
cls._instance = super().__new__(cls)
|
|
42
|
+
return cls._instance
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
if not hasattr(self, "_initialized"):
|
|
46
|
+
# 确保关键属性在初始化时都有默认值
|
|
47
|
+
self._last_snapshot_time = time.time()
|
|
48
|
+
self._snapshot_interval = 3600
|
|
49
|
+
self._init_db()
|
|
50
|
+
self._initialized = True
|
|
51
|
+
|
|
52
|
+
def _init_db(self) -> None:
|
|
53
|
+
"""
|
|
54
|
+
{!--< internal-use >!--}
|
|
55
|
+
初始化数据库
|
|
56
|
+
"""
|
|
57
|
+
os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
|
|
58
|
+
os.makedirs(self.SNAPSHOT_DIR, exist_ok=True)
|
|
59
|
+
conn = sqlite3.connect(self.db_path)
|
|
60
|
+
|
|
61
|
+
# 启用WAL模式提高并发性能
|
|
62
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
63
|
+
conn.execute("PRAGMA synchronous=NORMAL")
|
|
64
|
+
|
|
65
|
+
cursor = conn.cursor()
|
|
66
|
+
cursor.execute("""
|
|
67
|
+
CREATE TABLE IF NOT EXISTS config (
|
|
68
|
+
key TEXT PRIMARY KEY,
|
|
69
|
+
value TEXT NOT NULL
|
|
70
|
+
)
|
|
71
|
+
""")
|
|
72
|
+
conn.commit()
|
|
73
|
+
conn.close()
|
|
74
|
+
|
|
75
|
+
# 初始化自动快照调度器
|
|
76
|
+
self._last_snapshot_time = time.time() # 初始化为当前时间
|
|
77
|
+
self._snapshot_interval = 3600 # 默认每小时自动快照
|
|
78
|
+
|
|
79
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
80
|
+
"""
|
|
81
|
+
获取存储项的值
|
|
82
|
+
|
|
83
|
+
:param key: 存储项键名
|
|
84
|
+
:param default: 默认值(当键不存在时返回)
|
|
85
|
+
:return: 存储项的值
|
|
86
|
+
|
|
87
|
+
:example:
|
|
88
|
+
>>> timeout = storage.get("network.timeout", 30)
|
|
89
|
+
>>> user_settings = storage.get("user.settings", {})
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
with sqlite3.connect(self.db_path) as conn:
|
|
93
|
+
cursor = conn.cursor()
|
|
94
|
+
cursor.execute("SELECT value FROM config WHERE key = ?", (key,))
|
|
95
|
+
result = cursor.fetchone()
|
|
96
|
+
if result:
|
|
97
|
+
try:
|
|
98
|
+
return json.loads(result[0])
|
|
99
|
+
except json.JSONDecodeError:
|
|
100
|
+
return result[0]
|
|
101
|
+
return default
|
|
102
|
+
except sqlite3.OperationalError as e:
|
|
103
|
+
if "no such table" in str(e):
|
|
104
|
+
self._init_db()
|
|
105
|
+
return self.get(key, default)
|
|
106
|
+
else:
|
|
107
|
+
from . import logger
|
|
108
|
+
logger.error(f"数据库操作错误: {e}")
|
|
109
|
+
|
|
110
|
+
def get_all_keys(self) -> List[str]:
|
|
111
|
+
"""
|
|
112
|
+
获取所有存储项的键名
|
|
113
|
+
|
|
114
|
+
:return: 键名列表
|
|
115
|
+
|
|
116
|
+
:example:
|
|
117
|
+
>>> all_keys = storage.get_all_keys()
|
|
118
|
+
>>> print(f"共有 {len(all_keys)} 个存储项")
|
|
119
|
+
"""
|
|
120
|
+
with sqlite3.connect(self.db_path) as conn:
|
|
121
|
+
cursor = conn.cursor()
|
|
122
|
+
cursor.execute("SELECT key FROM config")
|
|
123
|
+
return [row[0] for row in cursor.fetchall()]
|
|
124
|
+
|
|
125
|
+
def set(self, key: str, value: Any) -> bool:
|
|
126
|
+
"""
|
|
127
|
+
设置存储项的值
|
|
128
|
+
|
|
129
|
+
:param key: 存储项键名
|
|
130
|
+
:param value: 存储项的值
|
|
131
|
+
:return: 操作是否成功
|
|
132
|
+
|
|
133
|
+
:example:
|
|
134
|
+
>>> storage.set("app.name", "MyApp")
|
|
135
|
+
>>> storage.set("user.settings", {"theme": "dark"})
|
|
136
|
+
"""
|
|
137
|
+
try:
|
|
138
|
+
serialized_value = json.dumps(value) if isinstance(value, (dict, list)) else str(value)
|
|
139
|
+
with self.transaction():
|
|
140
|
+
conn = sqlite3.connect(self.db_path)
|
|
141
|
+
cursor = conn.cursor()
|
|
142
|
+
cursor.execute("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)", (key, serialized_value))
|
|
143
|
+
conn.commit()
|
|
144
|
+
conn.close()
|
|
145
|
+
|
|
146
|
+
self._check_auto_snapshot()
|
|
147
|
+
return True
|
|
148
|
+
except Exception as e:
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
def set_multi(self, items: Dict[str, Any]) -> bool:
|
|
152
|
+
"""
|
|
153
|
+
批量设置多个存储项
|
|
154
|
+
|
|
155
|
+
:param items: 键值对字典
|
|
156
|
+
:return: 操作是否成功
|
|
157
|
+
|
|
158
|
+
:example:
|
|
159
|
+
>>> storage.set_multi({
|
|
160
|
+
>>> "app.name": "MyApp",
|
|
161
|
+
>>> "app.version": "1.0.0",
|
|
162
|
+
>>> "app.debug": True
|
|
163
|
+
>>> })
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
with self.transaction():
|
|
167
|
+
conn = sqlite3.connect(self.db_path)
|
|
168
|
+
cursor = conn.cursor()
|
|
169
|
+
for key, value in items.items():
|
|
170
|
+
serialized_value = json.dumps(value) if isinstance(value, (dict, list)) else str(value)
|
|
171
|
+
cursor.execute("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)",
|
|
172
|
+
(key, serialized_value))
|
|
173
|
+
conn.commit()
|
|
174
|
+
conn.close()
|
|
175
|
+
|
|
176
|
+
self._check_auto_snapshot()
|
|
177
|
+
return True
|
|
178
|
+
except Exception as e:
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
def getConfig(self, key: str, default: Any = None) -> Any:
|
|
182
|
+
"""
|
|
183
|
+
获取模块/适配器配置项(委托给config模块)
|
|
184
|
+
:param key: 配置项的键(支持点分隔符如"module.sub.key")
|
|
185
|
+
:param default: 默认值
|
|
186
|
+
:return: 配置项的值
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
from .config import config
|
|
190
|
+
return config.getConfig(key, default)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
return default
|
|
193
|
+
|
|
194
|
+
def setConfig(self, key: str, value: Any) -> bool:
|
|
195
|
+
"""
|
|
196
|
+
设置模块/适配器配置(委托给config模块)
|
|
197
|
+
:param key: 配置项键名(支持点分隔符如"module.sub.key")
|
|
198
|
+
:param value: 配置项值
|
|
199
|
+
:return: 操作是否成功
|
|
200
|
+
"""
|
|
201
|
+
try:
|
|
202
|
+
from .config import config
|
|
203
|
+
return config.setConfig(key, value)
|
|
204
|
+
except Exception as e:
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
def delete(self, key: str) -> bool:
|
|
208
|
+
"""
|
|
209
|
+
删除存储项
|
|
210
|
+
|
|
211
|
+
:param key: 存储项键名
|
|
212
|
+
:return: 操作是否成功
|
|
213
|
+
|
|
214
|
+
:example:
|
|
215
|
+
>>> storage.delete("temp.session")
|
|
216
|
+
"""
|
|
217
|
+
try:
|
|
218
|
+
with self.transaction():
|
|
219
|
+
conn = sqlite3.connect(self.db_path)
|
|
220
|
+
cursor = conn.cursor()
|
|
221
|
+
cursor.execute("DELETE FROM config WHERE key = ?", (key,))
|
|
222
|
+
conn.commit()
|
|
223
|
+
conn.close()
|
|
224
|
+
|
|
225
|
+
self._check_auto_snapshot()
|
|
226
|
+
return True
|
|
227
|
+
except Exception as e:
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
def delete_multi(self, keys: List[str]) -> bool:
|
|
231
|
+
"""
|
|
232
|
+
批量删除多个存储项
|
|
233
|
+
|
|
234
|
+
:param keys: 键名列表
|
|
235
|
+
:return: 操作是否成功
|
|
236
|
+
|
|
237
|
+
:example:
|
|
238
|
+
>>> storage.delete_multi(["temp.key1", "temp.key2"])
|
|
239
|
+
"""
|
|
240
|
+
try:
|
|
241
|
+
with self.transaction():
|
|
242
|
+
conn = sqlite3.connect(self.db_path)
|
|
243
|
+
cursor = conn.cursor()
|
|
244
|
+
cursor.executemany("DELETE FROM config WHERE key = ?", [(k,) for k in keys])
|
|
245
|
+
conn.commit()
|
|
246
|
+
conn.close()
|
|
247
|
+
|
|
248
|
+
self._check_auto_snapshot()
|
|
249
|
+
return True
|
|
250
|
+
except Exception as e:
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
def get_multi(self, keys: List[str]) -> Dict[str, Any]:
|
|
254
|
+
"""
|
|
255
|
+
批量获取多个存储项的值
|
|
256
|
+
|
|
257
|
+
:param keys: 键名列表
|
|
258
|
+
:return: 键值对字典
|
|
259
|
+
|
|
260
|
+
:example:
|
|
261
|
+
>>> settings = storage.get_multi(["app.name", "app.version"])
|
|
262
|
+
"""
|
|
263
|
+
conn = sqlite3.connect(self.db_path)
|
|
264
|
+
cursor = conn.cursor()
|
|
265
|
+
placeholders = ','.join(['?'] * len(keys))
|
|
266
|
+
cursor.execute(f"SELECT key, value FROM config WHERE key IN ({placeholders})", keys)
|
|
267
|
+
results = {row[0]: json.loads(row[1]) if row[1].startswith(('{', '[')) else row[1]
|
|
268
|
+
for row in cursor.fetchall()}
|
|
269
|
+
conn.close()
|
|
270
|
+
return results
|
|
271
|
+
|
|
272
|
+
def transaction(self) -> 'StorageManager._Transaction':
|
|
273
|
+
"""
|
|
274
|
+
创建事务上下文
|
|
275
|
+
|
|
276
|
+
:return: 事务上下文管理器
|
|
277
|
+
|
|
278
|
+
:example:
|
|
279
|
+
>>> with storage.transaction():
|
|
280
|
+
>>> storage.set("key1", "value1")
|
|
281
|
+
>>> storage.set("key2", "value2")
|
|
282
|
+
"""
|
|
283
|
+
return self._Transaction(self)
|
|
284
|
+
|
|
285
|
+
class _Transaction:
|
|
286
|
+
"""
|
|
287
|
+
事务上下文管理器
|
|
288
|
+
|
|
289
|
+
{!--< internal-use >!--}
|
|
290
|
+
确保多个操作的原子性
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
def __init__(self, storage_manager: 'StorageManager'):
|
|
294
|
+
self.storage_manager = storage_manager
|
|
295
|
+
self.conn = None
|
|
296
|
+
self.cursor = None
|
|
297
|
+
|
|
298
|
+
def __enter__(self) -> 'StorageManager._Transaction':
|
|
299
|
+
"""
|
|
300
|
+
进入事务上下文
|
|
301
|
+
"""
|
|
302
|
+
self.conn = sqlite3.connect(self.storage_manager.db_path)
|
|
303
|
+
self.cursor = self.conn.cursor()
|
|
304
|
+
self.cursor.execute("BEGIN TRANSACTION")
|
|
305
|
+
return self
|
|
306
|
+
|
|
307
|
+
def __exit__(self, exc_type: Type[Exception], exc_val: Exception, exc_tb: Any) -> None:
|
|
308
|
+
"""
|
|
309
|
+
退出事务上下文
|
|
310
|
+
"""
|
|
311
|
+
if exc_type is None:
|
|
312
|
+
self.conn.commit()
|
|
313
|
+
else:
|
|
314
|
+
self.conn.rollback()
|
|
315
|
+
from .logger import logger
|
|
316
|
+
logger.error(f"事务执行失败: {exc_val}")
|
|
317
|
+
self.conn.close()
|
|
318
|
+
|
|
319
|
+
def _check_auto_snapshot(self) -> None:
|
|
320
|
+
"""
|
|
321
|
+
{!--< internal-use >!--}
|
|
322
|
+
检查并执行自动快照
|
|
323
|
+
"""
|
|
324
|
+
from .logger import logger
|
|
325
|
+
|
|
326
|
+
if not hasattr(self, '_last_snapshot_time') or self._last_snapshot_time is None:
|
|
327
|
+
self._last_snapshot_time = time.time()
|
|
328
|
+
|
|
329
|
+
if not hasattr(self, '_snapshot_interval') or self._snapshot_interval is None:
|
|
330
|
+
self._snapshot_interval = 3600
|
|
331
|
+
|
|
332
|
+
current_time = time.time()
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
time_diff = current_time - self._last_snapshot_time
|
|
336
|
+
if not isinstance(time_diff, (int, float)):
|
|
337
|
+
raise ValueError("时间差应为数值类型")
|
|
338
|
+
|
|
339
|
+
if not isinstance(self._snapshot_interval, (int, float)):
|
|
340
|
+
raise ValueError("快照间隔应为数值类型")
|
|
341
|
+
|
|
342
|
+
if time_diff > self._snapshot_interval:
|
|
343
|
+
self._last_snapshot_time = current_time
|
|
344
|
+
self.snapshot(f"auto_{datetime.now().strftime('%Y%m%d_%H%M%S')}")
|
|
345
|
+
|
|
346
|
+
except Exception as e:
|
|
347
|
+
logger.error(f"自动快照检查失败: {e}")
|
|
348
|
+
self._last_snapshot_time = current_time
|
|
349
|
+
self._snapshot_interval = 3600
|
|
350
|
+
|
|
351
|
+
def set_snapshot_interval(self, seconds: int) -> None:
|
|
352
|
+
"""
|
|
353
|
+
设置自动快照间隔
|
|
354
|
+
|
|
355
|
+
:param seconds: 间隔秒数
|
|
356
|
+
|
|
357
|
+
:example:
|
|
358
|
+
>>> # 每30分钟自动快照
|
|
359
|
+
>>> storage.set_snapshot_interval(1800)
|
|
360
|
+
"""
|
|
361
|
+
self._snapshot_interval = seconds
|
|
362
|
+
|
|
363
|
+
def clear(self) -> bool:
|
|
364
|
+
"""
|
|
365
|
+
清空所有存储项
|
|
366
|
+
|
|
367
|
+
:return: 操作是否成功
|
|
368
|
+
|
|
369
|
+
:example:
|
|
370
|
+
>>> storage.clear() # 清空所有存储
|
|
371
|
+
"""
|
|
372
|
+
try:
|
|
373
|
+
conn = sqlite3.connect(self.db_path)
|
|
374
|
+
cursor = conn.cursor()
|
|
375
|
+
cursor.execute("DELETE FROM config")
|
|
376
|
+
conn.commit()
|
|
377
|
+
conn.close()
|
|
378
|
+
return True
|
|
379
|
+
except Exception as e:
|
|
380
|
+
return False
|
|
381
|
+
|
|
382
|
+
def __getattr__(self, key: str) -> Any:
|
|
383
|
+
"""
|
|
384
|
+
通过属性访问存储项
|
|
385
|
+
|
|
386
|
+
:param key: 存储项键名
|
|
387
|
+
:return: 存储项的值
|
|
388
|
+
|
|
389
|
+
:raises KeyError: 当存储项不存在时抛出
|
|
390
|
+
|
|
391
|
+
:example:
|
|
392
|
+
>>> app_name = storage.app_name
|
|
393
|
+
"""
|
|
394
|
+
try:
|
|
395
|
+
return self.get(key)
|
|
396
|
+
except KeyError:
|
|
397
|
+
from . import logger
|
|
398
|
+
logger.error(f"存储项 {key} 不存在")
|
|
399
|
+
|
|
400
|
+
def __setattr__(self, key: str, value: Any) -> None:
|
|
401
|
+
"""
|
|
402
|
+
通过属性设置存储项
|
|
403
|
+
|
|
404
|
+
:param key: 存储项键名
|
|
405
|
+
:param value: 存储项的值
|
|
406
|
+
|
|
407
|
+
:example:
|
|
408
|
+
>>> storage.app_name = "MyApp"
|
|
409
|
+
"""
|
|
410
|
+
try:
|
|
411
|
+
self.set(key, value)
|
|
412
|
+
except Exception as e:
|
|
413
|
+
from . import logger
|
|
414
|
+
logger.error(f"设置存储项 {key} 失败: {e}")
|
|
415
|
+
|
|
416
|
+
def snapshot(self, name: Optional[str] = None) -> str:
|
|
417
|
+
"""
|
|
418
|
+
创建数据库快照
|
|
419
|
+
|
|
420
|
+
:param name: 快照名称(可选)
|
|
421
|
+
:return: 快照文件路径
|
|
422
|
+
|
|
423
|
+
:example:
|
|
424
|
+
>>> # 创建命名快照
|
|
425
|
+
>>> snapshot_path = storage.snapshot("before_update")
|
|
426
|
+
>>> # 创建时间戳快照
|
|
427
|
+
>>> snapshot_path = storage.snapshot()
|
|
428
|
+
"""
|
|
429
|
+
if not name:
|
|
430
|
+
name = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
431
|
+
snapshot_path = os.path.join(self.SNAPSHOT_DIR, f"{name}.db")
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
# 快照目录
|
|
435
|
+
os.makedirs(self.SNAPSHOT_DIR, exist_ok=True)
|
|
436
|
+
|
|
437
|
+
# 安全关闭连接
|
|
438
|
+
if hasattr(self, "_conn") and self._conn is not None:
|
|
439
|
+
try:
|
|
440
|
+
self._conn.close()
|
|
441
|
+
except Exception as e:
|
|
442
|
+
from . import logger
|
|
443
|
+
logger.warning(f"关闭数据库连接时出错: {e}")
|
|
444
|
+
|
|
445
|
+
# 创建快照
|
|
446
|
+
shutil.copy2(self.db_path, snapshot_path)
|
|
447
|
+
from . import logger
|
|
448
|
+
logger.info(f"数据库快照已创建: {snapshot_path}")
|
|
449
|
+
return snapshot_path
|
|
450
|
+
except Exception as e:
|
|
451
|
+
from . import logger
|
|
452
|
+
logger.error(f"创建快照失败: {e}")
|
|
453
|
+
raise
|
|
454
|
+
|
|
455
|
+
def restore(self, snapshot_name: str) -> bool:
|
|
456
|
+
"""
|
|
457
|
+
从快照恢复数据库
|
|
458
|
+
|
|
459
|
+
:param snapshot_name: 快照名称或路径
|
|
460
|
+
:return: 恢复是否成功
|
|
461
|
+
|
|
462
|
+
:example:
|
|
463
|
+
>>> storage.restore("before_update")
|
|
464
|
+
"""
|
|
465
|
+
snapshot_path = os.path.join(self.SNAPSHOT_DIR, f"{snapshot_name}.db") \
|
|
466
|
+
if not snapshot_name.endswith('.db') else snapshot_name
|
|
467
|
+
|
|
468
|
+
if not os.path.exists(snapshot_path):
|
|
469
|
+
from . import logger
|
|
470
|
+
logger.error(f"快照文件不存在: {snapshot_path}")
|
|
471
|
+
return False
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
# 安全关闭连接
|
|
475
|
+
if hasattr(self, "_conn") and self._conn is not None:
|
|
476
|
+
try:
|
|
477
|
+
self._conn.close()
|
|
478
|
+
except Exception as e:
|
|
479
|
+
from . import logger
|
|
480
|
+
logger.warning(f"关闭数据库连接时出错: {e}")
|
|
481
|
+
|
|
482
|
+
# 执行恢复操作
|
|
483
|
+
shutil.copy2(snapshot_path, self.db_path)
|
|
484
|
+
self._init_db() # 恢复后重新初始化数据库连接
|
|
485
|
+
from . import logger
|
|
486
|
+
logger.info(f"数据库已从快照恢复: {snapshot_path}")
|
|
487
|
+
return True
|
|
488
|
+
except Exception as e:
|
|
489
|
+
from . import logger
|
|
490
|
+
logger.error(f"恢复快照失败: {e}")
|
|
491
|
+
return False
|
|
492
|
+
|
|
493
|
+
def list_snapshots(self) -> List[Tuple[str, datetime, int]]:
|
|
494
|
+
"""
|
|
495
|
+
列出所有可用的快照
|
|
496
|
+
|
|
497
|
+
:return: 快照信息列表(名称, 创建时间, 大小)
|
|
498
|
+
|
|
499
|
+
:example:
|
|
500
|
+
>>> for name, date, size in storage.list_snapshots():
|
|
501
|
+
>>> print(f"{name} - {date} ({size} bytes)")
|
|
502
|
+
"""
|
|
503
|
+
snapshots = []
|
|
504
|
+
for f in os.listdir(self.SNAPSHOT_DIR):
|
|
505
|
+
if f.endswith('.db'):
|
|
506
|
+
path = os.path.join(self.SNAPSHOT_DIR, f)
|
|
507
|
+
stat = os.stat(path)
|
|
508
|
+
snapshots.append((
|
|
509
|
+
f[:-3], # 去掉.db后缀
|
|
510
|
+
datetime.fromtimestamp(stat.st_ctime),
|
|
511
|
+
stat.st_size
|
|
512
|
+
))
|
|
513
|
+
return sorted(snapshots, key=lambda x: x[1], reverse=True)
|
|
514
|
+
|
|
515
|
+
def delete_snapshot(self, snapshot_name: str) -> bool:
|
|
516
|
+
"""
|
|
517
|
+
删除指定的快照
|
|
518
|
+
|
|
519
|
+
:param snapshot_name: 快照名称
|
|
520
|
+
:return: 删除是否成功
|
|
521
|
+
|
|
522
|
+
:example:
|
|
523
|
+
>>> storage.delete_snapshot("old_backup")
|
|
524
|
+
"""
|
|
525
|
+
snapshot_path = os.path.join(self.SNAPSHOT_DIR, f"{snapshot_name}.db") \
|
|
526
|
+
if not snapshot_name.endswith('.db') else snapshot_name
|
|
527
|
+
|
|
528
|
+
if not os.path.exists(snapshot_path):
|
|
529
|
+
from . import logger
|
|
530
|
+
logger.error(f"快照文件不存在: {snapshot_path}")
|
|
531
|
+
return False
|
|
532
|
+
|
|
533
|
+
try:
|
|
534
|
+
os.remove(snapshot_path)
|
|
535
|
+
from . import logger
|
|
536
|
+
logger.info(f"快照已删除: {snapshot_path}")
|
|
537
|
+
return True
|
|
538
|
+
except Exception as e:
|
|
539
|
+
from . import logger
|
|
540
|
+
logger.error(f"删除快照失败: {e}")
|
|
541
|
+
return False
|
|
542
|
+
|
|
543
|
+
storage = StorageManager()
|
|
544
|
+
|
|
545
|
+
__all__ = [
|
|
546
|
+
"storage"
|
|
547
|
+
]
|