1use crate::attachments;
11use crate::blob::{self, Blob};
12use crate::config;
13use crate::service::{Service, UrlOptions};
14use crate::signing::{Disposition, Signer};
15use doido_core::Result;
16use doido_model::sea_orm::DatabaseConnection;
17use serde_json::Value;
18use std::sync::Arc;
19use std::time::Duration;
20
21const PURPOSE_BLOB: &str = "blob";
23pub(crate) const PURPOSE_DISK_UPLOAD: &str = "disk_upload";
25
26#[derive(Clone)]
28pub struct Storage {
29 conn: DatabaseConnection,
30 service: Arc<dyn Service>,
31 signer: Signer,
32 prefix: String,
33 expires_in: Duration,
34}
35
36impl Storage {
37 pub fn new(conn: DatabaseConnection, service: Arc<dyn Service>, signer: Signer) -> Self {
39 Self {
40 conn,
41 service,
42 signer,
43 prefix: "/doido/storage".to_string(),
44 expires_in: Duration::from_secs(300),
45 }
46 }
47
48 pub async fn from_config(conn: DatabaseConnection) -> Result<Self> {
51 let service = config::load().build().await?;
52 Ok(Self::new(conn, service, Signer::from_env()))
53 }
54
55 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
57 self.prefix = prefix.into();
58 self
59 }
60
61 pub fn with_expiry(mut self, expires_in: Duration) -> Self {
63 self.expires_in = expires_in;
64 self
65 }
66
67 pub fn conn(&self) -> &DatabaseConnection {
69 &self.conn
70 }
71
72 pub fn service(&self) -> &Arc<dyn Service> {
74 &self.service
75 }
76
77 pub fn prefix(&self) -> &str {
79 &self.prefix
80 }
81
82 pub(crate) fn signer(&self) -> &Signer {
83 &self.signer
84 }
85
86 pub(crate) fn expires_in(&self) -> Duration {
87 self.expires_in
88 }
89
90 pub async fn ensure_tables(&self) -> Result<()> {
92 crate::schema::ensure_tables(&self.conn).await
93 }
94
95 pub async fn create_and_upload(
100 &self,
101 filename: &str,
102 data: Vec<u8>,
103 metadata: Option<Value>,
104 ) -> Result<Blob> {
105 let blob = blob::build(filename, &data, self.service.name(), metadata);
106 self.service
107 .upload(&blob.key, data, blob.content_type.as_deref())
108 .await?;
109 blob::insert(&self.conn, &blob).await?;
110 Ok(blob)
111 }
112
113 pub async fn find_blob(&self, key: &str) -> Result<Option<Blob>> {
115 blob::find(&self.conn, key).await
116 }
117
118 pub async fn download(&self, key: &str) -> Result<Vec<u8>> {
120 self.service.download(key).await
121 }
122
123 pub async fn purge(&self, key: &str) -> Result<()> {
126 self.service.delete(key).await?;
127 attachments::delete_for_blob(&self.conn, key).await?;
128 blob::delete(&self.conn, key).await?;
129 Ok(())
130 }
131
132 pub async fn attach_upload(
137 &self,
138 record_type: &str,
139 record_id: &str,
140 name: &str,
141 filename: &str,
142 data: Vec<u8>,
143 ) -> Result<Blob> {
144 let blob = self.create_and_upload(filename, data, None).await?;
145 attachments::attach(&self.conn, name, record_type, record_id, &blob.key).await?;
146 Ok(blob)
147 }
148
149 pub async fn attach(
151 &self,
152 record_type: &str,
153 record_id: &str,
154 name: &str,
155 blob_key: &str,
156 ) -> Result<()> {
157 attachments::attach(&self.conn, name, record_type, record_id, blob_key).await
158 }
159
160 pub async fn one(
162 &self,
163 record_type: &str,
164 record_id: &str,
165 name: &str,
166 ) -> Result<Option<Blob>> {
167 attachments::one(&self.conn, name, record_type, record_id).await
168 }
169
170 pub async fn many(&self, record_type: &str, record_id: &str, name: &str) -> Result<Vec<Blob>> {
172 attachments::many(&self.conn, name, record_type, record_id).await
173 }
174
175 pub async fn detach(&self, record_type: &str, record_id: &str, name: &str) -> Result<()> {
177 attachments::detach(&self.conn, name, record_type, record_id).await
178 }
179
180 pub async fn purge_for_record(&self, record_type: &str, record_id: &str) -> Result<()> {
183 let keys = attachments::all_keys_for_record(&self.conn, record_type, record_id).await?;
184 for key in keys {
185 self.purge(&key).await?;
186 }
187 Ok(())
188 }
189
190 pub fn signed_id(&self, key: &str) -> String {
194 self.signer.sign(key, PURPOSE_BLOB, None)
195 }
196
197 pub fn verify_signed_id(&self, signed_id: &str) -> Result<String> {
199 self.signer.verify(signed_id, PURPOSE_BLOB)
200 }
201
202 pub fn redirect_path(&self, blob: &Blob) -> String {
206 format!(
207 "{}/blobs/redirect/{}/{}",
208 self.prefix,
209 self.signed_id(&blob.key),
210 encode_filename(&blob.filename)
211 )
212 }
213
214 pub fn proxy_path(&self, blob: &Blob) -> String {
216 format!(
217 "{}/blobs/proxy/{}/{}",
218 self.prefix,
219 self.signed_id(&blob.key),
220 encode_filename(&blob.filename)
221 )
222 }
223
224 pub async fn url_for(&self, blob: &Blob, disposition: Disposition) -> Result<String> {
227 let opts = UrlOptions {
228 expires_in: self.expires_in,
229 disposition,
230 filename: Some(blob.filename.clone()),
231 content_type: blob.content_type.clone(),
232 };
233 match self.service.url(&blob.key, &opts).await? {
234 Some(url) => Ok(url),
235 None => Ok(self.proxy_path(blob)),
236 }
237 }
238}
239
240fn encode_filename(filename: &str) -> String {
242 filename.replace(['/', '\\'], "_")
243}