Skip to content

Commit 346488b

Browse files
committed
actually check sha256 for managing coman sqsh
1 parent 73ca4ed commit 346488b

6 files changed

Lines changed: 120 additions & 56 deletions

File tree

Cargo.lock

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

coman/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ dirs = "6.0.0"
8383
iroh = "0.95.1"
8484
rand = "0.9.2"
8585
regex = "1.12.2"
86+
sha2 = "0.10.9"
8687

8788
[build-dependencies]
8889
anyhow = "1.0.90"

coman/src/cli/app.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,8 @@ pub enum CscsSystemCommands {
471471
},
472472
}
473473

474+
pub const COMAN_VERSION: &str = env!("CARGO_PKG_VERSION");
475+
474476
const VERSION_MESSAGE: &str = concat!(
475477
env!("CARGO_PKG_VERSION"),
476478
"-",

coman/src/cscs/api_client/client.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ use firecrest_client::{
99
get_compute_system_jobs, post_compute_system_job,
1010
},
1111
filesystem_api::{
12-
delete_filesystem_ops_rm, get_filesystem_ops_download, get_filesystem_ops_ls, get_filesystem_ops_stat,
13-
get_filesystem_ops_tail, post_filesystem_ops_mkdir, post_filesystem_ops_upload,
12+
delete_filesystem_ops_rm, get_filesystem_ops_checksum, get_filesystem_ops_download, get_filesystem_ops_ls,
13+
get_filesystem_ops_stat, get_filesystem_ops_tail, post_filesystem_ops_mkdir, post_filesystem_ops_upload,
1414
post_filesystem_transfer_download, post_filesystem_transfer_upload, put_filesystem_ops_chmod,
1515
},
1616
status_api::{get_status_systems, get_status_userinfo},
@@ -231,6 +231,11 @@ impl CscsApi {
231231
None => Ok(vec![]),
232232
}
233233
}
234+
pub async fn checksum(&self, system_name: &str, path: PathBuf) -> Result<Option<String>> {
235+
get_filesystem_ops_checksum(&self.client, system_name, path)
236+
.await
237+
.wrap_err("couldn't stat file")
238+
}
234239
pub async fn stat_path(&self, system_name: &str, path: PathBuf) -> Result<Option<FileStat>> {
235240
let result = get_filesystem_ops_stat(&self.client, system_name, path)
236241
.await

coman/src/cscs/handlers.rs

Lines changed: 88 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use iroh::{Endpoint, EndpointId, SecretKey};
1818
use itertools::Itertools;
1919
use regex::Regex;
2020
use reqwest::Url;
21+
use sha2::{Digest, Sha256};
2122
use tokio::{
2223
fs::File,
2324
io::AsyncWriteExt,
@@ -26,6 +27,7 @@ use tokio::{
2627

2728
use super::api_client::client::{EdfSpec, ScriptSpec};
2829
use crate::{
30+
cli::app::COMAN_VERSION,
2931
config::{ComputePlatform, Config, get_data_dir},
3032
cscs::{
3133
api_client::{
@@ -396,6 +398,82 @@ async fn store_ssh_information(
396398
Ok(connection_name)
397399
}
398400

401+
async fn maybe_download_latest_squash(current_system: &str, config: &Config) -> Result<PathBuf, eyre::Error> {
402+
if let Some(path) = config.values.coman_squash_path.clone() {
403+
return Ok(path);
404+
}
405+
//download from github for architecture
406+
let system = config
407+
.values
408+
.cscs
409+
.systems
410+
.get(current_system)
411+
.ok_or(eyre!("couldn't find architecture for system {}", current_system))?;
412+
let architecture = system
413+
.architecture
414+
.first()
415+
.ok_or(eyre!("no architecture set for {}", current_system))?;
416+
let target_path = get_data_dir().join(format!("coman_{}.sqsh", architecture));
417+
let arch = match architecture.as_str() {
418+
"arm64" => "aarch64",
419+
"amd64" => "x86_64",
420+
_ => {
421+
return Err(eyre!("unsupported architecture {}", architecture));
422+
}
423+
};
424+
if target_path.exists() {
425+
// if file exists, check checksum against newest version and delete if no match
426+
let mut hasher = Sha256::new();
427+
let mut file = std::fs::File::open(&target_path)?;
428+
let _n = std::io::copy(&mut file, &mut hasher)?;
429+
let hash = hasher.finalize();
430+
let hash = format!("{hash:02x}");
431+
432+
let client = reqwest::Client::new();
433+
let resp = client
434+
.get(format!(
435+
"https://api.github.com/repos/SwissDataScienceCenter/coman/releases/tags/v{}",
436+
COMAN_VERSION
437+
))
438+
.header("Accept", "application/vnd.github+json")
439+
.header("User-Agent", format!("coman cli {COMAN_VERSION}"))
440+
.send()
441+
.await?;
442+
if resp.status().is_success() {
443+
let body = resp.text().await?;
444+
let body: serde_json::Value = serde_json::from_str(&body)?;
445+
if let Some(assets) = body["assets"].as_array() {
446+
for asset in assets {
447+
if asset["name"].as_str() == Some(format!("coman_Linux-{arch}.sqsh").as_str())
448+
&& let Some(remote_hash) = asset["digest"].as_str()
449+
&& remote_hash.strip_prefix("sha256:").unwrap() != hash.as_str()
450+
{
451+
std::fs::remove_file(&target_path)?;
452+
}
453+
}
454+
}
455+
}
456+
}
457+
if !target_path.exists() {
458+
let url = format!(
459+
"https://github.com/SwissDataScienceCenter/coman/releases/download/v{COMAN_VERSION}/coman_Linux-{arch}.sqsh"
460+
);
461+
let mut out = File::create(target_path.clone()).await?;
462+
let resp = reqwest::get(url).await?;
463+
match resp.error_for_status() {
464+
Ok(resp) => {
465+
let mut stream = resp.bytes_stream();
466+
while let Some(chunk_result) = stream.next().await {
467+
let chunk = chunk_result?;
468+
out.write_all(&chunk).await?;
469+
}
470+
out.flush().await?;
471+
}
472+
Err(e) => return Err(eyre!("couldn't download coman squash file: {}", e)),
473+
}
474+
}
475+
Ok(target_path)
476+
}
399477
async fn inject_coman_squash(
400478
api_client: &CscsApi,
401479
base_path: &Path,
@@ -406,50 +484,7 @@ async fn inject_coman_squash(
406484
return Ok(None);
407485
}
408486
let config = Config::new().unwrap();
409-
let local_squash_path = match config.values.coman_squash_path.clone() {
410-
Some(path) => path,
411-
None => {
412-
//download from github for architecture
413-
let system = config
414-
.values
415-
.cscs
416-
.systems
417-
.get(current_system)
418-
.ok_or(eyre!("couldn't find architecture for system {}", current_system))?;
419-
let architecture = system
420-
.architecture
421-
.first()
422-
.ok_or(eyre!("no architecture set for {}", current_system))?;
423-
let target_path = get_data_dir().join(format!("coman_{}.sqsh", architecture));
424-
if !target_path.exists() {
425-
let url = match architecture.as_str() {
426-
"arm64" => {
427-
"https://github.com/SwissDataScienceCenter/coman/releases/latest/download/coman_Linux-aarch64.sqsh"
428-
}
429-
"amd64" => {
430-
"https://github.com/SwissDataScienceCenter/coman/releases/latest/download/coman_Linux-x86_64.sqsh"
431-
}
432-
_ => {
433-
return Err(eyre!("unsupported architecture {}", architecture));
434-
}
435-
};
436-
let mut out = File::create(target_path.clone()).await?;
437-
let resp = reqwest::get(url).await?;
438-
match resp.error_for_status() {
439-
Ok(resp) => {
440-
let mut stream = resp.bytes_stream();
441-
while let Some(chunk_result) = stream.next().await {
442-
let chunk = chunk_result?;
443-
out.write_all(&chunk).await?;
444-
}
445-
out.flush().await?;
446-
}
447-
Err(e) => return Err(eyre!("couldn't download coman squash file: {}", e)),
448-
}
449-
}
450-
target_path
451-
}
452-
};
487+
let local_squash_path = maybe_download_latest_squash(current_system, &config).await?;
453488
let target = base_path.join("coman.sqsh");
454489
let file_meta = std::fs::metadata(local_squash_path.clone())?;
455490

@@ -459,13 +494,16 @@ async fn inject_coman_squash(
459494
#[cfg(target_family = "windows")]
460495
let size = file_meta.file_size() as usize;
461496

462-
let response = api_client.list_path(current_system, target.clone()).await;
463-
if let Ok(existing) = response
464-
&& !existing.is_empty()
465-
{
497+
let mut hasher = Sha256::new();
498+
let mut file = std::fs::File::open(&local_squash_path)?;
499+
let _n = std::io::copy(&mut file, &mut hasher)?;
500+
let hash = hasher.finalize();
501+
let hash = format!("{hash:02x}");
502+
503+
let response = api_client.checksum(current_system, target.clone()).await;
504+
if let Ok(Some(remote_hash)) = response {
466505
//squash file already present on remote, don't upload if it's the same
467-
let entry = existing.first().unwrap();
468-
if entry.size.unwrap_or_default() == size {
506+
if hash == remote_hash {
469507
return Ok(Some(target));
470508
} else {
471509
// remove file before upload

firecrest_client/src/filesystem_api.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ use eyre::{Result, eyre};
55
use crate::{
66
client::FirecrestClient,
77
types::{
8-
DownloadFileResponse, DownloadFileResponseTransferDirectives, GetDirectoryLsResponse, GetFileStatResponse,
9-
GetFileTailResponse, PostFileDownloadRequest, PostFileDownloadRequestTransferDirectives, PostFileUploadRequest,
10-
PostMakeDirRequest, PostMkdirResponse, PutFileChmodRequest, PutFileChmodResponse, S3TransferRequest,
11-
S3TransferResponse, UploadFileResponse,
8+
DownloadFileResponse, DownloadFileResponseTransferDirectives, GetDirectoryLsResponse, GetFileChecksumResponse,
9+
GetFileStatResponse, GetFileTailResponse, PostFileDownloadRequest, PostFileDownloadRequestTransferDirectives,
10+
PostFileUploadRequest, PostMakeDirRequest, PostMkdirResponse, PutFileChmodRequest, PutFileChmodResponse,
11+
S3TransferRequest, S3TransferResponse, UploadFileResponse,
1212
},
1313
};
1414

@@ -238,3 +238,20 @@ pub async fn delete_filesystem_ops_rm(client: &FirecrestClient, system_name: &st
238238
.await?;
239239
Ok(())
240240
}
241+
242+
pub async fn get_filesystem_ops_checksum(
243+
client: &FirecrestClient,
244+
system_name: &str,
245+
path: PathBuf,
246+
) -> Result<Option<String>> {
247+
let path = path.as_os_str().to_str().ok_or(eyre!("couldn't cast path to string"))?;
248+
let response = client
249+
.get(
250+
format!("filesystem/{system_name}/ops/checksum").as_str(),
251+
Some(vec![("path", path)]),
252+
)
253+
.await?;
254+
let model: GetFileChecksumResponse = serde_json::from_str(response.as_str())?;
255+
256+
Ok(model.output.map(|o| o.checksum))
257+
}

0 commit comments

Comments
 (0)