|
|
@@ -846,8 +846,8 @@ MAX_CLIENT_THUMBNAIL_EDGE = 2048
|
|
|
STORED_CLIENT_THUMBNAIL_EDGE = 512
|
|
|
|
|
|
|
|
|
-async def _backfill_external_stl_thumbnails(folder_ids: list[int]) -> None:
|
|
|
- """Generate STL thumbnails for an external folder tree in the background.
|
|
|
+async def _backfill_external_thumbnails(folder_ids: list[int]) -> None:
|
|
|
+ """Generate STL and PDF thumbnails for an external folder tree in the background.
|
|
|
|
|
|
Spawned via ``asyncio.create_task`` from ``scan_external_folder`` so the
|
|
|
HTTP request can return as soon as the filesystem walk + folder/file rows
|
|
|
@@ -855,7 +855,9 @@ async def _backfill_external_stl_thumbnails(folder_ids: list[int]) -> None:
|
|
|
the request open for many minutes (each file triggers a ``trimesh.load``
|
|
|
+ matplotlib render, ~1-5s each) and the FE modal times out before the
|
|
|
final ``db.commit()`` runs — causing the original symptom in #1299 where
|
|
|
- subdirectories never showed up because nothing got committed.
|
|
|
+ subdirectories never showed up because nothing got committed. PDFs are
|
|
|
+ faster (a PDFium page render) but a share holding hundreds of them would
|
|
|
+ still hold the request open, so they are rendered here too.
|
|
|
|
|
|
Opens its own session because the request session is closed by the time
|
|
|
this task starts running. Commits per-file so a worker restart mid-run
|
|
|
@@ -869,22 +871,30 @@ async def _backfill_external_stl_thumbnails(folder_ids: list[int]) -> None:
|
|
|
result = await db.execute(
|
|
|
LibraryFile.active().where(
|
|
|
LibraryFile.folder_id.in_(folder_ids),
|
|
|
- LibraryFile.file_type == "stl",
|
|
|
+ LibraryFile.file_type.in_(("stl", "pdf")),
|
|
|
LibraryFile.thumbnail_path.is_(None),
|
|
|
)
|
|
|
)
|
|
|
- stl_files = result.scalars().all()
|
|
|
- if not stl_files:
|
|
|
+ target_files = result.scalars().all()
|
|
|
+ if not target_files:
|
|
|
return
|
|
|
logger.info(
|
|
|
- "Backfilling STL thumbnails: %d file(s) across %d folder(s)",
|
|
|
- len(stl_files),
|
|
|
+ "Backfilling STL/PDF thumbnails: %d file(s) across %d folder(s)",
|
|
|
+ len(target_files),
|
|
|
len(folder_ids),
|
|
|
)
|
|
|
- for stl_file in stl_files:
|
|
|
- abs_path = to_absolute_path(stl_file.file_path)
|
|
|
+ for target_file in target_files:
|
|
|
+ abs_path = to_absolute_path(target_file.file_path)
|
|
|
if not abs_path or not abs_path.exists():
|
|
|
continue
|
|
|
+ if target_file.file_type == "pdf":
|
|
|
+ # generate_pdf_thumbnail never raises; an unreadable PDF
|
|
|
+ # returns None and keeps the browser-preview fallback.
|
|
|
+ thumb_path = await asyncio.to_thread(generate_pdf_thumbnail, abs_path, thumbnails_dir)
|
|
|
+ if thumb_path:
|
|
|
+ target_file.thumbnail_path = to_relative_path(Path(thumb_path))
|
|
|
+ await db.commit()
|
|
|
+ continue
|
|
|
# Pre-skip files too small to contain even a single triangle.
|
|
|
# Bulk-uploaded ZIPs of stub STLs would otherwise trigger one
|
|
|
# trimesh.load() call + one debug log line per stub.
|
|
|
@@ -899,7 +909,7 @@ async def _backfill_external_stl_thumbnails(folder_ids: list[int]) -> None:
|
|
|
logger.debug("STL thumbnail backfill skipped %s: %s", abs_path, exc)
|
|
|
continue
|
|
|
if thumb_path:
|
|
|
- stl_file.thumbnail_path = to_relative_path(Path(thumb_path))
|
|
|
+ target_file.thumbnail_path = to_relative_path(Path(thumb_path))
|
|
|
await db.commit()
|
|
|
|
|
|
|
|
|
@@ -1999,8 +2009,8 @@ async def scan_external_folder(
|
|
|
except Exception as e:
|
|
|
logger.debug("Failed to extract metadata from external 3mf %s: %s", filepath, e)
|
|
|
|
|
|
- # STL thumbnails are deferred to a background task spawned after
|
|
|
- # the scan's db.commit() — see _backfill_external_stl_thumbnails.
|
|
|
+ # STL and PDF thumbnails are deferred to a background task spawned
|
|
|
+ # after the scan's db.commit() — see _backfill_external_thumbnails.
|
|
|
# Doing them inline would block the HTTP request for minutes on a
|
|
|
# large NAS mount (#1299).
|
|
|
|
|
|
@@ -2020,14 +2030,6 @@ async def scan_external_folder(
|
|
|
if thumbnail_path_str:
|
|
|
thumbnail_path = to_relative_path(Path(thumbnail_path_str))
|
|
|
|
|
|
- # Render page one of a PDF so it has a thumbnail before anyone opens it
|
|
|
- if file_type == "pdf" and thumbnail_path is None:
|
|
|
- thumbnail_path_str = await asyncio.to_thread(
|
|
|
- generate_pdf_thumbnail, filepath, get_library_thumbnails_dir()
|
|
|
- )
|
|
|
- if thumbnail_path_str:
|
|
|
- thumbnail_path = to_relative_path(Path(thumbnail_path_str))
|
|
|
-
|
|
|
db_file = LibraryFile(
|
|
|
folder_id=target_folder_id,
|
|
|
is_external=True,
|
|
|
@@ -2106,17 +2108,17 @@ async def scan_external_folder(
|
|
|
|
|
|
await db.commit()
|
|
|
|
|
|
- # Spawn STL thumbnail backfill in the background — the scan endpoint
|
|
|
+ # Spawn STL/PDF thumbnail backfill in the background — the scan endpoint
|
|
|
# returns immediately so the FE modal closes and subdirectories are
|
|
|
# visible right away; thumbnails fill in over the following seconds /
|
|
|
- # minutes as the task processes each STL file. Survives FE refresh —
|
|
|
+ # minutes as the task processes each file. Survives FE refresh —
|
|
|
# the task lives in the FastAPI event loop, not the request scope.
|
|
|
# folder_cache.values() covers the root + every pre-existing subfolder
|
|
|
# + every subfolder created during this scan. all_folder_ids on its own
|
|
|
# would miss the newly-created ones (it's snapshotted before the walk).
|
|
|
spawn_background_task(
|
|
|
- _backfill_external_stl_thumbnails(list(set(folder_cache.values()))),
|
|
|
- name=f"stl-backfill-folder-{folder_id}",
|
|
|
+ _backfill_external_thumbnails(list(set(folder_cache.values()))),
|
|
|
+ name=f"thumbnail-backfill-folder-{folder_id}",
|
|
|
)
|
|
|
|
|
|
return {"status": "success", "added": added, "removed": removed}
|
|
|
@@ -2772,12 +2774,20 @@ async def extract_zip_file(
|
|
|
async def batch_generate_stl_thumbnails(
|
|
|
request: BatchThumbnailRequest,
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
- _: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_UPDATE_ALL)),
|
|
|
+ auth_result: tuple[User | None, bool] = Depends(
|
|
|
+ require_ownership_permission(
|
|
|
+ Permission.LIBRARY_UPDATE_ALL,
|
|
|
+ Permission.LIBRARY_UPDATE_OWN,
|
|
|
+ )
|
|
|
+ ),
|
|
|
):
|
|
|
"""Generate thumbnails for STL and PDF files in batch.
|
|
|
|
|
|
- Note: Requires library:update_all permission since this is a batch operation
|
|
|
- that may affect files owned by different users.
|
|
|
+ With library:update_all this covers every matching file; with only
|
|
|
+ library:update_own it is narrowed to the caller's own files, the same
|
|
|
+ rule as update_file. The File Manager offers the toolbar button and the
|
|
|
+ per-file "Generate Thumbnail" entry to update_own users, and both land
|
|
|
+ here.
|
|
|
|
|
|
PDFs are included so the ones added before server-side PDF thumbnails
|
|
|
existed can be backfilled without opening each preview. The route keeps
|
|
|
@@ -2794,6 +2804,10 @@ async def batch_generate_stl_thumbnails(
|
|
|
# Build query based on request
|
|
|
query = LibraryFile.active().where(LibraryFile.file_type.in_(("stl", "pdf")))
|
|
|
|
|
|
+ user, can_modify_all = auth_result
|
|
|
+ if not can_modify_all:
|
|
|
+ query = query.where(LibraryFile.created_by_id == user.id)
|
|
|
+
|
|
|
if request.file_ids:
|
|
|
# Specific files
|
|
|
query = query.where(LibraryFile.id.in_(request.file_ids))
|