Удалить большое количество (> 100 тыс.) Файлов с помощью С# при сохранении производительности в веб-приложении?
Я пытаюсь удалить количество больших файлов из местоположения (большим я имею в виду более 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
Этот метод уже был опубликован несколько раз:
Как удалить все файлы и папки в каталоге?
а также
Удалить файлы из каталога, если имя файла содержит определенное слово
Но проблема с этим методом заключается в том, что если вы скажете сто тысяч файлов, это становится проблемой производительности, так как она должна сначала сгенерировать все пути к файлу, прежде чем перебирать их.
Добавлен к этому, если веб-страница ждет ответа от метода, который выполняет это, поскольку вы можете себе представить, что это будет выглядеть немного мусором!
Мне показалось, что я должен был обернуть это в айшнорный веб-сервисный вызов, и когда он завершит его, он возвращает ответ на веб-страницу, чтобы сказать, что они были удалены? Может быть, поместите метод delete в отдельный поток? Или, возможно, даже использовать отдельный пакетный процесс для выполнения удаления?
У меня есть аналогичная проблема при попытке подсчета количества файлов в каталоге - если она содержит большое количество файлов.
Мне было интересно, все это немного переборщило? То есть есть ли более простой способ справиться с этим? Любая помощь будет оценена.
Ответы
Ответ 1
-
GetFiles
работает очень медленно.
- Если вы вызываете его с веб-сайта, вы можете просто добавить новый поток, который сделает этот трюк.
- Абонентский вызов ASP.NET AJAX, возвращающий все ли совпадающие файлы, может использоваться для выполнения базовых обновлений прогресса.
Ниже для реализации быстрой загрузки Win32 для GetFiles
используйте его в сочетании с новой 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;
}
Ответ 2
Можете ли вы поместить все ваши файлы в один каталог?
Если да, почему бы вам просто не позвонить Directory.Delete(string,bool)
в поддире, который вы хотите удалить?
Если у вас уже есть список путей к файлам, от которых вы хотите избавиться, вы можете получить лучшие результаты, переместив их в temp dir, а затем удалив их, а не удалив каждый файл вручную.
Cheers,
Флориан
Ответ 3
Сделайте это в отдельном потоке или отправьте сообщение в очередь (возможно MSMQ?), где другое приложение (возможно, служба Windows) подписывается на эту очередь и выполняет команды (т.е. "Удалить e:\dir *.txt" ) в своем собственном процессе.
Возможно, сообщение должно включать только имя папки. Если вы используете что-то вроде NServiceBus и транзакционных очередей, вы можете опубликовать свое сообщение и сразу же вернуться, пока сообщение было успешно отправлено. Если есть проблема с фактической обработкой сообщения, он будет повторять попытку и в конечном итоге перейти в очередь ошибок, в которой вы можете смотреть и выполнять техническое обслуживание.
Ответ 4
Наличие более 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
Ответ 5
Загрузите работу в рабочий поток, а затем верните ответ пользователю.
Я бы запустил переменную приложения, чтобы сказать, что вы выполняете "большое задание удаления", чтобы остановить запуск нескольких потоков, выполняющих ту же работу. Затем вы можете опросить другую страницу, которая может дать вам обновление хода количества файлов, удаленных до сих пор, если вы хотите?
Просто запрос, но почему так много файлов?
Ответ 6
Вы можете создать простой веб-метод ajax в своем коде aspx и вызвать его с помощью javascript.
Ответ 7
Лучшим выбором (imho) было бы создание отдельного процесса для удаления/подсчета файлов и проверки прогресса путем опроса, иначе у вас могут возникнуть проблемы с тайм-аутами браузера.
Ответ 8
Ого. Я думаю, что вы определенно на правильном пути, имея некоторые другие службы или организации, которые заботятся об удалении. При этом вы также можете предоставить методы отслеживания процесса удаления и отображения результата пользователю с помощью asynch javascript.
Как говорили другие, это в другом процессе - отличная идея. Вы не хотите, чтобы IIS задерживали ресурсы, используя такие длительные рабочие задания. Другая причина для этого - безопасность. Возможно, вы не захотите предоставить вашему рабочему процессу возможность удалять файлы с диска.
Ответ 9
Я знаю это старую нить, но в дополнение к ответу Яна Джонгбома я предлагаю аналогичное решение, которое является довольно совершенным и универсальным. Мое решение было создано для быстрого удаления структуры каталогов в 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);
}
}