使用 C# 删除大量 (>100K) 文件,同时保持 Web 应用程序的性能?
-
25-09-2019 - |
题
我正在尝试删除一个 大的 来自某个位置的文件数量(我的意思是超过 100000 个),其中该操作是从网页启动的。显然我可以使用
string[] files = System.IO.Directory.GetFiles("path with files to delete");
foreach (var file in files) {
IO.File.Delete(file);
}
目录.GetFileshttp://msdn.microsoft.com/en-us/library/wz42302f.aspx
这个方法已经被发布过几次了:如何删除一个目录下的所有文件和文件夹?和如果文件名包含特定单词,则从目录中删除文件
但这种方法的问题是,如果你说有十万个文件,它就会成为性能问题,因为它必须先生成所有文件路径,然后再循环它们。
添加到此,如果网页正在等待执行此操作的方法的响应,正如您可以想象的那样,它看起来有点垃圾!
我的一个想法是将其包装在一个异步 Web 服务调用中,当它完成时,它会向网页发出一个响应,表明它们已被删除?也许将删除方法放在单独的线程中?或者甚至可以使用单独的批处理来执行删除?
当尝试计算目录中的文件数量时,我遇到了类似的问题 - 如果它包含大量文件。
我想知道这是否有点矫枉过正?IE。有没有更简单的方法来处理这个问题?任何帮助,将不胜感激。
解决方案
GetFiles
是非常缓慢。- 如果你是援引它从一个网站,你可能只是扔一个新的线这不会这一招。
- 一个ASP.NET 阿贾克斯呼叫返回是否仍有匹配的文件,可以用来做基本进展的更新。
下一个执行情况的一个快速Win32包裹 GetFiles
, 使用它结合一个新的线和阿贾克斯功能,如: 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<string> 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<FoundFileData>
{
#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<FoundFileData> 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<FoundFileData>
{
readonly string _fileName;
public FilesFinder(string fileName)
{
_fileName = fileName;
}
public IEnumerator<FoundFileData> 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;
}
}
/// <summary>
/// Safely wraps handles that need to be closed via FindClose() WIN32 method (obtained by FindFirstFile())
/// </summary>
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)
在录你想要删除的?
如果你已经有了一个列表的文件路径你想要摆脱的,则可能实际上获得更好的结果,通过将它们移到一个临时dir然后删除它们,而不是删除的每个文件。
干杯, 弗洛里安
有超过1000个文件的目录是一个巨大的问题。
如果你是在开发阶段,现在,你应该考虑把在的 <强> ALGO 强> 将这些文件放入到随机文件夹(根文件夹中)与文件的数量的该文件夹中担保人为<强> 下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调用它。
的最佳选择(恕我直言)是创建一个单独的进程,删除/计数的文件和检查通过查询进度,否则你可能会得到与浏览器超时问题。
哇。我想你肯定是正确的轨道上有一些其他服务或实体照顾删除上。在这样做时,您也可以用于跟踪删除的过程,并利用非同步的JavaScript显示结果给用户提供的方法。
正如其他人说把这个在另一个进程中是一个好主意。你不想使用这种长时间运行的作业IIS霸占资源。另一个原因是这样做的安全性。你可能不希望给您的工作流程,才能从磁盘中删除文件。
我知道这是旧线,但除了扬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<string>();
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<Exception>();
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<string> 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<Exception>();
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请求仍将超时的大量文件,所以后面结束进程应该是执行在单独的工作线,并通知结果回到网客户的整理完毕。