using System.Collections.Concurrent; using System.Diagnostics; using FastSeek.Core.Index; using FastSeek.Core.Mft; using FastSeek.Core.Search; using FastSeek.Core.Utils; namespace FastSeek.Core; public sealed class StartupMetrics { public bool CacheLoaded { get; init; } public int TotalFiles { get; init; } public double DriveDiscoveryMs { get; init; } public double ScanMs { get; init; } public double IndexMs { get; init; } public double CacheLoadMs { get; init; } public double CacheSaveMs { get; init; } public double TotalStartupMs { get; init; } } public sealed class FastSeekEngine : IDisposable { private readonly ReaderWriterLockSlim _storeLock = new(); private readonly BlockingCollection _queue = new(); private readonly CancellationTokenSource _cts = new(); private readonly object _checkpointLock = new(); private readonly string _cachePath = Path.Combine(Path.GetTempPath(), "fastseek_cache.bin"); private readonly List _checkpoints = new(); private Task? _eventTask; private readonly List _watcherTasks = new(); public IndexStore Store { get; } = new(); public bool CaseSensitive { get; set; } public List ExcludedDirs { get; } = new(); public StartupMetrics? LastStartupMetrics { get; private set; } public string CachePath => _cachePath; public async Task InitializeAsync(Action? log = null) { log?.Invoke("Initializing FastSeek engine..."); var swTotal = Stopwatch.StartNew(); var swDrives = Stopwatch.StartNew(); var drives = Drives.GetNtfsDrives(); swDrives.Stop(); log?.Invoke($"Detected NTFS drives: {string.Join(", ", drives.Select(d => $"{d.Letter}:"))}"); if (drives.Count == 0) throw new InvalidOperationException("No NTFS drives found."); var swCacheLoad = Stopwatch.StartNew(); log?.Invoke($"Loading cache: {_cachePath}"); var loaded = TryLoadCache(_cachePath); swCacheLoad.Stop(); log?.Invoke(loaded ? "Cache hit. Loaded index from cache." : "Cache miss. Running full MFT scan..."); double scanMs = 0; double indexMs = 0; double cacheSaveMs = 0; if (!loaded) { var swScan = Stopwatch.StartNew(); foreach (var d in drives) { log?.Invoke($"Scanning drive {d.Letter}:"); using var reader = MftReader.Open(d); var direct = reader.ScanDirect(TimeSpan.FromSeconds(6), m => log?.Invoke($"[{d.Letter}:direct] {m}")); var scan = direct ?? reader.Scan(m => log?.Invoke($"[{d.Letter}:ioctl] {m}")); var method = direct is null ? "ioctl (fallback)" : "direct"; _storeLock.EnterWriteLock(); try { Store.PopulateFromScan(scan, d.Root); } finally { _storeLock.ExitWriteLock(); } log?.Invoke($"Finished drive {d.Letter}: {scan.Records.Count:N0} records ({method})"); } swScan.Stop(); scanMs = swScan.Elapsed.TotalMilliseconds; var swIndex = Stopwatch.StartNew(); _storeLock.EnterWriteLock(); try { Store.FinalizeStore(); } finally { _storeLock.ExitWriteLock(); } swIndex.Stop(); indexMs = swIndex.Elapsed.TotalMilliseconds; var swCacheSave = Stopwatch.StartNew(); log?.Invoke("Saving cache..."); SaveCache(_cachePath); swCacheSave.Stop(); cacheSaveMs = swCacheSave.Elapsed.TotalMilliseconds; log?.Invoke("Cache saved."); } _storeLock.EnterReadLock(); try { _checkpoints.AddRange(Store.Checkpoints); } finally { _storeLock.ExitReadLock(); } foreach (var d in drives) { var drive = d; _watcherTasks.Add(Task.Run(() => { try { using var watcher = UsnWatcher.New(drive, _queue); watcher.RunShared(_checkpoints, _checkpointLock, _cts.Token); } catch { } }, _cts.Token)); } _eventTask = Task.Run(() => { foreach (var ev in _queue.GetConsumingEnumerable(_cts.Token)) { _storeLock.EnterWriteLock(); try { switch (ev) { case IndexEvent.Created c: Store.Insert(c.Record); break; case IndexEvent.Deleted d: Store.Remove(d.FileRef); break; case IndexEvent.Renamed r: Store.Rename(r.OldRef, r.NewRecord); break; case IndexEvent.Moved m: Store.ApplyMove(m.FileRef, m.NewParentRef, m.Name, m.Kind); break; } } finally { _storeLock.ExitWriteLock(); } } }, _cts.Token); swTotal.Stop(); LastStartupMetrics = new StartupMetrics { CacheLoaded = loaded, TotalFiles = Count(), DriveDiscoveryMs = swDrives.Elapsed.TotalMilliseconds, ScanMs = scanMs, IndexMs = indexMs, CacheLoadMs = swCacheLoad.Elapsed.TotalMilliseconds, CacheSaveMs = cacheSaveMs, TotalStartupMs = swTotal.Elapsed.TotalMilliseconds, }; log?.Invoke($"Startup complete in {LastStartupMetrics.TotalStartupMs:F2} ms"); await Task.CompletedTask; } public List Search(string input, int limit = 50) { if (string.IsNullOrWhiteSpace(input)) return []; var parsed = ParseQuery(input.Trim()); _storeLock.EnterReadLock(); try { if (parsed.ExtFilter is not null) { var dot = "." + parsed.ExtFilter; return Store.Entries.Where(e => e.NameLower.EndsWith(dot, StringComparison.Ordinal)) .Where(e => parsed.Filter == Filter.All || (parsed.Filter == Filter.Dirs && e.IsDir) || (parsed.Filter == Filter.Files && !e.IsDir)) .Select(e => new SearchResult { FullPath = SearchEngine.BuildPath(e.FileRef, Store), Name = e.Name, Rank = 0, IsDir = e.IsDir }) .Where(r => ExcludedDirs.Count == 0 || !ExcludedDirs.Any(ex => r.FullPath.ToLowerInvariant().StartsWith(ex, StringComparison.Ordinal))) .Take(limit) .ToList(); } var overshootLimit = Math.Max(limit * 4, 400); return SearchEngine.Search(Store, parsed.Query, overshootLimit, CaseSensitive, ExcludedDirs) .Where(r => parsed.Filter == Filter.All || (parsed.Filter == Filter.Dirs && r.IsDir) || (parsed.Filter == Filter.Files && !r.IsDir)) .Take(limit) .ToList(); } finally { _storeLock.ExitReadLock(); } } public int Count() { _storeLock.EnterReadLock(); try { return Store.Len(); } finally { _storeLock.ExitReadLock(); } } private bool TryLoadCache(string path) { if (!File.Exists(path)) return false; try { using var fs = File.OpenRead(path); using var br = new BinaryReader(fs, System.Text.Encoding.UTF8, leaveOpen: false); var magic = br.ReadUInt32(); if (magic != 0x4B534146) return false; // FASK var version = br.ReadInt32(); if (version != 1) return false; var driveRoot = br.ReadString(); var checkpointCount = br.ReadInt32(); var checkpoints = new List(checkpointCount); for (var i = 0; i < checkpointCount; i++) { checkpoints.Add(new JournalCheckpoint { NextUsn = br.ReadInt64(), JournalId = br.ReadUInt64(), DriveLetter = br.ReadChar() }); } var entryCount = br.ReadInt32(); var cacheData = new CacheData { DriveRoot = driveRoot, Checkpoints = checkpoints, Entries = new List(entryCount) }; for (var i = 0; i < entryCount; i++) { cacheData.Entries.Add(new CachedEntry { FileRef = br.ReadUInt64(), ParentRef = br.ReadUInt64(), Kind = br.ReadByte() == 1 ? FileKind.Directory : FileKind.File, Name = br.ReadString() }); } var restored = IndexStore.FromCache(cacheData); _storeLock.EnterWriteLock(); try { Store.Entries.Clear(); Store.RefLookup.Clear(); Store.DriveRoot = restored.DriveRoot; Store.Checkpoints = restored.Checkpoints; Store.Entries.AddRange(restored.Entries); Store.RefLookup.AddRange(restored.RefLookup); } finally { _storeLock.ExitWriteLock(); } return true; } catch { return false; } } private void SaveCache(string path) { try { _storeLock.EnterReadLock(); using var fs = File.Create(path); using var bw = new BinaryWriter(fs, System.Text.Encoding.UTF8, leaveOpen: false); bw.Write(0x4B534146u); // FASK bw.Write(1); // version bw.Write(Store.DriveRoot ?? string.Empty); bw.Write(Store.Checkpoints.Count); for (var i = 0; i < Store.Checkpoints.Count; i++) { var c = Store.Checkpoints[i]; bw.Write(c.NextUsn); bw.Write(c.JournalId); bw.Write(c.DriveLetter); } bw.Write(Store.Entries.Count); for (var i = 0; i < Store.Entries.Count; i++) { var e = Store.Entries[i]; bw.Write(e.FileRef); bw.Write(e.ParentRef); bw.Write((byte)(e.IsDir ? 1 : 0)); bw.Write(e.Name); } } catch { } } public void Dispose() { _cts.Cancel(); _queue.CompleteAdding(); lock (_checkpointLock) { _storeLock.EnterWriteLock(); try { Store.Checkpoints = _checkpoints.ToList(); } finally { _storeLock.ExitWriteLock(); } } SaveCache(_cachePath); _storeLock.Dispose(); _queue.Dispose(); _cts.Dispose(); } private enum Filter { All, Dirs, Files } private sealed class ParsedQuery { public required string Query; public Filter Filter; public string? ExtFilter; } private static ParsedQuery ParseQuery(string input) { if (input.StartsWith("ext:", StringComparison.OrdinalIgnoreCase)) return new ParsedQuery { Query = string.Empty, Filter = Filter.Files, ExtFilter = input[4..].ToLowerInvariant() }; if (input.StartsWith("*.", StringComparison.Ordinal)) return new ParsedQuery { Query = string.Empty, Filter = Filter.All, ExtFilter = input[2..].ToLowerInvariant() }; if (input.StartsWith("folder:", StringComparison.OrdinalIgnoreCase)) return new ParsedQuery { Query = input[7..].Trim(), Filter = Filter.Dirs }; if (input.StartsWith("file:", StringComparison.OrdinalIgnoreCase)) return new ParsedQuery { Query = input[5..].Trim(), Filter = Filter.Files }; if (input.StartsWith(':')) return new ParsedQuery { Query = input[1..], Filter = Filter.Dirs }; if (input.StartsWith('!')) return new ParsedQuery { Query = input[1..], Filter = Filter.Files }; return new ParsedQuery { Query = input, Filter = Filter.All }; } }