From acebc82a5926d6eacabe6c654bf93d2290969091 Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Sat, 10 Feb 2024 19:17:23 -0300 Subject: [PATCH 1/2] app_store: allow frontend to select mirror to use via POST body --- .../app_store/app_store/src/http_api.rs | 17 +++++++++++------ kinode/packages/app_store/app_store/src/lib.rs | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/kinode/packages/app_store/app_store/src/http_api.rs b/kinode/packages/app_store/app_store/src/http_api.rs index 30bb66e89..d6d24880e 100644 --- a/kinode/packages/app_store/app_store/src/http_api.rs +++ b/kinode/packages/app_store/app_store/src/http_api.rs @@ -235,10 +235,12 @@ fn serve_paths( } Method::POST => { // download an app - // TODO get fields from POST body let pkg_listing: &PackageListing = state .get_listing(&package_id) .ok_or(anyhow::anyhow!("No package"))?; + // from POST body, look for download_from field and use that as the mirror + let body = crate::get_blob()?.bytes; + let body_json: serde_json::Value = serde_json::from_slice(&body)?; let mirrors: &Vec = pkg_listing .metadata .as_ref() @@ -246,11 +248,14 @@ fn serve_paths( .mirrors .as_ref() .ok_or(anyhow::anyhow!("No mirrors for package {package_id}"))?; - // TODO select on FE - let download_from = mirrors - .first() - .ok_or(anyhow::anyhow!("No mirrors for package {package_id}"))?; - // TODO select on FE + let download_from = body_json + .get("download_from") + .ok_or(json!(mirrors.first().ok_or(anyhow::anyhow!( + "No mirrors for package {package_id}" + ))?))? + .as_str() + .ok_or(json!("download_from not a string"))?; + // TODO select on FE? or after download but before install? let mirror = false; let auto_update = false; let desired_version_hash = None; diff --git a/kinode/packages/app_store/app_store/src/lib.rs b/kinode/packages/app_store/app_store/src/lib.rs index 96bb5901f..56b190661 100644 --- a/kinode/packages/app_store/app_store/src/lib.rs +++ b/kinode/packages/app_store/app_store/src/lib.rs @@ -407,7 +407,7 @@ fn handle_receive_download( Some(hash) => { if download_hash != hash { return Err(anyhow::anyhow!( - "app store: downloaded package is not latest version--rejecting download!" + "app store: downloaded package is not desired version--rejecting download! download hash: {download_hash}, desired hash: {hash}" )); } } @@ -422,7 +422,7 @@ fn handle_receive_download( if let Some(latest_hash) = metadata.versions.clone().unwrap_or(vec![]).last() { if &download_hash != latest_hash { return Err(anyhow::anyhow!( - "app store: downloaded package is not latest version--rejecting download!" + "app store: downloaded package is not latest version--rejecting download! download hash: {download_hash}, latest hash: {latest_hash}" )); } } else { From a8b2d3534c438a8c2d68806a15662631970a1aa5 Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Sat, 10 Feb 2024 20:21:15 -0300 Subject: [PATCH 2/2] add auto-update, make `update` http action real --- Cargo.lock | 2 +- .../app_store/app_store/src/http_api.rs | 52 +++++++++++++++---- .../packages/app_store/app_store/src/lib.rs | 39 ++++++++++++-- .../packages/app_store/app_store/src/types.rs | 49 ++++++++++++++++- 4 files changed, 125 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 41943ebf7..c7285c74a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3227,7 +3227,7 @@ dependencies = [ [[package]] name = "kit" version = "0.1.0" -source = "git+https://github.com/kinode-dao/kit?rev=25b098f#25b098fab136387065d6058162d33c727d277ab8" +source = "git+https://github.com/kinode-dao/kit?rev=0c43430#0c434306fdce55e11d3309959fc4a0fe6ae28def" dependencies = [ "anyhow", "base64 0.21.7", diff --git a/kinode/packages/app_store/app_store/src/http_api.rs b/kinode/packages/app_store/app_store/src/http_api.rs index d6d24880e..4d52c0772 100644 --- a/kinode/packages/app_store/app_store/src/http_api.rs +++ b/kinode/packages/app_store/app_store/src/http_api.rs @@ -188,8 +188,38 @@ fn serve_paths( } Method::PUT => { // update an app - // TODO - Ok((StatusCode::NO_CONTENT, None, format!("TODO").into_bytes())) + let pkg_listing: &PackageListing = state + .get_listing(&package_id) + .ok_or(anyhow::anyhow!("No package"))?; + let pkg_state: &PackageState = state + .downloaded_packages + .get(&package_id) + .ok_or(anyhow::anyhow!("No package"))?; + let download_from = pkg_state + .mirrored_from + .as_ref() + .ok_or(anyhow::anyhow!("No mirror for package {package_id}"))? + .to_string(); + match crate::start_download( + our, + requested_packages, + &package_id, + &download_from, + pkg_state.mirroring, + pkg_state.auto_update, + &None, + ) { + DownloadResponse::Started => Ok(( + StatusCode::CREATED, + None, + format!("Downloading").into_bytes(), + )), + DownloadResponse::Failure => Ok(( + StatusCode::SERVICE_UNAVAILABLE, + None, + format!("Failed to download").into_bytes(), + )), + } } Method::DELETE => { // uninstall an app @@ -239,8 +269,11 @@ fn serve_paths( .get_listing(&package_id) .ok_or(anyhow::anyhow!("No package"))?; // from POST body, look for download_from field and use that as the mirror - let body = crate::get_blob()?.bytes; - let body_json: serde_json::Value = serde_json::from_slice(&body)?; + let body = crate::get_blob() + .ok_or(anyhow::anyhow!("missing blob"))? + .bytes; + let body_json: serde_json::Value = + serde_json::from_slice(&body).unwrap_or_default(); let mirrors: &Vec = pkg_listing .metadata .as_ref() @@ -250,11 +283,12 @@ fn serve_paths( .ok_or(anyhow::anyhow!("No mirrors for package {package_id}"))?; let download_from = body_json .get("download_from") - .ok_or(json!(mirrors.first().ok_or(anyhow::anyhow!( - "No mirrors for package {package_id}" - ))?))? + .unwrap_or(&json!(mirrors + .first() + .ok_or(anyhow::anyhow!("No mirrors for package {package_id}"))?)) .as_str() - .ok_or(json!("download_from not a string"))?; + .ok_or(anyhow::anyhow!("download_from not a string"))? + .to_string(); // TODO select on FE? or after download but before install? let mirror = false; let auto_update = false; @@ -263,7 +297,7 @@ fn serve_paths( our, requested_packages, &package_id, - download_from, + &download_from, mirror, auto_update, &desired_version_hash, diff --git a/kinode/packages/app_store/app_store/src/lib.rs b/kinode/packages/app_store/app_store/src/lib.rs index 56b190661..a2bde02d7 100644 --- a/kinode/packages/app_store/app_store/src/lib.rs +++ b/kinode/packages/app_store/app_store/src/lib.rs @@ -178,7 +178,7 @@ fn handle_message( if source.node() != our.node() || source.process != "eth:distro:sys" { return Err(anyhow::anyhow!("eth sub event from weird addr: {source}")); } - handle_eth_sub_event(&mut state, e)?; + handle_eth_sub_event(our, &mut state, e)?; } Req::Http(incoming) => { if source.node() != our.node() @@ -187,7 +187,7 @@ fn handle_message( return Err(anyhow::anyhow!("http_server from non-local node")); } if let HttpServerRequest::Http(req) = incoming { - http_api::handle_http_request(&our, &mut state, requested_packages, &req)?; + http_api::handle_http_request(our, &mut state, requested_packages, &req)?; } } }, @@ -269,6 +269,7 @@ fn handle_local_request( our_version, installed: false, caps_approved: true, // TODO see if we want to auto-approve local installs + manifest_hash: None, // generated in the add fn mirroring: *mirror, auto_update: false, // can't auto-update a local package metadata: None, // TODO @@ -436,6 +437,14 @@ fn handle_receive_download( } } + let old_manifest_hash = match state.downloaded_packages.get(&package_id) { + Some(package_state) => package_state + .manifest_hash + .clone() + .unwrap_or("OLD".to_string()), + _ => "OLD".to_string(), + }; + state.add_downloaded_package( &package_id, PackageState { @@ -443,12 +452,28 @@ fn handle_receive_download( our_version: download_hash, installed: false, caps_approved: false, + manifest_hash: None, // generated in the add fn mirroring: requested_package.mirror, auto_update: requested_package.auto_update, metadata: None, // TODO }, Some(blob.bytes), - ) + )?; + + let new_manifest_hash = match state.downloaded_packages.get(&package_id) { + Some(package_state) => package_state + .manifest_hash + .clone() + .unwrap_or("NEW".to_string()), + _ => "NEW".to_string(), + }; + + // lastly, if auto_update is true, AND the caps_hash has NOT changed, + // trigger install! + if requested_package.auto_update && old_manifest_hash == new_manifest_hash { + handle_install(our, state, &package_id)?; + } + Ok(()) } fn handle_ft_worker_result(body: &[u8], context: &[u8]) -> anyhow::Result<()> { @@ -470,11 +495,15 @@ fn handle_ft_worker_result(body: &[u8], context: &[u8]) -> anyhow::Result<()> { Ok(()) } -fn handle_eth_sub_event(state: &mut State, event: EthSubEvent) -> anyhow::Result<()> { +fn handle_eth_sub_event( + our: &Address, + state: &mut State, + event: EthSubEvent, +) -> anyhow::Result<()> { let EthSubEvent::Log(log) = event else { return Err(anyhow::anyhow!("app store: got non-log event")); }; - state.ingest_listings_contract_event(log) + state.ingest_listings_contract_event(our, log) } fn fetch_package_manifest(package: &PackageId) -> anyhow::Result> { diff --git a/kinode/packages/app_store/app_store/src/types.rs b/kinode/packages/app_store/app_store/src/types.rs index 3e9504f6c..c31bc1ad5 100644 --- a/kinode/packages/app_store/app_store/src/types.rs +++ b/kinode/packages/app_store/app_store/src/types.rs @@ -1,3 +1,4 @@ +use crate::LocalRequest; use alloy_rpc_types::Log; use alloy_sol_types::{sol, SolEvent}; use kinode_process_lib::kernel_types as kt; @@ -86,6 +87,7 @@ pub struct PackageState { pub our_version: String, pub installed: bool, pub caps_approved: bool, + pub manifest_hash: Option, /// are we serving this package to others? pub mirroring: bool, /// if we get a listing data update, will we try to download it? @@ -171,7 +173,7 @@ impl State { pub fn add_downloaded_package( &mut self, package_id: &PackageId, - package_state: PackageState, + mut package_state: PackageState, package_bytes: Option>, ) -> anyhow::Result<()> { if let Some(package_bytes) = package_bytes { @@ -215,6 +217,13 @@ impl State { })?) .blob(blob) .send_and_await_response(5)??; + + let manifest_file = vfs::File { + path: format!("/{}/pkg/manifest.json", package_id), + }; + let manifest_bytes = manifest_file.read()?; + let manifest_hash = generate_metadata_hash(&manifest_bytes); + package_state.manifest_hash = Some(manifest_hash); } self.downloaded_packages .insert(package_id.to_owned(), package_state); @@ -296,6 +305,10 @@ impl State { // generate entry from this data // for the version hash, take the SHA-256 hash of the zip file let our_version = generate_version_hash(&zip_file_bytes); + let manifest_file = vfs::File { + path: format!("/{}/pkg/manifest.json", package_id), + }; + let manifest_bytes = manifest_file.read()?; // the user will need to turn mirroring and auto-update back on if they // have to reset the state of their app store for some reason. the apps // themselves will remain on disk unless explicitly deleted. @@ -306,6 +319,7 @@ impl State { our_version, installed: true, caps_approved: true, // since it's already installed this must be true + manifest_hash: Some(generate_metadata_hash(&manifest_bytes)), mirroring: false, auto_update: false, metadata: None, @@ -362,7 +376,11 @@ impl State { } /// only saves state if last_saved_block is more than 1000 blocks behind - pub fn ingest_listings_contract_event(&mut self, log: Log) -> anyhow::Result<()> { + pub fn ingest_listings_contract_event( + &mut self, + our: &Address, + log: Log, + ) -> anyhow::Result<()> { let block_number: u64 = log .block_number .ok_or(anyhow::anyhow!("app store: got log with no block number"))? @@ -454,6 +472,33 @@ impl State { current_listing.metadata_hash = metadata_hash; current_listing.metadata = metadata; + + let package_id = PackageId::new(¤t_listing.name, ¤t_listing.publisher); + + // if we have this app installed, and we have auto_update set to true, + // we should try to download new version from the mirrored_from node + // and install it if successful. + if let Some(package_state) = self.downloaded_packages.get(&package_id) { + if package_state.auto_update { + if let Some(mirrored_from) = &package_state.mirrored_from { + crate::print_to_terminal( + 1, + &format!( + "app store: auto-updating package {package_id} from {mirrored_from}" + ), + ); + Request::to(our) + .body(serde_json::to_vec(&LocalRequest::Download { + package: package_id, + download_from: mirrored_from.clone(), + mirror: package_state.mirroring, + auto_update: package_state.auto_update, + desired_version_hash: None, + })?) + .send()?; + } + } + } } Transfer::SIGNATURE_HASH => { let from = alloy_primitives::Address::from_word(log.topics[1]);