Skip to content

Commit 16c93ed

Browse files
committed
feat: Customizable shared folder
1 parent f4db9da commit 16c93ed

File tree

4 files changed

+161
-37
lines changed

4 files changed

+161
-37
lines changed

src/renderer/lib/install.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,25 @@ export class InstallManager {
105105
composeContent.services.windows.volumes[storageFolderIdx] = `${this.conf.installFolder}:/storage`;
106106
}
107107

108-
// Home folder mapping
109-
if (!this.conf.shareHomeFolder) {
110-
const sharedFolderIdx = composeContent.services.windows.volumes.findIndex(vol => vol.includes("/shared"));
108+
// Shared folder mapping
109+
const sharedFolderIdx = composeContent.services.windows.volumes.findIndex(vol => vol.includes("/shared"));
110+
111+
if (!this.conf.sharedFolderPath) {
112+
// Remove shared folder if not enabled
113+
if (sharedFolderIdx !== -1) {
114+
composeContent.services.windows.volumes.splice(sharedFolderIdx, 1);
115+
logger.info("Removed shared folder as per user configuration");
116+
}
117+
} else {
118+
// Add or update shared folder
119+
const volumeStr = `${this.conf.sharedFolderPath}:/shared`;
120+
111121
if (sharedFolderIdx === -1) {
112-
logger.info("No home folder sharing volume found, nothing to remove");
122+
composeContent.services.windows.volumes.push(volumeStr);
123+
logger.info(`Added shared folder: ${this.conf.sharedFolderPath}`);
113124
} else {
114-
composeContent.services.windows.volumes.splice(sharedFolderIdx, 1);
115-
logger.info("Removed home folder sharing as per user configuration");
125+
composeContent.services.windows.volumes[sharedFolderIdx] = volumeStr;
126+
logger.info(`Updated shared folder to: ${this.conf.sharedFolderPath}`);
116127
}
117128
}
118129

src/renderer/views/Config.vue

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,39 @@
2727
v-model:value="numCores"
2828
/>
2929

30-
<!-- Shared Home Folder -->
30+
<!-- Shared Folder -->
3131
<ConfigCard
3232
icon="fluent:folder-link-32-filled"
33-
title="Shared Home Folder"
33+
title="Shared Folder"
3434
type="switch"
35-
v-model:value="shareHomeFolder"
35+
v-model:value="shareFolder"
3636
>
3737
<template v-slot:desc>
38-
If enabled, you will be able to access your Linux home folder within Windows under
38+
If enabled, you will be able to access your selected folder within Windows under
3939
<span class="font-mono bg-neutral-700 rounded-md px-1 py-0.5">Network\host.lan</span>
4040
</template>
4141
</ConfigCard>
4242

43+
<!-- Shared Folder Location -->
44+
<ConfigCard
45+
v-if="shareFolder"
46+
icon="mdi:folder-cog"
47+
title="Shared Folder Location"
48+
type="custom"
49+
>
50+
<template v-slot:desc>
51+
<span v-if="sharedFolderPath">
52+
Currently sharing: <span class="font-mono bg-neutral-700 rounded-md px-1 py-0.5">{{ sharedFolderPath }}</span>
53+
</span>
54+
<span v-else>
55+
Select a folder to share with Windows
56+
</span>
57+
</template>
58+
<x-button @click="selectSharedFolder">
59+
Browse
60+
</x-button>
61+
</ConfigCard>
62+
4363
<!-- Auto Start Container -->
4464
<ConfigCard
4565
icon="clarity:power-solid"
@@ -455,7 +475,8 @@ import {
455475
} from "../lib/constants";
456476
import { ComposePortEntry, ComposePortMapper, Range } from "../utils/port";
457477
const { app }: typeof import("@electron/remote") = require("@electron/remote");
458-
478+
const electron: typeof import("electron") = require("electron").remote || require("@electron/remote");
479+
const os: typeof import("os") = require("node:os");
459480
460481
// For Resources
461482
const compose = ref<ComposeConfig | null>(null);
@@ -465,8 +486,10 @@ const maxNumCores = ref(0);
465486
const ramGB = ref(0);
466487
const origRamGB = ref(0);
467488
const maxRamGB = ref(0);
468-
const origShareHomeFolder = ref(false);
469-
const shareHomeFolder = ref(false);
489+
const shareFolder = ref(false);
490+
const origShareFolder = ref(false);
491+
const sharedFolderPath = ref("");
492+
const origSharedFolderPath = ref("");
470493
const origAutoStartContainer = ref(false);
471494
const autoStartContainer = ref(false);
472495
const freerdpPort = ref(0);
@@ -490,7 +513,6 @@ const winboat = Winboat.getInstance();
490513
const usbManager = USBManager.getInstance();
491514
492515
// Constants
493-
const HOMEFOLDER_SHARE_STR = winboat.containerMgr!.defaultCompose.services.windows.volumes.find(v => v.startsWith("${HOME}"))!;
494516
const USB_BUS_PATH = "/dev/bus/usb:/dev/bus/usb";
495517
const QMP_ARGUMENT = "-qmp tcp:0.0.0.0:7149,server,wait=off"; // 7149 can remain hardcoded as it refers to a guest port
496518
@@ -512,8 +534,19 @@ async function assignValues() {
512534
ramGB.value = Number(compose.value.services.windows.environment.RAM_SIZE.split("G")[0]);
513535
origRamGB.value = ramGB.value;
514536
515-
shareHomeFolder.value = compose.value.services.windows.volumes.includes(HOMEFOLDER_SHARE_STR);
516-
origShareHomeFolder.value = shareHomeFolder.value;
537+
// Find any volume that ends with /shared
538+
const sharedVolume = compose.value.services.windows.volumes.find(v => v.includes("/shared"));
539+
if (sharedVolume) {
540+
shareFolder.value = true;
541+
// Extract the path before :/shared
542+
const [hostPath] = sharedVolume.split(":");
543+
sharedFolderPath.value = hostPath.replace("${HOME}", os.homedir());
544+
} else {
545+
shareFolder.value = false;
546+
sharedFolderPath.value = "";
547+
}
548+
origShareFolder.value = shareFolder.value;
549+
origSharedFolderPath.value = sharedFolderPath.value;
517550
518551
autoStartContainer.value = compose.value.services.windows.restart === RESTART_ON_FAILURE;
519552
origAutoStartContainer.value = autoStartContainer.value;
@@ -536,16 +569,20 @@ async function saveCompose() {
536569
compose.value!.services.windows.environment.RAM_SIZE = `${ramGB.value}G`;
537570
compose.value!.services.windows.environment.CPU_CORES = `${numCores.value}`;
538571
539-
const composeHasHomefolderShare = compose.value!.services.windows.volumes.includes(HOMEFOLDER_SHARE_STR);
540-
541-
if (shareHomeFolder.value && !composeHasHomefolderShare) {
542-
compose.value!.services.windows.volumes.push(HOMEFOLDER_SHARE_STR);
543-
} else if (!shareHomeFolder.value && composeHasHomefolderShare) {
572+
// Remove any existing shared volume
573+
const existingSharedVolume = compose.value!.services.windows.volumes.find(v => v.includes("/shared"));
574+
if (existingSharedVolume) {
544575
compose.value!.services.windows.volumes = compose.value!.services.windows.volumes.filter(
545-
v => v !== HOMEFOLDER_SHARE_STR,
576+
v => !v.includes("/shared"),
546577
);
547578
}
548579
580+
// Add the new shared volume if enabled
581+
if (shareFolder.value && sharedFolderPath.value) {
582+
const volumeStr = `${sharedFolderPath.value}:/shared`;
583+
compose.value!.services.windows.volumes.push(volumeStr);
584+
}
585+
549586
compose.value!.services.windows.restart = autoStartContainer.value ? RESTART_ON_FAILURE : RESTART_NO;
550587
551588
portMapper.value!.setShortPortMapping(GUEST_RDP_PORT, freerdpPort.value, {
@@ -572,6 +609,23 @@ async function saveCompose() {
572609
}
573610
}
574611
612+
/**
613+
* Opens a dialog to select a folder to share with Windows
614+
*/
615+
function selectSharedFolder() {
616+
electron.dialog
617+
.showOpenDialog({
618+
title: "Select Folder to Share",
619+
properties: ["openDirectory"],
620+
defaultPath: sharedFolderPath.value || os.homedir(),
621+
})
622+
.then(result => {
623+
if (!result.canceled && result.filePaths.length > 0) {
624+
sharedFolderPath.value = result.filePaths[0];
625+
}
626+
});
627+
}
628+
575629
/**
576630
* Adds the required fields for USB passthrough to work
577631
* to the Compose file if they don't already exist
@@ -668,7 +722,8 @@ const saveButtonDisabled = computed(() => {
668722
const hasResourceChanges =
669723
origNumCores.value !== numCores.value ||
670724
origRamGB.value !== ramGB.value ||
671-
shareHomeFolder.value !== origShareHomeFolder.value ||
725+
shareFolder.value !== origShareFolder.value ||
726+
sharedFolderPath.value !== origSharedFolderPath.value ||
672727
(!Number.isNaN(freerdpPort.value) && freerdpPort.value !== origFreerdpPort.value) ||
673728
autoStartContainer.value !== origAutoStartContainer.value;
674729
@@ -733,6 +788,13 @@ async function toggleExperimentalFeatures() {
733788
winboat.createQMPInterval();
734789
}
735790
}
791+
792+
// Watch for when shared folder is enabled and set default path
793+
watch(shareFolder, (newValue) => {
794+
if (newValue && !sharedFolderPath.value) {
795+
sharedFolderPath.value = os.homedir();
796+
}
797+
});
736798
</script>
737799

738800
<style scoped>

src/renderer/views/SetupUI.vue

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -607,34 +607,60 @@
607607
</div>
608608
</div>
609609

610-
<!-- Home Folder Sharing -->
610+
<!-- Folder Sharing -->
611611
<div v-if="currentStep.id === StepID.SHOULD_SHARE_HOME_FOLDER" class="step-block">
612-
<h1 class="text-3xl font-semibold">{{ currentStep.title }}</h1>
612+
<h1 class="text-3xl font-semibold">Folder Sharing</h1>
613613
<p class="text-lg text-gray-400">
614-
WinBoat allows you to share your Linux home folder with the Windows virtual machine, here
615-
you can choose whether to enable this feature or not.
614+
WinBoat allows you to share a folder from your Linux system with the Windows virtual machine.
615+
You can choose whether to enable this feature and select which folder to share.
616616
</p>
617617
<p class="text-lg text-gray-400">
618618
<b>⚠️ WARNING:</b>
619-
Sharing your home folder exposes your Linux files to Windows-specific malware and viruses.
619+
Sharing a folder exposes your Linux files to Windows-specific malware and viruses.
620620
Only enable this feature if you understand the risks involved. Always be careful with the
621621
files you download and open in Windows.
622622
</p>
623623

624624
<x-checkbox
625625
class="my-4"
626-
@toggle="homeFolderSharing = !homeFolderSharing"
627-
:toggled="homeFolderSharing"
626+
@toggle="folderSharing = !folderSharing"
627+
:toggled="folderSharing"
628628
>
629-
<x-label><strong>Enable home folder sharing</strong></x-label>
629+
<x-label><strong>Enable folder sharing</strong></x-label>
630630
<x-label class="text-gray-400">
631631
By checking this box, you acknowledge the risks mentioned above
632632
</x-label>
633633
</x-checkbox>
634634

635+
<div v-if="folderSharing" class="flex flex-col gap-2 my-4">
636+
<label class="text-sm text-neutral-400">Shared Folder Location</label>
637+
<div class="flex flex-row items-center">
638+
<x-input
639+
type="text"
640+
placeholder="Select Folder to Share"
641+
readonly
642+
:value="sharedFolderPath"
643+
class="!max-w-full w-[300px] rounded-r-none"
644+
>
645+
<x-icon href="#folder"></x-icon>
646+
<x-label>/your/shared/folder</x-label>
647+
</x-input>
648+
<x-button class="!rounded-l-none" toggled @click="selectSharedFolder">
649+
{{ sharedFolderPath ? "Change" : "Select" }}
650+
</x-button>
651+
</div>
652+
</div>
653+
635654
<div class="flex flex-row gap-4 mt-6">
636655
<x-button class="px-6" @click="currentStepIdx--">Back</x-button>
637-
<x-button toggled class="px-6" @click="currentStepIdx++">Next</x-button>
656+
<x-button
657+
toggled
658+
class="px-6"
659+
@click="currentStepIdx++"
660+
:disabled="folderSharing && !sharedFolderPath"
661+
>
662+
Next
663+
</x-button>
638664
</div>
639665
</div>
640666

@@ -783,7 +809,7 @@
783809

784810
<script setup lang="ts">
785811
import { Icon } from "@iconify/vue";
786-
import { computed, onMounted, onUnmounted, ref } from "vue";
812+
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
787813
import { useRouter } from "vue-router";
788814
import { computedAsync } from "@vueuse/core";
789815
import { InstallConfiguration, Specs } from "../../types";
@@ -864,7 +890,7 @@ const steps: Step[] = [
864890
},
865891
{
866892
id: StepID.SHOULD_SHARE_HOME_FOLDER,
867-
title: "Home Folder Sharing",
893+
title: "Folder Sharing",
868894
icon: "line-md:link",
869895
},
870896
{
@@ -904,7 +930,8 @@ const diskSpaceGB = ref(32);
904930
const username = ref("winboat");
905931
const password = ref("");
906932
const confirmPassword = ref("");
907-
const homeFolderSharing = ref(false);
933+
const folderSharing = ref(false);
934+
const sharedFolderPath = ref("");
908935
const installState = ref<InstallStates>(InstallStates.IDLE);
909936
const preinstallMsg = ref("");
910937
const containerRuntime = ref(ContainerRuntimes.DOCKER);
@@ -926,6 +953,9 @@ onMounted(async () => {
926953
927954
username.value = os.userInfo().username;
928955
console.log("Username", username.value);
956+
957+
// Set default shared folder path to home directory
958+
sharedFolderPath.value = os.homedir();
929959
});
930960
931961
onUnmounted(() => {
@@ -934,6 +964,13 @@ onUnmounted(() => {
934964
}
935965
});
936966
967+
// Watch for when folder sharing is enabled and set default path
968+
watch(folderSharing, (newValue) => {
969+
if (newValue && !sharedFolderPath.value) {
970+
sharedFolderPath.value = os.homedir();
971+
}
972+
});
973+
937974
const containerSpecs = computedAsync(async () => {
938975
return await getContainerSpecs(containerRuntime.value);
939976
});
@@ -1069,6 +1106,20 @@ const installFolderDiskSpaceGB = computedAsync(async () => {
10691106
return freeGB;
10701107
});
10711108
1109+
function selectSharedFolder() {
1110+
electron.dialog
1111+
.showOpenDialog({
1112+
title: "Select Folder to Share",
1113+
properties: ["openDirectory"],
1114+
defaultPath: sharedFolderPath.value || os.homedir(),
1115+
})
1116+
.then(result => {
1117+
if (!result.canceled && result.filePaths.length > 0) {
1118+
sharedFolderPath.value = result.filePaths[0];
1119+
}
1120+
});
1121+
}
1122+
10721123
function install() {
10731124
const installConfig: InstallConfiguration = {
10741125
windowsVersion: windowsVersion.value,
@@ -1079,7 +1130,7 @@ function install() {
10791130
diskSpaceGB: diskSpaceGB.value,
10801131
username: username.value,
10811132
password: password.value,
1082-
shareHomeFolder: homeFolderSharing.value,
1133+
sharedFolderPath: folderSharing.value ? sharedFolderPath.value : undefined,
10831134
...(customIsoPath.value ? { customIsoPath: customIsoPath.value } : {}),
10841135
container: containerRuntime.value, // Hardcdde for now
10851136
};

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export type InstallConfiguration = {
1919
username: string;
2020
password: string;
2121
customIsoPath?: string;
22-
shareHomeFolder: boolean;
22+
sharedFolderPath?: string;
2323
container: ContainerRuntimes;
2424
};
2525

0 commit comments

Comments
 (0)