| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178 |
- """Store a virtual printer's upload in the File Manager.
- Every server mode keeps what the slicer sends in the File Manager: library
- mode right away, review mode once the upload is approved, queue mode for the
- queue items that print it. The archive only gets a file once it prints.
- """
- import asyncio
- import logging
- from pathlib import Path
- from sqlalchemy import exists, select
- from sqlalchemy.ext.asyncio import AsyncSession
- from backend.app.models.library import LibraryFile, LibraryFolder, LibraryTag
- from backend.app.models.print_queue import PrintQueueItem
- logger = logging.getLogger(__name__)
- SOURCE_TYPE = "virtual_printer"
- DEFAULT_FOLDER_NAME = "Virtual Printer"
- async def upload_folder(db: AsyncSession, folder_id: int | None) -> LibraryFolder:
- """The folder a virtual printer's uploads land in.
- Its configured folder, or the shared top-level "Virtual Printer" folder
- when it has none, the folder was deleted, or it is an external folder (an
- external mount refuses a second file of the same name, and slicer names
- like "Plate_1.gcode.3mf" repeat). Only a shared or ownerless folder of
- that name counts: a user's private one of the same name would take every
- such upload out of everyone else's sight (#3201). None yet: one is made.
- """
- if folder_id is not None:
- folder = await db.get(LibraryFolder, folder_id)
- if folder is not None and not folder.is_external:
- return folder
- logger.warning("Virtual printer folder %s is gone or external; using '%s'", folder_id, DEFAULT_FOLDER_NAME)
- shared = (
- await db.execute(
- select(LibraryFolder)
- .where(
- LibraryFolder.name == DEFAULT_FOLDER_NAME,
- LibraryFolder.parent_id.is_(None),
- LibraryFolder.is_external.is_(False),
- LibraryFolder.shared.is_(True) | LibraryFolder.created_by_id.is_(None),
- )
- .order_by(LibraryFolder.id)
- .limit(1)
- )
- ).scalar_one_or_none()
- if shared is not None:
- return shared
- folder = LibraryFolder(name=DEFAULT_FOLDER_NAME, parent_id=None, created_by_id=None, shared=True)
- db.add(folder)
- await db.flush()
- return folder
- async def _apply_tags(db: AsyncSession, library_file: LibraryFile, tags: str | None) -> None:
- """Attach the comma-separated ``tags`` (review mode's field), creating missing ones."""
- from backend.app.api.routes.library_tags import _name_key
- names = []
- for raw in (tags or "").split(","):
- name = raw.strip()[:64]
- if name and _name_key(name) not in {_name_key(n) for n in names}:
- names.append(name)
- if not names:
- return
- await db.refresh(library_file, ["tags"])
- have = {tag.name_key for tag in library_file.tags}
- for name in names:
- key = _name_key(name)
- if key in have:
- continue
- tag = (await db.execute(select(LibraryTag).where(LibraryTag.name_key == key))).scalar_one_or_none()
- if tag is None:
- tag = LibraryTag(name=name, name_key=key)
- db.add(tag)
- await db.flush()
- library_file.tags.append(tag)
- have.add(key)
- async def upload_settings(db: AsyncSession, vp_id: int) -> tuple[int | None, bool]:
- """``(library_folder_id, queue_remove_after_print)`` of a virtual printer.
- Read at each upload rather than held by the running instance, so changing
- them takes effect without restarting the virtual printer.
- """
- from backend.app.models.virtual_printer import VirtualPrinter
- row = (
- await db.execute(
- select(VirtualPrinter.library_folder_id, VirtualPrinter.queue_remove_after_print).where(
- VirtualPrinter.id == vp_id
- )
- )
- ).one_or_none()
- if row is None:
- return None, False
- return row[0], bool(row[1])
- async def store_upload(
- db: AsyncSession,
- *,
- source_path: Path,
- filename: str,
- folder_id: int | None,
- owner_id: int | None = None,
- notes: str | None = None,
- project_id: int | None = None,
- tags: str | None = None,
- reuse: bool = True,
- ) -> tuple[LibraryFile, bool]:
- """Put an upload into the File Manager; returns ``(file, reused)``.
- The same file sent again under the same name into the same folder is the
- file already there (``reused``), so sending a plate twice does not leave
- two copies; a queue gets a job for each send all the same. Only files a
- virtual printer stored count: a file someone put there by hand is never
- taken over. Notes, project and tags are applied to the file either way.
- A file a queue job removes once it prints ("Remove from File Manager once
- it prints") is never shared: such a job asks for a file of its own
- (``reuse=False``), and no other upload takes that one, or the job's print
- would remove a file someone else stored to keep. Commits.
- """
- from backend.app.api.routes.library import save_3mf_bytes_to_library
- from backend.app.services.archive import ArchiveService
- folder = await upload_folder(db, folder_id)
- file_hash = await asyncio.to_thread(ArchiveService.compute_file_hash, source_path)
- # The job that removes this file at dispatch: not a file to share.
- removed_on_print = exists().where(
- PrintQueueItem.library_file_id == LibraryFile.id,
- PrintQueueItem.cleanup_library_after_dispatch.is_(True),
- )
- existing = (
- None
- if not reuse
- else (
- await db.execute(
- LibraryFile.active()
- .where(
- LibraryFile.folder_id == folder.id,
- LibraryFile.filename == filename,
- LibraryFile.file_hash == file_hash,
- LibraryFile.source_type == SOURCE_TYPE,
- ~removed_on_print,
- )
- .order_by(LibraryFile.id)
- .limit(1)
- )
- ).scalar_one_or_none()
- )
- if existing is not None:
- library_file, reused = existing, True
- else:
- library_file, reused = await save_3mf_bytes_to_library(
- db,
- source_path=source_path,
- filename=filename,
- folder_id=folder.id,
- source_type=SOURCE_TYPE,
- owner_id=owner_id,
- # No source URL: duplicates are found by content above.
- dedupe_within=None,
- )
- if notes:
- library_file.notes = notes
- if project_id is not None:
- library_file.project_id = project_id
- await _apply_tags(db, library_file, tags)
- await db.commit()
- return library_file, reused
|