Skip to content

Commit 041ccc3

Browse files
feat: warn about missing mount drivers and default Windows to Dokan
On an interactive start the app now checks the driver required by the selected backend (Dokan/WinFsp on Windows, libfuse3/macFUSE elsewhere); when it is missing, a message box explains what is needed and offers to open the matching download page. Windows now defaults to Dokan (AppSettings.DefaultMountBackend); Auto still prefers WinFsp when installed. The Settings backend list offers only the platform's drivers - Dokan/WinFsp/Auto on Windows, FUSE/Auto on Linux/macOS - and a settings file moved between platforms is normalized on load (FUSE -> Dokan on Windows, Dokan/WinFsp -> FUSE elsewhere). New MountBackendAvailability helper centralizes effective-backend resolution, availability probing, display names and download URLs; MountService uses it for Auto resolution. README/docs/WhatsNew updated to explain that either Windows driver can be installed and how to install the desired one. Suite: 1447 tests, 0 warnings.
1 parent ea86cc4 commit 041ccc3

15 files changed

Lines changed: 375 additions & 47 deletions

‎ReadMe.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
A single application supports every filesystem backend:
2626

27-
* **Windows** - [WinFsp](https://github.com/winfsp/winfsp) or [DokanNet](https://github.com/dokan-dev/dokan-dotnet), selectable in Settings (Auto prefers WinFsp when installed).
27+
* **Windows** - [Dokan](https://github.com/dokan-dev/dokany) (default) or [WinFsp](https://github.com/winfsp/winfsp). Install whichever driver you prefer and select it in Settings → Mount backend; *Auto* picks WinFsp when installed, otherwise Dokan.
2828
* **Linux / macOS** - FUSE via the bundled FuseSharp library (libfuse3 on Linux, macFUSE on macOS).
2929

3030
The UI is built with [Avalonia](https://avaloniaui.net/) and shares one codebase across all platforms.
@@ -64,9 +64,9 @@ Unlike traditional archive utilities that extract the entire archive to a tempor
6464
Before running Simple Zip Drive, ensure your system meets the following requirements:
6565

6666
1. **.NET 10.0 Runtime:** Download the latest [.NET 10 runtime](https://dotnet.microsoft.com/download).
67-
2. **Filesystem Driver** (depends on your platform):
68-
* **Windows — Dokan backend:** Download and install `DokanSetup.exe` from the [Official Releases](https://github.com/dokan-dev/dokany/releases). **Dokan v2 2.3.0 or later is required** — older driver installs are refused with a *"Dokan Driver Outdated"* dialog.
69-
* **Windows — WinFsp backend:** Download and install [WinFsp](https://github.com/winfsp/winfsp/releases) **2.1 or later** (2.1 is the latest stable release; 2.2+ are beta versions). Keep `winfsp-msil.dll` beside the executable — it ships with the app and is removed neither by you nor your antivirus without breaking mounts.
67+
2. **Filesystem Driver** (depends on your platform). On Windows you can use **either Dokan or WinFsp** - install the driver for the backend you want to use (Dokan is the default; if the selected driver is missing, the app offers to open its download page):
68+
* **Windows — Dokan (default):** Download and install `DokanSetup.exe` from the [Official Releases](https://github.com/dokan-dev/dokany/releases). **Dokan v2 2.3.0 or later is required** — older driver installs are refused with a *"Dokan Driver Outdated"* dialog.
69+
* **Windows — WinFsp (alternative):** Download and install [WinFsp](https://github.com/winfsp/winfsp/releases) **2.1 or later** (2.1 is the latest stable release; 2.2+ are beta versions), then select *WinFsp* (or *Auto*) in *Settings → Mount backend*. Keep `winfsp-msil.dll` beside the executable — it ships with the app and is removed neither by you nor your antivirus without breaking mounts.
7070
* **Linux:** Install libfuse3 (e.g. `sudo apt install libfuse3-3` or `sudo dnf install fuse3`).
7171
* **macOS:** Install [macFUSE](https://macfuse.github.io/).
7272

@@ -76,7 +76,7 @@ Before running Simple Zip Drive, ensure your system meets the following requirem
7676

7777
| Platform | Backends | Notes |
7878
|:---------|:---------|:------|
79-
| **Windows** | **WinFsp**, **Dokan**, or **Auto** | Select in *Settings → Mount backend*. Auto prefers WinFsp when installed, otherwise Dokan. |
79+
| **Windows** | **Dokan** (default), **WinFsp**, or **Auto** | You can use either driver - install the one you want and select it in *Settings → Mount backend*; if it is missing, the app offers to open its download page. Auto prefers WinFsp when installed, otherwise Dokan. |
8080
| **Linux / macOS** | **FUSE** | libfuse3 / macFUSE required. Archives mount on folders. |
8181

8282
All backends share the same UI and feature set; only the underlying filesystem driver differs. See [Mount Backends](docs/variants.md) for the full comparison.

‎SimpleZipDrive.Tests/AppSettingsDefaultsTests.cs‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ namespace SimpleZipDrive.Tests;
1010
public class AppSettingsDefaultsTests
1111
{
1212
[Fact]
13-
public void MountBackend_Default_IsAuto()
13+
public void MountBackend_Default_IsPlatformDefault()
1414
{
15-
Assert.Equal(MountBackend.Auto, new AppSettings().MountBackend);
15+
Assert.Equal(AppSettings.DefaultMountBackend, new AppSettings().MountBackend);
16+
17+
// Windows defaults to Dokan; Linux/macOS default to Auto (which resolves to FUSE).
18+
Assert.Equal(
19+
OperatingSystem.IsWindows() ? MountBackend.Dokan : MountBackend.Auto,
20+
AppSettings.DefaultMountBackend);
1621
}
1722

1823
[Fact]
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using SimpleZipDrive.Core.Models;
2+
using SimpleZipDrive.Mounting;
3+
4+
namespace SimpleZipDrive.Tests;
5+
6+
/// <summary>
7+
/// Tests for <see cref="MountBackendAvailability" />: effective-backend resolution, the
8+
/// availability probe, and the user-facing names and download pages used by the startup
9+
/// driver warning.
10+
/// </summary>
11+
public class MountBackendAvailabilityTests
12+
{
13+
[Theory]
14+
[InlineData(MountBackend.Dokan)]
15+
[InlineData(MountBackend.WinFsp)]
16+
[InlineData(MountBackend.Fuse)]
17+
public void ResolveEffective_ExplicitBackend_IsReturnedUnchanged(MountBackend backend)
18+
{
19+
Assert.Equal(backend, MountBackendAvailability.ResolveEffective(backend));
20+
}
21+
22+
[Fact]
23+
public void ResolveEffective_Auto_MatchesPlatformPreference()
24+
{
25+
var resolved = MountBackendAvailability.ResolveEffective(MountBackend.Auto);
26+
27+
if (OperatingSystem.IsWindows())
28+
{
29+
Assert.Contains(resolved, new[] { MountBackend.Dokan, MountBackend.WinFsp });
30+
}
31+
else
32+
{
33+
Assert.Equal(MountBackend.Fuse, resolved);
34+
}
35+
}
36+
37+
[Fact]
38+
public void IsAvailable_FuseOnWindows_ReturnsFalseWithReason()
39+
{
40+
if (!OperatingSystem.IsWindows()) return;
41+
42+
Assert.False(MountBackendAvailability.IsAvailable(MountBackend.Fuse, out var reason));
43+
Assert.False(string.IsNullOrWhiteSpace(reason));
44+
}
45+
46+
[Fact]
47+
public void IsAvailable_UnknownBackend_ReturnsFalseWithReason()
48+
{
49+
Assert.False(MountBackendAvailability.IsAvailable((MountBackend)99, out var reason));
50+
Assert.Contains("Unknown mount backend", reason, StringComparison.OrdinalIgnoreCase);
51+
}
52+
53+
[Theory]
54+
[InlineData(MountBackend.Dokan, "Dokan")]
55+
[InlineData(MountBackend.WinFsp, "WinFsp")]
56+
[InlineData(MountBackend.Fuse, "FUSE")]
57+
public void DisplayName_ReturnsExpectedName(MountBackend backend, string expected)
58+
{
59+
Assert.Equal(expected, MountBackendAvailability.DisplayName(backend));
60+
}
61+
62+
[Theory]
63+
[InlineData(MountBackend.Dokan, "Dokan driver")]
64+
[InlineData(MountBackend.WinFsp, "WinFsp driver")]
65+
public void DriverName_WindowsDrivers_ReturnsExpectedName(MountBackend backend, string expected)
66+
{
67+
Assert.Equal(expected, MountBackendAvailability.DriverName(backend));
68+
}
69+
70+
[Fact]
71+
public void DriverName_Fuse_MatchesPlatform()
72+
{
73+
var expected = OperatingSystem.IsMacOS() ? "macFUSE" : "libfuse3";
74+
Assert.Equal(expected, MountBackendAvailability.DriverName(MountBackend.Fuse));
75+
}
76+
77+
[Theory]
78+
[InlineData(MountBackend.Dokan, "https://github.com/dokan-dev/dokany/releases")]
79+
[InlineData(MountBackend.WinFsp, "https://github.com/winfsp/winfsp/releases")]
80+
public void DownloadUrl_WindowsDrivers_ReturnsOfficialPage(MountBackend backend, string expected)
81+
{
82+
Assert.Equal(expected, MountBackendAvailability.DownloadUrl(backend));
83+
}
84+
85+
[Fact]
86+
public void DownloadUrl_Fuse_MatchesPlatform()
87+
{
88+
var url = MountBackendAvailability.DownloadUrl(MountBackend.Fuse);
89+
90+
if (OperatingSystem.IsMacOS())
91+
Assert.Equal("https://macfuse.github.io/", url);
92+
else
93+
Assert.Contains("installation", url, StringComparison.OrdinalIgnoreCase);
94+
}
95+
96+
[Fact]
97+
public void DefaultMountBackend_IsDokanOnWindowsAndAutoElsewhere()
98+
{
99+
var expected = OperatingSystem.IsWindows() ? MountBackend.Dokan : MountBackend.Auto;
100+
Assert.Equal(expected, AppSettings.DefaultMountBackend);
101+
}
102+
103+
[Fact]
104+
public void NormalizeMountBackend_PlatformInvalidValue_IsMappedToAvailableBackend()
105+
{
106+
if (OperatingSystem.IsWindows())
107+
{
108+
// FUSE is not offered on Windows; it falls back to the Windows default (Dokan).
109+
Assert.Equal(AppSettings.DefaultMountBackend, AppSettings.NormalizeMountBackend(MountBackend.Fuse));
110+
Assert.Equal(MountBackend.Dokan, AppSettings.NormalizeMountBackend(MountBackend.Dokan));
111+
Assert.Equal(MountBackend.WinFsp, AppSettings.NormalizeMountBackend(MountBackend.WinFsp));
112+
Assert.Equal(MountBackend.Auto, AppSettings.NormalizeMountBackend(MountBackend.Auto));
113+
}
114+
else
115+
{
116+
// Dokan/WinFsp are not offered on Linux/macOS; they fall back to FUSE.
117+
Assert.Equal(MountBackend.Fuse, AppSettings.NormalizeMountBackend(MountBackend.Dokan));
118+
Assert.Equal(MountBackend.Fuse, AppSettings.NormalizeMountBackend(MountBackend.WinFsp));
119+
Assert.Equal(MountBackend.Fuse, AppSettings.NormalizeMountBackend(MountBackend.Fuse));
120+
Assert.Equal(MountBackend.Auto, AppSettings.NormalizeMountBackend(MountBackend.Auto));
121+
}
122+
}
123+
}

‎SimpleZipDrive.Tests/MountBackendTests.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ public async Task Facade_UnmountWithoutMount_DoesNotThrow()
6060
}
6161

6262
[Fact]
63-
public void AppSettings_MountBackend_DefaultsToAuto()
63+
public void AppSettings_MountBackend_DefaultsToPlatformDefault()
6464
{
65-
Assert.Equal(MountBackend.Auto, new AppSettings().MountBackend);
65+
Assert.Equal(AppSettings.DefaultMountBackend, new AppSettings().MountBackend);
6666
}
6767

6868
[Fact]

‎SimpleZipDrive/Core/Models/AppSettings.cs‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,34 @@ public long MaxMemoryPerFileMb
7171
/// <summary>Gets or sets the default mount type used when opening archives.</summary>
7272
public MountType DefaultMountType { get; set; } = MountType.DriveLetter;
7373

74+
/// <summary>
75+
/// Gets the default mount backend for the current platform: <see cref="MountBackend.Dokan" />
76+
/// on Windows, <see cref="MountBackend.Auto" /> (FUSE) on Linux/macOS.
77+
/// </summary>
78+
public static MountBackend DefaultMountBackend =>
79+
OperatingSystem.IsWindows() ? MountBackend.Dokan : MountBackend.Auto;
80+
7481
/// <summary>
7582
/// Gets or sets which file-system driver is used to mount archives.
76-
/// <see cref="MountBackend.Auto" /> prefers WinFsp, then Dokan on Windows, and FUSE on Linux/macOS.
83+
/// Defaults to <see cref="DefaultMountBackend" />; <see cref="MountBackend.Auto" /> prefers
84+
/// WinFsp, then Dokan on Windows, and FUSE on Linux/macOS.
85+
/// </summary>
86+
public MountBackend MountBackend { get; set; } = DefaultMountBackend;
87+
88+
/// <summary>
89+
/// Maps a backend that is not available on the current platform to one that is:
90+
/// FUSE becomes <see cref="DefaultMountBackend" /> (Dokan) on Windows, and Dokan/WinFsp
91+
/// become FUSE on Linux/macOS. Auto and valid values are returned unchanged.
7792
/// </summary>
78-
public MountBackend MountBackend { get; set; } = MountBackend.Auto;
93+
/// <param name="backend">The backend to normalize.</param>
94+
/// <returns>A backend usable on the current platform.</returns>
95+
internal static MountBackend NormalizeMountBackend(MountBackend backend)
96+
{
97+
if (OperatingSystem.IsWindows())
98+
return backend == MountBackend.Fuse ? DefaultMountBackend : backend;
99+
100+
return backend is MountBackend.Dokan or MountBackend.WinFsp ? MountBackend.Fuse : backend;
101+
}
79102

80103
/// <summary>Gets or sets a value indicating whether the mounted drive should be opened in Explorer automatically.</summary>
81104
public bool AutoOpenMountedDrive { get; set; }
@@ -149,6 +172,10 @@ public static AppSettings Load()
149172
// Return validated settings or defaults
150173
settings ??= new AppSettings();
151174

175+
// A settings file can travel between platforms; keep the backend valid for this OS
176+
// (FUSE is not available on Windows, Dokan/WinFsp are not available elsewhere).
177+
settings.MountBackend = NormalizeMountBackend(settings.MountBackend);
178+
152179
// Re-apply validation in case the loaded value exceeds current system limits
153180
var availableMemoryBytes = GC.GetGCMemoryInfo().TotalAvailableMemoryBytes;
154181
var availableMemoryMb = availableMemoryBytes / 1024 / 1024;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
using SimpleZipDrive.Mounting.Dokan;
2+
using SimpleZipDrive.Mounting.Fuse;
3+
using SimpleZipDrive.Mounting.WinFsp;
4+
5+
namespace SimpleZipDrive.Mounting;
6+
7+
/// <summary>
8+
/// Resolves the effective mount backend for the current platform, reports whether its
9+
/// driver is installed, and supplies the download/installation page to offer the user.
10+
/// </summary>
11+
internal static class MountBackendAvailability
12+
{
13+
private const string DokanDownloadUrl = "https://github.com/dokan-dev/dokany/releases";
14+
private const string WinFspDownloadUrl = "https://github.com/winfsp/winfsp/releases";
15+
private const string MacFuseDownloadUrl = "https://macfuse.github.io/";
16+
private const string FuseInstallInstructionsUrl =
17+
"https://github.com/purelogiccode/SimpleZipDrive/blob/master/docs/installation.md";
18+
19+
/// <summary>
20+
/// Resolves <see cref="MountBackend.Auto" /> to the platform's preferred backend
21+
/// (WinFsp → Dokan on Windows, FUSE elsewhere) and returns every other value unchanged.
22+
/// </summary>
23+
/// <param name="requested">The backend selected in settings.</param>
24+
/// <returns>The backend that will actually be used.</returns>
25+
public static MountBackend ResolveEffective(MountBackend requested)
26+
{
27+
if (requested != MountBackend.Auto)
28+
return requested;
29+
30+
if (!OperatingSystem.IsWindows())
31+
return MountBackend.Fuse;
32+
33+
return WinFspMountService.IsAvailable(out _) ? MountBackend.WinFsp : MountBackend.Dokan;
34+
}
35+
36+
/// <summary>
37+
/// Determines whether the driver for the given backend is installed and usable.
38+
/// </summary>
39+
/// <param name="backend">The backend to check.</param>
40+
/// <param name="reason">When unavailable, a human-readable explanation; otherwise <see langword="null" />.</param>
41+
/// <returns><see langword="true" /> when the backend can be used.</returns>
42+
public static bool IsAvailable(MountBackend backend, out string? reason)
43+
{
44+
switch (backend)
45+
{
46+
case MountBackend.Dokan:
47+
if (!OperatingSystem.IsWindows())
48+
{
49+
reason = "Dokan is only available on Windows.";
50+
return false;
51+
}
52+
53+
return DokanMountService.IsAvailable(out reason);
54+
55+
case MountBackend.WinFsp:
56+
if (!OperatingSystem.IsWindows())
57+
{
58+
reason = "WinFsp is only available on Windows.";
59+
return false;
60+
}
61+
62+
return WinFspMountService.IsAvailable(out reason);
63+
64+
case MountBackend.Fuse:
65+
return FuseMountService.IsAvailable(out reason);
66+
67+
default:
68+
reason = $"Unknown mount backend '{backend}'.";
69+
return false;
70+
}
71+
}
72+
73+
/// <summary>Returns the user-facing name of a backend (e.g. "Dokan", "FUSE").</summary>
74+
/// <param name="backend">The backend.</param>
75+
/// <returns>The display name.</returns>
76+
public static string DisplayName(MountBackend backend)
77+
{
78+
return backend switch
79+
{
80+
MountBackend.Dokan => "Dokan",
81+
MountBackend.WinFsp => "WinFsp",
82+
MountBackend.Fuse => "FUSE",
83+
_ => backend.ToString()
84+
};
85+
}
86+
87+
/// <summary>Returns the name of the driver a backend needs (e.g. "Dokan driver", "macFUSE").</summary>
88+
/// <param name="backend">The backend.</param>
89+
/// <returns>The driver name.</returns>
90+
public static string DriverName(MountBackend backend)
91+
{
92+
return backend switch
93+
{
94+
MountBackend.Dokan => "Dokan driver",
95+
MountBackend.WinFsp => "WinFsp driver",
96+
MountBackend.Fuse => OperatingSystem.IsMacOS() ? "macFUSE" : "libfuse3",
97+
_ => DisplayName(backend)
98+
};
99+
}
100+
101+
/// <summary>Returns the download/installation page to offer when a backend is missing.</summary>
102+
/// <param name="backend">The backend.</param>
103+
/// <returns>The URL to open.</returns>
104+
public static string DownloadUrl(MountBackend backend)
105+
{
106+
return backend switch
107+
{
108+
MountBackend.Dokan => DokanDownloadUrl,
109+
MountBackend.WinFsp => WinFspDownloadUrl,
110+
MountBackend.Fuse => OperatingSystem.IsMacOS() ? MacFuseDownloadUrl : FuseInstallInstructionsUrl,
111+
_ => FuseInstallInstructionsUrl
112+
};
113+
}
114+
}

‎SimpleZipDrive/Mounting/MountService.cs‎

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -155,18 +155,10 @@ public void Dispose()
155155

156156
if (requested == MountBackend.Auto)
157157
{
158-
if (OperatingSystem.IsWindows())
159-
{
160-
requested = WinFspMountService.IsAvailable(out _)
161-
? MountBackend.WinFsp
162-
: MountBackend.Dokan;
163-
_loggingService.Log($"Mount backend: auto-selected {requested}.");
164-
}
165-
else
166-
{
167-
requested = MountBackend.Fuse;
168-
_loggingService.Log("Mount backend: FUSE (only backend available on this platform).");
169-
}
158+
requested = MountBackendAvailability.ResolveEffective(requested);
159+
_loggingService.Log(requested == MountBackend.Fuse
160+
? "Mount backend: FUSE (only backend available on this platform)."
161+
: $"Mount backend: auto-selected {requested}.");
170162
}
171163

172164
switch (requested)

0 commit comments

Comments
 (0)