Skip to main content

MediaManager

Struct MediaManager 

Source
pub struct MediaManager { /* private fields */ }
Expand description

Glue between the Media model and a StorageRegistry. Cheap to clone — internal state is Arc-shared.

Every query method dispatches per backend (PG / MySQL 8+ / SQLite) through the crate::sql::Pool enum, so PG-only idioms like ANY($1) or ON CONFLICT DO UPDATE are rewritten per dialect.

Implementations§

Source§

impl MediaManager

Source

pub fn new(pool: PgPool, registry: StorageRegistry) -> Self

PG back-compat constructor.

Source

pub fn new_pool(pool: impl Into<Pool>, registry: StorageRegistry) -> Self

Constructor that works on all three backends.

Source

pub fn with_max_upload_bytes(self, bytes: u64) -> Self

Refuse a Self::begin_upload that declares more than bytes. Default DEFAULT_MAX_UPLOAD_BYTES.

Source

pub fn registry(&self) -> &StorageRegistry

Source

pub fn pool(&self) -> &Pool

The pool, on any backend; .as_postgres() for the PG one (#2070).

Source

pub fn pool_dyn(&self) -> &Pool

👎Deprecated since 0.60.1:

use pool()

Same as Self::pool.

Source

pub async fn save_bytes(&self, opts: SaveOpts) -> Result<Media, MediaError>

Write opts.bytes to the storage backend, then insert a Media row in the Ready state. Returns the inserted row.

§Errors

UnknownDisk if the disk isn’t registered, Storage for any upload failure, Db for the row insert.

Source

pub async fn begin_upload( &self, intent: UploadIntent, ) -> Result<UploadTicket, MediaError>

Issue a presigned PUT URL for a direct browser upload, and create a Media row in Pending state. The browser PUTs straight to S3 (or compatible). The server then calls Self::finalize_upload to check the object landed and flip the row to Ready.

§Errors

UnknownDisk, Db, or Storage if the backend cannot sign URLs. Other for a negative size or one over the upload limit.

Source

pub async fn finalize_upload(&self, media_id: i64) -> Result<Media, MediaError>

Check the storage object for media_id and flip the row from Pending to Ready. It must have the size the row declared and, where the backend stores one, the signed type. Otherwise the row flips to Failed and a mismatched object is deleted. Returns the row either way; a row that is not Pending is returned untouched.

§Errors

Db if the row is missing or the update fails. Storage for transport failures, or a backend that cannot report crate::storage::ObjectMeta. Without s3:ListBucket, S3 answers a HEAD on a missing key with 403, so this fails closed (router: 502).

Source

pub async fn get(&self, id: i64) -> Result<Option<Media>, MediaError>

Fetch by id. Soft-deleted rows are excluded; pass through to Self::get_including_deleted when you want them.

Source

pub async fn get_including_deleted( &self, id: i64, ) -> Result<Option<Media>, MediaError>

Like Self::get but returns soft-deleted rows too. Use from admin / restore flows.

Source

pub fn url(&self, m: &Media) -> Option<String>

CDN-aware URL for m. Returns None when neither the disk’s CDN prefix nor the backend’s public URL is available.

Source

pub async fn public_url(&self, id: i64) -> Result<Option<String>, MediaError>

CDN-aware public URL for a media id, minting no signature.

This is the supported way to put an uploaded image on a public page. crate::media::router is the internal management API and refuses anonymous requests by design. A public page renders this string into its own template, from its own route.

Ok(None) has two causes: the row is missing or soft-deleted, or the disk has no CDN prefix and no base URL. Call Self::get first if you need to tell them apart.

The address is only as public as the bucket. This says where the object would be served from; it does not make it readable. On a private bucket the URL is correct and the fetch is a 403. Use Self::presigned_get there instead.

No signing means no await in a template, which matters because Tera filters are sync.

ⓘ
let ctx = tera::Context::from_serialize(serde_json::json!({
    "hero": manager.public_url(hero_id).await?,
}))?;
§Errors

Propagates the row lookup’s driver error.

Source

pub fn origin_url(&self, m: &Media) -> Option<String>

Bare backend URL (no CDN). For internal admin / debug.

Source

pub async fn presigned_get(&self, m: &Media, ttl: Duration) -> Option<String>

Time-limited download link suitable for <a href=...>. Returns None when the disk’s backend can’t sign.

Source

pub async fn load_bytes(&self, m: &Media) -> Result<Vec<u8>, MediaError>

Read the file bytes server-side.

Source

pub async fn delete(&self, m: &Media) -> Result<(), MediaError>

Soft-delete: set deleted_at. The storage object stays. Purge it later with Self::purge, or let the purge_orphans sweep take it.

Source

pub async fn purge(&self, m: &Media) -> Result<(), MediaError>

Hard-delete: remove the storage object, the row’s tag links, and the row. Usually called by the post_delete signal after delete() has soft-deleted.

This is also what revokes access. delete() only soft-deletes, so a presigned URL minted before it keeps working until its TTL expires. The storage object has to go for the credential to stop resolving.

§Errors

UnknownDisk if m.disk is not registered, Storage if the object could not be removed, Db for either delete. The row is left in place in every error case.

Source

pub async fn purge_orphans( &self, older_than: Duration, ) -> Result<u64, MediaError>

Hard-delete every soft-deleted Media row older than older_than, removing the storage object as we go. Returns the count of rows purged.

Run from the crate::scheduler (e.g. nightly) to keep orphan storage objects from accumulating.

Under tenancy, pass a tenant-scoped pool. rustango_media is per-tenant, so this sweeps only the tenant self.pool points at (on a registry pool in schema mode, only public). It also deletes storage objects, so fan it out with crate::tenancy::for_each_tenant, and run Self::purge_orphans_dry_run first if you are unsure what a pool points at.

§Errors

The first failure, after trying every row, so one unreachable object does not block the rest of the sweep. Each failure is logged at warn with its disk and key, and the run is summarised at error, because the purged count does not survive the Err.

Source

pub async fn purge_orphans_dry_run( &self, older_than: Duration, ) -> Result<Vec<Media>, MediaError>

What Self::purge_orphans would delete. Same query, no purge call, so no rows and no storage objects are removed.

Run it before wiring the real sweep in a tenancy app: the rows carry their disk and storage_key, so you can see whether the pool points where you think it does.

§Errors

Driver error reading rustango_media.

Source

pub async fn purge_pending( &self, older_than: Duration, ) -> Result<u64, MediaError>

Hard-delete Media rows still Pending, or Failed, after older_than, with their storage objects, up to PURGE_PENDING_BATCH per call.

Direct browser uploads leave Pending rows behind when the browser gives up before calling finalize_upload, and a PUT can still land under a Failed row. Run this from the crate::scheduler.

Returns the number of media rows deleted. A full batch means there is more work — call again, or let the next run take it.

Each row is deleted on id and the status it was read with, so one finalized in between is kept. Its object is deleted inside that transaction: on a storage error the row stays for the next sweep. Tag links of gone rows are reclaimed at the end.

§Errors

The first failure, after trying every row.

Source

pub async fn create_collection( &self, name: impl Into<String>, slug: impl Into<String>, parent: Option<i64>, description: impl Into<String>, ) -> Result<MediaCollection, MediaError>

Create a new collection. slug must be unique among live collections; a deleted one’s slug is reused. parent may be None (root) or another collection’s id (sub-folder).

§Errors

Db for unique-constraint violations on slug or any other underlying sqlx error. Other("parent collection N is missing or deleted") when parent is.

Source

pub async fn get_collection( &self, id: i64, ) -> Result<Option<MediaCollection>, MediaError>

Look up by id (excludes soft-deleted).

Source

pub async fn get_collection_by_slug( &self, slug: &str, ) -> Result<Option<MediaCollection>, MediaError>

Look up by slug (excludes soft-deleted).

Source

pub async fn list_collections(&self) -> Result<Vec<MediaCollection>, MediaError>

👎Deprecated:

use list_collections_paged

List every non-deleted collection, roots first, then by (parent_id, name) so siblings group together — handy for tree-renderers.

Unbounded: one row per collection.

Source

pub async fn list_collections_paged( &self, limit: i64, offset: i64, ) -> Result<Vec<MediaCollection>, MediaError>

One page of Self::list_collections; limit is clamped to 1..=MAX_LIST_LIMIT.

Source

pub async fn collection_path(&self, id: i64) -> Result<String, MediaError>

Build the slug-joined path for a collection: "products/2026/launch". Walks up the parent chain. Cycles raise Other.

Source

pub async fn delete_collection(&self, id: i64) -> Result<(), MediaError>

Soft-delete a collection and its descendants. Media inside them is not deleted, only orphaned (collection_id set to NULL), so the rows stay queryable and the storage objects survive.

The subtree goes too. Leaving it would leave children pointing at a parent that no longer resolves: still listed by Self::list_collections, and Self::collection_path on any of them a permanent error.

Source

pub async fn move_to_collection( &self, media_id: i64, collection_id: Option<i64>, ) -> Result<(), MediaError>

Move a Media into a collection (or None to set “loose”).

Source

pub async fn list_in_collection( &self, collection_id: i64, recursive: bool, ) -> Result<Vec<Media>, MediaError>

List media in collection_id. When recursive, descends into every nested collection.

Returns at most [DEFAULT_LIST_CAP] rows (100). Use Self::list_in_collection_paged to choose the page.

Source

pub async fn list_in_collection_paged( &self, collection_id: i64, recursive: bool, limit: i64, offset: i64, ) -> Result<Vec<Media>, MediaError>

Self::list_in_collection with an explicit page.

limit is clamped to 1..=MAX_LIST_LIMIT, matching Self::list_with_tag.

§Errors

Other (HTTP 400) for an offset above 10 000 when the subtree has more collections than the backend’s bind limit.

Source

pub async fn ensure_tag(&self, slug: &str) -> Result<MediaTag, MediaError>

Find or create a tag by slug (auto-derives name from slug if creating).

Source

pub async fn get_tag_by_slug( &self, slug: &str, ) -> Result<Option<MediaTag>, MediaError>

Look up a tag by slug.

Source

pub async fn tag(&self, media_id: i64, slugs: &[&str]) -> Result<(), MediaError>

Apply tags to a media row. Auto-creates missing tags. Idempotent — duplicates ignored.

§Errors

Other for more than 1000 distinct slugs; Db otherwise.

Source

pub async fn untag(&self, media_id: i64, slug: &str) -> Result<(), MediaError>

Remove a single tag from a media row.

Source

pub async fn set_tags( &self, media_id: i64, slugs: &[&str], ) -> Result<(), MediaError>

Replace the entire tag set for a media row. Tags not in slugs are removed; tags in slugs are added (auto-created if needed).

Atomic. The delete and the inserts are one transaction, so a failure part-way leaves the row’s tags as they were.

§Errors

Db for the delete, either insert, or the commit.

Source

pub async fn tags_for(&self, media_id: i64) -> Result<Vec<MediaTag>, MediaError>

List tags applied to a media row, alphabetically by slug.

Source

pub async fn tags_for_many( &self, media_ids: &[i64], ) -> Result<HashMap<i64, Vec<String>>, MediaError>

Tag slugs for many media rows, in one query.

Use this instead of calling Self::tags_for in a loop, which costs a round trip per row. Returns a map so a caller can drain it row by row; a media id with no tags is absent.

Source

pub async fn list_with_tag( &self, slug: &str, limit: i64, offset: i64, ) -> Result<Vec<Media>, MediaError>

List media that carry slug. Soft-deleted media excluded.

Source

pub async fn list_tags( &self, limit: i64, offset: i64, ) -> Result<Vec<MediaTag>, MediaError>

Tags by slug, one page. limit is clamped to 1..=MAX_LIST_LIMIT.

Reads only the tag table, so a page costs the same however many links exist — unlike Self::popular_tags.

Source

pub async fn popular_tags( &self, limit: i64, ) -> Result<Vec<(MediaTag, i64)>, MediaError>

Top tags by usage count, descending. Limit clamped to 1..=[MAX_LIST_LIMIT].

The count aggregates every link before the limit applies, so the cost grows with the link table, not the page.

Trait Implementations§

Source§

impl Clone for MediaManager

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
Source§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more