Vraag Een groot aantal (> 100.000) bestanden verwijderen met c # terwijl de prestaties in een webtoepassing worden gehandhaafd?


Ik probeer een te verwijderen groot aantal bestanden van een locatie (met grote bedoel ik meer dan 100000), waarbij de actie wordt geïnitieerd vanaf een webpagina. Natuurlijk kan ik het gewoon gebruiken

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

Deze methode is al een paar keer gepost: Hoe verwijder ik alle bestanden en mappen in een map? en Verwijder bestanden uit de map als bestandsnaam een ​​bepaald woord bevat

Maar het probleem met deze methode is dat als je honderdduizend bestanden hebt gezegd, het een prestatieprobleem wordt, omdat het eerst alle bestandsnamen moet genereren voordat het doorloopt.

Hieraan toegevoegd als een webpagina wacht op een antwoord van een methode die dit uitvoert, zoals je je kunt voorstellen, zal het een beetje onzin lijken!

Eén gedachte die ik had was om dit in een asychrone webserviceaanroep in te pakken en wanneer deze klaar is, wordt een reactie op de webpagina afgevuurd om te zeggen dat ze zijn verwijderd? Misschien zet de verwijderingsmethode in een aparte thread? Of misschien zelfs een afzonderlijk batchproces gebruiken om de verwijdering uit te voeren?

Ik heb een soortgelijk probleem bij het tellen van het aantal bestanden in een map - als het een groot aantal bestanden bevat.

Ik vroeg me af of dit allemaal een beetje overkill is? D.w.z. is er een eenvoudigere methode om hiermee om te gaan? Alle hulp wordt op prijs gesteld.


15
2018-02-02 16:43


oorsprong


antwoorden:


  1. GetFiles is extreem langzaam.
  2. Als je het oproept vanaf een website, zou je gewoon een nieuwe thread kunnen gooien die deze truc doet.
  3. Een AJAX-aanroep van ASP.NET die retourneert of er nog steeds overeenkomende bestanden zijn, kan worden gebruikt om standaard voortgangsupdates uit te voeren.

Hieronder een implementatie van een snelle Win32-verpakking voor GetFiles, gebruik het in combinatie met een nieuwe Thread en een AJAX-functie zoals: GetFilesUnmanaged(@"C:\myDir", "*.txt*).GetEnumerator().MoveNext().

Gebruik

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

10
2018-02-02 16:47



Kun je al je bestanden in dezelfde map plaatsen?

Als dat zo is, waarom bel je dan niet gewoon Directory.Delete(string,bool) in het subdir dat u wilt verwijderen?

Als je al een lijst hebt met bestandspaden die je wilt verwijderen, krijg je misschien betere resultaten door ze naar een tijdelijke map te verplaatsen en ze vervolgens te verwijderen in plaats van elk bestand handmatig te verwijderen.

cheers, Florian


3
2018-02-02 17:28



Doe het in een aparte thread, of plaats een bericht in een wachtrij (misschien MSMQ?) waar een andere toepassing (misschien een Windows-service) is geabonneerd op die wachtrij en voert de opdrachten (d.w.z. "Verwijder e: \ dir * .txt") uit in zijn eigen proces.

Het bericht zou waarschijnlijk alleen de mapnaam moeten bevatten. Als je zoiets gebruikt NServiceBus en transactionele wachtrijen, dan kunt u uw bericht plaatsen en onmiddellijk terugkeren, mits het bericht met succes is gepost. Als er een probleem is met het verwerken van het bericht, probeert het opnieuw en gaat het uiteindelijk door fout wachtrij waar je onderhoud aan kunt kijken en uitvoeren.


1
2018-02-02 16:46



Het hebben van meer dan 1000 bestandenin een directory is een enorm probleem.

Als je nu in de ontwikkelingsfase bent, zou je moeten overwegen om een algo welke de bestanden in een willekeurige map plaatst (in je root-map) met een zekerheid van het aantal bestanden in die map onder 1024.

Zoiets als

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

Zorg er daarbij voor dat elke keer dat u een bestand aanmaakt, het tegelijkertijd toevoegt aan een HashMap of lijst (het pad). Periodiek serialiseren met behulp van iets als JSON.net naar het bestandssysteem (integriteitsbelang, zodat zelfs als je service faalt, je de bestandslijst terug kunt krijgen van het geserialiseerde formulier).

Wanneer u de bestanden of query tussen hen wilt opruimen, doe eerst een opzoeking van deze HashMap of lijst en dan handelen in het bestand. Dit is beter dan System.IO.Directory.GetFiles


1
2018-02-03 06:21



Start de work-out op een worker-thread en beantwoord vervolgens uw reactie aan de gebruiker.

Ik zou een toepassingsvariabele markeren om te zeggen dat je "de grote verwijderopdracht" doet om te voorkomen dat meerdere threads hetzelfde werk doen. Je zou dan nog een pagina kunnen pollen die je een voortgangsupdate zou kunnen geven van het aantal verwijderde bestanden tot nu toe, als je dat wilde?

Gewoon een vraag, maar waarom zoveel bestanden?


0
2018-02-02 16:47



Je zou een eenvoudige ajax-webmethode in je aspx-code achter kunnen maken en deze met javascript kunnen aanroepen.


0
2018-02-02 16:48



De beste keuze (imho) zou zijn om een ​​apart proces te maken om de bestanden te verwijderen / te tellen en de voortgang te controleren door polling, anders krijg je mogelijk problemen met de time-outs van de browser.


0
2018-02-02 16:48