Löschen Sie eine große Anzahl (> 100 K) von Dateien mit c #, während der Leistung in einer Web-Anwendung beibehalten?

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

  •  25-09-2019
  •  | 
  •  

Frage

Ich versuche, eine groß Anzahl von Dateien von einem Ort (durch große I Mittelwert über 100000) zu löschen, wobei die Aktion von einer Webseite wird initiierte. Offensichtlich konnte ich nur verwenden

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

hat diese Methode bereits ein paar Mal geschrieben worden: Wie alle Dateien und Ordner in einem Verzeichnis löschen ? und Löschen von Dateien aus dem Verzeichnis, wenn Dateiname enthält ein bestimmtes Wort

Aber das Problem mit dieser Methode ist, dass, wenn Sie hunderttausend Dateien sagen, es wird ein Performance-Problem, da sie alle der Dateipfade zu erzeugen haben, bevor sie durch Looping.

Hinzu kommt, wenn eine Webseite eine Antwort von einem Verfahren wartet, die dies ausführt, wie man sich vorstellen kann es ein wenig Müll aussehen wird!

Ein dachte ich hatte, war dies in einem einen asychrnonous Web-Service-Aufruf einpacken und wenn sie abgeschlossen ist es feuert zurück eine Antwort auf die Web-Seite zu sagen, dass sie entfernt wurde? setzte die Löschmethode in einem separaten Thread vielleicht? Oder vielleicht sogar einen separaten Batch-Prozess verwenden, um das Löschdurchführen?

Ich habe ein ähnliches Problem, wenn sie versuchen, die Anzahl der Dateien in einem Verzeichnis zu zählen - wenn es eine große Anzahl an Dateien enthält

.

Ich habe mich gefragt, ob das alles übertrieben ist ein bisschen? D. h gibt es eine einfachere Methode, damit umzugehen? Jede mögliche Hilfe würde geschätzt.

War es hilfreich?

Lösung

  1. GetFiles ist extrem langsam.
  2. Wenn Sie es von einer Website sind aufgerufen wird, können Sie nur einen neuen Thread werfen, die diesen Trick.
  3. Eine ASP.NET AJAX-Aufruf, dass gibt zurück, ob es noch freie Dateien passen, verwendet werden können, grundlegende Fortschritte Updates zu tun.

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

Verwendung

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

Andere Tipps

Können Sie setzen alle Ihre Dateien im selben Verzeichnis?

Wenn ja, warum Sie nicht nur Directory.Delete(string,bool) auf der subdir rufen Sie löschen möchten?

Wenn Sie haben bereits eine Liste von Dateipfaden Sie wollen, um loszuwerden, könnte man tatsächlich bessere Ergebnisse erzielen, indem sie in ein temporäres Verzeichnis bewegt sie dann zu löschen, anstatt das Löschen jeder Datei manuell.

Cheers, Florian

Machen Sie es in einem separaten Thread oder eine Nachricht an eine Warteschlange (vielleicht MSMQ ), in dem eine andere Anwendung (vielleicht ein Windows-Dienst) zu dieser Warteschlange und führt die Befehle (zB "Delete e: \ dir * .txt" abonniert.) in einen eigenen Prozess ist

Die Nachricht sollte wahrscheinlich auch nur die Ordnernamen. Wenn Sie so etwas wie verwenden NServiceBus und Transaktionswarteschlangen, dann können Sie Ihre Nachricht verfassen und sofort zurück, solange die Nachricht wurde erfolgreich gebucht. Wenn es ein Problem Verarbeiten der Nachricht tatsächlich ist, dann wird es erneut versuchen und schließlich gehen auf Fehlerwarteschlange , dass Sie sehen können und Wartungsarbeiten an.

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

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

sein

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 sich in einem HashMap oder Liste gleichzeitig (der Weg). 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 fehlschlägt, können Sie die Dateiliste aus der serialisierten Form zurück).

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

Starten Sie die Arbeit an einem Worker-Thread und dann wieder Ihre Antwort an den Benutzer.

Ich würde Fahne, eine Anwendungsvariable zu sagen, dass Sie „der großen Löschauftrag“ tun, mehrere Threads zu stoppen läuft die gleiche Arbeit tun. Sie könnten dann eine andere Seite abfragen, die Ihnen einen Fortschritt Aktualisierung der Anzahl der Dateien so viel zu entfernt geben könnte, wenn man wollte?

Nur eine Abfrage, aber warum so viele Dateien?

Sie können hinter einer einfachen Ajax Webmethod in Ihrem aspx-Code erstellen und es mit Javascript aufrufen.

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 mit Browser-Timeouts bekommen.

Wow. Ich denke, dass Sie auf jeden Fall auf dem richtigen Weg sind mit etwas anderen Service oder Organisation kümmert sich um den Lösch haben. Dabei können Sie auch Methoden für den Prozess des Lösch Tracking und das Ergebnis an den Benutzer unter Verwendung asynch Javascript zeigt.

Wie schon andere gesagt haben, dies in einem anderen Prozess setzen ist eine großartige Idee. Sie wollen nicht, IIS Hamster Ressourcen wie lange laufende Aufträge verwenden. Ein weiterer Grund dafür ist die Sicherheit. Vielleicht mögen Sie nicht Ihren Arbeitsprozess, dass die Fähigkeit zum Löschen von Dateien von der Festplatte geben.

Ich weiß, dass es die alten Thread aber neben Jan Jongboom Antwort, die ich ähnliche Lösung vorzuschlagen, die sehr performant ist und universal. Meine Lösung wurde gebaut, schnell mit Unterstützung für lange Dateinamen (> 255 Zeichen) Verzeichnisstruktur in DFS zu entfernen. Der erste Unterschied ist in DLL Einfuhranmeldung.

[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 lange Wege die Pfad Bedürfnisse der Verwendung hergestellt werden wie folgt:

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

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

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

Einige Verbesserungen es im Backend zu beschleunigen:

  • Die Nutzung Directory.EnumerateFiles(..): dies wird durch die Dateien iterieren ohne zu warten, nachdem alle Dateien abgerufen wurden.

  • Mit Parallel.Foreach(..). Diese Dateien gleichzeitig löschen, werden

Es sollte schneller sein, aber anscheinend die HTTP-Anforderung würde immer noch mit der großen Anzahl von Dateien sein Timeout so der Back-End-Prozess sollte in einem separaten Arbeitsthread ausgeführt werden, und das Ergebnis zurück auf dem Web-Client nach der Veredelung mit.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top