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.
Files changed (51) hide show
  1. {mcmanager-0.1.2 → mcmanager-0.1.3}/PKG-INFO +5 -2
  2. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/cli.py +135 -115
  3. mcmanager-0.1.3/mcmanager/console/admin.py +54 -0
  4. mcmanager-0.1.3/mcmanager/console/apps.py +10 -0
  5. mcmanager-0.1.3/mcmanager/console/compat.py +31 -0
  6. mcmanager-0.1.3/mcmanager/console/forms.py +15 -0
  7. mcmanager-0.1.3/mcmanager/console/json_utils.py +8 -0
  8. mcmanager-0.1.3/mcmanager/console/models.py +118 -0
  9. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/css/tailwind.css +813 -742
  10. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/templates/console/index.html +149 -13
  11. mcmanager-0.1.3/mcmanager/console/templates/console/jars.html +122 -0
  12. mcmanager-0.1.3/mcmanager/console/templates/index.html +152 -0
  13. mcmanager-0.1.3/mcmanager/console/urls.py +23 -0
  14. mcmanager-0.1.3/mcmanager/console/views.py +149 -0
  15. mcmanager-0.1.3/mcmanager/console/views_backups.py +63 -0
  16. mcmanager-0.1.3/mcmanager/console/views_jars.py +54 -0
  17. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/settings.py +8 -0
  18. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager.egg-info/PKG-INFO +5 -2
  19. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager.egg-info/SOURCES.txt +7 -2
  20. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager.egg-info/requires.txt +4 -0
  21. {mcmanager-0.1.2 → mcmanager-0.1.3}/pyproject.toml +45 -36
  22. {mcmanager-0.1.2 → mcmanager-0.1.3}/requirements.txt +2 -2
  23. mcmanager-0.1.2/mcmanager/console/admin.py +0 -29
  24. mcmanager-0.1.2/mcmanager/console/apps.py +0 -22
  25. mcmanager-0.1.2/mcmanager/console/models.py +0 -112
  26. mcmanager-0.1.2/mcmanager/console/templates/index.html +0 -42
  27. mcmanager-0.1.2/mcmanager/console/tests.py +0 -3
  28. mcmanager-0.1.2/mcmanager/console/urls.py +0 -14
  29. mcmanager-0.1.2/mcmanager/console/views.py +0 -175
  30. {mcmanager-0.1.2 → mcmanager-0.1.3}/MANIFEST.in +0 -0
  31. {mcmanager-0.1.2 → mcmanager-0.1.3}/README.md +0 -0
  32. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/__init__.py +0 -0
  33. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/__main__.py +0 -0
  34. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/asgi.py +0 -0
  35. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/configs/server.properties +0 -0
  36. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/__init__.py +0 -0
  37. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/css/font-awesome.css +0 -0
  38. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/css/font-awesome.min.css +0 -0
  39. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/css/styles.css +0 -0
  40. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/css/toastify.min.css +0 -0
  41. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/fonts/fontawesome-webfont.ttf +0 -0
  42. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/fonts/fontawesome-webfont.woff +0 -0
  43. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/fonts/fontawesome-webfont.woff2 +0 -0
  44. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/img/favicon.ico +0 -0
  45. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/console/static/js/toastify-js.js +0 -0
  46. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/urls.py +0 -0
  47. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager/wsgi.py +0 -0
  48. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager.egg-info/dependency_links.txt +0 -0
  49. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager.egg-info/entry_points.txt +0 -0
  50. {mcmanager-0.1.2 → mcmanager-0.1.3}/mcmanager.egg-info/top_level.txt +0 -0
  51. {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.2
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.8
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
- init_parser = 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
- createsuperuser_parser = subparsers.add_parser(
35
- "createsuperuser", help="Cria um usuário administrador para o painel"
36
- )
37
-
38
- # Comando shell
39
- shell_parser = subparsers.add_parser("shell", help="Inicia o console interativo do Django")
40
-
41
- args = parser.parse_args()
42
-
43
- # 1. Obter e criar estrutura de dados do usuário
44
- data_dir = get_data_dir()
45
- data_dir.mkdir(parents=True, exist_ok=True)
46
- (data_dir / "servers").mkdir(exist_ok=True)
47
- (data_dir / "jar").mkdir(exist_ok=True)
48
- (data_dir / "configs").mkdir(exist_ok=True)
49
-
50
- # 2. Copiar server.properties padrão se não existir
51
- properties_dest = data_dir / "configs" / "server.properties"
52
- if not properties_dest.exists():
53
- # Tenta carregar usando importlib.resources (PEP 517)
54
- copied = False
55
- try:
56
- from importlib import resources
57
- # Python 3.9+
58
- source_path = resources.files("mcmanager").joinpath("configs/server.properties")
59
- with source_path.open("rb") as fsrc:
60
- with open(properties_dest, "wb") as fdst:
61
- shutil.copyfileobj(fsrc, fdst)
62
- copied = True
63
- except Exception:
64
- pass
65
-
66
- if not copied:
67
- # Fallback para caminho relativo
68
- source_path = Path(__file__).resolve().parent / "configs" / "server.properties"
69
- if source_path.exists():
70
- shutil.copy(source_path, properties_dest)
71
- copied = True
72
-
73
- if copied:
74
- print(f"[*] Arquivo server.properties padrão copiado para: {properties_dest}")
75
- else:
76
- print("[!] Aviso: Não foi possível copiar o arquivo server.properties padrão.")
77
-
78
- # 3. Configurar variáveis de ambiente do Django
79
- os.environ.setdefault("MCMANAGER_DATA_DIR", str(data_dir))
80
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mcmanager.settings")
81
-
82
- # 4. Inicializar Django
83
- import django
84
- django.setup()
85
-
86
- from django.core.management import call_command
87
-
88
- # 5. Executar os subcomandos
89
- if args.command == "init":
90
- print("[*] Executando migrações do banco de dados...")
91
- call_command("migrate", interactive=False)
92
- print("[*] Coletando arquivos estáticos...")
93
- call_command("collectstatic", "--noinput", "--clear")
94
- print("\n[+] Inicialização concluída com sucesso!")
95
- print(f"[+] Seus dados estão em: {data_dir}")
96
- print("[+] Agora você pode rodar: mcmanager createsuperuser")
97
-
98
- elif args.command == "run":
99
- bind_address = f"{args.host}:{args.port}"
100
- print(f"[*] Iniciando servidor mcmanager em http://{bind_address}/")
101
- print("[*] Pressione CTRL+C para encerrar.")
102
- # Em produção, rodamos sem autoreload
103
- call_command("runserver", "--noreload", "--nostatic", bind_address)
104
-
105
- elif args.command == "createsuperuser":
106
- try:
107
- call_command("createsuperuser")
108
- except KeyboardInterrupt:
109
- print("\n[!] Operação cancelada.")
110
-
111
- elif args.command == "shell":
112
- call_command("shell")
113
-
114
- if __name__ == "__main__":
115
- main()
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})'