Files
bambuddy/backend/app/schemas/library.py
T
maziggy 82278e8d93 Add external folder mounting for File Manager (#124)
Host directories (NAS, USB, network shares) can now be mounted
  into the File Manager without copying files. Files are indexed
  into the database on scan but read directly from their original
  location. Supports read-only mode, hidden file filtering, and
  automatic thumbnail extraction for 3MF/STL/gcode.

  - POST /library/folders/external — create with path validation
  - POST /library/folders/{id}/scan — discover/sync files
  - Block uploads, moves, and deletes for read-only external folders
  - Never delete actual files from external paths (DB-only removal)
  - Purple folder icon + info bar with rescan button in UI
  - i18n for all 7 languages
  - 19 backend + 11 frontend tests
2026-03-24 08:43:23 +01:00

328 lines
7.4 KiB
Python

"""Pydantic schemas for library (File Manager) functionality."""
from datetime import datetime
from pydantic import BaseModel, Field
# ============ Folder Schemas ============
class FolderCreate(BaseModel):
"""Schema for creating a new folder."""
name: str = Field(..., min_length=1, max_length=255)
parent_id: int | None = None
project_id: int | None = None
archive_id: int | None = None
class ExternalFolderCreate(BaseModel):
"""Schema for linking an external folder."""
name: str = Field(..., min_length=1, max_length=255)
external_path: str = Field(..., min_length=1, max_length=500)
readonly: bool = True
show_hidden: bool = False
parent_id: int | None = None
class FolderUpdate(BaseModel):
"""Schema for updating a folder."""
name: str | None = Field(None, min_length=1, max_length=255)
parent_id: int | None = None
project_id: int | None = None # 0 to unlink
archive_id: int | None = None # 0 to unlink
class FolderResponse(BaseModel):
"""Schema for folder response."""
id: int
name: str
parent_id: int | None
project_id: int | None = None
archive_id: int | None = None
project_name: str | None = None
archive_name: str | None = None
is_external: bool = False
external_path: str | None = None
external_readonly: bool = False
external_show_hidden: bool = False
file_count: int = 0 # Computed field
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class FolderTreeItem(BaseModel):
"""Schema for folder tree item (includes children)."""
id: int
name: str
parent_id: int | None
project_id: int | None = None
archive_id: int | None = None
project_name: str | None = None
archive_name: str | None = None
is_external: bool = False
external_path: str | None = None
external_readonly: bool = False
file_count: int = 0
children: list["FolderTreeItem"] = []
class Config:
from_attributes = True
# ============ File Schemas ============
class FileCreate(BaseModel):
"""Schema for creating a file entry (internal use after upload)."""
filename: str
file_path: str
file_type: str
file_size: int
file_hash: str | None = None
thumbnail_path: str | None = None
metadata: dict | None = None
folder_id: int | None = None
project_id: int | None = None
class FileUpdate(BaseModel):
"""Schema for updating a file."""
filename: str | None = Field(None, min_length=1, max_length=255)
folder_id: int | None = None
project_id: int | None = None
notes: str | None = None
class FileDuplicate(BaseModel):
"""Reference to a duplicate file."""
id: int
filename: str
folder_id: int | None
folder_name: str | None
created_at: datetime
class FileResponse(BaseModel):
"""Schema for file response."""
id: int
folder_id: int | None
folder_name: str | None = None
project_id: int | None
project_name: str | None = None
is_external: bool = False
filename: str
file_path: str
file_type: str
file_size: int
file_hash: str | None
thumbnail_path: str | None
metadata: dict | None
print_count: int
last_printed_at: datetime | None
notes: str | None
# Duplicate detection
duplicates: list[FileDuplicate] | None = None
duplicate_count: int = 0
# User tracking (Issue #206)
created_by_id: int | None = None
created_by_username: str | None = None
created_at: datetime
updated_at: datetime
# Metadata fields
print_name: str | None = None
print_time_seconds: int | None = None
filament_used_grams: float | None = None
sliced_for_model: str | None = None
class Config:
from_attributes = True
class FileListResponse(BaseModel):
"""Schema for file list item (lighter than full response)."""
id: int
folder_id: int | None
is_external: bool = False
filename: str
file_type: str
file_size: int
thumbnail_path: str | None
print_count: int
duplicate_count: int = 0
# User tracking (Issue #206)
created_by_id: int | None = None
created_by_username: str | None = None
created_at: datetime
# Key metadata fields for display
print_name: str | None = None
print_time_seconds: int | None = None
filament_used_grams: float | None = None
sliced_for_model: str | None = None
class Config:
from_attributes = True
class FileMoveRequest(BaseModel):
"""Schema for moving files to a folder."""
file_ids: list[int]
folder_id: int | None = None # None = move to root
class FilePrintRequest(BaseModel):
"""Schema for printing a file from the library.
Note: printer_id is passed as a query parameter, not in the body.
"""
# Print options (same as archive reprint)
plate_id: int | None = None
plate_name: str | None = None
ams_mapping: list[int] | None = None
bed_levelling: bool = True
flow_cali: bool = False
vibration_cali: bool = True
layer_inspect: bool = False
timelapse: bool = False
use_ams: bool = True
class FileUploadResponse(BaseModel):
"""Schema for file upload response."""
id: int
filename: str
file_type: str
file_size: int
thumbnail_path: str | None
duplicate_of: int | None = None # ID of existing file with same hash
metadata: dict | None = None
# ============ Bulk Operations ============
class BulkDeleteRequest(BaseModel):
"""Schema for bulk delete operations."""
file_ids: list[int] = []
folder_ids: list[int] = []
class BulkDeleteResponse(BaseModel):
"""Schema for bulk delete response."""
deleted_files: int
deleted_folders: int
# ============ Queue Operations ============
class AddToQueueRequest(BaseModel):
"""Schema for adding library files to the print queue."""
file_ids: list[int] = Field(..., min_length=1)
class AddToQueueResult(BaseModel):
"""Result for a single file added to queue."""
file_id: int
filename: str
queue_item_id: int
class AddToQueueError(BaseModel):
"""Error for a file that couldn't be added to queue."""
file_id: int
filename: str
error: str
class AddToQueueResponse(BaseModel):
"""Schema for add-to-queue response."""
added: list[AddToQueueResult]
errors: list[AddToQueueError]
# ============ ZIP Extraction ============
class ZipExtractResult(BaseModel):
"""Result for a single file extracted from ZIP."""
filename: str
file_id: int
folder_id: int | None = None
class ZipExtractError(BaseModel):
"""Error for a file that couldn't be extracted."""
filename: str
error: str
class ZipExtractResponse(BaseModel):
"""Schema for ZIP extraction response."""
extracted: int
folders_created: int
files: list[ZipExtractResult]
errors: list[ZipExtractError]
# ============ STL Thumbnail Generation ============
class BatchThumbnailRequest(BaseModel):
"""Schema for batch STL thumbnail generation request."""
file_ids: list[int] | None = None
folder_id: int | None = None
all_missing: bool = False
class BatchThumbnailResult(BaseModel):
"""Result for a single file thumbnail generation."""
file_id: int
filename: str
success: bool
error: str | None = None
class BatchThumbnailResponse(BaseModel):
"""Schema for batch thumbnail generation response."""
processed: int
succeeded: int
failed: int
results: list[BatchThumbnailResult]