mcmanager 0.1.2__tar.gz → 0.1.3__tar.gz
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.
- {mcmanager-0.1.2 → mcmanager-0.1.3}/PKG-INFO +5 -2
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/cli.py +135 -115
- mcmanager-0.1.3/mcmanager/console/admin.py +54 -0
- mcmanager-0.1.3/mcmanager/console/apps.py +10 -0
- mcmanager-0.1.3/mcmanager/console/compat.py +31 -0
- mcmanager-0.1.3/mcmanager/console/forms.py +15 -0
- mcmanager-0.1.3/mcmanager/console/json_utils.py +8 -0
- mcmanager-0.1.3/mcmanager/console/models.py +118 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/css/tailwind.css +813 -742
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/templates/console/index.html +149 -13
- mcmanager-0.1.3/mcmanager/console/templates/console/jars.html +122 -0
- mcmanager-0.1.3/mcmanager/console/templates/index.html +152 -0
- mcmanager-0.1.3/mcmanager/console/urls.py +23 -0
- mcmanager-0.1.3/mcmanager/console/views.py +149 -0
- mcmanager-0.1.3/mcmanager/console/views_backups.py +63 -0
- mcmanager-0.1.3/mcmanager/console/views_jars.py +54 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/settings.py +8 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager.egg-info/PKG-INFO +5 -2
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager.egg-info/SOURCES.txt +7 -2
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager.egg-info/requires.txt +4 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/pyproject.toml +45 -36
- {mcmanager-0.1.2 → mcmanager-0.1.3}/requirements.txt +2 -2
- mcmanager-0.1.2/mcmanager/console/admin.py +0 -29
- mcmanager-0.1.2/mcmanager/console/apps.py +0 -22
- mcmanager-0.1.2/mcmanager/console/models.py +0 -112
- mcmanager-0.1.2/mcmanager/console/templates/index.html +0 -42
- mcmanager-0.1.2/mcmanager/console/tests.py +0 -3
- mcmanager-0.1.2/mcmanager/console/urls.py +0 -14
- mcmanager-0.1.2/mcmanager/console/views.py +0 -175
- {mcmanager-0.1.2 → mcmanager-0.1.3}/MANIFEST.in +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/README.md +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/__init__.py +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/__main__.py +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/asgi.py +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/configs/server.properties +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/__init__.py +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/css/font-awesome.css +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/css/font-awesome.min.css +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/css/styles.css +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/css/toastify.min.css +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/fonts/fontawesome-webfont.ttf +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/fonts/fontawesome-webfont.woff +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/fonts/fontawesome-webfont.woff2 +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/img/favicon.ico +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/js/toastify-js.js +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/urls.py +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/wsgi.py +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager.egg-info/dependency_links.txt +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager.egg-info/entry_points.txt +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager.egg-info/top_level.txt +0 -0
- {mcmanager-0.1.2 → mcmanager-0.1.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcmanager
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A Django-based web interface to manage Minecraft servers on a VPS
|
|
5
5
|
License: MIT
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -8,11 +8,14 @@ Classifier: Framework :: Django
|
|
|
8
8
|
Classifier: License :: OSI Approved :: MIT License
|
|
9
9
|
Classifier: Operating System :: POSIX :: Linux
|
|
10
10
|
Classifier: Topic :: Games/Entertainment
|
|
11
|
-
Requires-Python: >=3.
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
Requires-Dist: Django<5.2.0,>=4.2.0
|
|
14
14
|
Requires-Dist: psutil>=5.8.0
|
|
15
15
|
Requires-Dist: whitenoise>=6.0.0
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest-django>=4.7.0; extra == "dev"
|
|
16
19
|
|
|
17
20
|
# mcmanager (BETA)
|
|
18
21
|
|
|
@@ -1,115 +1,135 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import sys
|
|
3
|
-
import argparse
|
|
4
|
-
import shutil
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
def get_data_dir():
|
|
8
|
-
"""Determina o diretório de dados com base na variável de ambiente ou home"""
|
|
9
|
-
if os.environ.get("MCMANAGER_DATA_DIR"):
|
|
10
|
-
return Path(os.environ["MCMANAGER_DATA_DIR"]).resolve()
|
|
11
|
-
|
|
12
|
-
debug_mode = os.environ.get("MCMANAGER_DEBUG", "False").lower() in ("true", "1", "yes")
|
|
13
|
-
if debug_mode:
|
|
14
|
-
# Se estiver em modo debug local, usa a pasta do próprio projeto
|
|
15
|
-
return Path(__file__).resolve().parent.parent
|
|
16
|
-
else:
|
|
17
|
-
return Path.home() / ".mcmanager"
|
|
18
|
-
|
|
19
|
-
def main():
|
|
20
|
-
parser = argparse.ArgumentParser(
|
|
21
|
-
description="mcmanager - Painel de Controle de Servidores Minecraft"
|
|
22
|
-
)
|
|
23
|
-
subparsers = parser.add_subparsers(dest="command", required=True, help="Subcomando a executar")
|
|
24
|
-
|
|
25
|
-
# Comando init
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# Comando run
|
|
29
|
-
run_parser = subparsers.add_parser("run", help="Inicia o servidor web do painel")
|
|
30
|
-
run_parser.add_argument("--host", default="0.0.0.0", help="Endereço IP para escutar (padrão: 0.0.0.0)")
|
|
31
|
-
run_parser.add_argument("--port", type=int, default=8000, help="Porta para escutar (padrão: 8000)")
|
|
32
|
-
|
|
33
|
-
# Comando createsuperuser
|
|
34
|
-
|
|
35
|
-
"createsuperuser", help="Cria um usuário administrador para o painel"
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
# Comando shell
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if copied:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
print("[
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
elif args.command == "
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
call_command("
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import argparse
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
def get_data_dir():
|
|
8
|
+
"""Determina o diretório de dados com base na variável de ambiente ou home"""
|
|
9
|
+
if os.environ.get("MCMANAGER_DATA_DIR"):
|
|
10
|
+
return Path(os.environ["MCMANAGER_DATA_DIR"]).resolve()
|
|
11
|
+
|
|
12
|
+
debug_mode = os.environ.get("MCMANAGER_DEBUG", "False").lower() in ("true", "1", "yes")
|
|
13
|
+
if debug_mode:
|
|
14
|
+
# Se estiver em modo debug local, usa a pasta do próprio projeto
|
|
15
|
+
return Path(__file__).resolve().parent.parent
|
|
16
|
+
else:
|
|
17
|
+
return Path.home() / ".mcmanager"
|
|
18
|
+
|
|
19
|
+
def main():
|
|
20
|
+
parser = argparse.ArgumentParser(
|
|
21
|
+
description="mcmanager - Painel de Controle de Servidores Minecraft"
|
|
22
|
+
)
|
|
23
|
+
subparsers = parser.add_subparsers(dest="command", required=True, help="Subcomando a executar")
|
|
24
|
+
|
|
25
|
+
# Comando init
|
|
26
|
+
subparsers.add_parser("init", help="Inicializa o diretório de dados, banco e arquivos padrões")
|
|
27
|
+
|
|
28
|
+
# Comando run
|
|
29
|
+
run_parser = subparsers.add_parser("run", help="Inicia o servidor web do painel")
|
|
30
|
+
run_parser.add_argument("--host", default="0.0.0.0", help="Endereço IP para escutar (padrão: 0.0.0.0)")
|
|
31
|
+
run_parser.add_argument("--port", type=int, default=8000, help="Porta para escutar (padrão: 8000)")
|
|
32
|
+
|
|
33
|
+
# Comando createsuperuser
|
|
34
|
+
subparsers.add_parser(
|
|
35
|
+
"createsuperuser", help="Cria um usuário administrador para o painel"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Comando shell
|
|
39
|
+
subparsers.add_parser("shell", help="Inicia o console interativo do Django")
|
|
40
|
+
|
|
41
|
+
# Comando doctor
|
|
42
|
+
subparsers.add_parser(
|
|
43
|
+
"doctor", help="Diagnostica o ambiente (Java, diretórios, banco de dados)"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
args = parser.parse_args()
|
|
47
|
+
|
|
48
|
+
# 1. Obter e criar estrutura de dados do usuário
|
|
49
|
+
data_dir = get_data_dir()
|
|
50
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
(data_dir / "servers").mkdir(exist_ok=True)
|
|
52
|
+
(data_dir / "jar").mkdir(exist_ok=True)
|
|
53
|
+
(data_dir / "configs").mkdir(exist_ok=True)
|
|
54
|
+
(data_dir / "run").mkdir(exist_ok=True)
|
|
55
|
+
(data_dir / "backups").mkdir(exist_ok=True)
|
|
56
|
+
|
|
57
|
+
# 2. Copiar server.properties padrão se não existir
|
|
58
|
+
properties_dest = data_dir / "configs" / "server.properties"
|
|
59
|
+
if not properties_dest.exists():
|
|
60
|
+
# Tenta carregar usando importlib.resources (PEP 517)
|
|
61
|
+
copied = False
|
|
62
|
+
try:
|
|
63
|
+
from importlib import resources
|
|
64
|
+
# Python 3.9+
|
|
65
|
+
source_path = resources.files("mcmanager").joinpath("configs/server.properties")
|
|
66
|
+
with source_path.open("rb") as fsrc:
|
|
67
|
+
with open(properties_dest, "wb") as fdst:
|
|
68
|
+
shutil.copyfileobj(fsrc, fdst)
|
|
69
|
+
copied = True
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
if not copied:
|
|
74
|
+
# Fallback para caminho relativo
|
|
75
|
+
source_path = Path(__file__).resolve().parent / "configs" / "server.properties"
|
|
76
|
+
if source_path.exists():
|
|
77
|
+
shutil.copy(source_path, properties_dest)
|
|
78
|
+
copied = True
|
|
79
|
+
|
|
80
|
+
if copied:
|
|
81
|
+
print(f"[*] Arquivo server.properties padrão copiado para: {properties_dest}")
|
|
82
|
+
else:
|
|
83
|
+
print("[!] Aviso: Não foi possível copiar o arquivo server.properties padrão.")
|
|
84
|
+
|
|
85
|
+
# 3. Configurar variáveis de ambiente do Django
|
|
86
|
+
os.environ.setdefault("MCMANAGER_DATA_DIR", str(data_dir))
|
|
87
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mcmanager.settings")
|
|
88
|
+
|
|
89
|
+
# 4. Inicializar Django
|
|
90
|
+
import django
|
|
91
|
+
django.setup()
|
|
92
|
+
|
|
93
|
+
from django.core.management import call_command
|
|
94
|
+
|
|
95
|
+
# 5. Executar os subcomandos
|
|
96
|
+
if args.command == "init":
|
|
97
|
+
print("[*] Executando migrações do banco de dados...")
|
|
98
|
+
call_command("migrate", interactive=False)
|
|
99
|
+
print("[*] Coletando arquivos estáticos...")
|
|
100
|
+
call_command("collectstatic", "--noinput", "--clear")
|
|
101
|
+
print("\n[+] Inicialização concluída com sucesso!")
|
|
102
|
+
print(f"[+] Seus dados estão em: {data_dir}")
|
|
103
|
+
print("[+] Agora você pode rodar: mcmanager createsuperuser")
|
|
104
|
+
|
|
105
|
+
elif args.command == "run":
|
|
106
|
+
from mcmanager.console.services import supervisor
|
|
107
|
+
supervisor.start()
|
|
108
|
+
bind_address = f"{args.host}:{args.port}"
|
|
109
|
+
print(f"[*] Iniciando servidor mcmanager em http://{bind_address}/")
|
|
110
|
+
print("[*] Pressione CTRL+C para encerrar.")
|
|
111
|
+
# Em produção, rodamos sem autoreload
|
|
112
|
+
call_command("runserver", "--noreload", "--nostatic", bind_address)
|
|
113
|
+
|
|
114
|
+
elif args.command == "createsuperuser":
|
|
115
|
+
try:
|
|
116
|
+
call_command("createsuperuser")
|
|
117
|
+
except KeyboardInterrupt:
|
|
118
|
+
print("\n[!] Operação cancelada.")
|
|
119
|
+
|
|
120
|
+
elif args.command == "shell":
|
|
121
|
+
call_command("shell")
|
|
122
|
+
|
|
123
|
+
elif args.command == "doctor":
|
|
124
|
+
from mcmanager.console.services import doctor
|
|
125
|
+
results = doctor.run_checks()
|
|
126
|
+
all_passed = True
|
|
127
|
+
for result in results:
|
|
128
|
+
symbol = "OK" if result['passed'] else "FAIL"
|
|
129
|
+
print(f"[{symbol}] {result['name']}: {result['message']}")
|
|
130
|
+
if not result['passed']:
|
|
131
|
+
all_passed = False
|
|
132
|
+
sys.exit(0 if all_passed else 1)
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
main()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from django.contrib import admin
|
|
4
|
+
|
|
5
|
+
from .forms import ServerForm
|
|
6
|
+
from .models import Server, Type
|
|
7
|
+
from .services import process, provisioning
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@admin.register(Server)
|
|
11
|
+
class ServerAdmin(admin.ModelAdmin):
|
|
12
|
+
form = ServerForm
|
|
13
|
+
list_display = ('name', 'port', 'type', 'is_running')
|
|
14
|
+
exclude = ('jar', 'server_properties', 'desired_running', 'consecutive_restart_failures', 'last_scheduled_backup_date')
|
|
15
|
+
search_fields = ('name', 'port', 'type')
|
|
16
|
+
list_filter = ('type',)
|
|
17
|
+
readonly_fields = ()
|
|
18
|
+
|
|
19
|
+
def is_running(self, obj):
|
|
20
|
+
return process.is_running(obj)
|
|
21
|
+
is_running.boolean = True
|
|
22
|
+
is_running.short_description = 'Running'
|
|
23
|
+
|
|
24
|
+
def get_form(self, request, obj=..., change=..., **kwargs) -> Any:
|
|
25
|
+
internal_fields = ('desired_running', 'consecutive_restart_failures', 'last_scheduled_backup_date')
|
|
26
|
+
if obj is None:
|
|
27
|
+
self.exclude = ('jar', 'server_properties') + internal_fields
|
|
28
|
+
self.readonly_fields = ()
|
|
29
|
+
else:
|
|
30
|
+
self.exclude = internal_fields
|
|
31
|
+
self.readonly_fields = ('jar',)
|
|
32
|
+
return super().get_form(request, obj, change, **kwargs)
|
|
33
|
+
|
|
34
|
+
def save_model(self, request, obj, form, change):
|
|
35
|
+
super().save_model(request, obj, form, change)
|
|
36
|
+
if not obj.jar:
|
|
37
|
+
provisioning.create_server_files(obj)
|
|
38
|
+
else:
|
|
39
|
+
provisioning.sync_server_properties_file(obj)
|
|
40
|
+
|
|
41
|
+
def delete_model(self, request, obj):
|
|
42
|
+
provisioning.delete_server_files(obj)
|
|
43
|
+
super().delete_model(request, obj)
|
|
44
|
+
|
|
45
|
+
def delete_queryset(self, request, queryset):
|
|
46
|
+
for obj in queryset:
|
|
47
|
+
provisioning.delete_server_files(obj)
|
|
48
|
+
super().delete_queryset(request, queryset)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@admin.register(Type)
|
|
52
|
+
class TypeAdmin(admin.ModelAdmin):
|
|
53
|
+
list_display = ('name',)
|
|
54
|
+
search_fields = ('name',)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ConsoleConfig(AppConfig):
|
|
5
|
+
default_auto_field = 'django.db.models.BigAutoField'
|
|
6
|
+
name = 'mcmanager.console'
|
|
7
|
+
|
|
8
|
+
def ready(self):
|
|
9
|
+
from .compat import patch_context_copy_for_python314
|
|
10
|
+
patch_context_copy_for_python314()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Runtime compatibility shims for this project's supported environments."""
|
|
2
|
+
from django.template.context import BaseContext
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def patch_context_copy_for_python314():
|
|
6
|
+
"""Python 3.14 changed copy.copy()'s handling of `super` proxy objects,
|
|
7
|
+
breaking Django 5.1's BaseContext.__copy__ (which does
|
|
8
|
+
`duplicate = copy(super())` to get a shallow copy of self while bypassing
|
|
9
|
+
dict's own __copy__ -- BaseContext isn't a dict subclass, but relies on
|
|
10
|
+
this idiom to allocate a bare instance). This now raises
|
|
11
|
+
`AttributeError: 'super' object has no attribute 'dicts' and no __dict__
|
|
12
|
+
for setting new attributes` any time Django copies a template Context --
|
|
13
|
+
which happens on every Django admin change-list page render (via the
|
|
14
|
+
`{% change_list_object_tools %}` templatetag), and more generally
|
|
15
|
+
whenever `Context.new()` is called. Reproduced independently of Django
|
|
16
|
+
with a minimal `copy(super())` snippet, so this is an upstream Python
|
|
17
|
+
3.14 regression, not an application bug. Django 5.1.15 (latest 5.1.x as
|
|
18
|
+
of writing) does not include a fix.
|
|
19
|
+
|
|
20
|
+
TODO: remove once either (a) Django ships a fix for the copy(super())
|
|
21
|
+
idiom under Python 3.14+, or (b) this project's supported Python floor
|
|
22
|
+
is raised past whatever version fixes it -- check `python --version`
|
|
23
|
+
and retest by removing this call before deleting it.
|
|
24
|
+
"""
|
|
25
|
+
def _base_context_copy(self):
|
|
26
|
+
duplicate = object.__new__(self.__class__)
|
|
27
|
+
duplicate.__dict__.update(self.__dict__)
|
|
28
|
+
duplicate.dicts = self.dicts[:]
|
|
29
|
+
return duplicate
|
|
30
|
+
|
|
31
|
+
BaseContext.__copy__ = _base_context_copy
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from django import forms
|
|
2
|
+
|
|
3
|
+
from .models import Server, get_jar_files
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ServerForm(forms.ModelForm):
|
|
7
|
+
jar_template = forms.ChoiceField(choices=[])
|
|
8
|
+
|
|
9
|
+
class Meta:
|
|
10
|
+
model = Server
|
|
11
|
+
fields = '__all__'
|
|
12
|
+
|
|
13
|
+
def __init__(self, *args, **kwargs):
|
|
14
|
+
super().__init__(*args, **kwargs)
|
|
15
|
+
self.fields['jar_template'].choices = get_jar_files()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Shared JSON error-response helper. Success responses keep their existing,
|
|
2
|
+
per-view flat shape unchanged — only error responses use this, so they get a
|
|
3
|
+
real HTTP status code instead of always returning 200."""
|
|
4
|
+
from django.http import JsonResponse
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def json_error(message, status=400):
|
|
8
|
+
return JsonResponse({'status': 'error', 'message': message}, status=status)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django.core.exceptions import ValidationError
|
|
5
|
+
from django.db import models
|
|
6
|
+
from django.db.models import Q
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_jar_files():
|
|
10
|
+
jar_dir = getattr(settings, 'JAR_DIR', os.path.join(settings.BASE_DIR, 'jar'))
|
|
11
|
+
if not os.path.exists(jar_dir):
|
|
12
|
+
os.makedirs(jar_dir)
|
|
13
|
+
return [(x, x) for x in os.listdir(jar_dir) if x.endswith('.jar')]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_default_server_prop():
|
|
17
|
+
configs_dir = getattr(settings, 'CONFIGS_DIR', os.path.join(settings.BASE_DIR, 'configs'))
|
|
18
|
+
with open(os.path.join(configs_dir, 'server.properties'), 'r', encoding='utf-8') as arq:
|
|
19
|
+
text = arq.read()
|
|
20
|
+
return text
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Type(models.Model):
|
|
24
|
+
name = models.CharField(max_length=100)
|
|
25
|
+
dependencies = models.JSONField(blank=True, null=True)
|
|
26
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
27
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
28
|
+
|
|
29
|
+
def __str__(self):
|
|
30
|
+
return self.name
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
MAX_GAME_PORT = 55535 # port + 10000 (the RCON port) must not exceed 65535
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Server(models.Model):
|
|
37
|
+
name = models.CharField(max_length=100)
|
|
38
|
+
jar_template = models.CharField(max_length=100)
|
|
39
|
+
jar = models.CharField(max_length=100, blank=True, null=True)
|
|
40
|
+
port = models.IntegerField(default=25565)
|
|
41
|
+
memory_limit = models.IntegerField("Memory Limit (MB)", default=1024)
|
|
42
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
43
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
44
|
+
type = models.ForeignKey(
|
|
45
|
+
Type, on_delete=models.CASCADE, related_name='servers')
|
|
46
|
+
server_properties = models.TextField(blank=True, null=True)
|
|
47
|
+
rcon_port = models.IntegerField(editable=False, default=0)
|
|
48
|
+
rcon_password = models.CharField(max_length=32, editable=False, default='', blank=True)
|
|
49
|
+
auto_restart_enabled = models.BooleanField(default=False)
|
|
50
|
+
desired_running = models.BooleanField(default=False, editable=False)
|
|
51
|
+
consecutive_restart_failures = models.IntegerField(default=0, editable=False)
|
|
52
|
+
scheduled_backup_time = models.TimeField(null=True, blank=True)
|
|
53
|
+
last_scheduled_backup_date = models.DateField(null=True, blank=True, editable=False)
|
|
54
|
+
|
|
55
|
+
def __str__(self):
|
|
56
|
+
return self.name
|
|
57
|
+
|
|
58
|
+
def clean(self):
|
|
59
|
+
super().clean()
|
|
60
|
+
if self.port is None:
|
|
61
|
+
return
|
|
62
|
+
if self.port > MAX_GAME_PORT:
|
|
63
|
+
raise ValidationError({
|
|
64
|
+
'port': 'Port must leave room for the RCON port (port + 10000 must not exceed 65535).',
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
implied_rcon_port = self.port + 10000
|
|
68
|
+
conflict = Server.objects.exclude(pk=self.pk).filter(
|
|
69
|
+
Q(port=implied_rcon_port)
|
|
70
|
+
| Q(rcon_port=implied_rcon_port)
|
|
71
|
+
| Q(rcon_port=self.port)
|
|
72
|
+
).first()
|
|
73
|
+
if conflict is not None:
|
|
74
|
+
raise ValidationError({
|
|
75
|
+
'port': (
|
|
76
|
+
f'This port implies RCON port {implied_rcon_port}, which conflicts with '
|
|
77
|
+
f'server "{conflict.name}" (port={conflict.port}, rcon_port={conflict.rcon_port}).'
|
|
78
|
+
),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class JarDownload(models.Model):
|
|
83
|
+
STATUS_CHOICES = [
|
|
84
|
+
('pending', 'Pending'),
|
|
85
|
+
('downloading', 'Downloading'),
|
|
86
|
+
('done', 'Done'),
|
|
87
|
+
('error', 'Error'),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
provider = models.CharField(max_length=20)
|
|
91
|
+
version = models.CharField(max_length=50)
|
|
92
|
+
filename = models.CharField(max_length=255, blank=True)
|
|
93
|
+
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
|
|
94
|
+
error_message = models.TextField(blank=True)
|
|
95
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
96
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
97
|
+
|
|
98
|
+
def __str__(self):
|
|
99
|
+
return f'{self.provider} {self.version} ({self.status})'
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Backup(models.Model):
|
|
103
|
+
STATUS_CHOICES = [
|
|
104
|
+
('pending', 'Pending'),
|
|
105
|
+
('running', 'Running'),
|
|
106
|
+
('done', 'Done'),
|
|
107
|
+
('error', 'Error'),
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
server = models.ForeignKey(Server, on_delete=models.CASCADE, related_name='backups')
|
|
111
|
+
filename = models.CharField(max_length=255, blank=True)
|
|
112
|
+
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
|
|
113
|
+
error_message = models.TextField(blank=True)
|
|
114
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
115
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
116
|
+
|
|
117
|
+
def __str__(self):
|
|
118
|
+
return f'{self.server.name} backup {self.filename or "(pending)"} ({self.status})'
|