"""Pliki baz: wybór, wgrywanie, archiwizacja i reguły przyjmowania (DAN-27).""" from __future__ import annotations from fastapi import File, Form, Request, UploadFile from fastapi.responses import HTMLResponse, RedirectResponse import base64 import httpx from app.features import Feature from app.podstawa import (app, templates, logic, features, files_state, _perms, _logic_error) # ---------------- Pliki baz (DAN-27) ---------------- # Trzy poziomy dostępu, opisane w features.ROUTES: # „files" — widzi listę i decyduje, z czego program korzysta, # „files_input" — dokłada wgrywanie i ARCHIWIZACJĘ (plik zostaje zamrożony # ze znacznikiem czasu, znika tylko z użytku), # administrator — kasowanie, przywracanie z archiwum i REGUŁY WALIDACJI. # # Kwarantanna (plik wgrany, ale odrzucony przez walidację) jest odsiewana W WARSTWIE # DANYCH przy `for_admin=False`. Nie filtrujemy jej tutaj ani w szablonie: gdyby # takie pliki dochodziły do przeglądarki, wystarczyłby podgląd źródła, żeby poznać # reguły — a te ma znać wyłącznie administrator. def _files_context(request: Request, error: str = "", done: str = "") -> dict: is_admin = features.ADMIN in _perms(request) try: data = logic.files_list(for_admin=is_admin) except httpx.HTTPError as e: return {"files": [], "rules": {}, "is_admin": is_admin, "error": _logic_error(e), "done": ""} return {"files": data.get("files") or [], "rules": data.get("rules") or {}, "is_admin": is_admin, "error": error, "done": done} @app.get("/files", response_class=HTMLResponse) def files_view(request: Request, error: str = "", done: str = ""): return templates.TemplateResponse(request, "files.html", _files_context(request, error, done)) def _files_redirect(error: str = "", done: str = "") -> RedirectResponse: from urllib.parse import urlencode q = urlencode({k: v for k, v in (("error", error), ("done", done)) if v}) return RedirectResponse(f"/files{'?' + q if q else ''}", status_code=303) def _who(request: Request) -> str: return getattr(request.state, "user", "") or "-" @app.post("/files/use") def files_use(request: Request, path: str = Form(...), use: str = Form("")): """Włącza albo odstawia bazę. Włączenie przechodzi przez bramkę walidacji w warstwie danych — odmowa wraca BEZ POWODU, bo powód zdradzałby reguły.""" want = files_state.ACTIVE if use.strip().lower() in {"1", "true", "on", "tak"} \ else files_state.READY try: logic.files_status(path, want, by=_who(request)) except httpx.HTTPStatusError as e: detail = _http_detail(e) if features.ADMIN not in _perms(request): detail = "Tego pliku nie da się teraz włączyć do użytku." return _files_redirect(error=detail) except httpx.HTTPError as e: return _files_redirect(error=_logic_error(e)) return _files_redirect(done="Zapisano." if want == files_state.READY else "Plik włączony do użytku.") @app.post("/files/upload") async def files_upload(request: Request, upload: UploadFile = File(...)): """Wgranie nowej bazy. Plik zostaje NIEZALEŻNIE od wyniku walidacji — nie tracimy niczego, co ktoś wgrał.""" raw = await upload.read() if not raw: return _files_redirect(error="Pusty plik.") try: out = logic.files_upload(upload.filename or "plik.xlsx", base64.b64encode(raw).decode("ascii"), by=_who(request)) except httpx.HTTPError as e: return _files_redirect(error=_logic_error(e)) # DOKŁADNIE ten sam komunikat niezależnie od wyniku sprawdzenia. Dwa różne # komunikaty były wyrocznią: wystarczyło wgrywać spreparowane pliki i czytać # odpowiedź, żeby odgadnąć reguły, które ma znać wyłącznie administrator. # Poprzednia treść mówiła przy tym wprost, że istnieje „administrator", który # musi plik „zatwierdzić" — czyli że konto wgrywającego jest ograniczone # i że plik przechodzi przez jakąś bramkę. return _files_redirect(done=f"Wgrano „{out.get('name')}”.") @app.post("/files/archive") def files_archive(request: Request, path: str = Form(...)): """Archiwizacja: plik ZOSTAJE, zamrożony, ze znacznikiem czasu — znika tylko z użytku. To najdalej idąca operacja dostępna osobie wgrywającej dane.""" try: logic.files_status(path, files_state.ARCHIVED, by=_who(request)) except httpx.HTTPError as e: return _files_redirect(error=_logic_error(e)) return _files_redirect(done=f"Zarchiwizowano „{path}”. Plik został zachowany.") @app.post("/files/restore") def files_restore(request: Request, path: str = Form(...)): try: logic.files_status(path, files_state.READY, by=_who(request)) except httpx.HTTPError as e: return _files_redirect(error=_logic_error(e)) return _files_redirect(done=f"Przywrócono „{path}” z archiwum.") @app.post("/files/delete") def files_delete(request: Request, path: str = Form(...)): try: logic.files_delete(path) except httpx.HTTPError as e: return _files_redirect(error=_logic_error(e)) return _files_redirect(done=f"Skasowano „{path}” bezpowrotnie.") @app.post("/files/rules") def files_rules(request: Request, extensions: str = Form(".xlsx"), max_size_mb: float = Form(50), min_rows: int = Form(1), required_columns: str = Form(""), reject_duplicate_content: str = Form("")): rules = { "extensions": [e.strip() for e in extensions.split(",") if e.strip()], "max_size_mb": max_size_mb, "min_rows": min_rows, "required_columns": [c.strip() for c in required_columns.split(",") if c.strip()], "reject_duplicate_content": reject_duplicate_content.strip().lower() in {"1", "true", "on", "tak"}, } try: logic.files_rules(rules) except httpx.HTTPError as e: return _files_redirect(error=_logic_error(e)) return _files_redirect(done="Zapisano reguły walidacji.") def _http_detail(e: httpx.HTTPStatusError) -> str: try: return str(e.response.json().get("detail") or e) except Exception: # noqa: BLE001 return str(e) features.zarejestruj( ekran=Feature("files", "Pliki", "Wybór plików, z których korzysta program.", "Ekrany", "/files", kolejnosc=70), # Kasowanie, przywracanie i REGUŁY PRZYJMOWANIA to wyłącznie administrator — # o istnieniu reguł nikt poza nim nie ma skąd wiedzieć (DAN-27). trasy={("GET", "/files"): "files", ("POST", "/files/use"): "files", ("POST", "/files/upload"): "files_input", ("POST", "/files/archive"): "files_input", ("POST", "/files/restore"): features.ADMIN, ("POST", "/files/delete"): features.ADMIN, ("POST", "/files/rules"): features.ADMIN}, zasoby={"s-files.css": "files"}, ) features.zarejestruj(rozszerzenie=Feature( "files_input", "Wgrywanie i archiwizacja plików", "Dodawanie nowych plików i wycofywanie ich z użytku (plik zostaje, zamrożony, " "ze znacznikiem czasu). Kasować może wyłącznie administrator.", "Rozszerzenia", kolejnosc=40))