173 lines
7.5 KiB
C#
173 lines
7.5 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace FileInterface.Scheduler
|
|
{
|
|
public sealed class FileSystemWatcherManager : IDisposable
|
|
{
|
|
private sealed class ActiveWatcher : IDisposable
|
|
{
|
|
public WatcherJobConfig Job;
|
|
public FileSystemWatcher Watcher;
|
|
public void Dispose() { if (Watcher != null) { Watcher.EnableRaisingEvents = false; Watcher.Dispose(); } }
|
|
}
|
|
|
|
private readonly object _sync = new object();
|
|
private readonly WatcherJobRepository _repository;
|
|
private readonly WatcherJobExecutor _executor;
|
|
private readonly JobLog _log;
|
|
private readonly Dictionary<string, ActiveWatcher> _watchers = new Dictionary<string, ActiveWatcher>(StringComparer.OrdinalIgnoreCase);
|
|
private readonly ConcurrentDictionary<string, DateTime> _lastEvents = new ConcurrentDictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);
|
|
private readonly ConcurrentDictionary<string, byte> _runningFiles = new ConcurrentDictionary<string, byte>(StringComparer.OrdinalIgnoreCase);
|
|
private bool _disposed;
|
|
|
|
public FileSystemWatcherManager(WatcherJobRepository repository, WatcherJobExecutor executor, JobLog log)
|
|
{
|
|
_repository = repository;
|
|
_executor = executor;
|
|
_log = log;
|
|
}
|
|
|
|
public void Start() { Reload(); }
|
|
|
|
public void Reload()
|
|
{
|
|
lock (_sync)
|
|
{
|
|
ThrowIfDisposed();
|
|
foreach (ActiveWatcher old in _watchers.Values) old.Dispose();
|
|
_watchers.Clear();
|
|
|
|
foreach (WatcherJobConfig job in _repository.GetAll())
|
|
{
|
|
if (!job.Enabled) continue;
|
|
try
|
|
{
|
|
if (!Directory.Exists(job.SourceDirectory))
|
|
{
|
|
_log.Error(job, "Watcher source directory does not exist: " + job.SourceDirectory);
|
|
continue;
|
|
}
|
|
|
|
var fsw = new FileSystemWatcher(job.SourceDirectory, String.IsNullOrWhiteSpace(job.SearchPattern) ? "*.*" : job.SearchPattern)
|
|
{
|
|
IncludeSubdirectories = job.IncludeSubdirectories,
|
|
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.CreationTime,
|
|
EnableRaisingEvents = false
|
|
};
|
|
|
|
WatcherJobConfig captured = job;
|
|
if (job.WatchCreated) fsw.Created += (s, e) => OnFileEvent(captured, e.FullPath, "Created");
|
|
if (job.WatchChanged) fsw.Changed += (s, e) => OnFileEvent(captured, e.FullPath, "Changed");
|
|
if (job.WatchRenamed) fsw.Renamed += (s, e) => OnFileEvent(captured, e.FullPath, "Renamed");
|
|
fsw.Error += (s, e) => _log.Error(captured, "FileSystemWatcher error: " + (e.GetException() == null ? "unknown" : e.GetException().Message));
|
|
|
|
var active = new ActiveWatcher { Job = job, Watcher = fsw };
|
|
_watchers[job.Id] = active;
|
|
fsw.EnableRaisingEvents = true;
|
|
_log.Info(job, "FileSystemWatcher active: " + job.SourceDirectory + "\\" + job.SearchPattern);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.Error(job, "Cannot start FileSystemWatcher: " + ex.Message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public IList<string> GetActiveWatcherIds()
|
|
{
|
|
lock (_sync) return _watchers.Keys.ToList();
|
|
}
|
|
|
|
public bool ProcessExistingNow(string watcherId)
|
|
{
|
|
WatcherJobConfig job = _repository.GetById(watcherId);
|
|
if (job == null) return false;
|
|
Task.Run(() => ProcessExisting(job));
|
|
return true;
|
|
}
|
|
|
|
private void ProcessExisting(WatcherJobConfig job)
|
|
{
|
|
if (!Directory.Exists(job.SourceDirectory))
|
|
{
|
|
_log.Error(job, "Source directory does not exist: " + job.SourceDirectory);
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
SearchOption option = job.IncludeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
|
string[] files = Directory.GetFiles(job.SourceDirectory, String.IsNullOrWhiteSpace(job.SearchPattern) ? "*.*" : job.SearchPattern, option)
|
|
.OrderBy(f => f, StringComparer.OrdinalIgnoreCase).ToArray();
|
|
_log.Info(job, "Manual watcher scan found " + files.Length + " file(s).");
|
|
foreach (string file in files) QueueFile(job, file, "ManualScan", false);
|
|
}
|
|
catch (Exception ex) { _log.Error(job, "Manual watcher scan failed: " + ex.Message); }
|
|
}
|
|
|
|
private void OnFileEvent(WatcherJobConfig job, string fullPath, string reason)
|
|
{
|
|
if (!job.Enabled || String.IsNullOrWhiteSpace(fullPath)) return;
|
|
if (IsMoveTarget(job, fullPath)) return;
|
|
QueueFile(job, fullPath, reason, true);
|
|
}
|
|
|
|
private void QueueFile(WatcherJobConfig job, string fullPath, string reason, bool debounce)
|
|
{
|
|
string key = job.Id + "|" + fullPath;
|
|
DateTime now = DateTime.UtcNow;
|
|
if (debounce)
|
|
{
|
|
DateTime previous;
|
|
if (_lastEvents.TryGetValue(key, out previous) && (now - previous).TotalMilliseconds < Math.Max(0, job.DebounceMilliseconds))
|
|
return;
|
|
_lastEvents[key] = now;
|
|
}
|
|
|
|
byte ignored;
|
|
if (!_runningFiles.TryAdd(key, 0)) return;
|
|
Task.Run(() =>
|
|
{
|
|
try
|
|
{
|
|
if (!File.Exists(fullPath)) return;
|
|
_log.Info(job, "File event queued: " + reason + " - " + fullPath);
|
|
_executor.ExecuteFile(job, fullPath, reason);
|
|
}
|
|
catch (Exception ex) { _log.Error(job, "Watcher processing failed for " + fullPath + ": " + ex); }
|
|
finally { _runningFiles.TryRemove(key, out ignored); }
|
|
});
|
|
}
|
|
|
|
private static bool IsMoveTarget(WatcherJobConfig job, string fullPath)
|
|
{
|
|
if (!job.MoveFileAfterSuccess || String.IsNullOrWhiteSpace(job.MoveTargetDirectory)) return false;
|
|
try
|
|
{
|
|
string file = Path.GetFullPath(fullPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
|
string target = Path.GetFullPath(job.MoveTargetDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
|
return file.StartsWith(target, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
private void ThrowIfDisposed() { if (_disposed) throw new ObjectDisposedException(GetType().FullName); }
|
|
|
|
public void Dispose()
|
|
{
|
|
lock (_sync)
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
foreach (ActiveWatcher w in _watchers.Values) w.Dispose();
|
|
_watchers.Clear();
|
|
}
|
|
}
|
|
}
|
|
}
|