Profilnamen aufloesen, unbekannte Drucker ablehnen (400), SumatraPDF-ExitCode als Fehler, success:false in Fehlerantworten

This commit is contained in:
dimedtec 2026-10-02 15:39:00 +02:00
parent 93f5c72ef2
commit b88752186d
2 changed files with 100 additions and 35 deletions

View file

@ -23,7 +23,7 @@ public class PrintController : ControllerBase
/// <summary> /// <summary>
/// PDF hochladen und drucken. /// PDF hochladen und drucken.
/// POST /api/print/upload /// 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)
/// </summary> /// </summary>
[HttpPost("upload")] [HttpPost("upload")]
[Consumes("multipart/form-data")] [Consumes("multipart/form-data")]
@ -34,11 +34,11 @@ public class PrintController : ControllerBase
[FromForm] int copies = 0) [FromForm] int copies = 0)
{ {
if (file is null || file.Length == 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(); var ext = Path.GetExtension(file.FileName).ToLower();
if (!_options.AllowedExtensions.Contains(ext)) 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); Directory.CreateDirectory(_options.TempDirectory);
var tempFile = Path.Combine(_options.TempDirectory, $"{Guid.NewGuid()}{ext}"); 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 using (var fs = System.IO.File.Create(tempFile))
await file.CopyToAsync(fs); await file.CopyToAsync(fs);
_printService.PrintPdf(new PrintJob var r = _printService.PrintPdf(new PrintJob
{ {
FilePath = tempFile, FilePath = tempFile,
MailSubject = file.FileName, MailSubject = file.FileName,
@ -58,12 +58,16 @@ public class PrintController : ControllerBase
CopiesOverride = copies > 0 ? copies : null 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) catch (Exception ex)
{ {
_logger.LogError(ex, "WebAPI Druckfehler: {File}", file.FileName); _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<IActionResult> PrintUrl([FromBody] PrintUrlRequest request) public async Task<IActionResult> PrintUrl([FromBody] PrintUrlRequest request)
{ {
if (!Uri.TryCreate(request.Url, UriKind.Absolute, out var uri)) 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); Directory.CreateDirectory(_options.TempDirectory);
var tempFile = Path.Combine(_options.TempDirectory, $"{Guid.NewGuid()}.pdf"); 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) }; using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
await System.IO.File.WriteAllBytesAsync(tempFile, await http.GetByteArrayAsync(uri)); await System.IO.File.WriteAllBytesAsync(tempFile, await http.GetByteArrayAsync(uri));
_printService.PrintPdf(new PrintJob var r = _printService.PrintPdf(new PrintJob
{ {
FilePath = tempFile, FilePath = tempFile,
MailSubject = uri.Segments.LastOrDefault() ?? "download.pdf", MailSubject = uri.Segments.LastOrDefault() ?? "download.pdf",
@ -95,12 +99,16 @@ public class PrintController : ControllerBase
CopiesOverride = request.Copies > 0 ? request.Copies : null 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) catch (Exception ex)
{ {
_logger.LogError(ex, "WebAPI URL-Druckfehler: {Url}", request.Url); _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 });
} }
} }

View file

@ -23,25 +23,31 @@ public class PrintService
_options = options.Value; _options = options.Value;
} }
public void PrintPdf(PrintJob job) /// <summary>
/// Druckt das PDF. Wirft <see cref="PrintConfigException"/> bei unbekanntem Drucker/Profil,
/// sonst Exception bei jedem Druckfehler (inkl. SumatraPDF-ExitCode ≠ 0).
/// </summary>
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 try
{ {
for (int i = 0; i < copies; i++) var r = Resolve(job);
PrintOnce(job.FilePath, printerName, paperSource, profile.Duplex);
_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); _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) catch (Exception ex)
{ {
@ -54,15 +60,58 @@ public class PrintService
} }
} }
private PrinterProfile ResolveProfile(PrintJob job) /// <summary>
/// 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).
/// </summary>
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 => profile = FindProfile(printerOverride);
x.Name.Equals(job.PrinterProfileName, StringComparison.OrdinalIgnoreCase)); if (profile != null) printerOverride = null;
if (p != null) return p;
} }
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<string>().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") private void PrintOnce(string pdfPath, string printerName, string paperSource, string duplex = "none")
@ -78,8 +127,9 @@ public class PrintService
// Fallback Shell-Print // Fallback Shell-Print
_logger.LogWarning("Kein SumatraPDF – Shell-Print (kein Papierfach-Support)"); _logger.LogWarning("Kein SumatraPDF – Shell-Print (kein Papierfach-Support)");
var psi = new ProcessStartInfo { FileName = pdfPath, Verb = "print", UseShellExecute = true, WindowStyle = ProcessWindowStyle.Hidden }; var psi = new ProcessStartInfo { FileName = pdfPath, Verb = "print", UseShellExecute = true, WindowStyle = ProcessWindowStyle.Hidden };
using var p = Process.Start(psi)!; using var p = Process.Start(psi) ?? throw new InvalidOperationException("Shell-Print nicht startbar/bestätigbar.");
p.WaitForExit(30_000); 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) 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 }; var psi = new ProcessStartInfo(exe, args) { UseShellExecute = false, CreateNoWindow = true };
using var p = Process.Start(psi) ?? throw new InvalidOperationException($"Nicht startbar: {exe}"); 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.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<PrinterInfo> GetPrinterInfos() public List<PrinterInfo> GetPrinterInfos()
@ -121,7 +172,7 @@ public class PrintService
} }
public IEnumerable<string> GetAvailablePrinters() => GetPrinterInfos().Select(p => p.Name); public IEnumerable<string> GetAvailablePrinters() => GetPrinterInfos().Select(p => p.Name);
private static string GetDefaultPrinter() => new PrinterSettings().PrinterName;
private void TryDelete(string path) private void TryDelete(string path)
{ {
try { if (File.Exists(path)) File.Delete(path); } try { if (File.Exists(path)) File.Delete(path); }
@ -130,3 +181,9 @@ public class PrintService
} }
public record PrinterInfo(string Name, List<string> PaperSources); public record PrinterInfo(string Name, List<string> PaperSources);
/// <summary>Tatsächlich verwendete Druckparameter nach Auflösung von Profil/Override.</summary>
public record PrintResult(string Profile, string Printer, string PaperSource, string Duplex, int Copies);
/// <summary>Ungültige Anfrage/Konfiguration (unbekannter Drucker oder Profil) – kein Systemfehler.</summary>
public class PrintConfigException(string message) : Exception(message);