wasm-cli 0.14.0__py3-none-any.whl → 0.14.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.
- wasm/__init__.py +1 -1
- wasm/cli/commands/backup.py +18 -10
- wasm/cli/commands/store.py +20 -4
- wasm/cli/commands/webapp.py +4 -4
- wasm/cli/parser.py +5 -0
- wasm/completions/_wasm +31 -7
- wasm/completions/wasm.bash +25 -1
- wasm/completions/wasm.fish +24 -1
- wasm/core/utils.py +18 -3
- wasm/deployers/base.py +3 -3
- wasm/managers/backup_manager.py +108 -14
- wasm/managers/service_manager.py +91 -45
- wasm/web/api/apps.py +6 -4
- wasm/web/api/backups.py +37 -17
- wasm/web/api/services.py +31 -25
- wasm/web/websockets/router.py +3 -1
- {wasm_cli-0.14.0.dist-info → wasm_cli-0.14.1.dist-info}/METADATA +1 -1
- {wasm_cli-0.14.0.dist-info → wasm_cli-0.14.1.dist-info}/RECORD +23 -23
- {wasm_cli-0.14.0.dist-info → wasm_cli-0.14.1.dist-info}/WHEEL +1 -1
- {wasm_cli-0.14.0.data → wasm_cli-0.14.1.data}/data/share/man/man1/wasm.1 +0 -0
- {wasm_cli-0.14.0.dist-info → wasm_cli-0.14.1.dist-info}/entry_points.txt +0 -0
- {wasm_cli-0.14.0.dist-info → wasm_cli-0.14.1.dist-info}/licenses/LICENSE +0 -0
- {wasm_cli-0.14.0.dist-info → wasm_cli-0.14.1.dist-info}/top_level.txt +0 -0
wasm/__init__.py
CHANGED
wasm/cli/commands/backup.py
CHANGED
|
@@ -105,6 +105,7 @@ def _backup_create(args: Namespace, verbose: bool) -> int:
|
|
|
105
105
|
include_env = not getattr(args, "no_env", False)
|
|
106
106
|
include_node_modules = getattr(args, "include_node_modules", False)
|
|
107
107
|
include_build = getattr(args, "include_build", False)
|
|
108
|
+
include_databases = getattr(args, "include_databases", False)
|
|
108
109
|
tags = getattr(args, "tags", None)
|
|
109
110
|
|
|
110
111
|
if not domain:
|
|
@@ -127,6 +128,7 @@ def _backup_create(args: Namespace, verbose: bool) -> int:
|
|
|
127
128
|
include_env=include_env,
|
|
128
129
|
include_node_modules=include_node_modules,
|
|
129
130
|
include_build=include_build,
|
|
131
|
+
include_databases=include_databases,
|
|
130
132
|
tags=tag_list,
|
|
131
133
|
)
|
|
132
134
|
|
|
@@ -135,7 +137,13 @@ def _backup_create(args: Namespace, verbose: bool) -> int:
|
|
|
135
137
|
logger.info(f" Size: {metadata.size_human}")
|
|
136
138
|
if metadata.git_commit:
|
|
137
139
|
logger.info(f" Commit: {metadata.git_commit} ({metadata.git_branch})")
|
|
138
|
-
|
|
140
|
+
if metadata.database_backups:
|
|
141
|
+
db_count = len(metadata.database_backups)
|
|
142
|
+
logger.info(f" Databases: {db_count} backed up")
|
|
143
|
+
for db_info in metadata.database_backups:
|
|
144
|
+
size_mb = db_info.get("size_bytes", 0) / (1024 * 1024)
|
|
145
|
+
logger.info(f" - {db_info['engine']}/{db_info['name']} ({size_mb:.1f} MB)")
|
|
146
|
+
|
|
139
147
|
return 0
|
|
140
148
|
|
|
141
149
|
except BackupError as e:
|
|
@@ -195,7 +203,7 @@ def _backup_list(args: Namespace, verbose: bool) -> int:
|
|
|
195
203
|
by_domain[backup.domain].append(backup)
|
|
196
204
|
|
|
197
205
|
for dom, dom_backups in by_domain.items():
|
|
198
|
-
logger.info(f"\n
|
|
206
|
+
logger.info(f"\n[{dom}]")
|
|
199
207
|
_print_backup_table(dom_backups, logger, indent=True)
|
|
200
208
|
|
|
201
209
|
return 0
|
|
@@ -226,7 +234,7 @@ def _print_backup_table(backups, logger, indent: bool = False):
|
|
|
226
234
|
desc_str = f" - {backup.description}"
|
|
227
235
|
|
|
228
236
|
logger.info(
|
|
229
|
-
f"{prefix}
|
|
237
|
+
f"{prefix}- {backup.id}: {backup.size_human}, "
|
|
230
238
|
f"{backup.age}{commit_str}{tags_str}{desc_str}"
|
|
231
239
|
)
|
|
232
240
|
|
|
@@ -359,17 +367,17 @@ def _backup_verify(args: Namespace, verbose: bool) -> int:
|
|
|
359
367
|
|
|
360
368
|
if result["valid"]:
|
|
361
369
|
logger.success("Backup is valid")
|
|
362
|
-
if result.get("
|
|
363
|
-
logger.info("
|
|
364
|
-
if result.get("
|
|
365
|
-
logger.info(f"
|
|
370
|
+
if result.get("checksum_ok"):
|
|
371
|
+
logger.info(" [OK] Checksum verified")
|
|
372
|
+
if result.get("files_ok"):
|
|
373
|
+
logger.info(f" [OK] Archive valid ({result.get('file_count', '?')} files)")
|
|
366
374
|
else:
|
|
367
375
|
logger.error("Backup is invalid")
|
|
368
376
|
for err in result["errors"]:
|
|
369
|
-
logger.error(f"
|
|
370
|
-
|
|
377
|
+
logger.error(f" [ERROR] {err}")
|
|
378
|
+
|
|
371
379
|
for warn in result.get("warnings", []):
|
|
372
|
-
logger.warning(f"
|
|
380
|
+
logger.warning(f" [WARN] {warn}")
|
|
373
381
|
|
|
374
382
|
return 0 if result["valid"] else 1
|
|
375
383
|
|
wasm/cli/commands/store.py
CHANGED
|
@@ -313,13 +313,29 @@ def _store_import(args: Namespace, verbose: bool) -> int:
|
|
|
313
313
|
else:
|
|
314
314
|
logger.step(2, 3, "No Apache sites found")
|
|
315
315
|
|
|
316
|
-
# 3. Import from systemd services (wasm-*
|
|
316
|
+
# 3. Import from systemd services (both legacy wasm-* and new format)
|
|
317
317
|
logger.step(3, 3, "Scanning systemd services")
|
|
318
318
|
if SYSTEMD_DIR.exists():
|
|
319
|
-
|
|
319
|
+
# Find all potential WASM service files
|
|
320
|
+
service_files = list(SYSTEMD_DIR.glob("wasm-*.service"))
|
|
321
|
+
# Also check for services matching domain pattern (new format)
|
|
322
|
+
for sf in SYSTEMD_DIR.glob("*.service"):
|
|
323
|
+
name = sf.stem
|
|
324
|
+
# Skip if already found as wasm-* or if it's a system service
|
|
325
|
+
if name.startswith("wasm-") or not "-" in name:
|
|
326
|
+
continue
|
|
327
|
+
# Check if it looks like a domain-based name (has hyphen, no @ or other special chars)
|
|
328
|
+
if "@" not in name and name.count("-") >= 1:
|
|
329
|
+
service_files.append(sf)
|
|
330
|
+
|
|
331
|
+
for service_file in service_files:
|
|
320
332
|
# Extract app name from service file name
|
|
321
|
-
service_name = service_file.stem
|
|
322
|
-
|
|
333
|
+
service_name = service_file.stem
|
|
334
|
+
# Handle both legacy (wasm-example-com) and new format (example-com)
|
|
335
|
+
if service_name.startswith("wasm-"):
|
|
336
|
+
app_name = service_name[5:] # Remove wasm- prefix
|
|
337
|
+
else:
|
|
338
|
+
app_name = service_name
|
|
323
339
|
|
|
324
340
|
if store.get_service(app_name):
|
|
325
341
|
continue
|
wasm/cli/commands/webapp.py
CHANGED
|
@@ -639,7 +639,7 @@ def _handle_delete(args: Namespace) -> int:
|
|
|
639
639
|
try:
|
|
640
640
|
status = service_manager.status(app_name)
|
|
641
641
|
if status.get("exists"):
|
|
642
|
-
logger.key_value("Stop and remove service",
|
|
642
|
+
logger.key_value("Stop and remove service", app_name)
|
|
643
643
|
except Exception:
|
|
644
644
|
pass
|
|
645
645
|
|
|
@@ -734,9 +734,9 @@ def _handle_logs(args: Namespace) -> int:
|
|
|
734
734
|
domain = validate_domain(args.domain)
|
|
735
735
|
app_name = domain_to_app_name(domain)
|
|
736
736
|
|
|
737
|
-
# Get service name (
|
|
738
|
-
service_name =
|
|
739
|
-
|
|
737
|
+
# Get resolved service name (handles both legacy wasm-* and new format)
|
|
738
|
+
service_name = service_manager._resolve_service_name(app_name)
|
|
739
|
+
|
|
740
740
|
if args.follow:
|
|
741
741
|
# Use journalctl directly for follow mode
|
|
742
742
|
import subprocess
|
wasm/cli/parser.py
CHANGED
|
@@ -908,6 +908,11 @@ def _add_backup_parser(subparsers) -> None:
|
|
|
908
908
|
action="store_true",
|
|
909
909
|
help="Include build artifacts (.next, dist, build)",
|
|
910
910
|
)
|
|
911
|
+
create.add_argument(
|
|
912
|
+
"--include-databases", "--include-db",
|
|
913
|
+
action="store_true",
|
|
914
|
+
help="Include associated database dumps in backup",
|
|
915
|
+
)
|
|
911
916
|
create.add_argument(
|
|
912
917
|
"--tags", "-t",
|
|
913
918
|
help="Comma-separated tags for the backup",
|
wasm/completions/_wasm
CHANGED
|
@@ -14,14 +14,38 @@
|
|
|
14
14
|
|
|
15
15
|
# Helper functions
|
|
16
16
|
_wasm_apps() {
|
|
17
|
-
local apps_dir="/var/www/apps"
|
|
18
17
|
local apps=()
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
|
|
19
|
+
# Try to get domains from SQLite store (fast and accurate)
|
|
20
|
+
local db_path="/var/lib/wasm/wasm.db"
|
|
21
|
+
if [[ ! -f "$db_path" ]]; then
|
|
22
|
+
db_path="$HOME/.local/share/wasm/wasm.db"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
if [[ -f "$db_path" ]] && command -v sqlite3 &>/dev/null; then
|
|
26
|
+
apps=(${(f)"$(sqlite3 "$db_path" "SELECT domain FROM apps" 2>/dev/null)"})
|
|
27
|
+
else
|
|
28
|
+
# Fallback: list app directories and convert names to domains
|
|
29
|
+
local apps_dir="/var/www/apps"
|
|
30
|
+
if [[ -d "$apps_dir" ]]; then
|
|
31
|
+
for app in "$apps_dir"/*/; do
|
|
32
|
+
if [[ -d "$app" ]]; then
|
|
33
|
+
local name
|
|
34
|
+
name=$(basename "$app")
|
|
35
|
+
# Convert directory name to domain format
|
|
36
|
+
# Legacy: wasm-example-com -> example.com
|
|
37
|
+
# New: example-com -> example.com
|
|
38
|
+
if [[ "$name" == wasm-* ]]; then
|
|
39
|
+
apps+=("${${name#wasm-}//\-/.}")
|
|
40
|
+
elif [[ "$name" == *-* ]]; then
|
|
41
|
+
# New format: convert hyphens to dots
|
|
42
|
+
apps+=("${name//\-/.}")
|
|
43
|
+
else
|
|
44
|
+
apps+=("$name")
|
|
45
|
+
fi
|
|
46
|
+
fi
|
|
47
|
+
done 2>/dev/null
|
|
48
|
+
fi
|
|
25
49
|
fi
|
|
26
50
|
_describe -t apps 'deployed applications' apps
|
|
27
51
|
}
|
wasm/completions/wasm.bash
CHANGED
|
@@ -16,11 +16,35 @@
|
|
|
16
16
|
|
|
17
17
|
# Helper function to get list of deployed apps (domains)
|
|
18
18
|
_wasm_get_apps() {
|
|
19
|
+
# Try to get domains from SQLite store (fast and accurate)
|
|
20
|
+
local db_path="/var/lib/wasm/wasm.db"
|
|
21
|
+
if [[ ! -f "$db_path" ]]; then
|
|
22
|
+
db_path="$HOME/.local/share/wasm/wasm.db"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
if [[ -f "$db_path" ]] && command -v sqlite3 &>/dev/null; then
|
|
26
|
+
sqlite3 "$db_path" "SELECT domain FROM apps" 2>/dev/null
|
|
27
|
+
return
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Fallback: list app directories and convert names to domains
|
|
19
31
|
local apps_dir="/var/www/apps"
|
|
20
32
|
if [[ -d "$apps_dir" ]]; then
|
|
21
33
|
for app in "$apps_dir"/*/; do
|
|
22
34
|
if [[ -d "$app" ]]; then
|
|
23
|
-
|
|
35
|
+
local name
|
|
36
|
+
name=$(basename "$app")
|
|
37
|
+
# Convert directory name to domain format
|
|
38
|
+
# Legacy: wasm-example-com -> example.com
|
|
39
|
+
# New: example-com -> example.com
|
|
40
|
+
if [[ "$name" == wasm-* ]]; then
|
|
41
|
+
echo "${name#wasm-}" | tr '-' '.'
|
|
42
|
+
elif [[ "$name" == *-* ]]; then
|
|
43
|
+
# New format: convert hyphens to dots
|
|
44
|
+
echo "$name" | tr '-' '.'
|
|
45
|
+
else
|
|
46
|
+
echo "$name"
|
|
47
|
+
fi
|
|
24
48
|
fi
|
|
25
49
|
done 2>/dev/null
|
|
26
50
|
fi
|
wasm/completions/wasm.fish
CHANGED
|
@@ -14,11 +14,34 @@ complete -c wasm -f
|
|
|
14
14
|
|
|
15
15
|
# Helper functions
|
|
16
16
|
function __wasm_get_apps
|
|
17
|
+
# Try to get domains from SQLite store (fast and accurate)
|
|
18
|
+
set -l db_path "/var/lib/wasm/wasm.db"
|
|
19
|
+
if not test -f "$db_path"
|
|
20
|
+
set db_path "$HOME/.local/share/wasm/wasm.db"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
if test -f "$db_path"; and command -v sqlite3 >/dev/null
|
|
24
|
+
sqlite3 "$db_path" "SELECT domain FROM apps" 2>/dev/null
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Fallback: list app directories and convert names to domains
|
|
17
29
|
set -l apps_dir "/var/www/apps"
|
|
18
30
|
if test -d "$apps_dir"
|
|
19
31
|
for app in $apps_dir/*/
|
|
20
32
|
if test -d "$app"
|
|
21
|
-
basename "$app"
|
|
33
|
+
set -l name (basename "$app")
|
|
34
|
+
# Convert directory name to domain format
|
|
35
|
+
# Legacy: wasm-example-com -> example.com
|
|
36
|
+
# New: example-com -> example.com
|
|
37
|
+
if string match -q 'wasm-*' "$name"
|
|
38
|
+
string replace 'wasm-' '' "$name" | string replace -a '-' '.'
|
|
39
|
+
else if string match -q '*-*' "$name"
|
|
40
|
+
# New format: convert hyphens to dots
|
|
41
|
+
string replace -a '-' '.' "$name"
|
|
42
|
+
else
|
|
43
|
+
echo "$name"
|
|
44
|
+
end
|
|
22
45
|
end
|
|
23
46
|
end 2>/dev/null
|
|
24
47
|
end
|
wasm/core/utils.py
CHANGED
|
@@ -219,12 +219,27 @@ def sanitize_name(name: str) -> str:
|
|
|
219
219
|
def domain_to_app_name(domain: str) -> str:
|
|
220
220
|
"""
|
|
221
221
|
Convert a domain to an application name.
|
|
222
|
-
|
|
222
|
+
|
|
223
223
|
Args:
|
|
224
224
|
domain: Domain name (e.g., "myapp.example.com").
|
|
225
|
-
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Application name (e.g., "myapp-example-com").
|
|
228
|
+
"""
|
|
229
|
+
return sanitize_name(domain)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def legacy_app_name(domain: str) -> str:
|
|
233
|
+
"""
|
|
234
|
+
Get legacy app name format (with wasm- prefix).
|
|
235
|
+
|
|
236
|
+
Used for backwards compatibility with apps created before v0.14.1.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
domain: Domain name (e.g., "myapp.example.com").
|
|
240
|
+
|
|
226
241
|
Returns:
|
|
227
|
-
|
|
242
|
+
Legacy application name (e.g., "wasm-myapp-example-com").
|
|
228
243
|
"""
|
|
229
244
|
return f"wasm-{sanitize_name(domain)}"
|
|
230
245
|
|
wasm/deployers/base.py
CHANGED
|
@@ -899,9 +899,9 @@ class BaseDeployer(ABC):
|
|
|
899
899
|
app = self.store.get_app(self.domain)
|
|
900
900
|
app_id = app.id if app else None
|
|
901
901
|
|
|
902
|
-
# Service name
|
|
902
|
+
# Service name (no prefix for new format)
|
|
903
903
|
service_name = self.app_name
|
|
904
|
-
service_file = SYSTEMD_DIR / f"
|
|
904
|
+
service_file = SYSTEMD_DIR / f"{self.app_name}.service"
|
|
905
905
|
|
|
906
906
|
existing_service = self.store.get_service(service_name)
|
|
907
907
|
|
|
@@ -1142,7 +1142,7 @@ class BaseDeployer(ABC):
|
|
|
1142
1142
|
# Basic info
|
|
1143
1143
|
protocol = "https" if ssl_obtained else "http"
|
|
1144
1144
|
self.logger.key_value("URL", f"{protocol}://{self.domain}")
|
|
1145
|
-
self.logger.key_value("Service",
|
|
1145
|
+
self.logger.key_value("Service", self.app_name)
|
|
1146
1146
|
self.logger.key_value("Port", str(self.port))
|
|
1147
1147
|
self.logger.key_value("App Path", str(self.app_path))
|
|
1148
1148
|
|
wasm/managers/backup_manager.py
CHANGED
|
@@ -46,6 +46,9 @@ class BackupMetadata:
|
|
|
46
46
|
description: str
|
|
47
47
|
includes_env: bool
|
|
48
48
|
includes_node_modules: bool
|
|
49
|
+
includes_build: bool = False
|
|
50
|
+
includes_databases: bool = False
|
|
51
|
+
database_backups: List[Dict[str, Any]] = field(default_factory=list)
|
|
49
52
|
git_commit: Optional[str] = None
|
|
50
53
|
git_branch: Optional[str] = None
|
|
51
54
|
checksum: Optional[str] = None
|
|
@@ -64,6 +67,9 @@ class BackupMetadata:
|
|
|
64
67
|
"description": self.description,
|
|
65
68
|
"includes_env": self.includes_env,
|
|
66
69
|
"includes_node_modules": self.includes_node_modules,
|
|
70
|
+
"includes_build": self.includes_build,
|
|
71
|
+
"includes_databases": self.includes_databases,
|
|
72
|
+
"database_backups": self.database_backups,
|
|
67
73
|
"git_commit": self.git_commit,
|
|
68
74
|
"git_branch": self.git_branch,
|
|
69
75
|
"checksum": self.checksum,
|
|
@@ -84,6 +90,9 @@ class BackupMetadata:
|
|
|
84
90
|
description=data.get("description", ""),
|
|
85
91
|
includes_env=data.get("includes_env", False),
|
|
86
92
|
includes_node_modules=data.get("includes_node_modules", False),
|
|
93
|
+
includes_build=data.get("includes_build", False),
|
|
94
|
+
includes_databases=data.get("includes_databases", False),
|
|
95
|
+
database_backups=data.get("database_backups", []),
|
|
87
96
|
git_commit=data.get("git_commit"),
|
|
88
97
|
git_branch=data.get("git_branch"),
|
|
89
98
|
checksum=data.get("checksum"),
|
|
@@ -269,24 +278,26 @@ class BackupManager:
|
|
|
269
278
|
include_env: bool = True,
|
|
270
279
|
include_node_modules: bool = False,
|
|
271
280
|
include_build: bool = False,
|
|
281
|
+
include_databases: bool = False,
|
|
272
282
|
tags: Optional[List[str]] = None,
|
|
273
283
|
pre_backup_hook: Optional[str] = None,
|
|
274
284
|
) -> BackupMetadata:
|
|
275
285
|
"""
|
|
276
286
|
Create a backup of an application.
|
|
277
|
-
|
|
287
|
+
|
|
278
288
|
Args:
|
|
279
289
|
domain: Domain name of the application.
|
|
280
290
|
description: Optional description for the backup.
|
|
281
291
|
include_env: Include .env files in backup.
|
|
282
292
|
include_node_modules: Include node_modules (large!).
|
|
283
293
|
include_build: Include build artifacts.
|
|
294
|
+
include_databases: Include associated database dumps.
|
|
284
295
|
tags: Optional tags for the backup.
|
|
285
296
|
pre_backup_hook: Optional command to run before backup.
|
|
286
|
-
|
|
297
|
+
|
|
287
298
|
Returns:
|
|
288
299
|
BackupMetadata for the created backup.
|
|
289
|
-
|
|
300
|
+
|
|
290
301
|
Raises:
|
|
291
302
|
BackupError: If backup fails.
|
|
292
303
|
"""
|
|
@@ -378,6 +389,11 @@ class BackupManager:
|
|
|
378
389
|
result = run_command_sudo(["sha256sum", str(backup_file)])
|
|
379
390
|
checksum = result.stdout.split()[0] if result.success else None
|
|
380
391
|
|
|
392
|
+
# Backup associated databases if requested
|
|
393
|
+
database_backups = []
|
|
394
|
+
if include_databases:
|
|
395
|
+
database_backups = self._backup_databases(domain)
|
|
396
|
+
|
|
381
397
|
# Create metadata
|
|
382
398
|
metadata = BackupMetadata(
|
|
383
399
|
id=backup_id,
|
|
@@ -390,6 +406,9 @@ class BackupManager:
|
|
|
390
406
|
description=description,
|
|
391
407
|
includes_env=include_env,
|
|
392
408
|
includes_node_modules=include_node_modules,
|
|
409
|
+
includes_build=include_build,
|
|
410
|
+
includes_databases=include_databases,
|
|
411
|
+
database_backups=database_backups,
|
|
393
412
|
git_commit=git_commit,
|
|
394
413
|
git_branch=git_branch,
|
|
395
414
|
checksum=checksum,
|
|
@@ -415,29 +434,33 @@ class BackupManager:
|
|
|
415
434
|
def list_backups(
|
|
416
435
|
self,
|
|
417
436
|
domain: Optional[str] = None,
|
|
437
|
+
app_name: Optional[str] = None,
|
|
418
438
|
tags: Optional[List[str]] = None,
|
|
419
439
|
limit: Optional[int] = None,
|
|
420
440
|
) -> List[BackupMetadata]:
|
|
421
441
|
"""
|
|
422
442
|
List backups for an application or all applications.
|
|
423
|
-
|
|
443
|
+
|
|
424
444
|
Args:
|
|
425
445
|
domain: Filter by domain (None for all).
|
|
446
|
+
app_name: Filter by app name directly (alternative to domain).
|
|
426
447
|
tags: Filter by tags.
|
|
427
448
|
limit: Maximum number of backups to return.
|
|
428
|
-
|
|
449
|
+
|
|
429
450
|
Returns:
|
|
430
451
|
List of BackupMetadata objects.
|
|
431
452
|
"""
|
|
432
453
|
backups = []
|
|
433
|
-
|
|
454
|
+
|
|
434
455
|
if not self.backup_dir.exists():
|
|
435
456
|
return backups
|
|
436
|
-
|
|
457
|
+
|
|
437
458
|
# Determine which directories to scan
|
|
438
|
-
if
|
|
439
|
-
app_name = domain_to_app_name(domain)
|
|
459
|
+
if app_name:
|
|
440
460
|
dirs_to_scan = [self._get_app_backup_dir(app_name)]
|
|
461
|
+
elif domain:
|
|
462
|
+
resolved_app_name = domain_to_app_name(domain)
|
|
463
|
+
dirs_to_scan = [self._get_app_backup_dir(resolved_app_name)]
|
|
441
464
|
else:
|
|
442
465
|
dirs_to_scan = [d for d in self.backup_dir.iterdir() if d.is_dir()]
|
|
443
466
|
|
|
@@ -739,11 +762,11 @@ class BackupManager:
|
|
|
739
762
|
def _rotate_backups(self, app_name: str) -> None:
|
|
740
763
|
"""
|
|
741
764
|
Rotate old backups to keep only the most recent ones.
|
|
742
|
-
|
|
765
|
+
|
|
743
766
|
Args:
|
|
744
767
|
app_name: Application name.
|
|
745
768
|
"""
|
|
746
|
-
backups = self.list_backups(
|
|
769
|
+
backups = self.list_backups(app_name=app_name)
|
|
747
770
|
|
|
748
771
|
if len(backups) > self.max_backups:
|
|
749
772
|
# Delete oldest backups
|
|
@@ -753,7 +776,78 @@ class BackupManager:
|
|
|
753
776
|
self.logger.debug(f"Rotated old backup: {backup.id}")
|
|
754
777
|
except Exception as e:
|
|
755
778
|
self.logger.warning(f"Failed to rotate backup {backup.id}: {e}")
|
|
756
|
-
|
|
779
|
+
|
|
780
|
+
def _backup_databases(self, domain: str) -> List[Dict[str, Any]]:
|
|
781
|
+
"""
|
|
782
|
+
Backup databases associated with an application.
|
|
783
|
+
|
|
784
|
+
Args:
|
|
785
|
+
domain: Domain name of the application.
|
|
786
|
+
|
|
787
|
+
Returns:
|
|
788
|
+
List of database backup info dictionaries.
|
|
789
|
+
"""
|
|
790
|
+
database_backups = []
|
|
791
|
+
|
|
792
|
+
try:
|
|
793
|
+
from wasm.core.store import WASMStore
|
|
794
|
+
from wasm.managers.database.registry import DatabaseRegistry
|
|
795
|
+
except ImportError as e:
|
|
796
|
+
self.logger.warning(f"Database backup not available: {e}")
|
|
797
|
+
return database_backups
|
|
798
|
+
|
|
799
|
+
try:
|
|
800
|
+
store = WASMStore()
|
|
801
|
+
app = store.get_app_by_domain(domain)
|
|
802
|
+
|
|
803
|
+
if not app or not app.id:
|
|
804
|
+
self.logger.debug(f"No app found in store for {domain}")
|
|
805
|
+
return database_backups
|
|
806
|
+
|
|
807
|
+
databases = store.list_databases(app_id=app.id)
|
|
808
|
+
|
|
809
|
+
if not databases:
|
|
810
|
+
self.logger.debug(f"No databases associated with {domain}")
|
|
811
|
+
return database_backups
|
|
812
|
+
|
|
813
|
+
self.logger.info(f"Backing up {len(databases)} database(s) for {domain}")
|
|
814
|
+
|
|
815
|
+
for db in databases:
|
|
816
|
+
try:
|
|
817
|
+
manager = DatabaseRegistry.get(db.engine, verbose=self.verbose)
|
|
818
|
+
|
|
819
|
+
if not manager:
|
|
820
|
+
self.logger.warning(f"No manager available for {db.engine}")
|
|
821
|
+
continue
|
|
822
|
+
|
|
823
|
+
if not manager.is_installed():
|
|
824
|
+
self.logger.warning(
|
|
825
|
+
f"{db.engine} not installed, skipping {db.name}"
|
|
826
|
+
)
|
|
827
|
+
continue
|
|
828
|
+
|
|
829
|
+
backup_info = manager.backup(database=db.name, compress=True)
|
|
830
|
+
|
|
831
|
+
database_backups.append({
|
|
832
|
+
"engine": db.engine,
|
|
833
|
+
"name": db.name,
|
|
834
|
+
"backup_path": str(backup_info.path),
|
|
835
|
+
"size_bytes": backup_info.size,
|
|
836
|
+
"created": backup_info.created.isoformat(),
|
|
837
|
+
})
|
|
838
|
+
|
|
839
|
+
self.logger.info(f" Backed up {db.engine} database: {db.name}")
|
|
840
|
+
|
|
841
|
+
except Exception as e:
|
|
842
|
+
self.logger.warning(
|
|
843
|
+
f" Failed to backup {db.engine} '{db.name}': {e}"
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
except Exception as e:
|
|
847
|
+
self.logger.warning(f"Database backup failed: {e}")
|
|
848
|
+
|
|
849
|
+
return database_backups
|
|
850
|
+
|
|
757
851
|
def verify(self, backup_id: str) -> Dict[str, Any]:
|
|
758
852
|
"""
|
|
759
853
|
Verify a backup's integrity.
|
|
@@ -795,7 +889,7 @@ class BackupManager:
|
|
|
795
889
|
results["valid"] = False
|
|
796
890
|
results["errors"].append("Checksum mismatch")
|
|
797
891
|
else:
|
|
798
|
-
results["
|
|
892
|
+
results["checksum_ok"] = True
|
|
799
893
|
else:
|
|
800
894
|
results["warnings"].append("Could not verify checksum")
|
|
801
895
|
else:
|
|
@@ -807,7 +901,7 @@ class BackupManager:
|
|
|
807
901
|
results["valid"] = False
|
|
808
902
|
results["errors"].append("Archive is corrupted")
|
|
809
903
|
else:
|
|
810
|
-
results["
|
|
904
|
+
results["files_ok"] = True
|
|
811
905
|
results["file_count"] = len(result.stdout.strip().split("\n"))
|
|
812
906
|
|
|
813
907
|
return results
|
wasm/managers/service_manager.py
CHANGED
|
@@ -18,18 +18,19 @@ from wasm.managers.base_manager import BaseManager
|
|
|
18
18
|
class ServiceManager(BaseManager):
|
|
19
19
|
"""
|
|
20
20
|
Manager for systemd services.
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
Handles creating, starting, stopping, and managing systemd services.
|
|
23
23
|
"""
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
SYSTEMD_DIR = SYSTEMD_DIR
|
|
26
|
-
SERVICE_PREFIX = "
|
|
27
|
-
|
|
26
|
+
SERVICE_PREFIX = "" # New services don't use prefix
|
|
27
|
+
LEGACY_PREFIX = "wasm-" # For backwards compatibility
|
|
28
|
+
|
|
28
29
|
def __init__(self, verbose: bool = False):
|
|
29
30
|
"""Initialize service manager."""
|
|
30
31
|
super().__init__(verbose=verbose)
|
|
31
32
|
self.store = get_store()
|
|
32
|
-
|
|
33
|
+
|
|
33
34
|
try:
|
|
34
35
|
self.jinja_env = Environment(
|
|
35
36
|
loader=PackageLoader("wasm", "templates/systemd"),
|
|
@@ -38,12 +39,12 @@ class ServiceManager(BaseManager):
|
|
|
38
39
|
)
|
|
39
40
|
except Exception:
|
|
40
41
|
self.jinja_env = None
|
|
41
|
-
|
|
42
|
+
|
|
42
43
|
def is_installed(self) -> bool:
|
|
43
44
|
"""Check if systemd is available."""
|
|
44
45
|
result = self._run(["which", "systemctl"])
|
|
45
46
|
return result.success
|
|
46
|
-
|
|
47
|
+
|
|
47
48
|
def get_version(self) -> Optional[str]:
|
|
48
49
|
"""Get systemd version."""
|
|
49
50
|
result = self._run(["systemctl", "--version"])
|
|
@@ -52,28 +53,58 @@ class ServiceManager(BaseManager):
|
|
|
52
53
|
if match:
|
|
53
54
|
return match.group(1)
|
|
54
55
|
return None
|
|
55
|
-
|
|
56
|
+
|
|
56
57
|
def _get_service_name(self, name: str) -> str:
|
|
57
58
|
"""
|
|
58
|
-
Get
|
|
59
|
-
|
|
59
|
+
Get service name for new services (no prefix).
|
|
60
|
+
|
|
60
61
|
Args:
|
|
61
62
|
name: Base service name.
|
|
62
|
-
|
|
63
|
+
|
|
63
64
|
Returns:
|
|
64
|
-
|
|
65
|
+
Service name without prefix.
|
|
65
66
|
"""
|
|
66
|
-
if
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
# Strip legacy prefix if present
|
|
68
|
+
if name.startswith(self.LEGACY_PREFIX):
|
|
69
|
+
return name[len(self.LEGACY_PREFIX):]
|
|
70
|
+
return name
|
|
71
|
+
|
|
72
|
+
def _resolve_service_name(self, name: str) -> str:
|
|
73
|
+
"""
|
|
74
|
+
Resolve actual service name, checking both new and legacy formats.
|
|
75
|
+
|
|
76
|
+
For backwards compatibility, checks if legacy (wasm-*) service exists
|
|
77
|
+
and returns that if found. Otherwise returns the new format.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
name: Base service name.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Actual service name (may have legacy prefix if exists).
|
|
84
|
+
"""
|
|
85
|
+
base_name = self._get_service_name(name)
|
|
86
|
+
|
|
87
|
+
# Check if legacy service exists
|
|
88
|
+
legacy_name = f"{self.LEGACY_PREFIX}{base_name}"
|
|
89
|
+
legacy_file = self.SYSTEMD_DIR / f"{legacy_name}.service"
|
|
90
|
+
if legacy_file.exists():
|
|
91
|
+
return legacy_name
|
|
92
|
+
|
|
93
|
+
# Check if new format exists
|
|
94
|
+
new_file = self.SYSTEMD_DIR / f"{base_name}.service"
|
|
95
|
+
if new_file.exists():
|
|
96
|
+
return base_name
|
|
97
|
+
|
|
98
|
+
# Neither exists, return new format for creation
|
|
99
|
+
return base_name
|
|
100
|
+
|
|
70
101
|
def _get_service_file(self, name: str) -> Path:
|
|
71
102
|
"""
|
|
72
|
-
Get service file path.
|
|
73
|
-
|
|
103
|
+
Get service file path for new services.
|
|
104
|
+
|
|
74
105
|
Args:
|
|
75
106
|
name: Service name.
|
|
76
|
-
|
|
107
|
+
|
|
77
108
|
Returns:
|
|
78
109
|
Path to service file.
|
|
79
110
|
"""
|
|
@@ -81,6 +112,21 @@ class ServiceManager(BaseManager):
|
|
|
81
112
|
if not service_name.endswith(".service"):
|
|
82
113
|
service_name = f"{service_name}.service"
|
|
83
114
|
return self.SYSTEMD_DIR / service_name
|
|
115
|
+
|
|
116
|
+
def _resolve_service_file(self, name: str) -> Path:
|
|
117
|
+
"""
|
|
118
|
+
Resolve service file path, checking both new and legacy formats.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
name: Service name.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Path to existing service file (or new format if none exists).
|
|
125
|
+
"""
|
|
126
|
+
service_name = self._resolve_service_name(name)
|
|
127
|
+
if not service_name.endswith(".service"):
|
|
128
|
+
service_name = f"{service_name}.service"
|
|
129
|
+
return self.SYSTEMD_DIR / service_name
|
|
84
130
|
|
|
85
131
|
def daemon_reload(self) -> bool:
|
|
86
132
|
"""
|
|
@@ -153,14 +199,14 @@ class ServiceManager(BaseManager):
|
|
|
153
199
|
def get_status(self, name: str) -> Dict:
|
|
154
200
|
"""
|
|
155
201
|
Get service status.
|
|
156
|
-
|
|
202
|
+
|
|
157
203
|
Args:
|
|
158
204
|
name: Service name.
|
|
159
|
-
|
|
205
|
+
|
|
160
206
|
Returns:
|
|
161
207
|
Dictionary with status information.
|
|
162
208
|
"""
|
|
163
|
-
service_name = self.
|
|
209
|
+
service_name = self._resolve_service_name(name)
|
|
164
210
|
|
|
165
211
|
# Check if active
|
|
166
212
|
result = self._run(["systemctl", "is-active", service_name])
|
|
@@ -285,16 +331,16 @@ class ServiceManager(BaseManager):
|
|
|
285
331
|
Returns:
|
|
286
332
|
True if service was deleted.
|
|
287
333
|
"""
|
|
288
|
-
service_name = self.
|
|
289
|
-
|
|
334
|
+
service_name = self._resolve_service_name(name)
|
|
335
|
+
|
|
290
336
|
# Stop service if running
|
|
291
337
|
self.stop(name)
|
|
292
|
-
|
|
338
|
+
|
|
293
339
|
# Disable service
|
|
294
340
|
self.disable(name)
|
|
295
|
-
|
|
341
|
+
|
|
296
342
|
# Remove service file
|
|
297
|
-
service_file = self.
|
|
343
|
+
service_file = self._resolve_service_file(name)
|
|
298
344
|
if service_file.exists():
|
|
299
345
|
if not remove_file(service_file, sudo=True):
|
|
300
346
|
raise ServiceError(f"Failed to delete service: {service_name}")
|
|
@@ -314,14 +360,14 @@ class ServiceManager(BaseManager):
|
|
|
314
360
|
def start(self, name: str) -> bool:
|
|
315
361
|
"""
|
|
316
362
|
Start a service.
|
|
317
|
-
|
|
363
|
+
|
|
318
364
|
Args:
|
|
319
365
|
name: Service name.
|
|
320
|
-
|
|
366
|
+
|
|
321
367
|
Returns:
|
|
322
368
|
True if service started successfully.
|
|
323
369
|
"""
|
|
324
|
-
service_name = self.
|
|
370
|
+
service_name = self._resolve_service_name(name)
|
|
325
371
|
result = self._run_sudo(["systemctl", "start", service_name])
|
|
326
372
|
|
|
327
373
|
if not result.success:
|
|
@@ -341,14 +387,14 @@ class ServiceManager(BaseManager):
|
|
|
341
387
|
def stop(self, name: str) -> bool:
|
|
342
388
|
"""
|
|
343
389
|
Stop a service.
|
|
344
|
-
|
|
390
|
+
|
|
345
391
|
Args:
|
|
346
392
|
name: Service name.
|
|
347
|
-
|
|
393
|
+
|
|
348
394
|
Returns:
|
|
349
395
|
True if service stopped successfully.
|
|
350
396
|
"""
|
|
351
|
-
service_name = self.
|
|
397
|
+
service_name = self._resolve_service_name(name)
|
|
352
398
|
result = self._run_sudo(["systemctl", "stop", service_name])
|
|
353
399
|
|
|
354
400
|
# Update store status
|
|
@@ -362,14 +408,14 @@ class ServiceManager(BaseManager):
|
|
|
362
408
|
def restart(self, name: str) -> bool:
|
|
363
409
|
"""
|
|
364
410
|
Restart a service.
|
|
365
|
-
|
|
411
|
+
|
|
366
412
|
Args:
|
|
367
413
|
name: Service name.
|
|
368
|
-
|
|
414
|
+
|
|
369
415
|
Returns:
|
|
370
416
|
True if service restarted successfully.
|
|
371
417
|
"""
|
|
372
|
-
service_name = self.
|
|
418
|
+
service_name = self._resolve_service_name(name)
|
|
373
419
|
result = self._run_sudo(["systemctl", "restart", service_name])
|
|
374
420
|
|
|
375
421
|
if not result.success:
|
|
@@ -389,14 +435,14 @@ class ServiceManager(BaseManager):
|
|
|
389
435
|
def enable(self, name: str) -> bool:
|
|
390
436
|
"""
|
|
391
437
|
Enable a service to start on boot.
|
|
392
|
-
|
|
438
|
+
|
|
393
439
|
Args:
|
|
394
440
|
name: Service name.
|
|
395
|
-
|
|
441
|
+
|
|
396
442
|
Returns:
|
|
397
443
|
True if service was enabled.
|
|
398
444
|
"""
|
|
399
|
-
service_name = self.
|
|
445
|
+
service_name = self._resolve_service_name(name)
|
|
400
446
|
result = self._run_sudo(["systemctl", "enable", service_name])
|
|
401
447
|
|
|
402
448
|
# Update store
|
|
@@ -413,14 +459,14 @@ class ServiceManager(BaseManager):
|
|
|
413
459
|
def disable(self, name: str) -> bool:
|
|
414
460
|
"""
|
|
415
461
|
Disable a service from starting on boot.
|
|
416
|
-
|
|
462
|
+
|
|
417
463
|
Args:
|
|
418
464
|
name: Service name.
|
|
419
|
-
|
|
465
|
+
|
|
420
466
|
Returns:
|
|
421
467
|
True if service was disabled.
|
|
422
468
|
"""
|
|
423
|
-
service_name = self.
|
|
469
|
+
service_name = self._resolve_service_name(name)
|
|
424
470
|
result = self._run_sudo(["systemctl", "disable", service_name])
|
|
425
471
|
|
|
426
472
|
# Update store
|
|
@@ -442,16 +488,16 @@ class ServiceManager(BaseManager):
|
|
|
442
488
|
) -> str:
|
|
443
489
|
"""
|
|
444
490
|
Get service logs.
|
|
445
|
-
|
|
491
|
+
|
|
446
492
|
Args:
|
|
447
493
|
name: Service name.
|
|
448
494
|
lines: Number of lines to return.
|
|
449
495
|
follow: Follow log output (not supported in this method).
|
|
450
|
-
|
|
496
|
+
|
|
451
497
|
Returns:
|
|
452
498
|
Log output.
|
|
453
499
|
"""
|
|
454
|
-
service_name = self.
|
|
500
|
+
service_name = self._resolve_service_name(name)
|
|
455
501
|
|
|
456
502
|
cmd = ["journalctl", "-u", service_name, "-n", str(lines), "--no-pager"]
|
|
457
503
|
result = self._run(cmd)
|
wasm/web/api/apps.py
CHANGED
|
@@ -104,7 +104,7 @@ async def list_apps(
|
|
|
104
104
|
# Get service from store by app_id
|
|
105
105
|
service = store.get_service_by_app_id(app.id) if app.id else None
|
|
106
106
|
if service:
|
|
107
|
-
status = service_manager.get_status(service.name
|
|
107
|
+
status = service_manager.get_status(service.name)
|
|
108
108
|
active = status.get("active", False)
|
|
109
109
|
enabled = status.get("enabled", False)
|
|
110
110
|
pid = status.get("pid")
|
|
@@ -158,7 +158,7 @@ async def get_app(
|
|
|
158
158
|
service = store.get_service_by_app_id(app.id) if app.id else None
|
|
159
159
|
if service:
|
|
160
160
|
service_manager = ServiceManager(verbose=False)
|
|
161
|
-
status = service_manager.get_status(service.name
|
|
161
|
+
status = service_manager.get_status(service.name)
|
|
162
162
|
active = status.get("active", False)
|
|
163
163
|
enabled = status.get("enabled", False)
|
|
164
164
|
pid = status.get("pid")
|
|
@@ -356,9 +356,11 @@ async def get_app_logs(
|
|
|
356
356
|
except Exception as e:
|
|
357
357
|
raise HTTPException(status_code=400, detail=str(e))
|
|
358
358
|
|
|
359
|
+
from wasm.managers.service_manager import ServiceManager
|
|
359
360
|
app_name = domain_to_app_name(validated_domain)
|
|
360
|
-
|
|
361
|
-
|
|
361
|
+
service_manager = ServiceManager(verbose=False)
|
|
362
|
+
service_name = service_manager._resolve_service_name(app_name)
|
|
363
|
+
|
|
362
364
|
try:
|
|
363
365
|
result = subprocess.run(
|
|
364
366
|
["journalctl", "-u", service_name, "-n", str(lines), "--no-pager"],
|
wasm/web/api/backups.py
CHANGED
|
@@ -4,7 +4,7 @@ Backups API endpoints.
|
|
|
4
4
|
Provides endpoints for managing application backups.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import List, Optional
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
10
|
from fastapi import APIRouter, Request, HTTPException, Depends, Query
|
|
@@ -24,10 +24,16 @@ class BackupInfo(BaseModel):
|
|
|
24
24
|
size: int
|
|
25
25
|
size_human: str
|
|
26
26
|
age: str
|
|
27
|
+
description: str = ""
|
|
27
28
|
app_type: Optional[str] = None
|
|
29
|
+
includes_env: bool = False
|
|
30
|
+
includes_node_modules: bool = False
|
|
31
|
+
includes_build: bool = False
|
|
28
32
|
has_database: bool = False
|
|
33
|
+
database_backups: List[Dict[str, Any]] = Field(default_factory=list)
|
|
29
34
|
git_commit: Optional[str] = None
|
|
30
35
|
git_branch: Optional[str] = None
|
|
36
|
+
tags: List[str] = Field(default_factory=list)
|
|
31
37
|
|
|
32
38
|
|
|
33
39
|
class BackupListResponse(BaseModel):
|
|
@@ -48,8 +54,12 @@ class BackupStorageResponse(BaseModel):
|
|
|
48
54
|
class CreateBackupRequest(BaseModel):
|
|
49
55
|
"""Request to create a new backup."""
|
|
50
56
|
domain: str = Field(..., description="Domain of the app to backup")
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
description: str = Field(default="", description="Description for the backup")
|
|
58
|
+
include_env: bool = Field(default=True, description="Include .env files")
|
|
59
|
+
include_node_modules: bool = Field(default=False, description="Include node_modules (large!)")
|
|
60
|
+
include_build: bool = Field(default=False, description="Include build artifacts")
|
|
61
|
+
include_database: bool = Field(default=False, description="Include database dumps")
|
|
62
|
+
tags: List[str] = Field(default_factory=list, description="Tags for the backup")
|
|
53
63
|
|
|
54
64
|
|
|
55
65
|
class RestoreBackupRequest(BaseModel):
|
|
@@ -96,10 +106,16 @@ async def list_backups(
|
|
|
96
106
|
size=backup.size_bytes,
|
|
97
107
|
size_human=backup.size_human,
|
|
98
108
|
age=backup.age,
|
|
109
|
+
description=backup.description,
|
|
99
110
|
app_type=backup.app_type,
|
|
100
|
-
|
|
111
|
+
includes_env=backup.includes_env,
|
|
112
|
+
includes_node_modules=backup.includes_node_modules,
|
|
113
|
+
includes_build=backup.includes_build,
|
|
114
|
+
has_database=backup.includes_databases,
|
|
115
|
+
database_backups=backup.database_backups,
|
|
101
116
|
git_commit=backup.git_commit,
|
|
102
|
-
git_branch=backup.git_branch
|
|
117
|
+
git_branch=backup.git_branch,
|
|
118
|
+
tags=backup.tags
|
|
103
119
|
))
|
|
104
120
|
|
|
105
121
|
return BackupListResponse(
|
|
@@ -178,10 +194,16 @@ async def get_backup(
|
|
|
178
194
|
size=backup.size_bytes,
|
|
179
195
|
size_human=backup.size_human,
|
|
180
196
|
age=backup.age,
|
|
197
|
+
description=backup.description,
|
|
181
198
|
app_type=backup.app_type,
|
|
182
|
-
|
|
199
|
+
includes_env=backup.includes_env,
|
|
200
|
+
includes_node_modules=backup.includes_node_modules,
|
|
201
|
+
includes_build=backup.includes_build,
|
|
202
|
+
has_database=backup.includes_databases,
|
|
203
|
+
database_backups=backup.database_backups,
|
|
183
204
|
git_commit=backup.git_commit,
|
|
184
|
-
git_branch=backup.git_branch
|
|
205
|
+
git_branch=backup.git_branch,
|
|
206
|
+
tags=backup.tags
|
|
185
207
|
)
|
|
186
208
|
except HTTPException:
|
|
187
209
|
raise
|
|
@@ -214,6 +236,12 @@ async def create_backup(
|
|
|
214
236
|
|
|
215
237
|
backup_meta = manager.create(
|
|
216
238
|
domain=data.domain,
|
|
239
|
+
description=data.description,
|
|
240
|
+
include_env=data.include_env,
|
|
241
|
+
include_node_modules=data.include_node_modules,
|
|
242
|
+
include_build=data.include_build,
|
|
243
|
+
include_databases=data.include_database,
|
|
244
|
+
tags=data.tags,
|
|
217
245
|
)
|
|
218
246
|
|
|
219
247
|
return BackupActionResponse(
|
|
@@ -271,19 +299,11 @@ async def restore_backup(
|
|
|
271
299
|
|
|
272
300
|
# Determine target domain
|
|
273
301
|
target_domain = data.target_domain or backup.domain
|
|
274
|
-
|
|
275
|
-
# Find or create target path
|
|
276
|
-
from wasm.core.config import Config
|
|
277
|
-
from wasm.core.utils import domain_to_app_name
|
|
278
|
-
|
|
279
|
-
config = Config()
|
|
280
|
-
app_name = domain_to_app_name(target_domain)
|
|
281
|
-
target_path = config.apps_dir / app_name
|
|
282
|
-
|
|
302
|
+
|
|
283
303
|
# Perform restore
|
|
284
304
|
success = manager.restore(
|
|
285
305
|
backup_id=backup_id,
|
|
286
|
-
|
|
306
|
+
target_domain=target_domain
|
|
287
307
|
)
|
|
288
308
|
|
|
289
309
|
if success:
|
wasm/web/api/services.py
CHANGED
|
@@ -71,11 +71,8 @@ async def list_services(
|
|
|
71
71
|
|
|
72
72
|
result = []
|
|
73
73
|
for svc in stored_services:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
# Get live status from systemd
|
|
78
|
-
live_status = service_manager.get_status(svc.name.replace("wasm-", ""))
|
|
74
|
+
# Get live status from systemd (ServiceManager resolves name automatically)
|
|
75
|
+
live_status = service_manager.get_status(svc.name)
|
|
79
76
|
|
|
80
77
|
result.append(ServiceInfo(
|
|
81
78
|
name=svc.name,
|
|
@@ -105,16 +102,16 @@ async def get_service(
|
|
|
105
102
|
store = get_store()
|
|
106
103
|
service_manager = ServiceManager(verbose=False)
|
|
107
104
|
|
|
108
|
-
# Handle both
|
|
105
|
+
# Handle both prefixed and non-prefixed names for backwards compatibility
|
|
109
106
|
svc = store.get_service(name)
|
|
110
107
|
if not svc:
|
|
111
108
|
svc = store.get_service(f"wasm-{name}")
|
|
112
|
-
|
|
109
|
+
|
|
113
110
|
if not svc:
|
|
114
111
|
raise HTTPException(status_code=404, detail=f"Service not found: {name}")
|
|
115
|
-
|
|
116
|
-
# Get live status from systemd
|
|
117
|
-
live_status = service_manager.get_status(svc.name
|
|
112
|
+
|
|
113
|
+
# Get live status from systemd (ServiceManager resolves name automatically)
|
|
114
|
+
live_status = service_manager.get_status(svc.name)
|
|
118
115
|
|
|
119
116
|
return ServiceInfo(
|
|
120
117
|
name=svc.name,
|
|
@@ -140,7 +137,7 @@ async def start_service(
|
|
|
140
137
|
from wasm.managers.service_manager import ServiceManager
|
|
141
138
|
|
|
142
139
|
service_manager = ServiceManager(verbose=False)
|
|
143
|
-
app_name = name
|
|
140
|
+
app_name = name
|
|
144
141
|
|
|
145
142
|
status = service_manager.get_status(app_name)
|
|
146
143
|
if not status["exists"]:
|
|
@@ -169,7 +166,7 @@ async def stop_service(
|
|
|
169
166
|
from wasm.managers.service_manager import ServiceManager
|
|
170
167
|
|
|
171
168
|
service_manager = ServiceManager(verbose=False)
|
|
172
|
-
app_name = name
|
|
169
|
+
app_name = name
|
|
173
170
|
|
|
174
171
|
status = service_manager.get_status(app_name)
|
|
175
172
|
if not status["exists"]:
|
|
@@ -198,7 +195,7 @@ async def restart_service(
|
|
|
198
195
|
from wasm.managers.service_manager import ServiceManager
|
|
199
196
|
|
|
200
197
|
service_manager = ServiceManager(verbose=False)
|
|
201
|
-
app_name = name
|
|
198
|
+
app_name = name
|
|
202
199
|
|
|
203
200
|
status = service_manager.get_status(app_name)
|
|
204
201
|
if not status["exists"]:
|
|
@@ -227,7 +224,7 @@ async def enable_service(
|
|
|
227
224
|
from wasm.managers.service_manager import ServiceManager
|
|
228
225
|
|
|
229
226
|
service_manager = ServiceManager(verbose=False)
|
|
230
|
-
app_name = name
|
|
227
|
+
app_name = name
|
|
231
228
|
|
|
232
229
|
status = service_manager.get_status(app_name)
|
|
233
230
|
if not status["exists"]:
|
|
@@ -256,7 +253,7 @@ async def disable_service(
|
|
|
256
253
|
from wasm.managers.service_manager import ServiceManager
|
|
257
254
|
|
|
258
255
|
service_manager = ServiceManager(verbose=False)
|
|
259
|
-
app_name = name
|
|
256
|
+
app_name = name
|
|
260
257
|
|
|
261
258
|
status = service_manager.get_status(app_name)
|
|
262
259
|
if not status["exists"]:
|
|
@@ -283,8 +280,10 @@ async def get_service_logs(
|
|
|
283
280
|
"""
|
|
284
281
|
Get service logs from journalctl.
|
|
285
282
|
"""
|
|
286
|
-
|
|
287
|
-
|
|
283
|
+
from wasm.managers.service_manager import ServiceManager
|
|
284
|
+
service_manager = ServiceManager(verbose=False)
|
|
285
|
+
service_name = service_manager._resolve_service_name(name)
|
|
286
|
+
|
|
288
287
|
try:
|
|
289
288
|
result = subprocess.run(
|
|
290
289
|
["journalctl", "-u", service_name, "-n", str(lines), "--no-pager"],
|
|
@@ -315,8 +314,10 @@ async def get_service_config(
|
|
|
315
314
|
Get the systemd unit file content for a service.
|
|
316
315
|
"""
|
|
317
316
|
from pathlib import Path
|
|
318
|
-
|
|
319
|
-
|
|
317
|
+
from wasm.managers.service_manager import ServiceManager
|
|
318
|
+
|
|
319
|
+
service_manager = ServiceManager(verbose=False)
|
|
320
|
+
service_name = service_manager._resolve_service_name(name)
|
|
320
321
|
service_path = Path(f"/etc/systemd/system/{service_name}.service")
|
|
321
322
|
|
|
322
323
|
if not service_path.exists():
|
|
@@ -349,8 +350,10 @@ async def update_service_config(
|
|
|
349
350
|
Update the systemd unit file content for a service.
|
|
350
351
|
"""
|
|
351
352
|
from pathlib import Path
|
|
352
|
-
|
|
353
|
-
|
|
353
|
+
from wasm.managers.service_manager import ServiceManager
|
|
354
|
+
|
|
355
|
+
service_manager = ServiceManager(verbose=False)
|
|
356
|
+
service_name = service_manager._resolve_service_name(name)
|
|
354
357
|
service_path = Path(f"/etc/systemd/system/{service_name}.service")
|
|
355
358
|
|
|
356
359
|
if not service_path.exists():
|
|
@@ -382,8 +385,9 @@ async def create_service(
|
|
|
382
385
|
Create a new systemd service.
|
|
383
386
|
"""
|
|
384
387
|
from pathlib import Path
|
|
385
|
-
|
|
386
|
-
|
|
388
|
+
|
|
389
|
+
# New services don't use wasm- prefix
|
|
390
|
+
service_name = data.name
|
|
387
391
|
service_path = Path(f"/etc/systemd/system/{service_name}.service")
|
|
388
392
|
|
|
389
393
|
if service_path.exists():
|
|
@@ -455,8 +459,10 @@ async def delete_service(
|
|
|
455
459
|
Delete a systemd service.
|
|
456
460
|
"""
|
|
457
461
|
from pathlib import Path
|
|
458
|
-
|
|
459
|
-
|
|
462
|
+
from wasm.managers.service_manager import ServiceManager
|
|
463
|
+
|
|
464
|
+
service_manager = ServiceManager(verbose=False)
|
|
465
|
+
service_name = service_manager._resolve_service_name(name)
|
|
460
466
|
service_path = Path(f"/etc/systemd/system/{service_name}.service")
|
|
461
467
|
|
|
462
468
|
if not service_path.exists():
|
wasm/web/websockets/router.py
CHANGED
|
@@ -58,8 +58,10 @@ async def websocket_logs(
|
|
|
58
58
|
|
|
59
59
|
await websocket.accept()
|
|
60
60
|
|
|
61
|
+
from wasm.managers.service_manager import ServiceManager
|
|
61
62
|
app_name = domain_to_app_name(domain)
|
|
62
|
-
|
|
63
|
+
service_manager = ServiceManager(verbose=False)
|
|
64
|
+
service_name = service_manager._resolve_service_name(app_name)
|
|
63
65
|
|
|
64
66
|
# Add to connections
|
|
65
67
|
if domain not in _log_connections:
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
wasm/__init__.py,sha256=
|
|
1
|
+
wasm/__init__.py,sha256=jVKeUvFzS2tI3X9vjQEbHKm1_UMzYpUHV1IEaRo_Rf4,560
|
|
2
2
|
wasm/__main__.py,sha256=i3EgrkaduRUtc_1KtThBY6DHa9s9Uj22Ns9gjrsV75E,120
|
|
3
3
|
wasm/main.py,sha256=4QqnXn6ToD1DY37WlAQ2j4uFrYqTgHLvV8TT-upXOrg,5095
|
|
4
4
|
wasm/cli/__init__.py,sha256=KvBJMVlHhKRme-y2IoRwdag9bcp4soyNTgSZ01JL62Q,193
|
|
5
5
|
wasm/cli/interactive.py,sha256=RkxgdfyWEVke9VuWGh0FjLCjsYJIQrGEjUVV3cX4NLU,20596
|
|
6
|
-
wasm/cli/parser.py,sha256=
|
|
6
|
+
wasm/cli/parser.py,sha256=Jy-YS4yxtJGCZkNi5yMXDBd-twNpZ2fU0lAQJ0AYWE8,41132
|
|
7
7
|
wasm/cli/commands/__init__.py,sha256=_FGYr4xRKpRUAbRhA9hJxpoLJehj1t3bq_qOZ9ajtcM,525
|
|
8
|
-
wasm/cli/commands/backup.py,sha256=
|
|
8
|
+
wasm/cli/commands/backup.py,sha256=IfmDYgjyu6hN_I4ljXaGTHhUz0jroTWN-gc7MuSzPik,15661
|
|
9
9
|
wasm/cli/commands/cert.py,sha256=12vpgnJ4gfqJ25QpG8XH0ZKcjVQP4VgqHk5fMHSmS1U,6550
|
|
10
10
|
wasm/cli/commands/config.py,sha256=HIm1jNH5SV3-HiOKPpLK7yXoPzjRB0--1LOsqWC2OCg,2581
|
|
11
11
|
wasm/cli/commands/db.py,sha256=f9tXgW98ZfURcBZ3A1-BRDYTxFatpt3nIR4TgwfNZsc,30575
|
|
@@ -14,14 +14,14 @@ wasm/cli/commands/monitor.py,sha256=G0dm3Zr_6dDZm-guJMfIGx0Ww47NVVs5Q9F5O2nCvZs,
|
|
|
14
14
|
wasm/cli/commands/service.py,sha256=zQ20jkHE2aKpUPtEhQLTT13atUepxx-DLiIQmdrOdx8,5407
|
|
15
15
|
wasm/cli/commands/setup.py,sha256=VhA1eD9niWCRpfBFTNmY2472NFPwJMN602k5s5XvGBY,38209
|
|
16
16
|
wasm/cli/commands/site.py,sha256=Fi4ZWNlmUohLBPc2SJDxYIue1lYSsEHIkMDL9v6PW84,6213
|
|
17
|
-
wasm/cli/commands/store.py,sha256=
|
|
17
|
+
wasm/cli/commands/store.py,sha256=U3F7vQtXQDlf00igN1lEZMTmQRySnK5XgptuZHJqmVs,17569
|
|
18
18
|
wasm/cli/commands/version.py,sha256=1gv-bnH0ur3Tlj0UoEdrCaQ6ZI_dA-TsHodgCdw1h1Y,3573
|
|
19
19
|
wasm/cli/commands/web.py,sha256=aQW5kq5wt13pTLEi8Sg1uQAPX75qTo7XkMNVyfPUSHk,20364
|
|
20
|
-
wasm/cli/commands/webapp.py,sha256=
|
|
20
|
+
wasm/cli/commands/webapp.py,sha256=XvhzwqNR9S_mc_7K4zgs3yy-Vj_SdGZhxcgviP2U6bg,25521
|
|
21
21
|
wasm/completions/__init__.py,sha256=EKNNDUKp8XRYcE56CJq3stkgVqUo9dbnKjCqj1mwd7U,716
|
|
22
|
-
wasm/completions/_wasm,sha256=
|
|
23
|
-
wasm/completions/wasm.bash,sha256=
|
|
24
|
-
wasm/completions/wasm.fish,sha256=
|
|
22
|
+
wasm/completions/_wasm,sha256=Hagz1H7K4MCsAhHZBg8G53cio7dpO5OoSZOnk3Tbn2U,40427
|
|
23
|
+
wasm/completions/wasm.bash,sha256=KNZbzUzRen7RONj6RNYSd6ysgza9vDwkn7q51zT4QKM,31229
|
|
24
|
+
wasm/completions/wasm.fish,sha256=lrQjB6XRMRBxusG8g_EYTA739Xe5mm1MaSIUYiCWegc,35448
|
|
25
25
|
wasm/core/__init__.py,sha256=Sat72_6P3eqiWCHVU1GsZpwPhaXFNf8-t1Hfoxu5kpY,573
|
|
26
26
|
wasm/core/config.py,sha256=3sA0GsRJzAt4C-0qGrMWM5AYrCi0PH8n_7WcBm00BPo,11635
|
|
27
27
|
wasm/core/dependencies.py,sha256=q2qPt8unoYEM5IhjF9qb2DPn6chkIERvGoThwrHBWP0,21538
|
|
@@ -29,9 +29,9 @@ wasm/core/exceptions.py,sha256=G4NT13SsHwWgg3s9DEaq7gZbouvCIqlp5hpVvQaN9os,6084
|
|
|
29
29
|
wasm/core/logger.py,sha256=-tuOxcLYLXQ7g8eA7OkYSN5iq_qGBZyNhGUeK4xwATI,11289
|
|
30
30
|
wasm/core/store.py,sha256=SB9GiJMqdh2A01TkfsKDijXywH8yOKoXmtclKdoWaJU,37064
|
|
31
31
|
wasm/core/update_checker.py,sha256=fBtKqbHpwErPOiVWadyv1MpQ7qGk974an0qgKynMXfk,11811
|
|
32
|
-
wasm/core/utils.py,sha256=
|
|
32
|
+
wasm/core/utils.py,sha256=0_15_7ZsWzKWFvDTxuxNkBFpNVi1MHd85G1lRCgtD_0,14685
|
|
33
33
|
wasm/deployers/__init__.py,sha256=Rl3Y7joRm7EH1-o0zxysYiYQKcVZv0Dyg29YvLQz4mk,258
|
|
34
|
-
wasm/deployers/base.py,sha256=
|
|
34
|
+
wasm/deployers/base.py,sha256=KV1vklcx470pnp2zgMBc-ULx8EFgprXO9_9CWsnnXhA,40888
|
|
35
35
|
wasm/deployers/nextjs.py,sha256=MLDAHFAGBxvIgBwuY_IyHEQkPnhEk6W_migYReHo-DA,4917
|
|
36
36
|
wasm/deployers/nodejs.py,sha256=15II2Fyw9OZTX9RtFZzw3dzzc9PEBUyNDBVQ9eH-brI,4153
|
|
37
37
|
wasm/deployers/python.py,sha256=A7RJwqXVQZgYg2C2zl-tU2meRNYdnPq-dSSaOe_20Ww,7891
|
|
@@ -44,11 +44,11 @@ wasm/deployers/helpers/path_resolver.py,sha256=HkJTGen1WK6OL3Argsa_rFkDw6SCykftd
|
|
|
44
44
|
wasm/deployers/helpers/prisma.py,sha256=gzSuWwqkEpYBEidnA3pwvK7uqtqSSyYOZZKqyFycncA,4001
|
|
45
45
|
wasm/managers/__init__.py,sha256=KjR8yntlqi88dldzda7Ube1QL0EjyzjOpDfUifKXhxI,633
|
|
46
46
|
wasm/managers/apache_manager.py,sha256=KcDKe-gJ2NnP0VtVA0KTZyvmiptiWTtBNLvF6caOIiA,12263
|
|
47
|
-
wasm/managers/backup_manager.py,sha256=
|
|
47
|
+
wasm/managers/backup_manager.py,sha256=7KUHzpi2za38JJBwVi-7lOisgNvOtpW-jkc8GD7yqSU,39525
|
|
48
48
|
wasm/managers/base_manager.py,sha256=dy9yLHjAZkTwWH2f8ZkeSVNADFT4qtw7WDEbvVfpo98,2948
|
|
49
49
|
wasm/managers/cert_manager.py,sha256=fMZxbZJBFCa1Tpl4n8eizxRlhSSTHLNwrTI9nMk-O6w,15030
|
|
50
50
|
wasm/managers/nginx_manager.py,sha256=tnThgOqIlMUEiaiCFAbNtUzNUq1uJ_GXdGBu1IP6oRo,11287
|
|
51
|
-
wasm/managers/service_manager.py,sha256=
|
|
51
|
+
wasm/managers/service_manager.py,sha256=IBreQjTiKumE529h4ukTJA9QTp_9yJkckdBbuiVw1hg,15915
|
|
52
52
|
wasm/managers/source_manager.py,sha256=glysx-YF_zkHMI3WUBIA_JR9aoPTW_j__bwg205YymM,20535
|
|
53
53
|
wasm/managers/database/__init__.py,sha256=P_bfkT7LpjYnI2vEDoyM4SjtUTq4FDwLzvA-BhPnUDY,898
|
|
54
54
|
wasm/managers/database/base.py,sha256=vcVMAkqfnOcasIfKNIRPeXEcOGygmPxnQl0ikd3k27Y,18215
|
|
@@ -78,16 +78,16 @@ wasm/web/auth.py,sha256=7QZDrhhFdsYcH62_eK9znpHO0N3-bvMvOOW3Q1msRl8,16467
|
|
|
78
78
|
wasm/web/jobs.py,sha256=wajwWmfmOwRh97kKaZ-7ygt5aZiou-14RuadtwgQwVk,20625
|
|
79
79
|
wasm/web/server.py,sha256=QkorvqKZageFKPYoXRyb73Nkn3pwk0vD1RKhsc6fX_g,13851
|
|
80
80
|
wasm/web/api/__init__.py,sha256=sZw-PeWY0cYWYK2g_KM3FSJOAUoaR37ELbHM6yuqmmY,105
|
|
81
|
-
wasm/web/api/apps.py,sha256=
|
|
81
|
+
wasm/web/api/apps.py,sha256=leWamRrvJR1Fvqz-Vgf6xQx7Gu4TVaTDkW6BDjK4xTw,13578
|
|
82
82
|
wasm/web/api/auth.py,sha256=ELFmDO9J_vzYIL8wOByKyaIbs_H0Gqzs_C0AGAjAosY,4134
|
|
83
|
-
wasm/web/api/backups.py,sha256=
|
|
83
|
+
wasm/web/api/backups.py,sha256=I8MYy4v26VEaDczcK0dcnE_PjQpNgLl1ykoj4n0tYi8,11221
|
|
84
84
|
wasm/web/api/certs.py,sha256=uJ349OgWDx4-67Y82hH6xmIxiezpwQjMVnDUAqs-jpE,10667
|
|
85
85
|
wasm/web/api/config.py,sha256=aVimawBwntjravi464ax1w-8WXkcw2DpkhNaysnDuLg,10960
|
|
86
86
|
wasm/web/api/databases.py,sha256=90u2736J9lnES54p3NoHmJnNEbMRRiGL6E9Gf9tN0n4,24272
|
|
87
87
|
wasm/web/api/jobs.py,sha256=Z7rgt97yLuVuumxO_gYuPa5HPu0UdGBjdYtj6vbYEPQ,9283
|
|
88
88
|
wasm/web/api/monitor.py,sha256=okptwHh6yP79EJJzj4wu-5TtNKxAWgQeBRMamihGlvo,20040
|
|
89
89
|
wasm/web/api/router.py,sha256=tYX_9MBDpO2vgfxawvxBdC2p0cCCyZ6cr9-LgYq3DFA,1557
|
|
90
|
-
wasm/web/api/services.py,sha256=
|
|
90
|
+
wasm/web/api/services.py,sha256=QPiytz1o7mJGlNwiiWduU0GseqgRtgjAEj_GVxseu7A,14619
|
|
91
91
|
wasm/web/api/sites.py,sha256=DbQLtHknCqih9jIvxLWXf122ZT6cTFxioe49o5C4kp0,14310
|
|
92
92
|
wasm/web/api/system.py,sha256=91A0W1LQCvmg662cAnOGKWwwL5CJAQKaKSaca8D1meM,13914
|
|
93
93
|
wasm/web/static/index.html,sha256=JOL5_5XnOb3vH3BvuBu748H5Co7O0Z6mZffH4lvInQA,128832
|
|
@@ -121,11 +121,11 @@ wasm/web/static/js/pages/monitor.js,sha256=A7V3WuimKjLVN4Ug3t9of7u88T8688iUl1WDW
|
|
|
121
121
|
wasm/web/static/js/pages/services.js,sha256=TT1s48dtQ4genq0-XFeCN2HSzTJHpHbAZs5ctYqOfN4,11027
|
|
122
122
|
wasm/web/static/js/pages/sites.js,sha256=Xxh8y0mefOi5kF8knu9s1iHlq9CueG5a7Cx9Eb202So,8822
|
|
123
123
|
wasm/web/websockets/__init__.py,sha256=GC-bLXm91pGA037ZeUKEsATKDf7ZuygDhxl6wd9hrOo,118
|
|
124
|
-
wasm/web/websockets/router.py,sha256=
|
|
125
|
-
wasm_cli-0.14.
|
|
126
|
-
wasm_cli-0.14.
|
|
127
|
-
wasm_cli-0.14.
|
|
128
|
-
wasm_cli-0.14.
|
|
129
|
-
wasm_cli-0.14.
|
|
130
|
-
wasm_cli-0.14.
|
|
131
|
-
wasm_cli-0.14.
|
|
124
|
+
wasm/web/websockets/router.py,sha256=ujaOxcZqNBKZW8CDRKeyn6H5MIBpn9fbq8sOsZRYNeU,20896
|
|
125
|
+
wasm_cli-0.14.1.data/data/share/man/man1/wasm.1,sha256=Q4ML-26TFS5lCnKEUPD2Rgaof80i4g4uvLtqncGD9is,10619
|
|
126
|
+
wasm_cli-0.14.1.dist-info/licenses/LICENSE,sha256=5PYGsNFEQYrzS8bN6_g4TrW-O999_ToZb4strA3xO4g,4083
|
|
127
|
+
wasm_cli-0.14.1.dist-info/METADATA,sha256=HnbSNxhYlW1VHQFvUSd0Csh1ifbAUGtLHSiPxKn7xBY,15444
|
|
128
|
+
wasm_cli-0.14.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
129
|
+
wasm_cli-0.14.1.dist-info/entry_points.txt,sha256=oe0IjfmSyedc--7xWlRsA4xKpZmNCl6aCi8WoaSPtdI,39
|
|
130
|
+
wasm_cli-0.14.1.dist-info/top_level.txt,sha256=ICXMW6pzfO638yao6kQxfeSg3vPXusFOPJAY1LOu5-8,5
|
|
131
|
+
wasm_cli-0.14.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|