app_store: fix zip and start, downloads display

This commit is contained in:
bitful-pannul 2024-08-15 18:53:15 +03:00
parent b666d7a9f9
commit 59cfbaa59b
13 changed files with 289 additions and 130 deletions

12
Cargo.lock generated
View File

@ -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",

View File

@ -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::<DownloadResponses>(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::<DownloadResponses>(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,

View File

@ -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"]

View File

@ -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<vfs::DirEntry>, state: &State) -> Vec<Entry> {
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") {
if is_file && 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
};
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: manifest.unwrap_or_default(),
manifest,
}))
} else {
None // ignore non-zip files
}
} else {
let mirroring = PackageId::from_str(&name)
.map(|package_id| state.mirroring.contains(&package_id))
.unwrap_or(false);
} 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<File> {
match vfs::open_file(path, false, None) {

View File

@ -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"]

View File

@ -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";

View File

@ -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,8 +13,7 @@ export default function AppPage() {
const [currentVersion, setCurrentVersion] = useState<string | null>(null);
const [latestVersion, setLatestVersion] = useState<string | null>(null);
useEffect(() => {
const loadData = async () => {
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;
@ -40,10 +39,11 @@ export default function AppPage() {
}
}
}
};
}, [id, fetchListings, fetchInstalled]);
useEffect(() => {
loadData();
}, [id, fetchListings, fetchInstalled, listings, installed]);
}, [loadData]);
if (!app) {
return <div className="app-page"><h4>App details not found for {id}</h4></div>;

View File

@ -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<DownloadItem[]>([]);
const [selectedMirror, setSelectedMirror] = useState<string | null>(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<PackageManifest | null>(null);
const [showCapApproval, setShowCapApproval] = useState(false);
const [selectedVersion, setSelectedVersion] = useState<string | null>(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() {
</thead>
<tbody>
{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 (
<tr key={version}>
<td>{version}</td>
<td>{download?.size ? `${(download.size / 1024 / 1024).toFixed(2)} MB` : 'N/A'}</td>
<td>{download?.File?.size ? `${(download.File.size / 1024 / 1024).toFixed(2)} MB` : 'N/A'}</td>
<td>
{isInstalled ? 'Installed' : isDownloaded ? 'Downloaded' : 'Not downloaded'}
</td>
@ -207,6 +229,21 @@ export default function DownloadPage() {
</div>
)}
</div>
{showCapApproval && manifest && (
<div className="cap-approval-popup">
<h3>Approve Capabilities</h3>
<pre className="json-display">
{JSON.stringify(manifest.request_capabilities, null, 2)}
</pre>
<div className="approval-buttons">
<button onClick={() => setShowCapApproval(false)}>Cancel</button>
<button onClick={confirmInstall} disabled={isInstalling}>
{isInstalling ? <FaSpinner className="fa-spin" /> : 'Approve and Install'}
</button>
</div>
</div>
)}
</div>
);
}

View File

@ -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<string[]>([]);
const [items, setItems] = useState<DownloadItem[]>([]);
const [isInstalling, setIsInstalling] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showCapApproval, setShowCapApproval] = useState(false);
const [manifest, setManifest] = useState<PackageManifest | null>(null);
const [selectedItem, setSelectedItem] = useState<DownloadItem | null>(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);
}
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;
await loadItems();
} catch (error) {
console.error("Error toggling mirroring:", error);
setError(`Error toggling mirroring: ${error instanceof Error ? error.message : String(error)}`);
}
}
};
return 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)}`);
}
}
};
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() {
<th>Name</th>
<th>Type</th>
<th>Size</th>
<th>Version</th>
<th>Mirroring</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{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 (
<tr key={index} onClick={() => navigateToItem(item)} className={item.is_file ? 'file' : 'directory'}>
<tr key={index} onClick={() => navigateToItem(item)} className={isFile ? 'file' : 'directory'}>
<td>
{item.is_file ? <FaFile /> : <FaFolder />} {item.name}
{isFile ? <FaFile /> : <FaFolder />} {name}
</td>
<td>{isFile ? 'File' : 'Directory'}</td>
<td>{isFile ? `${(item.File!.size / 1024).toFixed(2)} KB` : '-'}</td>
<td>{!isFile && (item.Dir!.mirroring ? 'Yes' : 'No')}</td>
<td>
{!isFile && (
<button onClick={(e) => { e.stopPropagation(); toggleMirroring(item); }}>
<FaSync /> {item.Dir!.mirroring ? 'Stop' : 'Start'} Mirroring
</button>
)}
{isFile && !isInstalled && (
<button onClick={(e) => { e.stopPropagation(); handleInstall(item); }}>
<FaRocket /> Install
</button>
)}
{isFile && isInstalled && (
<FaCheck className="installed" />
)}
</td>
<td>{item.is_file ? 'File' : 'Directory'}</td>
<td>{item.size ? `${(item.size / 1024).toFixed(2)} KB` : '-'}</td>
<td>{version || '-'}</td>
</tr>
);
})}
</tbody>
</table>
</div>
{error && (
<div className="error-message">
{error}
</div>
)}
{showCapApproval && manifest && (
<div className="cap-approval-popup">
<h3>Approve Capabilities</h3>
<pre className="json-display">
{JSON.stringify(manifest.request_capabilities, null, 2)}
</pre>
<div className="approval-buttons">
<button onClick={() => setShowCapApproval(false)}>Cancel</button>
<button onClick={confirmInstall} disabled={isInstalling}>
{isInstalling ? <FaSpinner className="fa-spin" /> : 'Approve and Install'}
</button>
</div>
</div>
)}
</div>
);
}

View File

@ -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;

View File

@ -24,6 +24,7 @@ const Testing: React.FC = () => {
}, [])
const handleAction = async (action: () => Promise<void>, key: string) => {
console.log('in handleAction')
try {
await action()
setResult(JSON.stringify(useAppsStore.getState()[key], null, 2))

View File

@ -28,7 +28,8 @@ interface AppsStore {
downloadApp: (id: string, version_hash: string, downloadFrom: string) => Promise<void>
getCaps: (id: string) => Promise<PackageManifest>
approveCaps: (id: string) => Promise<void>
setMirroring: (id: string, version_hash: string, mirroring: boolean) => Promise<void>
startMirroring: (id: string) => Promise<void>
stopMirroring: (id: string) => Promise<void>
setAutoUpdate: (id: string, version_hash: string, autoUpdate: boolean) => Promise<void>
}
@ -77,7 +78,7 @@ const useAppsStore = create<AppsStore>()(
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<AppsStore>()(
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<AppsStore>()(
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<AppsStore>()(
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) => {

View File

@ -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 {