bsm-cli 1.6.0b3__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.
- bsm_cli/__init__.py +0 -0
- bsm_cli/__main__.py +88 -0
- bsm_cli/account.py +75 -0
- bsm_cli/addon.py +362 -0
- bsm_cli/allowlist.py +229 -0
- bsm_cli/auth.py +106 -0
- bsm_cli/backup.py +263 -0
- bsm_cli/bans.py +190 -0
- bsm_cli/config.py +89 -0
- bsm_cli/content.py +21 -0
- bsm_cli/decorators.py +138 -0
- bsm_cli/main_menus.py +293 -0
- bsm_cli/permissions.py +191 -0
- bsm_cli/player.py +59 -0
- bsm_cli/plugins.py +296 -0
- bsm_cli/properties.py +197 -0
- bsm_cli/server.py +471 -0
- bsm_cli/system.py +142 -0
- bsm_cli/users.py +245 -0
- bsm_cli/world.py +171 -0
- bsm_cli-1.6.0b3.dist-info/METADATA +77 -0
- bsm_cli-1.6.0b3.dist-info/RECORD +26 -0
- bsm_cli-1.6.0b3.dist-info/WHEEL +5 -0
- bsm_cli-1.6.0b3.dist-info/entry_points.txt +2 -0
- bsm_cli-1.6.0b3.dist-info/licenses/LICENSE +21 -0
- bsm_cli-1.6.0b3.dist-info/top_level.txt +1 -0
bsm_cli/allowlist.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import questionary
|
|
3
|
+
|
|
4
|
+
from bsm_api_client.models import AllowlistAddPayload, AllowlistRemovePayload
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.group()
|
|
8
|
+
def allowlist():
|
|
9
|
+
"""Manages a server's player allowlist."""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@allowlist.command("add")
|
|
14
|
+
@click.option(
|
|
15
|
+
"-s", "--server", "server_name", required=True, help="The name of the server."
|
|
16
|
+
)
|
|
17
|
+
@click.option(
|
|
18
|
+
"-p",
|
|
19
|
+
"--player",
|
|
20
|
+
"players",
|
|
21
|
+
multiple=True,
|
|
22
|
+
help="Gamertag of the player to add. Use multiple times for multiple players.",
|
|
23
|
+
)
|
|
24
|
+
@click.option(
|
|
25
|
+
"--ignore-limit",
|
|
26
|
+
is_flag=True,
|
|
27
|
+
help="Allow player(s) to join even if the server is full.",
|
|
28
|
+
)
|
|
29
|
+
@click.pass_context
|
|
30
|
+
async def add(ctx, server_name: str, players: tuple[str], ignore_limit: bool):
|
|
31
|
+
"""Adds one or more players to a server's allowlist."""
|
|
32
|
+
client = ctx.obj.get("client")
|
|
33
|
+
if not client:
|
|
34
|
+
click.secho("You are not logged in.", fg="red")
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
if not players:
|
|
39
|
+
click.secho(
|
|
40
|
+
f"No player specified; starting interactive editor for '{server_name}'...",
|
|
41
|
+
fg="yellow",
|
|
42
|
+
)
|
|
43
|
+
await interactive_allowlist_workflow(client, server_name)
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
payload = AllowlistAddPayload(
|
|
47
|
+
players=list(players), ignoresPlayerLimit=ignore_limit
|
|
48
|
+
)
|
|
49
|
+
response = await client.async_add_server_allowlist(server_name, payload)
|
|
50
|
+
|
|
51
|
+
message = response.message
|
|
52
|
+
click.secho(
|
|
53
|
+
message,
|
|
54
|
+
fg="green",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
except Exception as e:
|
|
58
|
+
click.secho(f"\nAn error occurred: {e}", fg="red")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@allowlist.command("remove")
|
|
62
|
+
@click.option(
|
|
63
|
+
"-s", "--server", "server_name", required=True, help="The name of the server."
|
|
64
|
+
)
|
|
65
|
+
@click.option(
|
|
66
|
+
"-p",
|
|
67
|
+
"--player",
|
|
68
|
+
"players",
|
|
69
|
+
multiple=True,
|
|
70
|
+
required=True,
|
|
71
|
+
help="Gamertag of the player to remove. Use multiple times for multiple players.",
|
|
72
|
+
)
|
|
73
|
+
@click.pass_context
|
|
74
|
+
async def remove(ctx, server_name: str, players: tuple[str]):
|
|
75
|
+
"""Removes one or more players from a server's allowlist."""
|
|
76
|
+
client = ctx.obj.get("client")
|
|
77
|
+
if not client:
|
|
78
|
+
click.secho("You are not logged in.", fg="red")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
player_list = list(players)
|
|
82
|
+
click.echo(
|
|
83
|
+
f"Removing {len(player_list)} player(s) from '{server_name}' allowlist..."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
payload = AllowlistRemovePayload(players=player_list)
|
|
87
|
+
response = await client.async_remove_server_allowlist_players(server_name, payload)
|
|
88
|
+
|
|
89
|
+
if response.status == "success":
|
|
90
|
+
details = response.details or {}
|
|
91
|
+
removed_players = details["removed"] or []
|
|
92
|
+
not_found_players = details["not_found"] or []
|
|
93
|
+
|
|
94
|
+
message = response.message
|
|
95
|
+
click.secho(message, fg="cyan" if not removed_players else "green")
|
|
96
|
+
|
|
97
|
+
if removed_players:
|
|
98
|
+
click.secho(
|
|
99
|
+
f"\nSuccessfully removed {len(removed_players)} player(s):", fg="green"
|
|
100
|
+
)
|
|
101
|
+
for p_name in removed_players:
|
|
102
|
+
click.echo(f" - {p_name}")
|
|
103
|
+
if not_found_players:
|
|
104
|
+
click.secho(
|
|
105
|
+
f"\n{len(not_found_players)} player(s) were not found in the allowlist:",
|
|
106
|
+
fg="yellow",
|
|
107
|
+
)
|
|
108
|
+
for p_name in not_found_players:
|
|
109
|
+
click.echo(f" - {p_name}")
|
|
110
|
+
else:
|
|
111
|
+
click.secho(
|
|
112
|
+
f"Failed to remove players from allowlist: {response.message}", fg="red"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@allowlist.command("list")
|
|
117
|
+
@click.option(
|
|
118
|
+
"-s", "--server", "server_name", required=True, help="The name of the server."
|
|
119
|
+
)
|
|
120
|
+
@click.pass_context
|
|
121
|
+
async def list_players(ctx, server_name: str):
|
|
122
|
+
"""Lists all players currently on a server's allowlist."""
|
|
123
|
+
client = ctx.obj.get("client")
|
|
124
|
+
if not client:
|
|
125
|
+
click.secho("You are not logged in.", fg="red")
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
response = await client.async_get_server_allowlist(server_name)
|
|
129
|
+
|
|
130
|
+
if response.status == "success":
|
|
131
|
+
players = response.players
|
|
132
|
+
if not players:
|
|
133
|
+
click.secho(
|
|
134
|
+
f"The allowlist for server '{server_name}' is empty.", fg="yellow"
|
|
135
|
+
)
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
click.secho(f"\nAllowlist for '{server_name}':", bold=True)
|
|
139
|
+
for p in players:
|
|
140
|
+
limit_str = (
|
|
141
|
+
click.style(" (Ignores Player Limit)", fg="yellow")
|
|
142
|
+
if p.get("ignoresPlayerLimit")
|
|
143
|
+
else ""
|
|
144
|
+
)
|
|
145
|
+
click.echo(f" - {p.get('name')}{limit_str}")
|
|
146
|
+
else:
|
|
147
|
+
click.secho(f"Failed to list allowlist: {response.message}", fg="red")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
async def interactive_allowlist_workflow(client, server_name: str): # noqa: C901
|
|
151
|
+
"""Guides the user through an interactive session to view and add players to the allowlist."""
|
|
152
|
+
response = await client.async_get_server_allowlist(server_name)
|
|
153
|
+
existing_players = response.players or []
|
|
154
|
+
|
|
155
|
+
click.secho("\n--- Interactive Allowlist Configuration ---", bold=True)
|
|
156
|
+
if existing_players:
|
|
157
|
+
click.echo("Current players in allowlist:")
|
|
158
|
+
for p in existing_players:
|
|
159
|
+
limit_str = (
|
|
160
|
+
click.style(" (Ignores Limit)", fg="yellow")
|
|
161
|
+
if p.get("ignoresPlayerLimit")
|
|
162
|
+
else ""
|
|
163
|
+
)
|
|
164
|
+
click.echo(f" - {p.get('name')}{limit_str}")
|
|
165
|
+
else:
|
|
166
|
+
click.secho("Allowlist is currently empty.", fg="yellow")
|
|
167
|
+
|
|
168
|
+
from typing import Any, Dict, List
|
|
169
|
+
|
|
170
|
+
new_players_to_add: List[Dict[str, Any]] = []
|
|
171
|
+
click.echo("\nEnter new players to add. Press Enter on an empty line to finish.")
|
|
172
|
+
while True:
|
|
173
|
+
player_name = await questionary.text("Player gamertag:").ask_async()
|
|
174
|
+
if not player_name or not player_name.strip():
|
|
175
|
+
break
|
|
176
|
+
|
|
177
|
+
if any(
|
|
178
|
+
p["name"].lower() == player_name.lower()
|
|
179
|
+
for p in existing_players + new_players_to_add
|
|
180
|
+
):
|
|
181
|
+
click.secho(
|
|
182
|
+
f"Player '{player_name}' is already in the list. Skipping.", fg="yellow"
|
|
183
|
+
)
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
ignore_limit = await questionary.confirm(
|
|
187
|
+
f"Should '{player_name}' ignore the player limit?", default=False
|
|
188
|
+
).ask_async()
|
|
189
|
+
new_players_to_add.append(
|
|
190
|
+
{"name": player_name.strip(), "ignoresPlayerLimit": ignore_limit}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if new_players_to_add:
|
|
194
|
+
# The interactive mode will add all players with their specified ignore limit.
|
|
195
|
+
# We need to group them by the ignore limit flag and make separate API calls.
|
|
196
|
+
players_ignore_limit = [
|
|
197
|
+
p["name"] for p in new_players_to_add if p["ignoresPlayerLimit"]
|
|
198
|
+
]
|
|
199
|
+
players_no_ignore_limit = [
|
|
200
|
+
p["name"] for p in new_players_to_add if not p["ignoresPlayerLimit"]
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
if players_ignore_limit:
|
|
204
|
+
click.echo("Adding players with ignore limit...")
|
|
205
|
+
payload = AllowlistAddPayload(
|
|
206
|
+
players=players_ignore_limit, ignoresPlayerLimit=True
|
|
207
|
+
)
|
|
208
|
+
response = await client.async_add_server_allowlist(server_name, payload)
|
|
209
|
+
if response.status != "success":
|
|
210
|
+
click.secho(
|
|
211
|
+
f"Failed to add players with ignore limit: {response.message}",
|
|
212
|
+
fg="red",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if players_no_ignore_limit:
|
|
216
|
+
click.echo("Adding players without ignore limit...")
|
|
217
|
+
payload = AllowlistAddPayload(
|
|
218
|
+
players=players_no_ignore_limit, ignoresPlayerLimit=False
|
|
219
|
+
)
|
|
220
|
+
response = await client.async_add_server_allowlist(server_name, payload)
|
|
221
|
+
if response.status != "success":
|
|
222
|
+
click.secho(
|
|
223
|
+
f"Failed to add players without ignore limit: {response.message}",
|
|
224
|
+
fg="red",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
click.secho("Allowlist updated.", fg="green")
|
|
228
|
+
else:
|
|
229
|
+
click.secho("No new players were added.", fg="cyan")
|
bsm_cli/auth.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from bsm_api_client import AuthError, BedrockServerManagerApi
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _validate_and_get_url(url: str) -> str:
|
|
7
|
+
"""Validates and returns a url with a scheme."""
|
|
8
|
+
if not url.startswith("http://") and not url.startswith("https://"):
|
|
9
|
+
return f"http://{url}"
|
|
10
|
+
return url
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group()
|
|
14
|
+
def auth():
|
|
15
|
+
"""Manages authentication."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@auth.command()
|
|
20
|
+
@click.option(
|
|
21
|
+
"--base-url", prompt=True, help="The base URL of the Bedrock Server Manager API."
|
|
22
|
+
)
|
|
23
|
+
@click.option(
|
|
24
|
+
"--verify-ssl/--no-verify-ssl",
|
|
25
|
+
is_flag=True,
|
|
26
|
+
default=True,
|
|
27
|
+
prompt=True,
|
|
28
|
+
help="Enable/disable SSL verification.",
|
|
29
|
+
)
|
|
30
|
+
@click.option("--username", help="The username for authentication.")
|
|
31
|
+
@click.option("--password", help="The password for authentication.")
|
|
32
|
+
@click.option("--token", help="The JWT token for authentication.")
|
|
33
|
+
@click.pass_context
|
|
34
|
+
async def login(ctx, base_url, username, password, verify_ssl, token):
|
|
35
|
+
"""Logs in to the Bedrock Server Manager API."""
|
|
36
|
+
config = ctx.obj["config"]
|
|
37
|
+
|
|
38
|
+
if base_url:
|
|
39
|
+
validated_url = _validate_and_get_url(base_url)
|
|
40
|
+
config.set("base_url", validated_url)
|
|
41
|
+
|
|
42
|
+
config.set("verify_ssl", verify_ssl)
|
|
43
|
+
|
|
44
|
+
if token:
|
|
45
|
+
config.jwt_token = token
|
|
46
|
+
click.echo("Token set.")
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
if not username and not password and not token:
|
|
50
|
+
await interactive_login(ctx)
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
if username and not password:
|
|
54
|
+
password = click.prompt("Password", hide_input=True)
|
|
55
|
+
|
|
56
|
+
client = BedrockServerManagerApi(
|
|
57
|
+
base_url=config.base_url,
|
|
58
|
+
username=username,
|
|
59
|
+
password=password,
|
|
60
|
+
verify_ssl=config.verify_ssl,
|
|
61
|
+
)
|
|
62
|
+
try:
|
|
63
|
+
token_data = await client.authenticate()
|
|
64
|
+
config.jwt_token = token_data.access_token
|
|
65
|
+
click.echo("Login successful.")
|
|
66
|
+
except AuthError as e:
|
|
67
|
+
click.secho(f"Login failed: {e}", fg="red")
|
|
68
|
+
finally:
|
|
69
|
+
await client.close()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def interactive_login(ctx):
|
|
73
|
+
"""Handles the interactive login prompt."""
|
|
74
|
+
config = ctx.obj["config"]
|
|
75
|
+
username = click.prompt("Username")
|
|
76
|
+
password = click.prompt("Password", hide_input=True)
|
|
77
|
+
|
|
78
|
+
validated_url = _validate_and_get_url(config.base_url)
|
|
79
|
+
if validated_url != config.base_url:
|
|
80
|
+
config.set("base_url", validated_url)
|
|
81
|
+
|
|
82
|
+
client = BedrockServerManagerApi(
|
|
83
|
+
base_url=validated_url,
|
|
84
|
+
username=username,
|
|
85
|
+
password=password,
|
|
86
|
+
verify_ssl=config.verify_ssl,
|
|
87
|
+
)
|
|
88
|
+
try:
|
|
89
|
+
token_data = await client.authenticate()
|
|
90
|
+
config.jwt_token = token_data.access_token
|
|
91
|
+
click.echo("Login successful.")
|
|
92
|
+
except AuthError as e:
|
|
93
|
+
click.secho(f"Login failed: {e}", fg="red")
|
|
94
|
+
finally:
|
|
95
|
+
await client.close()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@auth.command()
|
|
99
|
+
@click.pass_context
|
|
100
|
+
async def logout(ctx):
|
|
101
|
+
"""Logs out from the Bedrock Server Manager API."""
|
|
102
|
+
config = ctx.obj["config"]
|
|
103
|
+
config.jwt_token = None
|
|
104
|
+
click.echo("Logged out.")
|
|
105
|
+
if "client" in ctx.obj and ctx.obj["client"]:
|
|
106
|
+
await ctx.obj["client"].close()
|
bsm_cli/backup.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import questionary
|
|
5
|
+
from bsm_cli.decorators import monitor_task, pass_async_context
|
|
6
|
+
|
|
7
|
+
from bsm_api_client.models import BackupActionPayload, RestoreActionPayload
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group()
|
|
11
|
+
def backup():
|
|
12
|
+
"""Manages server backups."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@backup.command("create")
|
|
17
|
+
@click.option(
|
|
18
|
+
"-s", "--server", "server_name", required=True, help="Name of the target server."
|
|
19
|
+
)
|
|
20
|
+
@click.option(
|
|
21
|
+
"-t",
|
|
22
|
+
"--type",
|
|
23
|
+
"backup_type",
|
|
24
|
+
type=click.Choice(["world", "config", "all"], case_sensitive=False),
|
|
25
|
+
help="Type of backup to create; skips interactive menu.",
|
|
26
|
+
)
|
|
27
|
+
@click.option(
|
|
28
|
+
"-f",
|
|
29
|
+
"--file",
|
|
30
|
+
"file_to_backup",
|
|
31
|
+
help="Specific file to back up (required if --type=config).",
|
|
32
|
+
)
|
|
33
|
+
@pass_async_context
|
|
34
|
+
async def create_backup(ctx, server_name: str, backup_type: str, file_to_backup: str):
|
|
35
|
+
"""Creates a backup of specified server data."""
|
|
36
|
+
client = ctx.obj.get("client")
|
|
37
|
+
if not client:
|
|
38
|
+
click.secho("You are not logged in.", fg="red")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
if not backup_type:
|
|
43
|
+
backup_type, file_to_backup, _ = await _interactive_backup_menu(server_name)
|
|
44
|
+
|
|
45
|
+
if backup_type == "config" and not file_to_backup:
|
|
46
|
+
raise click.UsageError(
|
|
47
|
+
"Option '--file' is required when using '--type config'."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
click.echo(f"Starting '{backup_type}' backup for server '{server_name}'...")
|
|
51
|
+
|
|
52
|
+
payload = BackupActionPayload(
|
|
53
|
+
backup_type=backup_type, file_to_backup=file_to_backup
|
|
54
|
+
)
|
|
55
|
+
response = await client.async_trigger_server_backup(server_name, payload)
|
|
56
|
+
|
|
57
|
+
if response.task_id:
|
|
58
|
+
await monitor_task(
|
|
59
|
+
client,
|
|
60
|
+
response.task_id,
|
|
61
|
+
"Backup completed successfully",
|
|
62
|
+
"Failed to create backup",
|
|
63
|
+
)
|
|
64
|
+
click.echo("Pruning old backups...")
|
|
65
|
+
prune_response = await client.async_prune_server_backups(server_name)
|
|
66
|
+
if prune_response.task_id:
|
|
67
|
+
await monitor_task(
|
|
68
|
+
client,
|
|
69
|
+
prune_response.task_id,
|
|
70
|
+
"Pruning complete",
|
|
71
|
+
"Failed to prune backups",
|
|
72
|
+
)
|
|
73
|
+
elif prune_response.status == "success":
|
|
74
|
+
click.secho("Pruning complete.", fg="green")
|
|
75
|
+
else:
|
|
76
|
+
click.secho(
|
|
77
|
+
f"Failed to prune backups: {prune_response.message}", fg="red"
|
|
78
|
+
)
|
|
79
|
+
elif response.status == "success":
|
|
80
|
+
click.secho("Backup completed successfully.", fg="green")
|
|
81
|
+
else:
|
|
82
|
+
click.secho(f"Failed to create backup: {response.message}", fg="red")
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
click.secho(f"An error occurred: {e}", fg="red")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@backup.command("restore")
|
|
89
|
+
@click.option(
|
|
90
|
+
"-s", "--server", "server_name", required=True, help="Name of the target server."
|
|
91
|
+
)
|
|
92
|
+
@click.option(
|
|
93
|
+
"-f",
|
|
94
|
+
"--file",
|
|
95
|
+
"backup_file_path",
|
|
96
|
+
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
|
|
97
|
+
help="Path to the backup file to restore; skips interactive menu.",
|
|
98
|
+
)
|
|
99
|
+
@pass_async_context
|
|
100
|
+
async def restore_backup(ctx, server_name: str, backup_file_path: str): # noqa: C901
|
|
101
|
+
"""Restores server data from a specified backup file."""
|
|
102
|
+
client = ctx.obj.get("client")
|
|
103
|
+
if not client:
|
|
104
|
+
click.secho("You are not logged in.", fg="red")
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
if not backup_file_path:
|
|
109
|
+
restore_type, backup_file_path, _ = await _interactive_restore_menu(
|
|
110
|
+
client, server_name
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
filename = os.path.basename(backup_file_path).lower()
|
|
114
|
+
if "world" in filename:
|
|
115
|
+
restore_type = "world"
|
|
116
|
+
elif "allowlist" in filename:
|
|
117
|
+
restore_type = "allowlist"
|
|
118
|
+
elif "permissions" in filename:
|
|
119
|
+
restore_type = "permissions"
|
|
120
|
+
elif "properties" in filename:
|
|
121
|
+
restore_type = "properties"
|
|
122
|
+
else:
|
|
123
|
+
raise click.UsageError(
|
|
124
|
+
f"Could not determine restore type from filename '{filename}'."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
click.echo(
|
|
128
|
+
f"Starting '{restore_type}' restore for server '{server_name}' from '{os.path.basename(backup_file_path)}'..."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
payload = RestoreActionPayload(
|
|
132
|
+
restore_type=restore_type, backup_file=os.path.basename(backup_file_path)
|
|
133
|
+
)
|
|
134
|
+
response = await client.async_restore_server_backup(server_name, payload)
|
|
135
|
+
|
|
136
|
+
if response.task_id:
|
|
137
|
+
await monitor_task(
|
|
138
|
+
client,
|
|
139
|
+
response.task_id,
|
|
140
|
+
"Restore completed successfully",
|
|
141
|
+
"Failed to restore backup",
|
|
142
|
+
)
|
|
143
|
+
elif response.status == "success":
|
|
144
|
+
click.secho("Restore completed successfully.", fg="green")
|
|
145
|
+
else:
|
|
146
|
+
click.secho(f"Failed to restore backup: {response.message}", fg="red")
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
click.secho(f"An error occurred: {e}", fg="red")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@backup.command("prune")
|
|
153
|
+
@click.option(
|
|
154
|
+
"-s",
|
|
155
|
+
"--server",
|
|
156
|
+
"server_name",
|
|
157
|
+
required=True,
|
|
158
|
+
help="Name of the server whose backups to prune.",
|
|
159
|
+
)
|
|
160
|
+
@pass_async_context
|
|
161
|
+
async def prune_backups(ctx, server_name: str):
|
|
162
|
+
"""Deletes old backups for a server."""
|
|
163
|
+
client = ctx.obj.get("client")
|
|
164
|
+
if not client:
|
|
165
|
+
click.secho("You are not logged in.", fg="red")
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
click.echo(f"Pruning old backups for server '{server_name}'...")
|
|
170
|
+
response = await client.async_prune_server_backups(server_name)
|
|
171
|
+
if response.task_id:
|
|
172
|
+
await monitor_task(
|
|
173
|
+
client,
|
|
174
|
+
response.task_id,
|
|
175
|
+
"Pruning complete",
|
|
176
|
+
"Failed to prune backups",
|
|
177
|
+
)
|
|
178
|
+
elif response.status == "success":
|
|
179
|
+
click.secho("Pruning complete.", fg="green")
|
|
180
|
+
else:
|
|
181
|
+
click.secho(f"Failed to prune backups: {response.message}", fg="red")
|
|
182
|
+
except Exception as e:
|
|
183
|
+
click.secho(f"An error occurred during pruning: {e}", fg="red")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
async def _interactive_backup_menu(server_name: str):
|
|
187
|
+
click.secho(f"Entering interactive backup for server: {server_name}", fg="yellow")
|
|
188
|
+
|
|
189
|
+
backup_type_map = {
|
|
190
|
+
"Backup World Only": ("world", None, True),
|
|
191
|
+
"Backup Everything (World + Configs)": ("all", None, True),
|
|
192
|
+
"Backup a Specific Configuration File": ("config", None, False),
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
choice = await questionary.select(
|
|
196
|
+
"Select a backup option:",
|
|
197
|
+
choices=list(backup_type_map.keys()) + ["Cancel"],
|
|
198
|
+
).ask_async()
|
|
199
|
+
|
|
200
|
+
if not choice or choice == "Cancel":
|
|
201
|
+
raise click.Abort()
|
|
202
|
+
|
|
203
|
+
b_type, b_file, b_change_status = backup_type_map[choice]
|
|
204
|
+
|
|
205
|
+
if b_type == "config":
|
|
206
|
+
config_file_map = {
|
|
207
|
+
"allowlist.json": "allowlist.json",
|
|
208
|
+
"permissions.json": "permissions.json",
|
|
209
|
+
"server.properties": "server.properties",
|
|
210
|
+
}
|
|
211
|
+
file_choice = await questionary.select(
|
|
212
|
+
"Which configuration file do you want to back up?",
|
|
213
|
+
choices=list(config_file_map.keys()) + ["Cancel"],
|
|
214
|
+
).ask_async()
|
|
215
|
+
|
|
216
|
+
if not file_choice or file_choice == "Cancel":
|
|
217
|
+
raise click.Abort()
|
|
218
|
+
b_file = config_file_map[file_choice]
|
|
219
|
+
|
|
220
|
+
return b_type, b_file, b_change_status
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
async def _interactive_restore_menu(client, server_name: str):
|
|
224
|
+
click.secho(f"Entering interactive restore for server: {server_name}", fg="yellow")
|
|
225
|
+
|
|
226
|
+
restore_type_map = {
|
|
227
|
+
"Restore World": "world",
|
|
228
|
+
"Restore Allowlist": "allowlist",
|
|
229
|
+
"Restore Permissions": "permissions",
|
|
230
|
+
"Restore Properties": "properties",
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
choice = await questionary.select(
|
|
234
|
+
"What do you want to restore?",
|
|
235
|
+
choices=list(restore_type_map.keys()) + ["Cancel"],
|
|
236
|
+
).ask_async()
|
|
237
|
+
|
|
238
|
+
if not choice or choice == "Cancel":
|
|
239
|
+
raise click.Abort()
|
|
240
|
+
restore_type = restore_type_map[choice]
|
|
241
|
+
|
|
242
|
+
response = await client.async_list_server_backups(server_name, restore_type)
|
|
243
|
+
backup_files = response.backups
|
|
244
|
+
if not backup_files:
|
|
245
|
+
click.secho(
|
|
246
|
+
f"No '{restore_type}' backups found for server '{server_name}'.",
|
|
247
|
+
fg="yellow",
|
|
248
|
+
)
|
|
249
|
+
raise click.Abort()
|
|
250
|
+
|
|
251
|
+
file_map = {os.path.basename(f): f for f in backup_files}
|
|
252
|
+
file_choices = sorted(list(file_map.keys()), reverse=True)
|
|
253
|
+
|
|
254
|
+
file_to_restore_basename = await questionary.select(
|
|
255
|
+
f"Select a '{restore_type}' backup to restore:",
|
|
256
|
+
choices=file_choices + ["Cancel"],
|
|
257
|
+
).ask_async()
|
|
258
|
+
|
|
259
|
+
if not file_to_restore_basename or file_to_restore_basename == "Cancel":
|
|
260
|
+
raise click.Abort()
|
|
261
|
+
selected_file_path = file_map[file_to_restore_basename]
|
|
262
|
+
|
|
263
|
+
return restore_type, selected_file_path, True
|