wip full new kit install downloads flow

This commit is contained in:
bitful-pannul 2024-08-13 03:32:52 +03:00
parent beeae033c9
commit 6753de511a
12 changed files with 476 additions and 130 deletions

View File

@ -11,6 +11,7 @@ interface downloads {
progress(progress-update), progress(progress-update),
size(size-update), size(size-update),
get-files(option<package-id>), get-files(option<package-id>),
add-download(add-download-request),
} }
record download-request { record download-request {
@ -45,6 +46,13 @@ interface downloads {
files: list<entry>, files: list<entry>,
} }
// part of new-package-request local-only flow.
record add-download-request {
package-id: package-id,
version-hash: string,
mirror: bool,
}
record progress-update { record progress-update {
package-id: package-id, package-id: package-id,
version-hash: string, version-hash: string,
@ -115,6 +123,13 @@ interface main {
use chain.{on-chain-metadata}; use chain.{on-chain-metadata};
// general error type used by both chain: and downloads:
// todo: add variant for types to match on
record error {
reason: string,
}
variant request { variant request {
local(local-request), local(local-request),
} }
@ -138,7 +153,7 @@ interface main {
variant local-request { variant local-request {
new-package(new-package-request), new-package(new-package-request),
install(package-id), install(install-package-request),
uninstall(package-id), uninstall(package-id),
apis, apis,
get-api(package-id), get-api(package-id),
@ -146,10 +161,15 @@ interface main {
record new-package-request { record new-package-request {
package-id: package-id, package-id: package-id,
metadata: on-chain-metadata,
mirror: bool, mirror: bool,
} }
record install-package-request {
package-id: package-id,
metadata: option<on-chain-metadata>, // if None == local sideload package.
version-hash: string,
}
variant local-response { variant local-response {
new-package-response(new-package-response), new-package-response(new-package-response),
install-response(install-response), install-response(install-response),

View File

@ -1,12 +1,14 @@
use crate::{ use crate::{
kinode::process::downloads::{DownloadRequest, DownloadResponse}, kinode::process::downloads::{DownloadRequest, DownloadResponse, Downloads},
kinode::process::main::Error,
state::{MirrorCheck, PackageState, State}, state::{MirrorCheck, PackageState, State},
Resp,
}; };
use kinode_process_lib::{ use kinode_process_lib::{
http::server, http::server,
http::{self, Method, StatusCode}, http::{self, Method, StatusCode},
Address, LazyLoadBlob, NodeId, PackageId, Request, Address, LazyLoadBlob, PackageId, Request,
}; };
use kinode_process_lib::{SendError, SendErrorKind}; use kinode_process_lib::{SendError, SendErrorKind};
use serde_json::json; use serde_json::json;
@ -206,9 +208,15 @@ pub fn handle_http_request(
bytes: body, bytes: body,
}), }),
), ),
Err(_e) => ( Err(e) => (
server::HttpResponse::new(http::StatusCode::INTERNAL_SERVER_ERROR), server::HttpResponse::new(http::StatusCode::INTERNAL_SERVER_ERROR),
None, Some(LazyLoadBlob {
mime: None,
bytes: serde_json::to_vec(&Error {
reason: e.to_string(),
})
.unwrap(),
}),
), ),
} }
} }
@ -222,6 +230,13 @@ fn get_package_id(url_params: &HashMap<String, String>) -> anyhow::Result<Packag
Ok(id) Ok(id)
} }
fn get_version_hash(url_params: &HashMap<String, String>) -> anyhow::Result<String> {
let Some(version_hash) = url_params.get("version_hash") else {
return Err(anyhow::anyhow!("Missing version_hash"));
};
Ok(version_hash.to_string())
}
fn gen_package_info(id: &PackageId, state: &PackageState) -> serde_json::Value { fn gen_package_info(id: &PackageId, state: &PackageState) -> serde_json::Value {
// installed package. // installed package.
// on-chain info combined somewhere else. // on-chain info combined somewhere else.
@ -265,20 +280,50 @@ fn serve_paths(
return Ok((StatusCode::OK, None, serde_json::to_vec(&all)?)); return Ok((StatusCode::OK, None, serde_json::to_vec(&all)?));
} }
"/downloads" => { "/downloads" => {
// TODO // get all local downloads!
Ok((StatusCode::OK, None, vec![])) let downloads = Address::from_str("our@downloads:app_store:sys")?;
let resp = Request::new()
.target(downloads)
.body(serde_json::to_vec(&Downloads::GetFiles(None))?)
.send_and_await_response(5)??;
let msg = serde_json::from_slice::<Resp>(resp.body())?;
println!("downlaods response: {:?}", msg);
// shouldn't really return status code
Ok((StatusCode::OK, None, resp.body().to_vec()))
} }
"/installed" => { "/installed" => {
// TODO // TODO format json for UI
Ok((StatusCode::OK, None, vec![])) println!("/installed response: {:?}", state.packages);
let body = serde_json::to_vec(&state.packages)?;
Ok((StatusCode::OK, None, body))
} }
"/ourapps" => { "/ourapps" => {
// TODO // TODO, fetch from state
Ok((StatusCode::OK, None, vec![])) Ok((StatusCode::OK, None, vec![]))
} }
"/downloads/:id" => { "/downloads/:id" => {
// TODO // get downloads for a specific app
Ok((StatusCode::OK, None, vec![])) let Ok(package_id) = get_package_id(url_params) else {
return Ok((
StatusCode::BAD_REQUEST,
None,
format!("Missing id").into_bytes(),
));
};
// fix
let package_id = crate::kinode::process::main::PackageId::from_process_lib(package_id);
let downloads = Address::from_str("our@downloads:app_store:sys")?;
let resp = Request::new()
.target(downloads)
.body(serde_json::to_vec(&Downloads::GetFiles(Some(package_id)))?)
.send_and_await_response(5)??;
let msg = serde_json::from_slice::<Resp>(resp.body())?;
println!("downlaods response: {:?}", msg);
// shouldn't really return status code
Ok((StatusCode::OK, None, resp.body().to_vec()))
} }
// GET detail about a specific app // GET detail about a specific app
// update a downloaded app: PUT // update a downloaded app: PUT
@ -367,7 +412,8 @@ fn serve_paths(
.body(serde_json::to_vec(&download_request).unwrap()) .body(serde_json::to_vec(&download_request).unwrap())
.send_and_await_response(5)??; .send_and_await_response(5)??;
let response: DownloadResponse = serde_json::from_slice(&response.body())?; let response: Resp = serde_json::from_slice(&response.body())?;
println!("got download response: {:?}", response);
Ok((StatusCode::OK, None, serde_json::to_vec(&response)?)) Ok((StatusCode::OK, None, serde_json::to_vec(&response)?))
} }
// POST /apps/:id/update // POST /apps/:id/update
@ -383,7 +429,7 @@ fn serve_paths(
// TODO send to downloads: // TODO send to downloads:
// // actually just install something, we go get it from vfs or ask for it from downloads? // // actually just install something, we go get it from vfs or ask for it from downloads?
// yeah this should be obsolete
match method { match method {
_ => Ok(( _ => Ok((
StatusCode::METHOD_NOT_ALLOWED, StatusCode::METHOD_NOT_ALLOWED,
@ -403,7 +449,24 @@ fn serve_paths(
)); ));
}; };
match crate::utils::install(&package_id, &our.to_string()) { let Ok(version_hash) = get_version_hash(url_params) else {
return Ok((
StatusCode::BAD_REQUEST,
None,
format!("Missing version_hash").into_bytes(),
));
};
let process_package_id =
crate::kinode::process::main::PackageId::from_process_lib(package_id);
match crate::utils::install(
&process_package_id,
None,
&version_hash,
state,
&our.to_string(),
) {
Ok(_) => Ok((StatusCode::CREATED, None, vec![])), Ok(_) => Ok((StatusCode::CREATED, None, vec![])),
Err(e) => Ok(( Err(e) => Ok((
StatusCode::SERVICE_UNAVAILABLE, StatusCode::SERVICE_UNAVAILABLE,

View File

@ -18,8 +18,8 @@
//! - set to automatically update if a new version is available //! - set to automatically update if a new version is available
use crate::kinode::process::downloads::{DownloadResponse, ProgressUpdate}; use crate::kinode::process::downloads::{DownloadResponse, ProgressUpdate};
use crate::kinode::process::main::{ use crate::kinode::process::main::{
ApisResponse, GetApiResponse, InstallResponse, LocalRequest, LocalResponse, NewPackageRequest, ApisResponse, Error, GetApiResponse, InstallPackageRequest, InstallResponse, LocalRequest,
NewPackageResponse, UninstallResponse, LocalResponse, NewPackageRequest, NewPackageResponse, UninstallResponse,
}; };
use kinode_process_lib::{ use kinode_process_lib::{
await_message, call_init, get_blob, http, println, vfs, Address, LazyLoadBlob, Message, await_message, call_init, get_blob, http, println, vfs, Address, LazyLoadBlob, Message,
@ -56,6 +56,7 @@ pub enum Req {
pub enum Resp { pub enum Resp {
LocalResponse(LocalResponse), LocalResponse(LocalResponse),
Download(DownloadResponse), Download(DownloadResponse),
Err(Error),
} }
call_init!(init); call_init!(init);
@ -118,6 +119,31 @@ fn handle_message(
}, },
); );
} }
Req::Progress(progress) => {
if !message.is_local(&our) {
return Err(anyhow::anyhow!("http_server from non-local node"));
}
println!("progress: {:?}", progress);
http_server.ws_push_all_channels(
"/",
http::server::WsMessageType::Text,
LazyLoadBlob {
mime: Some("application/json".to_string()),
bytes: serde_json::json!({
"kind": "progress",
"data": {
"package_id": progress.package_id,
"version_hash": progress.version_hash,
"downloaded": progress.downloaded,
"total": progress.total,
}
})
.to_string()
.as_bytes()
.to_vec(),
},
);
}
_ => {} _ => {}
} }
} else { } else {
@ -140,11 +166,8 @@ fn handle_local_request(
request: LocalRequest, request: LocalRequest,
) -> (LocalResponse, Option<LazyLoadBlob>) { ) -> (LocalResponse, Option<LazyLoadBlob>) {
match request { match request {
LocalRequest::NewPackage(NewPackageRequest { LocalRequest::NewPackage(NewPackageRequest { package_id, mirror }) => {
package_id, // note, use metadata and mirror?
metadata,
mirror,
}) => {
let Some(blob) = get_blob() else { let Some(blob) = get_blob() else {
return ( return (
LocalResponse::NewPackageResponse(NewPackageResponse::NoBlob), LocalResponse::NewPackageResponse(NewPackageResponse::NoBlob),
@ -152,16 +175,29 @@ fn handle_local_request(
); );
}; };
( (
match utils::new_package(&package_id.to_process_lib(), state, blob.bytes) { match utils::new_package(package_id, mirror, blob.bytes) {
Ok(()) => LocalResponse::NewPackageResponse(NewPackageResponse::Success), Ok(()) => LocalResponse::NewPackageResponse(NewPackageResponse::Success),
Err(_) => LocalResponse::NewPackageResponse(NewPackageResponse::InstallFailed), Err(_) => LocalResponse::NewPackageResponse(NewPackageResponse::InstallFailed),
}, },
None, None,
) )
} }
LocalRequest::Install(package_id) => ( LocalRequest::Install(InstallPackageRequest {
match utils::install(&package_id.to_process_lib(), &our.to_string()) { package_id,
Ok(()) => LocalResponse::InstallResponse(InstallResponse::Success), metadata,
version_hash,
}) => (
match utils::install(
&package_id,
metadata,
&version_hash,
state,
&our.to_string(),
) {
Ok(()) => {
println!("successfully installed package:");
LocalResponse::InstallResponse(InstallResponse::Success)
}
Err(e) => { Err(e) => {
println!("error installing package: {e}"); println!("error installing package: {e}");
LocalResponse::InstallResponse(InstallResponse::Failure) LocalResponse::InstallResponse(InstallResponse::Failure)

View File

@ -77,31 +77,6 @@ impl State {
Ok(state) Ok(state)
} }
/// if package_bytes is None, we already have the package downloaded
/// in VFS and this is being called to rebuild our process state
pub fn add_downloaded_package(
&mut self,
package_id: &PackageId,
mut package_state: PackageState,
package_zip_bytes: Option<Vec<u8>>,
) -> anyhow::Result<()> {
let Some(listing) = self.packages.get_mut(package_id) else {
return Err(anyhow::anyhow!("package not found"));
};
// if passed zip bytes, make drive
if let Some(package_bytes) = package_zip_bytes {
let manifest_hash = utils::create_package_drive(package_id, package_bytes)?;
package_state.manifest_hash = Some(manifest_hash);
}
if let Ok(extracted) = utils::extract_api(package_id) {
if extracted {
self.installed_apis.insert(package_id.to_owned());
}
}
Ok(())
}
// /// returns True if the package was found and updated, False otherwise // /// returns True if the package was found and updated, False otherwise
// pub fn update_downloaded_package( // pub fn update_downloaded_package(
// &mut self, // &mut self,

View File

@ -1,11 +1,17 @@
use { use {
crate::state::{PackageState, State}, crate::{
crate::VFS_TIMEOUT, kinode::process::{
chain::{Chains, GetAppResponse, OnChainMetadata},
downloads::{AddDownloadRequest, DownloadResponse, Downloads},
},
state::{PackageState, State},
VFS_TIMEOUT,
},
kinode_process_lib::{ kinode_process_lib::{
get_blob, kernel_types as kt, println, vfs, Address, LazyLoadBlob, PackageId, ProcessId, get_blob, kernel_types as kt, println, vfs, Address, LazyLoadBlob, PackageId, ProcessId,
Request, Request,
}, },
std::collections::HashSet, std::{collections::HashSet, str::FromStr},
}; };
// quite annoyingly, we must convert from our gen'd version of PackageId // quite annoyingly, we must convert from our gen'd version of PackageId
@ -67,38 +73,66 @@ pub fn fetch_package_manifest(
)?) )?)
} }
pub fn fetch_package_metadata(package_id: &PackageId) -> anyhow::Result<kt::Erc721Metadata> { pub fn fetch_package_metadata(
vfs_request( package_id: &crate::kinode::process::main::PackageId,
format!("/{package_id}/pkg/metadata.json"), // OK BIG todo: figure out why tf this is not here... ) -> anyhow::Result<OnChainMetadata> {
vfs::VfsAction::Read, let chain = Address::from_str("our@chain:app_store:sys")?;
) let resp = Request::new()
.send_and_await_response(VFS_TIMEOUT)??; .target(chain)
let Some(blob) = get_blob() else { .body(serde_json::to_vec(&Chains::GetApp(package_id.clone())).unwrap())
return Err(anyhow::anyhow!("no blob")); .send_and_await_response(5)??;
let resp = serde_json::from_slice::<GetAppResponse>(&resp.body())?;
let app = match resp.app {
Some(app) => app,
None => {
return Err(anyhow::anyhow!(
"No app data found in response from chain:app_store:sys"
))
}
}; };
Ok(serde_json::from_slice::<kt::Erc721Metadata>(&blob.bytes)?) let metadata = match app.metadata {
Some(metadata) => metadata,
None => {
return Err(anyhow::anyhow!(
"No metadata found in response from chain:app_store:sys"
))
}
};
Ok(metadata)
} }
pub fn new_package( pub fn new_package(
package_id: &PackageId, package_id: crate::kinode::process::main::PackageId,
state: &mut State, mirror: bool,
bytes: Vec<u8>, bytes: Vec<u8>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
// add to listings
// TODO: add? to uhh listings at all?
// state.insert_package(package_id, metadata);
// set the version hash for this new local package // set the version hash for this new local package
let our_version_hash = sha_256_hash(&bytes); let version_hash = sha_256_hash(&bytes);
let package_state = PackageState { let downloads = Address::from_str("our@downloads:app_store:sys")?;
our_version_hash, let resp = Request::new()
verified: true, // sideloaded apps are implicitly verified because there is no "source" to verify against .target(downloads)
caps_approved: true, // TODO see if we want to auto-approve local installs .body(serde_json::to_vec(&Downloads::AddDownload(
manifest_hash: None, // generated in the add fn AddDownloadRequest {
package_id: package_id.clone(),
version_hash: version_hash.clone(),
mirror,
},
))?)
.blob_bytes(bytes)
.send_and_await_response(5)??;
let download_resp = serde_json::from_slice::<DownloadResponse>(&resp.body())?;
println!("got download resp: {:?}", download_resp);
if !download_resp.success {
return Err(anyhow::anyhow!(
"failed to add download: {:?}",
download_resp.error
));
}; };
state.add_downloaded_package(&package_id, package_state, Some(bytes))
Ok(())
} }
/// create a new package drive in VFS and add the package zip to it. /// create a new package drive in VFS and add the package zip to it.
@ -138,6 +172,7 @@ pub fn create_package_drive(
)); ));
}; };
// be careful, this is technically a duplicate.. but..
// save the zip file itself in VFS for sharing with other nodes // save the zip file itself in VFS for sharing with other nodes
// call it <package_id>.zip // call it <package_id>.zip
let zip_path = format!("{}/{}.zip", drive_name, package_id); let zip_path = format!("{}/{}.zip", drive_name, package_id);
@ -174,7 +209,8 @@ pub fn extract_api(package_id: &PackageId) -> anyhow::Result<bool> {
Ok(false) Ok(false)
} }
/// given a `PackageId`, interact with VFS and kernel to get manifest, /// given a `PackageId`, interact with VFS and kernel to get {package_hash}.zip,
/// unzip the manifest and pkg,
/// grant the capabilities in manifest, then initialize and start /// grant the capabilities in manifest, then initialize and start
/// the processes in manifest. /// the processes in manifest.
/// ///
@ -182,13 +218,49 @@ pub fn extract_api(package_id: &PackageId) -> anyhow::Result<bool> {
/// which we can only do if we were the process to create that drive. /// which we can only do if we were the process to create that drive.
/// note also that each capability will only be granted if we, the process /// note also that each capability will only be granted if we, the process
/// using this function, own that capability ourselves. /// using this function, own that capability ourselves.
pub fn install(package_id: &PackageId, our_node: &str) -> anyhow::Result<()> { pub fn install(
// get the package manifest package_id: &crate::kinode::process::main::PackageId,
let drive_path = format!("/{package_id}/pkg"); metadata: Option<OnChainMetadata>,
let manifest = fetch_package_manifest(package_id)?; version_hash: &str,
state: &mut State,
our_node: &str,
) -> anyhow::Result<()> {
let process_package_id = package_id.clone().to_process_lib();
let file = vfs::open_file(
&format!("/app_store:sys/downloads/{process_package_id}/{version_hash}.zip"),
false,
Some(VFS_TIMEOUT),
)?;
let bytes = file.read()?;
let manifest_hash = create_package_drive(&process_package_id, bytes)?;
let package_state = PackageState {
our_version_hash: version_hash.to_string(),
verified: true, // sideloaded apps are implicitly verified because there is no "source" to verify against
caps_approved: true, // TODO see if we want to auto-approve local installs
manifest_hash: Some(manifest_hash),
};
if let Ok(extracted) = extract_api(&process_package_id) {
if extracted {
state.installed_apis.insert(process_package_id.clone());
}
}
state
.packages
.insert(process_package_id.clone(), package_state);
// get the package manifest
let drive_path = format!("/{process_package_id}/pkg");
let manifest = fetch_package_manifest(&process_package_id)?;
// get wit version from metadata if local or chain if remote.
let metadata = if let Some(metadata) = metadata {
metadata
} else {
fetch_package_metadata(&package_id)?
};
// get wit version from metadata:
let metadata = fetch_package_metadata(package_id)?;
let wit_version = metadata.properties.wit_version; let wit_version = metadata.properties.wit_version;
// first, for each process in manifest, initialize it // first, for each process in manifest, initialize it
@ -201,11 +273,11 @@ pub fn install(package_id: &PackageId, our_node: &str) -> anyhow::Result<()> {
format!("/{}", entry.process_wasm_path) format!("/{}", entry.process_wasm_path)
}; };
let wasm_path = format!("{}{}", drive_path, wasm_path); let wasm_path = format!("{}{}", drive_path, wasm_path);
println!("wasm path: {wasm_path}");
let process_id = ProcessId::new( let process_id = ProcessId::new(
Some(&entry.process_name), Some(&entry.process_name),
package_id.package(), process_package_id.package(),
package_id.publisher(), process_package_id.publisher(),
); );
// kill process if it already exists // kill process if it already exists
@ -236,10 +308,11 @@ pub fn install(package_id: &PackageId, our_node: &str) -> anyhow::Result<()> {
) else { ) else {
return Err(anyhow::anyhow!("failed to initialize process")); return Err(anyhow::anyhow!("failed to initialize process"));
}; };
// println!("kernel process gave us something back.");
// build initial caps from manifest // // build initial caps from manifest
let mut requested_capabilities = parse_capabilities(our_node, &entry.request_capabilities); let mut requested_capabilities = parse_capabilities(our_node, &entry.request_capabilities);
println!("parsed caps.");
if entry.request_networking { if entry.request_networking {
requested_capabilities.push(kt::Capability { requested_capabilities.push(kt::Capability {
issuer: Address::new(our_node, ("kernel", "distro", "sys")), issuer: Address::new(our_node, ("kernel", "distro", "sys")),
@ -247,6 +320,8 @@ pub fn install(package_id: &PackageId, our_node: &str) -> anyhow::Result<()> {
}); });
} }
println!("requested caps: {:?}", requested_capabilities);
// always grant read/write to their drive, which we created for them // always grant read/write to their drive, which we created for them
requested_capabilities.push(kt::Capability { requested_capabilities.push(kt::Capability {
issuer: Address::new(our_node, ("vfs", "distro", "sys")), issuer: Address::new(our_node, ("vfs", "distro", "sys")),
@ -265,6 +340,7 @@ pub fn install(package_id: &PackageId, our_node: &str) -> anyhow::Result<()> {
.to_string(), .to_string(),
}); });
// NOTE.. this crashes...
kernel_request(kt::KernelCommand::GrantCapabilities { kernel_request(kt::KernelCommand::GrantCapabilities {
target: process_id.clone(), target: process_id.clone(),
capabilities: requested_capabilities, capabilities: requested_capabilities,
@ -278,8 +354,8 @@ pub fn install(package_id: &PackageId, our_node: &str) -> anyhow::Result<()> {
for entry in &manifest { for entry in &manifest {
let process_id = ProcessId::new( let process_id = ProcessId::new(
Some(&entry.process_name), Some(&entry.process_name),
package_id.package(), process_package_id.package(),
package_id.publisher(), process_package_id.publisher(),
); );
for value in &entry.grant_capabilities { for value in &entry.grant_capabilities {
@ -340,6 +416,7 @@ pub fn install(package_id: &PackageId, our_node: &str) -> anyhow::Result<()> {
) else { ) else {
return Err(anyhow::anyhow!("failed to start process")); return Err(anyhow::anyhow!("failed to start process"));
}; };
println!("started the process!");
} }
Ok(()) Ok(())
} }

View File

@ -8,6 +8,7 @@ use std::{collections::HashMap, str::FromStr};
use crate::kinode::process::chain::{ use crate::kinode::process::chain::{
Chains, GetAppResponse, OnChainApp, OnChainMetadata, OnChainProperties, Chains, GetAppResponse, OnChainApp, OnChainMetadata, OnChainProperties,
}; };
use crate::kinode::process::main::Error;
use alloy_primitives::keccak256; use alloy_primitives::keccak256;
use alloy_sol_types::SolEvent; use alloy_sol_types::SolEvent;
use kinode_process_lib::{ use kinode_process_lib::{
@ -93,6 +94,14 @@ fn init(our: Address) {
Ok(message) => { Ok(message) => {
if let Err(e) = handle_message(&our, &mut state, &message) { if let Err(e) = handle_message(&our, &mut state, &message) {
println!("error handling message: {:?}", e); println!("error handling message: {:?}", e);
let _ = Response::new()
.body(
serde_json::to_vec(&Error {
reason: e.to_string(),
})
.unwrap(),
)
.send();
} }
} }
} }

View File

@ -3,19 +3,21 @@
//! manages downloading and sharing of versioned packages. //! manages downloading and sharing of versioned packages.
//! //!
use crate::kinode::process::downloads::{ use crate::kinode::process::downloads::{
AvailableFiles, DownloadRequest, DownloadResponse, Downloads, Entry, ProgressUpdate, AddDownloadRequest, AvailableFiles, DownloadRequest, DownloadResponse, Downloads, Entry,
ProgressUpdate,
}; };
use crate::kinode::process::main::Error;
use std::{collections::HashSet, str::FromStr}; use std::{collections::HashSet, str::FromStr};
use ft_worker_lib::{spawn_receive_transfer, spawn_send_transfer}; use ft_worker_lib::{spawn_receive_transfer, spawn_send_transfer};
use kinode::process::standard::print_to_terminal; use kinode_process_lib::get_blob;
use kinode_process_lib::{ use kinode_process_lib::{
await_message, call_init, await_message, call_init,
http::{ http::{
self, self,
client::{HttpClientError, HttpClientResponse}, client::{HttpClientError, HttpClientResponse},
}, },
println, print_to_terminal, println,
vfs::{self, DirEntry, Directory, File, SeekFrom}, vfs::{self, DirEntry, Directory, File, SeekFrom},
Address, Message, PackageId, ProcessId, Request, Response, Address, Message, PackageId, ProcessId, Request, Response,
}; };
@ -56,23 +58,28 @@ fn init(our: Address) {
mirroring: HashSet::new(), mirroring: HashSet::new(),
}; };
// should the files be actively in state? probably not due to updates? // /log/.log file for debugs
// just install the downloads directory. vfs::create_drive(our.package_id(), "log", None).expect("could not create /log drive");
// /downloads/ for downloads
vfs::create_drive(our.package_id(), "downloads", None)
.expect("could not create /downloads drive");
let mut logfile = let mut logfile =
vfs::open_file("/app_store:sys/.log", true, Some(5)).expect("could not open logfile"); vfs::open_file("/app_store:sys/log/.log", true, Some(5)).expect("could not open logfile");
logfile logfile
.seek(SeekFrom::End(0)) .seek(SeekFrom::End(0))
.expect("could not seek to end of logfile"); .expect("could not seek to end of logfile");
// FIX this api... first creates (fails if already exists. second shouldn't fail like this... like files.)
let _ = vfs::open_dir("/app_store:sys/downloads", true, None);
let mut downloads = let mut downloads =
vfs::open_dir("/app_store:sys/downloads", true, None).expect("could not open downloads"); vfs::open_dir("/app_store:sys/downloads", false, None).expect("could not open downloads");
let mut tmp = vfs::open_dir("/tmp", true, None).expect("could not open tmp"); let _ = vfs::open_dir("/app_store:sys/downloads/tmp", true, None);
let mut tmp =
vfs::open_dir("/app_store:sys/downloads/tmp", false, None).expect("could not open tmp");
loop { loop {
match await_message() { match await_message() {
Err(send_error) => { Err(send_error) => {
// TODO handle these based on what they are triggered by
println!("got network error: {send_error}"); println!("got network error: {send_error}");
} }
Ok(message) => { Ok(message) => {
@ -85,6 +92,14 @@ fn init(our: Address) {
&mut tmp, &mut tmp,
) { ) {
println!("error handling message: {:?}", e); println!("error handling message: {:?}", e);
let _ = Response::new()
.body(
serde_json::to_vec(&Error {
reason: e.to_string(),
})
.unwrap(),
)
.send();
} }
} }
} }
@ -150,25 +165,6 @@ fn handle_message(
.unwrap(), .unwrap(),
) )
.send(); .send();
// http_server.ws_push_all_channels(
// "/",
// http::server::WsMessageType::Text,
// LazyLoadBlob {
// mime: Some("application/json".to_string()),
// bytes: serde_json::json!({
// "kind": "progress",
// "data": {
// "file_name": file_name,
// "chunks_received": chunks_received,
// "total_chunks": total_chunks,
// }
// })
// .to_string()
// .as_bytes()
// .to_vec(),
// },
// );
} }
Downloads::GetFiles(maybe_id) => { Downloads::GetFiles(maybe_id) => {
// if not local, throw to the boonies. (could also implement and discovery protocol here..) // if not local, throw to the boonies. (could also implement and discovery protocol here..)
@ -194,6 +190,35 @@ fn handle_message(
.body(serde_json::to_vec(&AvailableFiles { files })?) .body(serde_json::to_vec(&AvailableFiles { files })?)
.send()?; .send()?;
} }
Downloads::AddDownload(add_req) => {
if !message.is_local(our) {
// todo figure out full error throwing for http pathways.
return Err(anyhow::anyhow!("not local"));
}
let Some(blob) = get_blob() else {
return Err(anyhow::anyhow!("could not get blob"));
};
let bytes = blob.bytes;
let package_dir = format!(
"{}/{}",
downloads.path,
add_req.package_id.to_process_lib().to_string()
);
let _ = vfs::open_dir(&package_dir, true, None);
let file = vfs::create_file(
&format!("{}/{}.zip", package_dir, add_req.version_hash),
None,
)?;
let _ = file.write(bytes.as_slice())?;
Response::new()
.body(serde_json::to_vec(&Resp::Download(DownloadResponse {
success: true,
error: None,
}))?)
.send()?;
}
_ => {} _ => {}
} }
} else { } else {

View File

@ -34,13 +34,13 @@ fn init(our: Address) {
let Ok(Ok(Message::Response { body, .. })) = let Ok(Ok(Message::Response { body, .. })) =
Request::to((our.node(), ("main", "app_store", "sys"))) Request::to((our.node(), ("main", "app_store", "sys")))
.body( .body(
serde_json::to_vec(&LocalRequest::Install( b"temp", // serde_json::to_vec(&LocalRequest::Install(
crate::kinode::process::main::PackageId { // crate::kinode::process::main::PackageId {
package_name: package_id.package_name.clone(), // package_name: package_id.package_name.clone(),
publisher_node: package_id.publisher_node.clone(), // publisher_node: package_id.publisher_node.clone(),
}, // },
)) // ))
.unwrap(), // .unwrap(),
) )
.send_and_await_response(5) .send_and_await_response(5)
else { else {

View File

@ -7,6 +7,8 @@
"request_capabilities": [ "request_capabilities": [
"http_client:distro:sys", "http_client:distro:sys",
"http_server:distro:sys", "http_server:distro:sys",
"main:app_store:sys",
"chain:app_store:sys",
"vfs:distro:sys", "vfs:distro:sys",
{ {
"process": "vfs:distro:sys", "process": "vfs:distro:sys",
@ -22,6 +24,35 @@
], ],
"public": false "public": false
}, },
{
"process_name": "chain",
"process_wasm_path": "/chain.wasm",
"on_exit": "Restart",
"request_networking": true,
"request_capabilities": [
"main:app_store:sys",
"downloads:app_store:sys",
"vfs:distro:sys",
"kns_indexer:kns_indexer:sys",
"eth:distro:sys",
"http_server:distro:sys",
"http_client:distro:sys",
{
"process": "vfs:distro:sys",
"params": {
"root": true
}
}
],
"grant_capabilities": [
"http_server:distro:sys",
"kns_indexer:kns_indexer:sys",
"vfs:distro:sys",
"http_client:distro:sys",
"eth:distro:sys"
],
"public": false
},
{ {
"process_name": "main", "process_name": "main",
"process_wasm_path": "/app_store.wasm", "process_wasm_path": "/app_store.wasm",
@ -34,6 +65,8 @@
"http_server:distro:sys", "http_server:distro:sys",
"http_client:distro:sys", "http_client:distro:sys",
"net:distro:sys", "net:distro:sys",
"downloads:app_store:sys",
"chain:app_store:sys",
"vfs:distro:sys", "vfs:distro:sys",
"kernel:distro:sys", "kernel:distro:sys",
"eth:distro:sys", "eth:distro:sys",
@ -59,6 +92,7 @@
"net:distro:sys", "net:distro:sys",
"http_client:distro:sys", "http_client:distro:sys",
"http_server:distro:sys", "http_server:distro:sys",
"kernel:distro:sys",
"kns_indexer:kns_indexer:sys", "kns_indexer:kns_indexer:sys",
"terminal:terminal:sys", "terminal:terminal:sys",
"vfs:distro:sys" "vfs:distro:sys"

View File

@ -1,11 +1,13 @@
import React from "react"; import React from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import Header from "./components/Header";
import { APP_DETAILS_PATH, PUBLISH_PATH, STORE_PATH } from "./constants/path";
import StorePage from "./pages/StorePage"; import StorePage from "./pages/StorePage";
import AppPage from "./pages/AppPage"; import AppPage from "./pages/AppPage";
import PublishPage from "./pages/PublishPage"; import PublishPage from "./pages/PublishPage";
import Header from "./components/Header"; import Testing from "./pages/Testing";
import { APP_DETAILS_PATH, PUBLISH_PATH, STORE_PATH } from "./constants/path";
const BASE_URL = import.meta.env.BASE_URL; const BASE_URL = import.meta.env.BASE_URL;
@ -18,6 +20,7 @@ function App() {
<Router basename={BASE_URL}> <Router basename={BASE_URL}>
<Header /> <Header />
<Routes> <Routes>
<Route path="/testing" element={<Testing />} />
<Route path={STORE_PATH} element={<StorePage />} /> <Route path={STORE_PATH} element={<StorePage />} />
<Route path={`${APP_DETAILS_PATH}/:id`} element={<AppPage />} /> <Route path={`${APP_DETAILS_PATH}/:id`} element={<AppPage />} />
<Route path={PUBLISH_PATH} element={<PublishPage />} /> <Route path={PUBLISH_PATH} element={<PublishPage />} />

View File

@ -0,0 +1,42 @@
import React, { useState } from 'react'
import useAppsStore from '../store'
const Testing: React.FC = () => {
const { getDownloads, getInstalledApps, getOurApps, getDownloadsForApp } = useAppsStore()
const [result, setResult] = useState<any>(null)
const [appId, setAppId] = useState('')
const handleAction = async (action: () => Promise<any>) => {
try {
const data = await action()
setResult(JSON.stringify(data, null, 2))
} catch (error) {
setResult(`Error: ${error.message}`)
}
}
return (
<div>
<h1>Testing Page</h1>
<div>
<button onClick={() => handleAction(getDownloads)}>Get Downloads</button>
<button onClick={() => handleAction(getInstalledApps)}>Get Installed Apps</button>
<button onClick={() => handleAction(getOurApps)}>Get Our Apps</button>
</div>
<div>
<input
type="text"
value={appId}
onChange={(e) => setAppId(e.target.value)}
placeholder="Enter App ID"
/>
<button onClick={() => handleAction(() => getDownloadsForApp(appId))}>
Get Downloads for App
</button>
</div>
<pre>{result}</pre>
</div>
)
}
export default Testing

View File

@ -24,6 +24,12 @@ interface AppsStore {
setMirroring: (app: AppInfo, mirroring: boolean) => Promise<void> setMirroring: (app: AppInfo, mirroring: boolean) => Promise<void>
setAutoUpdate: (app: AppInfo, autoUpdate: boolean) => Promise<void> setAutoUpdate: (app: AppInfo, autoUpdate: boolean) => Promise<void>
rebuildIndex: () => Promise<void> rebuildIndex: () => Promise<void>
// new.
getDownloads: () => Promise<any>
getInstalledApps: () => Promise<any>
getOurApps: () => Promise<any>
getDownloadsForApp: (id: string) => Promise<any>
} }
const useAppsStore = create<AppsStore>()( const useAppsStore = create<AppsStore>()(
@ -166,6 +172,62 @@ const useAppsStore = create<AppsStore>()(
throw new Error('Failed to rebuild index') throw new Error('Failed to rebuild index')
} }
}, },
getDownloads: async () => {
try {
const res = await fetch(`${BASE_URL}/downloads`)
const data = await res.json()
if (!res.ok) {
console.error('Error fetching downloads:', data)
}
return data
} catch (error) {
console.error('Error fetching downloads:', error)
return { error: 'Failed to get downloads' }
}
},
getInstalledApps: async () => {
try {
const res = await fetch(`${BASE_URL}/installed`)
const data = await res.json()
if (!res.ok) {
console.error('Error fetching installed apps:', data)
}
return data
} catch (error) {
console.error('Error fetching installed apps:', error)
return { error: 'Failed to get installed apps' }
}
},
getOurApps: async () => {
try {
const res = await fetch(`${BASE_URL}/ourapps`)
const data = await res.json()
if (!res.ok) {
console.error('Error fetching our apps:', data)
}
return data
} catch (error) {
console.error('Error fetching our apps:', error)
return { error: 'Failed to get our apps' }
}
},
getDownloadsForApp: async (id: string) => {
try {
const res = await fetch(`${BASE_URL}/downloads/${id}`)
const data = await res.json()
if (!res.ok) {
console.error(`Error fetching downloads for app ${id}:`, data)
}
return data
} catch (error) {
console.error(`Error fetching downloads for app ${id}:`, error)
return { error: `Failed to get downloads for app: ${id}` }
}
},
}), }),
{ {
name: 'app_store', name: 'app_store',