/* Copyright (c) Citrix Systems, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, * with or without modification, are permitted provided * that the following conditions are met: * * * Redistributions of source code must retain the above * copyright notice, this list of conditions and the * following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the * following disclaimer in the documentation and/or other * materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ using System; using System.Collections.Generic; using System.IO; using System.Text; using XenCenterLib.Archive; namespace XenAdmin.Actions { public class ZipStatusReportAction : AsyncAction { private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); /// /// The folder containing the raw files as downloaded from the server /// private readonly string _inputTempFolder; /// /// The destination zip file for the repackaged server log files /// private readonly string _destFile; /// /// A dictionary mapping file to original modification time. The filepath used is the full path /// within the staging directory (extractTempDir) where we put all the files before repacking them. /// The modification time either comes from the source tarball (if downloaded from a server), or /// the source file (if copying a local file). /// private readonly Dictionary ModTimes = new Dictionary(); private long bytesToCompress = 1; public ZipStatusReportAction(string tempFolder, string destFile) : base(null, Messages.BUGTOOL_SAVING, Messages.BUGTOOL_SAVING, true) { _inputTempFolder = tempFolder; _destFile = destFile; } protected override void Run() { // The directory in which we assemble the log files from the server before repackaging them // in a single zip file. string extractTempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); try { // Calculate total bytes to save long bytesToExtract = 1, bytesExtracted = 0; foreach (string inputFile in Directory.GetFiles(_inputTempFolder)) { bytesToExtract += new FileInfo(inputFile).Length; } // Create temp dir for extracted stuff if (Directory.Exists(extractTempDir)) { Directory.Delete(extractTempDir); } Directory.CreateDirectory(extractTempDir); // Extract each of the raw server files to the temp extraction directory foreach (string inputFile in Directory.GetFiles(_inputTempFolder)) { if (inputFile.ToLowerInvariant().EndsWith(".tar")) { // Un-tar it. SharpZipLib doesn't account for illegal filenames or characters in // filenames (e.g. ':'in Windows), so first we stream the tar to a new tar, // sanitizing any bad filenames as we go. // We also need to record the modification times of all the files, so that we can // restore them into the final zip. string outFilename = inputFile.Substring(0, inputFile.Length - 4); if (outFilename.Length == 0) outFilename = Path.GetRandomFileName(); string outputDir = Path.Combine(extractTempDir, Path.GetFileName(outFilename)); string sanitizedTar = Path.GetTempFileName(); using (ArchiveIterator tarIterator = ArchiveFactory.Reader(ArchiveFactory.Type.Tar, File.OpenRead(inputFile))) { using (ArchiveWriter tarWriter = ArchiveFactory.Writer(ArchiveFactory.Type.Tar, File.OpenWrite(sanitizedTar))) { Dictionary usedNames = new Dictionary(); while (tarIterator.HasNext()) { if (Cancelling) { throw new CancelledException(); } using( MemoryStream ms = new MemoryStream() ) { tarIterator.ExtractCurrentFile(ms); string saneName = SanitizeTarName(tarIterator.CurrentFileName(), usedNames); tarWriter.Add(ms, saneName); ModTimes[Path.Combine(outputDir, saneName)] = tarIterator.CurrentFileModificationTime(); } } } } // Now extract the sanitized tar using(FileStream fs = File.OpenRead(sanitizedTar)) { using (ArchiveIterator tarIterator = ArchiveFactory.Reader(ArchiveFactory.Type.Tar, fs)) { Directory.CreateDirectory(outputDir); tarIterator.ExtractAllContents(outputDir); bytesToCompress += Core.Helpers.GetDirSize(new DirectoryInfo(outputDir)); } } } else { // Just copy vanilla input files unmodified to the temp directory string outputFile = Path.Combine(extractTempDir, Path.GetFileName(inputFile)); File.Copy(inputFile, outputFile); ModTimes[outputFile] = new FileInfo(inputFile).LastWriteTimeUtc; bytesToCompress += new FileInfo(outputFile).Length; } bytesExtracted += new FileInfo(inputFile).Length; File.Delete(inputFile); this.PercentComplete = (int)(50.0 * bytesExtracted / bytesToExtract); if (Cancelling) { throw new CancelledException(); } } // Now zip up all the temporarily extracted files into a single zip file for the user log.DebugFormat("Packing {0} of bug report files into zip file {1}", Util.DiskSizeString(bytesToCompress), _destFile); LogDescriptionChanges = false; try { ZipToOutputFile(extractTempDir); PercentComplete = 100; // Only cleanup files if it succeeded (or cancelled) CleanupFiles(extractTempDir); } finally { LogDescriptionChanges = true; } if (Cancelling) throw new CancelledException(); } catch (CancelledException) { throw; } catch (Exception exn) { ZipToOutputFile(_inputTempFolder); PercentComplete = 100; log.ErrorFormat("An exception was trapped while creating a server status report: " + exn.Message); throw new Exception(Messages.STATUS_REPORT_ZIP_FAILED); } } private void ZipToOutputFile(string folderToZip) { using (ArchiveWriter zip = ArchiveFactory.Writer(ArchiveFactory.Type.Zip, File.OpenWrite(_destFile))) { zip.CreateArchive(folderToZip); } } private void CleanupFiles(string extractTempDir) { // We completed successfully: delete temporary files log.Debug("Deleting temporary files"); try { // Delete temp directory of raw server files to-be-decompressed Directory.Delete(_inputTempFolder, true); } catch (Exception exn) { log.Warn("Could not delete temporary decompressed files directory", exn); } try { // Try to remove temp decompressed files dir Directory.Delete(extractTempDir, true); } catch (Exception exn) { log.Warn("Could not delete temporary extracted files directory", exn); } } /// /// Maps file/directory names that are illegal under Windows to 'sanitized' versions. The usedNames /// parameter ensures this is done consistently within a directory tree. /// /// The dictionary is used by SanitizeTarName() to ensure names are consistently sanitized. e.g.: /// dir1: -> dir1_ /// dir1? -> dir1_ (1) /// dir1_ -> dir1_ (2) /// dir1:/file -> dir1_/file /// dir1?/file -> dir1_ (1)/file /// /// Pass the same dictionary to each invocation to get unique outputs within the same tree. /// private static string SanitizeTarName(string path, Dictionary usedNames) { string sanitizedPath = ""; Stack bitsToEscape = new Stack(); // Trim any trailing slashes (usually indicates path is a directory) path = path.TrimEnd(new char[] { '/' }); // Take members off the end of the path until we have a name that already is // a key in our dictionary, or until we have the empty string. while (!usedNames.ContainsKey(path) && path.Length > 0) { string[] bits = path.Split(new char[] { '/' }); string lastBit = bits[bits.Length - 1]; int lengthOfLastBit = lastBit.Length; bitsToEscape.Push(lastBit); path = path.Substring(0, path.Length - lengthOfLastBit); path = path.TrimEnd(new char[] { '/' }); } if (usedNames.ContainsKey(path)) { sanitizedPath = usedNames[path]; } // Now for each member in the path, look up the escaping of that member if it exists; otherwise // generate a new, unique escaping. Then append the escaped member to the end of the sanitized // path and continue. foreach (string member in bitsToEscape) { System.Diagnostics.Trace.Assert(member.Length > 0); string sanitizedMember = SanitizeTarPathMember(member); sanitizedPath = Path.Combine(sanitizedPath, sanitizedMember); path = path + Path.DirectorySeparatorChar + member; // Note: even if sanitizedMember == member, we must add it to the dictionary, since // tar permits names that differ only in case, while Windows does not. We must e.g.: // abc -> abc // aBC -> aBC (1) if (usedNames.ContainsKey(path)) { // We have already generated an escaping for this path prefix: use it sanitizedPath = usedNames[path]; continue; } // Generate the unique mapping string pre = sanitizedPath; int i = 1; while (DictionaryContainsIgnoringCase(usedNames, sanitizedPath)) { sanitizedPath = string.Format("{0} ({1})", pre, i); i++; } usedNames.Add(path, sanitizedPath); } return sanitizedPath; } private static bool DictionaryContainsIgnoringCase(Dictionary dict, string value) { foreach (string v in dict.Values) { if (v.ToUpperInvariant() == value.ToUpperInvariant()) { return true; } } return false; } // See http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/naming_a_file.asp private static readonly string[] forbiddenNames = { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" }; public static string SanitizeTarPathMember(string member) { // Strip any whitespace, or Windows will do it for us, and we might generate non-unique names member = member.Trim(); foreach (string reserved in forbiddenNames) { // Names can't be any of com1, com2, or com1.xyz, com2.abc etc. if (member.ToUpperInvariant() == reserved.ToUpperInvariant() || member.ToUpperInvariant().StartsWith(reserved.ToUpperInvariant() + ".")) { member = "_" + member; } } // Allow only 31 < c < 126, excluding < > : " / \ | ? * StringBuilder sb = new StringBuilder(member.Length); foreach (char c in member.ToCharArray()) { if (c > 31 && c < 127 && !IsCharExcluded(c)) { sb.Append(c); } else { sb.Append("_"); } } member = sb.ToString(); // Windows also seems not to like filenames ending '.' if (member.EndsWith(".")) { member = member.Substring(0, member.Length - 1) + "_"; } // Don't allow empty filename if (member.Length == 0) { member = "_"; } return member; } private static readonly char[] excludedChars = new char[] { '<', '>', ':', '"', '/', '\\', '|', '?', '*' }; private static bool IsCharExcluded(char c) { foreach (char excluded in excludedChars) { if (c == excluded) { return true; } } return false; } /// /// Copies the specified number of bytes from one stream to another via the provided buffer. /// private static void CopyStream(Stream inputStream, Stream outputStream, long bytesToCopy, byte[] buf) { while (bytesToCopy > 0) { int bytesRead = inputStream.Read(buf, 0, Math.Min(bytesToCopy > int.MaxValue ? int.MaxValue : (int)bytesToCopy, buf.Length)); outputStream.Write(buf, 0, bytesRead); bytesToCopy -= bytesRead; } } public override void RecomputeCanCancel() { CanCancel = !Cancelling && !IsCompleted; } protected override void CancelRelatedTask() { } } }