631 lines
21 KiB
Python
631 lines
21 KiB
Python
"""Watch-Ordner, Upload und Vorschaubild."""
|
|
|
|
import re
|
|
import shutil
|
|
import time
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
from grotti import store
|
|
from grotti.errors import GrottiError
|
|
from grotti.parse import embedded_png, inspect_file, safe_zip_name
|
|
|
|
_LAST_WATCH_SYNC = 0.0
|
|
_WATCH_MIN_INTERVAL_S = 20
|
|
_FOLDER_PART = re.compile(r"^[A-Za-z0-9._\- äöüÄÖÜß]+$")
|
|
_MAX_FOLDER_DEPTH = 6
|
|
|
|
|
|
def skip_path(path, data_dir):
|
|
if any(part.startswith(".") or part == "__pycache__" for part in path.parts):
|
|
return True
|
|
resolved = path.resolve()
|
|
data = data_dir.resolve()
|
|
return resolved == data or data in resolved.parents
|
|
|
|
|
|
def meta_for(path):
|
|
parsed = inspect_file(path)
|
|
stat = path.stat()
|
|
parsed.update({
|
|
"name": path.name,
|
|
"stored_path": str(path.resolve()),
|
|
"size_bytes": stat.st_size,
|
|
"mtime": stat.st_mtime,
|
|
})
|
|
return parsed
|
|
|
|
|
|
def sync_watch(conn, cfg, force=False):
|
|
global _LAST_WATCH_SYNC
|
|
folder = cfg["watch_dir"]
|
|
if folder is None:
|
|
return {"gelesen": 0, "hinweis": "Unter Einstellungen den Slice-Ordner setzen."}
|
|
if not folder.exists():
|
|
return {"gelesen": 0, "hinweis": "Der Watch-Ordner fehlt: %s" % folder}
|
|
now = time.monotonic()
|
|
if not force and _LAST_WATCH_SYNC and now - _LAST_WATCH_SYNC < _WATCH_MIN_INTERVAL_S:
|
|
return {"gelesen": 0, "hinweis": "", "skipped": True}
|
|
seen = []
|
|
for path in folder.rglob("*"):
|
|
if not path.is_file() or skip_path(path, cfg["data_dir"]):
|
|
continue
|
|
lower = path.name.lower()
|
|
if not (lower.endswith(".gcode") or lower.endswith(".3mf")):
|
|
continue
|
|
try:
|
|
store.upsert_scanned(conn, meta_for(path), force=force)
|
|
seen.append(str(path.resolve()))
|
|
except (OSError, zipfile.BadZipFile):
|
|
continue
|
|
store.forget_missing_watch(conn, seen)
|
|
link_watch_files_to_projects(conn, cfg)
|
|
_LAST_WATCH_SYNC = now
|
|
return {"gelesen": len(seen), "hinweis": ""}
|
|
|
|
|
|
def _apply_meta(conn, file_id, row, meta):
|
|
store.update_file(
|
|
conn,
|
|
file_id,
|
|
{
|
|
"printer_family": meta.get("printer_family") or row.get("printer_family"),
|
|
"filaments": meta.get("filaments") or [],
|
|
"weight_g": meta.get("weight_g") or 0,
|
|
"estimated_s": meta.get("estimated_s") or 0,
|
|
"thumb_entry": meta.get("thumb_entry") or "",
|
|
"kind": meta.get("kind") or row.get("kind"),
|
|
"plate": meta.get("plate") or "",
|
|
"slice_meta": meta.get("slice_meta") or {},
|
|
},
|
|
)
|
|
|
|
|
|
def refresh_one_file(conn, file_id, watch_dir=None):
|
|
"""Eine Datei neu parsen (paint_index / ACE). None wenn nicht gefunden."""
|
|
row = store.get_file_row(conn, file_id)
|
|
if not row:
|
|
return None
|
|
stored = row.get("stored_path") or ""
|
|
path = Path(stored) if stored else None
|
|
if path is not None and path.is_file():
|
|
try:
|
|
meta = meta_for(path)
|
|
_apply_meta(conn, file_id, row, meta)
|
|
row = store.get_file_row(conn, file_id) or row
|
|
except (OSError, zipfile.BadZipFile):
|
|
pass
|
|
return store.file_out(row, watch_dir=watch_dir)
|
|
|
|
|
|
def filaments_need_paint_index(filaments):
|
|
rows = filaments if isinstance(filaments, list) else []
|
|
if len(rows) < 2:
|
|
return False
|
|
for item in rows:
|
|
if not isinstance(item, dict) or "paint_index" not in item:
|
|
return True
|
|
return False
|
|
|
|
|
|
def refresh_file_meta(conn, force=False):
|
|
"""Metadaten bestehender Dateien neu lesen (z. B. paint_info → filaments)."""
|
|
if not force:
|
|
return 0
|
|
# list_files/file_out ohne stored_path — Rohzeilen nutzen.
|
|
updated = 0
|
|
for row in store.list_file_rows(conn):
|
|
stored = row.get("stored_path") or ""
|
|
if not stored:
|
|
continue
|
|
path = Path(stored)
|
|
if not path.is_file():
|
|
continue
|
|
try:
|
|
meta = meta_for(path)
|
|
except (OSError, zipfile.BadZipFile):
|
|
continue
|
|
_apply_meta(conn, row["id"], row, meta)
|
|
updated += 1
|
|
return updated
|
|
|
|
|
|
def safe_filename(name):
|
|
base = Path(name).name.replace("\\", "_").replace("/", "_").replace('"', "")
|
|
base = base.strip().strip(".")
|
|
if not base or base in (".", ".."):
|
|
return "datei.gcode"
|
|
return base
|
|
|
|
|
|
def unique_path(directory, filename):
|
|
filename = safe_filename(filename)
|
|
if filename.lower().endswith(".gcode.3mf"):
|
|
stem, suffix = filename[: -len(".gcode.3mf")], ".gcode.3mf"
|
|
else:
|
|
stem, suffix = Path(filename).stem, Path(filename).suffix
|
|
candidate = directory / filename
|
|
number = 2
|
|
while candidate.exists():
|
|
candidate = directory / ("%s-%s%s" % (stem, number, suffix))
|
|
number += 1
|
|
return candidate
|
|
|
|
|
|
def require_watch_dir(cfg):
|
|
folder = cfg.get("watch_dir")
|
|
if not folder:
|
|
raise GrottiError("Kein Slice-Ordner gesetzt — unter Einstellungen festlegen.", "watch")
|
|
root = Path(folder).resolve()
|
|
if not root.is_dir():
|
|
raise GrottiError("Der Slice-Ordner fehlt: %s" % root, "watch")
|
|
return root
|
|
|
|
|
|
def normalize_folder(rel):
|
|
"""Relativpfad unter watch_dir; leer = Wurzel. Kein .., begrenzte Tiefe."""
|
|
text = str(rel or "").strip().replace("\\", "/").strip("/")
|
|
if not text:
|
|
return ""
|
|
parts = []
|
|
for part in text.split("/"):
|
|
part = part.strip()
|
|
if not part or part in (".", ".."):
|
|
raise GrottiError("Ungültiger Ordnername.", "ordner")
|
|
if not _FOLDER_PART.match(part):
|
|
raise GrottiError(
|
|
"Ordnername nur Buchstaben, Ziffern, Leerzeichen, . _ -.",
|
|
"ordner",
|
|
)
|
|
parts.append(part)
|
|
if len(parts) > _MAX_FOLDER_DEPTH:
|
|
raise GrottiError("Ordner zu tief verschachtelt (max. %s)." % _MAX_FOLDER_DEPTH, "ordner")
|
|
return "/".join(parts)
|
|
|
|
|
|
def path_under_watch(watch_dir, rel_folder, name=None):
|
|
"""Absoluter Pfad unter watch_dir; wirft bei Escape."""
|
|
root = Path(watch_dir).resolve()
|
|
folder = normalize_folder(rel_folder)
|
|
target = root if not folder else (root / folder)
|
|
if name:
|
|
target = target / safe_filename(name)
|
|
resolved = target.resolve()
|
|
if resolved != root and root not in resolved.parents:
|
|
raise GrottiError("Pfad außerhalb des Slice-Ordners.", "ordner")
|
|
return resolved
|
|
|
|
|
|
def list_folders(cfg):
|
|
"""Unterordner unter watch_dir (auch leer), relativ, sortiert."""
|
|
try:
|
|
root = require_watch_dir(cfg)
|
|
except GrottiError:
|
|
return []
|
|
data_dir = cfg.get("data_dir")
|
|
found = set()
|
|
for path in root.rglob("*"):
|
|
if not path.is_dir():
|
|
continue
|
|
if data_dir is not None and skip_path(path, data_dir):
|
|
continue
|
|
try:
|
|
rel = path.resolve().relative_to(root).as_posix()
|
|
except ValueError:
|
|
continue
|
|
if rel and rel != ".":
|
|
found.add(rel)
|
|
return sorted(found, key=lambda s: s.lower())
|
|
|
|
|
|
def create_folder(cfg, rel):
|
|
root = require_watch_dir(cfg)
|
|
folder = normalize_folder(rel)
|
|
if not folder:
|
|
raise GrottiError("Ordnername fehlt.", "ordner")
|
|
target = path_under_watch(root, folder)
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
return folder
|
|
|
|
|
|
def delete_folder(cfg, rel):
|
|
root = require_watch_dir(cfg)
|
|
folder = normalize_folder(rel)
|
|
if not folder:
|
|
raise GrottiError("Die Wurzel lässt sich nicht löschen.", "ordner")
|
|
target = path_under_watch(root, folder)
|
|
if not target.is_dir():
|
|
raise GrottiError("Der Ordner fehlt.", "ordner", 404)
|
|
try:
|
|
next(target.iterdir())
|
|
raise GrottiError("Ordner ist nicht leer.", "ordner")
|
|
except StopIteration:
|
|
pass
|
|
target.rmdir()
|
|
return folder
|
|
|
|
|
|
def _link_file_to_project_folder(conn, file_id, stored_path, watch_dir):
|
|
"""Wenn Pfad unter einem Projekt-Ordner liegt → project_id setzen."""
|
|
folder = store.file_folder(stored_path, "watch", watch_dir)
|
|
if not folder:
|
|
return
|
|
for row in store.list_project_folders(conn):
|
|
base = (row.get("folder") or "").strip().replace("\\", "/").strip("/")
|
|
if not base:
|
|
continue
|
|
if folder == base or folder.startswith(base + "/"):
|
|
store.set_file_project(conn, file_id, row["id"])
|
|
return
|
|
|
|
|
|
def move_file(conn, cfg, file_id, folder):
|
|
"""Watch- oder Upload-Datei in Slice-Unterordner (oder Wurzel); Upload wird Watch."""
|
|
root = require_watch_dir(cfg)
|
|
folder = normalize_folder(folder)
|
|
row = store.get_file_row(conn, file_id)
|
|
if not row:
|
|
raise GrottiError("Die Datei fehlt.", "datei", 404)
|
|
source = row.get("source") or ""
|
|
if source not in ("watch", "upload"):
|
|
raise GrottiError("Diese Datei lässt sich nicht verschieben.", "datei")
|
|
src = Path(row.get("stored_path") or "")
|
|
if not src.is_file():
|
|
raise GrottiError("Die Datei liegt nicht mehr auf der Platte.", "datei", 404)
|
|
dest_dir = path_under_watch(root, folder)
|
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
dest = dest_dir / src.name
|
|
if dest.resolve() == src.resolve() and source == "watch":
|
|
return store.file_out(row, watch_dir=root)
|
|
if dest.exists() and dest.resolve() != src.resolve():
|
|
dest = unique_path(dest_dir, src.name)
|
|
try:
|
|
shutil.move(str(src), str(dest))
|
|
except OSError as exc:
|
|
raise GrottiError("Verschieben fehlgeschlagen: %s" % exc, "ordner") from exc
|
|
stat = dest.stat()
|
|
store.update_file_location(
|
|
conn,
|
|
file_id,
|
|
str(dest.resolve()),
|
|
name=dest.name,
|
|
size_bytes=stat.st_size,
|
|
mtime=stat.st_mtime,
|
|
source="watch",
|
|
)
|
|
_link_file_to_project_folder(conn, file_id, str(dest.resolve()), root)
|
|
row = store.get_file_row(conn, file_id)
|
|
return store.file_out(row, watch_dir=root)
|
|
|
|
|
|
def path_allowed_for_delete(stored_path, cfg):
|
|
"""Nur Dateien unter Slice-Ordner oder data/eingang."""
|
|
try:
|
|
resolved = Path(stored_path).resolve()
|
|
except (OSError, ValueError):
|
|
return False
|
|
roots = []
|
|
watch = cfg.get("watch_dir")
|
|
if watch:
|
|
roots.append(Path(watch).resolve())
|
|
data_dir = cfg.get("data_dir")
|
|
if data_dir:
|
|
roots.append((Path(data_dir) / "eingang").resolve())
|
|
for root in roots:
|
|
try:
|
|
if resolved == root or root in resolved.parents:
|
|
return True
|
|
except OSError:
|
|
continue
|
|
return False
|
|
|
|
|
|
def delete_library_file(conn, cfg, file_id):
|
|
"""Bibliotheksdatei löschen (Upload oder Watch) — DB + Datei auf der Platte."""
|
|
row = store.get_file_row(conn, file_id)
|
|
if not row:
|
|
raise GrottiError("Die Datei fehlt.", "datei", 404)
|
|
stored = row.get("stored_path") or ""
|
|
if stored and not path_allowed_for_delete(stored, cfg):
|
|
raise GrottiError("Löschen außerhalb von Slice-Ordner oder Eingang ist nicht erlaubt.", "datei")
|
|
store.delete_file(conn, file_id)
|
|
if stored:
|
|
Path(stored).unlink(missing_ok=True)
|
|
return row
|
|
|
|
|
|
def _ensure_print_suffix(new_name, old_name):
|
|
"""Endung .gcode / .3mf / .gcode.3mf behalten, wenn der Nutzer sie weglässt."""
|
|
name = safe_filename(new_name)
|
|
low = name.lower()
|
|
old_low = (old_name or "").lower()
|
|
if low.endswith(".gcode.3mf") or low.endswith(".3mf") or low.endswith(".gcode"):
|
|
return name
|
|
if old_low.endswith(".gcode.3mf"):
|
|
return name + ".gcode.3mf"
|
|
if old_low.endswith(".3mf"):
|
|
return name + ".3mf"
|
|
if old_low.endswith(".gcode"):
|
|
return name + ".gcode"
|
|
return name
|
|
|
|
|
|
def rename_library_file(conn, cfg, file_id, new_name):
|
|
"""Datei im selben Ordner umbenennen — Platte + DB."""
|
|
row = store.get_file_row(conn, file_id)
|
|
if not row:
|
|
raise GrottiError("Die Datei fehlt.", "datei", 404)
|
|
src = Path(row.get("stored_path") or "")
|
|
if not src.is_file():
|
|
raise GrottiError("Die Datei liegt nicht mehr auf der Platte.", "datei", 404)
|
|
if not path_allowed_for_delete(str(src), cfg):
|
|
raise GrottiError("Umbenennen außerhalb von Slice-Ordner oder Eingang ist nicht erlaubt.", "datei")
|
|
final = _ensure_print_suffix(new_name, row.get("name") or src.name)
|
|
if not final or final in (".", ".."):
|
|
raise GrottiError("Ungültiger Dateiname.", "datei")
|
|
dest = src.with_name(final)
|
|
if dest.resolve() == src.resolve():
|
|
return store.file_out(row, watch_dir=cfg.get("watch_dir"))
|
|
if dest.exists():
|
|
raise GrottiError("Unter diesem Namen liegt schon eine Datei.", "datei")
|
|
try:
|
|
src.rename(dest)
|
|
except OSError as exc:
|
|
raise GrottiError("Umbenennen fehlgeschlagen: %s" % exc, "datei") from exc
|
|
stat = dest.stat()
|
|
store.update_file_location(
|
|
conn,
|
|
file_id,
|
|
str(dest.resolve()),
|
|
name=dest.name,
|
|
size_bytes=stat.st_size,
|
|
mtime=stat.st_mtime,
|
|
)
|
|
row = store.get_file_row(conn, file_id)
|
|
return store.file_out(row, watch_dir=cfg.get("watch_dir"))
|
|
|
|
|
|
_PRINT_SUFFIXES = (".gcode", ".3mf", ".gcode.3mf")
|
|
|
|
|
|
def is_print_filename(name):
|
|
low = (name or "").lower()
|
|
return any(low.endswith(suf) for suf in _PRINT_SUFFIXES)
|
|
|
|
|
|
def project_slug(name):
|
|
"""Ordner-sichere Kurzform aus Projektnamen."""
|
|
text = re.sub(r"[^\w\- äöüÄÖÜß.]+", "-", (name or "").strip(), flags=re.UNICODE)
|
|
text = re.sub(r"\s+", "-", text)
|
|
text = re.sub(r"-{2,}", "-", text).strip("-.")
|
|
text = text[:60] or "projekt"
|
|
return normalize_folder(text)
|
|
|
|
|
|
def ensure_project_folder(cfg, folder_rel):
|
|
"""Projektordner unter watch_dir anlegen; gibt normalisierten Relativpfad zurück."""
|
|
root = require_watch_dir(cfg)
|
|
folder = normalize_folder(folder_rel)
|
|
if not folder:
|
|
raise GrottiError("Projektordner fehlt.", "ordner")
|
|
path_under_watch(root, folder).mkdir(parents=True, exist_ok=True)
|
|
return folder
|
|
|
|
|
|
def default_project_folder(name):
|
|
return "Projekte/" + project_slug(name)
|
|
|
|
|
|
def link_watch_files_to_projects(conn, cfg):
|
|
"""Watch-Dateien in Projektordnern dem passenden Projekt zuordnen."""
|
|
watch = cfg.get("watch_dir")
|
|
if not watch:
|
|
return 0
|
|
projects = store.list_project_folders(conn)
|
|
if not projects:
|
|
return 0
|
|
linked = 0
|
|
for row in store.list_file_rows(conn):
|
|
if (row.get("source") or "") != "watch":
|
|
continue
|
|
folder = store.file_folder(row.get("stored_path"), "watch", watch)
|
|
if not folder:
|
|
continue
|
|
for proj in projects:
|
|
base = (proj.get("folder") or "").strip().replace("\\", "/").strip("/")
|
|
if not base:
|
|
continue
|
|
if folder == base or folder.startswith(base + "/"):
|
|
if row.get("project_id") != proj["id"]:
|
|
store.set_file_project(conn, row["id"], proj["id"])
|
|
linked += 1
|
|
break
|
|
return linked
|
|
|
|
|
|
def list_project_assets(cfg, folder_rel):
|
|
"""Nicht-Druckdateien im Projektordner (flach + Unterordner)."""
|
|
root = require_watch_dir(cfg)
|
|
folder = normalize_folder(folder_rel)
|
|
if not folder:
|
|
return []
|
|
base = path_under_watch(root, folder)
|
|
if not base.is_dir():
|
|
return []
|
|
items = []
|
|
for path in sorted(base.rglob("*")):
|
|
if not path.is_file() or skip_path(path, cfg["data_dir"]):
|
|
continue
|
|
if is_print_filename(path.name):
|
|
continue
|
|
try:
|
|
rel = path.resolve().relative_to(base).as_posix()
|
|
except ValueError:
|
|
continue
|
|
stat = path.stat()
|
|
items.append({
|
|
"name": path.name,
|
|
"rel": rel,
|
|
"size_bytes": stat.st_size,
|
|
"mtime": stat.st_mtime,
|
|
})
|
|
return items
|
|
|
|
|
|
def resolve_project_asset(cfg, folder_rel, rel_name):
|
|
"""Sicheren Pfad zu einer Asset-Datei im Projektordner."""
|
|
root = require_watch_dir(cfg)
|
|
folder = normalize_folder(folder_rel)
|
|
if not folder:
|
|
raise GrottiError("Projektordner fehlt.", "ordner")
|
|
base = path_under_watch(root, folder)
|
|
rel = str(rel_name or "").replace("\\", "/").strip("/")
|
|
if not rel or ".." in rel.split("/"):
|
|
raise GrottiError("Ungültiger Dateiname.", "datei")
|
|
target = (base / rel).resolve()
|
|
if base not in target.parents and target != base:
|
|
raise GrottiError("Pfad außerhalb des Projektordners.", "ordner")
|
|
if not target.is_file():
|
|
raise GrottiError("Die Datei fehlt.", "datei", 404)
|
|
return target
|
|
|
|
|
|
def save_project_asset(cfg, folder_rel, filename, content):
|
|
root = require_watch_dir(cfg)
|
|
folder = normalize_folder(folder_rel)
|
|
if not folder:
|
|
raise GrottiError("Projektordner fehlt.", "ordner")
|
|
dest_dir = path_under_watch(root, folder)
|
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
path = unique_path(dest_dir, filename)
|
|
path.write_bytes(content)
|
|
return {
|
|
"name": path.name,
|
|
"rel": path.name,
|
|
"size_bytes": path.stat().st_size,
|
|
"mtime": path.stat().st_mtime,
|
|
}
|
|
|
|
|
|
def delete_project_asset(cfg, folder_rel, rel_name):
|
|
path = resolve_project_asset(cfg, folder_rel, rel_name)
|
|
path.unlink(missing_ok=True)
|
|
return True
|
|
|
|
|
|
def delete_project_disk_folder(cfg, folder_rel, force=False):
|
|
"""Projektordner auf der Platte entfernen (leer oder force inkl. Inhalt)."""
|
|
if not folder_rel:
|
|
return
|
|
try:
|
|
root = require_watch_dir(cfg)
|
|
except GrottiError:
|
|
return
|
|
folder = normalize_folder(folder_rel)
|
|
if not folder:
|
|
return
|
|
target = path_under_watch(root, folder)
|
|
if not target.is_dir():
|
|
return
|
|
if force:
|
|
shutil.rmtree(target, ignore_errors=True)
|
|
return
|
|
try:
|
|
next(target.iterdir())
|
|
raise GrottiError(
|
|
"Projektordner ist nicht leer — mit Bestätigung inkl. Inhalt löschen.",
|
|
"ordner",
|
|
)
|
|
except StopIteration:
|
|
target.rmdir()
|
|
|
|
|
|
def save_upload(conn, filename, content, cfg, project_id=None):
|
|
directory = cfg["data_dir"] / "eingang"
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
path = unique_path(directory, filename)
|
|
path.write_bytes(content)
|
|
meta = meta_for(path)
|
|
meta["project_id"] = project_id
|
|
return store.insert_upload(conn, meta)
|
|
|
|
|
|
def ensure_library_gcode(conn, cfg, host, filename):
|
|
"""Wenn die laufende Datei fehlt: einmal vom Drucker nach Grotti ziehen."""
|
|
base = (filename or "").replace("\\", "/").split("/")[-1]
|
|
if not base or not base.lower().endswith(".gcode"):
|
|
return None
|
|
known = store.file_by_name(conn, base)
|
|
if known:
|
|
path = Path(known.get("stored_path") or "")
|
|
if path.is_file():
|
|
return store.file_out(known)
|
|
from grotti import moonraker
|
|
|
|
pulled = moonraker.download_gcode(host, base)
|
|
if not pulled:
|
|
return None
|
|
name, content = pulled
|
|
return save_upload(conn, name, content, cfg)
|
|
|
|
|
|
def resolve_stored_path(conn, row, data_dir=None):
|
|
"""Gespeicherter Pfad, ggf. nach Umzug von data_dir unter eingang/<name> wiederfinden."""
|
|
stored = (row or {}).get("stored_path") or ""
|
|
path = Path(stored) if stored else None
|
|
if path and path.is_file():
|
|
return path
|
|
name = (row or {}).get("name") or (path.name if path else "")
|
|
if not name or data_dir is None:
|
|
return None
|
|
candidate = Path(data_dir) / "eingang" / name
|
|
if not candidate.is_file():
|
|
return None
|
|
resolved = str(candidate.resolve())
|
|
file_id = (row or {}).get("id")
|
|
if file_id and conn is not None:
|
|
conn.execute("UPDATE files SET stored_path = %s WHERE id = %s", (resolved, file_id))
|
|
row["stored_path"] = resolved
|
|
return candidate.resolve()
|
|
|
|
|
|
def printer_file(row, conn=None, data_dir=None):
|
|
path = resolve_stored_path(conn, row, data_dir) if (conn is not None or data_dir is not None) else Path(row["stored_path"])
|
|
if path is None:
|
|
path = Path(row.get("stored_path") or "")
|
|
if not path.is_file():
|
|
raise GrottiError("Die Datei liegt nicht mehr auf der Platte.", "datei", 404)
|
|
if row["kind"] == "gcode":
|
|
return path.name, path.read_bytes(), ""
|
|
if row["kind"] != "gcode.3mf":
|
|
raise GrottiError("Die Datei enthält keinen G-code. Zuerst slicen.", "ungesliced")
|
|
return path.name, path.read_bytes(), row["plate"] or ""
|
|
|
|
|
|
def preview_png(row, conn=None, data_dir=None):
|
|
path = resolve_stored_path(conn, row, data_dir) if (conn is not None or data_dir is not None) else Path(row["stored_path"])
|
|
if path is None or not path.is_file():
|
|
return None
|
|
entry = row["thumb_entry"]
|
|
if row["kind"] == "gcode":
|
|
return embedded_png(path.read_bytes()[:400_000].decode("utf-8", "replace"))
|
|
if not zipfile.is_zipfile(path):
|
|
return None
|
|
with zipfile.ZipFile(path) as archive:
|
|
names = []
|
|
if entry and entry != "embedded" and safe_zip_name(entry) and entry in archive.namelist():
|
|
names.append(entry)
|
|
names.extend(
|
|
name for name in archive.namelist()
|
|
if name.lower().endswith(".png") and safe_zip_name(name) and name not in names
|
|
)
|
|
for name in names:
|
|
data = archive.read(name)
|
|
if data.startswith(b"\x89PNG"):
|
|
return data
|
|
plate = row["plate"]
|
|
if plate and safe_zip_name(plate) and plate in archive.namelist():
|
|
with archive.open(plate) as handle:
|
|
return embedded_png(handle.read(400_000).decode("utf-8", "replace"))
|
|
return None
|