using Microsoft.Extensions.Options; using PrintService.Models; namespace PrintService.Services; /// /// Monitors the Captures folder for new files and moves them to queue /// public class FileMonitorService : IDisposable { private readonly AppSettings _settings; private readonly PrintQueueService _queueService; private readonly ILogger _logger; private FileSystemWatcher? _watcher; private readonly Dictionary _pendingFiles = new(); private readonly Timer _debounceTimer; private readonly object _lock = new(); public FileMonitorService( IOptions settings, PrintQueueService queueService, ILogger logger) { _settings = settings.Value; _queueService = queueService; _logger = logger; // Timer for debouncing file system events _debounceTimer = new Timer(ProcessPendingFiles, null, TimeSpan.FromMilliseconds(_settings.DebounceDelayMs), TimeSpan.FromMilliseconds(_settings.DebounceDelayMs)); } /// /// Start monitoring the Captures folder /// public void Start() { if (!Directory.Exists(_settings.CapturesPath)) { Directory.CreateDirectory(_settings.CapturesPath); _logger.LogInformation("Created captures directory: {Path}", _settings.CapturesPath); } _watcher = new FileSystemWatcher(_settings.CapturesPath) { Filter = "*.TXT", NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime, EnableRaisingEvents = true }; _watcher.Created += OnFileCreated; _watcher.Changed += OnFileChanged; _logger.LogInformation("Started monitoring: {Path}", _settings.CapturesPath); } /// /// Stop monitoring /// public void Stop() { if (_watcher != null) { _watcher.EnableRaisingEvents = false; _watcher.Dispose(); _watcher = null; } _logger.LogInformation("Stopped monitoring"); } private void OnFileCreated(object sender, FileSystemEventArgs e) { lock (_lock) { _pendingFiles[e.FullPath] = DateTime.Now; if (_settings.VerboseLogging) { _logger.LogDebug("File created: {Path}", e.FullPath); } } } private void OnFileChanged(object sender, FileSystemEventArgs e) { lock (_lock) { _pendingFiles[e.FullPath] = DateTime.Now; if (_settings.VerboseLogging) { _logger.LogDebug("File changed: {Path}", e.FullPath); } } } /// /// Process files after debounce delay /// private void ProcessPendingFiles(object? state) { List filesToProcess; lock (_lock) { var cutoff = DateTime.Now.AddMilliseconds(-_settings.DebounceDelayMs); filesToProcess = _pendingFiles .Where(kvp => kvp.Value <= cutoff) .Select(kvp => kvp.Key) .ToList(); foreach (var file in filesToProcess) { _pendingFiles.Remove(file); } } foreach (var filePath in filesToProcess) { ProcessFile(filePath); } } /// /// Process a single file: move to queue with GUID name /// private void ProcessFile(string filePath) { try { if (!File.Exists(filePath)) { return; // File may have been moved or deleted } var fileName = Path.GetFileName(filePath); var jobId = Guid.NewGuid(); var queuedFileName = $"{jobId}.txt"; var queuedPath = Path.Combine(_settings.QueuePath, queuedFileName); // Move file to queue with GUID name to prevent collisions File.Move(filePath, queuedPath); _logger.LogInformation("Moved {FileName} to queue as {QueuedFile}", fileName, queuedFileName); // Note: Job will be created when CLI sends command with document type // For now, we just have the file safely in the queue folder } catch (Exception ex) { _logger.LogError(ex, "Failed to process file: {Path}", filePath); } } /// /// Manually process a file (called from IPC when CLI provides metadata) /// public PrintJob CreateJobFromFile(string filePath, string documentType, string? orderNumber) { var fileName = Path.GetFileName(filePath); var jobId = Guid.NewGuid(); var queuedFileName = $"{jobId}.txt"; var queuedPath = Path.Combine(_settings.QueuePath, queuedFileName); // Check if this document type has SkipPostProcessing enabled var config = _settings.DocumentTypes.FirstOrDefault(dt => dt.Name.Equals(documentType, StringComparison.OrdinalIgnoreCase)); if (config?.SkipPostProcessing == true) { // Copy instead of move for testing scenarios File.Copy(filePath, queuedPath, overwrite: true); _logger.LogInformation("Copied {FileName} to queue (test mode - source file preserved)", fileName); } else { // Move file to queue (normal operation) File.Move(filePath, queuedPath, overwrite: true); _logger.LogInformation("Moved {FileName} to queue", fileName); } var job = new PrintJob { Id = jobId, SourceFilePath = filePath, QueuedFilePath = queuedPath, DocumentType = documentType, OrderNumber = orderNumber, OriginalFilename = fileName, Status = PrintJobStatus.Pending, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }; _logger.LogInformation("Created job {JobId} for {FileName} (type: {DocType})", job.Id, fileName, documentType); return job; } public void Dispose() { _debounceTimer?.Dispose(); Stop(); } }