feat(anhaenge): Datenmodell und Ablage fuer Termin-Anhaenge
Neue Tabelle event_attachments samt Ablage-Modul. Dateien liegen unter DATA_DIR/attachments, geshardet nach einem servergenerierten Namen -- weder der hochgeladene Dateiname noch die Termin-UID beruehren je einen Pfad, da die UID bei CalDAV-Terminen aus einem fremden Client stammt. Der Dateityp wird an den Bytes bestimmt, nie am Content-Type des Clients. SVG und HTML sind bewusst nicht erlaubt: beide fuehren Skript im Origin der App aus, wo das Sitzungstoken liegt. Aufgeraeumt wird an allen fuenf Loeschpfaden (Termin, Kalender, Gruppe, Benutzer, CalDAV-DELETE), und zwar Zeilen vor dem Commit, Dateien erst danach -- ein Rollback darf keine Bytes verlieren. Der Sweep als Sicherheitsnetz ist implementiert, haengt aber noch an keinem Scheduler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6e981e7aea
commit
22a469d449
@@ -243,6 +243,47 @@ class LocalEvent(Base):
|
||||
|
||||
calendar = relationship("LocalCalendar", back_populates="events")
|
||||
creator = relationship("User")
|
||||
# Deleting the event drops its attachment rows; the FILES are removed by
|
||||
# attachments_store.purge_for_events() — the ORM cascade never touches disk.
|
||||
attachments = relationship(
|
||||
"EventAttachment", cascade="all, delete-orphan", passive_deletes=False
|
||||
)
|
||||
|
||||
|
||||
class EventAttachment(Base):
|
||||
"""A file attached to an event (PDF, image or text).
|
||||
|
||||
The event is addressed by ``(source, event_uid)`` so non-local sources
|
||||
(CalDAV, Google, Home Assistant) can be enabled later without a migration.
|
||||
Today only ``source="local"`` is ever written, and for those rows
|
||||
``event_id`` carries the FK that makes joins and the ORM cascade work.
|
||||
"""
|
||||
|
||||
__tablename__ = "event_attachments"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
source = Column(String(20), nullable=False, default="local")
|
||||
event_uid = Column(String(255), nullable=False, index=True)
|
||||
event_id = Column(Integer, ForeignKey("local_events.id"), nullable=True, index=True)
|
||||
|
||||
# The name the user uploaded — only ever used to label the download. It is
|
||||
# NEVER part of an on-disk path: for CalDAV-created events even the event
|
||||
# UID comes from an external client, so paths are built from stored_name.
|
||||
filename = Column(String(255), nullable=False)
|
||||
# Server-generated uuid4 hex plus extension, e.g. "a1b2...ef.pdf".
|
||||
stored_name = Column(String(80), nullable=False, unique=True)
|
||||
# The result of sniffing the bytes, never the client's Content-Type header.
|
||||
content_type = Column(String(100), nullable=False)
|
||||
size_bytes = Column(Integer, nullable=False)
|
||||
has_thumb = Column(Boolean, default=False)
|
||||
# Capability token for the ICS ATTACH URL, which external CalDAV clients
|
||||
# fetch without any credentials. Unguessable, and revoked only by deleting
|
||||
# the attachment.
|
||||
token = Column(String(64), nullable=False, unique=True, index=True)
|
||||
uploaded_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
uploader = relationship("User")
|
||||
|
||||
|
||||
class ICalSubscription(Base):
|
||||
|
||||
Reference in New Issue
Block a user