Skip to content
This repository was archived by the owner on Jan 21, 2026. It is now read-only.

Commit b69a63d

Browse files
committed
add oci blob download support
1 parent 8f4ec9d commit b69a63d

File tree

2 files changed

+120
-42
lines changed

2 files changed

+120
-42
lines changed

src/downloader.rs

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use std::{
2+
collections::HashMap,
23
fs::Permissions,
34
os::unix::fs::PermissionsExt,
4-
path::Path,
5+
path::{Path, PathBuf},
56
sync::{Arc, Mutex},
67
};
78

@@ -16,7 +17,7 @@ use url::Url;
1617

1718
use crate::{
1819
error::DownloadError,
19-
oci::{OciClient, Reference},
20+
oci::{OciClient, OciLayer, Reference},
2021
utils::{extract_filename, is_elf},
2122
};
2223

@@ -120,10 +121,63 @@ impl Downloader {
120121
Ok(filename)
121122
}
122123

124+
pub async fn download_blob(
125+
&self,
126+
client: OciClient,
127+
options: DownloadOptions,
128+
) -> Result<(), DownloadError> {
129+
let reference = client.reference.clone();
130+
let digest = reference.tag;
131+
let downloaded_bytes = Arc::new(Mutex::new(0u64));
132+
let output_path = options.output_path;
133+
let ref_name = reference
134+
.package
135+
.rsplit_once('/')
136+
.map_or(digest.clone(), |(_, name)| name.to_string());
137+
let file_path = output_path.unwrap_or_else(|| ref_name.clone());
138+
let file_path = if file_path.ends_with('/') {
139+
fs::create_dir_all(&file_path).await?;
140+
format!("{}/{}", file_path.trim_end_matches('/'), ref_name)
141+
} else {
142+
file_path
143+
};
144+
145+
let fake_layer = OciLayer {
146+
media_type: String::from("application/octet-stream"),
147+
digest: digest.clone(),
148+
size: 0,
149+
annotations: HashMap::new(),
150+
};
151+
152+
let cb_clone = options.progress_callback.clone();
153+
client
154+
.pull_layer(&fake_layer, &file_path, move |bytes, total_bytes| {
155+
if let Some(ref callback) = cb_clone {
156+
if total_bytes > 0 {
157+
callback(DownloadState::Preparing(total_bytes));
158+
}
159+
let mut current = downloaded_bytes.lock().unwrap();
160+
*current = bytes;
161+
callback(DownloadState::Progress(*current));
162+
}
163+
})
164+
.await?;
165+
166+
if let Some(ref callback) = options.progress_callback {
167+
callback(DownloadState::Complete);
168+
}
169+
170+
Ok(())
171+
}
172+
123173
pub async fn download_oci(&self, options: DownloadOptions) -> Result<(), DownloadError> {
124174
let url = options.url.clone();
125175
let reference: Reference = url.into();
126-
let oci_client = OciClient::new(reference);
176+
let oci_client = OciClient::new(&reference);
177+
178+
if reference.tag.starts_with("sha256:") {
179+
return self.download_blob(oci_client, options).await;
180+
}
127181

128182
let manifest = oci_client.manifest().await.unwrap();
129183

@@ -136,16 +190,25 @@ impl Downloader {
136190

137191
let downloaded_bytes = Arc::new(Mutex::new(0u64));
138192
let outdir = options.output_path;
193+
let base_path = if let Some(dir) = outdir {
194+
fs::create_dir_all(&dir).await?;
195+
PathBuf::from(dir)
196+
} else {
197+
PathBuf::new()
198+
};
139199

140200
for layer in manifest.layers {
141201
let client_clone = oci_client.clone();
142202
let cb_clone = options.progress_callback.clone();
143203
let downloaded_bytes = downloaded_bytes.clone();
144-
let outdir = outdir.clone();
204+
let Some(filename) = layer.get_title() else {
205+
continue;
206+
};
207+
let file_path = base_path.join(filename);
145208

146209
let task = task::spawn(async move {
147210
client_clone
148-
.pull_layer(&layer, outdir, move |bytes| {
211+
.pull_layer(&layer, &file_path, move |bytes, _| {
149212
if let Some(ref callback) = cb_clone {
150213
let mut current = downloaded_bytes.lock().unwrap();
151214
*current = bytes;

src/oci.rs

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::{
22
collections::HashMap,
3+
fs::Permissions,
4+
os::unix::fs::PermissionsExt,
35
path::{Path, PathBuf},
46
};
57

@@ -11,7 +13,7 @@ use tokio::{
1113
io::AsyncWriteExt,
1214
};
1315

14-
use crate::error::DownloadError;
16+
use crate::{error::DownloadError, utils::is_elf};
1517

1618
#[derive(Deserialize)]
1719
pub struct OciLayer {
@@ -41,43 +43,55 @@ pub struct OciManifest {
4143
#[derive(Clone)]
4244
pub struct OciClient {
4345
client: reqwest::Client,
44-
reference: Reference,
46+
pub reference: Reference,
4547
}
4648

4749
#[derive(Clone)]
4850
pub struct Reference {
49-
package: String,
50-
tag: String,
51+
pub package: String,
52+
pub tag: String,
5153
}
5254

5355
impl From<&str> for Reference {
5456
fn from(value: &str) -> Self {
5557
let paths = value.trim_start_matches("ghcr.io/");
56-
let (package, tag) = paths.split_once(':').unwrap_or((paths, "latest"));
58+
59+
// <package>@sha256:<digest>
60+
if let Some((package, digest)) = paths.split_once("@") {
61+
return Self {
62+
package: package.to_string(),
63+
tag: digest.to_string(),
64+
};
65+
}
66+
67+
// <package>:<tag>
68+
if let Some((package, tag)) = paths.split_once(':') {
69+
return Self {
70+
package: package.to_string(),
71+
tag: tag.to_string(),
72+
};
73+
}
5774

5875
Self {
59-
package: package.to_string(),
60-
tag: tag.to_string(),
76+
package: paths.to_string(),
77+
tag: "latest".to_string(),
6178
}
6279
}
6380
}
6481

6582
impl From<String> for Reference {
6683
fn from(value: String) -> Self {
67-
let paths = value.trim_start_matches("ghcr.io/");
68-
let (package, tag) = paths.split_once(':').unwrap_or((paths, "latest"));
69-
70-
Self {
71-
package: package.to_string(),
72-
tag: tag.to_string(),
73-
}
84+
value.as_str().into()
7485
}
7586
}
7687

7788
impl OciClient {
78-
pub fn new(reference: Reference) -> Self {
89+
pub fn new(reference: &Reference) -> Self {
7990
let client = reqwest::Client::new();
80-
Self { client, reference }
91+
Self {
92+
client,
93+
reference: reference.clone(),
94+
}
8195
}
8296

8397
pub fn headers(&self) -> HeaderMap {
@@ -114,14 +128,15 @@ impl OciClient {
114128
Ok(manifest)
115129
}
116130

117-
pub async fn pull_layer<F, P: AsRef<Path>>(
131+
pub async fn pull_layer<F, P>(
118132
&self,
119133
layer: &OciLayer,
120-
output_dir: Option<P>,
134+
output_path: P,
121135
progress_callback: F,
122136
) -> Result<u64, DownloadError>
123137
where
124-
F: Fn(u64) + Send + 'static,
138+
P: AsRef<Path>,
139+
F: Fn(u64, u64) + Send + 'static,
125140
{
126141
let blob_url = format!(
127142
"https://ghcr.io/v2/{}/blobs/{}",
@@ -142,22 +157,11 @@ impl OciClient {
142157
});
143158
}
144159

145-
let Some(filename) = layer.get_title() else {
146-
// skip if layer doesn't contain title
147-
return Ok(0);
148-
};
149-
150-
let (temp_path, final_path) = if let Some(output_dir) = output_dir {
151-
let output_dir = output_dir.as_ref();
152-
fs::create_dir_all(output_dir).await?;
153-
let final_path = output_dir.join(format!("{filename}"));
154-
let temp_path = output_dir.join(format!("{filename}.part"));
155-
(temp_path, final_path)
156-
} else {
157-
let final_path = PathBuf::from(&filename);
158-
let temp_path = PathBuf::from(format!("{filename}.part"));
159-
(temp_path, final_path)
160-
};
160+
let content_length = resp.content_length().unwrap_or(0);
161+
progress_callback(0, content_length);
162+
163+
let output_path = output_path.as_ref();
164+
let temp_path = PathBuf::from(&format!("{}.part", output_path.display()));
161165

162166
let mut file = OpenOptions::new()
163167
.create(true)
@@ -173,11 +177,15 @@ impl OciClient {
173177
let chunk_size = chunk.len() as u64;
174178
file.write_all(&chunk).await.unwrap();
175179

176-
progress_callback(chunk_size);
180+
progress_callback(chunk_size, 0);
177181
total_bytes_downloaded += chunk_size;
178182
}
179183

180-
fs::rename(&temp_path, &final_path).await?;
184+
fs::rename(&temp_path, &output_path).await?;
185+
186+
if is_elf(&output_path).await {
187+
fs::set_permissions(&output_path, Permissions::from_mode(0o755)).await?;
188+
}
181189

182190
Ok(total_bytes_downloaded)
183191
}
@@ -189,4 +197,11 @@ impl OciLayer {
189197
.get("org.opencontainers.image.title")
190198
.cloned()
191199
}
200+
201+
pub fn set_title(&mut self, title: &str) {
202+
self.annotations.insert(
203+
"org.opencontainers.image.title".to_string(),
204+
title.to_string(),
205+
);
206+
}
192207
}

0 commit comments

Comments
 (0)