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
28pub const SERVED_PATH: &str = "/_sofie/files/";
31const 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 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 fn served(&self, _path_and_query: &str, _now: Timestamp) -> Option<Served> {
50 None
51 }
52}
53
54pub 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 fn chunk(&mut self) -> Pending<'_, Option<Bytes>>;
66
67 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 pub filename: Option<&'a str>,
78 pub now: Timestamp,
79 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 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 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 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}