From 8f53ccd261efed144b70d59b74fe4b080689e22f Mon Sep 17 00:00:00 2001 From: Mohamed El Ghazali <48661966+mohamedelghazali@users.noreply.github.com> Date: Wed, 4 Nov 2026 14:36:43 +0100 Subject: [PATCH 1/4] Fix cleanup of read-only MSI staging files --- .../Infrastructure/EngineInstaller.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Filee.Engines/Infrastructure/EngineInstaller.cs b/src/Filee.Engines/Infrastructure/EngineInstaller.cs index ae69a1790..037d482be 100644 --- a/src/Filee.Engines/Infrastructure/EngineInstaller.cs +++ b/src/Filee.Engines/Infrastructure/EngineInstaller.cs @@ -386,10 +386,29 @@ private async Task UnpackAsync(EngineComponent component, string file, Cancellat finally { if (Directory.Exists(staging)) - Directory.Delete(staging, recursive: true); + DeleteStagingDirectory(staging); } } + /// + /// Removes read-only attributes before deleting an unpacking folder. Administrative MSI extraction preserves + /// Windows file attributes; Calibre includes read-only DLLs (e.g. amatch.dll), which make recursive deletion fail. + /// + internal static void DeleteStagingDirectory(string folder) + { + if (OperatingSystem.IsWindows()) + { + foreach (var file in Directory.EnumerateFiles(folder, "*", SearchOption.AllDirectories)) + { + var attributes = File.GetAttributes(file); + if ((attributes & FileAttributes.ReadOnly) != 0) + File.SetAttributes(file, attributes & ~FileAttributes.ReadOnly); + } + } + + Directory.Delete(folder, recursive: true); + } + internal static async Task UnpackTarAsync(string file, string target, string kind, CancellationToken cancellationToken) { Directory.CreateDirectory(target); From cae3f7c62765076cc48dab71e5807cd6d4fbde78 Mon Sep 17 00:00:00 2001 From: Mohamed El Ghazali <48661966+mohamedelghazali@users.noreply.github.com> Date: Wed, 4 Nov 2026 14:36:46 +0100 Subject: [PATCH 2/4] Test cleanup of read-only MSI staging files --- .../Filee.Engines.Tests/EngineInstallerTests.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/Filee.Engines.Tests/EngineInstallerTests.cs b/tests/Filee.Engines.Tests/EngineInstallerTests.cs index 33f81084c..8eefbb885 100644 --- a/tests/Filee.Engines.Tests/EngineInstallerTests.cs +++ b/tests/Filee.Engines.Tests/EngineInstallerTests.cs @@ -403,6 +403,22 @@ public async Task LibreOffice_extensions_go_into_the_installed_LibreOffice_and_n Assert.True(installer.IsInstalled(package)); } + [Fact] + public void Staging_cleanup_removes_read_only_MSI_files_on_Windows() + { + Assert.SkipUnless(OperatingSystem.IsWindows(), "Windows MSI packages preserve read-only file attributes."); + + var staging = Path.Combine(_root, "~staging"); + var nested = Directory.CreateDirectory(Path.Combine(staging, "Program", "bin")).FullName; + var dll = Path.Combine(nested, "amatch.dll"); + File.WriteAllText(dll, "MSI payload"); + File.SetAttributes(dll, File.GetAttributes(dll) | FileAttributes.ReadOnly); + + EngineInstaller.DeleteStagingDirectory(staging); + + Assert.False(Directory.Exists(staging)); + } + [Fact] public void LibreOffice_is_trimmed_to_what_headless_conversion_needs() { From af58471ccd38b7987657a11696f593a49beb54ee Mon Sep 17 00:00:00 2001 From: Mohamed El Ghazali <48661966+mohamedelghazali@users.noreply.github.com> Date: Wed, 4 Nov 2026 14:37:06 +0100 Subject: [PATCH 3/4] Avoid unused local in MSI cleanup test --- tests/Filee.Engines.Tests/EngineInstallerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Filee.Engines.Tests/EngineInstallerTests.cs b/tests/Filee.Engines.Tests/EngineInstallerTests.cs index 8eefbb885..29a78c760 100644 --- a/tests/Filee.Engines.Tests/EngineInstallerTests.cs +++ b/tests/Filee.Engines.Tests/EngineInstallerTests.cs @@ -409,7 +409,7 @@ public void Staging_cleanup_removes_read_only_MSI_files_on_Windows() Assert.SkipUnless(OperatingSystem.IsWindows(), "Windows MSI packages preserve read-only file attributes."); var staging = Path.Combine(_root, "~staging"); - var nested = Directory.CreateDirectory(Path.Combine(staging, "Program", "bin")).FullName; + Directory.CreateDirectory(Path.Combine(staging, "Program", "bin")); var dll = Path.Combine(nested, "amatch.dll"); File.WriteAllText(dll, "MSI payload"); File.SetAttributes(dll, File.GetAttributes(dll) | FileAttributes.ReadOnly); From 2164bf1dc2f834e54cdc6e0f870475e21a657d87 Mon Sep 17 00:00:00 2001 From: Mohamed El Ghazali <48661966+mohamedelghazali@users.noreply.github.com> Date: Wed, 4 Nov 2026 14:37:40 +0100 Subject: [PATCH 4/4] Fix path in read-only MSI cleanup test --- tests/Filee.Engines.Tests/EngineInstallerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Filee.Engines.Tests/EngineInstallerTests.cs b/tests/Filee.Engines.Tests/EngineInstallerTests.cs index 29a78c760..a28115b2e 100644 --- a/tests/Filee.Engines.Tests/EngineInstallerTests.cs +++ b/tests/Filee.Engines.Tests/EngineInstallerTests.cs @@ -410,7 +410,7 @@ public void Staging_cleanup_removes_read_only_MSI_files_on_Windows() var staging = Path.Combine(_root, "~staging"); Directory.CreateDirectory(Path.Combine(staging, "Program", "bin")); - var dll = Path.Combine(nested, "amatch.dll"); + var dll = Path.Combine(staging, "Program", "bin", "amatch.dll"); File.WriteAllText(dll, "MSI payload"); File.SetAttributes(dll, File.GetAttributes(dll) | FileAttributes.ReadOnly);