/// <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="msi">The MSI file to extract files from.</param> /// <param name="outputDir">The directory to extract the files into.</param> /// <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) { if (msi.IsEmpty) { throw new ArgumentNullException("msi"); } if (string.IsNullOrEmpty(outputDir)) { throw new ArgumentNullException("outputDir"); } int filesExtractedSoFar = 0; //Refrence on Embedding files: https://msdn.microsoft.com/en-us/library/aa369279.aspx ExtractionProgress progress = null; Database msidb = new Database(msi.PathString, DatabaseOpenMode.ReadOnly); try { if (filesToExtract == null || filesToExtract.Length < 1) { filesToExtract = MsiFile.CreateMsiFilesFromMSI(msidb); } progress = new ExtractionProgress(progressCallback, filesToExtract.Length); if (!FileSystem.Exists(msi)) { Trace.WriteLine("File \'" + msi + "\' not found."); progress.ReportProgress(ExtractionActivity.Complete, string.Empty, filesExtractedSoFar); return; } progress.ReportProgress(ExtractionActivity.Initializing, string.Empty, filesExtractedSoFar); FileSystem.CreateDirectory(new Path(outputDir)); //map short file names to the msi file entry var fileEntryMap = new Dictionary <string, MsiFile>(filesToExtract.Length, StringComparer.InvariantCulture); foreach (MsiFile fileEntry in filesToExtract) { MsiFile existingFile = null; if (fileEntryMap.TryGetValue(fileEntry.File, out existingFile)) { //NOTE: This used to be triggered when we ignored case of file, but now we don't ignore case so this is unlikely to occur. // Differing only by case is not compliant with the msi specification but some installers do it (e.g. python, see issue 28). Debug.Print("!!Found duplicate file using key {0}. The existing key was {1}", fileEntry.File, existingFile.File); } else { fileEntryMap.Add(fileEntry.File, fileEntry); } } Debug.Assert(fileEntryMap.Count == filesToExtract.Length, "Duplicate files must have caused some files to not be in the map."); progress.ReportProgress(ExtractionActivity.Uncompressing, string.Empty, filesExtractedSoFar); List <CabInfo> cabInfos = CabsFromMsiToDisk(msi, msidb, outputDir); try { foreach (CabInfo cab in cabInfos) { foreach (CabFileInfo cabFile in cab.GetFiles()) { var sourcePath = Path.Combine(cabFile.Path, cabFile.Name); if (fileEntryMap.TryGetValue(sourcePath.PathString, out MsiFile msiFile)) { Path destPath = Path.Combine(GetTargetDirectory(outputDir, msiFile.Directory.FullPath), msiFile.LongFileName); Path relativeDestPath = Path.Combine(msiFile.Directory.FullPath, msiFile.LongFileName); progress.ReportProgress(ExtractionActivity.ExtractingFile, relativeDestPath.PathString, ++filesExtractedSoFar); cab.UnpackFile(sourcePath.PathString, destPath.PathString); } } } } finally { foreach (CabInfo cab in cabInfos) { DeleteFileForcefully(new Path(cab.FullName)); } } } finally { if (msidb != null) { msidb.Close(); } if (progress != null) { progress.ReportProgress(ExtractionActivity.Complete, "", filesExtractedSoFar); } } }
/// <summary> /// Executes the specified command. Assume working directory is TestFiles\MsiInput\ dir. /// </summary> /// <param name="commandLineArguments">The command line arguments (everything after the exe name).</param> /// <param name="testName">The name of hte test (used to formulate the expectedEntries output dir).</param> /// <param name="actualEntriesOutputDir">The output directory where the actual extraction is expected to occur.</param> private void TestExtraction(string commandLineArguments, string testName, string actualEntriesOutputDir, bool useInProcessForDebugging) { string consoleOutput; var actualOutDir = new LessIO.Path(actualEntriesOutputDir).FullPath; if (actualOutDir.Exists) FileSystem.RemoveDirectory(actualOutDir, true); int exitCode; if (useInProcessForDebugging) exitCode = base.RunCommandLineInProccess(commandLineArguments); else exitCode = base.RunCommandLine(commandLineArguments, out consoleOutput); Assert.Equal(0, exitCode); var actualEntries = FileEntryGraph.GetActualEntries(actualOutDir.FullPathString, "Actual Entries"); var actualEntriesFile = GetActualOutputFile(testName); actualEntries.Save(actualEntriesFile); //Console.WriteLine("Actual entries saved to " + actualEntriesFile.FullName); var expectedEntries = GetExpectedEntriesForMsi(testName); AssertAreEqual(expectedEntries, actualEntries); }
public static void ExtractFiles(Path msi, string outputDir, string[] fileNamesToExtract, AsyncCallback progressCallback) { var msiFiles = GetMsiFileFromFileNames(msi, fileNamesToExtract); ExtractFiles(msi, outputDir, msiFiles, progressCallback); }
public static void ExtractFiles(Path msi, string outputDir, string[] fileNamesToExtract) { var msiFiles = GetMsiFileFromFileNames(msi, fileNamesToExtract); ExtractFiles(msi, outputDir, msiFiles, null); }
public static void ExtractFiles(Path msi, string outputDir) { ExtractFiles(msi, outputDir, new string[0], null); }
/// <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); }
/// <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) { if (msi.IsEmpty) { throw new ArgumentNullException("msi"); } if (string.IsNullOrEmpty(outputDir)) { throw new ArgumentNullException("outputDir"); } int filesExtractedSoFar = 0; //Refrence on Embedding files: https://msdn.microsoft.com/en-us/library/aa369279.aspx ExtractionProgress progress = null; Database msidb = new Database(msi.PathString, OpenDatabase.ReadOnly); try { if (filesToExtract == null || filesToExtract.Length < 1) { filesToExtract = MsiFile.CreateMsiFilesFromMSI(msidb); } progress = new ExtractionProgress(progressCallback, filesToExtract.Length); if (!FileSystem.Exists(msi)) { Trace.WriteLine("File \'" + msi + "\' not found."); progress.ReportProgress(ExtractionActivity.Complete, "", filesExtractedSoFar); return; } progress.ReportProgress(ExtractionActivity.Initializing, "", filesExtractedSoFar); FileSystem.CreateDirectory(new Path(outputDir)); //map short file names to the msi file entry var fileEntryMap = new Dictionary <string, MsiFile>(filesToExtract.Length, StringComparer.InvariantCulture); foreach (var fileEntry in filesToExtract) { MsiFile existingFile = null; if (fileEntryMap.TryGetValue(fileEntry.File, out existingFile)) { //NOTE: This used to be triggered when we ignored case of file, but now we don't ignore case so this is unlikely to occur. // Differing only by case is not compliant with the msi specification but some installers do it (e.g. python, see issue 28). Debug.Print("!!Found duplicate file using key {0}. The existing key was {1}", fileEntry.File, existingFile.File); } else { fileEntryMap.Add(fileEntry.File, fileEntry); } } Debug.Assert(fileEntryMap.Count == filesToExtract.Length, "Duplicate files must have caused some files to not be in the map."); var cabInfos = CabsFromMsiToDisk(msi, msidb, outputDir); var cabDecompressors = MergeCabs(cabInfos); try { foreach (MSCabinet decompressor in cabDecompressors) { foreach (var compressedFile in decompressor.GetFiles()) { // if the user didn't select this in the UI for extraction, skip it. if (!fileEntryMap.ContainsKey(compressedFile.Filename)) { continue; } var entry = fileEntryMap[compressedFile.Filename]; 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)) { Debug.Fail("output file already exists. We'll make it unique, but this is probably a strange msi or a bug in this program."); //make unique // ReSharper disable HeuristicUnreachableCode Trace.WriteLine(string.Concat("Duplicate file found \'", destName, "\'")); int duplicateCount = 0; Path uniqueName; do { uniqueName = new Path(destName + "." + "duplicate" + ++duplicateCount); } while (FileSystem.Exists(uniqueName)); destName = uniqueName; // ReSharper restore HeuristicUnreachableCode } Trace.WriteLine(string.Concat("Extracting File \'", compressedFile.Filename, "\' to \'", destName, "\'")); compressedFile.ExtractTo(destName.PathString); filesExtractedSoFar++; } } } finally { //cleanup the decompressors allocated in MergeCabs foreach (var decomp in cabDecompressors) { decomp.Close(false); DeleteFileForcefully(new Path(decomp.LocalFilePath)); } } } finally { if (msidb != null) { msidb.Close(); } if (progress != null) { progress.ReportProgress(ExtractionActivity.Complete, "", filesExtractedSoFar); } } }