Skip to content

Commit 0d4111b

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

File tree

3 files changed

+411
-0
lines changed

3 files changed

+411
-0
lines changed

data/src/cache.rs

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

0 commit comments

Comments
 (0)