diff --git a/XenAdminTests/ArchiveTests/ArchiveIteratorTests.cs b/XenAdminTests/ArchiveTests/ArchiveIteratorTests.cs index 57adc2ba2..040ed9ba1 100644 --- a/XenAdminTests/ArchiveTests/ArchiveIteratorTests.cs +++ b/XenAdminTests/ArchiveTests/ArchiveIteratorTests.cs @@ -32,6 +32,7 @@ using System; using System.IO; using System.Text; using NUnit.Framework; +using XenCenterLib; using XenCenterLib.Archive; namespace XenAdminTests.ArchiveTests @@ -55,7 +56,7 @@ namespace XenAdminTests.ArchiveTests disposed = false; } } - public string CurrentFileNameReturn { private get; set; } + public string CurrentFileNameReturn { get; set; } public long CurrentFileSizeReturn { private get; set; } public DateTime ModTimeReturn { private get; set; } public bool IsDirectoryReturn { private get; set; } @@ -97,10 +98,10 @@ namespace XenAdminTests.ArchiveTests public override string CurrentFileName() { - if (String.IsNullOrEmpty(CurrentFileNameReturn)) + if (string.IsNullOrEmpty(CurrentFileNameReturn)) return CurrentFileNameReturn; - return NumberOfCallsLeftReturn + CurrentFileNameReturn; + return CurrentFileNameReturn + NumberOfCallsLeftReturn; } public override long CurrentFileSize() @@ -121,19 +122,15 @@ namespace XenAdminTests.ArchiveTests protected override void Dispose(bool disposing) { base.Dispose(disposing); - if( !disposed ) - { - if( disposing ) - { - if(extractedFile != null) - extractedFile.Dispose(); - } - } + + if (!disposed && disposing) + extractedFile?.Dispose(); } } #endregion private ArchiveIteratorFake fakeIterator; + private string tempPath; [OneTimeSetUp] public void Setup() @@ -150,66 +147,101 @@ namespace XenAdminTests.ArchiveTests [SetUp] public void TestSetup() { + tempPath = null; fakeIterator.Reset(); } + [TearDown] + public void TestTearDown() + { + if (Directory.Exists(tempPath)) + Directory.Delete(tempPath, true); + } + [Test] - public void AnExceptionIsThrownForNullArgumentWhenCallingExtractAllContents() + public void TestExtractToNullDestinationPath() { Assert.Throws(typeof(ArgumentNullException), () => fakeIterator.ExtractAllContents(null)); } [Test] - public void AnExceptionIsThrownForANullFileNameWhenCallingExtractAllContents() + public void TestExtractNullFile() { fakeIterator.CurrentFileNameReturn = null; - Assert.Throws(typeof(ArgumentNullException), () => fakeIterator.ExtractAllContents(Path.GetTempPath())); + Assert.Throws(typeof(NullReferenceException), () => fakeIterator.ExtractAllContents(Path.GetTempPath())); } - - [Test] - public void VerifyAFileIsWrittenWhenCallingExtractAllContents() + + [TestCase(true, true)] + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(false, false)] + public void TestExtractFile(bool longDirectoryPath, bool longFilePath) { - string tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + var dirCharNumber = (longDirectoryPath ? 248 : 247) - tempPath.Length - 2; + //2 was removed for the combining slash between tempPath and dir, and the combining slash between dir and filename + var dir = new string('A', dirCharNumber); + var fileCharNumber = (longFilePath ? 260 : 259) - Path.Combine(tempPath, dir).Length - 2; + //2 was removed for the combining slash between the full dir path and filename, and the NumberOfCallsLeftReturn + var fileName = new string('B', fileCharNumber); + const int numberOfFiles = 3; fakeIterator.NumberOfCallsLeftReturn = numberOfFiles; + fakeIterator.CurrentFileNameReturn = Path.Combine(dir, fileName); fakeIterator.ExtractAllContents(tempPath); - //Test file has been created - string targetFile = Path.Combine(tempPath, fakeIterator.CurrentFileName()); - Assert.IsTrue(File.Exists(targetFile), "File Exists"); + for (var i = 0; i < 3; i++) + { + string targetFile = Path.Combine(tempPath, fakeIterator.CurrentFileNameReturn + i); - Assert.IsTrue(File.ReadAllBytes(targetFile).Length > 1, "File length > 1"); + if (longDirectoryPath || longFilePath) + targetFile = StringUtility.ToLongWindowsPathUnchecked(targetFile); + + Assert.IsTrue(File.Exists(targetFile), "File should exist"); + Assert.IsNotEmpty(File.ReadAllBytes(targetFile), "File should not be empty"); + + Assert.IsFalse((File.GetAttributes(targetFile) & FileAttributes.Directory) == FileAttributes.Directory, + "It should not have directory attributes"); + } //Check recursively that there are only the correct number of files - Assert.IsTrue(Directory.GetFiles(tempPath, "*.*", SearchOption.AllDirectories).Length == numberOfFiles, "File number is correct"); + var actualFileNumber = Directory.GetFiles(tempPath, "*.*", SearchOption.AllDirectories).Length; + Assert.AreEqual(numberOfFiles, actualFileNumber, $"There should be {numberOfFiles}"); - Assert.IsFalse((File.GetAttributes(targetFile) & FileAttributes.Directory) == FileAttributes.Directory, "Is not a dir"); - - Directory.Delete(tempPath,true); + if (longDirectoryPath || longFilePath) + tempPath = StringUtility.ToLongWindowsPathUnchecked(tempPath); } - [Test] - public void VerifyADirectoryIsWrittenWhenCallingExtractAllContents() + + [TestCase(true)] + [TestCase(false)] + public void TestExtractDirectory(bool longDirectoryPath) { - string tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var dirCharNumber = (longDirectoryPath ? 248 : 247) - tempPath.Length - 2; + //2 was removed for the combining slash between tempPath and dir, and the NumberOfCallsLeftReturn + var dir = new string('A', dirCharNumber); + fakeIterator.IsDirectoryReturn = true; - fakeIterator.CurrentFileNameReturn = "FakePath" + Path.DirectorySeparatorChar; + fakeIterator.CurrentFileNameReturn = dir; fakeIterator.ExtractAllContents(tempPath); - //Test file has been created string targetPath = Path.Combine(tempPath, fakeIterator.CurrentFileName()); - Assert.IsFalse(File.Exists(targetPath), "No files exist"); - Assert.IsTrue(Directory.Exists(targetPath), "Directories exist"); - //No files - just a directory - Assert.IsTrue(Directory.GetFiles(tempPath).Length < 1, "No file in the directory" ); + if (longDirectoryPath) + targetPath = StringUtility.ToLongWindowsPathUnchecked(targetPath); - //Check it's a directory - Assert.IsTrue((File.GetAttributes(targetPath) & FileAttributes.Directory) == FileAttributes.Directory, "Has directory attributes"); + Assert.IsFalse(File.Exists(targetPath), "Files should not exist"); + Assert.IsEmpty(Directory.GetFiles(tempPath), "Directory should not have files"); - Directory.Delete(tempPath, true); + Assert.IsTrue(Directory.Exists(targetPath), "Directory should exist"); + Assert.IsTrue((File.GetAttributes(targetPath) & FileAttributes.Directory) == FileAttributes.Directory, + "It should have directory attributes"); + + if (longDirectoryPath) + tempPath = StringUtility.ToLongWindowsPathUnchecked(tempPath); } - } } diff --git a/XenAdminTests/ArchiveTests/ArchiveWriterTests.cs b/XenAdminTests/ArchiveTests/ArchiveWriterTests.cs index 91a9d23be..a47b481dd 100644 --- a/XenAdminTests/ArchiveTests/ArchiveWriterTests.cs +++ b/XenAdminTests/ArchiveTests/ArchiveWriterTests.cs @@ -34,6 +34,7 @@ using System.IO; using System.Linq; using System.Text; using NUnit.Framework; +using XenCenterLib; using XenCenterLib.Archive; namespace XenAdminTests.ArchiveTests @@ -68,18 +69,18 @@ namespace XenAdminTests.ArchiveTests { if (AddedStreamData != null) { - foreach (Stream stream in AddedStreamData) + foreach (Stream stream in AddedStreamData) { - if( stream != null ) + if (stream != null) stream.Dispose(); - } + } } } - public override void Add(Stream filetoAdd, string fileName, DateTime modificationTime, Action cancellingDelegate) + public override void Add(Stream fileToAdd, string fileName, DateTime modificationTime, Action cancellingDelegate) { disposed = false; - AddedStreamData.Add(filetoAdd); + AddedStreamData.Add(fileToAdd); AddedFileNameData.Add(fileName); AddedDates.Add(modificationTime); } @@ -93,9 +94,9 @@ namespace XenAdminTests.ArchiveTests protected override void Dispose(bool disposing) { base.Dispose(disposing); - if(disposing) + if (disposing) { - if( !disposed ) + if (!disposed) { DisposeStreamList(); } @@ -136,20 +137,10 @@ namespace XenAdminTests.ArchiveTests Assert.AreEqual(1, fakeWriter.AddedDates.Count); Assert.AreEqual(fileName, fakeWriter.AddedFileNameData[0], "File name"); Assert.IsTrue(fakeWriter.AddedStreamData[0].Length == 14, "Stream has data"); - AssertCurrentDateIsPlausible(fakeWriter.AddedDates[0]); + Assert.That(fakeWriter.AddedDates[0], Is.EqualTo(DateTime.Now).Within(TimeSpan.FromSeconds(5))); } } - private void AssertCurrentDateIsPlausible(DateTime currentDate) - { - //If this is failing check that the number of seconds is enough - const double seconds = 5.0; - DateTime maxDate = DateTime.Now.AddSeconds(seconds); - DateTime minDate = DateTime.Now.AddSeconds(-1.0 * seconds); - Assert.IsTrue(currentDate > minDate, "Date is > minimum"); - Assert.IsTrue(currentDate < maxDate, "Date is < maximum"); - } - [Test] public void DatelessAddDirectoryCallsImplementation() { @@ -164,7 +155,7 @@ namespace XenAdminTests.ArchiveTests Assert.AreEqual(0, fakeWriter.AddedStreamData.Count); Assert.AreEqual(totalAdded, fakeWriter.AddedDates.Count); Assert.AreEqual(dirName, fakeWriter.AddedFileNameData[0], "File name"); - AssertCurrentDateIsPlausible(fakeWriter.AddedDates[0]); + Assert.That(fakeWriter.AddedDates[0], Is.EqualTo(DateTime.Now).Within(TimeSpan.FromSeconds(5))); } [Test] @@ -173,6 +164,96 @@ namespace XenAdminTests.ArchiveTests Assert.Throws(typeof(FileNotFoundException), () => fakeWriter.CreateArchive("Yellow brick road - not a path!")); } + [TestCase(true, true)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(false, false)] + public void CreateArchiveWithLongPath(bool longDirectoryPath, bool longFilePath) + { + //set up the path to zip + var zipPath = PopulateLongPathArchive(true, longDirectoryPath, longFilePath, out var addedData); + + fakeWriter.CreateArchive(zipPath); + + foreach (var datum in addedData) + Assert.Contains(datum, fakeWriter.AddedFileNameData); + + // 2 folders and one file + Assert.AreEqual(addedData.Count, fakeWriter.AddedFileNameData.Count); + + //clean up: we need to ensure we're deleting the folder + if (longDirectoryPath || longFilePath) + zipPath = StringUtility.ToLongWindowsPathUnchecked(zipPath); + + Directory.Delete(zipPath, true); + } + + [Test] + public void CreateArchiveWithLongPath_PathTooLong() + { + //! N.B.: If this test fails it might be because the project has moved to a version of .NET Core + //! that does not require calls to `StringUtils.ToLongWindowsPath`. Please review its uses + //! and remove it from the codebase if possible. + + // this test ensures PopulateLongPathArchive's correctness + // since CreateArchiveWithLongPath depends on it + + Assert.DoesNotThrow(() => PopulateLongPathArchive(false, false, false, out _)); + Assert.Throws(() => PopulateLongPathArchive(false, false, true, out _)); + Assert.Throws(() => PopulateLongPathArchive(false, true, true, out _)); + Assert.Throws(() => PopulateLongPathArchive(false, true, false, out _)); + } + + /// + /// Set up method creating a directory containing 2 subdirectories one of which has a file + /// + /// set to true to ensure folders and files are prepended with \\?\ + /// the path to the folder + private string PopulateLongPathArchive(bool createValidPaths, bool longDirectoryPath, bool longFilePath, out List addedData) + { + var zipPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(zipPath); + + var dirCharNumber1 = (longDirectoryPath ? 248 : 247) - zipPath.Length - 2; + //2 was removed for the combining slash between tempPath and dir, and the first character + var relativeDirectoryPath1 = 0 + new string('A', dirCharNumber1); + + var dirCharNumber2 = (longDirectoryPath ? 248 : 247) - zipPath.Length - 3; + //3 was removed for the combining slash between zipPath and dir, the first character, + //and the combining slash between dir and filename + var relativeDirectoryPath2 = 1 + new string('A', dirCharNumber2); + + var fileCharNumber = (longFilePath ? 260 : 259) - Path.Combine(zipPath, relativeDirectoryPath2).Length - 1; + //1 was removed for the combining slash between the full dir path and filename + var fileName = new string('B', fileCharNumber); + var relativeFilePath = Path.Combine(relativeDirectoryPath2, fileName); + + addedData = new List + { + relativeDirectoryPath1.Replace(@"\", "/"), + relativeDirectoryPath2.Replace(@"\", "/"), + relativeFilePath.Replace(@"\", "/") + }; + + var directoryPath1 = Path.Combine(zipPath, relativeDirectoryPath1); + var directoryPath2 = Path.Combine(zipPath, relativeDirectoryPath2); + var filePath = Path.Combine(directoryPath2, fileName); + + if (createValidPaths) + { + directoryPath1 = StringUtility.ToLongWindowsPathUnchecked(directoryPath1); + directoryPath2 = StringUtility.ToLongWindowsPathUnchecked(directoryPath2); + filePath = StringUtility.ToLongWindowsPathUnchecked(filePath); + } + + Directory.CreateDirectory(directoryPath1); + Directory.CreateDirectory(directoryPath2); + File.WriteAllText(filePath, "Hello, World!"); + + + return zipPath; + } + [Test] public void CreateArchiveWorksWithValidDirectoryStructure() { @@ -184,17 +265,17 @@ namespace XenAdminTests.ArchiveTests { string subfolder = Path.Combine(tempPath, Path.GetRandomFileName()); Directory.CreateDirectory(subfolder); - CreateFiles( subfolder, i); + CreateFiles(subfolder, i); } fakeWriter.CreateArchive(tempPath); - Assert.AreEqual(12, fakeWriter.AddedDates.Count ); + Assert.AreEqual(12, fakeWriter.AddedDates.Count); Assert.AreEqual(12, fakeWriter.AddedFileNameData.Count); Assert.AreEqual(8, fakeWriter.AddedStreamData.Count); - - foreach( DateTime date in fakeWriter.AddedDates ) - AssertCurrentDateIsPlausible(date); + + foreach (DateTime date in fakeWriter.AddedDates) + Assert.That(date, Is.EqualTo(DateTime.Now).Within(TimeSpan.FromSeconds(5))); foreach (string name in fakeWriter.AddedFileNameData) Assert.AreEqual(-1, name.IndexOfAny(@":\".ToArray()), "Unwanted chars found in path"); @@ -206,7 +287,7 @@ namespace XenAdminTests.ArchiveTests { for (int i = 0; i < numberOfFiles; i++) { - using( FileStream fs = File.OpenWrite(Path.Combine(tempPath, Path.GetRandomFileName()))) + using (FileStream fs = File.OpenWrite(Path.Combine(tempPath, Path.GetRandomFileName()))) { fs.Write(Encoding.ASCII.GetBytes("This is a test"), 0, 14); fs.Flush(); diff --git a/XenCenterLib/Archive/ArchiveIterator.cs b/XenCenterLib/Archive/ArchiveIterator.cs index b0c29cd55..91eb87f48 100644 --- a/XenCenterLib/Archive/ArchiveIterator.cs +++ b/XenCenterLib/Archive/ArchiveIterator.cs @@ -47,21 +47,30 @@ namespace XenCenterLib.Archive /// If while combining path and current file name a null arises public void ExtractAllContents(string pathToExtractTo, Action cancellingDelegate = null) { - if (String.IsNullOrEmpty(pathToExtractTo)) + if (string.IsNullOrEmpty(pathToExtractTo)) throw new ArgumentNullException(); while (HasNext()) { - //Make the file path from the details in the archive making the path windows friendly - string conflatedPath = Path.Combine(pathToExtractTo, CurrentFileName()).Replace('/', Path.DirectorySeparatorChar); + //make the path Windows friendly + var fileName = CurrentFileName(); + var isDirectory = IsDirectory(); - //Create directories - empty ones will be made too - Directory.CreateDirectory(Path.GetDirectoryName(conflatedPath)); + var sanitizedName = fileName.Replace('/', Path.DirectorySeparatorChar); + var conflatedPath = Path.Combine(pathToExtractTo, sanitizedName); + + var dir = isDirectory ? conflatedPath : Path.GetDirectoryName(conflatedPath); + dir = StringUtility.ToLongWindowsPath(dir, true); + + //Create directory - empty one will be made too + Directory.CreateDirectory(dir); //If we have a file extract the contents - if (!IsDirectory()) + if (!isDirectory) { - using (FileStream fs = File.Create(conflatedPath)) + conflatedPath = StringUtility.ToLongWindowsPath(conflatedPath, false); + + using (var fs = File.Create(conflatedPath)) ExtractCurrentFile(fs, cancellingDelegate); } } diff --git a/XenCenterLib/Archive/ArchiveWriter.cs b/XenCenterLib/Archive/ArchiveWriter.cs index 5ad3221de..c27b7363a 100644 --- a/XenCenterLib/Archive/ArchiveWriter.cs +++ b/XenCenterLib/Archive/ArchiveWriter.cs @@ -35,47 +35,18 @@ namespace XenCenterLib.Archive { public abstract class ArchiveWriter : IDisposable { - public abstract void Add(Stream filetoAdd, string fileName, DateTime modificationTime, Action cancellingDelegate); + private int _progressTracker; + + public abstract void Add(Stream fileToAdd, string fileName, DateTime modificationTime, Action cancellingDelegate); + public abstract void AddDirectory(string directoryName, DateTime modificationTime); public virtual void SetBaseStream(Stream outputStream) { throw new NotImplementedException(); } - public abstract void AddDirectory(string directoryName, DateTime modificationTime); - - /// - /// Disposal hook - /// - /// - protected virtual void Dispose(bool disposing){ } - - public void CreateArchive(string pathToArchive, Action cancellingDelegate = null, Action progressDelegate = null) + protected virtual void Dispose(bool disposing) { - if (!Directory.Exists(pathToArchive)) - throw new FileNotFoundException("The path " + pathToArchive + " does not exist"); - - var files = Directory.GetFiles(pathToArchive, "*.*", SearchOption.AllDirectories); - for (var i = 0; i < files.Length; i++) - { - string filePath = files[i]; - cancellingDelegate?.Invoke(); - - using (FileStream fs = File.OpenRead(filePath)) - { - Add(fs, CleanRelativePathName(pathToArchive, filePath), File.GetCreationTime(filePath), cancellingDelegate); - progressDelegate?.Invoke((int)50.0 * i / files.Length); - } - } - - var directories = Directory.GetDirectories(pathToArchive, "*.*", SearchOption.AllDirectories); - for (var j = 0; j < directories.Length; j++) - { - string dirPath = directories[j]; - cancellingDelegate?.Invoke(); - AddDirectory(CleanRelativePathName(pathToArchive, dirPath), Directory.GetCreationTime(dirPath)); - progressDelegate?.Invoke(50 + (int)50.0 * j / directories.Length); - } } public void Dispose() @@ -84,10 +55,80 @@ namespace XenCenterLib.Archive GC.SuppressFinalize(this); } - private string CleanRelativePathName(string rootPath, string pathName) + public void CreateArchive(string pathToArchive, Action cancellingDelegate = null, Action progressDelegate = null) { - return pathName.Replace(rootPath, "").Replace('\\', '/').TrimStart('/'); + // We look recursively and do not use Directory.GetDirectories and Directory.GetFiles with + // AllDirectories options because in .NET 4.8 they do not enumerate all elements if they + // have paths longer than 260 characters (248 for directories). + // If moving to .NET Core, please consider reverting this. + + _progressTracker = 0; + PopulateArchive(pathToArchive, pathToArchive, cancellingDelegate, progressDelegate); } + /// + /// Populate the archive by recursively calling the overridden and . + /// + /// The path to the root of the folder we're archiving + /// Keeps track of the current directory we're archiving. In the first recursive call it should be the same as + /// Action cal led for cancelling + /// Action for reporting progress. Method will populate its parameter with the current progress of the recursive operation + /// Total progress that needs to be added for archiving this directory. In the first recursive call it should be 100. If the folder we're adding should add 18 percentage points to the total progress, set this value to 18. + /// Offset to the progress. This is added to when setting the progress for this directory. If this folder should add 18 percentage points to the total progress, but it's for a folder past the 50% mark of the total progress (i.e.: completing this folder should set the total to 68), set this value to 50. + /// A directory could not be found. + private void PopulateArchive(string pathToArchive, string pathToCurrentDirectory, Action cancellingDelegate = null, Action progressDelegate = null, float totalProgressIncrease = 100, float progressOffset = 0) + { + cancellingDelegate?.Invoke(); + + pathToArchive = StringUtility.ToLongWindowsPath(pathToArchive, true); + if (!Directory.Exists(pathToArchive)) + throw new FileNotFoundException($"The path {pathToArchive} does not exist"); + + pathToCurrentDirectory = StringUtility.ToLongWindowsPath(pathToCurrentDirectory, true); + + //add the current directory except when it's the root directory + if (pathToArchive != pathToCurrentDirectory) + AddDirectory(CleanRelativePathName(pathToArchive, pathToCurrentDirectory), Directory.GetCreationTime(pathToCurrentDirectory)); + + var files = Directory.GetFiles(pathToCurrentDirectory); + + foreach (var file in files) + { + cancellingDelegate?.Invoke(); + + var filePath = StringUtility.ToLongWindowsPath(file, false); + + using (var fs = File.OpenRead(filePath)) + Add(fs, CleanRelativePathName(pathToArchive, filePath), File.GetCreationTime(filePath), cancellingDelegate); + } + + if (_progressTracker != (int)progressOffset) + { + _progressTracker = (int)progressOffset; + progressDelegate?.Invoke(_progressTracker); + } + + var directories = Directory.GetDirectories(pathToCurrentDirectory); + if (directories.Length == 0) + return; + + float increment = totalProgressIncrease / directories.Length; + + for (var i = 0; i < directories.Length; i++) + { + PopulateArchive(pathToArchive, directories[i], cancellingDelegate, progressDelegate, + increment, i * increment + progressOffset); + } + } + + private string CleanRelativePathName(string rootPath, string pathName) + { + var cleanedRootPath = rootPath.Replace(@"\\?\", string.Empty); + return pathName + .Replace(@"\\?\", string.Empty) + .Replace(cleanedRootPath, string.Empty) + .Replace('\\', '/') + .TrimStart('/'); + } } } diff --git a/XenCenterLib/Archive/TarArchiveWriter.cs b/XenCenterLib/Archive/TarArchiveWriter.cs index 7137da1c7..56b36e3e3 100644 --- a/XenCenterLib/Archive/TarArchiveWriter.cs +++ b/XenCenterLib/Archive/TarArchiveWriter.cs @@ -77,10 +77,10 @@ namespace XenCenterLib.Archive tar.CloseEntry(); } - public override void Add(Stream filetoAdd, string fileName, DateTime modificationTime, Action cancellingDelegate) + public override void Add(Stream fileToAdd, string fileName, DateTime modificationTime, Action cancellingDelegate) { TarEntry entry = TarEntry.CreateTarEntry(fileName); - entry.Size = filetoAdd.Length; + entry.Size = fileToAdd.Length; entry.ModTime = modificationTime; tar.PutNextEntry(entry); @@ -89,9 +89,9 @@ namespace XenCenterLib.Archive //You have to do this because if using a memory stream the pointer will be at the end it //it's just been read and this will cause nothing to be written out - filetoAdd.Position = 0; + fileToAdd.Position = 0; - while ((n = filetoAdd.Read(buffer, 0, buffer.Length)) > 0) + while ((n = fileToAdd.Read(buffer, 0, buffer.Length)) > 0) { cancellingDelegate?.Invoke(); tar.Write(buffer, 0, n); diff --git a/XenCenterLib/StringUtility.cs b/XenCenterLib/StringUtility.cs index c084aaecb..110d0859c 100644 --- a/XenCenterLib/StringUtility.cs +++ b/XenCenterLib/StringUtility.cs @@ -33,6 +33,8 @@ using System.Collections.Generic; using System.Text.RegularExpressions; using System.Linq; using System.Collections; +using System.IO; + namespace XenCenterLib { @@ -218,5 +220,39 @@ namespace XenCenterLib return true; } + + /// + /// To be used to format file and directory paths for File streams.
+ /// Prepends \\?\ to path if it is longer than (MAX_PATH - 12) in Windows. This is the legacy directory length limit.

+ /// See CreateFileA and Maximum Path Length Limitation for more info. + ///
+ /// The input path + /// Whether the input path is a directory + public static string ToLongWindowsPath(string inputPath, bool isDirectory) + { + if (string.IsNullOrEmpty(inputPath) || inputPath.StartsWith(@"\\?\")) + return inputPath; + + if (isDirectory) + return inputPath.Length >= 248 ? ToLongWindowsPathUnchecked(inputPath) : inputPath; + + var dir = Path.GetDirectoryName(inputPath); + if (string.IsNullOrEmpty(dir)) + return inputPath; + + return dir.Length >= 248 || inputPath.Length >= 260 ? ToLongWindowsPathUnchecked(inputPath) : inputPath; + } + + /// + /// To be used to format file and directory paths for File streams.
+ /// Prepends \\?\ to path if it is longer than (MAX_PATH - 12) in Windows. This is the legacy directory length limit.

+ /// See CreateFileA and + /// Maximum Path Length Limitation for more info. + ///
+ /// The input path + public static string ToLongWindowsPathUnchecked(string inputPath) + { + return $@"\\?\{inputPath}"; + } } } \ No newline at end of file diff --git a/XenModel/Actions/StatusReport/ZipStatusReportAction.cs b/XenModel/Actions/StatusReport/ZipStatusReportAction.cs index 2ac7370b3..9e23ee966 100644 --- a/XenModel/Actions/StatusReport/ZipStatusReportAction.cs +++ b/XenModel/Actions/StatusReport/ZipStatusReportAction.cs @@ -30,6 +30,7 @@ using System; using System.IO; +using XenCenterLib; using XenCenterLib.Archive; @@ -65,7 +66,7 @@ namespace XenAdmin.Actions public ZipStatusReportAction(string tempFolder, string destFile, string timeString = null, bool suppressHistory = true) : base(null, Messages.BUGTOOL_SAVING, destFile, timeString, suppressHistory) { - _inputTempFolder = tempFolder; + _inputTempFolder = StringUtility.ToLongWindowsPathUnchecked(tempFolder); _destFile = destFile; } @@ -74,7 +75,7 @@ namespace XenAdmin.Actions Status = ReportStatus.inProgress; do { - _extractTempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + _extractTempDir = StringUtility.ToLongWindowsPathUnchecked(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())); } while (Directory.Exists(_extractTempDir)); Directory.CreateDirectory(_extractTempDir);