Skip to content

Commit

Permalink
feat: a new xo command added to CLI to perform extraction while ove…
Browse files Browse the repository at this point in the history
…rwriting duplicates and preserving the original folder structure (#212)

* Added the ability to remove duplicate files using the xo parameter

* Added test methods and expected results files for overwrite extraction while maintaining folder structure

* Improved the logic for deleting duplicate files

* Moved ExtractionMode file from CLI project to Core project

* updated extraction logic to match new duplicate files design

* Updated test files to match the new order of the new duplicate files design

* Amended the code according to Codecy comment about optional param

* change default extraction name to "Default" instead of "None" for better description and readability

* chore: suggested simplification proposal

* Fixed code typos in getExtractionMode method

* Fixed Codecy code comment

---------

Co-authored-by: Scott Willeke <[email protected]>
  • Loading branch information
mega5800 and activescott authored Jan 8, 2025
1 parent c3b568b commit c5944b4
Show file tree
Hide file tree
Showing 12 changed files with 805 additions and 29 deletions.
18 changes: 12 additions & 6 deletions src/LessMsi.Cli/ExtractCommand.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using LessMsi.Msi;
using NDesk.Options;

namespace LessMsi.Cli
Expand Down Expand Up @@ -36,18 +37,23 @@ public override void Run(List<string> allArgs)
private ExtractionMode getExtractionMode(string commandArgument)
{
commandArgument = commandArgument.ToLowerInvariant();
ExtractionMode extractionMode = ExtractionMode.PreserveDirectoriesExtraction;

if (commandArgument[commandArgument.Length - 1] == 'o')
if (commandArgument == "xfo")
{
extractionMode = ExtractionMode.OverwriteFlatExtraction;
return ExtractionMode.OverwriteFlatExtraction;
}
else if (commandArgument[commandArgument.Length - 1] == 'r')

if (commandArgument == "xfr")
{
return ExtractionMode.RenameFlatExtraction;
}

if (commandArgument == "xo")
{
extractionMode = ExtractionMode.RenameFlatExtraction;
return ExtractionMode.OverwriteExtraction;
}

return extractionMode;
return ExtractionMode.PreserveDirectoriesExtraction;
}
}
}
1 change: 0 additions & 1 deletion src/LessMsi.Cli/LessMsi.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
<Compile Include="..\CommonAssemblyInfo.cs">
<Link>Properties\CommonAssemblyInfo.cs</Link>
</Compile>
<Compile Include="ExtractionMode.cs" />
<Compile Include="ExtractCommand.cs" />
<Compile Include="LessMsiCommand.cs" />
<Compile Include="ListTableCommand.cs" />
Expand Down
9 changes: 4 additions & 5 deletions src/LessMsi.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,10 @@
// Scott Willeke ([email protected])
//
#region Using directives

using System;
using System.Collections.Generic;
using System.IO;
using LessMsi.Msi;

#endregion

namespace LessMsi.Cli
Expand Down Expand Up @@ -63,6 +61,7 @@ public static int Main(string[] args)
var subcommands = new Dictionary<string, LessMsiCommand> {
{"o", new OpenGuiCommand()},
{"x", extractCommand},
{"xo", extractCommand},
{"xfo", extractCommand},
{"xfr", extractCommand},
{"/x", extractCommand},
Expand Down Expand Up @@ -107,7 +106,7 @@ public static int Main(string[] args)
/// <param name="msiFileName">The path of the specified MSI file.</param>
/// <param name="outDirName">The directory to extract to. If empty it will use the current directory.</param>
/// <param name="filesToExtract">The files to be extracted from the msi. If empty all files will be extracted.</param>
/// /// <param name="extractionMode">Enum value for files extraction without folder structure</param>
/// <param name="extractionMode">Enum value for files extraction without folder structure</param>
public static void DoExtraction(string msiFileName, string outDirName, List<string> filesToExtract, ExtractionMode extractionMode)
{
msiFileName = EnsureAbsolutePath(msiFileName);
Expand All @@ -127,7 +126,7 @@ public static void DoExtraction(string msiFileName, string outDirName, List<stri
if (isExtractionModeFlat(extractionMode))
{
string tempOutDirName = $"{outDirName}{TempFolderSuffix}";
Wixtracts.ExtractFiles(msiFile, tempOutDirName, filesToExtract.ToArray(), PrintProgress);
Wixtracts.ExtractFiles(msiFile, tempOutDirName, filesToExtract.ToArray(), PrintProgress, extractionMode);

var fileNameCountingDict = new Dictionary<string, int>();

Expand All @@ -138,7 +137,7 @@ public static void DoExtraction(string msiFileName, string outDirName, List<stri
}
else
{
Wixtracts.ExtractFiles(msiFile, outDirName, filesToExtract.ToArray(), PrintProgress);
Wixtracts.ExtractFiles(msiFile, outDirName, filesToExtract.ToArray(), PrintProgress, extractionMode);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/LessMsi.Core/LessMsi.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
<ItemGroup>
<Compile Include="Msi\ColumnInfo.cs" />
<Compile Include="Msi\ExternalCabNotFoundException.cs" />
<Compile Include="Msi\ExtractionMode.cs" />
<Compile Include="Msi\MsiDatabase.cs" />
<Compile Include="Msi\MsiDirectory.cs" />
<Compile Include="Msi\MsiFile.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
namespace LessMsi.Cli
namespace LessMsi.Msi
{
public enum ExtractionMode
{
/// <summary>
/// Default value indicating that no extraction should be performed.
/// Default value indicating that a regular extraction should be performed.
/// </summary>
None,
Default,
/// <summary>
/// Value indicating that a file extraction preserving directories should be performed.
/// </summary>
Expand All @@ -17,6 +17,11 @@ public enum ExtractionMode
/// <summary>
/// Value indicating that a file extraction overwriting identical files should be performed.
/// </summary>
OverwriteFlatExtraction
OverwriteFlatExtraction,
/// <summary>
/// Value indicating that a file extraction overwriting identical files should be performed.
/// While preserving the directories structures
/// </summary>
OverwriteExtraction
}
}
43 changes: 31 additions & 12 deletions src/LessMsi.Core/Msi/Wixtracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,19 +205,19 @@ public enum ExtractionActivity

public static void ExtractFiles(Path msi, string outputDir)
{
ExtractFiles(msi, outputDir, new string[0], null);
ExtractFiles(msi, outputDir, new string[0], null, ExtractionMode.Default);
}

public static void ExtractFiles(Path msi, string outputDir, string[] fileNamesToExtract)
{
var msiFiles = GetMsiFileFromFileNames(msi, fileNamesToExtract);
ExtractFiles(msi, outputDir, msiFiles, null);
ExtractFiles(msi, outputDir, msiFiles, null, ExtractionMode.Default);
}

public static void ExtractFiles(Path msi, string outputDir, string[] fileNamesToExtract, AsyncCallback progressCallback)
public static void ExtractFiles(Path msi, string outputDir, string[] fileNamesToExtract, AsyncCallback progressCallback, ExtractionMode extractionMode)
{
var msiFiles = GetMsiFileFromFileNames(msi, fileNamesToExtract);
ExtractFiles(msi, outputDir, msiFiles, progressCallback);
ExtractFiles(msi, outputDir, msiFiles, progressCallback, extractionMode);
}

private static MsiFile[] GetMsiFileFromFileNames(Path msi, string[] fileNamesToExtract)
Expand Down Expand Up @@ -256,13 +256,19 @@ int IComparer.Compare(object x, object y)
}
}

/// <summary>
/// <summary>
/// Extracts the compressed files from the specified MSI file to the specified output directory.
/// If specified, the list of <paramref name="filesToExtract"/> objects are the only files extracted.
/// </summary>
/// <param name="filesToExtract">The files to extract or null or empty to extract all files.</param>
/// <param name="progressCallback">Will be called during during the operation with progress information, and upon completion. The argument will be of type <see cref="ExtractionProgress"/>.</param>
public static void ExtractFiles(Path msi, string outputDir, MsiFile[] filesToExtract, AsyncCallback progressCallback)
/// <param name="extractionMode">Enum value for files extraction without folder structure</param>
public static void ExtractFiles(
Path msi,
string outputDir,
MsiFile[] filesToExtract,
AsyncCallback progressCallback,
ExtractionMode extractionMode)
{
if (msi.IsEmpty)
throw new ArgumentNullException("msi");
Expand All @@ -289,7 +295,8 @@ public static void ExtractFiles(Path msi, string outputDir, MsiFile[] filesToExt

progress.ReportProgress(ExtractionActivity.Initializing, "", filesExtractedSoFar);
var outputDirPath = new Path(outputDir);
if (!FileSystem.Exists(outputDirPath)) {
if (!FileSystem.Exists(outputDirPath))
{
FileSystem.CreateDirectory(outputDirPath);
}

Expand Down Expand Up @@ -326,9 +333,10 @@ public static void ExtractFiles(Path msi, string outputDir, MsiFile[] filesToExt
progress.ReportProgress(ExtractionActivity.ExtractingFile, entry.LongFileName, filesExtractedSoFar);
string targetDirectoryForFile = GetTargetDirectory(outputDir, entry.Directory);
LessIO.Path destName = LessIO.Path.Combine(targetDirectoryForFile, entry.LongFileName);
if (FileSystem.Exists(destName))
if (IsExtractionModeAllowsDuplicates(extractionMode) && FileSystem.Exists(destName))
{
Debug.Fail(string.Format("output file '{0}' already exists. We'll make it unique, but this is probably a strange msi or a bug in this program.", destName));

//make unique
// ReSharper disable HeuristicUnreachableCode
Trace.WriteLine(string.Concat("Duplicate file found \'", destName, "\'"));
Expand Down Expand Up @@ -371,11 +379,22 @@ public static void ExtractFiles(Path msi, string outputDir, MsiFile[] filesToExt
progress.ReportProgress(ExtractionActivity.Complete, "", filesExtractedSoFar);
}
}
/// <summary>
/// Checks if given extraction mode value allows duplicate files during extraction
/// </summary>
/// <param name="extractionMode"></param>
/// <returns>Boolean flag for using duplicate files</returns>
private static bool IsExtractionModeAllowsDuplicates(ExtractionMode extractionMode)
{
bool duplicateFilesFlag = extractionMode != ExtractionMode.OverwriteExtraction;

/// <summary>
/// Deletes a file even if it is readonly.
/// </summary>
private static void DeleteFileForcefully(Path localFilePath)
return duplicateFilesFlag;
}

/// <summary>
/// Deletes a file even if it is readonly.
/// </summary>
private static void DeleteFileForcefully(Path localFilePath)
{
// In github issue #4 found that the cab files in the Win7SDK have the readonly attribute set and File.Delete fails to delete them. Explicitly unsetting that bit before deleting works okay...
FileSystem.RemoveFile(localFilePath, true);
Expand Down
3 changes: 2 additions & 1 deletion src/LessMsi.Gui/MainForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -959,7 +959,8 @@ private void btnExtract_Click(object sender, EventArgs e)

var filesToExtract = selectedFiles.ToArray();
Wixtracts.ExtractFiles(msiFile, outputDir, filesToExtract,
new AsyncCallback(progressDialog.UpdateProgress));
new AsyncCallback(progressDialog.UpdateProgress),
ExtractionMode.Default);
}
catch (Exception err)
{
Expand Down
22 changes: 22 additions & 0 deletions src/Lessmsi.Tests/CommandLineExtractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ public void FlatRenameExtract1Arg()
TestExtraction(commandLine, GetTestName(), "NUnit-2.5.2.9222", false, flatExtractionFlag: true);
}

[Fact]
public void OverwriteExtract1Arg()
{
var commandLine = "xo TestFiles\\MsiInput\\AppleMobileDeviceSupport64.msi";
// setting "AppleMobileDeviceSupport64.msi" as actualEntriesOutputDir value, since no other output dir specified in command line text
TestExtraction(commandLine, GetTestName(), "AppleMobileDeviceSupport64", false);
}

[Fact]
public void Extract2Args()
{
Expand Down Expand Up @@ -68,6 +76,13 @@ public void FlatRenameExtract2Args()
TestExtraction(commandLine, GetTestName(), "FlatRenameExtract2Args", false, flatExtractionFlag: true);
}

[Fact]
public void OverwriteExtract2Args()
{
var commandLine = "xo TestFiles\\MsiInput\\AppleMobileDeviceSupport64.msi OverwriteExtract2Args\\";
TestExtraction(commandLine, GetTestName(), "OverwriteExtract2Args", false);
}

[Fact]
public void Extract3Args()
{
Expand Down Expand Up @@ -96,6 +111,13 @@ public void FlatRenameExtract3Args()
TestExtraction(commandLine, GetTestName(), "FlatRenameExtract3Args", false, flatExtractionFlag: true);
}

[Fact]
public void OverwriteExtract3Args()
{
var commandLine = "xo TestFiles\\MsiInput\\AppleMobileDeviceSupport64.msi OverwriteExtract3Args\\ \"api-ms-win-core-file-l1-1-0.dll\" \"api-ms-win-core-file-l1-1-0.dll.duplicate1\"";
TestExtraction(commandLine, GetTestName(), "OverwriteExtract3Args", false);
}

[Fact]
public void ExtractCompatibility1Arg()
{
Expand Down
Loading

0 comments on commit c5944b4

Please sign in to comment.