diff --git a/src/main/java/cz/kamma/kfmanager/MainApp.java b/src/main/java/cz/kamma/kfmanager/MainApp.java index f962611..a088d31 100644 --- a/src/main/java/cz/kamma/kfmanager/MainApp.java +++ b/src/main/java/cz/kamma/kfmanager/MainApp.java @@ -15,7 +15,7 @@ import java.io.InputStreamReader; */ public class MainApp { - public static final String APP_VERSION = "1.4.2"; + public static final String APP_VERSION = "1.4.3"; public enum OS { WINDOWS, LINUX, MACOS, UNKNOWN diff --git a/src/main/java/cz/kamma/kfmanager/ui/DriveSelector.java b/src/main/java/cz/kamma/kfmanager/ui/DriveSelector.java index dc88716..0c8c554 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/DriveSelector.java +++ b/src/main/java/cz/kamma/kfmanager/ui/DriveSelector.java @@ -25,32 +25,7 @@ public class DriveSelector extends JDialog { ((JComponent) getContentPane()).setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); // Get list of available drives - java.util.Set driveSet = new java.util.LinkedHashSet<>(); - - File[] roots = File.listRoots(); - if (roots != null) { - for (File r : roots) { - driveSet.add(r); - } - } - - // On Windows, additionally add mapped network drives if missed by listRoots - if (cz.kamma.kfmanager.MainApp.CURRENT_OS == cz.kamma.kfmanager.MainApp.OS.WINDOWS) { - javax.swing.filechooser.FileSystemView fsv = javax.swing.filechooser.FileSystemView.getFileSystemView(); - File[] fsvRoots = fsv.getRoots(); - if (fsvRoots != null) { - for (File r : fsvRoots) { - File[] devices = fsv.getFiles(r, false); - if (devices != null) { - for (File d : devices) { - if (fsv.isDrive(d) || fsv.isFileSystemRoot(d)) { - driveSet.add(d); - } - } - } - } - } - } + java.util.List driveSet = DriveUtils.getAvailableDrives(); List drives = new ArrayList<>(); for (File drive : driveSet) { diff --git a/src/main/java/cz/kamma/kfmanager/ui/DriveUtils.java b/src/main/java/cz/kamma/kfmanager/ui/DriveUtils.java new file mode 100644 index 0000000..fff0bad --- /dev/null +++ b/src/main/java/cz/kamma/kfmanager/ui/DriveUtils.java @@ -0,0 +1,349 @@ +package cz.kamma.kfmanager.ui; + +import cz.kamma.kfmanager.MainApp; + +import javax.swing.filechooser.FileSystemView; +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Helpers for discovering filesystem roots shown by drive selectors. + */ +final class DriveUtils { + private static final Pattern WINDOWS_DRIVE_PATTERN = Pattern.compile("(?i)(? getAvailableDrives() { + Set drives = new LinkedHashSet<>(); + + File[] roots = File.listRoots(); + if (roots != null) { + for (File root : roots) { + addDrive(drives, root); + } + } + log("File.listRoots(): " + describeFiles(roots)); + + if (MainApp.CURRENT_OS == MainApp.OS.WINDOWS) { + addWindowsFileSystemViewDrives(drives); + addWindowsMappedNetworkDrives(drives); + } + + log("available drives: " + describeFiles(drives)); + return new ArrayList<>(drives); + } + + static File resolveDriveForLoading(File drive) { + log("resolveDriveForLoading input: " + describeFile(drive)); + if (drive == null || MainApp.CURRENT_OS != MainApp.OS.WINDOWS || drive.isDirectory()) { + log("resolveDriveForLoading direct: " + describeFile(drive)); + return drive; + } + + File normalizedDrive = normalizeDriveKey(drive); + Map mappedDrives = getWindowsMappedNetworkDriveTargets(); + log("net use mappings: " + describeMappings(mappedDrives)); + File mappedTarget = mappedDrives.get(normalizedDrive); + log("resolved mapped target for " + describeFile(normalizedDrive) + ": " + describeFile(mappedTarget)); + + if (mappedDrives.containsKey(normalizedDrive) && reconnectWindowsMappedDrive(normalizedDrive)) { + log("resolveDriveForLoading reconnected drive accepted: " + describeFile(normalizedDrive)); + return normalizedDrive; + } + + if (mappedTarget != null && mappedTarget.isDirectory()) { + log("resolveDriveForLoading mapped target accepted: " + describeFile(mappedTarget)); + return mappedTarget; + } + log("resolveDriveForLoading fallback to selected drive: " + describeFile(drive)); + return drive; + } + + static boolean isWindowsMappedOrUncPath(File directory) { + if (directory == null || MainApp.CURRENT_OS != MainApp.OS.WINDOWS) { + return false; + } + + String path = directory.getAbsolutePath(); + if (path.startsWith("\\\\")) { + return true; + } + + try { + java.nio.file.Path rootPath = directory.toPath().toAbsolutePath().normalize().getRoot(); + if (rootPath == null) { + return false; + } + File root = rootPath.toFile(); + return getWindowsMappedNetworkDriveTargets().containsKey(normalizeDriveKey(root)); + } catch (Exception ex) { + log("mapped path check failed for " + describeFile(directory) + ": " + ex.getMessage()); + return false; + } + } + + static File[] listWindowsDirectoryWithCmd(File directory) { + if (directory == null || MainApp.CURRENT_OS != MainApp.OS.WINDOWS) { + return new File[0]; + } + + List names = runWindowsDirectoryListCommand(directory); + List files = new ArrayList<>(); + for (String name : names) { + if (name == null || name.isBlank() || name.equals(".") || name.equals("..")) { + continue; + } + files.add(new File(directory, name)); + } + log("cmd dir entries for " + describeFile(directory) + ": " + describeFiles(files)); + return files.toArray(new File[0]); + } + + private static void addWindowsFileSystemViewDrives(Set drives) { + try { + FileSystemView fsv = FileSystemView.getFileSystemView(); + File[] roots = fsv.getRoots(); + if (roots != null) { + for (File root : roots) { + addWindowsShellChildren(drives, fsv, root); + } + } + addWindowsShellChildren(drives, fsv, fsv.getHomeDirectory()); + addWindowsShellChildren(drives, fsv, fsv.getDefaultDirectory()); + } catch (Exception ignore) { + } + } + + private static void addWindowsShellChildren(Set drives, FileSystemView fsv, File root) { + if (root == null) { + return; + } + try { + if (fsv.isDrive(root) || fsv.isFileSystemRoot(root)) { + addDrive(drives, root); + } + File[] files = fsv.getFiles(root, false); + if (files == null) { + return; + } + for (File file : files) { + if (fsv.isDrive(file) || fsv.isFileSystemRoot(file)) { + addDrive(drives, file); + } + } + } catch (Exception ignore) { + } + } + + private static void addWindowsMappedNetworkDrives(Set drives) { + for (File drive : getWindowsMappedNetworkDriveTargets().keySet()) { + addDrive(drives, drive); + } + } + + private static Map getWindowsMappedNetworkDriveTargets() { + Map mappedDrives = new LinkedHashMap<>(); + Process process = null; + try { + process = new ProcessBuilder("cmd.exe", "/c", "net use") + .redirectErrorStream(true) + .start(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), Charset.defaultCharset()))) { + String line; + while ((line = reader.readLine()) != null) { + log("net use line: " + line); + Matcher mappingMatcher = WINDOWS_NET_USE_MAPPING_PATTERN.matcher(line); + if (mappingMatcher.find()) { + mappedDrives.put( + normalizeDriveKey(new File(mappingMatcher.group(1) + "\\")), + new File(mappingMatcher.group(2))); + continue; + } + + Matcher driveMatcher = WINDOWS_DRIVE_PATTERN.matcher(line); + while (driveMatcher.find()) { + mappedDrives.putIfAbsent( + normalizeDriveKey(new File(driveMatcher.group(1) + "\\")), + null); + } + } + } + if (process.waitFor(2, TimeUnit.SECONDS)) { + log("net use exit code: " + process.exitValue()); + } else { + log("net use did not finish within timeout"); + } + } catch (Exception ignore) { + log("net use failed: " + ignore.getMessage()); + } finally { + if (process != null && process.isAlive()) { + process.destroyForcibly(); + } + } + return mappedDrives; + } + + private static boolean reconnectWindowsMappedDrive(File drive) { + File normalizedDrive = normalizeDriveKey(drive); + if (normalizedDrive == null) { + return false; + } + + log("attempting mapped drive reconnect via dir: " + describeFile(normalizedDrive)); + int exitCode = runWindowsProbeCommand("cmd.exe", "/c", "dir", normalizedDrive.getPath()); + log("mapped drive reconnect dir exit code: " + exitCode + ", after: " + describeFile(normalizedDrive)); + return normalizedDrive.isDirectory(); + } + + private static int runWindowsProbeCommand(String... command) { + Process process = null; + try { + process = new ProcessBuilder(command) + .redirectErrorStream(true) + .start(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), Charset.defaultCharset()))) { + String line; + while ((line = reader.readLine()) != null) { + log("probe line: " + line); + } + } + if (process.waitFor(5, TimeUnit.SECONDS)) { + return process.exitValue(); + } + log("probe did not finish within timeout"); + } catch (Exception ex) { + log("probe failed: " + ex.getMessage()); + } finally { + if (process != null && process.isAlive()) { + process.destroyForcibly(); + } + } + return -1; + } + + private static List runWindowsDirectoryListCommand(File directory) { + List lines = new ArrayList<>(); + Process process = null; + try { + process = new ProcessBuilder("cmd.exe", "/c", "dir", "/a", "/b", directory.getPath()) + .redirectErrorStream(true) + .start(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), Charset.defaultCharset()))) { + String line; + while ((line = reader.readLine()) != null) { + log("dir list line: " + line); + lines.add(line); + } + } + if (process.waitFor(5, TimeUnit.SECONDS)) { + int exitCode = process.exitValue(); + log("dir list exit code: " + exitCode); + if (exitCode != 0) { + return new ArrayList<>(); + } + } else { + log("dir list did not finish within timeout"); + return new ArrayList<>(); + } + } catch (Exception ex) { + log("dir list failed: " + ex.getMessage()); + return new ArrayList<>(); + } finally { + if (process != null && process.isAlive()) { + process.destroyForcibly(); + } + } + return lines; + } + + private static File normalizeDriveKey(File drive) { + if (drive == null) { + return null; + } + Matcher matcher = WINDOWS_DRIVE_PATTERN.matcher(drive.getAbsolutePath()); + if (matcher.find() && matcher.start() == 0) { + return new File(matcher.group(1).toUpperCase(Locale.ROOT) + "\\"); + } + return drive.getAbsoluteFile(); + } + + private static void addDrive(Set drives, File drive) { + if (drive == null) { + return; + } + drives.add(normalizeDriveKey(drive)); + } + + private static void log(String message) { + System.err.println("[DRIVE] " + message); + } + + private static String describeFile(File file) { + if (file == null) { + return ""; + } + try { + return file.getPath() + + " [abs=" + file.getAbsolutePath() + + ", exists=" + file.exists() + + ", dir=" + file.isDirectory() + + ", canRead=" + file.canRead() + + "]"; + } catch (Exception ex) { + return file.getPath() + " [describe failed: " + ex.getMessage() + "]"; + } + } + + private static String describeFiles(File[] files) { + if (files == null) { + return ""; + } + return describeFiles(Arrays.asList(files)); + } + + private static String describeFiles(Iterable files) { + StringBuilder sb = new StringBuilder("["); + boolean first = true; + for (File file : files) { + if (!first) { + sb.append(", "); + } + sb.append(describeFile(file)); + first = false; + } + return sb.append(']').toString(); + } + + private static String describeMappings(Map mappings) { + StringBuilder sb = new StringBuilder("["); + boolean first = true; + for (Map.Entry entry : mappings.entrySet()) { + if (!first) { + sb.append(", "); + } + sb.append(describeFile(entry.getKey())).append(" -> ").append(describeFile(entry.getValue())); + first = false; + } + return sb.append(']').toString(); + } +} diff --git a/src/main/java/cz/kamma/kfmanager/ui/FilePanel.java b/src/main/java/cz/kamma/kfmanager/ui/FilePanel.java index ef22883..58bd0c8 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/FilePanel.java +++ b/src/main/java/cz/kamma/kfmanager/ui/FilePanel.java @@ -125,20 +125,28 @@ public class FilePanel extends JPanel { Object selObj = driveCombo.getSelectedItem(); if (selObj instanceof File sel) { + System.err.println("[DRIVE] combo selected: " + describeDriveForLog(sel)); FilePanelTab currentTab = getCurrentTab(); if (currentTab != null) { - File targetDirectory = sel; + File targetDirectory = DriveUtils.resolveDriveForLoading(sel); + System.err.println("[DRIVE] target after drive resolver: " + describeDriveForLog(targetDirectory)); if (driveSelectionTargetResolver != null) { try { - File resolved = driveSelectionTargetResolver.apply(sel); - if (resolved != null && resolved.exists() && resolved.isDirectory() && isWithinSelectedDrive(resolved, sel)) { + File resolved = driveSelectionTargetResolver.apply(targetDirectory); + System.err.println("[DRIVE] opposite-panel resolver returned: " + describeDriveForLog(resolved)); + if (resolved != null && resolved.exists() && resolved.isDirectory() && isWithinSelectedDrive(resolved, targetDirectory)) { targetDirectory = resolved; + System.err.println("[DRIVE] opposite-panel resolver accepted: " + describeDriveForLog(targetDirectory)); + } else { + System.err.println("[DRIVE] opposite-panel resolver rejected for selected drive: " + describeDriveForLog(targetDirectory)); } - } catch (Exception ignore) { + } catch (Exception ex) { + System.err.println("[DRIVE] opposite-panel resolver failed: " + ex.getMessage()); // fall back to selected drive root } } + System.err.println("[DRIVE] loading selected drive target: " + describeDriveForLog(targetDirectory)); currentTab.loadDirectory(targetDirectory); SwingUtilities.invokeLater(() -> { try { currentTab.getFileTable().requestFocusInWindow(); } catch (Exception ignore) {} @@ -269,6 +277,22 @@ public class FilePanel extends JPanel { } } + private static String describeDriveForLog(File file) { + if (file == null) { + return ""; + } + try { + return file.getPath() + + " [abs=" + file.getAbsolutePath() + + ", exists=" + file.exists() + + ", dir=" + file.isDirectory() + + ", canRead=" + file.canRead() + + "]"; + } catch (Exception ex) { + return file.getPath() + " [describe failed: " + ex.getMessage() + "]"; + } + } + private boolean isWithinSelectedDrive(File directory, File selectedDrive) { try { java.nio.file.Path dirPath = directory.toPath().toAbsolutePath().normalize(); @@ -649,33 +673,7 @@ public class FilePanel extends JPanel { try { driveCombo.removeAllItems(); java.util.Set driveSet = new java.util.LinkedHashSet<>(); - - // Add standard roots - File[] roots = File.listRoots(); - if (roots != null) { - for (File r : roots) { - driveSet.add(r); - } - } - - // On Windows, additionally add mapped network drives if missed by listRoots - if (MainApp.CURRENT_OS == MainApp.OS.WINDOWS) { - javax.swing.filechooser.FileSystemView fsv = javax.swing.filechooser.FileSystemView.getFileSystemView(); - File[] fsvRoots = fsv.getRoots(); - if (fsvRoots != null) { - for (File r : fsvRoots) { - // This usually returns "Desktop". We need to look into it or other FSV methods. - File[] devices = fsv.getFiles(r, false); - if (devices != null) { - for (File d : devices) { - if (fsv.isDrive(d) || fsv.isFileSystemRoot(d)) { - driveSet.add(d); - } - } - } - } - } - } + driveSet.addAll(DriveUtils.getAvailableDrives()); // Add home directory driveSet.add(new File(System.getProperty("user.home"))); diff --git a/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java b/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java index 3d7a86c..38d7b91 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java +++ b/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java @@ -1473,24 +1473,34 @@ public class FilePanelTab extends JPanel { } public void loadDirectory(File directory, List preloadedItems, boolean autoSelectFirst, boolean requestFocus, final Runnable postLoadAction) { + File requestedDirectory = directory; + System.err.println("[DRIVE] FilePanelTab.loadDirectory requested: " + describeDirectoryForLog(requestedDirectory)); + // Ensure we load an existing directory - try parents if necessary File dirToLoad = directory; while (dirToLoad != null && !dirToLoad.isDirectory()) { + System.err.println("[DRIVE] loadDirectory candidate is not directory, trying parent: " + describeDirectoryForLog(dirToLoad)); dirToLoad = dirToLoad.getParentFile(); } if (dirToLoad == null) { dirToLoad = new File(System.getProperty("user.home")); + System.err.println("[DRIVE] loadDirectory fell back to user.home: " + describeDirectoryForLog(dirToLoad)); if (!dirToLoad.isDirectory()) { dirToLoad = new File(File.separator); + System.err.println("[DRIVE] loadDirectory fell back to File.separator: " + describeDirectoryForLog(dirToLoad)); } } directory = dirToLoad; + if (requestedDirectory != directory) { + System.err.println("[DRIVE] loadDirectory final candidate: " + describeDirectoryForLog(directory)); + } // If we are switching directories, cleanup any previously extracted archive temp dirs cleanupArchiveTempDirIfNeeded(directory); if (directory == null || !directory.isDirectory()) { + System.err.println("[DRIVE] loadDirectory aborted, final candidate is invalid: " + describeDirectoryForLog(directory)); return; } @@ -1847,6 +1857,15 @@ public class FilePanelTab extends JPanel { return new ArrayList<>(); } File[] files = directory.listFiles(); + if (MainApp.CURRENT_OS == MainApp.OS.WINDOWS && DriveUtils.isWindowsMappedOrUncPath(directory)) { + File[] cmdFiles = DriveUtils.listWindowsDirectoryWithCmd(directory); + System.err.println("[DRIVE] listFiles count=" + (files != null ? files.length : -1) + + ", cmd dir count=" + cmdFiles.length + + " for " + describeDirectoryForLog(directory)); + files = mergeFileArrays(files, cmdFiles); + System.err.println("[DRIVE] merged directory entry count=" + (files != null ? files.length : -1) + + " for " + describeDirectoryForLog(directory)); + } List items = new ArrayList<>(); File parent = directory.getParentFile(); if (parent != null) { @@ -1868,6 +1887,28 @@ public class FilePanelTab extends JPanel { return items; } + private File[] mergeFileArrays(File[] primary, File[] secondary) { + if (primary == null || primary.length == 0) { + return secondary != null ? secondary : new File[0]; + } + if (secondary == null || secondary.length == 0) { + return primary; + } + + Map merged = new LinkedHashMap<>(); + for (File file : primary) { + if (file != null) { + merged.put(file.getAbsolutePath().toLowerCase(Locale.ROOT), file); + } + } + for (File file : secondary) { + if (file != null) { + merged.putIfAbsent(file.getAbsolutePath().toLowerCase(Locale.ROOT), file); + } + } + return merged.values().toArray(new File[0]); + } + private boolean isSameContent(List list1, List list2) { if (list1.size() != list2.size()) return false; for (int i = 0; i < list1.size(); i++) { @@ -4420,6 +4461,22 @@ public class FilePanelTab extends JPanel { public File getCurrentDirectory() { return currentDirectory; } + + private static String describeDirectoryForLog(File file) { + if (file == null) { + return ""; + } + try { + return file.getPath() + + " [abs=" + file.getAbsolutePath() + + ", exists=" + file.exists() + + ", dir=" + file.isDirectory() + + ", canRead=" + file.canRead() + + "]"; + } catch (Exception ex) { + return file.getPath() + " [describe failed: " + ex.getMessage() + "]"; + } + } // FileTableModel private class FileTableModel extends AbstractTableModel {