182 lines
6.0 KiB
Python
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
|