1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
//! Tgz store — download, verify integrity, and persist to OPFS.
use std::path::{Path, PathBuf};
use bytes::Bytes;
use crate::archive;
use crate::config::Config;
use crate::error::{OpfsError, VerifyResult};
/// Manages the tgz file store on OPFS.
pub struct Store {
root: PathBuf,
retries: u32,
retry_base_delay_ms: u64,
}
impl Store {
pub fn new(config: &Config) -> Self {
Self {
root: config.store_root.clone(),
retries: config.download_retries,
retry_base_delay_ms: config.retry_base_delay_ms,
}
}
/// Compute the OPFS path where a package tgz is stored.
pub fn tgz_path(&self, name: &str, tgz_url: &str) -> PathBuf {
let file_name = tgz_url.rsplit('/').next().unwrap_or("package.tgz");
self.root.join(name).join("-").join(file_name)
}
/// Check whether the tgz for a package is already on disk.
pub async fn is_cached(&self, name: &str, tgz_url: &str) -> bool {
let path = self.tgz_path(name, tgz_url);
tokio_fs_ext::metadata(&path)
.await
.map(|m| m.is_file())
.unwrap_or(false)
}
/// Ensure a tgz is on disk — download if missing.
///
/// Unlike [`fetch_tgz`], this does **not** read or re-verify cached files.
/// The integrity was already checked when the file was first downloaded
/// and saved. This makes second installs O(1) per package (metadata check)
/// instead of O(n) (full file read + SHA-512).
///
/// Returns `was_fresh` — `true` when the tgz was freshly downloaded
/// (i.e. it was not yet on disk). Callers should use this flag to
/// invalidate stale extraction caches.
pub async fn ensure_tgz(
&self,
name: &str,
version: &str,
tgz_url: &str,
integrity: Option<&str>,
shasum: Option<&str>,
) -> Result<bool, OpfsError> {
let store_path = self.tgz_path(name, tgz_url);
// Fast path: file already exists on disk — trust it.
// Integrity was verified when first downloaded.
if tokio_fs_ext::metadata(&store_path)
.await
.map(|m| m.is_file())
.unwrap_or(false)
{
return Ok(false);
}
// Not cached — download, verify, and persist.
let bytes = self.download_with_retry(tgz_url).await?;
if archive::verify_integrity(&bytes, integrity, shasum).is_failed() {
return Err(OpfsError::IntegrityFailed {
package: name.to_string(),
version: version.to_string(),
});
}
self.save(&store_path, &bytes).await?;
Ok(true)
}
/// Fetch a tgz — returns cached bytes if valid, otherwise downloads.
///
/// Performs full integrity re-verification on cached files. Use
/// [`ensure_tgz`] when you only need the file on disk (e.g. install flow).
///
/// Returns `(bytes, was_fresh)` where `was_fresh` is `true` when the tgz
/// was re-downloaded (e.g. because the cached copy failed integrity).
pub async fn fetch_tgz(
&self,
name: &str,
version: &str,
tgz_url: &str,
integrity: Option<&str>,
shasum: Option<&str>,
) -> Result<(Bytes, bool), OpfsError> {
let store_path = self.tgz_path(name, tgz_url);
// Try cached file — full read + integrity verification
if let Ok(existing) = tokio_fs_ext::read(&store_path).await {
match archive::verify_integrity(&existing, integrity, shasum) {
VerifyResult::Verified | VerifyResult::NoHashAvailable => {
return Ok((Bytes::from(existing), false));
}
VerifyResult::Failed => {
tracing::warn!("{name}@{version}: cached tgz failed integrity, re-downloading");
}
}
}
// Download with retry
let bytes = self.download_with_retry(tgz_url).await?;
// Verify downloaded bytes
if archive::verify_integrity(&bytes, integrity, shasum).is_failed() {
return Err(OpfsError::IntegrityFailed {
package: name.to_string(),
version: version.to_string(),
});
}
// Persist
self.save(&store_path, &bytes).await?;
Ok((Bytes::from(bytes), true))
}
// ── private ──────────────────────────────────────────────────────
async fn save(&self, path: &Path, bytes: &[u8]) -> Result<(), OpfsError> {
if let Some(parent) = path.parent() {
tokio_fs_ext::create_dir_all(parent).await?;
}
tokio_fs_ext::write(path, bytes).await?;
Ok(())
}
async fn download_with_retry(&self, url: &str) -> Result<Vec<u8>, OpfsError> {
let mut last_err = None;
for attempt in 0..self.retries {
if attempt > 0 {
let delay = self.retry_base_delay_ms.saturating_mul(1u64 << (attempt - 1).min(63));
wasmtimer::tokio::sleep(std::time::Duration::from_millis(delay)).await;
}
match self.download_once(url).await {
Ok(b) => return Ok(b),
Err(e) => {
tracing::warn!(
"download {}/{} for {url} failed: {e}",
attempt + 1,
self.retries
);
last_err = Some(e);
}
}
}
Err(last_err.unwrap_or_else(|| OpfsError::Other(format!("download failed: {url}"))))
}
async fn download_once(&self, url: &str) -> Result<Vec<u8>, OpfsError> {
let resp = reqwest::get(url).await?;
let status = resp.status();
if !status.is_success() {
return Err(OpfsError::Http {
status: status.as_u16(),
url: url.to_string(),
});
}
Ok(resp.bytes().await?.to_vec())
}
}