1use anyhow::bail;
6use bytes::{BufMut, Bytes, BytesMut};
7use hashbrown::HashSet;
8use ordinary_config::{AssetsLimits, CompressionAlgorithm};
9use parking_lot::Mutex;
10use saferlmdb::{
11 self as lmdb, Database, DatabaseOptions, Environment, ReadTransaction, WriteTransaction, put,
12};
13use std::fmt::{Display, Formatter};
14use std::sync::Arc;
15use tracing::{Level, instrument};
16
17pub struct PercentageDisplay(pub f64);
18
19impl Display for PercentageDisplay {
20 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
21 write!(f, "{:.2}%", self.0)
22 }
23}
24
25pub struct AssetStore {
26 limits: AssetsLimits,
27
28 env: Arc<Environment>,
29
30 assets_db: Arc<Database<'static>>,
32
33 log_sizes: bool,
34
35 store_size: Arc<Mutex<u64>>,
36}
37
38impl AssetStore {
39 pub fn new(
40 limits: AssetsLimits,
41 env: &Arc<Environment>,
42 log_sizes: bool,
43 ) -> anyhow::Result<Self> {
44 let asset_db = Arc::new(Database::open(
45 env.clone(),
46 Some("asset"),
47 &DatabaseOptions::new(lmdb::db::Flags::CREATE),
48 )?);
49
50 let mut store_size = 0;
51
52 let txn = ReadTransaction::new(env.clone())?;
53 let access = txn.access();
54
55 let mut asset_cursor = txn.cursor(asset_db.clone())?;
56
57 if let Ok((key, val)) = asset_cursor.first::<[u8], [u8]>(&access) {
58 store_size += key.len() as u64;
59 store_size += val.len() as u64;
60
61 while let Ok((key, val)) = asset_cursor.next::<[u8], [u8]>(&access) {
62 store_size += key.len() as u64;
63 store_size += val.len() as u64;
64 }
65 }
66
67 Ok(Self {
68 limits,
69 env: env.clone(),
70 assets_db: asset_db,
71 log_sizes,
72 store_size: Arc::new(Mutex::new(store_size)),
74 })
75 }
76
77 #[allow(clippy::cast_precision_loss)]
78 #[instrument(skip_all, err)]
79 pub fn put(
80 &self,
81 path: &str,
82 asset: &[u8],
83 compression: Option<(&CompressionAlgorithm, usize)>,
84 ) -> anyhow::Result<()> {
85 let mut key = BytesMut::with_capacity(path.len() + 1);
86 key.put(path.as_bytes());
87
88 if let Some(compression) = compression {
89 key.put_u8(compression.0.as_u8());
90 }
91
92 let size = (key.len() + asset.len()) as u64;
93
94 if size > self.limits.max_asset_size {
95 bail!("exceeds asset size limit");
96 }
97
98 let mut store_size = self.store_size.lock();
99
100 if *store_size + size > self.limits.max_store_size {
101 bail!("exceeds store size limit");
102 }
103
104 let mut overwrite_size = 0;
105
106 let txn = WriteTransaction::new(self.env.clone())?;
107
108 {
109 let mut access = txn.access();
110 if let Ok(result) = access.get::<[u8], [u8]>(&self.assets_db, key.as_ref()) {
111 overwrite_size = (key.len() + result.len()) as u64;
112 }
113
114 access.put(&self.assets_db, key.as_ref(), asset, &put::Flags::empty())?;
115 }
116
117 txn.commit()?;
118
119 *store_size -= overwrite_size;
120 *store_size += size;
121
122 if self.log_sizes {
123 let (reduction, source_size) = if let Some((_, pre_compression_size)) = compression {
124 (
125 Some(tracing::field::display(PercentageDisplay(
126 ((pre_compression_size as f64 - size as f64) / pre_compression_size as f64)
127 * 100.0,
128 ))),
129 pre_compression_size as u64,
130 )
131 } else {
132 (None, size)
133 };
134
135 tracing::info!(
136 %path,
137 store.size = %bytesize::ByteSize(*store_size).display().si_short(),
138 asset.compression = %match compression {
139 Some(c) => c.0.as_str(),
140 None => "none"
141 },
142 asset.size.source = %bytesize::ByteSize(source_size).display().si_short(),
143 asset.size.compressed = compression.is_some().then_some(display(bytesize::ByteSize(size).display().si_short())),
144 asset.size.reduction = reduction,
145 );
146 } else {
147 tracing::info!(
148 %path,
149 compression = %match compression {
150 Some(c) => c.0.as_str(),
151 None => "none",
152 }
153 );
154 }
155
156 drop(store_size);
157
158 Ok(())
159 }
160
161 #[instrument(skip_all, err(level = Level::WARN))]
162 pub fn get(
163 &self,
164 path: &str,
165 compression: Option<&CompressionAlgorithm>,
166 ) -> anyhow::Result<Bytes> {
167 tracing::info!(
168 path,
169 compression = match compression {
170 Some(c) => c.as_str(),
171 None => "none",
172 }
173 );
174
175 let mut key = BytesMut::with_capacity(path.len() + 1);
176 key.put(path.as_bytes());
177
178 if let Some(compression) = compression {
179 key.put_u8(compression.as_u8());
180 }
181
182 let txn = ReadTransaction::new(self.env.clone())?;
183
184 let access = txn.access();
185 let result = access.get(&self.assets_db, key.as_ref())?;
186
187 Ok(Bytes::copy_from_slice(result))
188 }
189
190 #[instrument(skip_all, err)]
191 pub fn delete(
192 &self,
193 path: &str,
194 compression: Option<&CompressionAlgorithm>,
195 ) -> anyhow::Result<()> {
196 tracing::info!(
197 path,
198 compression = match compression {
199 Some(c) => c.as_str(),
200 None => "none",
201 }
202 );
203
204 let mut key = BytesMut::with_capacity(path.len() + 1);
205 key.put(path.as_bytes());
206
207 if let Some(compression) = compression {
208 key.put_u8(compression.as_u8());
209 }
210
211 let txn = WriteTransaction::new(self.env.clone())?;
212
213 {
214 let mut access = txn.access();
215 let result = access.get::<[u8], [u8]>(&self.assets_db, key.as_ref())?;
216
217 let mut store_size = self.store_size.lock();
218 *store_size -= (result.len() + key.len()) as u64;
219
220 drop(store_size);
221
222 access.del_key(&self.assets_db, key.as_ref())?;
223 }
224
225 txn.commit()?;
226
227 Ok(())
228 }
229
230 #[instrument(skip_all, err)]
231 pub fn delete_all(&self, skip_list: &Vec<String>) -> anyhow::Result<()> {
232 #[cfg(tracing_unstable)]
233 tracing::info!(skip = tracing::field::valuable(&skip_list));
234 #[cfg(not(tracing_unstable))]
235 tracing::info!(skip = tracing::field::debug(&skip_list));
236
237 let mut skip_set = HashSet::new();
238
239 for skip in skip_list {
240 let mut key = BytesMut::with_capacity(skip.len() + 1);
241 key.put(skip.as_bytes());
242
243 skip_set.insert(key.clone());
244
245 for compression in [
246 CompressionAlgorithm::Brotli,
247 CompressionAlgorithm::Deflate,
248 CompressionAlgorithm::Gzip,
249 CompressionAlgorithm::Zstd { level: 0 },
250 ] {
251 key.put_u8(compression.as_u8());
252 skip_set.insert(key.clone());
253
254 key.truncate(skip.len());
255 }
256 }
257
258 let mut delete_keys: Vec<(Bytes, usize)> = Vec::new();
259
260 {
261 let txn = ReadTransaction::new(self.env.clone())?;
262
263 let access = txn.access();
264
265 let mut cursor = txn.cursor(self.assets_db.clone())?;
266 let (key, result) = cursor.first::<[u8], [u8]>(&access)?;
267
268 if !skip_set.contains(key) {
269 delete_keys.push((Bytes::copy_from_slice(key), result.len() + key.len()));
270 }
271
272 while let Ok((key, result)) = cursor.next::<[u8], [u8]>(&access) {
273 if !skip_set.contains(key) {
274 delete_keys.push((Bytes::copy_from_slice(key), result.len() + key.len()));
275 }
276 }
277 }
278
279 let mut store_size = self.store_size.lock();
280 let initial_size = *store_size;
281
282 let txn = WriteTransaction::new(self.env.clone())?;
283
284 {
285 let mut access = txn.access();
286
287 for (key, size) in delete_keys {
288 access.del_key(&self.assets_db, key.as_ref())?;
289 *store_size -= size as u64;
290 }
291 }
292
293 txn.commit()?;
294
295 if self.log_sizes {
296 tracing::info!(store.size = %bytesize::ByteSize(*store_size), store.size.reduced = %bytesize::ByteSize(*store_size - initial_size));
297 }
298
299 Ok(())
300 }
301}