Skip to content

Commit

Permalink
feat: Adds xfo and xfr commands to CLI to extract files from msi with…
Browse files Browse the repository at this point in the history
…out the path into a flat directory structure

closes #174 

New Commands:
  xfo  Extracts all or specified files from the specified msi_name to the same folder while overwriting files with the same name.
  xfr  Extracts all or specified files from the specified msi_name to the same folder while renaming files with the same name with a count suffix.

Thanks @mega5800
  • Loading branch information
mega5800 authored Jul 12, 2024
1 parent 09e1d96 commit 24b02c7
Show file tree
Hide file tree
Showing 18 changed files with 1,513 additions and 143 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ src/.build/nuget.exe
src/msbuild.log
src/.vs/
src/.temp/
.vs/
81 changes: 49 additions & 32 deletions src/LessMsi.Cli/ExtractCommand.cs
Original file line number Diff line number Diff line change
@@ -1,36 +1,53 @@
using System.Collections.Generic;
using System.IO;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NDesk.Options;

namespace LessMsi.Cli
{
internal class ExtractCommand : LessMsiCommand
{
public override void Run(List<string> allArgs)
{
var args = allArgs.Skip(1).ToList();
// "x msi_name [path_to_extract\] [file_names]+
if (args.Count < 1)
throw new OptionException("Invalid argument. Extract command must at least specify the name of an msi file.", "x");

var i = 0;
var msiFile = args[i++];
if (!File.Exists(msiFile))
throw new OptionException("Invalid argument. Specified msi file does not exist.", "x");
var filesToExtract = new List<string>();
var extractDir = "";
if (i < args.Count)
{
if (extractDir == "" && (args[i].EndsWith("\\") || args[i].EndsWith("\"")))
extractDir = args[i];
else
filesToExtract.Add(args[i]);
}
while (++i < args.Count)
filesToExtract.Add(args[i]);

Program.DoExtraction(msiFile, extractDir.TrimEnd('\"'), filesToExtract);
}
}
namespace LessMsi.Cli
{
internal class ExtractCommand : LessMsiCommand
{
public override void Run(List<string> allArgs)
{
var args = allArgs.Skip(1).ToList();
// "x msi_name [path_to_extract\] [file_names]+
if (args.Count < 1)
throw new OptionException("Invalid argument. Extract command must at least specify the name of an msi file.", "x");

var i = 0;
var msiFile = args[i++];
if (!File.Exists(msiFile))
throw new OptionException("Invalid argument. Specified msi file does not exist.", "x");
var filesToExtract = new List<string>();
var extractDir = "";
if (i < args.Count)
{
if (extractDir == "" && (args[i].EndsWith("\\") || args[i].EndsWith("\"")))
extractDir = args[i];
else
filesToExtract.Add(args[i]);
}
while (++i < args.Count)
filesToExtract.Add(args[i]);

Program.DoExtraction(msiFile, extractDir.TrimEnd('\"'), filesToExtract, getExtractionMode(allArgs[0]));
}

private ExtractionMode getExtractionMode(string commandArgument)
{
commandArgument = commandArgument.ToLowerInvariant();
ExtractionMode extractionMode = ExtractionMode.PreserveDirectoriesExtraction;

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

return extractionMode;
}
}
}
22 changes: 22 additions & 0 deletions src/LessMsi.Cli/ExtractionMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace LessMsi.Cli
{
public enum ExtractionMode
{
/// <summary>
/// Default value indicating that no extraction should be performed.
/// </summary>
None,
/// <summary>
/// Value indicating that a file extraction preserving directories should be performed.
/// </summary>
PreserveDirectoriesExtraction,
/// <summary>
/// Value indicating that a file extraction renaming identical files should be performed.
/// </summary>
RenameFlatExtraction,
/// <summary>
/// Value indicating that a file extraction overwriting identical files should be performed.
/// </summary>
OverwriteFlatExtraction
}
}
1 change: 1 addition & 0 deletions src/LessMsi.Cli/LessMsi.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
<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
126 changes: 95 additions & 31 deletions src/LessMsi.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ enum ConsoleReturnCode
UnrecognizedCommand=-3
}

/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
private const string TempFolderSuffix = "_temp";

/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
public static int Main(string[] args)
{
try
Expand All @@ -56,14 +58,18 @@ public static int Main(string[] args)
* See https://github.com/mono/mono/blob/master/mcs/tools/mdoc/Mono.Documentation/mdoc.cs#L54 for an example of using "commands" and "subcommands" with the NDesk.Options lib.
*/

ExtractCommand extractCommand = new ExtractCommand();

var subcommands = new Dictionary<string, LessMsiCommand> {
{"o", new OpenGuiCommand()},
{"x", new ExtractCommand()},
{"/x", new ExtractCommand()},
{"l", new ListTableCommand()},
{"v", new ShowVersionCommand()},
{"h", new ShowHelpCommand()}
};
{"o", new OpenGuiCommand()},
{"x", extractCommand},
{"xfo", extractCommand},
{"xfr", extractCommand},
{"/x", extractCommand},
{"l", new ListTableCommand()},
{"v", new ShowVersionCommand()},
{"h", new ShowHelpCommand()}
};

LessMsiCommand cmd;
if (args.Length > 0 && subcommands.TryGetValue(args[0], out cmd))
Expand Down Expand Up @@ -95,25 +101,81 @@ public static int Main(string[] args)
}
}

/// <summary>
/// Extracts all files contained in the specified .msi file into the specified output directory.
/// </summary>
/// <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>
public static void DoExtraction(string msiFileName, string outDirName, List<string> filesToExtract )
{
if (string.IsNullOrEmpty(outDirName))
outDirName = Path.GetFileNameWithoutExtension(msiFileName);
EnsureFileRooted(ref msiFileName);
EnsureFileRooted(ref outDirName);
/// <summary>
/// Extracts all files contained in the specified .msi file into the specified output directory.
/// </summary>
/// <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>
public static void DoExtraction(string msiFileName, string outDirName, List<string> filesToExtract, ExtractionMode extractionMode)
{
if (string.IsNullOrEmpty(outDirName))
outDirName = Path.GetFileNameWithoutExtension(msiFileName);

EnsureFileRooted(ref msiFileName);
EnsureFileRooted(ref outDirName);

var msiFile = new LessIO.Path(msiFileName);

Console.WriteLine("Extracting \'" + msiFile + "\' to \'" + outDirName + "\'.");
Console.WriteLine("Extracting \'" + msiFile + "\' to \'" + outDirName + "\'.");

Wixtracts.ExtractFiles(msiFile, outDirName, filesToExtract.ToArray(), PrintProgress);
}
if (isExtractionModeFlat(extractionMode))
{
string tempOutDirName = $"{outDirName}{TempFolderSuffix}";
Wixtracts.ExtractFiles(msiFile, tempOutDirName, filesToExtract.ToArray(), PrintProgress);

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

outDirName += "\\";
Directory.CreateDirectory(outDirName);
copyFilesInFlatWay(tempOutDirName, outDirName, extractionMode, fileNameCountingDict);
Directory.Delete(tempOutDirName, true);
}
else
{
Wixtracts.ExtractFiles(msiFile, outDirName, filesToExtract.ToArray(), PrintProgress);
}
}

private static bool isExtractionModeFlat(ExtractionMode extractionMode)
{
return extractionMode == ExtractionMode.RenameFlatExtraction || extractionMode == ExtractionMode.OverwriteFlatExtraction;
}

private static void copyFilesInFlatWay(string sourceDir, string targetDir, ExtractionMode extractionMode, Dictionary<string, int> fileNameCountingDict)
{
var allFiles = Directory.GetFiles(sourceDir);

foreach (var filePath in allFiles)
{
string fileSuffix = string.Empty;
string fileName = Path.GetFileName(filePath);

if (extractionMode == ExtractionMode.RenameFlatExtraction)
{
if (fileNameCountingDict.ContainsKey(fileName))
{
fileSuffix = $"_{fileNameCountingDict[fileName]}";
fileNameCountingDict[fileName]++;
}
else
{
fileNameCountingDict.Add(fileName, 1);
}
}

var outputPath = $"{targetDir}{Path.GetFileNameWithoutExtension(filePath)}{fileSuffix}{Path.GetExtension(filePath)}";

File.Copy(filePath, outputPath, extractionMode == ExtractionMode.OverwriteFlatExtraction);
}

var allFolders = Directory.GetDirectories(sourceDir);
foreach (var directory in allFolders)
{
copyFilesInFlatWay(directory, targetDir, extractionMode, fileNameCountingDict);
}
}

private static void PrintProgress(IAsyncResult result)
{
Expand All @@ -125,9 +187,11 @@ private static void PrintProgress(IAsyncResult result)
}

private static void EnsureFileRooted(ref string sFileName)
{
if (!Path.IsPathRooted(sFileName))
sFileName = Path.Combine(Directory.GetCurrentDirectory(), sFileName);
}
}
{
if (!Path.IsPathRooted(sFileName))
{
sFileName = Path.Combine(Directory.GetCurrentDirectory(), sFileName);
}
}
}
}
18 changes: 10 additions & 8 deletions src/LessMsi.Cli/ShowHelpCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ public override void Run(List<string> args)
public static void ShowHelp(string errorMessage)
{
string helpString =
@"Usage:
@"Usage:
lessmsi <command> [options] <msi_name> [<path_to_extract\>] [file_names]
Commands:
x Extracts all or specified files from the specified msi_name.
l Lists the contents of the specified msi table as CSV to stdout. Table is
specified with -t switch. Example: lessmsi l -t Component c:\foo.msi
v Lists the value of the ProductVersion Property in the msi
(typically this is the version of the MSI).
o Opens the specified msi_name in the GUI.
h Shows this help page.
x Extracts all or specified files from the specified msi_name.
xfo Extracts all or specified files from the specified msi_name to the same folder while overwriting files with the same name.
xfr Extracts all or specified files from the specified msi_name to the same folder while renaming files with the same name with a count suffix.
l Lists the contents of the specified msi table as CSV to stdout. Table is
specified with -t switch. Example: lessmsi l -t Component c:\foo.msi
v Lists the value of the ProductVersion Property in the msi
(typically this is the version of the MSI).
o Opens the specified msi_name in the GUI.
h Shows this help page.
For more information see http://lessmsi.activescott.com
";
Expand Down
Loading

0 comments on commit 24b02c7

Please sign in to comment.