Skip to content

Commit b3faff0

Browse files
authored
Merge pull request #1885 from michalrus/LW-11112-file-scheme-urls
feat: support `file://` URLs for snapshot locations
2 parents 33ee2fd + 6ffd08c commit b3faff0

File tree

5 files changed

+100
-20
lines changed

5 files changed

+100
-20
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@ mithril-infra/terraform.tfstate*
1616
mithril-infra/*.tfvars
1717
justfile
1818

19+
# Outputs of nix-build:
20+
result

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ As a minor extension, we have adopted a slightly different versioning convention
1111

1212
- Support for Mithril nodes footprint support in Prometheus monitoring in infrastructure
1313
- Add support for custom HTTP headers in Mithril client WASM library
14+
- Support `file://` URLs for snapshot locations
1415

1516
- **UNSTABLE** Cardano stake distribution certification:
1617

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mithril-client/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mithril-client"
3-
version = "0.8.14"
3+
version = "0.8.15"
44
description = "Mithril client library"
55
authors = { workspace = true }
66
edition = { workspace = true }

mithril-client/src/snapshot_downloader.rs

Lines changed: 95 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@
1010
use anyhow::{anyhow, Context};
1111
use async_trait::async_trait;
1212
use futures::StreamExt;
13+
use reqwest::Url;
1314
use reqwest::{Response, StatusCode};
1415
use slog::{debug, Logger};
16+
use std::fs;
1517
use std::path::Path;
18+
use tokio::fs::File;
19+
use tokio::io::AsyncReadExt;
1620

1721
#[cfg(test)]
1822
use mockall::automock;
@@ -78,6 +82,74 @@ impl HttpSnapshotDownloader {
7882
status_code => Err(anyhow!("Unhandled error {status_code}")),
7983
}
8084
}
85+
86+
fn file_scheme_to_local_path(file_url: &str) -> Option<String> {
87+
Url::parse(file_url)
88+
.ok()
89+
.filter(|url| url.scheme() == "file")
90+
.and_then(|url| url.to_file_path().ok())
91+
.map(|path| path.to_string_lossy().into_owned())
92+
}
93+
94+
async fn download_local_file<F, Fut>(
95+
&self,
96+
local_path: &str,
97+
sender: &flume::Sender<Vec<u8>>,
98+
report_progress: F,
99+
) -> MithrilResult<()>
100+
where
101+
F: Fn(u64) -> Fut,
102+
Fut: std::future::Future<Output = ()>,
103+
{
104+
// Stream the `location` directly from the local filesystem
105+
let mut downloaded_bytes: u64 = 0;
106+
let mut file = File::open(local_path).await?;
107+
108+
loop {
109+
// We can either allocate here each time, or clone a shared buffer into sender.
110+
// A larger read buffer is faster, less context switches:
111+
let mut buffer = vec![0; 16 * 1024 * 1024];
112+
let bytes_read = file.read(&mut buffer).await?;
113+
if bytes_read == 0 {
114+
break;
115+
}
116+
buffer.truncate(bytes_read);
117+
sender.send_async(buffer).await.with_context(|| {
118+
format!(
119+
"Local file read: could not write {} bytes to stream.",
120+
bytes_read
121+
)
122+
})?;
123+
downloaded_bytes += bytes_read as u64;
124+
report_progress(downloaded_bytes).await
125+
}
126+
Ok(())
127+
}
128+
129+
async fn download_remote_file<F, Fut>(
130+
&self,
131+
location: &str,
132+
sender: &flume::Sender<Vec<u8>>,
133+
report_progress: F,
134+
) -> MithrilResult<()>
135+
where
136+
F: Fn(u64) -> Fut,
137+
Fut: std::future::Future<Output = ()>,
138+
{
139+
let mut downloaded_bytes: u64 = 0;
140+
let mut remote_stream = self.get(location).await?.bytes_stream();
141+
while let Some(item) = remote_stream.next().await {
142+
let chunk = item.with_context(|| "Download: Could not read from byte stream")?;
143+
144+
sender.send_async(chunk.to_vec()).await.with_context(|| {
145+
format!("Download: could not write {} bytes to stream.", chunk.len())
146+
})?;
147+
148+
downloaded_bytes += chunk.len() as u64;
149+
report_progress(downloaded_bytes).await
150+
}
151+
Ok(())
152+
}
81153
}
82154

83155
#[cfg_attr(test, automock)]
@@ -97,8 +169,6 @@ impl SnapshotDownloader for HttpSnapshotDownloader {
97169
.context("Download-Unpack: prerequisite error"),
98170
)?;
99171
}
100-
let mut downloaded_bytes: u64 = 0;
101-
let mut remote_stream = self.get(location).await?.bytes_stream();
102172
let (sender, receiver) = flume::bounded(5);
103173

104174
let dest_dir = target_dir.to_path_buf();
@@ -107,21 +177,22 @@ impl SnapshotDownloader for HttpSnapshotDownloader {
107177
unpacker.unpack_snapshot(receiver, compression_algorithm, &dest_dir)
108178
});
109179

110-
while let Some(item) = remote_stream.next().await {
111-
let chunk = item.with_context(|| "Download: Could not read from byte stream")?;
112-
113-
sender.send_async(chunk.to_vec()).await.with_context(|| {
114-
format!("Download: could not write {} bytes to stream.", chunk.len())
115-
})?;
116-
117-
downloaded_bytes += chunk.len() as u64;
180+
let report_progress = |downloaded_bytes: u64| async move {
118181
self.feedback_sender
119182
.send_event(MithrilEvent::SnapshotDownloadProgress {
120183
download_id: download_id.to_owned(),
121184
downloaded_bytes,
122185
size: snapshot_size,
123186
})
124187
.await
188+
};
189+
190+
if let Some(local_path) = Self::file_scheme_to_local_path(location) {
191+
self.download_local_file(&local_path, &sender, report_progress)
192+
.await?;
193+
} else {
194+
self.download_remote_file(location, &sender, report_progress)
195+
.await?;
125196
}
126197

127198
drop(sender); // Signal EOF
@@ -143,15 +214,21 @@ impl SnapshotDownloader for HttpSnapshotDownloader {
143214
async fn probe(&self, location: &str) -> MithrilResult<()> {
144215
debug!(self.logger, "HEAD Snapshot location='{location}'.");
145216

146-
let request_builder = self.http_client.head(location);
147-
let response = request_builder.send().await.with_context(|| {
148-
format!("Cannot perform a HEAD for snapshot at location='{location}'")
149-
})?;
217+
if let Some(local_path) = Self::file_scheme_to_local_path(location) {
218+
fs::metadata(local_path)
219+
.with_context(|| format!("Local snapshot location='{location}' not found"))
220+
.map(drop)
221+
} else {
222+
let request_builder = self.http_client.head(location);
223+
let response = request_builder.send().await.with_context(|| {
224+
format!("Cannot perform a HEAD for snapshot at location='{location}'")
225+
})?;
150226

151-
match response.status() {
152-
StatusCode::OK => Ok(()),
153-
StatusCode::NOT_FOUND => Err(anyhow!("Snapshot location='{location} not found")),
154-
status_code => Err(anyhow!("Unhandled error {status_code}")),
227+
match response.status() {
228+
StatusCode::OK => Ok(()),
229+
StatusCode::NOT_FOUND => Err(anyhow!("Snapshot location='{location} not found")),
230+
status_code => Err(anyhow!("Unhandled error {status_code}")),
231+
}
155232
}
156233
}
157234
}

0 commit comments

Comments
 (0)