diff --git a/MailPrint/Controllers/PrintController.cs b/MailPrint/Controllers/PrintController.cs index 96cf208..a97a14b 100644 --- a/MailPrint/Controllers/PrintController.cs +++ b/MailPrint/Controllers/PrintController.cs @@ -23,7 +23,7 @@ public class PrintController : ControllerBase /// /// PDF hochladen und drucken. /// POST /api/print/upload - /// Form-Data: file, printer (opt), paperSource (opt), copies (opt) + /// Form-Data: file, printer (opt, Druckername oder Profilname), paperSource (opt), copies (opt) /// [HttpPost("upload")] [Consumes("multipart/form-data")] @@ -34,11 +34,11 @@ public class PrintController : ControllerBase [FromForm] int copies = 0) { if (file is null || file.Length == 0) - return BadRequest(new { error = "Keine Datei." }); + return BadRequest(new { success = false, error = "Keine Datei." }); var ext = Path.GetExtension(file.FileName).ToLower(); if (!_options.AllowedExtensions.Contains(ext)) - return BadRequest(new { error = $"Dateityp '{ext}' nicht erlaubt." }); + return BadRequest(new { success = false, error = $"Dateityp '{ext}' nicht erlaubt." }); Directory.CreateDirectory(_options.TempDirectory); var tempFile = Path.Combine(_options.TempDirectory, $"{Guid.NewGuid()}{ext}"); @@ -48,7 +48,7 @@ public class PrintController : ControllerBase await using (var fs = System.IO.File.Create(tempFile)) await file.CopyToAsync(fs); - _printService.PrintPdf(new PrintJob + var r = _printService.PrintPdf(new PrintJob { FilePath = tempFile, MailSubject = file.FileName, @@ -58,12 +58,16 @@ public class PrintController : ControllerBase CopiesOverride = copies > 0 ? copies : null }); - return Ok(new { success = true, file = file.FileName, printer, paperSource }); + return Ok(new { success = true, file = file.FileName, profile = r.Profile, printer = r.Printer, paperSource = r.PaperSource, copies = r.Copies }); + } + catch (PrintConfigException ex) + { + return BadRequest(new { success = false, error = ex.Message }); } catch (Exception ex) { _logger.LogError(ex, "WebAPI Druckfehler: {File}", file.FileName); - return StatusCode(500, new { error = ex.Message }); + return StatusCode(500, new { success = false, error = ex.Message }); } } @@ -75,7 +79,7 @@ public class PrintController : ControllerBase public async Task PrintUrl([FromBody] PrintUrlRequest request) { if (!Uri.TryCreate(request.Url, UriKind.Absolute, out var uri)) - return BadRequest(new { error = "Ungültige URL." }); + return BadRequest(new { success = false, error = "Ungültige URL." }); Directory.CreateDirectory(_options.TempDirectory); var tempFile = Path.Combine(_options.TempDirectory, $"{Guid.NewGuid()}.pdf"); @@ -85,7 +89,7 @@ public class PrintController : ControllerBase using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) }; await System.IO.File.WriteAllBytesAsync(tempFile, await http.GetByteArrayAsync(uri)); - _printService.PrintPdf(new PrintJob + var r = _printService.PrintPdf(new PrintJob { FilePath = tempFile, MailSubject = uri.Segments.LastOrDefault() ?? "download.pdf", @@ -95,12 +99,16 @@ public class PrintController : ControllerBase CopiesOverride = request.Copies > 0 ? request.Copies : null }); - return Ok(new { success = true, url = request.Url, printer = request.Printer, paperSource = request.PaperSource }); + return Ok(new { success = true, url = request.Url, profile = r.Profile, printer = r.Printer, paperSource = r.PaperSource, copies = r.Copies }); + } + catch (PrintConfigException ex) + { + return BadRequest(new { success = false, error = ex.Message }); } catch (Exception ex) { _logger.LogError(ex, "WebAPI URL-Druckfehler: {Url}", request.Url); - return StatusCode(500, new { error = ex.Message }); + return StatusCode(500, new { success = false, error = ex.Message }); } } diff --git a/MailPrint/Services/PrintService.cs b/MailPrint/Services/PrintService.cs index e8b3492..8d6a9f4 100644 --- a/MailPrint/Services/PrintService.cs +++ b/MailPrint/Services/PrintService.cs @@ -23,25 +23,31 @@ public class PrintService _options = options.Value; } - public void PrintPdf(PrintJob job) + /// + /// Druckt das PDF. Wirft bei unbekanntem Drucker/Profil, + /// sonst Exception bei jedem Druckfehler (inkl. SumatraPDF-ExitCode ≠ 0). + /// + public PrintResult PrintPdf(PrintJob job) { - // Profil auflösen: direkte Overrides > benanntes Profil > erstes Profil > Defaults - var profile = ResolveProfile(job); - var printerName = !string.IsNullOrEmpty(job.PrinterOverride) ? job.PrinterOverride : profile.PrinterName; - var paperSource = !string.IsNullOrEmpty(job.PaperSourceOverride) ? job.PaperSourceOverride : profile.PaperSource; - var copies = job.CopiesOverride ?? profile.Copies; - - if (string.IsNullOrEmpty(printerName)) - printerName = GetDefaultPrinter(); - - _logger.LogInformation("Drucke '{Subject}' → {Printer} | Fach: {Fach} | {Copies}x", - job.MailSubject, printerName, string.IsNullOrEmpty(paperSource) ? "Standard" : paperSource, copies); - try { - for (int i = 0; i < copies; i++) - PrintOnce(job.FilePath, printerName, paperSource, profile.Duplex); + var r = Resolve(job); + + _logger.LogInformation( + "Drucke '{Subject}' → {Printer} | Profil: {Profile} | Fach: {Fach} | Duplex: {Duplex} | {Copies}x", + job.MailSubject, r.Printer, string.IsNullOrEmpty(r.Profile) ? "-" : r.Profile, + string.IsNullOrEmpty(r.PaperSource) ? "Standard" : r.PaperSource, r.Duplex, r.Copies); + + for (int i = 0; i < r.Copies; i++) + PrintOnce(job.FilePath, r.Printer, r.PaperSource, r.Duplex); + _logger.LogInformation("Druck OK: {File}", job.FilePath); + return r; + } + catch (PrintConfigException ex) + { + _logger.LogWarning("Druck abgelehnt: {Message} ({File})", ex.Message, job.FilePath); + throw; } catch (Exception ex) { @@ -54,15 +60,58 @@ public class PrintService } } - private PrinterProfile ResolveProfile(PrintJob job) + /// + /// Auflösung: Override (Profilname oder Druckername) > Profil aus Postfach/Ordner > erstes Profil > Defaults. + /// Ein Override der einem Profilnamen entspricht wird zum Profil aufgelöst (Drucker, Fach, Duplex, Kopien). + /// + private PrintResult Resolve(PrintJob job) { - if (!string.IsNullOrEmpty(job.PrinterProfileName)) + var printerOverride = job.PrinterOverride?.Trim(); + PrinterProfile? profile = null; + + if (!string.IsNullOrEmpty(printerOverride)) { - var p = _options.PrinterProfiles.FirstOrDefault(x => - x.Name.Equals(job.PrinterProfileName, StringComparison.OrdinalIgnoreCase)); - if (p != null) return p; + profile = FindProfile(printerOverride); + if (profile != null) printerOverride = null; } - return _options.PrinterProfiles.FirstOrDefault() ?? new PrinterProfile(); + + if (profile == null && !string.IsNullOrWhiteSpace(job.PrinterProfileName)) + profile = FindProfile(job.PrinterProfileName) + ?? throw new PrintConfigException( + $"Unbekanntes Drucker-Profil '{job.PrinterProfileName}'. Vorhanden: {ProfileNames()}"); + + profile ??= _options.PrinterProfiles.FirstOrDefault() ?? new PrinterProfile(); + + var requested = !string.IsNullOrEmpty(printerOverride) ? printerOverride : profile.PrinterName; + var printer = string.IsNullOrWhiteSpace(requested) ? GetDefaultPrinter() : MatchInstalledPrinter(requested); + + var paperSource = !string.IsNullOrEmpty(job.PaperSourceOverride) ? job.PaperSourceOverride : profile.PaperSource; + var copies = Math.Max(1, job.CopiesOverride ?? profile.Copies); + + return new PrintResult(profile.Name, printer, paperSource ?? "", profile.Duplex ?? "none", copies); + } + + private PrinterProfile? FindProfile(string name) => + _options.PrinterProfiles.FirstOrDefault(x => + x.Name.Equals(name.Trim(), StringComparison.OrdinalIgnoreCase)); + + private string ProfileNames() => + _options.PrinterProfiles.Count == 0 ? "-" : string.Join(", ", _options.PrinterProfiles.Select(p => p.Name)); + + private string MatchInstalledPrinter(string name) + { + var installed = PrinterSettings.InstalledPrinters.Cast().ToList(); + return installed.FirstOrDefault(p => p.Equals(name, StringComparison.OrdinalIgnoreCase)) + ?? throw new PrintConfigException( + $"Drucker oder Profil '{name}' nicht gefunden. Profile: {ProfileNames()} | Drucker: {string.Join(", ", installed)}"); + } + + private static string GetDefaultPrinter() + { + var name = new PrinterSettings().PrinterName; + if (string.IsNullOrWhiteSpace(name)) + throw new PrintConfigException("Kein Drucker angegeben und kein Standarddrucker vorhanden."); + return name; } private void PrintOnce(string pdfPath, string printerName, string paperSource, string duplex = "none") @@ -78,8 +127,9 @@ public class PrintService // Fallback Shell-Print _logger.LogWarning("Kein SumatraPDF – Shell-Print (kein Papierfach-Support)"); var psi = new ProcessStartInfo { FileName = pdfPath, Verb = "print", UseShellExecute = true, WindowStyle = ProcessWindowStyle.Hidden }; - using var p = Process.Start(psi)!; - p.WaitForExit(30_000); + using var p = Process.Start(psi) ?? throw new InvalidOperationException("Shell-Print nicht startbar/bestätigbar."); + if (!p.WaitForExit(30_000)) throw new TimeoutException("Timeout: Shell-Print"); + if (p.ExitCode != 0) throw new InvalidOperationException($"Shell-Print meldet Fehler (ExitCode {p.ExitCode})."); } private static string BuildPrintSettings(string paperSource, string duplex) @@ -103,7 +153,8 @@ public class PrintService var psi = new ProcessStartInfo(exe, args) { UseShellExecute = false, CreateNoWindow = true }; using var p = Process.Start(psi) ?? throw new InvalidOperationException($"Nicht startbar: {exe}"); if (!p.WaitForExit(300_000)) { p.Kill(); throw new TimeoutException($"Timeout: {exe}"); } - if (p.ExitCode != 0) _logger.LogWarning("ExitCode {Code}: {Exe}", p.ExitCode, exe); + if (p.ExitCode != 0) + throw new InvalidOperationException($"SumatraPDF meldet Fehler (ExitCode {p.ExitCode})."); } public List GetPrinterInfos() @@ -121,7 +172,7 @@ public class PrintService } public IEnumerable GetAvailablePrinters() => GetPrinterInfos().Select(p => p.Name); - private static string GetDefaultPrinter() => new PrinterSettings().PrinterName; + private void TryDelete(string path) { try { if (File.Exists(path)) File.Delete(path); } @@ -130,3 +181,9 @@ public class PrintService } public record PrinterInfo(string Name, List PaperSources); + +/// Tatsächlich verwendete Druckparameter nach Auflösung von Profil/Override. +public record PrintResult(string Profile, string Printer, string PaperSource, string Duplex, int Copies); + +/// Ungültige Anfrage/Konfiguration (unbekannter Drucker oder Profil) – kein Systemfehler. +public class PrintConfigException(string message) : Exception(message);