Skip to content

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 ​

python
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.

StorageMasterQuerySet ​

python
class StorageMasterQuerySet(ArchiveQuerySet[Any], AngeeQuerySet[Any])

AngeeQuerySet plus the archive read vocabulary for storage master rows.

Shared by the archivable admin/infra rows (:class:Backend, :class:Drive) so .archived() / .unarchived() compose over the REBAC row scope.

StorageMasterManager ​

Default manager for the archivable storage master rows (Backend, Drive).

Backend ​

python
class Backend(SqidMixin, AuditMixin, ArchiveMixin, 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 ​

python
class Meta()

Django model options for storage backends.

__str__ ​

python
def __str__() -> str

Return the operator-facing backend label.

resolved_config ​

python
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 ​

python
@property
def storage() -> StorageBackend

Return the resolved backend instance, cached per (row, config).

The cache key includes the resolved config, so rotating an env-backed credential takes effect on the next lookup without a worker restart.

Drive ​

python
class Drive(SqidMixin, AuditMixin, ArchiveMixin, 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 ​

python
class Meta()

Django model options for drives.

__str__ ​

python
def __str__() -> str

Return the drive display name.

storage ​

python
@property
def storage() -> StorageBackend

Return 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 ​

python
def object_key(content_hash: str, filename: str) -> str

Return 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 ​

python
class FolderManager(AngeeManager)

Manager owning gated folder creation.

create_in_drive ​

python
def create_in_drive(*,
                    drive_id: str,
                    name: str,
                    parent_id: str = "",
                    description: str = "") -> Any

Create 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.

ensure_path ​

python
def ensure_path(
        drive: Any,
        parts: Sequence[str],
        *,
        cache: dict[tuple[str, ...], Any | None] | None = None) -> Any | None

Return the real folder at parts, creating missing segments.

External indexers already own a drive and need idempotent tree materialization rather than the actor-gated GraphQL creation path. The database uniqueness constraints serialize concurrent attempts. A caller-owned prefix cache lets a tree walk resolve each directory once.

prune_missing ​

python
def prune_missing(drive: Any, present: Collection[Sequence[str]]) -> int

Delete real folders whose source directory vanished and hold no rows.

The folder mirror of :meth:FileManager.trash_missing_external: a directory removed from an external source must not linger in the tree. Only a truly empty leaf is removed — a folder that still parents a file (a live one, or a soft-trashed one kept for history) or a surviving subfolder is retained, so the reconcile never orphans a row and needs no recursion. The folders_with_files guard is load-bearing precisely because File.folder is SET_NULL (not PROTECT): deleting a folder that still parents a file would silently null the file's folder rather than being blocked, so the guard — not the FK constraint — is what keeps a live sync from detaching a row. Deepest paths are removed first, so the parent PROTECT constraint always holds. present is the set of drive-relative directory paths the current sync mirrored.

Folder ​

python
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 ​

python
class SmartKind(models.TextChoices)

Backing queries a smart folder can surface.

Meta ​

python
class Meta()

Django model options for folders.

__str__ ​

python
def __str__() -> str

Return the folder name.

clean ​

python
def clean() -> None

Reject drive-less real folders, cross-drive parents, and cycles.

save ​

python
def save(*args: Any, **kwargs: Any) -> None

Persist the folder after revalidating mutable tree facts.

MimeType ​

python
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 ​

python
class Meta()

Django model options for MIME types.

__str__ ​

python
def __str__() -> str

Return the canonical MIME string.

FileQuerySet ​

python
class FileQuerySet(AngeeQuerySet["File"])

REBAC-scoped reads for file rows.

live ​

python
def live() -> FileQuerySet

Return rows that are not soft-deleted.

trashed ​

python
def trashed() -> FileQuerySet

Return soft-deleted rows — the Trash smart folder's backing query.

stale_drafts ​

python
def stale_drafts(cutoff: datetime) -> FileQuerySet

Return DRAFT rows reserved before cutoff that never finalized.

expired_trash ​

python
def expired_trash(cutoff: datetime) -> FileQuerySet

Return rows whose trash stay lapsed before cutoff.

delete ​

python
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 ​

python
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 ​

python
def draft(*,
          filename: str,
          mime_type: str = "",
          size_bytes: int = 0,
          drive_id: str = "",
          drive_slug: str = "",
          folder_id: str = "",
          content_hash: str = "") -> Any

Reserve 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 ​

python
def ingest_bytes(content: bytes,
                 *,
                 filename: str,
                 owner_id: Any = None,
                 drive_id: str = "",
                 drive_slug: str = "",
                 folder_id: str = "") -> Any

Ingest 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.

ingest_stream ​

python
def ingest_stream(reader: BinaryIO,
                  *,
                  filename: str,
                  content_hash: str,
                  size_bytes: int,
                  metadata: dict[str, Any] | None = None,
                  owner_id: Any = None,
                  drive_id: str = "",
                  drive_slug: str = "",
                  folder_id: str = "") -> Any

Ingest a pre-hashed stream without materializing it in memory.

The caller hashes outside this write path. A READY dedup hit returns before the reader is consumed; otherwise the bytes stream through the selected Django storage backend and the normal finalize contract verifies the declared digest and size.

index_external ​

python
def index_external(*,
                   drive: Any,
                   storage_path: str,
                   filename: str,
                   content_hash: str,
                   size_bytes: int,
                   mime_type: str = "",
                   folder: Any | None = None,
                   metadata: dict[str, Any] | None = None,
                   mime_cache: dict[str, Any | None] | None = None,
                   existing_pk: Any = None,
                   owner_id: Any = None) -> Any

Upsert one externally-owned object by its drive-relative path.

External bytes are already present, so this path creates READY rows directly. Path is the identity; the drive's content-hash uniqueness remains authoritative and reports a second path with identical bytes as :class:~angee.storage.exceptions.ExternalDuplicate.

trash_missing_external ​

python
def trash_missing_external(drive: Any, present_paths: Collection[str]) -> int

Soft-trash live rows in drive whose external path vanished.

for_upload_token ​

python
def for_upload_token(token: str) -> Any

Return 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 ​

python
def for_download_token(token: str) -> Any

Return 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 ​

python
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 ​

python
class Meta()

Django model options for files.

__str__ ​

python
def __str__() -> str

Return the display title or original filename.

clean ​

python
def clean() -> None

Reject placement in a smart folder or a folder outside this drive.

storage ​

python
@property
def storage() -> StorageBackend

Return 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.

local_path ​

python
def local_path() -> Path | None

Return this file's real on-disk path when its backend exposes one.

Reference-mode mounts store rows at real filesystem paths (a FileSystemStorage rooted at the mount folder), which lets a mount extractor open the bytes in place; object-store backends have no local path and return None. This owns the backend probe so no caller reaches into the storage shape from outside.

url ​

python
@property
def url() -> str

Return 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 ​

python
def issue_download_token() -> str

Return 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 ​

python
def download_url(request: Any | None = None) -> str

Return 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 ​

python
def open_stream() -> BinaryIO

Open this file's stored bytes for reading (the download view streams it).

issue_upload_token ​

python
def issue_upload_token() -> str

Return 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 ​

python
def receive_bytes(body: BinaryIO) -> None

Stream 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 ​

python
def finalize(*,
             expected_hash: str = "",
             expected_size: int | None = None) -> File

Verify 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 ​

python
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 ​

python
def restore() -> None

Reverse a previous soft-delete.

purge ​

python
def purge() -> None

Really 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.

FileAttachmentManager ​

python
class FileAttachmentManager(AngeeManager)

Owns the polymorphic file edge — the canonical-target attach write.

Mirrors :meth:angee.tags.models.TagAssignmentManager.attach: the edge keys on the target's canonical record target (:func:angee.base.refs.canonical_record_target), so a record and each of its REBAC-typed MTI ancestors share one attachment set instead of splitting it. Only the get_or_create runs elevated — storage/file_attachment declares no create permission (rows enter through gated call sites that already resolved the file and record), and a pre-insert check has no row id to gate on; created_by still stamps from the ambient actor, which elevation preserves.

attach ​

python
def attach(file: Any, record: models.Model, *, label: str = "") -> Any

Attach file to record, idempotently per (file, canonical target) edge.

FileAttachment ​

python
class FileAttachment(SqidMixin, AuditMixin, RecordRefMixin, AngeeModel)

Polymorphic edge attaching one :class:File to any model row.

Consumers attach explicitly through :meth:FileAttachmentManager.attach (which keys the edge on the target's canonical record target) or declare a GenericRelation("storage.FileAttachment") on the target for an ergonomic reverse accessor. Declare that reverse relation on the same topmost REBAC-typed MTI ancestor the canonical write keys on (:func:angee.base.refs.canonical_record_target), so the delete collector filters at the write content type — the placement invariant in :mod:angee.base.refs. Access control rides entirely on the file parent — see permissions.zed.

Meta ​

python
class Meta()

Django model options for file attachments.

__str__ ​

python
def __str__() -> str

Return the attachment label or a file-qualified fallback.

StorageRole ​

Table-less REBAC type anchor for the storage/role namespace.

The const-backed admin relation on storage/role (permissions.zed) registers through this anchor's rebac_resource_type (rebac.E009); the row is never created or read. See :func:angee.base.models.role_anchor.

Released under the AGPL-3.0 License.