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.

Backend

python
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

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 raw config, so rotating credentials takes effect on the next lookup without a worker restart.

Drive

python
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

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.

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.

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.

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.

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.

FileAttachment

python
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

python
class Meta()

Django model options for file attachments.

__str__

python
def __str__() -> str

Return the attachment label or a file-qualified fallback.

StorageRole

python
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

python
class Meta()

Django model options for the storage role anchor.

Released under the AGPL-3.0 License.