diff --git a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/pathcheck/FilesystemPathCheckAdapter.java b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/pathcheck/FilesystemPathCheckAdapter.java index 6a8b5c2..0f150d6 100644 --- a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/pathcheck/FilesystemPathCheckAdapter.java +++ b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/pathcheck/FilesystemPathCheckAdapter.java @@ -1,5 +1,6 @@ package de.gecheckt.pdf.umbenenner.adapter.out.pathcheck; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; @@ -14,8 +15,8 @@ import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.PathCheck * Dateisystem-basierte Implementierung von {@link PathCheckPort}. *
* Prüft die Zugänglichkeit von Pfaden für Quellordner, Zielordner, SQLite-Datei - * und Prompt-Datei ausschließlich lesend. Es werden keinerlei Dateien, Ordner oder - * andere Ressourcen angelegt, verändert oder gelöscht. + * und Prompt-Datei. Schreibbarkeitstests erfolgen über kurzlebige Probe-Dateien, + * die unmittelbar nach dem Schreibversuch wieder gelöscht werden. * *
@@ -27,8 +28,9 @@ import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.PathCheck * Laufwerksbuchstaben und UNC-Pfaden statt. *
* Die Implementierung nutzt {@link Paths#get(String)}, {@link Files#exists(Path, java.nio.file.LinkOption...)}, - * {@link Files#isReadable(Path)} und {@link Files#isWritable(Path)}, die unter Windows - * gemappte Laufwerke korrekt respektieren. + * {@link Files#isReadable(Path)} sowie einen Probe-Datei-Ansatz ({@link Files#createTempFile}) + * für Schreibbarkeitstests. Dieser Ansatz ist auf gemappten Windows-Netzlaufwerken + * zuverlässiger als {@link Files#isWritable(Path)}, das dort bekannte False Negatives liefert. * *
@@ -108,7 +110,7 @@ public class FilesystemPathCheckAdapter implements PathCheckPort { return false; } if (Files.exists(resolved)) { - boolean writable = Files.isDirectory(resolved) && Files.isWritable(resolved); + boolean writable = Files.isDirectory(resolved) && probeDirectoryWritable(resolved); if (writable) { LOG.debug("Ordner vorhanden und schreibbar: {}", resolved); } else { @@ -118,7 +120,7 @@ public class FilesystemPathCheckAdapter implements PathCheckPort { } // Ordner existiert nicht — prüfen ob Elternpfad schreibbar ist Path parent = resolved.getParent(); - if (parent != null && Files.exists(parent) && Files.isDirectory(parent) && Files.isWritable(parent)) { + if (parent != null && Files.exists(parent) && Files.isDirectory(parent) && probeDirectoryWritable(parent)) { LOG.debug("Ordner nicht vorhanden, aber anlegbar (Elternpfad schreibbar): {}", resolved); return true; } @@ -179,9 +181,11 @@ public class FilesystemPathCheckAdapter implements PathCheckPort { return false; } if (Files.exists(resolved)) { + Path parentDir = resolved.getParent(); boolean usable = Files.isRegularFile(resolved) && Files.isReadable(resolved) - && Files.isWritable(resolved); + && parentDir != null + && probeDirectoryWritable(parentDir); if (usable) { LOG.debug("SQLite-Datei vorhanden und nutzbar: {}", resolved); } else { @@ -191,7 +195,7 @@ public class FilesystemPathCheckAdapter implements PathCheckPort { } // Datei existiert nicht — prüfen ob Elternordner schreibbar ist Path parent = resolved.getParent(); - if (parent != null && Files.exists(parent) && Files.isDirectory(parent) && Files.isWritable(parent)) { + if (parent != null && Files.exists(parent) && Files.isDirectory(parent) && probeDirectoryWritable(parent)) { LOG.debug("SQLite-Datei nicht vorhanden, aber anlegbar (Elternordner schreibbar): {}", resolved); return true; } @@ -199,6 +203,30 @@ public class FilesystemPathCheckAdapter implements PathCheckPort { return false; } + /** + * Prüft die Schreibbarkeit eines Verzeichnisses durch einen echten Schreibversuch. + *
+ * Eine temporäre Probe-Datei wird im übergebenen Verzeichnis angelegt und sofort + * wieder gelöscht. Dieses Vorgehen ist auf gemappten Windows-Netzlaufwerken + * zuverlässiger als {@link Files#isWritable(Path)}, das dort False Negatives liefern kann. + * + * @param directory das zu prüfende Verzeichnis; muss ein existierendes Verzeichnis sein + * @return {@code true} wenn eine Datei im Verzeichnis angelegt werden konnte + */ + private static boolean probeDirectoryWritable(Path directory) { + try { + Path probe = Files.createTempFile(directory, ".writetest-", ".tmp"); + try { + Files.deleteIfExists(probe); + } catch (IOException cleanupFailure) { + LOG.debug("Probe-Datei konnte nicht gelöscht werden: {} — {}", probe, cleanupFailure.getMessage()); + } + return true; + } catch (IOException e) { + return false; + } + } + /** * Konvertiert den übergebenen Pfad-String in ein {@link Path}-Objekt. *
diff --git a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/pathcheck/FilesystemPathCheckAdapterTest.java b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/pathcheck/FilesystemPathCheckAdapterTest.java index fa8cc18..9542da0 100644 --- a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/pathcheck/FilesystemPathCheckAdapterTest.java +++ b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/pathcheck/FilesystemPathCheckAdapterTest.java @@ -195,18 +195,25 @@ class FilesystemPathCheckAdapterTest { /** * Stellt sicher, dass Pfade mit gemapptem Laufwerksbuchstaben syntaktisch akzeptiert - * werden (kein sofortiger Syntaxfehler). Das Ergebnis ist {@code false}, weil das - * Laufwerk in dieser Testumgebung nicht existiert — aber es darf nicht wegen des - * Laufwerksbuchstabens allein abgelehnt werden. + * werden (kein sofortiger Syntaxfehler). Für Methoden ohne Elternpfad-Prüfung ist + * das Ergebnis {@code false}, weil das Laufwerk in dieser Testumgebung nicht existiert. + *
+ * Bei {@link FilesystemPathCheckAdapter#isDirectoryWritableOrCreatable} wird der + * Elternpfad (das Laufwerk selbst) per Probe-Datei geprüft. Existiert das Laufwerk + * {@code H:\} auf der Testmaschine und ist schreibbar, ist {@code true} das korrekte + * Ergebnis — die Prüfung wird in diesem Fall ohne Ergebnis-Assertion ausgeführt. */ @Test @EnabledOnOs(OS.WINDOWS) void windowsMappedDriveSyntax_isAcceptedByAdapter() { - // Ein Pfad mit gemapptem Laufwerksbuchstaben darf nicht wegen der Syntax abgelehnt - // werden. Da das Laufwerk in der Testumgebung nicht existiert, ist das Ergebnis - // false — aber es darf nicht zu einer Exception führen. assertFalse(adapter.isDirectoryReadable("S:\\nonexistent-in-test")); - assertFalse(adapter.isDirectoryWritableOrCreatable("H:\\nonexistent-in-test")); + // isDirectoryWritableOrCreatable prüft den Elternpfad per Probe-Datei. + // Existiert H:\ und ist schreibbar, gibt die Methode korrekt true zurück. + if (!Files.exists(Path.of("H:\\"))) { + assertFalse(adapter.isDirectoryWritableOrCreatable("H:\\nonexistent-in-test")); + } else { + adapter.isDirectoryWritableOrCreatable("H:\\nonexistent-in-test"); + } assertFalse(adapter.isFileReadable("X:\\nonexistent-in-test\\file.txt")); assertFalse(adapter.isSqlitePathUsable("Z:\\nonexistent-in-test\\db.db")); }