library.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. """Library models for file manager functionality."""
  2. from datetime import datetime
  3. from sqlalchemy import JSON, Boolean, DateTime, ForeignKey, Integer, Select, String, Text, func, select
  4. from sqlalchemy.orm import Mapped, mapped_column, relationship
  5. from backend.app.core.database import Base
  6. class LibraryFolder(Base):
  7. """Folder for organizing library files."""
  8. __tablename__ = "library_folders"
  9. id: Mapped[int] = mapped_column(primary_key=True)
  10. name: Mapped[str] = mapped_column(String(255))
  11. parent_id: Mapped[int | None] = mapped_column(ForeignKey("library_folders.id", ondelete="CASCADE"), nullable=True)
  12. # External folder flags (for folders that point to external paths)
  13. is_external: Mapped[bool] = mapped_column(Boolean, default=False)
  14. external_readonly: Mapped[bool] = mapped_column(Boolean, default=False)
  15. external_show_hidden: Mapped[bool] = mapped_column(Boolean, default=False)
  16. external_path: Mapped[str | None] = mapped_column(String(500), nullable=True)
  17. # Link to project or archive
  18. project_id: Mapped[int | None] = mapped_column(ForeignKey("projects.id", ondelete="SET NULL"), nullable=True)
  19. archive_id: Mapped[int | None] = mapped_column(ForeignKey("print_archives.id", ondelete="SET NULL"), nullable=True)
  20. # Timestamps
  21. created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
  22. updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
  23. # Relationships
  24. parent: Mapped["LibraryFolder | None"] = relationship(
  25. "LibraryFolder",
  26. back_populates="children",
  27. remote_side="LibraryFolder.id",
  28. foreign_keys="LibraryFolder.parent_id",
  29. )
  30. children: Mapped[list["LibraryFolder"]] = relationship(
  31. "LibraryFolder",
  32. back_populates="parent",
  33. foreign_keys="LibraryFolder.parent_id",
  34. cascade="all, delete-orphan",
  35. )
  36. files: Mapped[list["LibraryFile"]] = relationship(
  37. back_populates="folder",
  38. cascade="all, delete-orphan",
  39. )
  40. project: Mapped["Project | None"] = relationship()
  41. archive: Mapped["PrintArchive | None"] = relationship()
  42. class LibraryFile(Base):
  43. """File stored in the library."""
  44. __tablename__ = "library_files"
  45. id: Mapped[int] = mapped_column(primary_key=True)
  46. folder_id: Mapped[int | None] = mapped_column(ForeignKey("library_folders.id", ondelete="CASCADE"), nullable=True)
  47. project_id: Mapped[int | None] = mapped_column(ForeignKey("projects.id", ondelete="SET NULL"), nullable=True)
  48. # External file flag
  49. is_external: Mapped[bool] = mapped_column(Boolean, default=False)
  50. # File info
  51. filename: Mapped[str] = mapped_column(String(255)) # Original filename
  52. file_path: Mapped[str] = mapped_column(String(500)) # Storage path
  53. file_type: Mapped[str] = mapped_column(String(10)) # "3mf" or "gcode"
  54. file_size: Mapped[int] = mapped_column(Integer)
  55. file_hash: Mapped[str | None] = mapped_column(String(64)) # SHA256 for duplicate detection
  56. thumbnail_path: Mapped[str | None] = mapped_column(String(500))
  57. # Extracted metadata (from 3MF parser)
  58. file_metadata: Mapped[dict | None] = mapped_column(JSON)
  59. # Usage tracking
  60. print_count: Mapped[int] = mapped_column(Integer, default=0)
  61. last_printed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
  62. # User notes
  63. notes: Mapped[str | None] = mapped_column(Text, nullable=True)
  64. # Provenance — when the file was imported from an external source (e.g.
  65. # MakerWorld), ``source_type`` identifies the source and ``source_url`` is
  66. # the canonical public URL. Used for "already imported" detection and
  67. # "re-open on MakerWorld" affordances. Index on source_url so the
  68. # dedupe lookup is O(log N).
  69. source_type: Mapped[str | None] = mapped_column(String(32), nullable=True)
  70. source_url: Mapped[str | None] = mapped_column(String(512), nullable=True, index=True)
  71. # User tracking (Issue #206)
  72. created_by_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
  73. # Soft-delete / trash bin (Issue #1008). When non-null, the file is in the
  74. # trash and should not appear in normal listings. A background sweeper
  75. # hard-deletes rows whose deleted_at is older than the retention window.
  76. deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
  77. # Timestamps
  78. created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
  79. updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
  80. # Relationships
  81. folder: Mapped["LibraryFolder | None"] = relationship(back_populates="files")
  82. project: Mapped["Project | None"] = relationship()
  83. created_by: Mapped["User | None"] = relationship()
  84. @classmethod
  85. def active(cls) -> "Select[tuple[LibraryFile]]":
  86. """Select statement that excludes trashed (soft-deleted) files.
  87. Use this in place of ``select(LibraryFile)`` for any user-facing listing
  88. or lookup so trashed files don't leak into normal flows. Endpoints that
  89. specifically operate on trashed rows (trash list, restore, sweeper)
  90. must use ``select(LibraryFile)`` directly.
  91. """
  92. return select(cls).where(cls.deleted_at.is_(None))
  93. from backend.app.models.archive import PrintArchive # noqa: E402, F811
  94. from backend.app.models.project import Project # noqa: E402, F811
  95. from backend.app.models.user import User # noqa: E402, F811