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..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 @@ -235,10 +265,15 @@ 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() + .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() @@ -246,11 +281,15 @@ 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") + .unwrap_or(&json!(mirrors + .first() + .ok_or(anyhow::anyhow!("No mirrors for package {package_id}"))?)) + .as_str() + .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; let desired_version_hash = None; @@ -258,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 96bb5901f..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 @@ -407,7 +408,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 +423,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 { @@ -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]);