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

时间:2010-02-02 16:43:59

标签: c# file-io file-management

我正在尝试从某个位置删除大量数量的文件(大的意思是超过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

此方法已经发布了几次: How to delete all files and folders in a directory?Delete files from directory if filename contains a certain word

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

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

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

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

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

10 个答案:

答案 0 :(得分:10)

  1. GetFiles非常慢。
  2. 如果你是从一个网站调用它,你可能只是抛出一个新的Thread来做这个技巧。
  3. 返回是否仍有匹配文件的ASP.NET AJAX调用可用于执行基本进度更新。
  4. 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<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;
    }
    

答案 1 :(得分:3)

您可以将所有文件放在同一目录中吗?

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

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

干杯, 弗洛里安

答案 2 :(得分:1)

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

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

答案 3 :(得分:1)

目录中超过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

更好

答案 4 :(得分:0)

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

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

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

答案 5 :(得分:0)

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

答案 6 :(得分:0)

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

答案 7 :(得分:0)

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

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

答案 8 :(得分:0)

我知道这是旧帖子,但除了Jan Jongboom的答案之外,我提出了类似的解决方案,这种解决方案非常高效且更具普遍性。我的解决方案是为了在DFS中快速删除目录结构而构建的,支持长文件名(&gt; 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);
        }
}

答案 9 :(得分:0)

在后端加快速度的一些改进:

  • 使用Directory.EnumerateFiles(..):这将迭代文件 检索完所有文件后无需等待。

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

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