Удалить большое количество (> 100 КБ) файлов с помощью c #, сохраняя при этом производительность в веб-приложении? - PullRequest
15 голосов
/ 02 февраля 2010

Я пытаюсь удалить большое количество файлов из местоположения (по большому счету я имею в виду более 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

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

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

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

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

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

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

Ответы [ 10 ]

10 голосов
/ 02 февраля 2010
  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;
}
3 голосов
/ 02 февраля 2010

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

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

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

Ура, Florian

1 голос
/ 03 февраля 2010

Наличие более 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

1 голос
/ 02 февраля 2010

Сделайте это в отдельном потоке или отправьте сообщение в очередь (возможно, MSMQ ?), Где другое приложение (возможно, служба Windows) подписано на эту очередь и выполняет команды (например, «Удалить»).e: \ dir * .txt ") в своем собственном процессе.

Сообщение должно, вероятно, содержать имя папки.Если вы используете что-то вроде NServiceBus и транзакционных очередей, вы можете опубликовать свое сообщение и сразу же вернуться, если сообщение было успешно отправлено.Если при обработке сообщения возникнет проблема, то оно повторится и в конечном итоге перейдет в очередь ошибок , которую можно просмотреть и выполнить обслуживание.

0 голосов
/ 11 апреля 2018

Некоторые улучшения для ускорения работы в бэкэнде:

  • Использовать Directory.EnumerateFiles(..): это будет перебирать файлы без ожидания после получения всех файлов.

  • Использовать Parallel.Foreach(..): одновременно удаляются файлы.

Это должно быть быстрее, но, очевидно, HTTP-запрос все еще будет иметь тайм-аут с большим количеством файлов, поэтому внутренний процесс должен выполняться в отдельном рабочем потоке и уведомлять о результатах обратно в веб-клиент после завершения.

0 голосов
/ 09 марта 2016

Я знаю, что это старая ветка, но в дополнение к ответу Яна Чонбоома я предлагаю аналогичное решение, которое является довольно производительным и более универсальным Мое решение было построено для быстрого удаления структуры каталогов в 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);
        }
}
0 голосов
/ 02 февраля 2010

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

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

0 голосов
/ 02 февраля 2010

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

0 голосов
/ 02 февраля 2010

Вы можете создать простой веб-метод ajax в своем коде aspx и вызывать его с помощью javascript.

0 голосов
/ 02 февраля 2010

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...