onsdag den 30. november 2011

Slet tomme mapper

I dag stod jeg over for en sjov udfordring på arbejde. Jeg havde en mappe struktur med en masse filer i, som skulle lægges i en database. Det i sig selv er ikke ret svært, og er ikke relevant i forhold til mit indlæg. Det, der er meget mere interessant, er BAGEFTER - når der skal ryddes op på harddisken. Jeg ønsker at slette alle de tomme mapper, men kun de tomme mapper. Hvis der ligger en eller flere filer i en mappe eller dens under mappe, skal mappen ikke slettes.

Jeg startede min søgen i IOUtils.pas, som kom i Delphi 2009. Helt konkret kiggede jeg på TDirectory, hvori jeg forventede at finde noget der kunne bruges til formålet. Det var nu ikke tilfældet, så måtte jeg skrive en selv.

Det vil dette blogindlæg handle om.

Først skal jeg have sat en test case op (endnu et fint dansk ord ;o)). Jeg har lavet den følgende mappestruktur i roden af mit D-drev:


Mappenavnene fortæller om eventuelt indhold.

Jeg ønsker at bruge den følgende syntaks: TDirectory.DeleteEmptyDirectories('D:\Test');
Primært fordi jeg synes metoden logisk hører hjemme på TDirectory.

Type definitionen kommer til at se sådan ud:

uses
  IOUtils, Types;

type
  TTDirectoryHelper = record helper for TDirectory
  public
    class function DeleteEmptyDirectories
    (
      const aRoot: string; aSearchPattern: string = '*'; 
       aSearchOption: TSearchOption = TSearchOption.soAllDirectories
    ): TStringDynArray; static;
  end;
På TDirectory er der nogle metoder jeg vil benytte mig af :

  • TDirectory.Exists()
    Skal bruges til at tjekke eksistensen af en given sti 
  • TDirectory.GetDirectories()
    Skal bruges til at hente alle under mapper i forhold til en given sti.
  • TDirectory.IsEmpty()
    Skal bruges til at tjekke om en folder er tom eller ej
  • TDirectory.Delete()
    Sletter en mappe, samt eventuelle under mapper
Med disse byggesten i hånden er det "bare" at gå i gang.

Metoden er meget kort fortalt, at løbe en mappe med under mapper igennem. Det kan gøres med TDirectory.GetDirectories(aRoot, aSearchPattern, aSearchOption). For hver af disse mapper skal man tjekke om de eventuelt er tomme, og i så fald slette dem. Hvis man støder på en mappe som ikke er tom, skal denne føjes til en liste så man undgår et uendeligt loop.

Personligt kan jeg nemt miste overblikket over en algoritme, hvis den bliver penslet ud i tekst. Så det vil jeg undlade her, blot vise koden i sin fulde udstrækning, og så i øvrigt henvise til kommentarene i koden.

unit FileAndFolderUtilsU;
interface

uses
  IOUtils, Types;

type
  TTDirectoryHelper = record helper for TDirectory
  public
    class function DeleteEmptyDirectories
    (
      const aRoot: string; aSearchPattern: string = '*'; 
       aSearchOption: TSearchOption = TSearchOption.soAllDirectories
    ): TStringDynArray; static;
  end;
implementation uses Sysutils, Classes; { TTDirectoryHelper }
class function TTDirectoryHelper.DeleteEmptyDirectories
(
  const aRoot: string; aSearchPattern: string = '*'; 
   aSearchOption: TSearchOption = TSearchOption.soAllDirectories
): TStringDynArray; static;
var Directory, Tmp: string; Directories: TStringDynArray; Stop: Boolean; DirBuffer: TStringList; begin // Hvis det ikke en en gyldig sti, så exit if not TDirectory.Exists(aRoot) then exit; DirBuffer := TStringList.Create; { DirDuffer skal bruges til at samle alle de mapper op,
    som enten ikke er tomme, eller det ikke lykkes at slette.
    Dette gøres fordi det skal bruges en "stop kondition",
    så vi undgår uendeligt loop: Hvis de foldere der er tilbage
    i vores struktur alle findes i listen, så er der ikke mere 
    vi kan gøre.
  }
  try
    Directories := TDirectory.GetDirectories(
      aRoot, aSearchPattern, aSearchOption);
    Stop := Length(Directories) = 0;

    if Stop then
      TDirectory.Delete(aRoot);

    {
      Hent en liste over alle subfolders.
      Hvis der ingen er, så slet "dig selv" og forlad funktionen
    }
    while not Stop do
    begin
      for Directory in Directories do
        if TDirectory.Exists(Directory) then
        {
          Det er nødvendigt at tjekke på mappens fortsatte eksistens
          idet funktionen her er rekursiv, og mappen kan være slettet
          i en af de andre rekursioner.
        }
          if TDirectory.IsEmpty(Directory) then
          begin
            TDirectory.Delete(Directory, True); //Slet den tomme folder.
            {
              Hvis den overordnet mappe nu er tom, skal den slettes.
              Det gøres ved at kalde funktionen igen, så herved slettes
              eventuelle mapper som NU er tomme som følge af den nylige
              slettede mappe.
            }
            DeleteEmptyDirectories(aRoot, aSearchPattern, aSearchOption);
            if TDirectory.Exists(Directory) then
              DirBuffer.Add(Directory);
            {
              Hvis folderen fortsat eksisterer efter seltning, er det fordi
              den er låst af en anden process. I det tilfælde tilføjes den 
              bare på DirBuffer.
            }
          end
          else
            DirBuffer.Add(Directory);
           //Hvis mappen ikke er tom skal den også bare tilføjes til DirBuffer.

      Directories := TDirectory.GetDirectories(
        aRoot, aSearchPattern, aSearchOption);
     //Hent en liste over alle de foldere der nu er tilbage

      Stop := True;
      for Directory in Directories do
        Stop := Stop and (DirBuffer.IndexOf(Directory) >= 0);
      //Hvis alle de resterende mapper enten er i brug eller har et indhold, så exit.

      DirBuffer.Clear;
      //Ryd DirBuffer til en eventuel ny omgang.
    end;
  finally
    FreeAndNil(DirBuffer); //Oprydning.
  end;

  //Returner en liste over de mapper der er tilbage - altså dem 
  //der ikke kunne slettes (var i brug) eller ikke var tomme.
  Result := TDirectory.GetDirectories(aRoot, aSearchPattern, aSearchOption);
end;

end.



Til sidst er der blot tilbage at kalde funktionen:


uses
  IoUtils, FileAndFolderUtilsU;

procedure TForm1.FormCreate(Sender: TObject);
begin
  TDirectory.DeleteEmptyDirectories('D:\Test');
end;



 Jens Borrisholt

1 kommentar:

  1. Uhh, den er let: "find ./ -type d -exec rmdir {} \;" :)

    Jeg ved ikke om man har noget ala symlinks/hardlinks på NTFS - men ellers ser jeg også et potentielt uendeligt loop der.

    SvarSlet