diff --git a/FileManager/AudibleFileStorage.cs b/FileManager/AudibleFileStorage.cs index 448f60dd..d2e5c733 100644 --- a/FileManager/AudibleFileStorage.cs +++ b/FileManager/AudibleFileStorage.cs @@ -41,6 +41,8 @@ namespace FileManager return Directory.CreateDirectory(Configuration.Instance.Books).FullName; } } + + private static BackgroundFileSystem BookDirectoryFiles { get; } = new BackgroundFileSystem(); #endregion #region instance @@ -53,7 +55,7 @@ namespace FileManager { extensions_noDots = Extensions.Select(ext => ext.Trim('.')).ToList(); extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}"); - } + } /// /// Example for full books: @@ -63,16 +65,31 @@ namespace FileManager /// public bool Exists(string productId) => GetPath(productId) != null; - public string GetPath(string productId) + public string GetPath(string productId) { var cachedFile = FilePathCache.GetPath(productId, FileType); if (cachedFile != null) return cachedFile; - var firstOrNull = - Directory - .EnumerateFiles(StorageDirectory, "*.*", SearchOption.AllDirectories) - .FirstOrDefault(s => Regex.IsMatch(s, $@"{productId}.*?\.({extAggr})$", RegexOptions.IgnoreCase)); + string storageDir = StorageDirectory; + string regexPattern = $@"{productId}.*?\.({extAggr})$"; + string firstOrNull; + + if (storageDir == BooksDirectory) + { + //If user changed the BooksDirectory, reinitialize. + if (storageDir != BookDirectoryFiles.RootDirectory) + BookDirectoryFiles.Init(storageDir, "*.*", SearchOption.AllDirectories); + + firstOrNull = BookDirectoryFiles.FindFile(regexPattern, RegexOptions.IgnoreCase); + } + else + { + firstOrNull = + Directory + .EnumerateFiles(storageDir, "*.*", SearchOption.AllDirectories) + .FirstOrDefault(s => Regex.IsMatch(s, regexPattern, RegexOptions.IgnoreCase)); + } if (firstOrNull is null) return null; diff --git a/FileManager/BackgroundFileSystem.cs b/FileManager/BackgroundFileSystem.cs new file mode 100644 index 00000000..98430686 --- /dev/null +++ b/FileManager/BackgroundFileSystem.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace FileManager +{ + class BackgroundFileSystem + { + public string RootDirectory { get; private set; } + public string SearchPattern { get; private set; } + public SearchOption SearchOption { get; private set; } + + private FileSystemWatcher fileSystemWatcher { get; set; } + private BlockingCollection directoryChangesEvents { get; set; } + private Task backgroundScanner { get; set; } + private List fsCache { get; set; } + + public string FindFile(string regexPattern, RegexOptions options) => + fsCache.FirstOrDefault(s => Regex.IsMatch(s, regexPattern, options)); + + + public void Init(string rootDirectory, string searchPattern, SearchOption searchOptions) + { + RootDirectory = rootDirectory; + SearchPattern = searchPattern; + SearchOption = searchOptions; + + //Calling CompleteAdding() will cause background scanner to terminate. + directoryChangesEvents?.CompleteAdding(); + fsCache?.Clear(); + directoryChangesEvents?.Dispose(); + fileSystemWatcher?.Dispose(); + + fsCache = Directory.EnumerateFiles(RootDirectory, SearchPattern, SearchOption).ToList(); + + directoryChangesEvents = new BlockingCollection(); + fileSystemWatcher = new FileSystemWatcher(RootDirectory); + fileSystemWatcher.Created += FileSystemWatcher_Changed; + fileSystemWatcher.Deleted += FileSystemWatcher_Changed; + fileSystemWatcher.Renamed += FileSystemWatcher_Changed; + fileSystemWatcher.Error += FileSystemWatcher_Error; + fileSystemWatcher.IncludeSubdirectories = true; + fileSystemWatcher.EnableRaisingEvents = true; + + //Wait for background scanner to terminate before reinitializing. + backgroundScanner?.Wait(); + backgroundScanner = new Task(BackgroundScanner); + backgroundScanner.Start(); + } + + private void FileSystemWatcher_Error(object sender, ErrorEventArgs e) + { + Init(RootDirectory, SearchPattern, SearchOption); + } + + private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e) + { + directoryChangesEvents.Add(e); + } + + private void UpdateLocalCache(FileSystemEventArgs change) + { + if (change.ChangeType == WatcherChangeTypes.Deleted) + { + RemovePath(change.FullPath); + } + else if (change.ChangeType == WatcherChangeTypes.Created) + { + AddPath(change.FullPath); + } + else if (change.ChangeType == WatcherChangeTypes.Renamed) + { + var renameChange = change as RenamedEventArgs; + + RemovePath(renameChange.OldFullPath); + AddPath(renameChange.FullPath); + } + } + + private void RemovePath(string path) + { + var pathsToRemove = fsCache.Where(p => p.StartsWith(path)).ToArray(); + + foreach (var p in pathsToRemove) + fsCache.Remove(p); + } + + private void AddPath(string path) + { + if (File.GetAttributes(path).HasFlag(FileAttributes.Directory)) + fsCache.AddRange(Directory.EnumerateFiles(path, SearchPattern, SearchOption)); + else + fsCache.Add(path); + } + + private void BackgroundScanner() + { + while (directoryChangesEvents.TryTake(out FileSystemEventArgs change, -1)) + UpdateLocalCache(change); + } + } +}