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); diff --git a/tests/Filee.Engines.Tests/EngineInstallerTests.cs b/tests/Filee.Engines.Tests/EngineInstallerTests.cs index 33f81084c..a28115b2e 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"); + Directory.CreateDirectory(Path.Combine(staging, "Program", "bin")); + var dll = Path.Combine(staging, "Program", "bin", "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() {