Bugfix Pfaderkennung

This commit is contained in:
2026-04-22 10:21:02 +02:00
parent 8286d0f0e5
commit 9ba32f1bb8
2 changed files with 50 additions and 15 deletions
@@ -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}.
* <p>
* 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.
*
* <h2>Windows- und Netzlaufwerk-Unterstützung</h2>
* <p>
@@ -27,8 +28,9 @@ import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.PathCheck
* Laufwerksbuchstaben und UNC-Pfaden statt.
* <p>
* 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.
*
* <h2>Thread-Safety</h2>
* <p>
@@ -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.
* <p>
* 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.
* <p>
@@ -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.
* <p>
* 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"));
}