Skip to main content

sofie_storage/
lib.rs

1mod content_type;
2mod disposition;
3mod key;
4#[cfg(feature = "local")]
5mod local;
6#[cfg(feature = "s3")]
7mod s3;
8#[cfg(feature = "variants")]
9mod variants;
10
11use std::pin::Pin;
12use std::sync::Arc;
13use std::time::Duration;
14
15pub use bytes::Bytes;
16use sofie_core::{Clock, Result, Timestamp};
17
18pub use content_type::{ContentType, Disposition};
19pub use disposition::content_disposition;
20pub use key::StorageKey;
21#[cfg(feature = "local")]
22pub use local::Local;
23#[cfg(feature = "s3")]
24pub use s3::{S3, S3Settings};
25#[cfg(feature = "variants")]
26pub use variants::{MAX_ORIGINAL_BYTES, MadeVariant, Resized, Resizer, Variant};
27
28/// Where sofie-http serves the links a store signs for the app itself to answer, before routing,
29/// so no app route can take it.
30pub const SERVED_PATH: &str = "/_sofie/files/";
31/// The most S3 signs for, held for every store so a caller's mistake makes no lasting link.
32const MAXIMUM_LINK_SECONDS: u64 = 7 * 24 * 60 * 60;
33
34pub type Pending<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + Send + 'a>>;
35
36pub trait Store: Send + Sync + 'static {
37    fn writer(&self, key: &StorageKey) -> Pending<'_, Box<dyn Writer>>;
38
39    fn reader(&self, key: &StorageKey) -> Pending<'_, Option<Box<dyn Reader>>>;
40
41    /// Deleting a key that holds nothing succeeds.
42    fn delete(&self, key: &StorageKey) -> Pending<'_, ()>;
43
44    fn exists(&self, key: &StorageKey) -> Pending<'_, bool>;
45
46    fn download_url(&self, link: &DownloadLink<'_>) -> Result<String>;
47
48    /// The file a link this store signed for the app itself to serve names, while it is valid.
49    fn served(&self, _path_and_query: &str, _now: Timestamp) -> Option<Served> {
50        None
51    }
52}
53
54/// Nothing is kept under its key until `finish` succeeds.
55pub trait Writer: Send {
56    fn write(&mut self, chunk: Bytes) -> Pending<'_, ()>;
57
58    fn finish(self: Box<Self>) -> Pending<'static, ()>;
59
60    fn abort(self: Box<Self>) -> Pending<'static, ()>;
61}
62
63pub trait Reader: Send {
64    /// `None` once the file is read.
65    fn chunk(&mut self) -> Pending<'_, Option<Bytes>>;
66
67    /// In bytes, when the store knows it before the content is read.
68    fn size(&self) -> Option<u64> {
69        None
70    }
71}
72
73pub struct DownloadLink<'a> {
74    pub key: &'a StorageKey,
75    pub content_type: &'a ContentType,
76    /// What the browser saves it as; without one it names the file from the link.
77    pub filename: Option<&'a str>,
78    pub now: Timestamp,
79    /// From a second to seven days: `Storage` refuses any other.
80    pub valid_for: Duration,
81}
82
83#[derive(Debug, PartialEq, Eq)]
84pub struct Served {
85    pub key: StorageKey,
86    pub content_type: ContentType,
87    pub filename: Option<String>,
88    /// Until the link expires: the content under a key never changes, so a browser may keep it.
89    pub fresh_for: Duration,
90}
91
92#[derive(Clone)]
93pub struct Storage {
94    store: Arc<dyn Store>,
95    clock: Clock,
96}
97
98impl Storage {
99    pub fn new(store: impl Store) -> Self {
100        Self {
101            store: Arc::new(store),
102            clock: Clock::system(),
103        }
104    }
105
106    #[must_use]
107    pub fn with_clock(self, clock: Clock) -> Self {
108        Self { clock, ..self }
109    }
110
111    pub async fn put(&self) -> Result<Put> {
112        let key = StorageKey::generate()?;
113        let writer = self.store.writer(&key).await?;
114        Ok(Put {
115            key,
116            writer,
117            size: 0,
118        })
119    }
120
121    pub async fn get(&self, key: &StorageKey) -> Result<Option<Content>> {
122        let reader = self.store.reader(key).await?;
123        Ok(reader.map(|reader| Content { reader }))
124    }
125
126    pub async fn delete(&self, key: &StorageKey) -> Result<()> {
127        self.store.delete(key).await
128    }
129
130    pub async fn exists(&self, key: &StorageKey) -> Result<bool> {
131        self.store.exists(key).await
132    }
133
134    /// Opens the file to whoever holds the link until it expires; served as `content_type` only
135    /// when that cannot run as a page.
136    pub fn download_url(
137        &self,
138        key: &StorageKey,
139        content_type: &ContentType,
140        valid_for: Duration,
141    ) -> Result<String> {
142        self.store.download_url(&DownloadLink {
143            key,
144            content_type,
145            filename: None,
146            now: self.clock.now().timestamp(),
147            valid_for: link_lifetime(valid_for)?,
148        })
149    }
150
151    /// As `download_url`, saved by the browser as `filename`, such as an upload's own name.
152    pub fn named_download_url(
153        &self,
154        key: &StorageKey,
155        content_type: &ContentType,
156        filename: &str,
157        valid_for: Duration,
158    ) -> Result<String> {
159        self.store.download_url(&DownloadLink {
160            key,
161            content_type,
162            filename: Some(filename),
163            now: self.clock.now().timestamp(),
164            valid_for: link_lifetime(valid_for)?,
165        })
166    }
167
168    #[doc(hidden)]
169    #[must_use]
170    pub fn served(&self, path_and_query: &str) -> Option<Served> {
171        self.store
172            .served(path_and_query, self.clock.now().timestamp())
173    }
174}
175
176pub struct Put {
177    key: StorageKey,
178    writer: Box<dyn Writer>,
179    size: u64,
180}
181
182impl Put {
183    #[must_use]
184    pub fn key(&self) -> &StorageKey {
185        &self.key
186    }
187
188    #[must_use]
189    pub fn size(&self) -> u64 {
190        self.size
191    }
192
193    pub async fn write(&mut self, chunk: Bytes) -> Result<()> {
194        self.size += u64::try_from(chunk.len()).unwrap_or(u64::MAX);
195        self.writer.write(chunk).await
196    }
197
198    pub async fn finish(self) -> Result<StorageKey> {
199        self.writer.finish().await?;
200        Ok(self.key)
201    }
202
203    pub async fn abort(self) -> Result<()> {
204        self.writer.abort().await
205    }
206}
207
208pub struct Content {
209    reader: Box<dyn Reader>,
210}
211
212impl Content {
213    pub async fn chunk(&mut self) -> Result<Option<Bytes>> {
214        self.reader.chunk().await
215    }
216
217    #[must_use]
218    pub fn size(&self) -> Option<u64> {
219        self.reader.size()
220    }
221}
222
223fn link_lifetime(valid_for: Duration) -> Result<Duration> {
224    let seconds = valid_for.as_secs();
225    if !(1..=MAXIMUM_LINK_SECONDS).contains(&seconds) {
226        return Err(sofie_core::Error::internal_message(format!(
227            "a download link lasts from a second to seven days, not {seconds} seconds"
228        )));
229    }
230    Ok(valid_for)
231}