Просмотр исходного кода

Extract archive with local software if it's a .7z or .cb7 file

Ruben 1 год назад
Родитель
Сommit
d7770f9219

+ 16 - 10
src/PicView.Avalonia.MacOS/App.axaml.cs

@@ -1,21 +1,21 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Threading;
-using PicView.Avalonia.MacOS.Views;
-using PicView.Avalonia.ViewModels;
-using PicView.Core.Config;
-using PicView.Core.FileHandling;
-using PicView.Core.Localization;
-using System;
+using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
 using System.Runtime;
 using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Threading;
 using PicView.Avalonia.Interfaces;
+using PicView.Avalonia.MacOS.Views;
 using PicView.Avalonia.Navigation;
 using PicView.Avalonia.UI;
+using PicView.Avalonia.ViewModels;
+using PicView.Core.Config;
+using PicView.Core.FileHandling;
+using PicView.Core.Localization;
 
 namespace PicView.Avalonia.MacOS;
 
@@ -276,4 +276,10 @@ public void ShowAboutWindow()
     {
         // TODO: Implement CopyFile
     }
+    
+    public Task<bool> ExtractWithLocalSoftwareAsync(string path, string tempDirectory)
+    {
+        // TODO: Implement ExtractWithLocalSoftwareAsync
+        return Task.FromResult(false);
+    }
 }

+ 5 - 0
src/PicView.Avalonia.Win32/App.axaml.cs

@@ -290,5 +290,10 @@ public class App : Application, IPlatformSpecificService
         ClipboardHelper.CopyFileToClipboard(path);
     }
     
+    public async Task<bool> ExtractWithLocalSoftwareAsync(string path, string tempDirectory)
+    {
+        return await ArchiveExtractionHelper.ExtractWithLocalSoftwareAsync(path, tempDirectory);
+    }
+    
     #endregion
 }

+ 2 - 0
src/PicView.Avalonia/Interfaces/IPlatformSpecificService.cs

@@ -34,4 +34,6 @@ public interface IPlatformSpecificService
     void SetAsLockScreen(string path);
     
     void CopyFile(string path);
+    
+    Task<bool> ExtractWithLocalSoftwareAsync(string path, string tempDirectory);
 }

+ 10 - 1
src/PicView.Avalonia/Navigation/NavigationHelper.cs

@@ -256,9 +256,18 @@ public static class NavigationHelper
         await PreviewPicAndLoadGallery(fileInfo, vm);
     }
 
+    /// <summary>
+    /// Asynchronously loads a picture from a specified archive file.
+    /// </summary>
+    /// <param name="path">The path to the archive file containing the picture(s) to load.</param>
+    /// <param name="vm">The main view model instance used to manage UI state and operations.</param>
+    /// <returns>
+    /// A task representing the asynchronous operation. This task completes when the picture is loaded
+    /// from the archive or when an error occurs during the extraction or loading process.
+    /// </returns>
     public static async Task LoadPicFromArchiveAsync(string path, MainViewModel vm)
     {
-        var extraction = await ArchiveExtraction.ExtractArchiveAsync(path);
+        var extraction = await ArchiveExtraction.ExtractArchiveAsync(path, vm.PlatformService.ExtractWithLocalSoftwareAsync);
         if (!extraction)
         {
             await ErrorHandling.ReloadAsync(vm);

+ 37 - 6
src/PicView.Core/ArchiveHandling/ArchiveExtraction.cs

@@ -13,17 +13,42 @@ public static class ArchiveExtraction
     /// Gets the path of the temporary directory where the archive contents are extracted.
     /// </summary>
     public static string? TempZipDirectory { get; private set; }
-    
+
     /// <summary>
     /// Asynchronously extracts supported files from a given archive to a temporary directory.
     /// </summary>
-    /// <param name="archivePath">The path of the archive file to extract.</param>
+    /// <param name="archivePath">
+    /// The path of the archive file to extract. The method throws an <see cref="ArgumentException"/>
+    /// if this path is null, empty, or the file does not exist.
+    /// </param>
+    /// <param name="extractWithLocalSoftwareAsync">
+    /// A delegate function that attempts to extract the archive using local software (e.g., 7-Zip, WinRAR).
+    /// This function should return a boolean value indicating whether the extraction was successful.
+    /// It takes two parameters: the path to the archive and the path to the temporary extraction directory.
+    /// </param>
     /// <returns>
-    /// A task that represents the asynchronous operation. The task result contains a boolean value:
-    /// <c>true</c> if any supported files were extracted successfully; otherwise, <c>false</c>.
+    /// A task representing the asynchronous operation. The task result is a boolean:
+    /// <c>true</c> if any supported files were successfully extracted; otherwise, <c>false</c>.
     /// </returns>
-    /// <exception cref="ArgumentException">Thrown when the <paramref name="archivePath"/> is null, empty, or the file does not exist.</exception>
-    public static async Task<bool> ExtractArchiveAsync(string archivePath)
+    /// <exception cref="ArgumentException">
+    /// Thrown when the <paramref name="archivePath"/> is null, empty, or the file does not exist.
+    /// </exception>
+    /// <exception cref="IOException">
+    /// Thrown if there is an I/O error during the extraction process (e.g., issues with reading or writing files).
+    /// </exception>
+    /// <exception cref="UnauthorizedAccessException">
+    /// Thrown if the extraction process encounters access issues (e.g., insufficient permissions to access files or directories).
+    /// </exception>
+    /// <exception cref="Exception">
+    /// A general exception that can be thrown for any other unexpected issues during extraction.
+    /// </exception>
+    /// <remarks>
+    /// The method first checks the file extension of the archive to determine if it should be extracted using local software.
+    /// If the file is supported by the local software (e.g., 7z, cb7), it delegates the extraction to the provided
+    /// <paramref name="extractWithLocalSoftwareAsync"/> function. If the archive is in another format, it uses SharpCompress
+    /// to read and extract supported files asynchronously.
+    /// </remarks>
+    public static async Task<bool> ExtractArchiveAsync(string archivePath, Func<string, string, Task<bool>> extractWithLocalSoftwareAsync)
     {
         try
         {
@@ -36,6 +61,12 @@ public static class ArchiveExtraction
             Directory.CreateDirectory(tempDirectory);
             TempZipDirectory = tempDirectory;
             
+            var ext = Path.GetExtension(archivePath);
+            if (ext.Equals(".7z", StringComparison.OrdinalIgnoreCase) || ext.Equals(".cb7", StringComparison.OrdinalIgnoreCase))
+            {
+                return await extractWithLocalSoftwareAsync(archivePath, tempDirectory);
+            }
+            
             await using var stream = File.OpenRead(archivePath);
             using var reader = ReaderFactory.Open(stream);
 

+ 154 - 0
src/PicView.Windows/FileHandling/ArchiveExtractionHelper.cs

@@ -0,0 +1,154 @@
+using System.Diagnostics;
+using Microsoft.Win32;
+using PicView.Core.FileHandling;
+
+namespace PicView.Windows.FileHandling;
+
+public static class ArchiveExtractionHelper
+{
+    private const string Winrar = "WinRAR.exe";
+    private const string SevenZip = "7z.exe";
+    private const string Nanazip = "NanaZipG.exe";
+    
+    public static async Task<bool> ExtractWithLocalSoftwareAsync(string pathToArchiveFile, string tempDirectory)
+    {
+        var appNames = new[] { Winrar, SevenZip, Nanazip };
+        var appPathNames = new[] { $@"\WinRAR\{Winrar}", $@"\7-Zip\{SevenZip}", $@"\Nanazip\{Nanazip}" };
+        
+        var appPath = GetExtractAppPath(appPathNames, appNames);
+        if (appPath == null)
+        {
+            return false;
+        }
+        
+        var is7Zip = appPath.Contains("7z", StringComparison.OrdinalIgnoreCase);
+        var isNanazip = appPath.Contains("Nanazip", StringComparison.OrdinalIgnoreCase);
+        var isWinrar = appPath.Contains("WinRAR", StringComparison.OrdinalIgnoreCase);
+        if (!is7Zip && !isNanazip && !isWinrar)
+        {
+            return false;
+        }
+        
+        string arguments;
+        if (is7Zip || isNanazip)
+        {
+            arguments = $"x \"{pathToArchiveFile}\" -o";
+        }
+        else if (isWinrar)
+        {
+            arguments = $"x -o- \"{pathToArchiveFile}\" ";
+        }
+        else
+        {
+            return false;
+        }
+
+        var supportedFilesFilter = " *" + string.Join(" *", SupportedFiles.FileExtensions) + " ";
+        arguments += tempDirectory + supportedFilesFilter + " -r -aou";
+        
+        var process = Process.Start(new ProcessStartInfo
+        {
+            FileName = appPath,
+            Arguments = arguments,
+            RedirectStandardOutput = true,
+            CreateNoWindow = false,
+            WorkingDirectory = Path.GetDirectoryName(appPath)
+        });
+
+        if (process == null)
+        {
+            return false;
+        }
+
+        await process.WaitForExitAsync();
+        
+        return process.ExitCode == 0;
+    }
+    
+    private static string? GetExtractAppPath(string[] commonPaths, string[] appNames)
+    {
+        if (appNames == null || commonPaths == null)
+        {
+            return null;
+        }
+
+        var x86Path = GetProgramFilePath(Environment.SpecialFolder.ProgramFilesX86, commonPaths);
+        if (!string.IsNullOrEmpty(x86Path))
+        {
+            return x86Path;
+        }
+
+        var x64Path = GetProgramFilePath(Environment.SpecialFolder.ProgramFiles, commonPaths);
+        if (!string.IsNullOrEmpty(x64Path))
+        {
+            return x64Path;
+        }
+
+        const string registryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
+        try
+        {
+            using var key = Registry.LocalMachine.OpenSubKey(registryKey);
+            if (key == null) return null;
+
+            foreach (var subKeyName in key.GetSubKeyNames())
+            {
+                using var subKey = key.OpenSubKey(subKeyName);
+                if (subKey == null) continue;
+
+                var installDir = subKey.GetValue("InstallLocation")?.ToString();
+                if (installDir == null) continue;
+
+                switch (subKeyName)
+                {
+                    case "7-Zip":
+                        return Path.Combine(installDir, SevenZip);
+                    case "WinRAR":
+                        return Path.Combine(installDir, Winrar);
+                    case "NanaZip":
+                        return Path.Combine(installDir, Nanazip);
+                }
+            }
+        }
+        catch (Exception e)
+        {
+#if DEBUG
+            Trace.WriteLine($"{nameof(GetExtractAppPath)} exception, \n {e.Message}");
+#endif
+            return appNames.Select(GetPathForExe).Where(registryPath => registryPath != null)
+                .FirstOrDefault(File.Exists);
+        }
+
+        return appNames.Select(GetPathForExe).Where(registryPath => registryPath != null)
+            .FirstOrDefault(File.Exists);
+
+        string GetProgramFilePath(Environment.SpecialFolder specialFolder, IEnumerable<string> paths)
+        {
+            foreach (var path in paths)
+            {
+                var fullPath = Environment.GetFolderPath(specialFolder) + path;
+                if (File.Exists(fullPath))
+                {
+                    return fullPath;
+                }
+            }
+
+            return string.Empty;
+        }
+    }
+        
+    private static string? GetPathForExe(string fileName)
+    {
+        var localMachine = Registry.LocalMachine;
+        var fileKey = localMachine.OpenSubKey(
+            $@"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\{fileName}");
+        var result = fileKey?.GetValue(string.Empty);
+        if (result == null)
+        {
+            return null;
+        }
+
+        fileKey.Close();
+
+        return (string)result;
+    }
+}