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
impl MediaManager
Sourcepub fn new(pool: PgPool, registry: StorageRegistry) -> Self
pub fn new(pool: PgPool, registry: StorageRegistry) -> Self
PG back-compat constructor.
Sourcepub fn new_pool(pool: impl Into<Pool>, registry: StorageRegistry) -> Self
pub fn new_pool(pool: impl Into<Pool>, registry: StorageRegistry) -> Self
Constructor that works on all three backends.
Sourcepub fn with_max_upload_bytes(self, bytes: u64) -> Self
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.
pub fn registry(&self) -> &StorageRegistry
Sourcepub fn pool_dyn(&self) -> &Pool
👎Deprecated since 0.60.1: use pool()
pub fn pool_dyn(&self) -> &Pool
use pool()
Same as Self::pool.
Sourcepub async fn save_bytes(&self, opts: SaveOpts) -> Result<Media, MediaError>
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.
Sourcepub async fn begin_upload(
&self,
intent: UploadIntent,
) -> Result<UploadTicket, MediaError>
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.
Sourcepub async fn finalize_upload(&self, media_id: i64) -> Result<Media, MediaError>
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).
Sourcepub async fn get(&self, id: i64) -> Result<Option<Media>, MediaError>
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.
Sourcepub async fn get_including_deleted(
&self,
id: i64,
) -> Result<Option<Media>, MediaError>
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.
Sourcepub fn url(&self, m: &Media) -> Option<String>
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.
Sourcepub async fn public_url(&self, id: i64) -> Result<Option<String>, MediaError>
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.
Sourcepub fn origin_url(&self, m: &Media) -> Option<String>
pub fn origin_url(&self, m: &Media) -> Option<String>
Bare backend URL (no CDN). For internal admin / debug.
Sourcepub async fn presigned_get(&self, m: &Media, ttl: Duration) -> Option<String>
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.
Sourcepub async fn load_bytes(&self, m: &Media) -> Result<Vec<u8>, MediaError>
pub async fn load_bytes(&self, m: &Media) -> Result<Vec<u8>, MediaError>
Read the file bytes server-side.
Sourcepub async fn delete(&self, m: &Media) -> Result<(), MediaError>
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.
Sourcepub async fn purge(&self, m: &Media) -> Result<(), MediaError>
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.
Sourcepub async fn purge_orphans(
&self,
older_than: Duration,
) -> Result<u64, MediaError>
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.
Sourcepub async fn purge_orphans_dry_run(
&self,
older_than: Duration,
) -> Result<Vec<Media>, MediaError>
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.
Sourcepub async fn purge_pending(
&self,
older_than: Duration,
) -> Result<u64, MediaError>
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.
Sourcepub async fn create_collection(
&self,
name: impl Into<String>,
slug: impl Into<String>,
parent: Option<i64>,
description: impl Into<String>,
) -> Result<MediaCollection, MediaError>
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.
Sourcepub async fn get_collection(
&self,
id: i64,
) -> Result<Option<MediaCollection>, MediaError>
pub async fn get_collection( &self, id: i64, ) -> Result<Option<MediaCollection>, MediaError>
Look up by id (excludes soft-deleted).
Sourcepub async fn get_collection_by_slug(
&self,
slug: &str,
) -> Result<Option<MediaCollection>, MediaError>
pub async fn get_collection_by_slug( &self, slug: &str, ) -> Result<Option<MediaCollection>, MediaError>
Look up by slug (excludes soft-deleted).
Sourcepub async fn list_collections(&self) -> Result<Vec<MediaCollection>, MediaError>
👎Deprecated: use list_collections_paged
pub async fn list_collections(&self) -> Result<Vec<MediaCollection>, MediaError>
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.
Sourcepub async fn list_collections_paged(
&self,
limit: i64,
offset: i64,
) -> Result<Vec<MediaCollection>, MediaError>
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.
Sourcepub async fn collection_path(&self, id: i64) -> Result<String, MediaError>
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.
Sourcepub async fn delete_collection(&self, id: i64) -> Result<(), MediaError>
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.
Sourcepub async fn move_to_collection(
&self,
media_id: i64,
collection_id: Option<i64>,
) -> Result<(), MediaError>
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”).
Sourcepub async fn list_in_collection(
&self,
collection_id: i64,
recursive: bool,
) -> Result<Vec<Media>, MediaError>
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.
Sourcepub async fn list_in_collection_paged(
&self,
collection_id: i64,
recursive: bool,
limit: i64,
offset: i64,
) -> Result<Vec<Media>, MediaError>
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.
Sourcepub async fn ensure_tag(&self, slug: &str) -> Result<MediaTag, MediaError>
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).
Sourcepub async fn get_tag_by_slug(
&self,
slug: &str,
) -> Result<Option<MediaTag>, MediaError>
pub async fn get_tag_by_slug( &self, slug: &str, ) -> Result<Option<MediaTag>, MediaError>
Look up a tag by slug.
Sourcepub async fn tag(&self, media_id: i64, slugs: &[&str]) -> Result<(), MediaError>
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.
Sourcepub async fn untag(&self, media_id: i64, slug: &str) -> Result<(), MediaError>
pub async fn untag(&self, media_id: i64, slug: &str) -> Result<(), MediaError>
Remove a single tag from a media row.
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.
List tags applied to a media row, alphabetically by slug.
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.
Sourcepub async fn list_with_tag(
&self,
slug: &str,
limit: i64,
offset: i64,
) -> Result<Vec<Media>, MediaError>
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.
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.
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§
Auto Trait Implementations§
impl !RefUnwindSafe for MediaManager
impl !UnwindSafe for MediaManager
impl Freeze for MediaManager
impl Send for MediaManager
impl Sync for MediaManager
impl Unpin for MediaManager
impl UnsafeUnpin for MediaManager
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<A, B, T> HttpServerConnExec<A, B> for Twhere
B: Body,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
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