[READ-ONLY] Mirror of https://github.com/probablykasper/time-machine-inspector. Time Machine backup size inspector app
backup macos tauri time-machine
0

Configure Feed

Select the types of activity you want to include in your feed.

Refactor DirMap

Kasper (Jan 16, 2022, 11:51 AM +0100) 8f9e343a 3a490f9d

+284 -205
+33 -72
src-tauri/src/cmd.rs
··· 1 1 use crate::dir_map::DirMap; 2 - use crate::{compare, dir_map, reset_dur, throw}; 2 + use crate::{compare, listbackups, throw}; 3 3 use std::fs::File; 4 - use std::io::{BufRead, BufReader}; 5 - use std::process::{Command, ExitStatus, Stdio}; 6 - use std::sync::Mutex; 7 - use std::time::Instant; 4 + use std::process::ExitStatus; 5 + use std::sync::{Mutex, MutexGuard}; 8 6 use tauri::api::{dialog, shell}; 9 7 use tauri::{command, State, Window}; 10 8 11 - fn parse_output(bytes: Vec<u8>) -> Result<String, String> { 9 + pub fn parse_output(bytes: Vec<u8>) -> Result<String, String> { 12 10 match String::from_utf8(bytes) { 13 11 Ok(s) => Ok(s), 14 12 Err(e) => throw!("Unable to parse output: {}", e), ··· 22 20 } 23 21 } 24 22 25 - fn check_cmd_success(status: &ExitStatus, stderr: Vec<u8>) -> Result<(), String> { 23 + pub fn check_cmd_success(status: &ExitStatus, stderr: Vec<u8>) -> Result<(), String> { 26 24 if !status.success() { 27 25 let stderr = parse_output(stderr)?; 28 26 throw!("tmutil error {}:\n{}", code_to_str(status.code()), stderr); ··· 30 28 Ok(()) 31 29 } 32 30 33 - #[derive(Default)] 34 - pub struct List(pub Mutex<Option<String>>); 35 - 36 31 pub async fn full_disk_access(dialog_window: Window) -> Result<(), String> { 37 32 match File::open("/Library/Preferences/com.apple.TimeMachine.plist") { 38 33 Ok(_file) => {} ··· 57 52 }, 58 53 }; 59 54 Ok(()) 55 + } 56 + 57 + #[derive(Default)] 58 + pub struct BackupList(pub Mutex<Option<DirMap<()>>>); 59 + 60 + impl BackupList { 61 + pub fn lock(&self) -> Result<MutexGuard<Option<DirMap<()>>>, String> { 62 + match self.0.lock() { 63 + Ok(mutex) => Ok(mutex), 64 + Err(e) => throw!("Unable to lock backup list: {}", e), 65 + } 66 + } 60 67 } 61 68 62 69 #[command] 63 70 pub async fn load_backups( 64 71 refresh: bool, 65 72 w: Window, 66 - backups_list_str: State<'_, List>, 67 - ) -> Result<Option<String>, String> { 73 + state: State<'_, BackupList>, 74 + ) -> Result<DirMap<()>, String> { 75 + // get cached backup_list 68 76 if !refresh { 69 - if let Some(s) = &*backups_list_str.0.lock().unwrap() { 70 - return Ok(Some(s.clone())); 77 + let backup_list = state.lock()?; 78 + match &*backup_list { 79 + Some(list) => return Ok(list.clone()), 80 + None => {} 71 81 } 72 82 } 73 83 74 84 full_disk_access(w).await?; 75 - 76 - // return Ok(Some( 77 - // "/Volumes/Time Machine Backups/Backups.backupdb/my-mac/2021-12-21-133750\n\ 78 - // /Volumes/Time Machine Backups/Backups.backupdb/my-mac/2021-12-27-193733\n" 79 - // .to_string(), 80 - // )); 85 + let dir_map = listbackups::listbackups()?; 86 + state.lock()?.replace(dir_map.clone()); 81 87 82 - let output = Command::new("tmutil") 83 - .arg("listbackups") 84 - .output() 85 - .expect("Error calling command"); 86 - check_cmd_success(&output.status, output.stderr.clone())?; 87 - 88 - let output_str = parse_output(output.stdout)?; 89 - let mut s = backups_list_str.0.lock().unwrap(); 90 - *s = Some(output_str.clone()); 91 - 92 - Ok(Some(output_str)) 88 + Ok(dir_map) 93 89 } 94 90 95 91 #[command] 96 - pub async fn compare_backups(old: String, new: String, w: Window) -> Result<DirMap, String> { 92 + pub async fn compare_backups<'a>( 93 + old: String, 94 + new: String, 95 + w: Window, 96 + ) -> Result<DirMap<u64>, String> { 97 97 full_disk_access(w).await?; 98 - 99 - let mut anchor = Instant::now(); 100 - 101 - let mut cmd = Command::new("tmutil") 102 - .arg("compare") 103 - .arg("-X") 104 - .arg("-s") 105 - .arg(&old) 106 - .arg(&new) 107 - .stdout(Stdio::piped()) 108 - .stderr(Stdio::piped()) 109 - .spawn() 110 - .expect("Error calling command"); 111 - 112 - println!("\u{23f1} {:.3}ms running tmutil", reset_dur(&mut anchor)); 113 - 114 - let mut child_out = BufReader::new(cmd.stdout.as_mut().unwrap()); 115 - let mut lines = Vec::new(); 116 - 117 - loop { 118 - match child_out.read_until(b'\n', &mut lines) { 119 - Ok(0) => break, 120 - _ => {} 121 - }; 122 - } 123 - 124 - let output = cmd.wait_with_output().expect("Failed ot wait on command"); 125 - check_cmd_success(&output.status, output.stderr)?; 126 - 127 - println!("\u{23f1} {:.3}ms reading output", reset_dur(&mut anchor)); 128 - 129 - let comparison = compare::parse_xml(&lines)?; 130 - println!("{:#?}", comparison.totals); 131 - 132 - println!("\u{23f1} {:.3}ms parse xml", reset_dur(&mut anchor)); 133 - 134 - let dir_map = dir_map::make_map(comparison)?; 135 - 136 - println!("\u{23f1} {:.3}ms constructing map", reset_dur(&mut anchor)); 137 - 98 + let dir_map = compare::compare(&old, &new)?; 138 99 Ok(dir_map) 139 100 }
+47 -6
src-tauri/src/compare.rs
··· 1 + use crate::cmd::check_cmd_success; 2 + use crate::dir_map::DirMap; 1 3 use crate::{reset_dur, throw}; 2 4 use plist::Value; 3 5 use serde::de::DeserializeOwned; 4 6 use serde::{Deserialize, Serialize}; 5 - use std::io::{BufWriter, Cursor}; 7 + use std::io::{BufRead, BufReader, BufWriter, Cursor}; 8 + use std::process::{Command, Stdio}; 6 9 use std::time::Instant; 7 10 8 11 #[derive(Serialize, Debug)] ··· 15 18 #[serde(deny_unknown_fields)] 16 19 struct ComparisonXml { 17 20 #[serde(rename = "Changes")] 18 - // changes: Vec<Change>, 19 21 changes: Vec<Value>, 20 22 #[serde(rename = "Totals")] 21 23 totals: Totals, ··· 82 84 83 85 let mut changes = Vec::with_capacity(comparison.changes.len()); 84 86 85 - let mut anchor = Instant::now(); 86 - 87 87 for change_dict in &comparison.changes { 88 88 let change: Change = match deserialize_value(&change_dict) { 89 89 Ok(v) => v, ··· 97 97 }; 98 98 changes.push(change); 99 99 } 100 - 101 - println!("\u{23f1} {:.3}ms parsing xml", reset_dur(&mut anchor)); 102 100 103 101 Ok(Comparison { 104 102 changes, ··· 123 121 }; 124 122 Ok(change) 125 123 } 124 + 125 + pub fn compare(old: &str, new: &str) -> Result<DirMap<u64>, String> { 126 + let mut anchor = Instant::now(); 127 + 128 + let mut cmd = Command::new("tmutil") 129 + .arg("compare") 130 + .arg("-X") 131 + .arg("-s") 132 + .arg(old) 133 + .arg(new) 134 + .stdout(Stdio::piped()) 135 + .stderr(Stdio::piped()) 136 + .spawn() 137 + .expect("Error calling command"); 138 + 139 + println!("\u{23f1} {:.3}ms running tmutil", reset_dur(&mut anchor)); 140 + 141 + let mut child_out = BufReader::new(cmd.stdout.as_mut().unwrap()); 142 + let mut lines = Vec::new(); 143 + 144 + loop { 145 + match child_out.read_until(b'\n', &mut lines) { 146 + Ok(0) => break, 147 + _ => {} 148 + }; 149 + } 150 + 151 + let output = cmd.wait_with_output().expect("Failed ot wait on command"); 152 + check_cmd_success(&output.status, output.stderr)?; 153 + 154 + println!("\u{23f1} {:.3}ms reading output", reset_dur(&mut anchor)); 155 + 156 + let comparison = parse_xml(&lines)?; 157 + println!("{:#?}", comparison.totals); 158 + 159 + println!("\u{23f1} {:.3}ms parse xml", reset_dur(&mut anchor)); 160 + 161 + let dir_map = DirMap::from_comparison(comparison)?; 162 + 163 + println!("\u{23f1} {:.3}ms constructing map", reset_dur(&mut anchor)); 164 + 165 + Ok(dir_map) 166 + }
+75 -34
src-tauri/src/dir_map.rs
··· 1 - use crate::compare; 1 + use crate::{compare, throw}; 2 + use serde::Serialize; 3 + use std::collections::hash_map::Entry; 2 4 use std::collections::HashMap; 3 - use std::path::PathBuf; 5 + use std::ffi::OsStr; 6 + use std::path::{Path, PathBuf}; 7 + 8 + pub type DirContents<I> = HashMap<String, I>; 9 + 10 + #[derive(Serialize, Clone, Debug)] 11 + pub struct DirMap<I> { 12 + pub map: HashMap<String, DirContents<I>>, 13 + } 14 + 15 + fn get_parent<'a>(path: &'a Path) -> Result<&'a Path, String> { 16 + match path.parent() { 17 + Some(p) => Ok(p), 18 + None => throw!("No parent of path {}", path.to_string_lossy()), 19 + } 20 + } 21 + fn get_basename<'a>(path: &'a Path) -> Result<&'a OsStr, String> { 22 + match path.file_name() { 23 + Some(p) => Ok(p), 24 + None => throw!("No base of path {}", path.to_string_lossy()), 25 + } 26 + } 27 + 28 + impl<I> DirMap<I> { 29 + pub fn new() -> Self { 30 + Self { 31 + map: HashMap::new(), 32 + } 33 + } 34 + pub fn get_or_create_dir(&mut self, path: String) -> &mut DirContents<I> { 35 + self.map.entry(path).or_insert(HashMap::new()) 36 + } 37 + pub fn item_entry(&mut self, path: &Path) -> Result<Entry<String, I>, String> { 38 + let dir = get_parent(path)?.to_string_lossy().to_string(); 39 + let basename = get_basename(path)?.to_string_lossy().to_string(); 4 40 5 - pub type DirItem = u64; 6 - pub type DirContent = HashMap<String, DirItem>; 7 - pub type DirMap = HashMap<String, DirContent>; 41 + let dir_contents = self.get_or_create_dir(dir); 42 + Ok(dir_contents.entry(basename)) 43 + } 44 + } 8 45 9 - pub fn make_map(comparison: compare::Comparison) -> Result<DirMap, String> { 10 - let mut dir_map: DirMap = HashMap::new(); 11 - dir_map.entry("/".into()).or_default(); 46 + impl DirMap<()> { 47 + pub fn from_string_paths(str_paths: Vec<String>) -> Result<Self, String> { 48 + let mut dir_map = DirMap::new(); 12 49 13 - for change in comparison.changes { 14 - let new_item = match change { 15 - compare::Change::Add(add) => add.added_item, 16 - compare::Change::Update(update) => update.newer_item, 17 - compare::Change::Delete(_) => continue, 18 - }; 19 - let path = PathBuf::from(new_item.path); 50 + for str_path in str_paths { 51 + let path = PathBuf::from(str_path); 20 52 21 - let mut base = path.file_name().expect("path base"); 22 - let mut parent = path.parent().expect("path parent"); 23 - loop { 24 - let dir_content = dir_map 25 - .entry(parent.to_string_lossy().to_string()) 26 - .or_insert(HashMap::new()); 53 + for ancestor in path.ancestors() { 54 + if ancestor == Path::new("/") { 55 + break; 56 + } 57 + dir_map.item_entry(ancestor)?.or_insert(()); 58 + } 59 + } 60 + Ok(dir_map) 61 + } 62 + } 27 63 28 - let dir_item = dir_content 29 - .entry(base.to_string_lossy().to_string()) 30 - .or_insert(0); 31 - *dir_item += new_item.size; 64 + impl DirMap<u64> { 65 + pub fn from_comparison(comparison: compare::Comparison) -> Result<Self, String> { 66 + let mut dir_map = DirMap::new(); 32 67 33 - base = match parent.file_name() { 34 - Some(v) => v, 35 - None => break, 68 + for change in comparison.changes { 69 + let new_item = match change { 70 + compare::Change::Add(add) => add.added_item, 71 + compare::Change::Update(update) => update.newer_item, 72 + compare::Change::Delete(_) => continue, 36 73 }; 37 - parent = match parent.parent() { 38 - Some(v) => v, 39 - None => break, 40 - }; 74 + let path = PathBuf::from(new_item.path); 75 + 76 + for ancestor in path.ancestors() { 77 + if ancestor == Path::new("/") { 78 + break; 79 + } 80 + let item = dir_map.item_entry(ancestor)?.or_insert(0); 81 + *item += new_item.size; 82 + } 41 83 } 84 + Ok(dir_map) 42 85 } 43 - 44 - Ok(dir_map) 45 86 }
+28
src-tauri/src/listbackups.rs
··· 1 + use crate::cmd::{check_cmd_success, parse_output}; 2 + use crate::dir_map::DirMap; 3 + use std::process::Command; 4 + 5 + pub fn listbackups() -> Result<DirMap<()>, String> { 6 + // return Ok(Some( 7 + // "/Volumes/Time Machine Backups/Backups.backupdb/my-mac/2021-12-21-133750\n\ 8 + // /Volumes/Time Machine Backups/Backups.backupdb/my-mac/2021-12-27-193733\n" 9 + // .to_string(), 10 + // )); 11 + 12 + let output = Command::new("tmutil") 13 + .arg("listbackups") 14 + .output() 15 + .expect("Error calling command"); 16 + check_cmd_success(&output.status, output.stderr.clone())?; 17 + 18 + let output_str = parse_output(output.stdout)?; 19 + 20 + let paths = output_str 21 + .split('\n') 22 + .map(|s| s.to_string()) 23 + .filter(|s| s != "") 24 + .collect(); 25 + 26 + let dir_map = DirMap::from_string_paths(paths)?; 27 + Ok(dir_map) 28 + }
+2 -1
src-tauri/src/main.rs
··· 14 14 mod cmd; 15 15 mod compare; 16 16 mod dir_map; 17 + mod listbackups; 17 18 18 19 #[command] 19 20 fn error_popup(msg: String, win: Window) { ··· 40 41 let ctx = tauri::generate_context!(); 41 42 42 43 tauri::Builder::default() 43 - .manage(cmd::List(Default::default())) 44 + .manage(cmd::BackupList(Default::default())) 44 45 .invoke_handler(tauri::generate_handler![ 45 46 error_popup, 46 47 cmd::load_backups,
+9 -41
src/App.svelte
··· 1 1 <script lang="ts"> 2 - import Item, { ItemClickEvent } from './Item.svelte' 3 - import { page, backups, close as closePage, loadBackups } from './page' 4 2 import Page from './Page.svelte' 3 + import Sidebar from './Sidebar.svelte' 4 + import { backups, close as closePage, loadBackups } from './page' 5 5 6 6 let loading = false 7 - async function refreshBackups(refresh = false) { 7 + async function load(refresh = false) { 8 8 if (loading) { 9 9 return 10 10 } ··· 13 13 await loadBackups(refresh) 14 14 loading = false 15 15 } 16 - 17 - function sidebarClick(e: ItemClickEvent) { 18 - if (e.detail.isFolder) { 19 - e.detail.toggleChildren() 20 - } else { 21 - $page = { 22 - name: e.detail.name, 23 - fullPath: e.detail.fullPath, 24 - } 25 - } 26 - } 27 - refreshBackups() 16 + load() 28 17 </script> 29 18 30 19 <div class="sidebar"> 31 - <button on:click={() => refreshBackups(true)} class:disabled={loading} tabindex="0"> 20 + <button on:click={() => load(true)} class:disabled={loading} tabindex="0"> 32 21 {#if loading} 33 22 Loading... 34 23 {:else} 35 24 Refresh 36 25 {/if} 37 26 </button> 38 - <div class="content"> 39 - {#if $backups !== null} 40 - {#each $backups.dirs[$backups.rootPath] as child} 41 - {#if $backups.rootPath === '/'} 42 - <Item 43 - map={$backups.dirs} 44 - selectedPath={$page.fullPath + '/' + $page.name} 45 - name={child} 46 - fullPath={'/' + child} 47 - on:click={sidebarClick} /> 48 - {:else} 49 - <Item 50 - map={$backups.dirs} 51 - selectedPath={$page.fullPath + '/' + $page.name} 52 - name={child} 53 - fullPath={$backups.rootPath + '/' + child} 54 - on:click={sidebarClick} /> 55 - {/if} 56 - {/each} 57 - {/if} 58 - </div> 27 + {#if $backups !== null} 28 + <Sidebar backups={$backups} /> 29 + {/if} 59 30 </div> 60 31 <Page /> 61 32 ··· 93 64 border-right: 1px solid hsla(230, 100%, 85%, 0.12) 94 65 display: flex 95 66 flex-direction: column 67 + $easing: cubic-bezier(0.4, 0.0, 0.2, 1) 96 68 button 97 69 font-family: inherit 98 70 user-select: none ··· 117 89 letter-spacing: 0.05em 118 90 &.disabled, &:active 119 91 opacity: 0.75 120 - .content 121 - overflow: auto 122 - height: 10px 123 - flex-grow: 1 124 92 </style>
+7 -7
src/Item.svelte
··· 1 1 <script lang="ts" context="module"> 2 2 export type ItemClickEventDetail = { 3 3 name: string 4 + dir: string 4 5 fullPath: string 5 6 isFolder: boolean 6 7 toggleChildren: () => void ··· 10 11 11 12 <script lang="ts"> 12 13 import { createEventDispatcher } from 'svelte' 13 - 14 - type DirMap = { 15 - '/': string[] 16 - [name: string]: string[] 17 - } 14 + import type { DirMap } from './page' 18 15 19 16 export let map: DirMap 20 17 21 18 export let name: string 19 + export let dir: string 22 20 export let fullPath: string 23 21 $: isFolder = map[fullPath] !== undefined 24 22 ··· 33 31 dispatch('click', { 34 32 name, 35 33 fullPath, 34 + dir, 36 35 isFolder, 37 36 toggleChildren: () => { 38 37 open = !open ··· 55 54 </div> 56 55 <div class="children"> 57 56 {#if open && isFolder} 58 - {#each map[fullPath] as child} 57 + {#each Object.keys(map[fullPath]).sort() as child} 59 58 <svelte:self 60 59 {map} 61 - {selectedPath} 62 60 name={child} 61 + dir={fullPath} 63 62 fullPath={fullPath + '/' + child} 63 + {selectedPath} 64 64 on:click 65 65 indentLevel={indentLevel + 1} /> 66 66 {/each}
+7 -6
src/Page.svelte
··· 19 19 return 20 20 } 21 21 const fullPathParent = $page.fullPath.substring(0, $page.fullPath.lastIndexOf('/')) 22 - const dir = $backups.dirs[fullPathParent] 23 - const selectedBackupIndex = dir.indexOf($page.name) 24 - if (selectedBackupIndex >= 1) { 25 - const prevBackup = fullPathParent + '/' + dir[selectedBackupIndex - 1] 26 - const args = { old: prevBackup, new: $page.fullPath } 22 + const dir = $backups.dirs[fullPathParent][$page.name] 23 + if (dir !== undefined) { 27 24 loading = true 28 - dirMap = (await runCmd('compare_backups', args)) as DirMap 25 + const result = (await runCmd('compare_backups', { 26 + old: $page.prevPath, 27 + new: $page.fullPath, 28 + })) as { map: DirMap } 29 + dirMap = result.map 29 30 console.log(dirMap) 30 31 } 31 32 loading = false
+52
src/Sidebar.svelte
··· 1 + <script lang="ts"> 2 + import Item, { ItemClickEvent } from './Item.svelte' 3 + import { page, Backups } from './page' 4 + 5 + export let backups: Backups 6 + 7 + function sidebarClick(e: ItemClickEvent) { 8 + if (e.detail.isFolder) { 9 + e.detail.toggleChildren() 10 + } else { 11 + const paths = Object.keys(backups.dirs[e.detail.dir]).sort() 12 + const backupIndex = paths.indexOf(e.detail.name) 13 + if (backupIndex >= 1) { 14 + let prevPathSeparator = e.detail.dir === '/' ? '' : '/' 15 + $page = { 16 + fullPath: e.detail.fullPath, 17 + name: e.detail.name, 18 + prevPath: e.detail.dir + prevPathSeparator + paths[backupIndex - 1], 19 + } 20 + } 21 + } 22 + } 23 + </script> 24 + 25 + <div class="content"> 26 + {#each Object.keys(backups.dirs[backups.rootPath]).sort() as child} 27 + {#if backups.rootPath === '/'} 28 + <Item 29 + map={backups.dirs} 30 + selectedPath={$page.fullPath + '/' + $page.name} 31 + name={child} 32 + dir="/" 33 + fullPath={'/' + child} 34 + on:click={sidebarClick} /> 35 + {:else} 36 + <Item 37 + map={backups.dirs} 38 + name={child} 39 + dir={backups.rootPath} 40 + fullPath={backups.rootPath + '/' + child} 41 + selectedPath={$page.fullPath + '/' + $page.name} 42 + on:click={sidebarClick} /> 43 + {/if} 44 + {/each} 45 + </div> 46 + 47 + <style lang="sass"> 48 + .content 49 + overflow: auto 50 + height: 10px 51 + flex-grow: 1 52 + </style>
+24 -38
src/page.ts
··· 6 6 rootPath: string 7 7 } 8 8 export type DirMap = { 9 - '/': string[] 10 - [name: string]: string[] 9 + '/': DirContent 10 + [name: string]: DirContent 11 + } 12 + export type DirContent = { 13 + [name: string]: null 11 14 } 12 15 13 16 export const backups = writable<Backups | null>(null) ··· 15 18 export const page = writable({ 16 19 fullPath: '', 17 20 name: '', 21 + prevPath: '', 18 22 }) 19 23 20 24 export function close() { 21 25 page.set({ 22 26 fullPath: '', 23 27 name: '', 28 + prevPath: '', 24 29 }) 25 30 backups.set(null) 26 31 } 27 32 28 - function parseDirs(paths: string[]): DirMap { 29 - const dirmap: DirMap = { '/': [] } 30 - paths.forEach((path) => { 31 - let base = path.substring(path.lastIndexOf('/') + 1) 32 - let parent = path.substring(0, path.lastIndexOf('/')) 33 - do { 34 - if (parent === '') { 35 - dirmap['/'].push(base) 36 - break 37 - } else if (dirmap[parent] === undefined) { 38 - dirmap[parent] = [base] 39 - } else { 40 - dirmap[parent].push(base) 41 - break 42 - } 43 - base = parent.substring(parent.lastIndexOf('/') + 1) 44 - parent = parent.substring(0, parent.lastIndexOf('/')) 45 - } while (dirmap[parent] === undefined) 46 - }) 47 - return dirmap 48 - } 33 + export async function loadBackups(refresh = false) { 34 + const result = (await runCmd('load_backups', { refresh })) as { map: DirMap } 35 + console.log(result) 36 + const map = result.map 49 37 50 - export function parseStdout(stdout: string | null): Backups | null { 51 - if (stdout === null) { 52 - return null 53 - } 54 - const paths = stdout.split('\n').filter((path) => path !== '') 55 - const dirs = parseDirs(paths) 56 38 let rootPath = '/' 57 - if (dirs[rootPath].length === 1 && dirs[rootPath][0] === 'Volumes') { 39 + if (Object.keys(map[rootPath]).length === 1 && map[rootPath]['Volumes'] !== undefined) { 58 40 rootPath += 'Volumes' 59 - if (dirs[rootPath].length === 1 && dirs[rootPath][0] === 'Time Machine Backups') { 41 + if ( 42 + Object.keys(map[rootPath]).length === 1 && 43 + map[rootPath]['Time Machine Backups'] !== undefined 44 + ) { 60 45 rootPath += '/Time Machine Backups' 61 - if (dirs[rootPath].length === 1 && dirs[rootPath][0] === 'Backups.backupdb') { 46 + if ( 47 + Object.keys(map[rootPath]).length === 1 && 48 + map[rootPath]['Backups.backupdb'] !== undefined 49 + ) { 62 50 rootPath += '/Backups.backupdb' 63 51 } 64 52 } 65 53 } 66 - return { dirs, rootPath } 67 - } 68 54 69 - export async function loadBackups(refresh = false) { 70 - const stdout = (await runCmd('load_backups', { refresh })) as string | null 71 - backups.set(parseStdout(stdout)) 72 - console.log(parseStdout(stdout)) 55 + backups.set({ 56 + dirs: result.map, 57 + rootPath, 58 + }) 73 59 }