diff --git a/src/main/java/com/example/plugin/ChooseServerWidget.java b/src/main/java/com/example/plugin/ChooseServerWidget.java index 688b658..f24615c 100644 --- a/src/main/java/com/example/plugin/ChooseServerWidget.java +++ b/src/main/java/com/example/plugin/ChooseServerWidget.java @@ -11,7 +11,6 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.event.MouseEvent; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -36,7 +35,6 @@ public class ChooseServerWidget implements StatusBarWidget, return this; } - // ---------- IconPresentation ---------- @Override public @Nullable Icon getIcon() { return null; @@ -76,7 +74,6 @@ public class ChooseServerWidget implements StatusBarWidget, }; } - // ---------- TextPresentation ---------- @Override public @Nullable String getText() { String serverId = ProjectServerMapping.getInstance().getServerIdForProject(project.getBasePath()); @@ -92,12 +89,10 @@ public class ChooseServerWidget implements StatusBarWidget, return JComponent.CENTER_ALIGNMENT; } - // ---------- Логика переключения ---------- private void switchToServer(SshServer server) { SshServerManager.getInstance().setCurrentServerId(server.id); - String localPath = Paths.get(System.getProperty("user.home"), "ssh-remote-projects", - sanitize(server.host), sanitize(server.remoteProjectPath)).toString().replace('\\', '/'); + String localPath = SshServerManager.getLocalProjectPath(server); for (Project openProject : ProjectManager.getInstance().getOpenProjects()) { if (openProject.getBasePath() != null && openProject.getBasePath().equals(localPath)) { @@ -118,8 +113,4 @@ public class ChooseServerWidget implements StatusBarWidget, com.intellij.openapi.options.ShowSettingsUtil.getInstance() .showSettingsDialog(project, SshServerConfigurable.class); } - - private String sanitize(String input) { - return input.replaceAll("[^a-zA-Z0-9.-]", "_"); - } } \ No newline at end of file diff --git a/src/main/java/com/example/plugin/ConnectToRemoteAction.java b/src/main/java/com/example/plugin/ConnectToRemoteAction.java index 4820bc5..bb84a51 100644 --- a/src/main/java/com/example/plugin/ConnectToRemoteAction.java +++ b/src/main/java/com/example/plugin/ConnectToRemoteAction.java @@ -16,33 +16,25 @@ import com.jcraft.jsch.*; import org.jetbrains.annotations.NotNull; import java.io.IOException; import org.jdom.JDOMException; -import java.io.File; import java.io.*; -import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Optional; import java.util.Vector; public class ConnectToRemoteAction extends AnAction { @Override public void actionPerformed(@NotNull AnActionEvent e) { - // Показываем окно настройки серверов ShowSettingsUtil.getInstance().showSettingsDialog(e.getProject(), SshServerConfigurable.class); } - // Статический метод для открытия проекта (используется из виджета и действия) public static void openProjectForServer(SshServer server) { - String localPath = Paths.get(System.getProperty("user.home"), "ssh-remote-projects", - sanitize(server.host), sanitize(server.remoteProjectPath)).toString().replace('\\', '/'); + String localPath = SshServerManager.getLocalProjectPath(server); - // Если папка уже существует, просто открываем File localDir = new File(localPath); if (localDir.exists() && localDir.listFiles().length > 0) { openExistingProject(localPath, server.id); } else { - // Скачиваем и открываем downloadAndOpen(localPath, server); } } @@ -74,6 +66,28 @@ public class ConnectToRemoteAction extends AnAction { } } + public static void syncProject(SshServer server, String localPath, ProgressIndicator indicator) throws Exception { + JSch jsch = new JSch(); + Session session = jsch.getSession(server.user, server.host, server.port); + session.setPassword(SshServerManager.getInstance().getPassword(server.id)); + java.util.Properties config = new java.util.Properties(); + config.put("StrictHostKeyChecking", "no"); + session.setConfig(config); + session.connect(5000); + + ChannelSftp sftp = (ChannelSftp) session.openChannel("sftp"); + sftp.connect(3000); + + try { + File localDir = new File(localPath); + if (!localDir.exists()) localDir.mkdirs(); + syncDir(sftp, server.remoteProjectPath, localDir, indicator); + } finally { + sftp.disconnect(); + session.disconnect(); + } + } + private static void downloadProject(ProgressIndicator indicator, String localPath, SshServer server) throws Exception { JSch jsch = new JSch(); Session session = jsch.getSession(server.user, server.host, server.port); @@ -116,6 +130,49 @@ public class ConnectToRemoteAction extends AnAction { } } + private static void syncDir(ChannelSftp sftp, String remoteDir, File localDir, ProgressIndicator indicator) { + try { + @SuppressWarnings("unchecked") + Vector entries = sftp.ls(remoteDir); + for (ChannelSftp.LsEntry entry : entries) { + if (indicator.isCanceled()) break; + + String filename = entry.getFilename(); + if (filename.equals(".") || filename.equals("..")) continue; + + String remotePath = remoteDir + "/" + filename; + try { + if (entry.getAttrs().isDir()) { + File subDir = new File(localDir, filename); + if (!subDir.exists()) subDir.mkdirs(); + syncDir(sftp, remotePath, subDir, indicator); + } else { + File localFile = new File(localDir, filename); + boolean needDownload = !localFile.exists(); + if (!needDownload && localFile.isFile()) { + long remoteSize = entry.getAttrs().getSize(); + long remoteMtime = entry.getAttrs().getMTime(); + if (localFile.length() != remoteSize || localFile.lastModified() / 1000L != remoteMtime) { + needDownload = true; + } + } + if (needDownload) { + indicator.setText("Syncing: " + filename); + sftp.get(remotePath, localFile.getAbsolutePath()); + if (entry.getAttrs().getMTime() > 0) { + localFile.setLastModified(entry.getAttrs().getMTime() * 1000L); + } + } + } + } catch (Exception e) { + System.err.println("Skip entry: " + remotePath + " - " + e.getMessage()); + } + } + } catch (SftpException e) { + System.err.println("Cannot list directory: " + remoteDir + " - " + e.getMessage()); + } + } + private static void createProjectStructure(File projectDir) throws IOException { File ideaDir = new File(projectDir, ".idea"); if (!ideaDir.exists()) ideaDir.mkdirs(); @@ -163,75 +220,4 @@ public class ConnectToRemoteAction extends AnAction { writer.write(content); } } - - private static String sanitize(String input) { - return input.replaceAll("[^a-zA-Z0-9.-]", "_"); - } - - - public static void syncProject(SshServer server, String localPath, ProgressIndicator indicator) throws Exception { - JSch jsch = new JSch(); - Session session = jsch.getSession(server.user, server.host, server.port); - session.setPassword(SshServerManager.getInstance().getPassword(server.id)); - java.util.Properties config = new java.util.Properties(); - config.put("StrictHostKeyChecking", "no"); - session.setConfig(config); - session.connect(5000); - - ChannelSftp sftp = (ChannelSftp) session.openChannel("sftp"); - sftp.connect(3000); - - try { - File localDir = new File(localPath); - if (!localDir.exists()) localDir.mkdirs(); - syncDir(sftp, server.remoteProjectPath, localDir, indicator); - } finally { - sftp.disconnect(); - session.disconnect(); - } - } - - private static void syncDir(ChannelSftp sftp, String remoteDir, File localDir, ProgressIndicator indicator) { - try { - @SuppressWarnings("unchecked") - Vector entries = sftp.ls(remoteDir); - for (ChannelSftp.LsEntry entry : entries) { - if (indicator.isCanceled()) break; - - String filename = entry.getFilename(); - if (filename.equals(".") || filename.equals("..")) continue; - - String remotePath = remoteDir + "/" + filename; - try { - if (entry.getAttrs().isDir()) { - File subDir = new File(localDir, filename); - if (!subDir.exists()) subDir.mkdirs(); - syncDir(sftp, remotePath, subDir, indicator); - } else { - File localFile = new File(localDir, filename); - boolean needDownload = !localFile.exists(); - if (!needDownload && localFile.isFile()) { - long remoteSize = entry.getAttrs().getSize(); - long remoteMtime = entry.getAttrs().getMTime(); - if (localFile.length() != remoteSize || localFile.lastModified() / 1000L != remoteMtime) { - needDownload = true; - } - } - if (needDownload) { - indicator.setText("Syncing: " + filename); - sftp.get(remotePath, localFile.getAbsolutePath()); - if (entry.getAttrs().getMTime() > 0) { - localFile.setLastModified(entry.getAttrs().getMTime() * 1000L); - } - } - } - } catch (Exception e) { - // Логируем и пропускаем файл, который не удалось скачать/записать - System.err.println("Skip entry: " + remotePath + " - " + e.getMessage()); - } - } - } catch (SftpException e) { - System.err.println("Cannot list directory: " + remoteDir + " - " + e.getMessage()); - } - } } \ No newline at end of file diff --git a/src/main/java/com/example/plugin/RefreshFromServerAction.java b/src/main/java/com/example/plugin/RefreshFromServerAction.java index c46e23d..d0e350e 100644 --- a/src/main/java/com/example/plugin/RefreshFromServerAction.java +++ b/src/main/java/com/example/plugin/RefreshFromServerAction.java @@ -13,7 +13,6 @@ import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.annotations.NotNull; -import java.nio.file.Paths; import java.util.Optional; public class RefreshFromServerAction extends AnAction { @@ -42,12 +41,9 @@ public class RefreshFromServerAction extends AnAction { @Override public void run(@NotNull ProgressIndicator indicator) { try { - String localRoot = Paths.get(System.getProperty("user.home"), "ssh-remote-projects", - sanitize(server.host), sanitize(server.remoteProjectPath)) - .toString().replace('\\', '/'); + String localRoot = SshServerManager.getLocalProjectPath(server); ConnectToRemoteAction.syncProject(server, localRoot, indicator); - // Обновляем виртуальную файловую систему, чтобы IDE увидела изменения ApplicationManager.getApplication().invokeLater(() -> { VirtualFile rootDir = LocalFileSystem.getInstance().findFileByPath(localRoot); if (rootDir != null) { @@ -62,8 +58,4 @@ public class RefreshFromServerAction extends AnAction { } }); } - - private String sanitize(String input) { - return input.replaceAll("[^a-zA-Z0-9.-]", "_"); - } } \ No newline at end of file diff --git a/src/main/java/com/example/plugin/SshServerConfigurable.java b/src/main/java/com/example/plugin/SshServerConfigurable.java index d11c681..d6ed2db 100644 --- a/src/main/java/com/example/plugin/SshServerConfigurable.java +++ b/src/main/java/com/example/plugin/SshServerConfigurable.java @@ -189,6 +189,11 @@ class ServerDialog extends DialogWrapper { result.add(new ValidationInfo("Invalid port", portField)); } if (remotePathField.getText().trim().isEmpty()) result.add(new ValidationInfo("Remote path required", remotePathField)); + + // Требуем пароль для нового сервера + if (SshServerManager.getInstance().findServer(server.id).isEmpty() && passwordField.getPassword().length == 0) { + result.add(new ValidationInfo("Password required for new server", passwordField)); + } return result; } @@ -206,7 +211,7 @@ class ServerDialog extends DialogWrapper { if (!pass.isEmpty()) { SshServerManager.getInstance().setPassword(server.id, pass); } - // Если поле пустое – оставляем старый пароль (если есть) нетронутым + // Если пароль пустой и сервер уже существует – старый пароль остаётся без изменений super.doOKAction(); } } diff --git a/src/main/java/com/example/plugin/SshServerManager.java b/src/main/java/com/example/plugin/SshServerManager.java index 68892dd..b715a64 100644 --- a/src/main/java/com/example/plugin/SshServerManager.java +++ b/src/main/java/com/example/plugin/SshServerManager.java @@ -9,6 +9,7 @@ import com.intellij.util.xmlb.XmlSerializerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -56,7 +57,6 @@ public class SshServerManager implements PersistentStateComponent maybeServer = SshServerManager.getInstance().findServer(serverId); if (maybeServer.isPresent()) { SshServer server = maybeServer.get(); - // Модальный диалог – блокирует IDE до завершения синхронизации ApplicationManager.getApplication().invokeLater(() -> { ProgressManager.getInstance().run(new Task.Modal(project, "Synchronizing with " + server.name, true) { @Override public void run(@NotNull ProgressIndicator indicator) { try { - String localRoot = Paths.get(System.getProperty("user.home"), "ssh-remote-projects", - sanitize(server.host), sanitize(server.remoteProjectPath)) - .toString().replace('\\', '/'); + String localRoot = SshServerManager.getLocalProjectPath(server); ConnectToRemoteAction.syncProject(server, localRoot, indicator); - - // Обновляем виртуальную файловую систему, чтобы IDE увидела изменения ApplicationManager.getApplication().invokeLater(() -> { VirtualFile rootDir = LocalFileSystem.getInstance().findFileByPath(localRoot); if (rootDir != null) { @@ -99,7 +99,6 @@ public class SshSyncStartupActivity implements StartupActivity { String projectPath = project.getBasePath(); if (projectPath == null) return; - // Определяем привязанный сервер String serverId = ProjectServerMapping.getInstance().getServerIdForProject(projectPath); if (serverId == null) return; @@ -107,31 +106,24 @@ public class SshSyncStartupActivity implements StartupActivity { if (maybeServer.isEmpty()) return; SshServer server = maybeServer.get(); - // Вычисляем локальный корень проекта (как он был сохранен при создании) - String localRoot = Paths.get(System.getProperty("user.home"), "ssh-remote-projects", - sanitize(server.host), sanitize(server.remoteProjectPath)) - .toString().replace('\\', '/'); + String localRoot = SshServerManager.getLocalProjectPath(server); - String localFilePath = localFile.getPath(); // всегда с прямыми слешами + String localFilePath = localFile.getPath(); - // Игнорируем служебные файлы IDE if (localFilePath.contains("/.idea/") || localFile.getName().equals("remote.iml")) { return; } - // Проверяем, что файл принадлежит проекту if (!localFilePath.startsWith(localRoot)) { return; } - // Относительный путь и удалённый путь String relativePath = localFilePath.substring(localRoot.length()); if (relativePath.startsWith("/")) { relativePath = relativePath.substring(1); } String remoteFilePath = server.remoteProjectPath + "/" + relativePath; - // Отправляем файл в отдельном потоке через общее SFTP-соединение uploadExecutor.submit(() -> { try { ChannelSftp sftp = SftpSessionManager.getInstance(project).getChannel(server); @@ -149,6 +141,84 @@ public class SshSyncStartupActivity implements StartupActivity { }); } + private void deleteRemoteFile(VirtualFile localFile, Project project) { + String projectPath = project.getBasePath(); + if (projectPath == null) return; + + String serverId = ProjectServerMapping.getInstance().getServerIdForProject(projectPath); + if (serverId == null) return; + + Optional maybeServer = SshServerManager.getInstance().findServer(serverId); + if (maybeServer.isEmpty()) return; + SshServer server = maybeServer.get(); + + String localRoot = SshServerManager.getLocalProjectPath(server); + + String localFilePath = localFile.getPath(); + + if (localFilePath.contains("/.idea/") || localFile.getName().equals("remote.iml")) { + return; + } + + if (!localFilePath.startsWith(localRoot)) { + return; + } + + String relativePath = localFilePath.substring(localRoot.length()); + if (relativePath.startsWith("/")) { + relativePath = relativePath.substring(1); + } + String remoteFilePath = server.remoteProjectPath + "/" + relativePath; + + uploadExecutor.submit(() -> { + try { + ChannelSftp sftp = SftpSessionManager.getInstance(project).getChannel(server); + deleteRemotePath(sftp, remoteFilePath, localFile.isDirectory()); + } catch (Exception e) { + ApplicationManager.getApplication().invokeLater(() -> + com.intellij.openapi.ui.Messages.showErrorDialog( + "Delete error " + localFile.getName() + ": " + e.getMessage(), + "SFTP Error" + ) + ); + } + }); + } + + private void deleteRemotePath(ChannelSftp sftp, String remotePath, boolean isDirectory) throws Exception { + if (isDirectory) { + try { + Vector entries = sftp.ls(remotePath); + for (ChannelSftp.LsEntry entry : entries) { + String name = entry.getFilename(); + if (name.equals(".") || name.equals("..")) continue; + String childPath = remotePath + "/" + name; + if (entry.getAttrs().isDir()) { + deleteRemotePath(sftp, childPath, true); + } else { + try { + sftp.rm(childPath); + } catch (SftpException e) { + if (e.id != ChannelSftp.SSH_FX_NO_SUCH_FILE) throw e; + } + } + } + } catch (SftpException e) { + if (e.id != ChannelSftp.SSH_FX_NO_SUCH_FILE) throw e; + } + sftp.rmdir(remotePath); + } else { + try { + sftp.rm(remotePath); + } catch (SftpException e) { + if (e.id != ChannelSftp.SSH_FX_NO_SUCH_FILE) { + throw e; + } + // иначе файл уже удалён – пропускаем + } + } + } + private void createRemoteDirs(ChannelSftp sftp, String dir) throws SftpException { if (dir.isEmpty() || dir.equals("/")) return; String[] parts = dir.split("/"); @@ -167,8 +237,4 @@ public class SshSyncStartupActivity implements StartupActivity { } } } - - private String sanitize(String input) { - return input.replaceAll("[^a-zA-Z0-9.-]", "_"); - } } \ No newline at end of file diff --git a/src/main/java/com/example/plugin/SwitchServerAction.java b/src/main/java/com/example/plugin/SwitchServerAction.java index 95f8a0e..ee7dbf7 100644 --- a/src/main/java/com/example/plugin/SwitchServerAction.java +++ b/src/main/java/com/example/plugin/SwitchServerAction.java @@ -48,11 +48,8 @@ public class SwitchServerAction extends AnAction { private void switchToServer(SshServer server, Project currentProject) { SshServerManager.getInstance().setCurrentServerId(server.id); - String localPath = Paths.get(System.getProperty("user.home"), "ssh-remote-projects", - sanitize(server.host), sanitize(server.remoteProjectPath)) - .toString().replace('\\', '/'); + String localPath = SshServerManager.getLocalProjectPath(server); - // Проверяем, открыт ли уже проект с этим путём for (Project openProject : ProjectManager.getInstance().getOpenProjects()) { if (openProject.getBasePath() != null && openProject.getBasePath().equals(localPath)) { ApplicationManager.getApplication().invokeLater(() -> @@ -61,15 +58,10 @@ public class SwitchServerAction extends AnAction { } } - // Закрываем текущий проект и открываем новый if (currentProject != null && !currentProject.isDisposed()) { ProjectManager.getInstance().closeProject(currentProject); } ConnectToRemoteAction.openProjectForServer(server); } - - private String sanitize(String input) { - return input.replaceAll("[^a-zA-Z0-9.-]", "_"); - } } \ No newline at end of file