[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.

Support multiple backup destinations

Kasper (May 19, 2023, 12:11 AM +0200) 9f51fbd2 5069591b

+91 -26
+2 -2
.github/workflows/test.yml
··· 28 28 29 29 - run: npm install 30 30 31 - - run: npm run test 32 - 33 31 - run: npm run build:web 32 + 33 + - run: npm run test 34 34 35 35 - run: npm run lint 36 36
+1
CHANGELOG.md
··· 2 2 3 3 ## Next 4 4 - Support macOS 13 5 + - Support multiple backup destinations 5 6 6 7 ## 1.1.5 - 2022 Jul 9 7 8 - Fix `No parent of path` error (#4)
+2 -2
bindings.ts
··· 25 25 } 26 26 27 27 export function destinationinfo() { 28 - return invoke<DestinationXml[]>("destinationinfo") 28 + return invoke<DestinationDetail[]>("destinationinfo") 29 29 } 30 30 31 31 export type Backup = { path: string; name: string } 32 - export type DestinationXml = { kind: string; url: string; name: string; id: string; last_destination: number; mount_point: string } 32 + export type DestinationDetail = { id: string; mount_point: string; mount_point_name: string } 33 33 export type DirMap = { map: { [key: string]: { [key: string]: LoadedBackupItem } } } 34 34 export type LoadedBackupItem = { size: number } 35 35 export type BackupInfo = { old: string; new: string; loading: boolean }
+28 -4
src-tauri/src/destinationinfo.rs
··· 14 14 pub destinations: Vec<DestinationXml>, 15 15 } 16 16 17 - #[derive(Serialize, Deserialize, Debug, Type)] 17 + #[derive(Deserialize, Debug, Type)] 18 18 #[serde(deny_unknown_fields)] 19 19 pub struct DestinationXml { 20 20 #[serde(alias = "Kind")] ··· 26 26 #[serde(alias = "ID")] 27 27 pub id: String, 28 28 #[serde(alias = "LastDestination")] 29 - pub last_destination: u32, // u32 due to tauri-specta 29 + pub last_destination: usize, 30 30 #[serde(alias = "MountPoint")] 31 31 pub mount_point: String, 32 32 } 33 33 34 + #[derive(Serialize, Debug, Type)] 35 + pub struct DestinationDetail { 36 + pub id: String, 37 + pub mount_point: String, 38 + pub mount_point_name: String, 39 + } 40 + 34 41 #[command] 35 42 #[specta::specta] 36 43 pub async fn destinationinfo( 37 44 state: State<'_, DestinationsState>, 38 - ) -> Result<Vec<DestinationXml>, String> { 45 + ) -> Result<Vec<DestinationDetail>, String> { 39 46 let output = Command::new("tmutil") 40 47 .arg("destinationinfo") 41 48 .arg("-X") ··· 61 68 } 62 69 state.lock()?.destinations = Some(destinations_map.clone()); 63 70 64 - Ok(output_xml.destinations) 71 + let destinations_details = output_xml 72 + .destinations 73 + .into_iter() 74 + .map(|dest| { 75 + let mount_point_name = if dest.mount_point.starts_with("/Volumes/") { 76 + dest.mount_point["/Volumes/".len()..].to_string() 77 + } else { 78 + dest.mount_point.clone() 79 + }; 80 + DestinationDetail { 81 + id: dest.id, 82 + mount_point: dest.mount_point, 83 + mount_point_name, 84 + } 85 + }) 86 + .collect(); 87 + 88 + Ok(destinations_details) 65 89 }
+56 -16
src/App.svelte
··· 7 7 import ProgressBar from './lib/ProgressBar.svelte' 8 8 import Button from './lib/Button.svelte' 9 9 import commands from './lib/commands' 10 - import type { Backup, DestinationXml } from '../bindings' 10 + import type { Backup, DestinationDetail } from '../bindings' 11 11 12 - let destinations: DestinationXml[] | null = null 13 - let destination: DestinationXml | null = null 12 + let destinations: DestinationDetail[] | null = null 13 + let selectedDestination: DestinationDetail | null = null 14 14 let backups: Backup[] | null = null 15 - 16 15 let loading = false 17 16 async function refresh(refresh = false) { 18 17 let minEndTime = Date.now() + 250 18 + function timeRemaining() { 19 + return Math.max(minEndTime - Date.now(), 0) 20 + } 19 21 if (loading) { 20 22 return 21 23 } 22 24 loading = true 25 + selectedDestination = null 23 26 closePage() 24 27 25 28 destinations = await commands.destinationinfo() 26 - console.log('Loaded destinations', destinations) 27 - destination = destinations[0] ?? null 28 29 29 - backups = await commands.loadBackupList(destination.id, refresh) 30 - console.log('Loaded backups', backups) 30 + if (destinations[0]) { 31 + setTimeout(() => { 32 + selectedDestination = destinations[0] 33 + }, timeRemaining()) 31 34 35 + backups = await commands.loadBackupList(destinations[0].id, refresh) 36 + console.log('Loaded backups', backups) 37 + } 32 38 await new Promise((resolve) => { 33 - let timeRemaining = Math.max(minEndTime - Date.now()) 34 - setTimeout(resolve, timeRemaining) 39 + setTimeout(resolve, timeRemaining()) 35 40 }) 36 41 loading = false 37 42 } ··· 39 44 </script> 40 45 41 46 <div class="sidebar"> 42 - {#if destination !== null} 43 - <p>{destinations[0].mount_point}</p> 44 - {/if} 45 47 {#if loading} 46 48 <div class="loading" transition:fade={{ duration: 500, easing: cubicInOut }}> 47 49 <ProgressBar /> 48 50 </div> 49 - {:else} 51 + {/if} 52 + <Button disabled={loading} on:click={() => refresh(true)}>Refresh</Button> 53 + <div class="mount-point"> 54 + {#if selectedDestination} 55 + <div transition:fade={{ duration: 300, easing: cubicInOut }}> 56 + {#if destinations.length >= 2} 57 + <select 58 + value={selectedDestination.id} 59 + disabled={loading} 60 + on:change={async (e) => { 61 + loading = true 62 + selectedDestination = destinations.find((d) => d.id === e.currentTarget.value) || null 63 + backups = await commands.loadBackupList(destinations[0].id, false) 64 + console.log('Loaded backups', backups) 65 + loading = false 66 + }} 67 + > 68 + {#each destinations as destination} 69 + <option value={destination.id}>{destination.mount_point_name}</option> 70 + {/each} 71 + </select> 72 + {:else} 73 + <span>{selectedDestination.mount_point_name}</span> 74 + <!-- <select value={destination}> 75 + {#each destinations as destination} 76 + <option value={destination}>{destination.mount_point_name}</option> 77 + {/each} 78 + </select> --> 79 + {/if} 80 + </div> 81 + {/if} 82 + </div> 83 + {#if !loading} 50 84 <div class="sidebar-stuff" transition:fade={{ duration: 300, easing: cubicInOut }}> 51 - <Button disabled={loading} on:click={() => refresh(true)}>Refresh</Button> 52 85 {#if backups} 53 86 <Sidebar {backups} /> 54 87 {/if} 55 88 </div> 56 89 {/if} 57 90 </div> 58 - <Page {destination} /> 91 + <Page destination={selectedDestination} /> 59 92 60 93 <style lang="sass"> 61 94 @font-face ··· 98 131 display: flex 99 132 flex-direction: column 100 133 flex-grow: 1 134 + .mount-point 135 + font-size: 15px 136 + font-weight: 600 137 + margin-left: auto 138 + margin-right: auto 139 + color: hsla(216, 50%, 85%, 1) 140 + height: 25px 101 141 .loading 102 142 height: 100% 103 143 position: absolute // for transition
+2 -2
src/page/Page.svelte
··· 4 4 import Button from '../lib/Button.svelte' 5 5 import ProgressBar from '../lib/ProgressBar.svelte' 6 6 import commands from '../lib/commands' 7 - import type { DestinationXml } from '../../bindings' 7 + import type { DestinationDetail } from '../../bindings' 8 8 9 - export let destination: DestinationXml | null = null 9 + export let destination: DestinationDetail | null = null 10 10 11 11 async function compare(autoLoad = false) { 12 12 if ($page.loading || $page.backup === null) {