Directory Change Monitor
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

125 lines
4.0 KiB

using System.Collections.Concurrent;
using Dir = System.IO.Directory;
namespace BitPlus.IO.Directory.Watcher;
public sealed class DirectoryWatcher : IDisposable
{
private readonly FileSystemWatcher _watcher;
private readonly ConcurrentDictionary<string, DateTimeOffset> _lastEventTimes = new();
private readonly TimeSpan _dedupeWindow = TimeSpan.FromMilliseconds(200);
private bool _disposed;
public string TargetDirectory { get; }
public bool IncludeSubdirectories
{
get => this._watcher.IncludeSubdirectories;
set => this._watcher.IncludeSubdirectories = value;
}
public event EventHandler<DirectoryFileEventArgs>? FileCreated;
public event EventHandler<DirectoryFileEventArgs>? FileChanged;
public event EventHandler<DirectoryFileEventArgs>? FileDeleted;
public event EventHandler<DirectoryFileEventArgs>? FileRenamed;
public event EventHandler<ErrorEventArgs>? WatcherError;
public DirectoryWatcher(string targetDirectory, string filter = "*.*")
{
if (string.IsNullOrWhiteSpace(targetDirectory))
throw new ArgumentException("Target directory cannot be empty.", nameof(targetDirectory));
if (!Dir.Exists(targetDirectory))
throw new DirectoryNotFoundException(targetDirectory);
this.TargetDirectory = targetDirectory;
this._watcher = new FileSystemWatcher(this.TargetDirectory, filter)
{
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime,
IncludeSubdirectories = false,
EnableRaisingEvents = false
};
this._watcher.Created += this.OnCreated;
this._watcher.Changed += this.OnChanged;
this._watcher.Deleted += this.OnDeleted;
this._watcher.Renamed += this.OnRenamed;
this._watcher.Error += this.OnError;
}
public void Start()
{
this.ThrowIfDisposed();
this._watcher.EnableRaisingEvents = true;
}
public void Stop()
{
this.ThrowIfDisposed();
this._watcher.EnableRaisingEvents = false;
}
private void OnCreated(object? sender, FileSystemEventArgs e)
{
if (this.ShouldRaise(e.FullPath))
FileCreated?.Invoke(this, new DirectoryFileEventArgs(e.FullPath, EventMode.Created));
}
private void OnChanged(object? sender, FileSystemEventArgs e)
{
if (this.ShouldRaise(e.FullPath))
FileChanged?.Invoke(this, new DirectoryFileEventArgs(e.FullPath, EventMode.Changed));
}
private void OnDeleted(object? sender, FileSystemEventArgs e)
{
if (this.ShouldRaise(e.FullPath))
FileDeleted?.Invoke(this, new DirectoryFileEventArgs(e.FullPath, EventMode.Deleted));
}
private void OnRenamed(object? sender, RenamedEventArgs e)
{
if (this.ShouldRaise(e.FullPath))
{
var args = new DirectoryFileEventArgs(e.FullPath, EventMode.Renamed, e.OldFullPath);
FileChanged?.Invoke(this, args);
FileRenamed?.Invoke(this, args);
}
}
private void OnError(object? sender, ErrorEventArgs e)
=> WatcherError?.Invoke(this, e);
private bool ShouldRaise(string fullPath)
{
var now = DateTimeOffset.UtcNow;
if (this._lastEventTimes.TryGetValue(fullPath, out var last))
{
if (now - last < this._dedupeWindow)
return false;
}
this._lastEventTimes[fullPath] = now;
return true;
}
private void ThrowIfDisposed()
{
if (this._disposed)
throw new ObjectDisposedException(nameof(DirectoryWatcher));
}
public void Dispose()
{
if (this._disposed) return;
this._disposed = true;
this._watcher.Created -= this.OnCreated;
this._watcher.Changed -= this.OnChanged;
this._watcher.Deleted -= this.OnDeleted;
this._watcher.Renamed -= this.OnRenamed;
this._watcher.Error -= this.OnError;
this._watcher.Dispose();
}
}