Skip to content

Commit 7f501dd

Browse files
committed
Adding snapshots feature with some other features.
1 parent 4e1aad3 commit 7f501dd

File tree

7 files changed

+1401
-51
lines changed

7 files changed

+1401
-51
lines changed

src/renderer/lib/config.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ export type WinboatConfigObj = {
2121
advancedFeatures: boolean
2222
multiMonitor: number
2323
rdpArgs: RdpArg[]
24+
snapshotMaxCount: number;
25+
snapshotCompression: boolean;
26+
snapshotPath: string;
2427
};
2528

2629
const defaultConfig: WinboatConfigObj = {
@@ -33,10 +36,13 @@ const defaultConfig: WinboatConfigObj = {
3336
experimentalFeatures: false,
3437
advancedFeatures: false,
3538
multiMonitor: 0,
36-
rdpArgs:[]
39+
rdpArgs:[],
40+
snapshotMaxCount: 3,
41+
snapshotCompression: true,
42+
snapshotPath: path.join(WINBOAT_DIR, "snapshots"),
3743
};
3844

39-
export class WinboatConfig {
45+
export class WinboatConfig {
4046
private static instance: WinboatConfig;
4147
#configPath: string = path.join(WINBOAT_DIR, "winboat.config.json");
4248
#configData: WinboatConfigObj = { ...defaultConfig };
@@ -113,4 +119,4 @@ export class WinboatConfig {
113119
return { ...defaultConfig };
114120
}
115121
}
116-
}
122+
}

src/renderer/lib/snapshot.ts

Lines changed: 871 additions & 0 deletions
Large diffs are not rendered by default.

src/renderer/lib/winboat.ts

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { QMPManager } from "./qmp";
1313
import { assert } from "@vueuse/core";
1414
import { setIntervalImmediately } from "../utils/interval";
1515
import { ComposePortEntry, PortManager } from "../utils/port";
16+
import { SnapshotManager } from "./snapshot";
1617

1718
const nodeFetch: typeof import('node-fetch').default = require('node-fetch');
1819
const fs: typeof import('fs') = require('fs');
@@ -37,7 +38,7 @@ const presetApps: WinApp[] = [
3738
Icon: AppIcons[InternalApps.WINDOWS_DESKTOP],
3839
Source: "internal",
3940
Path: InternalApps.WINDOWS_DESKTOP,
40-
Usage: 0
41+
Usage: 0
4142
},
4243
{
4344
Name: "⚙️ Windows Explorer",
@@ -95,7 +96,7 @@ class AppManager {
9596
appCache: WinApp[] = []
9697
appUsageCache: { [key: string]: number } = {};
9798
#wbConfig: WinboatConfig | null = null;
98-
99+
99100
constructor() {
100101
if(!fs.existsSync(USAGE_PATH)) {
101102
fs.writeFileSync(USAGE_PATH, "{}");
@@ -126,8 +127,8 @@ class AppManager {
126127
}
127128

128129
// Get the usage object that's on the disk
129-
const fsUsage = Object.entries(JSON.parse(fs.readFileSync(USAGE_PATH, 'utf-8'))) as any[];
130-
this.appCache = [];
130+
const fsUsage = Object.entries(JSON.parse(fs.readFileSync(USAGE_PATH, 'utf-8'))) as any[];
131+
this.appCache = [];
131132

132133
// Populate appCache with dummy WinApp object containing data from the disk
133134
for (let i = 0; i < fsUsage.length; i++) {
@@ -227,13 +228,14 @@ export class Winboat {
227228
appMgr: AppManager | null = null;
228229
qmpMgr: QMPManager | null = null;
229230
portMgr: Ref<PortManager | null> = ref(null);
231+
snapshotMgr: SnapshotManager | null = null;
230232

231233

232234
constructor() {
233235
if (Winboat.instance) {
234236
return Winboat.instance;
235237
}
236-
238+
237239
// This is a special interval which will never be destroyed
238240
this.#containerInterval = setInterval(async () => {
239241
const _containerStatus = await this.getContainerStatus();
@@ -252,13 +254,43 @@ export class Winboat {
252254

253255
this.#wbConfig = new WinboatConfig();
254256

257+
this.snapshotMgr = new SnapshotManager();
258+
255259
this.appMgr = new AppManager();
256260

257261
Winboat.instance = this;
258262

259263
return Winboat.instance;
260264
}
261265

266+
// Helper method to get storage folder
267+
getStorageInfo(): { type: "volume" | "bind"; path: string } {
268+
const compose = this.parseCompose();
269+
const storageVol = compose.services.windows.volumes.find((v) => v.includes("/storage"));
270+
271+
if (!storageVol) {
272+
throw new Error("Storage volume not found in compose file");
273+
}
274+
275+
// Check if it's a named volume (eg. "data:/storage")
276+
if (storageVol.startsWith("data:")) {
277+
return { type: "volume", path: "winboat_data" };
278+
}
279+
280+
// Bind mount otherwise (es. "/path/to/folder:/storage")
281+
return { type: "bind", path: storageVol.split(":")[0] };
282+
}
283+
284+
/**
285+
* Recreates the SnapshotManager instance with updated configuration.
286+
* Call this when the snapshot path changes.
287+
*/
288+
recreateSnapshotManager(): void {
289+
logger.info("Recreating SnapshotManager with updated configuration...");
290+
this.snapshotMgr = new SnapshotManager();
291+
logger.info("SnapshotManager recreated successfully");
292+
}
293+
262294
/**
263295
* Creates the intervals which rely on the Winboat Guest API.
264296
*/
@@ -438,7 +470,7 @@ export class Winboat {
438470

439471
/**
440472
* Returns the host port that maps to the given guest port
441-
*
473+
*
442474
* @param guestPort The port that gets looked up
443475
* @returns The host port that maps to the given guest port, or null if not found
444476
*/
@@ -487,7 +519,7 @@ export class Winboat {
487519
// QMP either doesn't exist or is disconnected
488520
await this.#connectQMPManager();
489521
logger.info("[QMPInterval] Created new QMP Manager");
490-
522+
491523
}, QMP_WAIT_MS);
492524
}
493525

@@ -567,15 +599,15 @@ export class Winboat {
567599
this.containerActionLoading.value = true;
568600

569601
const composeFilePath = path.join(WINBOAT_DIR, 'docker-compose.yml');
570-
602+
571603
if (restart) {
572604
// 1. Compose down the current container
573605
await execAsync(`docker compose -f ${composeFilePath} down`);
574606
}
575607

576608
// 2. Create a backup directory if it doesn't exist
577609
const backupDir = path.join(WINBOAT_DIR, 'backup');
578-
610+
579611
if (!fs.existsSync(backupDir)) {
580612
fs.mkdirSync(backupDir);
581613
logger.info(`Created compose backup dir: ${backupDir}`)
@@ -590,13 +622,13 @@ export class Winboat {
590622
const newComposeYAML = PrettyYAML.stringify(composeConfig).replaceAll("null", "");
591623
fs.writeFileSync(composeFilePath, newComposeYAML, { encoding: 'utf8' });
592624
logger.info(`Wrote new compose file to: ${composeFilePath}`);
593-
625+
594626
if (restart) {
595627
// 5. Deploy the container with the new compose file
596628
await execAsync(`docker compose -f ${composeFilePath} up -d`);
597629
remote.getCurrentWindow().reload();
598630
}
599-
631+
600632
logger.info("Replace compose config completed, successfully deployed new container");
601633

602634
this.containerActionLoading.value = false;
@@ -608,7 +640,7 @@ export class Winboat {
608640
// 1. Stop container
609641
await this.stopContainer();
610642
console.info("Stopped container");
611-
643+
612644
// 2. Remove the container
613645
await execAsync("docker rm WinBoat")
614646
console.info("Removed container")
@@ -652,7 +684,7 @@ export class Winboat {
652684
const rdpHostPort = this.getHostPort(GUEST_RDP_PORT);
653685

654686
logger.info(`Launching app: ${app.Name} at path ${app.Path}`);
655-
687+
656688
const freeRDPBin = await getFreeRDP();
657689

658690
logger.info(`Using FreeRDP Command: '${freeRDPBin}'`);
@@ -728,7 +760,7 @@ export class Winboat {
728760
: path.join(remote.app.getAppPath(), '..', '..', 'guest_server', 'winboat_guest_server.zip');
729761

730762
logger.info("ZIP Path", zipPath)
731-
763+
732764
// 4. Send the payload to the guest server, as a multipart/form-data with updateFile
733765
const formData = new FormData();
734766
formData.append('updateFile', fs.createReadStream(zipPath));
@@ -747,7 +779,7 @@ export class Winboat {
747779
const resJson = await res.json() as GuestServerUpdateResponse;
748780
logger.info(`Update params: ${JSON.stringify(resJson, null, 4)}`);
749781
logger.info("Successfully sent update payload to guest server");
750-
782+
751783
} catch(e) {
752784
logger.error("Failed to send update payload to guest server");
753785
logger.error(e);
@@ -774,4 +806,4 @@ export class Winboat {
774806
get hasQMPInterval() {
775807
return this.#qmpInterval !== null;
776808
}
777-
}
809+
}

src/renderer/router.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,20 @@ import Apps from './views/Apps.vue'
77
import About from './views/About.vue'
88
import Blank from './views/Blank.vue'
99
import Config from './views/Config.vue'
10+
import Snapshots from './views/Snapshots.vue'
1011

1112
export const routes: RouteRecordRaw[] = [
1213
{ path: '/', name: "Loading", component: Blank, meta: { icon: 'line-md:loading-loop' } },
1314
{ path: '/home', name: "Home", component: Home, meta: { icon: 'fluent:home-32-filled' } },
1415
{ path: '/setup', name: "SetupUI", component: SetupUI, meta: { icon: 'fluent-mdl2:install-to-drive' } },
1516
{ path: '/apps', name: "Apps", component: Apps, meta: { icon: 'fluent:apps-32-filled' } },
1617
{ path: '/configuration', name: "Configuration", component: Config, meta: { icon: 'icon-park-outline:config' } },
18+
{ path: '/snapshots', name: "Snapshots", component: Snapshots, meta: { icon: 'mdi:camera' } },
1719
{ path: '/about', name: "About", component: About, meta: { icon: 'fluent:info-32-filled' } },
1820

1921
]
2022

2123
export const router = createRouter({
2224
history: createMemoryHistory(),
2325
routes,
24-
})
26+
})

0 commit comments

Comments
 (0)