使用c#删除大量(> 100K)文件,同时保持Web应用程序的性能?

我试图从一个位置删除大量文件(大的意思是超过100000),从而从网页启动操作。 显然我可以使用

string[] files = System.IO.Directory.GetFiles("path with files to delete"); foreach (var file in files) { IO.File.Delete(file); } 

Directory.GetFiles http://msdn.microsoft.com/en-us/library/wz42302f.aspx

此方法已经发布了几次: 如何删除目录中的所有文件和文件夹? 如果filename包含某个单词,则从目录中删除文件

但是这个方法的问题在于,如果你说十万个文件就会成为一个性能问题,因为它必须首先生成所有的文件路径然后再循环它们。

如果网页正在等待正在执行此操作的方法的响应,则添加到此,因为您可以想象它看起来有点垃圾!

我有一个想法是在一个不正常的Web服务调用中将它包装起来,当它完成时它会激活对网页的响应,说它们已被删除了吗? 也许将删除方法放在一个单独的线程中? 或者甚至可以使用单独的批处理来执行删除?

在尝试计算目录中的文件数时,我遇到了类似的问题 – 如果它包含大量文件。

我想知道这是否有点矫枉过正? 即有一个更简单的方法来处理这个? 任何帮助,将不胜感激。

  1. GetFiles非常慢。
  2. 如果你是从网站调用它,你可能只是抛出一个新的线程来做这个技巧。
  3. 返回是否仍存在匹配文件的ASP.NET AJAX调用可用于执行基本进度更新。

下面是GetFiles的快速Win32包装的实现,将它与新的Thread和AJAX函数结合使用,如: GetFilesUnmanaged(@"C:\myDir", "*.txt*).GetEnumerator().MoveNext()

用法

 Thread workerThread = new Thread(new ThreadStart((MethodInvoker)(()=> { foreach(var file in GetFilesUnmanaged(@"C:\myDir", "*.txt")) File.Delete(file); }))); workerThread.Start(); //just go on with your normal requests, the directory will be cleaned while the user can just surf around 

  public static IEnumerable GetFilesUnmanaged(string directory, string filter) { return new FilesFinder(Path.Combine(directory, filter)) .Where(f => (f.Attributes & FileAttributes.Normal) == FileAttributes.Normal || (f.Attributes & FileAttributes.Archive) == FileAttributes.Archive) .Select(s => s.FileName); } } public class FilesEnumerator : IEnumerator { #region Interop imports private const int ERROR_FILE_NOT_FOUND = 2; private const int ERROR_NO_MORE_FILES = 18; [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] private static extern IntPtr FindFirstFile(string lpFileName, out WIN32_FIND_DATA lpFindFileData); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] private static extern bool FindNextFile(SafeHandle hFindFile, out WIN32_FIND_DATA lpFindFileData); #endregion #region Data Members private readonly string _fileName; private SafeHandle _findHandle; private WIN32_FIND_DATA _win32FindData; #endregion public FilesEnumerator(string fileName) { _fileName = fileName; _findHandle = null; _win32FindData = new WIN32_FIND_DATA(); } #region IEnumerator Members public FoundFileData Current { get { if (_findHandle == null) throw new InvalidOperationException("MoveNext() must be called first"); return new FoundFileData(ref _win32FindData); } } object IEnumerator.Current { get { return Current; } } public bool MoveNext() { if (_findHandle == null) { _findHandle = new SafeFileHandle(FindFirstFile(_fileName, out _win32FindData), true); if (_findHandle.IsInvalid) { int lastError = Marshal.GetLastWin32Error(); if (lastError == ERROR_FILE_NOT_FOUND) return false; throw new Win32Exception(lastError); } } else { if (!FindNextFile(_findHandle, out _win32FindData)) { int lastError = Marshal.GetLastWin32Error(); if (lastError == ERROR_NO_MORE_FILES) return false; throw new Win32Exception(lastError); } } return true; } public void Reset() { if (_findHandle.IsInvalid) return; _findHandle.Close(); _findHandle.SetHandleAsInvalid(); } public void Dispose() { _findHandle.Dispose(); } #endregion } public class FilesFinder : IEnumerable { readonly string _fileName; public FilesFinder(string fileName) { _fileName = fileName; } public IEnumerator GetEnumerator() { return new FilesEnumerator(_fileName); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } public class FoundFileData { public string AlternateFileName; public FileAttributes Attributes; public DateTime CreationTime; public string FileName; public DateTime LastAccessTime; public DateTime LastWriteTime; public UInt64 Size; internal FoundFileData(ref WIN32_FIND_DATA win32FindData) { Attributes = (FileAttributes)win32FindData.dwFileAttributes; CreationTime = DateTime.FromFileTime((long) (((UInt64)win32FindData.ftCreationTime.dwHighDateTime << 32) + (UInt64)win32FindData.ftCreationTime.dwLowDateTime)); LastAccessTime = DateTime.FromFileTime((long) (((UInt64)win32FindData.ftLastAccessTime.dwHighDateTime << 32) + (UInt64)win32FindData.ftLastAccessTime.dwLowDateTime)); LastWriteTime = DateTime.FromFileTime((long) (((UInt64)win32FindData.ftLastWriteTime.dwHighDateTime << 32) + (UInt64)win32FindData.ftLastWriteTime.dwLowDateTime)); Size = ((UInt64)win32FindData.nFileSizeHigh << 32) + win32FindData.nFileSizeLow; FileName = win32FindData.cFileName; AlternateFileName = win32FindData.cAlternateFileName; } } ///  /// Safely wraps handles that need to be closed via FindClose() WIN32 method (obtained by FindFirstFile()) ///  public class SafeFindFileHandle : SafeHandleZeroOrMinusOneIsInvalid { [DllImport("kernel32.dll", SetLastError = true)] private static extern bool FindClose(SafeHandle hFindFile); public SafeFindFileHandle(bool ownsHandle) : base(ownsHandle) { } protected override bool ReleaseHandle() { return FindClose(this); } } // The CharSet must match the CharSet of the corresponding PInvoke signature [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] public struct WIN32_FIND_DATA { public uint dwFileAttributes; public FILETIME ftCreationTime; public FILETIME ftLastAccessTime; public FILETIME ftLastWriteTime; public uint nFileSizeHigh; public uint nFileSizeLow; public uint dwReserved0; public uint dwReserved1; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string cFileName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] public string cAlternateFileName; } 

你能把所有文件放在同一个目录中吗?

如果是这样,为什么不在要删除的子目录上调用Directory.Delete(string,bool)

如果您已经有一个想要删除的文件路径列表,那么实际上可以通过将它们移动到临时目录然后删除它们而不是手动删除每个文件来获得更好的结果。

干杯,弗洛里安

在单独的线程中执行,或将消息发布到队列(可能是MSMQ ?),其中另一个应用程序(可能是Windows服务)订阅该队列并执行命令(即“删除e:\ dir * .txt”)在它自己的过程中。

该消息可能只包括文件夹名称。 如果您使用NServiceBus和事务性队列之类的东西,那么只要消息成功发布,您就可以发布消息并立即返回。 如果实际处理消息时出现问题,那么它将重试并最终进入您可以观察并执行维护的错误队列 。

在目录中有超过1000个文件是一个很大的问题。

如果你现在处于开发阶段,你应该考虑放入一个算法 ,将文件放入一个随机文件夹(在根文件夹中),确保该文件夹中的文件数量不超过1024

就像是

 public UserVolumeGenerator() { SetNumVolumes((short)100); SetNumSubVolumes((short)1000); SetVolumesRoot("/var/myproj/volumes"); } public String GenerateVolume() { int volume = random.nextInt(GetNumVolumes()); int subVolume = random.nextInt(GetNumSubVolumes()); return Integer.toString(volume) + "/" + Integer.toString(subVolume); } private static final Random random = new Random(System.currentTimeMillis()); 

在执行此操作时,还要确保每次创建文件时,将其同时添加到HashMap或列表(路径)。 定期使用类似JSON.net的文件系统将其序列化(完整性,即使您的服务失败,您也可以从序列化表单中取回文件列表)。

当您想要清理文件或查询它们时, 首先查找此HashMap或列表,然后对该文件进行操作。 这比System.IO.Directory.GetFiles更好

将工作引导到工作线程,然后将响应返回给用户。

我会标记一个应用程序变量来表示你正在做“大删除工作”以停止运行多个执行相同工作的线程。 然后,您可以轮询另一个页面,如果您愿意,可以为您提供目前已删除的文件数量的进度更新?

只是一个查询,但为什么这么多的文件?

您可以在后面的aspx代码中创建一个简单的ajax webmethod,并使用javascript调用它。

最好的选择(imho)是创建一个单独的进程来删除/计算文件并通过轮询检查进度,否则你可能会遇到浏览器超时问题。

哇。 我认为你肯定是在正确的轨道上,让其他服务或实体负责删除。 在这样做时,您还可以提供跟踪删除过程的方法,并使用asynch javascript向用户显示结果。

正如其他人所说,把它放在另一个过程中是一个好主意。 您不希望IIS使用如此长时间运行的作业来占用资源。 这样做的另一个原因是安全性。 您可能不希望为您的工作进程提供从磁盘中删除文件的能力。

我知道这是旧线程,但除了Jan Jongboom之外,我还提出了类似的解决方案,它具有相当高的性能和更高的通用性。 我的解决方案是为了快速删除DFS中的目录结构而构建的,支持长文件名(> 255个字符)。 第一个区别在于DLL导入声明。

 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern IntPtr FindFirstFile(string lpFileName, ref WIN32_FIND_DATA lpFindFileData); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool FindNextFile(IntPtr hDindFile, ref WIN32_FIND_DATA lpFindFileData); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MashalAs(UnmanagedType.Bool] static extern bool DeleteFile(string lpFileName) [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MashalAs(UnmanagedType.Bool] static extern bool DeleteDirectory(string lpPathName) [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool FindClose(IntPtr hFindFile); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLAstError = true)] static extern uint GetFileAttributes(string lpFileName); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLAstError = true)] static extern bool SetFileAttributes(string lpFileName, uint dwFileAttributes); 

WIN32_FIND_DATA结构也略有不同:

  [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode), Serializable, BestFitMapping(false)] internal struct WIN32_FIND_DATA { internal FileAttributes dwFileAttributes; internal FILETIME ftCreationTime; internal FILETIME ftLastAccessTime; internal FILETIME ftLastWriteTime; internal int nFileSizeHigh; internal int nFileSizeLow; internal int dwReserved0; internal int dwReserved1; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] internal string cFileName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] internal string cAlternative; } 

为了使用长路径,路径需要按如下方式准备:

 public void RemoveDirectory(string directoryPath) { var path = @"\\?\UNC\" + directoryPath.Trim(@" \/".ToCharArray()); SearchAndDelete(path); } 

这是主要方法:

 private void SearchAndDelete(string path) { var fd = new WIN32_FIND_DATA(); var found = false; var handle = IntPtr.Zero; var invalidHandle = new IntPtr(-1); var fileAttributeDir = 0x00000010; var filesToRemove = new List(); try { handle = FindFirsFile(path + @"\*", ref fd); if (handle == invalidHandle) return; do { var current = fd.cFileName; if (((int)fd.dwFileAttributes & fileAttributeDir) != 0) { if (current != "." && current != "..") { var newPath = Path.Combine(path, current); SearchAndDelete(newPath); } } else { filesToRemove.Add(Path.Combine(path, current)); } found = FindNextFile(handle, ref fd); } while (found); } finally { FindClose(handle); } try { object lockSource = new Object(); var exceptions = new List(); Parallel.ForEach(filesToRemove, file, => { var attrs = GetFileAttributes(file); attrs &= ~(uint)0x00000002; // hidden attrs &= ~(uint)0x00000001; // read-only SetFileAttributes(file, attrs); if (!DeleteFile(file)) { var msg = string.Format("Cannot remove file {0}.{1}{2}", file.Replace(@"\\?\UNC", @"\"), Environment.NewLine, new Win32Exception(Marshal.GetLastWin32Error()).Message); lock(lockSource) { exceptions.Add(new Exceptions(msg)); } } }); if (exceptions.Any()) { throw new AggregateException(exceptions); } } var dirAttr = GetFileAttributes(path); dirAttr &= ~(uint)0x00000002; // hidden dirAttr &= ~(uint)0x00000001; // read-only SetfileAttributtes(path, dirAttr); if (!RemoveDirectory(path)) { throw new Exception(new Win32Exception(Marshal.GetLAstWin32Error())); } } 

当然,我们可以进一步将目录存储在该方法之外的单独列表中,稍后在另一个方法中将其删除,如下所示:

 private void DeleteDirectoryTree(List directories) { // group directories by depth level and order it by level descending var data = directories.GroupBy(d => d.Split('\\'), d => d, (key, dirs) => new { Level = key, Directories = dirs.ToList() }).OrderByDescending(l => l.Level); var exceptions = new List(); var lockSource = new Object(); foreach (var level in data) { Parallel.ForEach(level.Directories, dir => { var attrs = GetFileAttributes(dir); attrs &= ~(uint)0x00000002; // hidden attrs &= ~(uint)0x00000001; // read-only SetFileAttributes(dir, attrs); if (!RemoveDirectory(dir)) { var msg = string.Format("Cannot remove directory {0}.{1}{2}", dir.Replace(@"\\?\UNC\", string.Empty), Environment.NewLine, new Win32Exception(Marshal.GetLastWin32Error()).Message); lock (lockSource) { exceptions.Add(new Exception(msg)); } } }); } if (exceptions.Any()) { throw new AggregateException(exceptions); } } 

一些改进,以加快后端:

  • 使用Directory.EnumerateFiles(..) :这将在检索完所有文件后无需等待即可遍历文件。

  • 使用Parallel.Foreach(..) :这将同时删除文件。

它应该更快但显然HTTP请求仍然会因大量文件而超时,因此后端进程应该在单独的工作线程中执行,并在完成后将结果通知给Web客户端。