代码之家  ›  专栏  ›  技术社区  ›  Aim Kai

在保持web应用程序性能的同时,删除大量(>100k)带有c的文件?

  •  15
  • Aim Kai  · 技术社区  · 14 年前

    我正试图删除 大的 来自某个位置的文件数(大体上,我的意思是超过100000个),在此位置,操作是从网页初始化的。显然我可以用

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

    目录.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服务调用来结束这个过程,当它完成时,它会返回一个对web页面的响应,说它们已经被删除了?也许把delete方法放在一个单独的线程中?或者甚至可以使用单独的批处理来执行删除?

    我有一个类似的问题,当我试图计算一个目录中的文件数量-如果它包含大量的文件。

    我想知道这是不是有点过分了?也就是说,有没有更简单的方法来处理这个问题?任何帮助都将不胜感激。

    10 回复  |  直到 6 年前
        1
  •  10
  •   Jan Jongboom    14 年前
    1. GetFiles 非常慢。
    2. 如果你是从一个网站调用它,你可能只是抛出一个新的线程来完成这个技巧。
    3. 返回是否仍有匹配文件的asp.net ajax调用可用于执行基本进度更新。

    下面是一个快速win32包装的实现 获取文件 ,将其与新线程和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;
    }
    
        2
  •  3
  •   Florian Doyon    14 年前

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

    如果是的话,你为什么不打个电话 Directory.Delete(string,bool) 在要删除的细分曲面上?

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

    干杯, 弗洛里安

        3
  •  1
  •   Neil Barnwell    14 年前

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

    邮件可能只包含文件夹名。如果你用类似的东西 NServiceBus 和事务性队列,然后您可以发布消息并在成功发布消息后立即返回。如果实际处理消息时出现问题,则它将重试并最终执行 error queue 你可以观察和执行维护。

        4
  •  1
  •   Cherian    14 年前

    超过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());
    

    执行此操作时,还要确保每次创建文件时,都将其同时添加到哈希映射或列表中(路径)。周期性地使用类似于 JSON.net 文件系统(为了完整性起见,这样即使服务失败,您也可以从序列化表单中获取文件列表)。

    当你想清理文件或查询它们时, 首先查找此哈希映射 或列出,然后 按文件办事。这比 System.IO.Directory.GetFiles

        5
  •  0
  •   Pete Duncanson    14 年前

    将工作启动到工作线程,然后将您的响应返回给用户。

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

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

        6
  •  0
  •   Steve Danner    14 年前

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

        7
  •  0
  •   Morfildur    14 年前

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

        8
  •  0
  •   smaclell    14 年前

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

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

        9
  •  0
  •   Janusz Skop    8 年前

    我知道这是老生常谈,但除了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_数据结构也略有不同:

        [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);
            }
    }
    
        10
  •  0
  •   Minh Nguyen    6 年前

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

    • 使用 Directory.EnumerateFiles(..) :这将遍历文件 在所有的文件都被检索出来之后。

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

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