diff --git a/Cargo.lock b/Cargo.lock index 9a43b061..1b2c02ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1294,9 +1294,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.11" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb8dd288a69fc53a1996d7ecfbf4a20d59065bff137ce7e56bbd620de191189" +checksum = "68064e60dbf1f17005c2fde4d07c16d8baa506fd7ffed8ccab702d93617975c7" dependencies = [ "jobserver", "libc", @@ -1462,9 +1462,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.50" +version = "0.1.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" dependencies = [ "cc", ] @@ -5138,9 +5138,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.124" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d" +checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" dependencies = [ "itoa", "memchr", 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 da59b16d..2e7e87b0 100644 --- a/kinode/packages/app_store/app_store/src/http_api.rs +++ b/kinode/packages/app_store/app_store/src/http_api.rs @@ -566,19 +566,49 @@ fn serve_paths( )); }; - // TODO move to downloads. + let downloads = Address::from_str("downloads@downloads:app_store:sys")?; match method { // start mirroring an app - // Method::PUT => { - // state.start_mirroring(&package_id); - // Ok((StatusCode::OK, None, vec![])) - // } - // // stop mirroring an app - // Method::DELETE => { - // state.stop_mirroring(&package_id); - // Ok((StatusCode::OK, None, vec![])) - // } + Method::PUT => { + let resp = Request::new() + .target(downloads) + .body(serde_json::to_vec(&DownloadRequests::StartMirroring( + crate::kinode::process::main::PackageId::from_process_lib(package_id), + ))?) + .send_and_await_response(5)??; + let msg = serde_json::from_slice::(resp.body())?; + match msg { + DownloadResponses::Success => Ok((StatusCode::OK, None, vec![])), + DownloadResponses::Error(e) => { + Err(anyhow::anyhow!("Error starting mirroring: {:?}", e)) + } + _ => Err(anyhow::anyhow!( + "Invalid response from downloads: {:?}", + msg + )), + } + } + // stop mirroring an app + Method::DELETE => { + let resp = Request::new() + .target(downloads) + .body(serde_json::to_vec(&DownloadRequests::StopMirroring( + crate::kinode::process::main::PackageId::from_process_lib(package_id), + ))?) + .send_and_await_response(5)??; + let msg = serde_json::from_slice::(resp.body())?; + match msg { + DownloadResponses::Success => Ok((StatusCode::OK, None, vec![])), + DownloadResponses::Error(e) => { + Err(anyhow::anyhow!("Error stopping mirroring: {:?}", e)) + } + _ => Err(anyhow::anyhow!( + "Invalid response from downloads: {:?}", + msg + )), + } + } _ => Ok(( StatusCode::METHOD_NOT_ALLOWED, None, diff --git a/kinode/packages/app_store/downloads/Cargo.toml b/kinode/packages/app_store/downloads/Cargo.toml index c85abde3..83961220 100644 --- a/kinode/packages/app_store/downloads/Cargo.toml +++ b/kinode/packages/app_store/downloads/Cargo.toml @@ -17,7 +17,7 @@ sha3 = "0.10.8" url = "2.4.1" urlencoding = "2.1.0" wit-bindgen = "0.24.0" -zip = { version = "1.1.4", default-features = false } +zip = { version = "1.1.4", default-features = false, features = ["deflate"] } [lib] crate-type = ["cdylib"] diff --git a/kinode/packages/app_store/downloads/src/lib.rs b/kinode/packages/app_store/downloads/src/lib.rs index 56e634fd..ec00fb9d 100644 --- a/kinode/packages/app_store/downloads/src/lib.rs +++ b/kinode/packages/app_store/downloads/src/lib.rs @@ -3,8 +3,8 @@ //! manages downloading and sharing of versioned packages. //! use crate::kinode::process::downloads::{ - DirEntry, DownloadError, DownloadRequests, DownloadResponses, Entry, FileEntry, - LocalDownloadRequest, ProgressUpdate, RemoteDownloadRequest, + DirEntry, DownloadRequests, DownloadResponses, Entry, FileEntry, LocalDownloadRequest, + ProgressUpdate, RemoteDownloadRequest, }; use std::{collections::HashSet, io::Read, str::FromStr}; @@ -15,7 +15,7 @@ use kinode_process_lib::{ self, client::{HttpClientError, HttpClientResponse}, }, - print_to_terminal, println, + print_to_terminal, println, set_state, vfs::{self, Directory, File}, Address, Message, PackageId, ProcessId, Request, Response, }; @@ -214,9 +214,9 @@ fn handle_message( } }; - Response::new() - .body(serde_json::to_string(&files)?) - .send()?; + let resp = DownloadResponses::GetFiles(files); + + Response::new().body(serde_json::to_string(&resp)?).send()?; } DownloadRequests::AddDownload(add_req) => { if !message.is_local(our) { @@ -230,7 +230,7 @@ fn handle_message( let package_dir = format!( "{}/{}", downloads.path, - add_req.package_id.to_process_lib().to_string() + add_req.package_id.clone().to_process_lib().to_string() ); let _ = vfs::open_dir(&package_dir, true, None); @@ -244,6 +244,12 @@ fn handle_message( let manifest_path = format!("{}/{}.json", package_dir, add_req.version_hash); extract_and_write_manifest(&bytes, &manifest_path)?; + // add mirrors if applicable and save: + if add_req.mirror { + state.mirroring.insert(add_req.package_id.to_process_lib()); + set_state(&serde_json::to_vec(&state)?); + } + Response::new() .body(serde_json::to_vec(&Resp::Download( DownloadResponses::Success, @@ -253,6 +259,7 @@ fn handle_message( DownloadRequests::StartMirroring(package_id) => { let package_id = package_id.to_process_lib(); state.mirroring.insert(package_id); + set_state(&serde_json::to_vec(&state)?); Response::new() .body(serde_json::to_vec(&Resp::Download( DownloadResponses::Success, @@ -262,6 +269,7 @@ fn handle_message( DownloadRequests::StopMirroring(package_id) => { let package_id = package_id.to_process_lib(); state.mirroring.remove(&package_id); + set_state(&serde_json::to_vec(&state)?); Response::new() .body(serde_json::to_vec(&Resp::Download( DownloadResponses::Success, @@ -337,41 +345,31 @@ fn format_entries(entries: Vec, state: &State) -> Vec { entries .into_iter() .filter_map(|entry| { - let name = entry - .path - .rsplit('/') - .next() - .unwrap_or_default() - .to_string(); + let name = entry.path.split('/').last()?.to_string(); let is_file = entry.file_type == vfs::FileType::File; - if is_file { - if name.ends_with(".zip") { - let size = vfs::metadata(&entry.path, None) - .ok() - .map(|meta| meta.len) - .unwrap_or(0); - let json_path = entry.path.replace(".zip", ".json"); - let manifest = if let Ok(file) = vfs::open_file(&json_path, false, None) { - file.read_to_string().ok() - } else { - None - }; - - Some(Entry::File(FileEntry { - name, - size, - manifest: manifest.unwrap_or_default(), - })) - } else { - None // ignore non-zip files - } - } else { - let mirroring = PackageId::from_str(&name) - .map(|package_id| state.mirroring.contains(&package_id)) - .unwrap_or(false); + if is_file && name.ends_with(".zip") { + let size = vfs::metadata(&entry.path, None) + .map(|meta| meta.len) + .unwrap_or(0); + let json_path = entry.path.replace(".zip", ".json"); + let manifest = vfs::open_file(&json_path, false, None) + .and_then(|file| file.read_to_string()) + .unwrap_or_default(); + Some(Entry::File(FileEntry { + name, + size, + manifest, + })) + } else if !is_file { + let mirroring = state.mirroring.iter().any(|pid| { + pid.package_name == name + || format!("{}:{}", pid.package_name, pid.publisher_node) == name + }); Some(Entry::Dir(DirEntry { name, mirroring })) + } else { + None // Skip non-zip files } }) .collect() @@ -398,14 +396,6 @@ fn extract_and_write_manifest(file_contents: &[u8], manifest_path: &str) -> anyh Ok(()) } -// note this, might be tricky: -// when ready, extract + write the damn manifest to a file location baby! - -// let wit_version = match metadata { -// Some(metadata) => metadata.properties.wit_version, -// None => Some(0), -// }; - /// helper function for vfs files, open if exists, if not create fn open_or_create_file(path: &str) -> anyhow::Result { match vfs::open_file(path, false, None) { diff --git a/kinode/packages/app_store/ft_worker/Cargo.toml b/kinode/packages/app_store/ft_worker/Cargo.toml index 4bae1305..8b9609a8 100644 --- a/kinode/packages/app_store/ft_worker/Cargo.toml +++ b/kinode/packages/app_store/ft_worker/Cargo.toml @@ -15,7 +15,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha2 = "0.10.8" wit-bindgen = "0.24.0" -zip = { version = "1.1.4", default-features = false } +zip = { version = "1.1.4", default-features = false, features = ["deflate"] } [lib] crate-type = ["cdylib"] diff --git a/kinode/packages/app_store/ui/src/abis/index.ts b/kinode/packages/app_store/ui/src/abis/index.ts index 0c826ea8..dea088c8 100644 --- a/kinode/packages/app_store/ui/src/abis/index.ts +++ b/kinode/packages/app_store/ui/src/abis/index.ts @@ -2,7 +2,7 @@ import { parseAbi } from "viem"; export { encodeMulticalls, encodeIntoMintCall } from "./helpers"; -export const KIMAP: `0x${string}` = "0xAfA2e57D3cBA08169b416457C14eBA2D6021c4b5"; +export const KIMAP: `0x${string}` = "0xcA92476B2483aBD5D82AEBF0b56701Bb2e9be658"; export const MULTICALL: `0x${string}` = "0xcA11bde05977b3631167028862bE2a173976CA11"; export const KINO_ACCOUNT_IMPL: `0x${string}` = "0x38766C70a4FB2f23137D9251a1aA12b1143fC716"; diff --git a/kinode/packages/app_store/ui/src/pages/AppPage.tsx b/kinode/packages/app_store/ui/src/pages/AppPage.tsx index 5681af50..7ec357b2 100644 --- a/kinode/packages/app_store/ui/src/pages/AppPage.tsx +++ b/kinode/packages/app_store/ui/src/pages/AppPage.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useCallback } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { FaDownload, FaCheck, FaTimes, FaPlay } from "react-icons/fa"; import useAppsStore from "../store"; @@ -13,37 +13,37 @@ export default function AppPage() { const [currentVersion, setCurrentVersion] = useState(null); const [latestVersion, setLatestVersion] = useState(null); - useEffect(() => { - const loadData = async () => { - await Promise.all([fetchListings(), fetchInstalled()]); + const loadData = useCallback(async () => { + await Promise.all([fetchListings(), fetchInstalled()]); - const foundApp = listings.find(a => `${a.package_id.package_name}:${a.package_id.publisher_node}` === id) || null; - setApp(foundApp); + const foundApp = listings.find(a => `${a.package_id.package_name}:${a.package_id.publisher_node}` === id) || null; + setApp(foundApp); - if (foundApp) { - const foundInstalledApp = installed.find(i => - i.package_id.package_name === foundApp.package_id.package_name && - i.package_id.publisher_node === foundApp.package_id.publisher_node - ) || null; - setInstalledApp(foundInstalledApp); + if (foundApp) { + const foundInstalledApp = installed.find(i => + i.package_id.package_name === foundApp.package_id.package_name && + i.package_id.publisher_node === foundApp.package_id.publisher_node + ) || null; + setInstalledApp(foundInstalledApp); - if (foundApp.metadata?.properties?.code_hashes) { - const versions = foundApp.metadata.properties.code_hashes; - if (versions.length > 0) { - setLatestVersion(versions[versions.length - 1][0]); - if (foundInstalledApp) { - const installedVersion = versions.find(([_, hash]) => hash === foundInstalledApp.our_version_hash); - if (installedVersion) { - setCurrentVersion(installedVersion[0]); - } + if (foundApp.metadata?.properties?.code_hashes) { + const versions = foundApp.metadata.properties.code_hashes; + if (versions.length > 0) { + setLatestVersion(versions[versions.length - 1][0]); + if (foundInstalledApp) { + const installedVersion = versions.find(([_, hash]) => hash === foundInstalledApp.our_version_hash); + if (installedVersion) { + setCurrentVersion(installedVersion[0]); } } } } - }; + } + }, [id, fetchListings, fetchInstalled]); + useEffect(() => { loadData(); - }, [id, fetchListings, fetchInstalled, listings, installed]); + }, [loadData]); if (!app) { return

App details not found for {id}

; diff --git a/kinode/packages/app_store/ui/src/pages/DownloadPage.tsx b/kinode/packages/app_store/ui/src/pages/DownloadPage.tsx index 4b058146..23bd5beb 100644 --- a/kinode/packages/app_store/ui/src/pages/DownloadPage.tsx +++ b/kinode/packages/app_store/ui/src/pages/DownloadPage.tsx @@ -6,7 +6,7 @@ import { AppListing, DownloadItem, MirrorCheckFile, PackageManifest } from "../t export default function DownloadPage() { const { id } = useParams(); - const { listings, fetchListings, fetchDownloadsForApp, downloadApp, installApp, checkMirror, fetchInstalled, installed, getCaps } = useAppsStore(); + const { listings, fetchListings, fetchDownloadsForApp, downloadApp, installApp, checkMirror, fetchInstalled, installed, getCaps, approveCaps } = useAppsStore(); const [downloads, setDownloads] = useState([]); const [selectedMirror, setSelectedMirror] = useState(null); const [mirrorStatuses, setMirrorStatuses] = useState<{ [mirror: string]: MirrorCheckFile | null }>({}); @@ -17,6 +17,8 @@ export default function DownloadPage() { const [showManifest, setShowManifest] = useState(false); const [showCaps, setShowCaps] = useState(false); const [manifest, setManifest] = useState(null); + const [showCapApproval, setShowCapApproval] = useState(false); + const [selectedVersion, setSelectedVersion] = useState(null); const app = listings.find(a => `${a.package_id.package_name}:${a.package_id.publisher_node}` === id); @@ -64,11 +66,26 @@ export default function DownloadPage() { const handleInstall = async (version: string) => { if (!app) return; + setSelectedVersion(version); + try { + const caps = await getCaps(`${app.package_id.package_name}:${app.package_id.publisher_node}`); + setManifest(caps); + setShowCapApproval(true); + } catch (error) { + console.error('Failed to get capabilities:', error); + setError(`Failed to get capabilities: ${error instanceof Error ? error.message : String(error)}`); + } + }; + + const confirmInstall = async () => { + if (!app || !selectedVersion) return; setIsInstalling(true); setError(null); try { - await installApp(`${app.package_id.package_name}:${app.package_id.publisher_node}`, version); + await approveCaps(`${app.package_id.package_name}:${app.package_id.publisher_node}`); + await installApp(`${app.package_id.package_name}:${app.package_id.publisher_node}`, selectedVersion); fetchInstalled(); + setShowCapApproval(false); } catch (error) { console.error('Installation failed:', error); setError(`Installation failed: ${error instanceof Error ? error.message : String(error)}`); @@ -123,14 +140,19 @@ export default function DownloadPage() { {app.metadata?.properties?.code_hashes.map(([version, hash]) => { - const download = downloads.find(d => d.name === `${hash}.zip`); + const download = downloads.find(d => { + if (d.File) { + return d.File.name === `${hash}.zip`; + } + return false; + }); const isDownloaded = !!download; const isInstalled = installed.some(i => i.package_id.package_name === app.package_id.package_name && i.our_version_hash === version); return ( {version} - {download?.size ? `${(download.size / 1024 / 1024).toFixed(2)} MB` : 'N/A'} + {download?.File?.size ? `${(download.File.size / 1024 / 1024).toFixed(2)} MB` : 'N/A'} {isInstalled ? 'Installed' : isDownloaded ? 'Downloaded' : 'Not downloaded'} @@ -207,6 +229,21 @@ export default function DownloadPage() { )} + + {showCapApproval && manifest && ( +
+

Approve Capabilities

+
+                        {JSON.stringify(manifest.request_capabilities, null, 2)}
+                    
+
+ + +
+
+ )} ); } \ No newline at end of file diff --git a/kinode/packages/app_store/ui/src/pages/MyDownloadsPage.tsx b/kinode/packages/app_store/ui/src/pages/MyDownloadsPage.tsx index 17b6a99f..e4fb0e09 100644 --- a/kinode/packages/app_store/ui/src/pages/MyDownloadsPage.tsx +++ b/kinode/packages/app_store/ui/src/pages/MyDownloadsPage.tsx @@ -1,15 +1,21 @@ import React, { useState, useEffect } from "react"; -import { FaFolder, FaFile, FaChevronLeft } from "react-icons/fa"; +import { FaFolder, FaFile, FaChevronLeft, FaSync, FaRocket, FaSpinner, FaCheck } from "react-icons/fa"; import useAppsStore from "../store"; -import { DownloadItem } from "../types/Apps"; +import { DownloadItem, PackageManifest } from "../types/Apps"; export default function MyDownloadsPage() { - const { fetchDownloads, fetchDownloadsForApp, listings } = useAppsStore(); + const { fetchDownloads, fetchDownloadsForApp, startMirroring, stopMirroring, installApp, getCaps, approveCaps, fetchInstalled, installed } = useAppsStore(); const [currentPath, setCurrentPath] = useState([]); const [items, setItems] = useState([]); + const [isInstalling, setIsInstalling] = useState(false); + const [error, setError] = useState(null); + const [showCapApproval, setShowCapApproval] = useState(false); + const [manifest, setManifest] = useState(null); + const [selectedItem, setSelectedItem] = useState(null); useEffect(() => { loadItems(); + fetchInstalled(); }, [currentPath]); const loadItems = async () => { @@ -23,8 +29,8 @@ export default function MyDownloadsPage() { }; const navigateToItem = (item: DownloadItem) => { - if (!item.is_file) { - setCurrentPath([...currentPath, item.name]); + if (item.Dir) { + setCurrentPath([...currentPath, item.Dir.name]); } }; @@ -32,20 +38,55 @@ export default function MyDownloadsPage() { setCurrentPath(currentPath.slice(0, -1)); }; - const getItemVersion = (name: string): string | undefined => { - if (currentPath.length === 0) { - return undefined; // No version for top-level directories + const toggleMirroring = async (item: DownloadItem) => { + if (item.Dir) { + const packageId = [...currentPath, item.Dir.name].join(':'); + try { + if (item.Dir.mirroring) { + await stopMirroring(packageId); + } else { + await startMirroring(packageId); + } + await loadItems(); + } catch (error) { + console.error("Error toggling mirroring:", error); + setError(`Error toggling mirroring: ${error instanceof Error ? error.message : String(error)}`); + } } + }; - const appId = currentPath.join(':'); - const app = listings.find(a => `${a.package_id.package_name}:${a.package_id.publisher_node}` === appId); - - if (app && app.metadata?.properties?.code_hashes) { - const matchingHash = app.metadata.properties.code_hashes.find(([_, hash]) => `${hash}.zip` === name); - return matchingHash ? matchingHash[0] : undefined; + const handleInstall = async (item: DownloadItem) => { + if (item.File) { + setSelectedItem(item); + try { + const packageId = [...currentPath, item.File.name.replace('.zip', '')].join(':'); + const caps = await getCaps(packageId); + setManifest(caps); + setShowCapApproval(true); + } catch (error) { + console.error('Failed to get capabilities:', error); + setError(`Failed to get capabilities: ${error instanceof Error ? error.message : String(error)}`); + } } + }; - return undefined; + const confirmInstall = async () => { + if (!selectedItem?.File) return; + setIsInstalling(true); + setError(null); + try { + const packageId = [...currentPath, selectedItem.File.name.replace('.zip', '')].join(':'); + await approveCaps(packageId); + await installApp(packageId, selectedItem.File.manifest); + await fetchInstalled(); + setShowCapApproval(false); + await loadItems(); + } catch (error) { + console.error('Installation failed:', error); + setError(`Installation failed: ${error instanceof Error ? error.message : String(error)}`); + } finally { + setIsInstalling(false); + } }; return ( @@ -66,26 +107,65 @@ export default function MyDownloadsPage() { Name Type Size - Version + Mirroring + Actions {items.map((item, index) => { - const version = getItemVersion(item.name); + const isFile = !!item.File; + const name = isFile ? item.File!.name : item.Dir!.name; + const isInstalled = isFile && installed.some(i => i.package_id.package_name === name.replace('.zip', '')); return ( - navigateToItem(item)} className={item.is_file ? 'file' : 'directory'}> + navigateToItem(item)} className={isFile ? 'file' : 'directory'}> - {item.is_file ? : } {item.name} + {isFile ? : } {name} + + {isFile ? 'File' : 'Directory'} + {isFile ? `${(item.File!.size / 1024).toFixed(2)} KB` : '-'} + {!isFile && (item.Dir!.mirroring ? 'Yes' : 'No')} + + {!isFile && ( + + )} + {isFile && !isInstalled && ( + + )} + {isFile && isInstalled && ( + + )} - {item.is_file ? 'File' : 'Directory'} - {item.size ? `${(item.size / 1024).toFixed(2)} KB` : '-'} - {version || '-'} ); })} + + {error && ( +
+ {error} +
+ )} + + {showCapApproval && manifest && ( +
+

Approve Capabilities

+
+                        {JSON.stringify(manifest.request_capabilities, null, 2)}
+                    
+
+ + +
+
+ )} ); } \ No newline at end of file diff --git a/kinode/packages/app_store/ui/src/pages/PublishPage.tsx b/kinode/packages/app_store/ui/src/pages/PublishPage.tsx index f2a629be..cd97c34d 100644 --- a/kinode/packages/app_store/ui/src/pages/PublishPage.tsx +++ b/kinode/packages/app_store/ui/src/pages/PublishPage.tsx @@ -59,6 +59,7 @@ export default function PublishPage() { try { // Check if the package already exists and get its TBA + console.log('packageName, publisherId: ', packageName, publisherId) let data = await publicClient.readContract({ abi: kimapAbi, address: KIMAP, @@ -69,7 +70,7 @@ export default function PublishPage() { let [tba, owner, _data] = data as [string, string, string]; let isUpdate = Boolean(tba && tba !== '0x' && owner === address); let currentTBA = isUpdate ? tba as `0x${string}` : null; - + console.log('currenttba, isupdate: ', currentTBA, isUpdate) // If the package doesn't exist, check for the publisher's TBA if (!currentTBA) { data = await publicClient.readContract({ @@ -82,6 +83,7 @@ export default function PublishPage() { [tba, owner, _data] = data as [string, string, string]; isUpdate = false; // It's a new package, but we might have a publisher TBA currentTBA = (tba && tba !== '0x') ? tba as `0x${string}` : null; + console.log('NEWcurrenttba, isupdate: ', currentTBA, isUpdate) } let metadata = metadataHash; diff --git a/kinode/packages/app_store/ui/src/pages/Testing.tsx b/kinode/packages/app_store/ui/src/pages/Testing.tsx index f5464a7c..50026156 100644 --- a/kinode/packages/app_store/ui/src/pages/Testing.tsx +++ b/kinode/packages/app_store/ui/src/pages/Testing.tsx @@ -24,6 +24,7 @@ const Testing: React.FC = () => { }, []) const handleAction = async (action: () => Promise, key: string) => { + console.log('in handleAction') try { await action() setResult(JSON.stringify(useAppsStore.getState()[key], null, 2)) diff --git a/kinode/packages/app_store/ui/src/store/index.ts b/kinode/packages/app_store/ui/src/store/index.ts index e72d2314..2e122324 100644 --- a/kinode/packages/app_store/ui/src/store/index.ts +++ b/kinode/packages/app_store/ui/src/store/index.ts @@ -28,7 +28,8 @@ interface AppsStore { downloadApp: (id: string, version_hash: string, downloadFrom: string) => Promise getCaps: (id: string) => Promise approveCaps: (id: string) => Promise - setMirroring: (id: string, version_hash: string, mirroring: boolean) => Promise + startMirroring: (id: string) => Promise + stopMirroring: (id: string) => Promise setAutoUpdate: (id: string, version_hash: string, autoUpdate: boolean) => Promise } @@ -77,7 +78,7 @@ const useAppsStore = create()( const res = await fetch(`${BASE_URL}/apps`) if (res.status === HTTP_STATUS.OK) { const data = await res.json() - set({ listings: data.apps || [] }) + set({ listings: data || [] }) } }, @@ -104,7 +105,7 @@ const useAppsStore = create()( fetchDownloads: async () => { const res = await fetch(`${BASE_URL}/downloads`) if (res.status === HTTP_STATUS.OK) { - const downloads = await res.json() + const downloads: DownloadItem[] = await res.json() set({ downloads: { root: downloads } }) return downloads } @@ -116,7 +117,7 @@ const useAppsStore = create()( if (res.status === HTTP_STATUS.OK) { const data = await res.json() - set({ ourApps: data.apps || [] }) + set({ ourApps: data || [] }) } }, @@ -185,16 +186,24 @@ const useAppsStore = create()( await get().fetchListing(id) }, - setMirroring: async (id: string, version_hash: string, mirroring: boolean) => { - const method = mirroring ? 'PUT' : 'DELETE' - const res = await fetch(`${BASE_URL}/apps/${id}/mirror`, { - method, - body: JSON.stringify({ version_hash }) + startMirroring: async (id: string) => { + const res = await fetch(`${BASE_URL}/downloads/${id}/mirror`, { + method: 'PUT' }) if (res.status !== HTTP_STATUS.OK) { - throw new Error(`Failed to ${mirroring ? 'start' : 'stop'} mirroring: ${id}`) + throw new Error(`Failed to start mirroring: ${id}`) } - await get().fetchListing(id) + await get().fetchDownloadsForApp(id.split(':').slice(0, -1).join(':')) + }, + + stopMirroring: async (id: string) => { + const res = await fetch(`${BASE_URL}/downloads/${id}/mirror`, { + method: 'DELETE' + }) + if (res.status !== HTTP_STATUS.OK) { + throw new Error(`Failed to stop mirroring: ${id}`) + } + await get().fetchDownloadsForApp(id.split(':').slice(0, -1).join(':')) }, setAutoUpdate: async (id: string, version_hash: string, autoUpdate: boolean) => { diff --git a/kinode/packages/app_store/ui/src/types/Apps.ts b/kinode/packages/app_store/ui/src/types/Apps.ts index 147aac5c..997cbe71 100644 --- a/kinode/packages/app_store/ui/src/types/Apps.ts +++ b/kinode/packages/app_store/ui/src/types/Apps.ts @@ -12,10 +12,20 @@ export interface AppListing { auto_update: boolean } -export interface DownloadItem { - name: string, - is_file: boolean, - size?: number +export type DownloadItem = { + Dir?: DirItem; + File?: FileItem; +}; + +export interface DirItem { + name: string; + mirroring: boolean; +} + +export interface FileItem { + name: string; + size: number; + manifest: string; } export interface MirrorCheckFile {