Удалить большое количество (> 100k) файлов с C # Whilst поддерживая производительность в веб-приложении?

StackOverflow https://stackoverflow.com/questions/2185837

  •  25-09-2019
  •  | 
  •  

Вопрос

Я пытаюсь удалить большой Количество файлов из местоположения (по большому мнению, я имею в виду более 100000), в результате чего действие инициируется с веб-страницы. Очевидно, я мог бы просто использовать

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

Каталог. GTEDFILES.http://msdn.microsoft.com/en-us/library/wz42302f.aspx.

Этот метод уже был опубликован несколько раз:Как удалить все файлы и папки в каталоге?а такжеУдалить файлы из каталога, если имя файла содержит определенное слово

Но проблема с этим методом заключается в том, что если вы говорите сто тысяч файлов, он становится проблемой производительности, поскольку она должна сначала генерировать все файлы, прежде чем зацикливаться через них.

Добавлено к этому, если веб-страница ждет ответа от метода, который выполняет это, как вы можете себе представить, это будет выглядеть немного мусор!

Одна мысль, когда у меня было, было, чтобы обернуть это в асихричном вызове веб-сервиса, и когда он завершает его отключить ответ на веб-страницу, чтобы сказать, что они были удалены? Может быть, поставить метод удаления в отдельном потоке? Или, может быть, даже использовать отдельный пакетный процесс для выполнения удаления?

У меня есть подобная проблема при попытке посчитать количество файлов в каталоге - если он содержит большое количество файлов.

Мне было интересно, если это все немного наполнено? Т.е. есть ли более простым методом для решения с этим? Любая помощь будет оценена.

Это было полезно?

Решение

  1. GetFiles чрезвычайно медленно.
  2. Если вы вызываете его с веб-сайта, вы можете просто бросить новую тему, которая делает этот трюк.
  3. Вызов ASP.NET AJAX, который возвращает, есть ли все еще подходящие файлы, могут использоваться для проведения основных обновлений прогресса.

Ниже реализации быстрого обертывания Win32 GetFiles, Используйте его в сочетании с новой нитью и функцией 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;
}

Другие советы

Можете ли вы поместить все свои файлы в тот же каталог?

Если это так, почему бы тебе не позвонить Directory.Delete(string,bool) На подпадине вы хотите удалить?

Если у вас уже есть список путей файлов, в которых вы хотите избавиться, вы можете получить лучшие результаты, переместив их в TEMP DIR, а затем удалять их, а не удалять каждый файл вручную.

Ура, Флориан

Сделайте это в отдельной нити или опубликуйте сообщение в очередь (может быть 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());

При этом, также убедитесь, что каждый раз, когда вы создаете файл, добавьте его в хесмап или список одновременно (путь). Периодически сериализовать это, используя что-то вроде Json.net. На файловую систему (ради значения целостности, чтобы даже если ваш сервис терпит неудачу, вы можете вернуть список файлов из сериализованной формы).

Когда вы хотите очистить файлы или запрос среди них, Сначала сделайте поиск этого хесмапа или список, а затем действовать в файл. Это лучше, чем System.IO.Directory.GetFiles

Загрузите работу до рабочей нити, а затем верните свой ответ на пользователя.

Я бы флагомпировал переменную приложения, чтобы сказать, что вы делаете «Большой удаление задания», чтобы остановить запустить несколько потоков, выполняющих одну и ту же работу. Затем вы можете опросить другую страницу, которая может дать вам обновление прогресса количества удаленных файлов, если вы хотите?

Просто запрос, но почему так много файлов?

Вы можете создать простой Ajax Webmethod в вашем коде ASPX позади и вызовите его с помощью JavaScript.

Лучший выбор (IMHO) было бы создать отдельный процесс для удаления / подсчета файлов и проверки прогресса путем опроса, в противном случае вы можете получить проблемы с таймаусами браузера.

Вот это да. Я думаю, что вы определенно на правильном пути, имея какое-то другое обслуживание или организацию, заботясь о удалении. При этом вы также можете предоставить методы для отслеживания процесса удаления и отображение результата пользователю с помощью Asynch JavaScript.

Поскольку другие сказали, что положить это в другой процесс, это отличная идея. Вы не хотите использовать ресурсы, использующие такие длинные рабочие места. Еще одна причина для этого является безопасность. Возможно, вы не хотите предоставить свой процесс работы этой способности удалять файлы с диска.

Я знаю, что это старая нить, но в дополнение к Jan Jongboom ответит, я предлагаю подобное решение, которое является довольно исполненным и более универсальным. Мое решение было создано для быстрого удаления структуры каталогов в DFS с поддержкой длинных имен файлов (> 255 символов). Первая разница находится в DLL Import Декларации.

[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-запрос все равно будет время время ожидания с большим количеством файлов, поэтому процесс заднего конечного управления должен быть выполнен в отдельной рабочей нити и уведомлять результаты обратно в веб-клиент после завершения.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top