Przeszukiwanie dysku – zbieranie informacji na bieżąco

W tym artykule przedstawię w jaki sposób przeszukiwać dysk twardy tak, aby użytkownik był informowany o aktualnie sprawdzanym katalogu oraz posiadał listę już odnalezionych plików. Oczywiście warunkiem koniecznym  jest aby interfejs użytkownika był cały czas aktywny, natomiast sam użytkownik mógł w dowolnym momencie przerwać przeszukiwanie. Moduł będzie zintegrowany z interfejsem w WPF.

Założenia ogólne

W kilku punktach co wykorzystamy i co zrobimy:

  • wysyłanie informacji o bieżącym katalogu – event z odpowiednią właściwością
  • wysyłanie informacji o odnalezieniu pliku pasującego do naszych wymagań – jak wyżej
  • wysłanie informacji o zakończeniu przeszukiwania
  • UI aktywny – metoda przeszukująca w innym wątku (wykorzystam starą, poczciwą klasę Thread )
  • możliwość anulowania przeszukiwania w dowolnym momencie – zaimplementujemy metodę Cancel wpływającą na główną pętle
  • filtrowanie plików – stworzymy odpowiedni interfejs, który każdy będzie mógł w łatwy sposób zaimplementować w swojej klasie i bez zmiany kodu samego silnika przeszukującego, zastosować w swoim projekcie
  • algorytm BFSprzeszukiwanie wszerz ma w tym wypadku większy sens, gdyż większe jest prawdopodobieństwo, że interesujący plik jest gdzieś wyżej w drzewie przeszukiwania niż schowany głęboko w katalogach

Oto efekt końcowy całej pracy:

Filtr plików

Moglibyśmy dodać kilka linii kodu w metodzie przeszukującej tak, aby informować o nowym pliku w odpowiednim momencie, ale jest to rozwiązanie niepraktyczne. Wykorzystując interfejs ISearchFilter będziemy mogli tworzyć dowolne filtry i stosować w naszym projekcie bez zmiany samego algorytmu.

public interface ISearchFilter
{
    bool Match(string path);
}

Argument path jest ścieżką do testowanego pliku na dysku. Jeżeli spełnia nasze wymagania zwracamy true, w przeciwnym wypadku false. Proste? Co możemy z tym zrobić? Np. akceptować tylko pliki o określonym rozszerzeniu, rozmiarze, dacie powstania czy modyfikacji albo o nazwie, która zawiera substring wpisany przez użytkownika. Na potrzeby tego artykułu utworzyłem bardzo prosty filtr po rozszerzeniu:

public class SimpleFilter : ISearchFilter
{
    private List<string> _extensions = new List<string>();

    public SimpleFilter()
    {
        _extensions.Add(".txt");
    }

    public bool Match(string path)
    {
        FileInfo fi = new FileInfo(path);

        if (_extensions.Contains(fi.Extension.ToLower()))
            return true;
        else
            return false;
    }
}

Tak więc nasz program, będzie wyszukiwał pliki txt. Możesz oczywiście dodać inne rozszerzenia, albo nawet upublicznić listę _extensions i pozostawić wybór użytkownikowi.

Moduł przeszukiwania

Stworzymy klasę o nazwie SearchEngine, która będzie zawierała dwie publiczne metody: Start oraz Cancel. Zanim do tego przejdziemy, zajmiemy się pozostałymi odpowiedzialnościami, tj. informowaniem użytkownika o progresie. Jak wcześniej wspominałem, skorzystamy ze zdarzeń.

public event EventHandler Completed;

public event EventHandler<PathEventArgs> FileFound;

public event EventHandler<PathEventArgs> DirectoryChanged;

Jeżeli ktoś pierwszy raz widzi takie konstrukcje odsyłam do msdn.

Wyjaśnienie dla EventHandler<T>: zamiast tworzyć delegaty o odpowiednich argumentach, pod T wstawiamy naszą klasę dziedziczącą z EventArgs. W ten sposób możemy podpiąć pod zdarzenie metodę void Method (object sender, T e).

A oto definicja PathEventArgs:

public class PathEventArgs : EventArgs
{
    private readonly string _path;
    public string Path
    {
        get
        {
            return _path;
        }
    }

    public PathEventArgs(string path)
    {
        _path = path;
    }
}

Co jeszcze nam potrzebne?

// Lista katalogów początkowych do przeszukiwania
private string[] _roots;

// True jeśli algorytm ma działać dalej
private bool _keepSearching;

// Nasz filtr
private ISearchFilter _filter;

Możemy teraz zająć się metodą Start.

public void Start(ISearchFilter filter, string[] roots)
{
    _filter = filter;
    _roots = roots;

    _thread = new Thread(new ThreadStart(Work));
    _thread.IsBackground = true;
    _thread.Name = "Search Thread";
    _thread.Start();
}

Tak więc odbieramy filtr oraz korzenie przeszukiwania (katalogi startowe). Następnie inicjujemy nowy wątek, w którym będzie wykonywany algorytm.

Ustawiamy IsBackground na true, gdyż w przeciwnym wypadku wyłączenie naszego programu przyciskiem X w prawym górnym rogu ekranu skończyłoby się zamknięciem samego okienka. Natomiast sam proces trwałby do zakończenia działania wszystkich jego wątków. W naszym wypadku do zakończenia przeszukiwania. Oczywiście nie ma problemu jeżeli wcześniej sami zabijemy wątek. IsBackground = true, gwarantuje nam, że wraz z zakończeniem pracy głównego wątku (wątku interfejsu użytkownika) on sam też zginie.

Teraz najważniejsza część, czyli metoda Work odpowiedzialna za przeszukiwanie oraz informowanie świata o progresie.

private void Work()
{
    _keepSearching = true;

    Queue<string> pathQueue = new Queue<string>();

    foreach (string directory in _roots)
        pathQueue.Enqueue(directory);

    while (_keepSearching && pathQueue.Count != 0)
    {
        // ścieżka do przeszukiwanego katalogu
        string path = pathQueue.Dequeue();

        // Powiadomienie o aktualnie przeszukiwanym katalogu
        if (DirectoryChanged != null)
            DirectoryChanged.BeginInvoke(this,
                new PathEventArgs(path), null, null);

        // UnauthorizedAccessException na niektórych katalogach
        try
        {
            foreach (string file in Directory.GetFiles(path))
	    {
                try
                {
                  if (_filter.Match(file) && FileFound != null)
                      FileFound.BeginInvoke(this,
                         new PathEventArgs(file), null, null);
                }
                catch { }
	    }

            foreach (string directory in Directory.GetDirectories(path))
                pathQueue.Enqueue(directory);
        }
        catch { }
    }

    if (Completed != null)
        Completed.BeginInvoke(this, new EventArgs(), null, null);
}

Po co bloki try-catch? Do niektórych katalogów i plików nie mamy uprawnień.

Po co porównywanie zdarzeń z null? Jeżeli nikt się nie podpiął pod dane zdarzenie, a my je wywołamy, otrzymujemy wyjątek.

Cała pętla while to algorytm BFS + wykorzystanie filtra.

Po co keepSearching w pętli? Jeżeli chcemy przerwać przeszukiwanie ustawiamy tę zmienną na false, co robimy w metodzie Cancel:

public void Cancel()
{
   _keepSearching = false;
}

Pozostało nam jedynie podczepienie klasy SearchEngine pod interfejs użytkownika czego opisywać nie będę. Działającą aplikację w WPF wraz z kompletnym kodem źródłowym można pobrać poniżej. Należy pamiętać, że zdarzenia wywoływane są w innym wątku niż wątek główny, dlatego przy dokonywaniu jakichkolwiek zmian w interfejsie trzeba pamiętać o Dispatcher.

W jaki sposób przeszukać cały dysk twardy? Musimy zebrać literki wszystkich partycji / dysków. Robimy to następująco:

_searchEngine.Start(new SimpleFilter(), Directory.GetLogicalDrives());

Na koniec

Klasę SearchEngine można bez modyfikacji wykorzystać w dowolnym projekcie implementując jedynie odpowiednie filtry.

Czasami wciśnięcie Stop wydaje się nie działać. Zwróć uwagę, że z pętli while wychodzimy dopiero po rozpatrzeniu wszystkich plików w bieżącym folderze. Także może zdarzyć się, że klikniemy Stop w momencie gdy przerabiany będzie katalog z dużą liczbą plików. Wtedy dopiero po pewnym czasie pojawi się informacja: Przeszukiwanie zakończone.

Mam nadzieję, że artykuł ten wyjaśnił przynajmniej samą idee działania przeszukiwania. Wszelkie niejasności wpisujcie w komentarzach.

{filelink=2}

Promuj

Share

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Spam protection by WP Captcha-Free