Files
grotti/grotti/library.py
T

182 lines
6.0 KiB
Python

"""Watch-Ordner, Upload und Vorschaubild."""
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
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": "In config.json watch_dir auf 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)
_LAST_WATCH_SYNC = now
return {"gelesen": len(seen), "hinweis": ""}
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
store.update_file(
conn,
row["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 {},
},
)
updated += 1
return updated
def safe_filename(name):
base = Path(name).name.replace("\\", "_").replace("/", "_").replace('"', "")
return base or "datei.gcode"
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 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 printer_file(row):
path = Path(row["stored_path"])
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):
path = Path(row["stored_path"])
if 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