From ea51b045a442ab188b005f81864b6b31fa6163d6 Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Tue, 6 Mar 2018 19:47:20 -0800 Subject: [PATCH 1/2] Support trimmed paths in FileInfo Some paths are not creatable in Windows without special syntax. Notably paths with trailing spaces and periods. As GetFullPath() (and GetFullPathName()) trim these we would lose the correct file name as we passed the path around. With the enumeration changes we now populate FileInfo correctly- this change allows the other methods to work when wrapped around such a path. --- .../src/System/IO/File.cs | 92 ++------- .../src/System/IO/FileInfo.cs | 38 ++-- .../src/System/IO/FileSystemInfo.Unix.cs | 3 + .../src/System/IO/FileSystemInfo.Windows.cs | 4 + .../Enumeration/TrimmedPaths.netcoreapp.cs | 177 +++++++++++++++++- 5 files changed, 213 insertions(+), 101 deletions(-) diff --git a/src/System.IO.FileSystem/src/System/IO/File.cs b/src/System.IO.FileSystem/src/System/IO/File.cs index 9af91fb0a669..b72ded54b7d7 100644 --- a/src/System.IO.FileSystem/src/System/IO/File.cs +++ b/src/System.IO.FileSystem/src/System/IO/File.cs @@ -44,39 +44,18 @@ public static StreamWriter AppendText(string path) return new StreamWriter(path, append: true); } - - // Copies an existing file to a new file. An exception is raised if the - // destination file already exists. Use the - // Copy(string, string, boolean) method to allow - // overwriting an existing file. - // - // The caller must have certain FileIOPermissions. The caller must have - // Read permission to sourceFileName and Create - // and Write permissions to destFileName. - // + /// + /// Copies an existing file to a new file. + /// An exception is raised if the destination file already exists. + /// public static void Copy(string sourceFileName, string destFileName) - { - if (sourceFileName == null) - throw new ArgumentNullException(nameof(sourceFileName), SR.ArgumentNull_FileName); - if (destFileName == null) - throw new ArgumentNullException(nameof(destFileName), SR.ArgumentNull_FileName); - if (sourceFileName.Length == 0) - throw new ArgumentException(SR.Argument_EmptyFileName, nameof(sourceFileName)); - if (destFileName.Length == 0) - throw new ArgumentException(SR.Argument_EmptyFileName, nameof(destFileName)); + => Copy(sourceFileName, destFileName, overwrite: false); - InternalCopy(sourceFileName, destFileName, false); - } - - // Copies an existing file to a new file. If overwrite is - // false, then an IOException is thrown if the destination file - // already exists. If overwrite is true, the file is - // overwritten. - // - // The caller must have certain FileIOPermissions. The caller must have - // Read permission to sourceFileName - // and Write permissions to destFileName. - // + /// + /// Copies an existing file to a new file. + /// If is false, and exception will be + /// raised if the destination exists. Otherwise it will be overwritten. + /// public static void Copy(string sourceFileName, string destFileName, bool overwrite) { if (sourceFileName == null) @@ -88,36 +67,13 @@ public static void Copy(string sourceFileName, string destFileName, bool overwri if (destFileName.Length == 0) throw new ArgumentException(SR.Argument_EmptyFileName, nameof(destFileName)); - InternalCopy(sourceFileName, destFileName, overwrite); - } - - /// - /// Note: This returns the fully qualified name of the destination file. - /// - internal static string InternalCopy(string sourceFileName, string destFileName, bool overwrite) - { - Debug.Assert(sourceFileName != null); - Debug.Assert(destFileName != null); - Debug.Assert(sourceFileName.Length > 0); - Debug.Assert(destFileName.Length > 0); - - string fullSourceFileName = Path.GetFullPath(sourceFileName); - string fullDestFileName = Path.GetFullPath(destFileName); - - FileSystem.CopyFile(fullSourceFileName, fullDestFileName, overwrite); - - return fullDestFileName; + FileSystem.CopyFile(Path.GetFullPath(sourceFileName), Path.GetFullPath(destFileName), overwrite); } - // Creates a file in a particular path. If the file exists, it is replaced. // The file is opened with ReadWrite access and cannot be opened by another // application until it has been closed. An IOException is thrown if the // directory specified doesn't exist. - // - // Your application must have Create, Read, and Write permissions to - // the file. - // public static FileStream Create(string path) { return Create(path, DefaultBufferSize); @@ -127,48 +83,30 @@ public static FileStream Create(string path) // The file is opened with ReadWrite access and cannot be opened by another // application until it has been closed. An IOException is thrown if the // directory specified doesn't exist. - // - // Your application must have Create, Read, and Write permissions to - // the file. - // public static FileStream Create(string path, int bufferSize) - { - return new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize); - } + => new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize); public static FileStream Create(string path, int bufferSize, FileOptions options) - { - return new FileStream(path, FileMode.Create, FileAccess.ReadWrite, - FileShare.None, bufferSize, options); - } + => new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize, options); // Deletes a file. The file specified by the designated path is deleted. // If the file does not exist, Delete succeeds without throwing // an exception. // // On NT, Delete will fail for a file that is open for normal I/O - // or a file that is memory mapped. - // - // Your application must have Delete permission to the target file. - // + // or a file that is memory mapped. public static void Delete(string path) { if (path == null) throw new ArgumentNullException(nameof(path)); - string fullPath = Path.GetFullPath(path); - - FileSystem.DeleteFile(fullPath); + FileSystem.DeleteFile(Path.GetFullPath(path)); } - // Tests if a file exists. The result is true if the file // given by the specified path exists; otherwise, the result is // false. Note that if path describes a directory, // Exists will return true. - // - // Your application must have Read permission for the target directory. - // public static bool Exists(string path) { try diff --git a/src/System.IO.FileSystem/src/System/IO/FileInfo.cs b/src/System.IO.FileSystem/src/System/IO/FileInfo.cs index 3811ed8a2274..c6b3589decca 100644 --- a/src/System.IO.FileSystem/src/System/IO/FileInfo.cs +++ b/src/System.IO.FileSystem/src/System/IO/FileInfo.cs @@ -71,23 +71,15 @@ public bool IsReadOnly } public StreamReader OpenText() - => new StreamReader(FullPath, Encoding.UTF8, detectEncodingFromByteOrderMarks: true); + => new StreamReader(NormalizedPath, Encoding.UTF8, detectEncodingFromByteOrderMarks: true); public StreamWriter CreateText() - => new StreamWriter(FullPath, append: false); + => new StreamWriter(NormalizedPath, append: false); public StreamWriter AppendText() - => new StreamWriter(FullPath, append: true); + => new StreamWriter(NormalizedPath, append: true); - public FileInfo CopyTo(string destFileName) - { - if (destFileName == null) - throw new ArgumentNullException(nameof(destFileName), SR.ArgumentNull_FileName); - if (destFileName.Length == 0) - throw new ArgumentException(SR.Argument_EmptyFileName, nameof(destFileName)); - - return new FileInfo(File.InternalCopy(FullPath, destFileName, false), isNormalized: true); - } + public FileInfo CopyTo(string destFileName) => CopyTo(destFileName, overwrite: false); public FileInfo CopyTo(string destFileName, bool overwrite) { @@ -96,10 +88,12 @@ public FileInfo CopyTo(string destFileName, bool overwrite) if (destFileName.Length == 0) throw new ArgumentException(SR.Argument_EmptyFileName, nameof(destFileName)); - return new FileInfo(File.InternalCopy(FullPath, destFileName, overwrite), isNormalized: true); + string destinationPath = Path.GetFullPath(destFileName); + FileSystem.CopyFile(FullPath, destinationPath, overwrite); + return new FileInfo(destinationPath, isNormalized: true); } - public FileStream Create() => File.Create(FullPath); + public FileStream Create() => File.Create(NormalizedPath); public override void Delete() => FileSystem.DeleteFile(FullPath); @@ -110,13 +104,13 @@ public FileStream Open(FileMode mode, FileAccess access) => Open(mode, access, FileShare.None); public FileStream Open(FileMode mode, FileAccess access, FileShare share) - => new FileStream(FullPath, mode, access, share); + => new FileStream(NormalizedPath, mode, access, share); public FileStream OpenRead() - => new FileStream(FullPath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, false); + => new FileStream(NormalizedPath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, false); public FileStream OpenWrite() - => new FileStream(FullPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); + => new FileStream(NormalizedPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); // Moves a given file to a new location and potentially a new file name. // This method does work across volumes. @@ -153,7 +147,15 @@ public FileInfo Replace(string destinationFileName, string destinationBackupFile public FileInfo Replace(string destinationFileName, string destinationBackupFileName, bool ignoreMetadataErrors) { - File.Replace(FullPath, destinationFileName, destinationBackupFileName, ignoreMetadataErrors); + if (destinationFileName == null) + throw new ArgumentNullException(nameof(destinationFileName)); + + FileSystem.ReplaceFile( + FullPath, + Path.GetFullPath(destinationFileName), + destinationBackupFileName != null ? Path.GetFullPath(destinationBackupFileName) : null, + ignoreMetadataErrors); + return new FileInfo(destinationFileName); } diff --git a/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs b/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs index cbc0e9be0560..aac479e6e117 100644 --- a/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs +++ b/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs @@ -78,5 +78,8 @@ internal static void ThrowNotFound(string path) bool directoryError = !Directory.Exists(Path.GetDirectoryName(PathInternal.TrimEndingDirectorySeparator(path))); throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(Interop.Error.ENOENT), path, directoryError); } + + // There is no special handling for Unix- see Windows code for the reason we do this + internal string NormalizedPath => FullPath; } } diff --git a/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Windows.cs b/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Windows.cs index bfec961847ee..3ea48437e85d 100644 --- a/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Windows.cs +++ b/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Windows.cs @@ -150,5 +150,9 @@ public void Refresh() // when someone actually accesses a property _dataInitialized = FileSystem.FillAttributeInfo(FullPath, ref _data, returnErrorOnNotFound: false); } + + // If we're opened around a enumerated path that ends in a period or space we need to be able to + // act on the path normally (open streams/writers/etc.) + internal string NormalizedPath => PathInternal.EnsureExtendedPrefixIfNeeded(FullPath); } } diff --git a/src/System.IO.FileSystem/tests/Enumeration/TrimmedPaths.netcoreapp.cs b/src/System.IO.FileSystem/tests/Enumeration/TrimmedPaths.netcoreapp.cs index 8621e8eb662a..73b31ffde3e6 100644 --- a/src/System.IO.FileSystem/tests/Enumeration/TrimmedPaths.netcoreapp.cs +++ b/src/System.IO.FileSystem/tests/Enumeration/TrimmedPaths.netcoreapp.cs @@ -17,11 +17,11 @@ public void TrimmedPathsAreFound_Windows() // to access without using the \\?\ device syntax. We should, however, be able to find them // and retain the filename in the info classes and string results. - var directory = Directory.CreateDirectory(GetTestFilePath()); + DirectoryInfo directory = Directory.CreateDirectory(GetTestFilePath()); File.Create(@"\\?\" + Path.Combine(directory.FullName, "Trailing space ")).Dispose(); File.Create(@"\\?\" + Path.Combine(directory.FullName, "Trailing period.")).Dispose(); - var files = directory.GetFiles(); + FileInfo[] files = directory.GetFiles(); Assert.Equal(2, files.Count()); FSAssert.EqualWhenOrdered(new string[] { "Trailing space ", "Trailing period." }, files.Select(f => f.Name)); @@ -38,7 +38,7 @@ public void TrimmedPathsDeletion_Windows() // to access without using the \\?\ device syntax. We should, however, be able to delete them // from the info class. - var directory = Directory.CreateDirectory(GetTestFilePath()); + DirectoryInfo directory = Directory.CreateDirectory(GetTestFilePath()); File.Create(@"\\?\" + Path.Combine(directory.FullName, "Trailing space ")).Dispose(); File.Create(@"\\?\" + Path.Combine(directory.FullName, "Trailing period.")).Dispose(); @@ -47,18 +47,183 @@ public void TrimmedPathsDeletion_Windows() var paths = Directory.GetFiles(directory.FullName); Assert.All(paths, p => Assert.False(File.Exists(p))); - var files = directory.GetFiles(); + FileInfo[] files = directory.GetFiles(); Assert.Equal(2, files.Count()); Assert.All(files, f => Assert.True(f.Exists)); - foreach (var f in files) + foreach (FileInfo f in files) f.Refresh(); Assert.All(files, f => Assert.True(f.Exists)); - foreach (var f in files) + foreach (FileInfo f in files) { f.Delete(); f.Refresh(); } Assert.All(files, f => Assert.False(f.Exists)); + + foreach (FileInfo f in files) + { + f.Create().Dispose(); + f.Refresh(); + } + Assert.All(files, f => Assert.True(f.Exists)); + } + + [Fact] + [PlatformSpecific(TestPlatforms.Windows)] + public void TrimmedPathsOpen_Windows() + { + // Trailing spaces and periods are eaten when normalizing in Windows, making them impossible + // to access without using the \\?\ device syntax. We should, however, be able to open them + // from the info class when enumerating. + + DirectoryInfo directory = Directory.CreateDirectory(GetTestFilePath()); + string fileOne = Path.Join(directory.FullName, "Trailing space "); + string fileTwo = Path.Join(directory.FullName, "Trailing period."); + File.Create(@"\\?\" + fileOne).Dispose(); + File.Create(@"\\?\" + fileTwo).Dispose(); + + FileInfo[] files = directory.GetFiles(); + Assert.Equal(2, files.Length); + foreach (FileInfo fi in directory.GetFiles()) + { + // Shouldn't throw hitting any of the Open overloads + using (FileStream stream = fi.Open(FileMode.Open)) + { } + using (FileStream stream = fi.Open(FileMode.Open, FileAccess.Read)) + { } + using (FileStream stream = fi.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { } + using (FileStream stream = fi.OpenRead()) + { } + using (FileStream stream = fi.OpenWrite()) + { } + } + } + + [Fact] + [PlatformSpecific(TestPlatforms.Windows)] + public void TrimmedPathsText_Windows() + { + // Trailing spaces and periods are eaten when normalizing in Windows, making them impossible + // to access without using the \\?\ device syntax. We should, however, be able to open readers + // and writers from the the info class when enumerating. + + DirectoryInfo directory = Directory.CreateDirectory(GetTestFilePath()); + string fileOne = Path.Join(directory.FullName, "Trailing space "); + string fileTwo = Path.Join(directory.FullName, "Trailing period."); + File.WriteAllText(@"\\?\" + fileOne, "space"); + File.WriteAllText(@"\\?\" + fileTwo, "period"); + + FileInfo[] files = directory.GetFiles(); + Assert.Equal(2, files.Length); + foreach (FileInfo fi in directory.GetFiles()) + { + using (StreamReader reader = fi.OpenText()) + { + string content = reader.ReadToEnd(); + if (fi.FullName.EndsWith(fileOne)) + { + Assert.Equal("space", content); + } + else if (fi.FullName.EndsWith(fileTwo)) + { + Assert.Equal("period", content); + } + else + { + Assert.False(true, $"Unexpected name '{fi.FullName}'"); + } + } + + using (StreamWriter writer = fi.CreateText()) + { + writer.Write("foo"); + } + + using (StreamReader reader = fi.OpenText()) + { + Assert.Equal("foo", reader.ReadToEnd()); + } + + using (StreamWriter writer = fi.AppendText()) + { + writer.Write("bar"); + } + + using (StreamReader reader = fi.OpenText()) + { + Assert.Equal("foobar", reader.ReadToEnd()); + } + } + } + + [Fact] + [PlatformSpecific(TestPlatforms.Windows)] + public void TrimmedPathsCopyTo_Windows() + { + // Trailing spaces and periods are eaten when normalizing in Windows, making them impossible + // to access without using the \\?\ device syntax. We should, however, be able to copy them + // without the special syntax from the info class when enumerating. + + DirectoryInfo directory = Directory.CreateDirectory(GetTestFilePath()); + string fileOne = Path.Join(directory.FullName, "Trailing space "); + string fileTwo = Path.Join(directory.FullName, "Trailing period."); + File.Create(@"\\?\" + fileOne).Dispose(); + File.Create(@"\\?\" + fileTwo).Dispose(); + + FileInfo[] files = directory.GetFiles(); + Assert.Equal(2, files.Length); + foreach (FileInfo fi in directory.GetFiles()) + { + FileInfo newInfo = fi.CopyTo(Path.Join(directory.FullName, GetTestFileName())); + Assert.True(newInfo.Exists); + FileInfo newerInfo = fi.CopyTo(Path.Join(directory.FullName, GetTestFileName()), overwrite: true); + Assert.True(newerInfo.Exists); + } + + Assert.Equal(6, directory.GetFiles().Length); + } + + [Fact] + [PlatformSpecific(TestPlatforms.Windows)] + public void TrimmedPathsReplace_Windows() + { + // Trailing spaces and periods are eaten when normalizing in Windows, making them impossible + // to access without using the \\?\ device syntax. We should, however, be able to replace them + // from the info class when enumerating. + + DirectoryInfo directory = Directory.CreateDirectory(GetTestFilePath()); + string fileOne = Path.Join(directory.FullName, "Trailing space "); + string fileTwo = Path.Join(directory.FullName, "Trailing period."); + File.WriteAllText(@"\\?\" + fileOne, "space"); + File.WriteAllText(@"\\?\" + fileTwo, "period"); + + FileInfo[] files = directory.GetFiles(); + Assert.Equal(2, files.Length); + + FileInfo destination = new FileInfo(Path.Join(directory.FullName, GetTestFilePath())); + destination.Create().Dispose(); + + foreach (FileInfo fi in directory.GetFiles()) + { + fi.Replace(destination.FullName, null); + using (StreamReader reader = destination.OpenText()) + { + string content = reader.ReadToEnd(); + if (fi.FullName.EndsWith(fileOne)) + { + Assert.Equal("space", content); + } + else if (fi.FullName.EndsWith(fileTwo)) + { + Assert.Equal("period", content); + } + else + { + Assert.False(true, $"Unexpected name '{fi.FullName}'"); + } + } + } } } } From 0ee222c192195db3a9bc15cb6cd0b48e6775f1fd Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Tue, 6 Mar 2018 20:29:29 -0800 Subject: [PATCH 2/2] Tweak comments. Remove NotSupported and SecurityException from Exists as these are no longer thrown. Add a test fix I hadn't staged correctly. --- .../Win32/SafeHandles/SafeFindHandle.Windows.cs | 3 --- src/System.IO.FileSystem/src/System/IO/Directory.cs | 6 ------ src/System.IO.FileSystem/src/System/IO/File.cs | 10 ++++------ .../tests/Enumeration/TrimmedPaths.netcoreapp.cs | 4 ++-- 4 files changed, 6 insertions(+), 17 deletions(-) diff --git a/src/System.IO.FileSystem/src/Microsoft/Win32/SafeHandles/SafeFindHandle.Windows.cs b/src/System.IO.FileSystem/src/Microsoft/Win32/SafeHandles/SafeFindHandle.Windows.cs index c9c118c1b51d..dc02183f4306 100644 --- a/src/System.IO.FileSystem/src/Microsoft/Win32/SafeHandles/SafeFindHandle.Windows.cs +++ b/src/System.IO.FileSystem/src/Microsoft/Win32/SafeHandles/SafeFindHandle.Windows.cs @@ -3,10 +3,7 @@ // See the LICENSE file in the project root for more information. using System; -using System.Security; using System.Runtime.InteropServices; -using System.Runtime.CompilerServices; -using Microsoft.Win32; namespace Microsoft.Win32.SafeHandles { diff --git a/src/System.IO.FileSystem/src/System/IO/Directory.cs b/src/System.IO.FileSystem/src/System/IO/Directory.cs index d5e09c82819c..93e2d57c00ad 100644 --- a/src/System.IO.FileSystem/src/System/IO/Directory.cs +++ b/src/System.IO.FileSystem/src/System/IO/Directory.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.IO.Enumeration; using System.Linq; -using System.Security; namespace System.IO { @@ -42,9 +41,6 @@ public static DirectoryInfo CreateDirectory(string path) } // Tests if the given path refers to an existing DirectoryInfo on disk. - // - // Your application must have Read permission to the directory's - // contents. public static bool Exists(string path) { try @@ -59,8 +55,6 @@ public static bool Exists(string path) return FileSystem.DirectoryExists(fullPath); } catch (ArgumentException) { } - catch (NotSupportedException) { } // Security can throw this on ":" - catch (SecurityException) { } catch (IOException) { } catch (UnauthorizedAccessException) { } diff --git a/src/System.IO.FileSystem/src/System/IO/File.cs b/src/System.IO.FileSystem/src/System/IO/File.cs index b72ded54b7d7..2253c2b68c25 100644 --- a/src/System.IO.FileSystem/src/System/IO/File.cs +++ b/src/System.IO.FileSystem/src/System/IO/File.cs @@ -5,7 +5,6 @@ using System.Buffers; using System.Collections.Generic; using System.Diagnostics; -using System.Security; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -53,7 +52,7 @@ public static void Copy(string sourceFileName, string destFileName) /// /// Copies an existing file to a new file. - /// If is false, and exception will be + /// If is false, an exception will be /// raised if the destination exists. Otherwise it will be overwritten. /// public static void Copy(string sourceFileName, string destFileName, bool overwrite) @@ -93,7 +92,7 @@ public static FileStream Create(string path, int bufferSize, FileOptions options // If the file does not exist, Delete succeeds without throwing // an exception. // - // On NT, Delete will fail for a file that is open for normal I/O + // On Windows, Delete will fail for a file that is open for normal I/O // or a file that is memory mapped. public static void Delete(string path) { @@ -103,7 +102,7 @@ public static void Delete(string path) FileSystem.DeleteFile(Path.GetFullPath(path)); } - // Tests if a file exists. The result is true if the file + // Tests whether a file exists. The result is true if the file // given by the specified path exists; otherwise, the result is // false. Note that if path describes a directory, // Exists will return true. @@ -117,6 +116,7 @@ public static bool Exists(string path) return false; path = Path.GetFullPath(path); + // After normalizing, check whether path ends in directory separator. // Otherwise, FillAttributeInfo removes it and we may return a false positive. // GetFullPath should never return null @@ -129,8 +129,6 @@ public static bool Exists(string path) return FileSystem.FileExists(path); } catch (ArgumentException) { } - catch (NotSupportedException) { } // Security can throw this on ":" - catch (SecurityException) { } catch (IOException) { } catch (UnauthorizedAccessException) { } diff --git a/src/System.IO.FileSystem/tests/Enumeration/TrimmedPaths.netcoreapp.cs b/src/System.IO.FileSystem/tests/Enumeration/TrimmedPaths.netcoreapp.cs index 73b31ffde3e6..7ae7943b3b03 100644 --- a/src/System.IO.FileSystem/tests/Enumeration/TrimmedPaths.netcoreapp.cs +++ b/src/System.IO.FileSystem/tests/Enumeration/TrimmedPaths.netcoreapp.cs @@ -201,10 +201,10 @@ public void TrimmedPathsReplace_Windows() FileInfo[] files = directory.GetFiles(); Assert.Equal(2, files.Length); - FileInfo destination = new FileInfo(Path.Join(directory.FullName, GetTestFilePath())); + FileInfo destination = new FileInfo(Path.Join(directory.FullName, GetTestFileName())); destination.Create().Dispose(); - foreach (FileInfo fi in directory.GetFiles()) + foreach (FileInfo fi in files) { fi.Replace(destination.FullName, null); using (StreamReader reader = destination.OpenText())