/// <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(FileInfo msi, DirectoryInfo outputDir, MsiFile[] filesToExtract, AsyncCallback progressCallback) { if (msi == null) throw new ArgumentNullException("msi"); if (outputDir == null) throw new ArgumentNullException("outputDir"); int filesExtractedSoFar = 0; ExtractionProgress progress = null; Database msidb = new Database(msi.FullName, OpenDatabase.ReadOnly); try { if (filesToExtract == null || filesToExtract.Length < 1) filesToExtract = MsiFile.CreateMsiFilesFromMSI(msidb); progress = new ExtractionProgress(progressCallback, filesToExtract.Length); if (!msi.Exists) { Trace.WriteLine("File \'" + msi.FullName + "\' not found."); progress.ReportProgress(ExtractionActivity.Complete, "", filesExtractedSoFar); return; } progress.ReportProgress(ExtractionActivity.Initializing, "", filesExtractedSoFar); outputDir.Create(); //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(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); DirectoryInfo targetDirectoryForFile = GetTargetDirectory(outputDir, entry.Directory); string destName = Path.Combine(targetDirectoryForFile.FullName, entry.LongFileName); if (File.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; string uniqueName; do { uniqueName = string.Concat(destName, ".", "duplicate", ++duplicateCount); } while (File.Exists(uniqueName)); destName = uniqueName; // ReSharper restore HeuristicUnreachableCode } Trace.WriteLine(string.Concat("Extracting File \'", compressedFile.Filename, "\' to \'", destName, "\'")); compressedFile.ExtractTo(destName); filesExtractedSoFar++; } } } finally { //cleanup the decompressors allocated in MergeCabs foreach (MSCabinet decomp in cabDecompressors) { decomp.Close(false); File.Delete(decomp.LocalFilePath); } } } finally { if (msidb != null) msidb.Close(); if (progress != null) progress.ReportProgress(ExtractionActivity.Complete, "", filesExtractedSoFar); } }
/// <summary> /// Opens the previous package (.msi or .exe) and reads the interesting information from it. /// </summary> /// <param name="filePath">Path to the package.</param> /// <param name="previousUpgradeCode">Upgrade code of the package.</param> /// <param name="previousVersion">Version of the package.</param> /// <param name="previousUri">Update URL of the package.</param> private void ReadPreviousPackage(string filePath, out Guid previousUpgradeCode, out Version previousVersion, out Uri previousUri) { // assume nothing about the previous package previousUpgradeCode = Guid.Empty; previousVersion = null; previousUri = null; string tempFileName = null; Database db = null; View view = null; try { string msiPath = filePath; // assume the file path is the path to the MSI // if the extension on the file path is ".exe" try to extract the MSI out of it if (String.Compare(Path.GetExtension(filePath), ".exe", true) == 0) { tempFileName = Path.GetTempFileName(); Process process = new Process(); process.StartInfo.FileName = filePath; process.StartInfo.CreateNoWindow = true; process.StartInfo.UseShellExecute = false; process.StartInfo.Arguments = String.Concat("-out ", tempFileName); process.Start(); process.WaitForExit(); if (process.ExitCode != 0) { throw new ApplicationException(String.Concat("Failed to extract MSI from ", process.StartInfo.FileName)); } msiPath = tempFileName; // the MSI is now at the temp filename location } db = new Database(msiPath, OpenDatabase.ReadOnly); view = db.OpenView("SELECT `Value` FROM `Property` WHERE `Property`=?"); string propertyValue; // get the UpgradeCode propertyValue = this.FetchPropertyValue(view, "UpgradeCode"); if (propertyValue != null) { previousUpgradeCode = new Guid(propertyValue); } // get the Version propertyValue = this.FetchPropertyValue(view, "ProductVersion"); if (propertyValue != null) { previousVersion = new Version(propertyValue); } // get the Update URL propertyValue = this.FetchPropertyValue(view, "ARPURLUPDATEINFO"); if (propertyValue != null) { previousUri = new Uri(propertyValue); } } finally { if (view != null) { view.Close(); } if (db != null) { db.Close(); } if (tempFileName != null) { File.Delete(tempFileName); } } }