angee.storage.models
Source models for the storage addon.
The file domain in six models: :class:Backend (credentialed backend instance), :class:Drive (addressable volume with its own key prefix), :class:Folder (tree node or per-user smart folder), :class:MimeType (reference taxonomy), :class:File (content-addressed row, deduplicated per drive, soft-deleted to Trash), and :class:FileAttachment (polymorphic edge from any model row to a file).
A File is created as a DRAFT targeting a backend key, then bytes arrive from some source and :meth:File.finalize verifies and publishes them. Two byte sources converge on that one finalize: a client upload (File.objects.draft reserves the row, :meth:File.issue_upload_token hands the client a one-shot URL, :meth:File.receive_bytes lands the bytes) and a server-side ingest of bytes already in hand (:meth:FileManager.ingest_bytes — what a sync fetching message attachments or contact photos uses). A presigned backend or adopting bytes already on the backend are further siblings on the same path. Renditions, virus scanning, extraction, and search belong to downstream addons that subscribe to storage.signals.file_finalized or attach rows through FileAttachment.
UploadState
class UploadState(models.TextChoices)File byte lifecycle.
DRAFT rows hold a reserved storage_path whose backend bytes may not exist yet; READY rows have verified bytes, so content_hash and size_bytes are authoritative; FAILED rows are retained for audit and user feedback after a rejected upload.
Module-scoped (not nested on :class:File) because :class:FileManager is defined before the model and the storage_prune command consumes it without loading a concrete model.
Backend
class Backend(SqidMixin, AuditMixin, AngeeModel)Credentialed storage backend instance.
One row names a :class:~angee.storage.backends.StorageBackend subclass by a key in ANGEE_STORAGE_BACKEND_CLASSES and carries its constructor config. Many drives can share one backend; credentials are written once, here, never on the drive.
Meta
class Meta()Django model options for storage backends.
__str__
def __str__() -> strReturn the operator-facing backend label.
resolved_config
def resolved_config() -> dict[str, Any]Return backend_config with {"env": "VAR"} placeholders expanded.
Credentials stay out of the database row; env references resolve against the process environment each time an instance is built.
storage
@property
def storage() -> StorageBackendReturn the resolved backend instance, cached per (row, config).
The cache key includes the raw config, so rotating credentials takes effect on the next lookup without a worker restart.
Drive
class Drive(SqidMixin, AuditMixin, AngeeModel)Addressable storage volume on top of a backend.
Object keys live under {prefix}/… inside the parent backend's namespace, so two drives can share one bucket as long as their prefixes differ. Drive rows are the unit of access control: per-row editor / viewer grants (see permissions.zed) scope every folder and file underneath.
Meta
class Meta()Django model options for drives.
__str__
def __str__() -> strReturn the drive display name.
storage
@property
def storage() -> StorageBackendReturn the resolved backend instance for this drive.
Backend rows are admin-gated infrastructure; the fetch runs elevated so any actor allowed to use the drive can perform storage IO without read access to the backend row itself.
object_key
def object_key(content_hash: str, filename: str) -> strReturn the deterministic backend key for one digest and filename.
{prefix}/{hash[:2]}/{hash[2:4]}/{hash}/{safe_filename} — the two-level digest sharding keeps any single directory small.
FolderManager
class FolderManager(AngeeManager)Manager owning gated folder creation.
create_in_drive
def create_in_drive(*,
drive_id: str,
name: str,
parent_id: str = "",
description: str = "") -> AnyCreate a real folder after checking write on its drive.
The drive-write check is the create gate — a per-row REBAC create cannot evaluate a not-yet-inserted row — so the insert rides per-instance sudo while the ambient actor stamps created_by. Mirrors :meth:FileManager.draft.
Folder
class Folder(SqidMixin, AuditMixin, AngeeModel)Tree node inside a drive, or a per-user smart folder.
A real folder has a drive and filesystem-style uniqueness on (drive, parent, name). A smart folder has no drive: it belongs to one owner, is flagged is_virtual, and surfaces files through a backing query — Trash lists File.objects.trashed() — so it stores no edges.
SmartKind
class SmartKind(models.TextChoices)Backing queries a smart folder can surface.
Meta
class Meta()Django model options for folders.
__str__
def __str__() -> strReturn the folder name.
clean
def clean() -> NoneReject drive-less real folders, cross-drive parents, and cycles.
MimeType
class MimeType(SqidMixin, AngeeModel)Reference row for one MIME type.
The master-tier taxonomy seed is the source of truth; rows are read-only at runtime and deliberately carry no REBAC type, like the resource ledger, so any caller may read the catalogue.
Meta
class Meta()Django model options for MIME types.
__str__
def __str__() -> strReturn the canonical MIME string.
FileQuerySet
class FileQuerySet(AngeeQuerySet["File"])REBAC-scoped reads for file rows.
live
def live() -> FileQuerySetReturn rows that are not soft-deleted.
trashed
def trashed() -> FileQuerySetReturn soft-deleted rows — the Trash smart folder's backing query.
stale_drafts
def stale_drafts(cutoff: datetime) -> FileQuerySetReturn DRAFT rows reserved before cutoff that never finalized.
expired_trash
def expired_trash(cutoff: datetime) -> FileQuerySetReturn rows whose trash stay lapsed before cutoff.
delete
def delete() -> tuple[int, dict[str, int]]Refuse bulk delete: it bypasses soft-trash and orphans backend bytes.
QuerySet.delete() issues SQL without calling :meth:File.delete, so callers must iterate and soft-delete, or :meth:File.purge to hard-delete.
FileManager
class FileManager(RebacManager.from_queryset(FileQuerySet))Manager owning File creation and the proxy-token lookup.
:meth:draft reserves the row; the byte intake and publish verbs live on the row (see the module docstring for the source model).
draft
def draft(*,
filename: str,
mime_type: str = "",
size_bytes: int = 0,
drive_id: str = "",
drive_slug: str = "",
folder_id: str = "",
content_hash: str = "") -> AnyReserve a DRAFT File targeting a backend key, or return the dedup hit.
Content-addressed get-or-create: when content_hash names bytes the drive already holds READY, that row is returned (restored if trashed) and nothing needs writing — callers branch on upload_state. The actor must hold write on the drive; that check is the create gate, since a per-row REBAC create cannot evaluate a not-yet-inserted row.
ingest_bytes
def ingest_bytes(content: bytes,
*,
filename: str,
owner_id: Any = None,
drive_id: str = "",
drive_slug: str = "",
folder_id: str = "") -> AnyIngest bytes already in hand into a drive and return the READY File.
The server-side sibling of the proxy upload: the bytes are trusted (a sync fetched them), so this writes them to the content-addressed key and finalizes in one call instead of minting an upload token. Idempotent — bytes the drive already holds READY return the existing row. owner_id stamps created_by (the file's owner relation); the whole verb runs elevated, so a sync needs no per-actor drive grant, and read access is scoped afterwards by the stamped owner.
for_upload_token
def for_upload_token(token: str) -> AnyReturn the DRAFT row a signed proxy upload token addresses.
Validates the signature, expiry, draft state, and unspent nonce — but does not consume it; :meth:File.receive_bytes does that atomically after its own actor check. The token's nonce is pinned on the returned instance so the consume step binds to this token, not merely to the row's current envelope.
for_download_token
def for_download_token(token: str) -> AnyReturn the READY file a signed proxy download token addresses.
The mirror of :meth:for_upload_token. The token is a capability: it is minted on the file's url field, which only resolves for an actor that already read the row, so the download view re-validates the signature and expiry alone (no second actor check). Trashed or unfinished rows have no servable bytes, so they are excluded here.
File
class File(SqidMixin, AuditMixin, AngeeModel)A stored asset, deduplicated per drive by content hash.
created_by (stamped by :class:~angee.base.mixins.AuditMixin) is the uploader and backs the owner relation in permissions.zed. delete() soft-trashes; :meth:purge is the real delete.
Meta
class Meta()Django model options for files.
__str__
def __str__() -> strReturn the display title or original filename.
clean
def clean() -> NoneReject placement in a smart folder or a folder outside this drive.
storage
@property
def storage() -> StorageBackendReturn the resolved backend for this row's drive.
One elevated query joins drive and backend; the instance itself comes from the per-(row, config) backend cache.
url
@property
def url() -> strReturn the backend download URL — presigned when the backend supports it.
The GraphQL url field serves the token proxy URL instead (see :meth:download_url); this is the raw backend address its fallback uses.
issue_download_token
def issue_download_token() -> strReturn a TTL-limited signed token authorizing a proxy download.
The mirror of :meth:issue_upload_token, minus the nonce — a download is idempotent (re-fetchable, range-requestable) within the token's life, so it is a reusable capability rather than one-shot. Expiry rides on the signature (DOWNLOAD_TOKEN_MAX_AGE).
download_url
def download_url(request: Any | None = None) -> strReturn the token-authenticated proxy download URL for this file.
The filename rides in the path (so the browser saves under it and the URL reads cleanly); the signed token identifies the row. Built absolute against request when one is given, otherwise root-relative.
open_stream
def open_stream() -> BinaryIOOpen this file's stored bytes for reading (the download view streams it).
issue_upload_token
def issue_upload_token() -> strReturn a one-shot signed token authorizing a proxy byte push.
The nonce persists in :attr:upload_envelope so the token can be consumed exactly once; expiry rides on the signature (UPLOAD_TOKEN_MAX_AGE).
receive_bytes
def receive_bytes(body: BinaryIO) -> NoneStream a proxied request body into this row's backend key.
One byte source for the upload flow: the actor must be the uploader (created_by) or hold write on the drive. The one-shot token is consumed atomically before the write, the body is capped at ANGEE_STORAGE_PROXY_UPLOAD_MAX_BYTES, and an overflow or backend error marks the row FAILED and removes the partial object.
finalize
def finalize(*,
expected_hash: str = "",
expected_size: int | None = None) -> FileVerify the bytes at this row's key and publish it READY.
Computes the SHA-256, size, and MIME from the actual stored bytes, dedups per drive, and flips to READY. expected_hash / expected_size are asserted against the computed values when a source supplies them (the upload path does); a mismatch fails the row and raises :class:exceptions.UploadConflict. Idempotent on an already READY row; a late READY duplicate restores the winner and conflicts.
delete
def delete(*args: Any, **kwargs: Any) -> tuple[int, dict[str, int]]Soft-delete into the Trash smart folder; backend bytes stay.
:meth:purge (or the storage_prune command after the trash TTL) does the real delete. The soft path persists through save(), so the library's delete gate would never fire — check it explicitly to keep the zed delete permission live.
restore
def restore() -> NoneReverse a previous soft-delete.
purge
def purge() -> NoneReally delete: remove the row, then the backend object.
Backend failures never block the row deletion. A failed backend delete is accepted as an orphaned object — rows are the source of truth and keys are content-addressed, so an orphan can only waste space, never serve stale content under a live row.
FileAttachment
class FileAttachment(SqidMixin, AuditMixin, AngeeModel)Polymorphic edge attaching one :class:File to any model row.
Consumers attach explicitly (create a row against the concrete model) or declare a GenericRelation("storage.FileAttachment") on the target for an ergonomic reverse accessor. Access control rides entirely on the file parent — see permissions.zed.
Meta
class Meta()Django model options for file attachments.
__str__
def __str__() -> strReturn the attachment label or a file-qualified fallback.
StorageRole
class StorageRole(AngeeModel)Table-less REBAC type anchor for the storage/role namespace.
The const-backed admin relation on storage/role (permissions.zed) needs a model carrying its rebac_resource_type to satisfy the rebac.E009 system check — the same anchor operator's connection uses. The row is never created or read; it exists only to register the type so a platform admin resolves as an effective storage-admin through the const.
Meta
class Meta()Django model options for the storage role anchor.