Skip to main content

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}