2010-02-02 2 views
15

Ich versuche, eine große Anzahl der Dateien von einem Speicherort (durch große ich meine über 100000) zu entfernen, wobei die Aktion von einer Webseite initiiert wird. Offensichtlich konnte ich nur verwendenEine große Anzahl (> 100K) von Dateien mit C# löschen, während die Leistung in einer Webanwendung beibehalten wird?

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

Diese Methode hat sich bereits ein paar Mal geschrieben worden: How to delete all files and folders in a directory? und Delete files from directory if filename contains a certain word

Aber das Problem mit dieser Methode ist, dass, wenn Wenn Sie sagen, dass es sich um hunderttausend Dateien handelt, wird dies zu einem Leistungsproblem, da alle Dateipfade zuerst generiert werden müssen, bevor Sie sie durchlaufen.

Hinzugefügt, wenn eine Webseite eine Antwort von einer Methode wartet, die dies durchführt, wie Sie sich vorstellen können, wird es ein bisschen Quatsch aussehen!

Ein Gedanke, den ich hatte, war, dies in einem asynchronen Webservice-Aufruf einzupacken, und wenn es abgeschlossen ist, wird eine Antwort auf die Webseite ausgelöst, um zu sagen, dass sie entfernt wurde? Vielleicht legen Sie die Löschmethode in einem separaten Thread? Oder vielleicht sogar einen separaten Batch-Prozess verwenden, um das Löschen durchzuführen?

Ich habe ein ähnliches Problem beim Versuch, die Anzahl der Dateien in einem Verzeichnis zu zählen - wenn es eine große Anzahl von Dateien enthält.

Ich fragte mich, ob das alles ein bisschen übertrieben ist? I.e. Gibt es eine einfachere Methode, um damit umzugehen? Jede Hilfe wäre willkommen.

+1

Ich bin nicht sicher über C#, aber im Allgemeinen ist es nicht eine gute Idee, eine große Anzahl von Dateien in einem einzigen Verzeichnis zu haben. – Sands

+0

Sie haben es selbst erwähnt, asynchron ist das Schlüsselwort. – Pool

+0

Zu "Sands" - vertrau mir ich habe diese Entscheidung nicht getroffen !! :) –

Antwort

10
  1. GetFiles ist extrem langsam.
  2. Wenn Sie es von einer Website aufrufen, können Sie einfach einen neuen Thread werfen, der diesen Trick ausführt.
  3. Ein ASP.NET AJAX-Aufruf, der zurückgibt, ob noch übereinstimmende Dateien vorhanden sind, kann für grundlegende Fortschrittsupdates verwendet werden.

Unterhalb einer Implementierung eines schnellen Verpackungs Win32 für GetFiles, verwenden Sie es in Kombination mit einem neuen Thread und einer AJAX-Funktion wie: GetFilesUnmanaged(@"C:\myDir", "*.txt*).GetEnumerator().MoveNext().

Nutzungs

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; 
} 
+0

Wenn es lange dauert, kann die HTTP-Anfrage noch Timeout, obwohl. –

+0

Ich denke, die Idee, einen nicht verwalteten Code-Code zu verpacken, ist sicherlich ein Weg. Aber ich habe immer noch ein Problem mit dem Training, wenn der Prozess beendet ist oder nicht. Ich denke, ich könnte dies in einen Web-Service-Anruf setzen! Danke für die Antwort obwohl Jan - ich werde einen Blick auf diesen Code .. :) –

+0

Sie können feststellen, ob der Prozess beendet ist, mit einem ASP.NET AJAX-Aufruf, der ruft GetFilesUnmanaged (@ "C: \ myDir", "* .txt"). GetEnumerator(). MoveNext() '; es ist ein sehr billiger Aufruf im Gegensatz zum Standard 'GetFiles', und wenn es etwas zurückgibt; der Prozess ist noch nicht beendet :-). –

1

In einem separaten Thread schreiben oder eine Nachricht in eine Warteschlange schreiben (vielleicht MSMQ?), Wo eine andere Anwendung (vielleicht ein Windows-Dienst) diese Warteschlange abonniert und die Befehle ausführt (zB "Delete e: \ dir * .txt ") in seinem eigenen Prozess.

Die Nachricht sollte wahrscheinlich nur den Ordnernamen enthalten. Wenn Sie so etwas wie NServiceBus und Transaktionswarteschlangen verwenden, können Sie Ihre Nachricht posten und sofort zurückkehren, solange die Nachricht erfolgreich gesendet wurde. Wenn bei der Verarbeitung der Nachricht tatsächlich ein Problem auftritt, versucht sie es erneut und geht schließlich auf eine error queue, die Sie überwachen und Wartungsarbeiten durchführen können.

+0

Ja, definitiv Thread trennen !! :) Ich mag Ihre Idee über die Verwendung von MSMQ! Wird recherchieren und antworten! –

+0

Nein, ich empfehle nicht, einen anderen Thread in einem IIS-Anwendungspool zu verwenden. Ich empfehle einen völlig separaten Prozess, bei dem Sie etwas wie MSMQ (d. H. Mit NServiceBus) verwenden, um dem Prozess eine Nachricht zum Löschen zu senden. Wenn Sie NSB- und transaktionale MSMQ-Warteschlangen verwenden, haben Sie die Sicherheit, dass die Nachricht verarbeitet wurde. –

+0

Sorry, ich habe dich missverstanden .. :)! –

0

Starten Sie das Workout zu einem Worker-Thread und geben Sie dann Ihre Antwort an den Benutzer zurück.

Ich würde eine Anwendungsvariable anzeigen, um zu sagen, dass Sie "den großen Löschjob" machen, um zu stoppen, mehrere Threads auszuführen, die die gleiche Arbeit machen. Sie könnten dann eine andere Seite abfragen, die Ihnen eine Fortschrittsaktualisierung der Anzahl der entfernten Dateien geben könnte, wenn Sie das wollten?

Nur eine Frage, aber warum so viele Dateien?

+0

100k Dateien ist nicht viel, ich arbeite derzeit an einer Anwendung, die rund 2-3 Millionen Dateien mischt, die (nach Spezifikation) in Verzeichnisse von 100k-150k-Dateien aufgeteilt sind. rsync benötigt 60 Minuten für einen Trockenlauf. – dbemerlin

+0

Es ist viel zu tun, über einen Link/Button auf einer Website ist alles, was ich meinte :) –

0

Sie eine einfache Ajax Webmethod hinter Ihrer aspx Code schaffen könnte und mit Javascript nennen.

+0

Wenn es lange dauert, kann die HTTP-Anfrage trotzdem Timeout sein. –

+0

Ja, ich dachte an eine Ajax-Webmethode - aber das ist nicht die Lösung, wenn ich die GetFiles-Methode verwende. –

0

Die beste Wahl (imho) wäre ein separates Verfahren zu erstellen, die Dateien zu löschen/zählen und prüfen Sie über den Fortschritt durch die Abfrage sonst könnten Sie Probleme bekommen mit Browser-Timeouts.

0

Wow. Ich denke, Sie sind definitiv auf dem richtigen Weg, wenn ein anderer Dienst oder eine andere Organisation sich um das Löschen kümmert. Dabei können Sie auch Methoden bereitstellen, um den Löschprozess zu verfolgen und das Ergebnis dem Benutzer mit asynchronem JavaScript anzuzeigen.

Wie andere gesagt haben dies in einem anderen Prozess setzen ist eine großartige Idee. Sie möchten nicht, dass IIS Ressourcen mit solchen Jobs mit langer Laufzeit in Anspruch nimmt. Ein weiterer Grund dafür ist die Sicherheit. Sie möchten Ihrem Arbeitsprozess möglicherweise nicht die Möglichkeit geben, Dateien von der Festplatte zu löschen.

3

Können Sie alle Ihre Dateien im selben Verzeichnis?

Wenn ja, warum gehst du nicht einfach anrufen Directory.Delete(string,bool) auf dem subdir Sie löschen möchten?

Wenn Sie bereits eine Liste von Dateipfaden haben, die Sie loswerden möchten, erhalten Sie möglicherweise bessere Ergebnisse, indem Sie sie in ein temporäres Verzeichnis verschieben und dann löschen, anstatt jede Datei manuell zu löschen.

Cheers, Florian

+0

Müsste ich die System.IO.Directory.GetFiles() -Methode verwenden, um alle Dateien zu bekommen, die ich haben muss Bewegung? wie im folgenden Beispiel? http://msdn.microsoft.com/en-us/library/cc148994.aspx oder http://www.eggheadcafe.com/community/aspnet/2/63950/moving-files-from-one-fol .aspx Dies würde nur das gleiche Leistungsproblem verursachen, über das ich oben gesprochen habe, nicht wahr? Ich denke, alternativ könnte ich ein Skript wie rmdir /q/s verwenden - könnte einen Blick wert sein? –

+0

Ich denke, es würde eine Leistungsverlangsamung verursachen, aber nicht so drastisch wie alle Dateien einzeln löschen. Verschieben einer Datei ist sehr billig, löschen Sie es nicht so, also sollten Sie noch einige Perfs erhalten, indem Sie die Dateien in das Verzeichnis verschieben, das Sie dann löschen. Der beste Weg wäre, die Dateien im selben Verzeichnis zu erstellen, wenn Sie eine Möglichkeit finden, die Dateien nach der Art zu gruppieren, in der sie gelöscht werden, wenn Sie sie erhalten. –

+0

Ja, ich stimme mit Ihnen überein über den Leistungsunterschied zwischen dem Verschieben und Löschen von Dateien. Leider steht die Erstellung der Dateien momentan nicht direkt unter meiner Kontrolle. –

1

Mit mehr als 1000 Dateien in einem Verzeichnis ein großes Problem ist.

Wenn Sie in der Entwicklungsphase jetzt sind, sollten Sie in einem algo setzen, welche die Dateien in einem zufälligen Ordner abgelegt werden (in Ihrem Stammordner) mit einer Sicherheit von der Anzahl der Dateien in diesem Ordner zu sein unter 1024.

So etwas wie

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

Während dies zu tun, auch dafür sorgen, dass jedes Mal, wenn Sie eine Datei zu erstellen, fügen Sie es gleichzeitig zu einem HashMap oder Liste (der Pfad). In regelmäßigen Abständen serialisiert diese mit so etwas wie JSON.net auf das Dateisystem (Integrität willen, so dass selbst wenn Ihr Dienst ausfällt, können Sie die Dateiliste aus der serialisierten Form zurück).

Wenn Sie die Dateien oder die Abfrage unter ihnen aufzuräumen, zuerst tun eine Suche nach dieser HashMap oder Liste und dann wirken auf die Datei.Das ist besser als System.IO.Directory.GetFiles

0

Ich weiß, es ist alter Thread, aber zusätzlich zu Jan Jongboom antworte ich vorschlagen ähnliche Lösung, die ziemlich performant und universeller ist. Meine Lösung wurde entwickelt, um die Verzeichnisstruktur in DFS mit Unterstützung für lange Dateinamen (> 255 Zeichen) schnell zu entfernen. Der erste Unterschied besteht in der DLL-Importdeklaration.

[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 Struktur ist auch etwas anders:

[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; 
    } 

Um den Weg vorbereitet werden muss lange Wege wie folgt zu verwenden:

public void RemoveDirectory(string directoryPath) 
{ 
    var path = @"\\?\UNC\" + directoryPath.Trim(@" \/".ToCharArray()); 
    SearchAndDelete(path); 
} 

und hier ist die wichtigste Methode:

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())); 
    } 
} 

natürlich wir außerhalb dieses Verfahren weiter und speichern Verzeichnisse in separaten Liste gehen und löschen Sie sie in einem anderen Verfahren später, die wie folgt aussehen könnte:

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); 
     } 
} 
Verwandte Themen