package de.galabau.dateieingang.sftp; import de.galabau.dateieingang.exception.SftpException; import io.quarkus.logging.Log; import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.sftp.RemoteResourceInfo; import net.schmizz.sshj.sftp.SFTPClient; import net.schmizz.sshj.transport.verification.PromiscuousVerifier; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; /** Kapselt alle SFTP-Operationen: Auflisten, Download und Umbenennen. */ @ApplicationScoped public class SftpService { @Inject SftpConfig config; @PostConstruct void init() { try { Files.createDirectories(Path.of(config.localWorkDir())); } catch (IOException e) { throw new RuntimeException("Lokales Arbeitsverzeichnis konnte nicht erstellt werden: " + config.localWorkDir(), e); } } @FunctionalInterface private interface SftpOperation { T execute(SFTPClient sftp) throws IOException; } /** * Öffnet eine SFTP-Verbindung, führt die Operation aus und trennt danach sauber. * Credentials und Host-Key-Verifikation werden aus {@link SftpConfig} gelesen. */ private T withSftp(SftpOperation operation) throws SftpException { try (SSHClient ssh = new SSHClient()) { configureHostKeyVerification(ssh); ssh.connect(config.host(), config.port()); authenticate(ssh); try (SFTPClient sftp = ssh.newSFTPClient()) { return operation.execute(sftp); } } catch (IOException e) { throw new SftpException("SFTP-Operation fehlgeschlagen auf " + config.host() + ": " + e.getMessage(), e); } } private void configureHostKeyVerification(SSHClient ssh) { if (config.hostKeyFingerprint().isPresent()) { ssh.addHostKeyVerifier(config.hostKeyFingerprint().get()); } else { Log.warn("SFTP Host-Key-Fingerprint nicht konfiguriert — PromiscuousVerifier aktiv (nur Dev!)"); ssh.addHostKeyVerifier(new PromiscuousVerifier()); } } private void authenticate(SSHClient ssh) throws IOException { if (config.privateKeyPath().isPresent()) { ssh.authPublickey(config.username(), config.privateKeyPath().get()); } else { ssh.authPassword(config.username(), config.password()); } } /** * Listet alle {@code *.zip}-Dateien im konfigurierten Remote-Verzeichnis. * * @return Liste der Dateinamen (ohne Pfad), z.B. {@code ["export_2026-04-08.zip"]} * @throws SftpException bei Verbindungs- oder Lesefehler */ public List listZipFiles() throws SftpException { return withSftp(sftp -> sftp.ls(config.remotePath()).stream() .filter(RemoteResourceInfo::isRegularFile) .map(RemoteResourceInfo::getName) .filter(name -> name.endsWith(".zip")) .toList() ); } /** * Lädt eine Datei vom SFTP-Server in das lokale Arbeitsverzeichnis herunter. * * @param filename Dateiname auf dem Remote-Server, z.B. {@code export_2026-04-08.zip} * @return Lokaler Pfad der heruntergeladenen Datei * @throws SftpException bei Verbindungs- oder Downloadfehler */ public Path download(String filename) throws SftpException { Path localFile = Path.of(config.localWorkDir(), filename); withSftp(sftp -> { sftp.get(config.remotePath() + "/" + filename, localFile.toString()); return null; }); return localFile; } /** * Benennt eine Datei auf dem Remote-SFTP-Server um. * Wird nach Erfolg ({@code .processed}) oder Fehler ({@code .error}) aufgerufen. * * @param filename aktueller Dateiname, z.B. {@code export_2026-04-08.zip} * @param newFilename neuer Dateiname, z.B. {@code export_2026-04-08.zip.processed} * @throws SftpException bei Verbindungs- oder Umbenennfehler */ public void renameRemote(String filename, String newFilename) throws SftpException { withSftp(sftp -> { sftp.rename( config.remotePath() + "/" + filename, config.remotePath() + "/" + newFilename ); return null; }); } }