Some checks failed
Deploy server-up (dev) / deploy (push) Failing after 2m40s
- LIBRARY_DIR, DATA_DIR en BACKUP_DIR worden afgeleid van BASE_DIR in plaats van hard op /opt/serverup te staan - docker-compose geeft BASE_DIR nu ook aan de container door; die stond alleen in de mount, dus de app bleef /opt/serverup voorstellen bij een /srv-mount - het installatieformulier vult appdata_dir met de ingestelde DATA_DIR, zodat alle 286 sjablonen de juiste map voorstellen - Instellingen → Paden heeft een hoofdmap met afgeleide submappen, een kopieerknop per pad en "Alles overnemen" - nieuw GET /api/paths/check: is het pad schrijfbaar en binnen de gemounte BASE_DIR, met de .env-regel als dat niet zo is - nieuw POST /api/paths/move: stacks stoppen, kopiëren, controleren, paden in compose/.env/metadata omschrijven, stacks starten en pas dán het oude opruimen - jobs.voortgang() plus een voortgangsbalk boven het terminallog - core/paden.py met meten, bereikbaarheid, verhuizen en omschrijven - docs/installeren.md en .env.example beschrijven BASE_DIR als de manier om alles te verplaatsen Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
240 lines
9.9 KiB
Python
240 lines
9.9 KiB
Python
"""De mappen van Server Up verplaatsen.
|
|
|
|
Dit is het enige onderdeel dat gegevens van de gebruiker weggooit, dus de
|
|
volgorde is de hele zaak: kopiëren, controleren, en pas daarna opruimen. Een
|
|
verhuizing die halverwege strandt met het origineel al verwijderd is het ergste
|
|
dat hier kan gebeuren.
|
|
"""
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
WORTEL = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(WORTEL / "server-up"))
|
|
|
|
from conftest import login
|
|
|
|
|
|
def _vul(d: Path, bestanden: dict[str, str]) -> Path:
|
|
for naam, inhoud in bestanden.items():
|
|
p = d / naam
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
p.write_text(inhoud, encoding="utf-8")
|
|
return d
|
|
|
|
|
|
# ── Hoofdmap en afgeleide paden ──────────────────────────────────────────────
|
|
|
|
def test_afgeleide_paden(env):
|
|
import core
|
|
assert core.afgeleide_paden("/srv/serverup") == {
|
|
"LIBRARY_DIR": "/srv/serverup/stacks",
|
|
"DATA_DIR": "/srv/serverup/appdata",
|
|
"BACKUP_DIR": "/srv/serverup/backups",
|
|
}
|
|
# Een afsluitende schuine streep hoort niet te verdubbelen.
|
|
assert core.afgeleide_paden("/srv/serverup/")["DATA_DIR"] == "/srv/serverup/appdata"
|
|
# Een relatief pad levert niets op in plaats van iets kapots.
|
|
assert core.afgeleide_paden("srv/data") == {}
|
|
assert core.afgeleide_paden("") == {}
|
|
|
|
|
|
def test_base_dir_stuurt_de_standaarden(monkeypatch, tmp_path):
|
|
"""De drie paden stonden hard op /opt/serverup/*, ook als je BASE_DIR
|
|
verzette — precies waarom de interface /opt bleef voorstellen."""
|
|
import importlib
|
|
monkeypatch.setenv("BASE_DIR", "/srv/eigen")
|
|
monkeypatch.setenv("SU_CONFIG", str(tmp_path / "c.json"))
|
|
for naam in [n for n in sys.modules if n == "core" or n.startswith("core.")]:
|
|
del sys.modules[naam]
|
|
core = importlib.import_module("core")
|
|
assert core.DEFAULTS["LIBRARY_DIR"] == "/srv/eigen/stacks"
|
|
assert core.DEFAULTS["DATA_DIR"] == "/srv/eigen/appdata"
|
|
assert core.DEFAULTS["BACKUP_DIR"] == "/srv/eigen/backups"
|
|
|
|
|
|
# ── Meten en verplaatsen ─────────────────────────────────────────────────────
|
|
|
|
def test_meet_telt_bestanden_en_bytes(env, tmp_path):
|
|
from core import paden
|
|
d = _vul(tmp_path / "bron", {"a.txt": "12345", "sub/b.txt": "678"})
|
|
aantal, bytes_ = paden.meet(d)
|
|
assert (aantal, bytes_) == (2, 8)
|
|
assert paden.meet(tmp_path / "bestaat-niet") == (0, 0)
|
|
|
|
|
|
def test_verhuizen_kopieert_en_ruimt_daarna_op(env, tmp_path):
|
|
from core import paden
|
|
bron = _vul(tmp_path / "oud", {"a.txt": "hallo", "sub/b.txt": "wereld"})
|
|
doel = tmp_path / "nieuw"
|
|
|
|
stappen = []
|
|
n = paden.verhuis(bron, doel, voortgang_fn=lambda g, t, l: stappen.append((g, t)))
|
|
|
|
assert n == 2
|
|
assert (doel / "a.txt").read_text() == "hallo"
|
|
assert (doel / "sub" / "b.txt").read_text() == "wereld"
|
|
assert not bron.exists(), "de bron hoort na een geslaagde verhuizing weg te zijn"
|
|
assert stappen and stappen[-1][0] == stappen[-1][1], "voortgang loopt niet vol"
|
|
|
|
|
|
def test_de_bron_blijft_staan_als_de_controle_faalt(env, tmp_path, monkeypatch):
|
|
"""Als er onderweg iets misgaat mag het origineel niet verdwijnen."""
|
|
from core import paden
|
|
bron = _vul(tmp_path / "oud", {"a.txt": "hallo"})
|
|
doel = tmp_path / "nieuw"
|
|
|
|
# Doe alsof er minder is aangekomen dan verstuurd.
|
|
echt = paden.meet
|
|
|
|
def nep(p):
|
|
return (0, 0) if Path(p) == doel.resolve() else echt(p)
|
|
|
|
monkeypatch.setattr(paden, "meet", nep)
|
|
with pytest.raises(paden.PadFout, match="Controle mislukt"):
|
|
paden.verhuis(bron, doel)
|
|
assert (bron / "a.txt").exists(), "de bron is weg terwijl de controle faalde"
|
|
|
|
|
|
def test_doel_binnen_de_bron_wordt_geweigerd(env, tmp_path):
|
|
from core import paden
|
|
bron = _vul(tmp_path / "oud", {"a.txt": "x"})
|
|
with pytest.raises(paden.PadFout, match="binnen"):
|
|
paden.verhuis(bron, bron / "erin")
|
|
|
|
|
|
def test_zelfde_map_doet_niets(env, tmp_path):
|
|
from core import paden
|
|
bron = _vul(tmp_path / "oud", {"a.txt": "x"})
|
|
assert paden.verhuis(bron, bron) == 0
|
|
assert (bron / "a.txt").exists()
|
|
|
|
|
|
def test_symlinks_blijven_symlinks(env, tmp_path):
|
|
"""Een link volgen zou de inhoud dupliceren of buiten de boom kunnen wijzen."""
|
|
from core import paden
|
|
bron = _vul(tmp_path / "oud", {"echt.txt": "inhoud"})
|
|
os.symlink("echt.txt", bron / "link.txt")
|
|
doel = tmp_path / "nieuw"
|
|
paden.verhuis(bron, doel)
|
|
assert (doel / "link.txt").is_symlink()
|
|
assert os.readlink(doel / "link.txt") == "echt.txt"
|
|
|
|
|
|
# ── De stacks meeverhuizen ───────────────────────────────────────────────────
|
|
|
|
def test_paden_in_stacks_worden_omgeschreven(env, tmp_path):
|
|
"""Alleen bestanden verplaatsen laat elke stack naar zijn oude appdata
|
|
wijzen — die dan leeg is."""
|
|
from core import paden
|
|
lib = tmp_path / "stacks"
|
|
stack = lib / "sonarr"
|
|
stack.mkdir(parents=True)
|
|
(stack / "docker-compose.yml").write_text(
|
|
"services:\n sonarr:\n volumes:\n"
|
|
" - /opt/serverup/appdata/sonarr:/config\n", encoding="utf-8")
|
|
(stack / ".env").write_text("PAD=/opt/serverup/appdata/sonarr\n", encoding="utf-8")
|
|
(stack / ".serverup.json").write_text(json.dumps(
|
|
{"source": "sonarr", "values": {"appdata_dir": "/opt/serverup/appdata"}}))
|
|
|
|
n = paden.schrijf_paden_om(lib, {"/opt/serverup/appdata": "/srv/serverup/appdata"})
|
|
assert n == 3
|
|
assert "/srv/serverup/appdata/sonarr:/config" in (stack / "docker-compose.yml").read_text()
|
|
assert "/srv/serverup/appdata/sonarr" in (stack / ".env").read_text()
|
|
meta = json.loads((stack / ".serverup.json").read_text())
|
|
assert meta["values"]["appdata_dir"] == "/srv/serverup/appdata"
|
|
|
|
|
|
def test_een_pad_dat_er_op_lijkt_blijft_met_rust(env, tmp_path):
|
|
"""`/opt/serverup` vervangen mag `/opt/serverup-oud` niet aanraken."""
|
|
from core import paden
|
|
lib = tmp_path / "stacks"
|
|
stack = lib / "app"
|
|
stack.mkdir(parents=True)
|
|
(stack / "docker-compose.yml").write_text(
|
|
"services:\n a:\n volumes:\n"
|
|
" - /opt/serverup-oud/data:/config\n"
|
|
" - /opt/serverup/data:/data\n", encoding="utf-8")
|
|
|
|
paden.schrijf_paden_om(lib, {"/opt/serverup": "/srv/serverup"})
|
|
tekst = (stack / "docker-compose.yml").read_text()
|
|
assert "/opt/serverup-oud/data:/config" in tekst, "de buurmap is meegenomen"
|
|
assert "/srv/serverup/data:/data" in tekst
|
|
|
|
|
|
# ── Bereikbaarheid ───────────────────────────────────────────────────────────
|
|
|
|
def test_relatief_pad_wordt_geweigerd(env):
|
|
from core import paden
|
|
uit = paden.bereikbaar("relatief/pad")
|
|
assert not uit["ok"] and uit["reden"] == "geen_absoluut_pad"
|
|
|
|
|
|
def test_onschrijfbaar_pad_wordt_gemeld(env, tmp_path, monkeypatch):
|
|
from core import paden
|
|
monkeypatch.setattr(Path, "mkdir",
|
|
lambda *a, **k: (_ for _ in ()).throw(OSError("alleen-lezen")))
|
|
uit = paden.bereikbaar(str(tmp_path / "kan-niet"))
|
|
assert not uit["ok"] and uit["reden"] == "niet_schrijfbaar"
|
|
assert "alleen-lezen" in uit["detail"]
|
|
|
|
|
|
# ── Voortgang ────────────────────────────────────────────────────────────────
|
|
|
|
def test_voortgang_komt_door_de_api(client, env):
|
|
"""Bij tientallen gigabytes zegt "bezig…" niets; de balk komt hiervandaan."""
|
|
from core import jobs
|
|
login(client)
|
|
jid, _q = jobs.create("test")
|
|
jobs.voortgang(jid, 256, 1024, "bestand.txt")
|
|
|
|
d = client.get(f"/api/jobs/{jid}").get_json()
|
|
assert d["progress"] == {"done": 256, "total": 1024,
|
|
"label": "bestand.txt", "pct": 25}
|
|
|
|
|
|
def test_voortgang_deelt_niet_door_nul(env):
|
|
from core import jobs
|
|
jid, _q = jobs.create("test")
|
|
jobs.voortgang(jid, 0, 0, "")
|
|
assert jobs.stream(jid)["progress"]["pct"] == 0
|
|
|
|
|
|
# ── Het formulier stelt de ingestelde map voor ───────────────────────────────
|
|
|
|
def test_installatieformulier_gebruikt_de_ingestelde_appdata(client, env, monkeypatch):
|
|
"""266 sjablonen noemen /opt/serverup/appdata. Wat je ook instelde, dáár
|
|
kwam je data terecht.
|
|
|
|
De testomgeving zet DATA_DIR als omgevingsvariabele, en die wint van het
|
|
configuratiebestand — dus die verzetten we hier, niet de config.
|
|
"""
|
|
from core import git
|
|
csrf = login(client)
|
|
monkeypatch.setenv("DATA_DIR", "/srv/eigen/appdata")
|
|
|
|
bron = git.cache_dir("server-up") / "apps" / "testapp"
|
|
(bron / "files").mkdir(parents=True)
|
|
(bron / "template.json").write_text(json.dumps({
|
|
"kind": "compose",
|
|
"metadata": {"name": "Test", "description": "d", "tags": ["t"]},
|
|
"variables": [{"title": "Algemeen", "items": [
|
|
{"name": "service_name", "type": "str", "title": "Naam",
|
|
"default": "testapp", "required": True},
|
|
{"name": "appdata_dir", "type": "str", "title": "Appdata-map",
|
|
"default": "/opt/serverup/appdata", "required": True},
|
|
]}],
|
|
}), encoding="utf-8")
|
|
(bron / "files" / "compose.yaml").write_text(
|
|
"services:\n << service_name >>:\n image: x\n"
|
|
" volumes:\n - << appdata_dir >>/<< service_name >>:/config\n",
|
|
encoding="utf-8")
|
|
|
|
d = client.post("/api/store/preview",
|
|
json={"stack": "testapp", "repo_id": "server-up"},
|
|
headers={"X-CSRF-Token": csrf}).get_json()
|
|
veld = next(f for f in d["fields"] if f["name"] == "appdata_dir")
|
|
assert veld["default"] == "/srv/eigen/appdata"
|