Eliminare un gran numero (> 100K) di file con C #, pur mantenendo le prestazioni in un'applicazione web?

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

  •  25-09-2019
  •  | 
  •  

Domanda

Sto cercando di rimuovere un grande numero di file da una posizione (da grandi intendo più di 100000), per cui l'azione è initated da una pagina web. Ovviamente potrei semplicemente usare

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

Questo metodo è già stato pubblicato un paio di volte: Come eliminare tutti i file e le cartelle in una directory ? e Eliminare i file dalla directory se il nome del file contiene una certa parola

Ma il problema con questo metodo è che se si dispone di dire un centinaio di migliaia di file diventa un problema di prestazioni in quanto ha di generare tutti i filepaths prima di loop attraverso di loro.

In aggiunta a questo, se una pagina web è in attesa di una risposta da un metodo che sta eseguendo questo come si può immaginare sarà un po 'spazzatura!

Un pensiero che ho avuto è stato quello di avvolgere questo in un una chiamata di servizio Web asychrnonous e quando lo completa spara di nuovo una risposta alla pagina web di dire che essi sono stati rimossi? Forse mettere il metodo di eliminazione in un thread separato? O forse anche utilizzare un processo batch separato per eseguire la cancellazione?

Ho un problema simile quando si cerca di contare il numero di file in una directory - se contiene un gran numero di file

.

mi chiedevo se tutto questo è un po 'eccessivo? Cioè c'è un metodo più semplice per affrontare questo? Qualsiasi aiuto sarebbe apprezzato.

È stato utile?

Soluzione

  1. GetFiles è estremamente lento.
  2. Se si invoca da un sito web, si potrebbe semplicemente lanciare un nuovo thread che fa questo trucco.
  3. Un ASP.NET AJAX chiamata che restituisce se ci sono ancora i file corrispondenti, possono essere utilizzati per fare gli aggiornamenti sullo stato di avanzamento di base.

Di seguito l'implementazione di un involucro veloce Win32 per GetFiles, utilizzarlo in combinazione con un nuovo thread e una funzione AJAX come:. GetFilesUnmanaged(@"C:\myDir", "*.txt*).GetEnumerator().MoveNext()

Utilizzo

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

Altri suggerimenti

Puoi mettere tutti i file nella stessa directory?

Se è così, perché non basta chiamare Directory.Delete(string,bool) sul subdir che si desidera eliminare?

Se hai già un elenco di percorsi di file che si desidera per sbarazzarsi di, si potrebbe effettivamente ottenere risultati migliori spostandoli in una directory temporanea poi eliminarli, piuttosto che l'eliminazione di ogni file manualmente.

Saluti, Florian

Do it in un thread separato, o di inviare un messaggio a una coda (forse MSMQ ) dove un'altra applicazione (forse un servizio di Windows) viene sottoscritto quella coda ed esegue i comandi (ad esempio "Delete e: \ dir * .txt".) in un proprio processo

Il messaggio dovrebbe probabilmente solo includere il nome della cartella. Se si usa qualcosa come NServiceBus e transazionali code, allora si può inviare il vostro messaggio e tornare immediatamente a condizione che il messaggio è stato inviato con successo. Se c'è un problema in realtà l'elaborazione del messaggio, allora sarà riprovare e, infine, andare su un errore di coda che è possibile guardare ed eseguire la manutenzione su.

Avere più di 1000 file in una directory è un problema enorme.

Se si è in fase di sviluppo ora, si dovrebbe valutare l'ipotesi in un algo che metterà i file in una cartella casuale (all'interno della cartella root) con una fideiussione del numero di file in quella cartella per essere in 1024 .

Qualcosa di simile

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

Nel fare questo, anche fare in modo che ogni volta che si crea un file, inserirlo in una HashMap o una lista contemporaneamente (il percorso). Periodicamente serializzare questo usando qualcosa come JSON.net al file system (l'amor di integrità, in modo che anche se il vostro servizio riesce, è possibile recuperare la lista dei file dalla forma serializzata).

Quando si desidera pulire i file o una query in mezzo a loro, prima fare una ricerca di questo HashMap o lista e poi agire sul file. Questo è meglio di System.IO.Directory.GetFiles

Avviare il lavoro fuori ad un thread di lavoro e poi tornare la risposta per l'utente.

Mi piacerebbe bandiera di una variabile di applicazione per dire che si sta facendo "il grande lavoro di eliminazione" per interrompere l'esecuzione di più thread che fanno lo stesso lavoro. Si potrebbe quindi sondaggio un'altra pagina, che potrebbe dare un aggiornamento progresso del numero di file rimosso in modo troppo se si voleva?

Basta una query ma perché così tanti file?

Si potrebbe creare un semplice WebMethod ajax nel codice aspx dietro e chiamarlo con javascript.

La scelta migliore (imho) potrebbe essere quella di creare un processo separato per eliminare / contare i file e verificare i progressi dal polling altrimenti si potrebbe ottenere problemi con i timeout del browser.

Wow. Penso che si sono sicuramente sulla strada giusta con avere qualche altro servizio o ente prendersi cura della cancellazione. In tal modo si potrebbe anche fornire metodi per il monitoraggio del processo di cancellazione e mostrando il risultato per l'utente utilizzando asincrono javascript.

Come altri hanno detto di mettere questo in un altro processo è una grande idea. Se non si desidera IIS risorse monopolizzavano l'utilizzo di tali posti di lavoro a lungo in esecuzione. Un altro motivo per farlo è la sicurezza. Non si potrebbe desiderare di dare il vostro processo di lavoro che la capacità di eliminare i file dal disco.

Lo so che è vecchio thread, ma in aggiunta a Jan Jongboom soluzione da me proposta soluzione simile che è abbastanza performante e più universale. La mia soluzione è stata costruita per rimuovere rapidamente la struttura delle directory in DFS con il supporto per i nomi di file lunghi (> 255 caratteri). La prima differenza è nella DLL dichiarazione di importazione.

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

Struttura WIN32_FIND_DATA è anche leggermente diverso:

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

Per poter utilizzare i percorsi lungo i bisogni di percorso per essere preparato come segue:

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

ed ecco il principale metodo:

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

Naturalmente potremmo andare oltre e memorizzare le directory nella lista al di fuori separata di quel metodo ed eliminarli in seguito a un altro metodo che potrebbe assomigliare a questo:

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

Alcuni miglioramenti per accelerarlo nel back-end:

  • Usa Directory.EnumerateFiles(..): questo sarà scorrere i file senza attendere dopo che tutti i file sono stati recuperati.

  • Usa Parallel.Foreach(..):. Questo sarà eliminare i file contemporaneamente

Dovrebbe essere più veloce, ma a quanto pare la richiesta HTTP sarebbe ancora timeout con il gran numero di file in modo che il processo di back-end deve essere eseguito nel thread di lavoro separato e comunicare risultato di nuovo al client Web dopo rifinitura.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top