library.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. """Pydantic schemas for library (File Manager) functionality."""
  2. from datetime import datetime
  3. from pydantic import BaseModel, Field
  4. # ============ Folder Schemas ============
  5. class FolderCreate(BaseModel):
  6. """Schema for creating a new folder."""
  7. name: str = Field(..., min_length=1, max_length=255)
  8. parent_id: int | None = None
  9. project_id: int | None = None
  10. archive_id: int | None = None
  11. class ExternalFolderCreate(BaseModel):
  12. """Schema for linking an external folder."""
  13. name: str = Field(..., min_length=1, max_length=255)
  14. external_path: str = Field(..., min_length=1, max_length=500)
  15. readonly: bool = True
  16. show_hidden: bool = False
  17. parent_id: int | None = None
  18. class FolderUpdate(BaseModel):
  19. """Schema for updating a folder."""
  20. name: str | None = Field(None, min_length=1, max_length=255)
  21. parent_id: int | None = None
  22. project_id: int | None = None # 0 to unlink
  23. archive_id: int | None = None # 0 to unlink
  24. class FolderResponse(BaseModel):
  25. """Schema for folder response."""
  26. id: int
  27. name: str
  28. parent_id: int | None
  29. project_id: int | None = None
  30. archive_id: int | None = None
  31. project_name: str | None = None
  32. archive_name: str | None = None
  33. is_external: bool = False
  34. external_path: str | None = None
  35. external_readonly: bool = False
  36. external_show_hidden: bool = False
  37. file_count: int = 0 # Computed field
  38. # max(folder.updated_at, max(immediate-child file.updated_at)). Used by the
  39. # File Manager folder tree's "sort by recent activity" mode (#1770) so that
  40. # adding a file inside a folder bubbles it up — folder.updated_at alone only
  41. # tracks rename/move events. Recursion across subfolders is intentionally
  42. # left out to keep the route a single GROUP BY rather than a recursive CTE.
  43. latest_activity_at: datetime | None = None
  44. created_at: datetime
  45. updated_at: datetime
  46. class Config:
  47. from_attributes = True
  48. class FolderTreeItem(BaseModel):
  49. """Schema for folder tree item (includes children)."""
  50. id: int
  51. name: str
  52. parent_id: int | None
  53. project_id: int | None = None
  54. archive_id: int | None = None
  55. project_name: str | None = None
  56. archive_name: str | None = None
  57. is_external: bool = False
  58. external_path: str | None = None
  59. external_readonly: bool = False
  60. file_count: int = 0
  61. # See FolderResponse.latest_activity_at — #1770 folder sort source.
  62. latest_activity_at: datetime | None = None
  63. children: list["FolderTreeItem"] = []
  64. class Config:
  65. from_attributes = True
  66. # ============ File Schemas ============
  67. class FileCreate(BaseModel):
  68. """Schema for creating a file entry (internal use after upload)."""
  69. filename: str
  70. file_path: str
  71. file_type: str
  72. file_size: int
  73. file_hash: str | None = None
  74. thumbnail_path: str | None = None
  75. metadata: dict | None = None
  76. folder_id: int | None = None
  77. project_id: int | None = None
  78. class FileUpdate(BaseModel):
  79. """Schema for updating a file."""
  80. filename: str | None = Field(None, min_length=1, max_length=255)
  81. folder_id: int | None = None
  82. project_id: int | None = None
  83. notes: str | None = None
  84. class FileDuplicate(BaseModel):
  85. """Reference to a duplicate file."""
  86. id: int
  87. filename: str
  88. folder_id: int | None
  89. folder_name: str | None
  90. created_at: datetime
  91. class FileResponse(BaseModel):
  92. """Schema for file response."""
  93. id: int
  94. folder_id: int | None
  95. folder_name: str | None = None
  96. project_id: int | None
  97. project_name: str | None = None
  98. is_external: bool = False
  99. filename: str
  100. file_path: str
  101. file_type: str
  102. file_size: int
  103. file_hash: str | None
  104. thumbnail_path: str | None
  105. metadata: dict | None
  106. print_count: int
  107. last_printed_at: datetime | None
  108. notes: str | None
  109. # Duplicate detection
  110. duplicates: list[FileDuplicate] | None = None
  111. duplicate_count: int = 0
  112. # User tracking (Issue #206)
  113. created_by_id: int | None = None
  114. created_by_username: str | None = None
  115. created_at: datetime
  116. updated_at: datetime
  117. # Metadata fields
  118. print_name: str | None = None
  119. print_time_seconds: int | None = None
  120. filament_used_grams: float | None = None
  121. sliced_for_model: str | None = None
  122. class Config:
  123. from_attributes = True
  124. class FileListResponse(BaseModel):
  125. """Schema for file list item (lighter than full response)."""
  126. id: int
  127. folder_id: int | None
  128. is_external: bool = False
  129. filename: str
  130. file_type: str
  131. file_size: int
  132. thumbnail_path: str | None
  133. print_count: int
  134. duplicate_count: int = 0
  135. # User tracking (Issue #206)
  136. created_by_id: int | None = None
  137. created_by_username: str | None = None
  138. created_at: datetime
  139. # Key metadata fields for display
  140. print_name: str | None = None
  141. print_time_seconds: int | None = None
  142. filament_used_grams: float | None = None
  143. sliced_for_model: str | None = None
  144. class Config:
  145. from_attributes = True
  146. class FileMoveRequest(BaseModel):
  147. """Schema for moving files to a folder."""
  148. file_ids: list[int]
  149. folder_id: int | None = None # None = move to root
  150. class FilePrintRequest(BaseModel):
  151. """Schema for printing a file from the library.
  152. Note: printer_id is passed as a query parameter, not in the body.
  153. """
  154. # Print options (same as archive reprint)
  155. plate_id: int | None = None
  156. plate_name: str | None = None
  157. ams_mapping: list[int] | None = None
  158. bed_levelling: bool = True
  159. flow_cali: bool = False
  160. vibration_cali: bool = True
  161. layer_inspect: bool = False
  162. timelapse: bool = False
  163. use_ams: bool = True
  164. nozzle_offset_cali: bool = True # Dual-nozzle printers only — MQTT-gated (#1682)
  165. # Project to associate the resulting archive with
  166. project_id: int | None = None
  167. # When true, delete the LibraryFile row + disk file after the archive has
  168. # been created and the print has been dispatched. Used by the Printers-page
  169. # Direct-Print flow (click / drag-drop a file onto a printer card) so the
  170. # transient upload doesn't linger in File Manager. Cleanup is skipped on
  171. # external library files.
  172. cleanup_library_after_dispatch: bool = False
  173. class FileUploadResponse(BaseModel):
  174. """Schema for file upload response."""
  175. id: int
  176. filename: str
  177. file_type: str
  178. file_size: int
  179. thumbnail_path: str | None
  180. duplicate_of: int | None = None # ID of existing file with same hash
  181. metadata: dict | None = None
  182. # ============ Bulk Operations ============
  183. class BulkDeleteRequest(BaseModel):
  184. """Schema for bulk delete operations."""
  185. file_ids: list[int] = []
  186. folder_ids: list[int] = []
  187. class BulkDeleteResponse(BaseModel):
  188. """Schema for bulk delete response."""
  189. deleted_files: int
  190. deleted_folders: int
  191. # ============ Queue Operations ============
  192. class AddToQueueRequest(BaseModel):
  193. """Schema for adding library files to the print queue."""
  194. file_ids: list[int] = Field(..., min_length=1)
  195. class AddToQueueResult(BaseModel):
  196. """Result for a single file added to queue."""
  197. file_id: int
  198. filename: str
  199. queue_item_id: int
  200. class AddToQueueError(BaseModel):
  201. """Error for a file that couldn't be added to queue."""
  202. file_id: int
  203. filename: str
  204. error: str
  205. class AddToQueueResponse(BaseModel):
  206. """Schema for add-to-queue response."""
  207. added: list[AddToQueueResult]
  208. errors: list[AddToQueueError]
  209. # ============ ZIP Extraction ============
  210. class ZipExtractResult(BaseModel):
  211. """Result for a single file extracted from ZIP."""
  212. filename: str
  213. file_id: int
  214. folder_id: int | None = None
  215. class ZipExtractError(BaseModel):
  216. """Error for a file that couldn't be extracted."""
  217. filename: str
  218. error: str
  219. class ZipExtractResponse(BaseModel):
  220. """Schema for ZIP extraction response."""
  221. extracted: int
  222. folders_created: int
  223. files: list[ZipExtractResult]
  224. errors: list[ZipExtractError]
  225. # ============ STL Thumbnail Generation ============
  226. class BatchThumbnailRequest(BaseModel):
  227. """Schema for batch STL thumbnail generation request."""
  228. file_ids: list[int] | None = None
  229. folder_id: int | None = None
  230. all_missing: bool = False
  231. class BatchThumbnailResult(BaseModel):
  232. """Result for a single file thumbnail generation."""
  233. file_id: int
  234. filename: str
  235. success: bool
  236. error: str | None = None
  237. class BatchThumbnailResponse(BaseModel):
  238. """Schema for batch thumbnail generation response."""
  239. processed: int
  240. succeeded: int
  241. failed: int
  242. results: list[BatchThumbnailResult]