package com.example.plugin; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.project.ProjectManagerListener; import com.intellij.openapi.startup.StartupActivity; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.vfs.*; import com.jcraft.jsch.*; import org.jetbrains.annotations.NotNull; import java.nio.file.Paths; import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @SuppressWarnings("deprecation") public class SshSyncStartupActivity implements StartupActivity { private static final ExecutorService uploadExecutor = Executors.newSingleThreadExecutor(); @Override public void runActivity(@NotNull Project project) { // При закрытии проекта разрываем SFTP-подключение project.getMessageBus().connect().subscribe(ProjectManager.TOPIC, new ProjectManagerListener() { @Override public void projectClosed(@NotNull Project closedProject) { if (closedProject.equals(project)) { SftpSessionManager.getInstance(project).disconnect(); } } }); // Слушатель изменений файлов для синхронизации VirtualFile baseDir = project.getBaseDir(); if (baseDir != null) { VirtualFileManager.getInstance().addVirtualFileListener(new VirtualFileListener() { @Override public void contentsChanged(@NotNull VirtualFileEvent event) { syncFile(event.getFile(), project); } @Override public void fileCreated(@NotNull VirtualFileEvent event) { syncFile(event.getFile(), project); } private void syncFile(VirtualFile file, Project project) { if (!file.isDirectory()) { uploadFile(file, project); } } }, project); } // === Автосинхронизация при открытии проекта === String projectPath = project.getBasePath(); if (projectPath != null) { String serverId = ProjectServerMapping.getInstance().getServerIdForProject(projectPath); if (serverId != null) { Optional 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('\\', '/'); ConnectToRemoteAction.syncProject(server, localRoot, indicator); // Обновляем виртуальную файловую систему, чтобы IDE увидела изменения ApplicationManager.getApplication().invokeLater(() -> { VirtualFile rootDir = LocalFileSystem.getInstance().findFileByPath(localRoot); if (rootDir != null) { VfsUtil.markDirtyAndRefresh(true, true, true, rootDir); } }); } catch (Exception e) { ApplicationManager.getApplication().invokeLater(() -> Messages.showErrorDialog("Sync failed: " + e.getMessage(), "Sync Error")); } } }); }); } } } } private void uploadFile(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 = Paths.get(System.getProperty("user.home"), "ssh-remote-projects", sanitize(server.host), sanitize(server.remoteProjectPath)) .toString().replace('\\', '/'); 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); String remoteParent = remoteFilePath.substring(0, remoteFilePath.lastIndexOf('/')); createRemoteDirs(sftp, remoteParent); sftp.put(localFilePath, remoteFilePath); } catch (Exception e) { ApplicationManager.getApplication().invokeLater(() -> com.intellij.openapi.ui.Messages.showErrorDialog( "Sync error " + localFile.getName() + ": " + e.getMessage(), "SFTP Error" ) ); } }); } private void createRemoteDirs(ChannelSftp sftp, String dir) throws SftpException { if (dir.isEmpty() || dir.equals("/")) return; String[] parts = dir.split("/"); StringBuilder path = new StringBuilder(); for (String part : parts) { if (part.isEmpty()) continue; path.append("/").append(part); try { sftp.stat(path.toString()); } catch (SftpException e) { if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) { sftp.mkdir(path.toString()); } else { throw e; } } } } private String sanitize(String input) { return input.replaceAll("[^a-zA-Z0-9.-]", "_"); } }