ftp improved

This commit is contained in:
Radek Davidek 2026-04-21 18:49:16 +02:00
parent a810cfac02
commit 14d386f736
5 changed files with 155 additions and 6 deletions

View File

@ -489,6 +489,17 @@ public class FtpService {
return pwd; return pwd;
} }
/**
* Explicitly close connection for a profile if any is cached.
* Note: Current infrastructure creates/closes connections per operation,
* but this provides a hook for future persistent connection management.
*/
public static void disconnect(FtpProfile profile) {
if (profile == null) return;
log("DISCONNECT requested for profile: " + profile.getName() + " (" + profile.getHost() + ")");
// When persistent connections are implemented, they should be closed here.
}
/** /**
* Parse an FTP URL (ftp://host:port/path) and find matching profile from AppConfig. * Parse an FTP URL (ftp://host:port/path) and find matching profile from AppConfig.
*/ */
@ -527,7 +538,7 @@ public class FtpService {
return FTP_PREFIX + profile.getHost() + ":" + profile.getPort() + remotePath; return FTP_PREFIX + profile.getHost() + ":" + profile.getPort() + remotePath;
} }
private static String getNameFromPath(String path) { public static String getNameFromPath(String path) {
if (path == null || path.equals("/") || path.isEmpty()) return ""; if (path == null || path.equals("/") || path.isEmpty()) return "";
int lastSlash = path.lastIndexOf('/'); int lastSlash = path.lastIndexOf('/');
return path.substring(lastSlash + 1); return path.substring(lastSlash + 1);

View File

@ -29,6 +29,12 @@ public class FileEditor extends JFrame {
private boolean readOnly; private boolean readOnly;
private JLabel statusPosLabel; private JLabel statusPosLabel;
private JLabel statusSelLabel; private JLabel statusSelLabel;
// FTP upload support
private cz.kamma.kfmanager.model.FtpProfile ftpProfile;
private String ftpPath;
private Runnable onSaveSuccess;
// Hex view support // Hex view support
private boolean hexMode = false; private boolean hexMode = false;
private byte[] fileBytes = null; private byte[] fileBytes = null;
@ -112,6 +118,15 @@ public class FileEditor extends JFrame {
}); });
} }
/**
* Set FTP details for automatic upload after saving.
*/
public void setFtpDetails(cz.kamma.kfmanager.model.FtpProfile profile, String remotePath, Runnable onSaveSuccess) {
this.ftpProfile = profile;
this.ftpPath = remotePath;
this.onSaveSuccess = onSaveSuccess;
}
private void initSearchPanel() { private void initSearchPanel() {
searchPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 2)); searchPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 2));
searchPanel.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, Color.GRAY)); searchPanel.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, Color.GRAY));
@ -959,7 +974,7 @@ public class FileEditor extends JFrame {
} }
private void loadFile() { private void loadFile() {
if (virtualPath != null) { if (virtualPath != null && ftpProfile == null) {
try { try {
fileBytes = cz.kamma.kfmanager.service.FileOperations.readFileFromArchive(file, virtualPath); fileBytes = cz.kamma.kfmanager.service.FileOperations.readFileFromArchive(file, virtualPath);
boolean binary = isBinary(fileBytes); boolean binary = isBinary(fileBytes);
@ -1212,6 +1227,40 @@ public class FileEditor extends JFrame {
Files.write(file.toPath(), content.getBytes("UTF-8")); Files.write(file.toPath(), content.getBytes("UTF-8"));
modified = false; modified = false;
updateTitle(); updateTitle();
if (ftpProfile != null && ftpPath != null) {
final cz.kamma.kfmanager.model.FtpProfile finalProfile = ftpProfile;
final String finalPath = ftpPath;
final File finalFile = file;
// Use the main window's common operation runner if possible, or just background it here.
// Since FileEditor works standalone too, we'll use a simple SwingWorker.
new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
cz.kamma.kfmanager.service.FtpService.uploadFile(finalProfile, finalFile, finalPath, null);
return null;
}
@Override
protected void done() {
try {
get();
if (onSaveSuccess != null) {
onSaveSuccess.run();
}
JOptionPane.showMessageDialog(FileEditor.this, "File saved and uploaded to FTP", "Success", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
JOptionPane.showMessageDialog(FileEditor.this,
"Successfully saved locally, but failed to upload to FTP:\n" + ex.getMessage(),
"FTP Upload Error",
JOptionPane.ERROR_MESSAGE);
}
}
}.execute();
return; // message handled in SwingWorker
}
JOptionPane.showMessageDialog(this, JOptionPane.showMessageDialog(this,
"File saved", "File saved",
"Success", "Success",

View File

@ -440,6 +440,8 @@ public class FilePanel extends JPanel {
// Extract display name from FTP URL for tab title // Extract display name from FTP URL for tab title
String tabTitle = getFtpTabTitle(ftpPath); String tabTitle = getFtpTabTitle(ftpPath);
applyConfiguredAppearance(tab);
tabbedPane.addTab(tabTitle, tab); tabbedPane.addTab(tabTitle, tab);
tabbedPane.setSelectedComponent(tab); tabbedPane.setSelectedComponent(tab);
@ -788,9 +790,11 @@ public class FilePanel extends JPanel {
Component comp = tabbedPane.getComponentAt(i); Component comp = tabbedPane.getComponentAt(i);
if (comp instanceof FilePanelTab tab && tab.isFtpTab()) { if (comp instanceof FilePanelTab tab && tab.isFtpTab()) {
if (tabbedPane.getTabCount() > 1) { if (tabbedPane.getTabCount() > 1) {
tab.cleanup();
tabbedPane.removeTabAt(i); tabbedPane.removeTabAt(i);
} else { } else {
// If it's the last tab, reset it to home instead of removing // If it's the last tab, reset it to home instead of removing
tab.cleanup();
tab.loadDirectory(new File(System.getProperty("user.home"))); tab.loadDirectory(new File(System.getProperty("user.home")));
} }
} }
@ -805,6 +809,10 @@ public class FilePanel extends JPanel {
public void closeCurrentTab() { public void closeCurrentTab() {
int index = tabbedPane.getSelectedIndex(); int index = tabbedPane.getSelectedIndex();
if (index >= 0 && tabbedPane.getTabCount() > 1) { if (index >= 0 && tabbedPane.getTabCount() > 1) {
Component comp = tabbedPane.getComponentAt(index);
if (comp instanceof FilePanelTab tab) {
tab.cleanup();
}
tabbedPane.removeTabAt(index); tabbedPane.removeTabAt(index);
updatePathField(); updatePathField();
updateTabStyles(); updateTabStyles();

View File

@ -1624,8 +1624,14 @@ public class FilePanelTab extends JPanel {
*/ */
public void navigateFtpUp() { public void navigateFtpUp() {
if (ftpCurrentPath == null || ftpCurrentPath.equals("/")) return; if (ftpCurrentPath == null || ftpCurrentPath.equals("/")) return;
String previousDirName = FtpService.getNameFromPath(ftpCurrentPath);
String parentPath = FtpService.getParentPath(ftpCurrentPath); String parentPath = FtpService.getParentPath(ftpCurrentPath);
loadFtpDirectory(parentPath, true, true); loadFtpDirectory(parentPath, false, true);
// Select the directory we just left
if (previousDirName != null && !previousDirName.isEmpty()) {
SwingUtilities.invokeLater(() -> selectItemByName(previousDirName));
}
} }
/** /**
@ -4323,6 +4329,15 @@ public class FilePanelTab extends JPanel {
updateSortButtonsDisplay(); updateSortButtonsDisplay();
} }
/**
* Cleanup resources when tab is closed.
*/
public void cleanup() {
if (isFtpTab && ftpProfile != null) {
FtpService.disconnect(ftpProfile);
}
}
// Getters // Getters
public JTable getFileTable() { public JTable getFileTable() {
return fileTable; return fileTable;

View File

@ -17,6 +17,7 @@ import java.awt.datatransfer.DataFlavor;
import java.awt.event.*; import java.awt.event.*;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -2436,6 +2437,30 @@ public class MainWindow extends JFrame {
if (item.isDirectory() || item.getName().equals("..")) { if (item.isDirectory() || item.getName().equals("..")) {
return; return;
} }
if (item.isFtp()) {
try {
File tempFile = Files.createTempFile("kf-ftp-view-", "-" + item.getName()).toFile();
tempFile.deleteOnExit();
performFileOperation((cb) -> {
cz.kamma.kfmanager.service.FtpService.downloadFile(item.getFtpProfile(), item.getFtpPath(), tempFile, cb);
}, "Download finished", false, false, () -> {
FileEditor viewer = new FileEditor(this, tempFile, item.getFtpPath(), config, true);
viewer.addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosed(java.awt.event.WindowEvent e) {
try { tempFile.delete(); } catch(Exception ignore) {}
requestFocusInActivePanel();
}
});
viewer.setVisible(true);
});
} catch (IOException e) {
JOptionPane.showMessageDialog(this, "Could not create temp file: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
return;
}
File file = item.getFile(); File file = item.getFile();
@ -2464,6 +2489,49 @@ public class MainWindow extends JFrame {
if (item.isDirectory() || item.getName().equals("..")) { if (item.isDirectory() || item.getName().equals("..")) {
return; return;
} }
if (item.isFtp()) {
try {
File tempFile = Files.createTempFile("kf-ftp-edit-", "-" + item.getName()).toFile();
tempFile.deleteOnExit();
performFileOperation((cb) -> {
cz.kamma.kfmanager.service.FtpService.downloadFile(item.getFtpProfile(), item.getFtpPath(), tempFile, cb);
}, "Download finished", false, false, () -> {
// Check if an external editor is configured
String ext = config.getExternalEditorPath();
if (ext != null && !ext.trim().isEmpty()) {
try {
java.util.List<String> cmd = new java.util.ArrayList<>();
cmd.add(ext);
cmd.add(tempFile.getAbsolutePath());
Process p = new ProcessBuilder(cmd).start();
// Optional: Monitor process to upload back on exit?
// For now internal editor is safer for auto-upload.
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "External editor failed: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
FileEditor editor = new FileEditor(this, tempFile, item.getFtpPath(), config, false);
editor.setFtpDetails(item.getFtpProfile(), item.getFtpPath(), () -> {
// Success callback after upload
if (activePanel != null) activePanel.refresh(false);
});
editor.addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosed(java.awt.event.WindowEvent e) {
try { tempFile.delete(); } catch(Exception ignore) {}
requestFocusInActivePanel();
}
});
editor.setVisible(true);
});
} catch (IOException e) {
JOptionPane.showMessageDialog(this, "Could not create temp file: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
return;
}
File file = item.getFile(); File file = item.getFile();
@ -3302,9 +3370,7 @@ public class MainWindow extends JFrame {
MainWindow.this.toFront(); MainWindow.this.toFront();
for (FilePanel panel : panelsToRefresh) { for (FilePanel panel : panelsToRefresh) {
if (panel.getCurrentDirectory() != null) { panel.refresh(false);
panel.loadDirectory(panel.getCurrentDirectory(), false, false);
}
} }
if (postTask != null) { if (postTask != null) {