Skip to content

Commit b568c3e

Browse files
committed
Introduce a common cache interface
1 parent 27f5bd6 commit b568c3e

File tree

3 files changed

+409
-0
lines changed

3 files changed

+409
-0
lines changed

data/src/cache.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
mod trim;
2+
3+
use std::path::{Path, PathBuf};
4+
5+
use chrono::Utc;
6+
use derive_more::AsRef;
7+
use serde::{Deserialize, Serialize, de::DeserializeOwned};
8+
use tokio::fs;
9+
use url::Url;
10+
11+
pub use trim::TrimConfig;
12+
13+
/// SHA256 digest of cache content.
14+
#[derive(Debug, Clone, Serialize, Deserialize, AsRef)]
15+
pub struct Digest(String);
16+
17+
impl Digest {
18+
pub fn new(data: &[u8]) -> Self {
19+
Self(hex::encode(data))
20+
}
21+
}
22+
23+
pub trait CachedAsset: Send + Sync + 'static {
24+
fn paths(&self) -> Vec<&Path>;
25+
}
26+
27+
#[derive(Debug, Clone, Serialize, Deserialize)]
28+
#[serde(rename_all = "snake_case")]
29+
pub enum CacheState<T> {
30+
Ok(T),
31+
Error,
32+
}
33+
34+
pub struct FileCache {
35+
root: PathBuf,
36+
trim: TrimConfig,
37+
}
38+
39+
impl FileCache {
40+
pub fn new(root: PathBuf, trim: TrimConfig) -> Self {
41+
Self { root, trim }
42+
}
43+
44+
pub async fn load<T>(&self, url: &Url) -> Option<CacheState<T>>
45+
where
46+
T: CachedAsset + DeserializeOwned,
47+
{
48+
let path = self.state_path(url);
49+
50+
let bytes = fs::read(&path).await.ok()?;
51+
let state: CacheState<T> = serde_json::from_slice(&bytes).ok()?;
52+
53+
if let CacheState::Ok(ref asset) = state {
54+
let any_missing = asset.paths().iter().any(|p| !p.exists());
55+
if any_missing {
56+
// If any of the asset's files are missing, treat the cache as invalid.
57+
return None;
58+
}
59+
}
60+
61+
Some(state)
62+
}
63+
64+
pub async fn save<T: Serialize>(&self, url: &Url, state: &CacheState<T>) {
65+
let path = self.state_path(url);
66+
67+
if let Some(parent) = path.parent().filter(|p| !p.exists()) {
68+
let _ = fs::create_dir_all(parent).await;
69+
}
70+
71+
let Ok(bytes) = serde_json::to_vec(state) else {
72+
return;
73+
};
74+
let _ = fs::write(&path, &bytes).await;
75+
}
76+
77+
pub fn account_blob(&self, size: u64, blob_path: PathBuf) {
78+
self.trim.maybe_trim(size, blob_path);
79+
}
80+
81+
pub fn state_path(&self, url: &Url) -> PathBuf {
82+
let hash =
83+
hex::encode(seahash::hash(url.as_str().as_bytes()).to_be_bytes());
84+
85+
self.root
86+
.join("state")
87+
.join(&hash[..2])
88+
.join(&hash[2..4])
89+
.join(&hash[4..6])
90+
.join(format!("{hash}.json"))
91+
}
92+
93+
pub fn blob_path(&self, digest: &Digest, ext: &str) -> PathBuf {
94+
let hash = digest.as_ref();
95+
96+
blob_dir_from_root(&self.root)
97+
.join(&hash[..2])
98+
.join(&hash[2..4])
99+
.join(&hash[4..6])
100+
.join(format!("{hash}.{ext}"))
101+
}
102+
103+
pub fn download_path(&self, url: &Url) -> PathBuf {
104+
let hash = seahash::hash(url.as_str().as_bytes());
105+
let nanos = Utc::now().timestamp_nanos_opt().unwrap_or_default();
106+
107+
self.root
108+
.join("downloads")
109+
.join(format!("{hash}-{nanos}.part"))
110+
}
111+
}
112+
113+
pub fn blob_dir_from_root(root: &Path) -> PathBuf {
114+
root.join("blobs")
115+
}

0 commit comments

Comments
 (0)