namespace ZA.CoreService.ESBCertificateManager.Services; public sealed class LocalCertificateDeployer { private static readonly HashSet CertificateExtensions = new(StringComparer.OrdinalIgnoreCase) { ".cer", ".crt", ".pem", ".pfx", ".p12" }; public async Task DeployAsync( string sourceCertificatePath, string configuredTargetDirectory, string targetFileName, bool backupEnabled, string backupDirectoryName, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(sourceCertificatePath)) { throw new ArgumentException( "Es wurde keine Zertifikatsquelle angegeben.", nameof(sourceCertificatePath)); } if (!File.Exists(sourceCertificatePath)) { throw new FileNotFoundException( "Die ausgewählte Zertifikatsdatei wurde nicht gefunden.", sourceCertificatePath); } if (string.IsNullOrWhiteSpace(configuredTargetDirectory)) { throw new ArgumentException( "Der Zertifikats-Zielordner ist nicht konfiguriert.", nameof(configuredTargetDirectory)); } if (string.IsNullOrWhiteSpace(targetFileName)) { throw new ArgumentException( "Der Zertifikats-Zieldateiname ist nicht konfiguriert.", nameof(targetFileName)); } if (Path.GetFileName(targetFileName) != targetFileName) { throw new InvalidOperationException( "TargetFileName darf keinen Verzeichnispfad enthalten."); } string targetDirectory = Path.GetFullPath( Path.IsPathRooted(configuredTargetDirectory) ? configuredTargetDirectory : Path.Combine( AppContext.BaseDirectory, configuredTargetDirectory)); cancellationToken.ThrowIfCancellationRequested(); Directory.CreateDirectory(targetDirectory); string destinationPath = Path.Combine( targetDirectory, targetFileName); string temporaryPath = destinationPath + ".tmp"; try { if (backupEnabled) { BackupExistingCertificates( targetDirectory, backupDirectoryName, cancellationToken); } await using (FileStream source = new( sourceCertificatePath, FileMode.Open, FileAccess.Read, FileShare.Read)) await using (FileStream temporaryTarget = new( temporaryPath, FileMode.Create, FileAccess.Write, FileShare.None)) { await source.CopyToAsync( temporaryTarget, cancellationToken); } cancellationToken.ThrowIfCancellationRequested(); File.Move( temporaryPath, destinationPath, overwrite: true); return destinationPath; } catch { TryDeleteTemporaryFile(temporaryPath); throw; } } private static void BackupExistingCertificates( string targetDirectory, string configuredBackupDirectoryName, CancellationToken cancellationToken) { List filesToBackup = Directory .EnumerateFiles( targetDirectory, "*", SearchOption.TopDirectoryOnly) .Where(path => CertificateExtensions.Contains( Path.GetExtension(path))) .ToList(); if (filesToBackup.Count == 0) { return; } string backupDirectoryName = string.IsNullOrWhiteSpace(configuredBackupDirectoryName) ? "Backup" : configuredBackupDirectoryName.Trim(); if (Path.GetFileName(backupDirectoryName) != backupDirectoryName) { throw new InvalidOperationException( "BackupDirectoryName darf keinen Verzeichnispfad enthalten."); } string backupDirectory = Path.Combine( targetDirectory, backupDirectoryName, DateTime.Now.ToString("yyyyMMdd_HHmmss_fff")); Directory.CreateDirectory(backupDirectory); foreach (string certificateFile in filesToBackup) { cancellationToken.ThrowIfCancellationRequested(); string backupPath = Path.Combine( backupDirectory, Path.GetFileName(certificateFile)); File.Copy( certificateFile, backupPath, overwrite: false); } } private static void TryDeleteTemporaryFile( string temporaryPath) { try { if (File.Exists(temporaryPath)) { File.Delete(temporaryPath); } } catch { // Der ursprüngliche Fehler darf nicht verdeckt werden. // TODO: Aufräumfehler später separat protokollieren. } } }