mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-01 11:47:46 +02:00
Two faults behind the same kind of print: one that arrives without a
retrievable 3MF, which on an H2S is any job started from the printer's
own internal library.
Such an archive has no file_path, and Path("").parent is Path("."), so
every site that derived the archive's folder from it landed on the data
directory itself. The finish-photo capture spotted that and wrote to
<archive_dir>/<id>/photos instead. Nothing else did. The photo was
written in one place and looked for in another: reads 404'd, deletes
dropped the name and left the file, and the notification attachment
never found the image. Hand-uploaded photos worked only because upload
and read agreed with each other rather than with the capture. Give the
question one owner in utils/archive_paths and have all four sites ask
it. Lookups check the old shared location too, so photos already
uploaded there stay reachable; uploads now go where captures go.
Separately, the remain%-delta fallback that stands in for a missing 3MF
can charge nothing for several reasons, and did so without a word. The
AMS reading is coarse and, on the reporter's printer, noisy: it rises
mid-print, swings five points over a job, sits at 100% through a
36-minute print on a fresh spool, and goes negative on a nearly empty
one -- which the start-of-print gate rejects, dropping the only slot
that was printing. Two of their prints went uncounted for two different
reasons and both read as "no spools updated", which is also what a print
with nothing to charge prints. Name the slot and the two readings in
each case, on the Spoolman path and on the internal-inventory path,
which has carried the same gates since #1119.
The Spoolman path also had no notion of which slots the print used, so a
spool swapped into an idle slot mid-print reads as consumption and is
billed to whoever that slot is assigned to -- the fault #1269 fixed for
the internal tracker, still open here, and likeliest on exactly the
prints this fallback serves, where nothing else narrows the field. Use
the same three pieces of evidence it does: the print's mapping, its
mid-print tray changes, and the tray it started on. The last needs
storing, because the internal tracker's row is deleted before this runs
and a screen-started print has no mapping to fall back on -- hence a new
nullable column, and no backfill, since a row from before it existed has
nothing to say. Where no evidence exists at all, every slot is still
considered.
Both paths also treated tray_now == 255 as naming a slot. It does not:
it is the field's initial value, the fallback for an unparseable
reading, and what it reports with nothing loaded. Mapped as a tray id it
becomes (255, 1), so as the only evidence it excluded every real slot
and charged nothing at all -- this issue's own bug, arriving by a new
route. On the internal path that is live today; on the Spoolman path it
would have shipped with the guard above. The external holder reports 254
when it is genuinely in use.
The arithmetic is untouched: at one percent per step this cannot resolve
a small print, and pretending otherwise would be worse than saying so.
80 lines
3.3 KiB
Python
80 lines
3.3 KiB
Python
"""Where an archive's files live on disk (#1820).
|
|
|
|
An archive normally owns a directory, derived from its ``file_path``:
|
|
``<base_dir>/<dirname of file_path>/``. An archive created without a 3MF has
|
|
``file_path == ""``, and ``Path("").parent`` is ``Path(".")`` -- so every site
|
|
that derived the directory that way silently resolved to ``base_dir`` itself,
|
|
and all such archives shared one pile.
|
|
|
|
The finish-photo capture path spotted that and used ``<archive_dir>/<id>/``
|
|
instead. Nothing else did, so a captured photo was written to one directory and
|
|
then looked for in another: the read 404'd, the delete removed the name and
|
|
left the file, and the notification attachment never found the image. Four
|
|
sites deriving the same directory four times is what let them drift, so they
|
|
now all ask here.
|
|
|
|
Photos written before this are still where they were put, which is why lookups
|
|
check both locations rather than only the current one.
|
|
|
|
Scope note: a *source 3MF* uploaded onto a no-3MF archive has its own layout,
|
|
``archive/no_source/<id>/``, chosen separately and stored in its own column.
|
|
This module does not model that -- do not reach for ``archive_dir`` to find one.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from backend.app.core.config import settings
|
|
from backend.app.utils.safe_path import PathTraversalError, safe_join_under
|
|
|
|
|
|
def archive_dir(archive: object) -> Path:
|
|
"""The directory belonging to *archive*.
|
|
|
|
Falls back to ``<archive_dir>/<id>`` for an archive with no 3MF, matching
|
|
what the finish-photo capture has always written.
|
|
"""
|
|
file_path = getattr(archive, "file_path", "") or ""
|
|
if file_path:
|
|
return settings.base_dir / Path(file_path).parent
|
|
return settings.archive_dir / str(archive.id) # SEC-PATH-OK: archive.id is an int primary key
|
|
|
|
|
|
def archive_photos_dir(archive: object) -> Path:
|
|
"""Where photos for *archive* are written."""
|
|
return archive_dir(archive) / "photos" # SEC-PATH-OK: constant subdirectory
|
|
|
|
|
|
def _legacy_shared_photos_dir(archive: object) -> Path | None:
|
|
"""Where a no-3MF archive's photos used to be read from, and uploaded to.
|
|
|
|
``<base_dir>/photos``, shared by every no-3MF archive at once. Only ever
|
|
consulted for an archive that has no ``file_path``; one with a real path
|
|
always resolved correctly and has no second location to check.
|
|
"""
|
|
if getattr(archive, "file_path", "") or "":
|
|
return None
|
|
return settings.base_dir / "photos" # SEC-PATH-OK: constant subdirectory
|
|
|
|
|
|
def find_archive_photo(archive: object, filename: str) -> Path | None:
|
|
"""Locate an existing photo, or None if it is in neither location.
|
|
|
|
*filename* must already have been checked for membership in
|
|
``archive.photos``; it is joined containment-checked regardless. A name
|
|
that fails that check is treated as not found rather than raised on --
|
|
one caller is a background notification task, where an HTTP error would
|
|
have nowhere to go.
|
|
"""
|
|
for directory in (archive_photos_dir(archive), _legacy_shared_photos_dir(archive)):
|
|
if directory is None:
|
|
continue
|
|
try:
|
|
candidate = safe_join_under(directory, filename, http=False)
|
|
except PathTraversalError:
|
|
return None
|
|
if candidate.exists():
|
|
return candidate
|
|
return None
|