doido_storage/service.rs
1//! The pluggable storage backend trait — the storage layer's analogue of
2//! [`doido_cache::CacheStore`].
3//!
4//! A [`Service`] moves opaque bytes under a string `key`. Metadata (filename,
5//! content type, checksum, byte size, attachments) lives in the database; the
6//! service only stores and retrieves the raw object. Backends that can mint their
7//! own access URLs (S3, Azure, R2) return them from [`Service::url`] /
8//! [`Service::presigned_put`]; disk and memory return `None` and are served
9//! through the app's own signed [`crate::serving`] routes.
10
11use crate::signing::Disposition;
12use doido_core::Result;
13use std::sync::Arc;
14use std::time::Duration;
15
16/// Options controlling a generated access URL.
17#[derive(Debug, Clone)]
18pub struct UrlOptions {
19 /// How long a signed/presigned URL stays valid.
20 pub expires_in: Duration,
21 /// `inline` (view in browser) or `attachment` (force download).
22 pub disposition: Disposition,
23 /// Suggested download filename (Content-Disposition).
24 pub filename: Option<String>,
25 /// Content type advertised for the object.
26 pub content_type: Option<String>,
27}
28
29impl Default for UrlOptions {
30 fn default() -> Self {
31 Self {
32 expires_in: Duration::from_secs(300),
33 disposition: Disposition::Inline,
34 filename: None,
35 content_type: None,
36 }
37 }
38}
39
40/// A storage backend: stores and retrieves objects by key.
41#[async_trait::async_trait]
42pub trait Service: Send + Sync {
43 /// Name of the configured service (e.g. `local`, `amazon`).
44 fn name(&self) -> &str;
45
46 /// Whether objects are publicly readable without signing.
47 fn public(&self) -> bool {
48 false
49 }
50
51 /// Store `data` under `key`, overwriting any existing object.
52 async fn upload(&self, key: &str, data: Vec<u8>, content_type: Option<&str>) -> Result<()>;
53
54 /// Read the full object at `key`.
55 async fn download(&self, key: &str) -> Result<Vec<u8>>;
56
57 /// Delete the object at `key`. Idempotent: a missing object is not an error.
58 async fn delete(&self, key: &str) -> Result<()>;
59
60 /// Whether an object exists at `key`.
61 async fn exists(&self, key: &str) -> Result<bool>;
62
63 /// Size in bytes of the object at `key`.
64 async fn size(&self, key: &str) -> Result<u64>;
65
66 /// A backend-native URL to GET the object, if the backend can mint one
67 /// (presigned for S3/Azure/R2, or a public URL). `None` means "no native
68 /// URL — serve it through the app" (disk, memory).
69 async fn url(&self, key: &str, opts: &UrlOptions) -> Result<Option<String>> {
70 let _ = (key, opts);
71 Ok(None)
72 }
73
74 /// A backend-native URL a client may PUT to for a direct upload, if
75 /// supported. `None` means "no native direct upload — use the app's disk
76 /// upload route".
77 async fn presigned_put(&self, key: &str, opts: &UrlOptions) -> Result<Option<String>> {
78 let _ = (key, opts);
79 Ok(None)
80 }
81}
82
83/// Lets a type-erased `Arc<dyn Service>` be used wherever a `Service` is expected,
84/// mirroring `doido_cache`'s `impl CacheStore for Arc<dyn CacheStore>`.
85#[async_trait::async_trait]
86impl Service for Arc<dyn Service> {
87 fn name(&self) -> &str {
88 (**self).name()
89 }
90 fn public(&self) -> bool {
91 (**self).public()
92 }
93 async fn upload(&self, key: &str, data: Vec<u8>, content_type: Option<&str>) -> Result<()> {
94 (**self).upload(key, data, content_type).await
95 }
96 async fn download(&self, key: &str) -> Result<Vec<u8>> {
97 (**self).download(key).await
98 }
99 async fn delete(&self, key: &str) -> Result<()> {
100 (**self).delete(key).await
101 }
102 async fn exists(&self, key: &str) -> Result<bool> {
103 (**self).exists(key).await
104 }
105 async fn size(&self, key: &str) -> Result<u64> {
106 (**self).size(key).await
107 }
108 async fn url(&self, key: &str, opts: &UrlOptions) -> Result<Option<String>> {
109 (**self).url(key, opts).await
110 }
111 async fn presigned_put(&self, key: &str, opts: &UrlOptions) -> Result<Option<String>> {
112 (**self).presigned_put(key, opts).await
113 }
114}