forked from gper/DirectoryWatcher
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
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(); |
|
} |
|
} |