diff --git a/BitPlus.IO.Directory.Watcher/BitPlus.IO.Directory.Watcher/DirectoryFileEventArgs.cs b/BitPlus.IO.Directory.Watcher/BitPlus.IO.Directory.Watcher/DirectoryFileEventArgs.cs new file mode 100644 index 0000000..0d97b28 --- /dev/null +++ b/BitPlus.IO.Directory.Watcher/BitPlus.IO.Directory.Watcher/DirectoryFileEventArgs.cs @@ -0,0 +1,15 @@ +namespace BitPlus.IO.Directory.Watcher; + +public sealed class DirectoryFileEventArgs : EventArgs +{ + public string FullPath { get; } + public EventMode Mode { get; } + public string? OldFullPath { get; } + + public DirectoryFileEventArgs(string fullPath, EventMode mode, string? oldFullPath = null) + { + this.FullPath = fullPath; + this.Mode = mode; + this.OldFullPath = oldFullPath; + } +} diff --git a/BitPlus.IO.Directory.Watcher/BitPlus.IO.Directory.Watcher/DirectoryWatcher.cs b/BitPlus.IO.Directory.Watcher/BitPlus.IO.Directory.Watcher/DirectoryWatcher.cs index 1abc406..1311c55 100644 --- a/BitPlus.IO.Directory.Watcher/BitPlus.IO.Directory.Watcher/DirectoryWatcher.cs +++ b/BitPlus.IO.Directory.Watcher/BitPlus.IO.Directory.Watcher/DirectoryWatcher.cs @@ -1,6 +1,125 @@ -namespace BitPlus.IO.Directory.Watcher; +using System.Collections.Concurrent; +using Dir = System.IO.Directory; -public class DirectoryWatcher +namespace BitPlus.IO.Directory.Watcher; + +public sealed class DirectoryWatcher : IDisposable { + private readonly FileSystemWatcher _watcher; + private readonly ConcurrentDictionary _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? FileCreated; + public event EventHandler? FileChanged; + public event EventHandler? FileDeleted; + public event EventHandler? FileRenamed; + public event EventHandler? 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(); + } +} \ No newline at end of file diff --git a/BitPlus.IO.Directory.Watcher/BitPlus.IO.Directory.Watcher/EventMode.cs b/BitPlus.IO.Directory.Watcher/BitPlus.IO.Directory.Watcher/EventMode.cs new file mode 100644 index 0000000..2164ee0 --- /dev/null +++ b/BitPlus.IO.Directory.Watcher/BitPlus.IO.Directory.Watcher/EventMode.cs @@ -0,0 +1,9 @@ +namespace BitPlus.IO.Directory.Watcher; + +public enum EventMode +{ + Created, + Changed, + Deleted, + Renamed +} diff --git a/BitPlus.IO.Directory.Watcher/BitPlus.IO.Directory.Watcher/bin/Debug/net10.0/BitPlus.IO.Directory.Watcher.deps.json b/BitPlus.IO.Directory.Watcher/BitPlus.IO.Directory.Watcher/bin/Debug/net10.0/BitPlus.IO.Directory.Watcher.deps.json new file mode 100644 index 0000000..1f0a904 --- /dev/null +++ b/BitPlus.IO.Directory.Watcher/BitPlus.IO.Directory.Watcher/bin/Debug/net10.0/BitPlus.IO.Directory.Watcher.deps.json @@ -0,0 +1,23 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v10.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v10.0": { + "BitPlus.IO.Directory.Watcher/1.0.0": { + "runtime": { + "BitPlus.IO.Directory.Watcher.dll": {} + } + } + } + }, + "libraries": { + "BitPlus.IO.Directory.Watcher/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/BitPlus.IO.Directory.Watcher/BitPlus.IO.Directory.Watcher/bin/Debug/net10.0/BitPlus.IO.Directory.Watcher.dll b/BitPlus.IO.Directory.Watcher/BitPlus.IO.Directory.Watcher/bin/Debug/net10.0/BitPlus.IO.Directory.Watcher.dll new file mode 100644 index 0000000..0697d18 Binary files /dev/null and b/BitPlus.IO.Directory.Watcher/BitPlus.IO.Directory.Watcher/bin/Debug/net10.0/BitPlus.IO.Directory.Watcher.dll differ diff --git a/BitPlus.IO.Directory.Watcher/BitPlus.IO.Directory.Watcher/bin/Debug/net10.0/BitPlus.IO.Directory.Watcher.pdb b/BitPlus.IO.Directory.Watcher/BitPlus.IO.Directory.Watcher/bin/Debug/net10.0/BitPlus.IO.Directory.Watcher.pdb new file mode 100644 index 0000000..adbe4b8 Binary files /dev/null and b/BitPlus.IO.Directory.Watcher/BitPlus.IO.Directory.Watcher/bin/Debug/net10.0/BitPlus.IO.Directory.Watcher.pdb differ diff --git a/BitPlus.IO.Directory.Watcher/Sampl/Program.cs b/BitPlus.IO.Directory.Watcher/Sampl/Program.cs index 581bddb..c559c44 100644 --- a/BitPlus.IO.Directory.Watcher/Sampl/Program.cs +++ b/BitPlus.IO.Directory.Watcher/Sampl/Program.cs @@ -1,7 +1,34 @@ -internal class Program +using BitPlus.IO.Directory.Watcher; + +internal class Program { private static void Main(string[] args) { - Console.WriteLine("Hello, World!"); + var path = args.Length > 0 ? args[0] : null; + + while (string.IsNullOrWhiteSpace(path) || !Directory.Exists(path)) + { + Console.Write("감시할 폴더 경로를 입력하세요(Enter the folder path to monitor.): "); + path = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(path)) + Console.WriteLine("경로가 비어 있습니다.(The path is empty.)"); + else if (!Directory.Exists(path)) + Console.WriteLine("폴더가 존재하지 않습니다.(The folder does not exist.)"); + } + + using var watcher = new DirectoryWatcher(path); + watcher.FileCreated += (_, e) => Console.WriteLine($"[CREATE] {e.FullPath}"); + watcher.FileChanged += (_, e) => Console.WriteLine($"[CHANGE] {e.FullPath}"); + watcher.FileDeleted += (_, e) => Console.WriteLine($"[DELETE] {e.FullPath}"); + watcher.FileRenamed += (_, e) => Console.WriteLine($"[RENAME] {e.OldFullPath} -> {e.FullPath}"); + watcher.WatcherError += (_, e) => Console.WriteLine($"[ERROR] {e.GetException().Message}"); + + watcher.Start(); + Console.WriteLine($"감시 시작(Start the surveillance): {path}"); + Console.WriteLine("종료하려면 Enter를 누르세요...(Press Enter to exit)"); + + Console.ReadLine(); + watcher.Stop(); } } \ No newline at end of file diff --git a/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/BitPlus.IO.Directory.Watcher.dll b/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/BitPlus.IO.Directory.Watcher.dll new file mode 100644 index 0000000..0697d18 Binary files /dev/null and b/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/BitPlus.IO.Directory.Watcher.dll differ diff --git a/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/BitPlus.IO.Directory.Watcher.pdb b/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/BitPlus.IO.Directory.Watcher.pdb new file mode 100644 index 0000000..adbe4b8 Binary files /dev/null and b/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/BitPlus.IO.Directory.Watcher.pdb differ diff --git a/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/Sample.deps.json b/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/Sample.deps.json new file mode 100644 index 0000000..a8f7c64 --- /dev/null +++ b/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/Sample.deps.json @@ -0,0 +1,39 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v10.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v10.0": { + "Sample/1.0.0": { + "dependencies": { + "BitPlus.IO.Directory.Watcher": "1.0.0" + }, + "runtime": { + "Sample.dll": {} + } + }, + "BitPlus.IO.Directory.Watcher/1.0.0": { + "runtime": { + "BitPlus.IO.Directory.Watcher.dll": { + "assemblyVersion": "1.0.0.0", + "fileVersion": "1.0.0.0" + } + } + } + } + }, + "libraries": { + "Sample/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "BitPlus.IO.Directory.Watcher/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/Sample.dll b/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/Sample.dll new file mode 100644 index 0000000..b05a222 Binary files /dev/null and b/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/Sample.dll differ diff --git a/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/Sample.exe b/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/Sample.exe new file mode 100644 index 0000000..752aef2 Binary files /dev/null and b/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/Sample.exe differ diff --git a/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/Sample.pdb b/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/Sample.pdb new file mode 100644 index 0000000..cc5c410 Binary files /dev/null and b/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/Sample.pdb differ diff --git a/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/Sample.runtimeconfig.json b/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/Sample.runtimeconfig.json new file mode 100644 index 0000000..01e4519 --- /dev/null +++ b/BitPlus.IO.Directory.Watcher/Sampl/bin/Debug/net10.0/Sample.runtimeconfig.json @@ -0,0 +1,12 @@ +{ + "runtimeOptions": { + "tfm": "net10.0", + "framework": { + "name": "Microsoft.NETCore.App", + "version": "10.0.0" + }, + "configProperties": { + "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index 496f628..6085309 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ # DirectoryWatcher 디렉터리를 감시 및 변경에 따른 이벤트 처리기 -Event handlers for monitoring and changing directories \ No newline at end of file +Event handlers for monitoring and changing directories + +![sample](./sample.png) \ No newline at end of file diff --git a/sample.png b/sample.png new file mode 100644 index 0000000..fec2651 Binary files /dev/null and b/sample.png differ