htcli 1.1.0__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.
- htcli-1.1.0.dist-info/METADATA +509 -0
- htcli-1.1.0.dist-info/RECORD +140 -0
- htcli-1.1.0.dist-info/WHEEL +4 -0
- htcli-1.1.0.dist-info/entry_points.txt +2 -0
- htcli-1.1.0.dist-info/licenses/LICENSE +21 -0
- src/__init__.py +0 -0
- src/htcli/__init__.py +5 -0
- src/htcli/client/__init__.py +338 -0
- src/htcli/client/extrinsics/__init__.py +26 -0
- src/htcli/client/extrinsics/base.py +487 -0
- src/htcli/client/extrinsics/consensus.py +79 -0
- src/htcli/client/extrinsics/governance.py +714 -0
- src/htcli/client/extrinsics/identity.py +490 -0
- src/htcli/client/extrinsics/node.py +1054 -0
- src/htcli/client/extrinsics/overwatch.py +401 -0
- src/htcli/client/extrinsics/staking.py +1504 -0
- src/htcli/client/extrinsics/subnet.py +2218 -0
- src/htcli/client/extrinsics/validator.py +203 -0
- src/htcli/client/extrinsics/wallet.py +323 -0
- src/htcli/client/offchain/__init__.py +10 -0
- src/htcli/client/offchain/backup.py +385 -0
- src/htcli/client/offchain/config.py +541 -0
- src/htcli/client/offchain/wallet.py +839 -0
- src/htcli/client/rpc/__init__.py +20 -0
- src/htcli/client/rpc/chain.py +568 -0
- src/htcli/client/rpc/node.py +783 -0
- src/htcli/client/rpc/overwatch.py +680 -0
- src/htcli/client/rpc/staking.py +216 -0
- src/htcli/client/rpc/subnet.py +2104 -0
- src/htcli/client/rpc/wallet.py +912 -0
- src/htcli/commands/__init__.py +31 -0
- src/htcli/commands/chain/__init__.py +66 -0
- src/htcli/commands/chain/display.py +204 -0
- src/htcli/commands/chain/handlers.py +260 -0
- src/htcli/commands/config/__init__.py +158 -0
- src/htcli/commands/config/display.py +353 -0
- src/htcli/commands/config/handlers.py +347 -0
- src/htcli/commands/config/prompts.py +357 -0
- src/htcli/commands/consensus/__init__.py +61 -0
- src/htcli/commands/consensus/handlers.py +100 -0
- src/htcli/commands/governance/__init__.py +49 -0
- src/htcli/commands/governance/handlers.py +81 -0
- src/htcli/commands/node/__init__.py +304 -0
- src/htcli/commands/node/display.py +749 -0
- src/htcli/commands/node/error_handling.py +470 -0
- src/htcli/commands/node/handlers.py +844 -0
- src/htcli/commands/node/prompts.py +346 -0
- src/htcli/commands/overwatch/__init__.py +219 -0
- src/htcli/commands/overwatch/display.py +396 -0
- src/htcli/commands/overwatch/error_handling.py +276 -0
- src/htcli/commands/overwatch/handlers.py +443 -0
- src/htcli/commands/overwatch/prompts.py +359 -0
- src/htcli/commands/stake/__init__.py +736 -0
- src/htcli/commands/stake/display.py +1103 -0
- src/htcli/commands/stake/error_handling.py +425 -0
- src/htcli/commands/stake/handlers.py +1902 -0
- src/htcli/commands/stake/prompts.py +1080 -0
- src/htcli/commands/subnet/__init__.py +639 -0
- src/htcli/commands/subnet/display.py +801 -0
- src/htcli/commands/subnet/error_handling.py +524 -0
- src/htcli/commands/subnet/handlers.py +2855 -0
- src/htcli/commands/subnet/prompts.py +1225 -0
- src/htcli/commands/validator/__init__.py +192 -0
- src/htcli/commands/validator/display.py +54 -0
- src/htcli/commands/validator/handlers.py +340 -0
- src/htcli/commands/wallet/__init__.py +546 -0
- src/htcli/commands/wallet/display.py +806 -0
- src/htcli/commands/wallet/error_handling.py +210 -0
- src/htcli/commands/wallet/handlers.py +3040 -0
- src/htcli/commands/wallet/prompts.py +1518 -0
- src/htcli/config.py +184 -0
- src/htcli/dependencies.py +186 -0
- src/htcli/errors/__init__.py +63 -0
- src/htcli/errors/base.py +141 -0
- src/htcli/errors/display.py +20 -0
- src/htcli/errors/handlers.py +710 -0
- src/htcli/main.py +343 -0
- src/htcli/models/__init__.py +21 -0
- src/htcli/models/enums/enum_types.py +35 -0
- src/htcli/models/errors.py +103 -0
- src/htcli/models/requests/__init__.py +197 -0
- src/htcli/models/requests/config.py +70 -0
- src/htcli/models/requests/consensus.py +19 -0
- src/htcli/models/requests/governance.py +38 -0
- src/htcli/models/requests/identity.py +51 -0
- src/htcli/models/requests/key.py +22 -0
- src/htcli/models/requests/node.py +91 -0
- src/htcli/models/requests/overwatch.py +64 -0
- src/htcli/models/requests/staking.py +580 -0
- src/htcli/models/requests/subnet.py +195 -0
- src/htcli/models/requests/validator.py +139 -0
- src/htcli/models/requests/wallet.py +118 -0
- src/htcli/models/responses/__init__.py +147 -0
- src/htcli/models/responses/base.py +18 -0
- src/htcli/models/responses/chain.py +39 -0
- src/htcli/models/responses/config.py +58 -0
- src/htcli/models/responses/identity.py +102 -0
- src/htcli/models/responses/overwatch.py +51 -0
- src/htcli/models/responses/staking.py +502 -0
- src/htcli/models/responses/subnet.py +856 -0
- src/htcli/models/responses/wallet.py +185 -0
- src/htcli/ui/__init__.py +87 -0
- src/htcli/ui/colors.py +309 -0
- src/htcli/ui/components/__init__.py +60 -0
- src/htcli/ui/components/panels.py +174 -0
- src/htcli/ui/components/progress.py +166 -0
- src/htcli/ui/components/spinners.py +92 -0
- src/htcli/ui/components/tables.py +809 -0
- src/htcli/ui/components/trees.py +721 -0
- src/htcli/ui/display.py +336 -0
- src/htcli/ui/prompts.py +870 -0
- src/htcli/utils/__init__.py +76 -0
- src/htcli/utils/blockchain/__init__.py +75 -0
- src/htcli/utils/blockchain/formatting.py +368 -0
- src/htcli/utils/blockchain/patches.py +286 -0
- src/htcli/utils/blockchain/peer_id.py +186 -0
- src/htcli/utils/blockchain/staking.py +448 -0
- src/htcli/utils/blockchain/type_registry.py +1373 -0
- src/htcli/utils/blockchain/validation.py +179 -0
- src/htcli/utils/cache.py +613 -0
- src/htcli/utils/constants.py +38 -0
- src/htcli/utils/legacy/__init__.py +12 -0
- src/htcli/utils/legacy/colors.py +311 -0
- src/htcli/utils/legacy/crypto.py +1176 -0
- src/htcli/utils/legacy/formatting.py +452 -0
- src/htcli/utils/legacy/interactive.py +306 -0
- src/htcli/utils/legacy/subnet_manifest.py +265 -0
- src/htcli/utils/legacy/validation.py +488 -0
- src/htcli/utils/logging.py +183 -0
- src/htcli/utils/network/__init__.py +20 -0
- src/htcli/utils/network/subnet.py +344 -0
- src/htcli/utils/prompts.py +27 -0
- src/htcli/utils/scale_codec.py +155 -0
- src/htcli/utils/validation/__init__.py +57 -0
- src/htcli/utils/validation/prompt_validators.py +267 -0
- src/htcli/utils/wallet/__init__.py +65 -0
- src/htcli/utils/wallet/auth.py +151 -0
- src/htcli/utils/wallet/core.py +1069 -0
- src/htcli/utils/wallet/crypto.py +1615 -0
- src/htcli/utils/wallet/migration.py +159 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Off-chain backup management operations.
|
|
3
|
+
Handles backup and restoration of wallets, configurations, and local state.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import shutil
|
|
8
|
+
import tarfile
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from ...utils.logging import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BackupManager:
|
|
19
|
+
"""Manager for off-chain backup operations."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, backup_dir: Optional[str] = None):
|
|
22
|
+
self.backup_dir = (
|
|
23
|
+
Path(backup_dir) if backup_dir else Path.home() / ".htcli" / "backups"
|
|
24
|
+
)
|
|
25
|
+
self.backup_dir.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
self.htcli_dir = Path.home() / ".htcli"
|
|
27
|
+
|
|
28
|
+
def _validate_member_path(self, destination: Path, member_name: str) -> Path:
|
|
29
|
+
"""Resolve a tar member path and ensure it stays under destination."""
|
|
30
|
+
target_path = (destination / member_name).resolve()
|
|
31
|
+
try:
|
|
32
|
+
target_path.relative_to(destination)
|
|
33
|
+
except ValueError as exc:
|
|
34
|
+
raise ValueError(f"Unsafe backup member path: {member_name}") from exc
|
|
35
|
+
return target_path
|
|
36
|
+
|
|
37
|
+
def _safe_extract_tar(self, tar: tarfile.TarFile, destination: Path) -> None:
|
|
38
|
+
"""Extract regular files and directories without trusting archive paths."""
|
|
39
|
+
destination = destination.resolve()
|
|
40
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
for member in tar.getmembers():
|
|
43
|
+
target_path = self._validate_member_path(destination, member.name)
|
|
44
|
+
|
|
45
|
+
if member.isdir():
|
|
46
|
+
target_path.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
if not member.isfile():
|
|
50
|
+
raise ValueError(f"Unsupported backup member type: {member.name}")
|
|
51
|
+
|
|
52
|
+
source = tar.extractfile(member)
|
|
53
|
+
if source is None:
|
|
54
|
+
raise ValueError(f"Unable to read backup member: {member.name}")
|
|
55
|
+
|
|
56
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
with source, open(target_path, "wb") as target:
|
|
58
|
+
shutil.copyfileobj(source, target)
|
|
59
|
+
|
|
60
|
+
def create_full_backup(self, backup_name: Optional[str] = None) -> dict:
|
|
61
|
+
"""Create a full backup of all HTCLI data."""
|
|
62
|
+
try:
|
|
63
|
+
if backup_name is None:
|
|
64
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
65
|
+
backup_name = f"htcli_backup_{timestamp}"
|
|
66
|
+
|
|
67
|
+
backup_file = self.backup_dir / f"{backup_name}.tar.gz"
|
|
68
|
+
|
|
69
|
+
# Create backup archive
|
|
70
|
+
with tarfile.open(backup_file, "w:gz") as tar:
|
|
71
|
+
if self.htcli_dir.exists():
|
|
72
|
+
# Add all files except existing backups to avoid recursion
|
|
73
|
+
for item in self.htcli_dir.iterdir():
|
|
74
|
+
if item.name != "backups":
|
|
75
|
+
tar.add(item, arcname=item.name)
|
|
76
|
+
|
|
77
|
+
# Create backup manifest
|
|
78
|
+
manifest = self._create_backup_manifest(backup_name)
|
|
79
|
+
manifest_file = self.backup_dir / f"{backup_name}_manifest.json"
|
|
80
|
+
|
|
81
|
+
with open(manifest_file, "w") as f:
|
|
82
|
+
json.dump(manifest, f, indent=2)
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
"success": True,
|
|
86
|
+
"message": "Full backup created successfully",
|
|
87
|
+
"data": {
|
|
88
|
+
"backup_name": backup_name,
|
|
89
|
+
"backup_file": str(backup_file),
|
|
90
|
+
"manifest_file": str(manifest_file),
|
|
91
|
+
"size": backup_file.stat().st_size,
|
|
92
|
+
"manifest": manifest,
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.error(f"Failed to create full backup: {str(e)}")
|
|
97
|
+
raise
|
|
98
|
+
|
|
99
|
+
def restore_full_backup(self, backup_name: str, confirm: bool = False) -> dict:
|
|
100
|
+
"""Restore a full backup (replaces current data)."""
|
|
101
|
+
try:
|
|
102
|
+
if not confirm:
|
|
103
|
+
raise ValueError("Full restore requires explicit confirmation")
|
|
104
|
+
|
|
105
|
+
backup_file = self.backup_dir / f"{backup_name}.tar.gz"
|
|
106
|
+
manifest_file = self.backup_dir / f"{backup_name}_manifest.json"
|
|
107
|
+
|
|
108
|
+
if not backup_file.exists():
|
|
109
|
+
raise ValueError(f"Backup file '{backup_name}.tar.gz' not found")
|
|
110
|
+
|
|
111
|
+
# Load manifest
|
|
112
|
+
manifest = {}
|
|
113
|
+
if manifest_file.exists():
|
|
114
|
+
with open(manifest_file) as f:
|
|
115
|
+
manifest = json.load(f)
|
|
116
|
+
|
|
117
|
+
# Create backup of current state before restore
|
|
118
|
+
current_backup_result = self.create_full_backup(
|
|
119
|
+
f"pre_restore_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Clear current HTCLI directory (except backups)
|
|
123
|
+
if self.htcli_dir.exists():
|
|
124
|
+
for item in self.htcli_dir.iterdir():
|
|
125
|
+
if item.name != "backups":
|
|
126
|
+
if item.is_dir():
|
|
127
|
+
shutil.rmtree(item)
|
|
128
|
+
else:
|
|
129
|
+
item.unlink()
|
|
130
|
+
|
|
131
|
+
# Extract backup
|
|
132
|
+
with tarfile.open(backup_file, "r:gz") as tar:
|
|
133
|
+
self._safe_extract_tar(tar, self.htcli_dir)
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
"success": True,
|
|
137
|
+
"message": f"Full backup '{backup_name}' restored successfully",
|
|
138
|
+
"data": {
|
|
139
|
+
"backup_name": backup_name,
|
|
140
|
+
"manifest": manifest,
|
|
141
|
+
"pre_restore_backup": current_backup_result["data"]["backup_name"],
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.error(f"Failed to restore full backup: {str(e)}")
|
|
146
|
+
raise
|
|
147
|
+
|
|
148
|
+
def backup_wallets(self, backup_name: Optional[str] = None) -> dict:
|
|
149
|
+
"""Create a backup of all wallets."""
|
|
150
|
+
try:
|
|
151
|
+
if backup_name is None:
|
|
152
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
153
|
+
backup_name = f"wallets_backup_{timestamp}"
|
|
154
|
+
|
|
155
|
+
wallets_dir = self.htcli_dir / "wallets"
|
|
156
|
+
backup_file = self.backup_dir / f"{backup_name}.tar.gz"
|
|
157
|
+
|
|
158
|
+
if not wallets_dir.exists():
|
|
159
|
+
return {
|
|
160
|
+
"success": False,
|
|
161
|
+
"message": "No wallets directory found",
|
|
162
|
+
"data": None,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Create wallets backup
|
|
166
|
+
with tarfile.open(backup_file, "w:gz") as tar:
|
|
167
|
+
tar.add(wallets_dir, arcname="wallets")
|
|
168
|
+
|
|
169
|
+
# Count wallet files
|
|
170
|
+
wallet_count = len(list(wallets_dir.glob("*.json")))
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
"success": True,
|
|
174
|
+
"message": "Wallets backup created successfully",
|
|
175
|
+
"data": {
|
|
176
|
+
"backup_name": backup_name,
|
|
177
|
+
"backup_file": str(backup_file),
|
|
178
|
+
"wallet_count": wallet_count,
|
|
179
|
+
"size": backup_file.stat().st_size,
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.error(f"Failed to backup wallets: {str(e)}")
|
|
184
|
+
raise
|
|
185
|
+
|
|
186
|
+
def restore_wallets(self, backup_name: str, confirm: bool = False) -> dict:
|
|
187
|
+
"""Restore wallets from backup."""
|
|
188
|
+
try:
|
|
189
|
+
if not confirm:
|
|
190
|
+
raise ValueError("Wallet restore requires explicit confirmation")
|
|
191
|
+
|
|
192
|
+
backup_file = self.backup_dir / f"{backup_name}.tar.gz"
|
|
193
|
+
|
|
194
|
+
if not backup_file.exists():
|
|
195
|
+
raise ValueError(f"Backup file '{backup_name}.tar.gz' not found")
|
|
196
|
+
|
|
197
|
+
wallets_dir = self.htcli_dir / "wallets"
|
|
198
|
+
|
|
199
|
+
# Create backup of current wallets if they exist
|
|
200
|
+
current_backup = None
|
|
201
|
+
if wallets_dir.exists() and any(wallets_dir.glob("*.json")):
|
|
202
|
+
current_backup_result = self.backup_wallets(
|
|
203
|
+
f"pre_restore_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
|
204
|
+
)
|
|
205
|
+
current_backup = current_backup_result["data"]["backup_name"]
|
|
206
|
+
|
|
207
|
+
# Clear current wallets
|
|
208
|
+
if wallets_dir.exists():
|
|
209
|
+
shutil.rmtree(wallets_dir)
|
|
210
|
+
|
|
211
|
+
# Extract backup
|
|
212
|
+
with tarfile.open(backup_file, "r:gz") as tar:
|
|
213
|
+
self._safe_extract_tar(tar, self.htcli_dir)
|
|
214
|
+
|
|
215
|
+
# Count restored wallets
|
|
216
|
+
wallet_count = (
|
|
217
|
+
len(list(wallets_dir.glob("*.json"))) if wallets_dir.exists() else 0
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
"success": True,
|
|
222
|
+
"message": f"Wallets restored from backup '{backup_name}' successfully",
|
|
223
|
+
"data": {
|
|
224
|
+
"backup_name": backup_name,
|
|
225
|
+
"wallet_count": wallet_count,
|
|
226
|
+
"pre_restore_backup": current_backup,
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.error(f"Failed to restore wallets: {str(e)}")
|
|
231
|
+
raise
|
|
232
|
+
|
|
233
|
+
def list_backups(self) -> dict:
|
|
234
|
+
"""List all available backups."""
|
|
235
|
+
try:
|
|
236
|
+
backup_files = list(self.backup_dir.glob("*.tar.gz"))
|
|
237
|
+
backups = []
|
|
238
|
+
|
|
239
|
+
for backup_file in backup_files:
|
|
240
|
+
try:
|
|
241
|
+
backup_name = backup_file.stem
|
|
242
|
+
manifest_file = self.backup_dir / f"{backup_name}_manifest.json"
|
|
243
|
+
|
|
244
|
+
backup_info = {
|
|
245
|
+
"name": backup_name,
|
|
246
|
+
"file": str(backup_file),
|
|
247
|
+
"size": backup_file.stat().st_size,
|
|
248
|
+
"created_at": datetime.fromtimestamp(
|
|
249
|
+
backup_file.stat().st_ctime
|
|
250
|
+
).isoformat(),
|
|
251
|
+
"manifest": None,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# Load manifest if exists
|
|
255
|
+
if manifest_file.exists():
|
|
256
|
+
with open(manifest_file) as f:
|
|
257
|
+
backup_info["manifest"] = json.load(f)
|
|
258
|
+
|
|
259
|
+
backups.append(backup_info)
|
|
260
|
+
except Exception as e:
|
|
261
|
+
logger.warning(f"Failed to read backup info for {backup_file}: {e}")
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
# Sort by creation time (newest first)
|
|
265
|
+
backups.sort(key=lambda x: x["created_at"], reverse=True)
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
"success": True,
|
|
269
|
+
"message": f"Found {len(backups)} backup(s)",
|
|
270
|
+
"data": backups,
|
|
271
|
+
}
|
|
272
|
+
except Exception as e:
|
|
273
|
+
logger.error(f"Failed to list backups: {str(e)}")
|
|
274
|
+
raise
|
|
275
|
+
|
|
276
|
+
def delete_backup(self, backup_name: str, confirm: bool = False) -> dict:
|
|
277
|
+
"""Delete a backup."""
|
|
278
|
+
try:
|
|
279
|
+
if not confirm:
|
|
280
|
+
raise ValueError("Backup deletion requires explicit confirmation")
|
|
281
|
+
|
|
282
|
+
backup_file = self.backup_dir / f"{backup_name}.tar.gz"
|
|
283
|
+
manifest_file = self.backup_dir / f"{backup_name}_manifest.json"
|
|
284
|
+
|
|
285
|
+
if not backup_file.exists():
|
|
286
|
+
raise ValueError(f"Backup '{backup_name}' not found")
|
|
287
|
+
|
|
288
|
+
# Delete files
|
|
289
|
+
backup_file.unlink()
|
|
290
|
+
if manifest_file.exists():
|
|
291
|
+
manifest_file.unlink()
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
"success": True,
|
|
295
|
+
"message": f"Backup '{backup_name}' deleted successfully",
|
|
296
|
+
"data": {"backup_name": backup_name},
|
|
297
|
+
}
|
|
298
|
+
except Exception as e:
|
|
299
|
+
logger.error(f"Failed to delete backup: {str(e)}")
|
|
300
|
+
raise
|
|
301
|
+
|
|
302
|
+
def cleanup_old_backups(self, keep_count: int = 10, confirm: bool = False) -> dict:
|
|
303
|
+
"""Clean up old backups, keeping only the most recent ones."""
|
|
304
|
+
try:
|
|
305
|
+
if not confirm:
|
|
306
|
+
raise ValueError("Backup cleanup requires explicit confirmation")
|
|
307
|
+
|
|
308
|
+
backups_result = self.list_backups()
|
|
309
|
+
backups = backups_result["data"]
|
|
310
|
+
|
|
311
|
+
if len(backups) <= keep_count:
|
|
312
|
+
return {
|
|
313
|
+
"success": True,
|
|
314
|
+
"message": f"No cleanup needed. Current backup count: {len(backups)}",
|
|
315
|
+
"data": {
|
|
316
|
+
"deleted_count": 0,
|
|
317
|
+
"remaining_count": len(backups),
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# Delete oldest backups
|
|
322
|
+
backups_to_delete = backups[keep_count:]
|
|
323
|
+
deleted_count = 0
|
|
324
|
+
|
|
325
|
+
for backup in backups_to_delete:
|
|
326
|
+
try:
|
|
327
|
+
self.delete_backup(backup["name"], confirm=True)
|
|
328
|
+
deleted_count += 1
|
|
329
|
+
except Exception as e:
|
|
330
|
+
logger.warning(f"Failed to delete backup {backup['name']}: {e}")
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
"success": True,
|
|
334
|
+
"message": f"Cleanup completed. Deleted {deleted_count} old backup(s)",
|
|
335
|
+
"data": {
|
|
336
|
+
"deleted_count": deleted_count,
|
|
337
|
+
"remaining_count": len(backups) - deleted_count,
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
except Exception as e:
|
|
341
|
+
logger.error(f"Failed to cleanup backups: {str(e)}")
|
|
342
|
+
raise
|
|
343
|
+
|
|
344
|
+
def _create_backup_manifest(self, backup_name: str) -> dict:
|
|
345
|
+
"""Create a manifest describing the backup contents."""
|
|
346
|
+
try:
|
|
347
|
+
manifest = {
|
|
348
|
+
"backup_name": backup_name,
|
|
349
|
+
"created_at": datetime.now().isoformat(),
|
|
350
|
+
"version": "1.0",
|
|
351
|
+
"contents": {"wallets": [], "config": None, "other_files": []},
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
# List wallets
|
|
355
|
+
wallets_dir = self.htcli_dir / "wallets"
|
|
356
|
+
if wallets_dir.exists():
|
|
357
|
+
for wallet_file in wallets_dir.glob("*.json"):
|
|
358
|
+
try:
|
|
359
|
+
with open(wallet_file) as f:
|
|
360
|
+
wallet_data = json.load(f)
|
|
361
|
+
manifest["contents"]["wallets"].append(
|
|
362
|
+
{
|
|
363
|
+
"name": wallet_data.get("name", wallet_file.stem),
|
|
364
|
+
"address": wallet_data.get("address"),
|
|
365
|
+
"file": wallet_file.name,
|
|
366
|
+
}
|
|
367
|
+
)
|
|
368
|
+
except Exception as e:
|
|
369
|
+
logger.warning(f"Failed to read wallet {wallet_file}: {e}")
|
|
370
|
+
|
|
371
|
+
# Check config
|
|
372
|
+
config_file = self.htcli_dir / "config.yaml"
|
|
373
|
+
if config_file.exists():
|
|
374
|
+
manifest["contents"]["config"] = "config.yaml"
|
|
375
|
+
|
|
376
|
+
# List other files
|
|
377
|
+
if self.htcli_dir.exists():
|
|
378
|
+
for item in self.htcli_dir.iterdir():
|
|
379
|
+
if item.name not in ["wallets", "config.yaml", "backups"]:
|
|
380
|
+
manifest["contents"]["other_files"].append(item.name)
|
|
381
|
+
|
|
382
|
+
return manifest
|
|
383
|
+
except Exception as e:
|
|
384
|
+
logger.error(f"Failed to create backup manifest: {str(e)}")
|
|
385
|
+
return {"error": str(e)}
|