From 050b09d1aaf6a0c7804e1d670356f289c641d284 Mon Sep 17 00:00:00 2001 From: MrLetsplay Date: Mon, 11 Dec 2023 13:28:39 +0100 Subject: [PATCH] Initial sync --- META-INF/MANIFEST.MF | 5 +- icons/content-copy.png | Bin 0 -> 317 bytes icons/content-copy@2x.png | Bin 0 -> 452 bytes .../mrletsplay/shareclient/ShareClient.java | 168 ++++++++++++++---- .../handlers/ShareProjectHandler.java | 10 +- .../shareclient/util/ShareSession.java | 55 ++++++ .../shareclient/views/ShareView.java | 38 +++- 7 files changed, 225 insertions(+), 51 deletions(-) create mode 100644 icons/content-copy.png create mode 100644 icons/content-copy@2x.png create mode 100644 src/main/java/me/mrletsplay/shareclient/util/ShareSession.java diff --git a/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF index 4d3842e..1df1236 100644 --- a/META-INF/MANIFEST.MF +++ b/META-INF/MANIFEST.MF @@ -10,8 +10,9 @@ Require-Bundle: org.eclipse.ui, org.eclipse.ui.editors;bundle-version="3.17.0", org.eclipse.text, Java-WebSocket;bundle-version="1.5.4", - wrapped.me.mrletsplay.ShareClient-Core;bundle-version="1.0.0", - org.eclipse.core.resources;bundle-version="3.19.100" + org.eclipse.core.resources;bundle-version="3.19.100", + wrapped.me.mrletsplay.ShareLib;bundle-version="1.0.0", + org.eclipse.ui.ide;bundle-version="3.22.0" Bundle-RequiredExecutionEnvironment: JavaSE-17 Automatic-Module-Name: ShareClient Bundle-ActivationPolicy: lazy diff --git a/icons/content-copy.png b/icons/content-copy.png new file mode 100644 index 0000000000000000000000000000000000000000..eb43718203b3ed619ce03bc0be44d723a9b4b2fc GIT binary patch literal 317 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#X#yh2s3WX6WN&PEET=#;06V~9oX+sPY!nH>dO@5fu0 z@bhIdb@t3*Xs-_EV3gg_y8G-0wJ#11oB<8lJG#H^6OLBcoZhJLB;EMd|1>vi<=ao@ zh(ETt=XZHalAB^nZO_NAHv3s5+8zp4OfY%Qe!@Xk;U(7z!((SvoqSJ!cJX?7?wi}C zMQIyibL_X~{^k+T;uqSmFQ7zM^K2QznwUoZVWsciWVZ+82u_75J5Z`;pw zrR>k{Uz-K(UdLWq_x|^)hZP0+n;QfU`D#9uT5*plAZP!aCguej9g0gP%x(wziow&> K&t;ucLK6T?On|Nc literal 0 HcmV?d00001 diff --git a/icons/content-copy@2x.png b/icons/content-copy@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..15c4e9abece0876697f3a29d9965568b07f1176e GIT binary patch literal 452 zcmV;#0XzPQP) peers; + private ShareSession activeSession; - /** - * The constructor - */ public ShareClient() { - this.peers = new ArrayList<>(); + } @Override @@ -55,6 +60,7 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener { plugin = this; getPreferenceStore().setDefault(ShareClientPreferences.SERVER_URI, "ws://localhost:5473"); + getPreferenceStore().setDefault(ShareClientPreferences.SHOW_CURSORS, true); // new ShareWSClient(URI.create("ws://localhost:5473")).connect(); } @@ -73,52 +79,56 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener { return plugin; } - private void updateView() { - if(view == null) return; - Display.getDefault().asyncExec(() -> view.getViewer().setInput(peers.stream().map(p -> p.name()).toArray(String[]::new))); + public void updateView() { + if (view == null) return; + Display.getDefault().asyncExec(() -> view.updateActionBars()); + + if (activeSession == null) return; + String[] peerNames = activeSession.getPeers().stream().map(p -> p.name()).toArray(String[]::new); + Display.getDefault().asyncExec(() -> view.getViewer().setInput(peerNames)); } - public RemoteConnection openConnection(String sessionID) { + public ShareSession startSession(String sessionID) { String serverURI = getPreferenceStore().getString(ShareClientPreferences.SERVER_URI); - if(serverURI == null) return null; + if (serverURI == null) return null; String username = getPreferenceStore().getString(ShareClientPreferences.USERNAME); - if(username == null || username.isBlank()) username = "user" + new Random().nextInt(1000); + if (username == null || username.isBlank()) + username = "user" + new Random().nextInt(1000); WebSocketConnection connection = new WebSocketConnection(URI.create(serverURI), username); try { connection.connect(sessionID); // TODO: connect to existing session } catch (ConnectionException e) { - MessageDialog.openInformation( - null, - "Share Client", - "Failed to connect to server: " + e); + e.printStackTrace(); + MessageDialog.openError(null, "Share Client", "Failed to connect to server: " + e); return null; } connection.addListener(this); - return activeConnection = connection; + updateView(); + return activeSession = new ShareSession(connection, sessionID); } - public RemoteConnection getOrOpenConnection() { - if(activeConnection == null) { - openConnection(UUID.randomUUID().toString()); + public ShareSession getOrStartSession() { + if (activeSession == null) { + startSession(UUID.randomUUID().toString()); } - return activeConnection; + return activeSession; } public void closeConnection() { - if(activeConnection == null) return; - activeConnection.disconnect(); - activeConnection = null; - peers.clear(); + if (activeSession == null) return; + ShareSession session = activeSession; + activeSession = null; + session.stop(); updateView(); } - public RemoteConnection getActiveConnection() { - return activeConnection; + public ShareSession getActiveSession() { + return activeSession; } public void setView(ShareView view) { @@ -132,35 +142,117 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener { @Override public void onMessage(Message message) { System.out.println("Got: " + message); - if(message instanceof PeerJoinMessage join) { - peers.add(new Peer(join.peerName(), join.peerSiteID())); + if (message instanceof PeerJoinMessage join) { + activeSession.getPeers().add(new Peer(join.peerName(), join.peerSiteID())); updateView(); } - if(message instanceof FullSyncMessage sync) { + if (message instanceof FullSyncMessage sync) { // TODO: handle FULL_SYNC ProjectRelativePath path; try { path = ProjectRelativePath.of(sync.documentPath()); - }catch(IllegalArgumentException e) { + } catch (IllegalArgumentException e) { + e.printStackTrace(); return; } - IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(path.projectName()); - if(project == null) return; // TODO: create project + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IWorkspaceRoot workspaceRoot = workspace.getRoot(); + IProject project = workspaceRoot.getProject(path.projectName()); + if(project == null) return; + // TODO: make sure to not overwrite existing non-shared projects + if (!project.exists()) { + IProjectDescription description = workspace.newProjectDescription(path.projectName()); + try { + project.create(description, null); + project.open(null); + } catch (CoreException e) { + e.printStackTrace(); + MessageDialog.openError(null, "Share Client", "Failed to create project: " + e.toString()); + return; + } + System.out.println("Created project " + project); + } Path filePath = project.getLocation().toPath().resolve(path.relativePath()); try { - if(!Files.exists(filePath)) { + if (!Files.exists(filePath)) { Files.createDirectories(filePath.getParent()); Files.createFile(filePath); } Files.write(filePath, sync.content()); - } catch (IOException e) { - // TODO: handle exception + project.refreshLocal(IResource.DEPTH_INFINITE, null); + } catch (IOException | CoreException e) { + e.printStackTrace(); + MessageDialog.openError(null, "Share Client", "Failed to update file: " + e.toString()); + return; + } + } + + if (message instanceof RequestFullSyncMessage req) { + Map paths = new HashMap<>(); + if (req.documentPath() == null) { + // Sync entire (shared) workspace + for (IProject project : activeSession.getSharedProjects()) { + var files = getProjectFiles(project); + if (files == null) return; + paths.putAll(files); + } + } else { + ProjectRelativePath path; + try { + path = ProjectRelativePath.of(req.documentPath()); + } catch (IllegalArgumentException e) { + return; + } + + IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(path.projectName()); + if (project == null || !project.exists()) return; + if (!activeSession.getSharedProjects().contains(project)) return; + + if (!path.relativePath().isEmpty()) { + Path projectLocation = project.getLocation().toPath(); + Path filePath = projectLocation.resolve(path.relativePath()).normalize(); + if (!filePath.startsWith(projectLocation)) return; + paths.put(new ProjectRelativePath(project.getName(), path.relativePath()), filePath); + } else { + // Sync entire project + var files = getProjectFiles(project); + if (files == null) return; + paths.putAll(files); + } + } + + RemoteConnection connection = activeSession.getConnection(); + for (var en : paths.entrySet()) { + if (!Files.isRegularFile(en.getValue())) + continue; + try { + byte[] bytes = Files.readAllBytes(en.getValue()); + connection.send(new FullSyncMessage(req.siteID(), en.getKey().toString(), bytes)); + } catch (IOException | ConnectionException e) { + e.printStackTrace(); + MessageDialog.openError(null, "Share Client", "Failed to send file contents: " + e.toString()); + return; + } } } } + private Map getProjectFiles(IProject project) { + try { + Path projectLocation = project.getLocation().toPath(); + return Files.walk(projectLocation) + .collect(Collectors.toMap( + p -> new ProjectRelativePath(project.getName(), projectLocation.relativize(p).toString()), + Function.identity())); + } catch (IOException e) { + e.printStackTrace(); + MessageDialog.openError(null, "Share Client", "Failed to collect project files: " + e.toString()); + return null; + } + } + } diff --git a/src/main/java/me/mrletsplay/shareclient/handlers/ShareProjectHandler.java b/src/main/java/me/mrletsplay/shareclient/handlers/ShareProjectHandler.java index 50d55d9..dcfc9a9 100644 --- a/src/main/java/me/mrletsplay/shareclient/handlers/ShareProjectHandler.java +++ b/src/main/java/me/mrletsplay/shareclient/handlers/ShareProjectHandler.java @@ -12,7 +12,7 @@ import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.handlers.HandlerUtil; import me.mrletsplay.shareclient.ShareClient; -import me.mrletsplay.shareclientcore.connection.RemoteConnection; +import me.mrletsplay.shareclient.util.ShareSession; public class ShareProjectHandler extends AbstractHandler { @@ -32,10 +32,12 @@ public class ShareProjectHandler extends AbstractHandler { IPath path = project.getLocation(); if(path == null) return null; - RemoteConnection con = ShareClient.getDefault().getOrOpenConnection(); - if(con == null) return null; + ShareSession session = ShareClient.getDefault().getOrStartSession(); + if(session == null) return null; - // TODO: handle case when adding project to existing session + session.getSharedProjects().add(project); + + // TODO: broadcast FULL_SYNC for newly added project // IEditorPart editor = window.getActivePage().getActiveEditor(); // if(!(editor instanceof ITextEditor)) return null; diff --git a/src/main/java/me/mrletsplay/shareclient/util/ShareSession.java b/src/main/java/me/mrletsplay/shareclient/util/ShareSession.java new file mode 100644 index 0000000..2efb02f --- /dev/null +++ b/src/main/java/me/mrletsplay/shareclient/util/ShareSession.java @@ -0,0 +1,55 @@ +package me.mrletsplay.shareclient.util; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.eclipse.core.resources.IProject; + +import me.mrletsplay.shareclient.ShareClient; +import me.mrletsplay.shareclientcore.connection.RemoteConnection; + +public class ShareSession { + + private RemoteConnection connection; + private String sessionID; + private List sharedProjects; + private List peers; + + public ShareSession(RemoteConnection connection, String sessionID) { + this.connection = connection; + this.sessionID = sessionID; + this.sharedProjects = new ArrayList<>(); + this.peers = new ArrayList<>(); + } + + public RemoteConnection getConnection() { + return connection; + } + + public String getSessionID() { + return sessionID; + } + + public List getSharedProjects() { + return sharedProjects; + } + + public List getPeers() { + return peers; + } + + public boolean hasRemotePeer() { + return peers.stream().anyMatch(p -> p.siteID() != connection.getSiteID()); + } + + public boolean isHost() { + return peers.stream().max(Comparator.comparingInt(p -> p.siteID())).get().siteID() == connection.getSiteID(); + } + + public void stop() { + connection.disconnect(); + ShareClient.getDefault().updateView(); + } + +} diff --git a/src/main/java/me/mrletsplay/shareclient/views/ShareView.java b/src/main/java/me/mrletsplay/shareclient/views/ShareView.java index 92c2c47..1ad49d5 100644 --- a/src/main/java/me/mrletsplay/shareclient/views/ShareView.java +++ b/src/main/java/me/mrletsplay/shareclient/views/ShareView.java @@ -18,8 +18,12 @@ import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.swt.SWT; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IActionBars; import org.eclipse.ui.IViewSite; import org.eclipse.ui.IWorkbench; @@ -28,6 +32,7 @@ import org.eclipse.ui.dialogs.PreferencesUtil; import org.eclipse.ui.part.ViewPart; import me.mrletsplay.shareclient.ShareClient; +import me.mrletsplay.shareclient.util.ShareSession; import me.mrletsplay.shareclientcore.connection.ConnectionException; import me.mrletsplay.shareclientcore.connection.RemoteConnection; import me.mrletsplay.shareclientcore.connection.message.RequestFullSyncMessage; @@ -90,7 +95,7 @@ public class ShareView extends ViewPart { updateActionBars(); } - private void updateActionBars() { + public void updateActionBars() { IActionBars bars = getViewSite().getActionBars(); IToolBarManager toolbars = bars.getToolBarManager(); @@ -106,11 +111,13 @@ public class ShareView extends ViewPart { input.setBlockOnOpen(true); if(input.open() != InputDialog.OK) return; - RemoteConnection connection = ShareClient.getDefault().getActiveConnection(); - if(connection != null) connection.disconnect(); + ShareSession session = ShareClient.getDefault().getActiveSession(); + if(session != null) session.stop(); - connection = ShareClient.getDefault().openConnection(input.getValue()); - if(connection == null) return; + session = ShareClient.getDefault().startSession(input.getValue()); + if(session == null) return; + + RemoteConnection connection = session.getConnection(); try { connection.send(new RequestFullSyncMessage(connection.getSiteID(), null)); @@ -122,7 +129,17 @@ public class ShareView extends ViewPart { } }; - if(ShareClient.getDefault().getActiveConnection() == null) toolbars.add(joinSession); + + Action copySessionID = new Action("Copy session ID", ImageDescriptor.createFromFile(ShareView.class, "/icons/content-copy.png")) { + + @Override + public void run() { + Clipboard clipboard = new Clipboard(Display.getDefault()); + clipboard.setContents(new String[] {ShareClient.getDefault().getActiveSession().getSessionID()}, new Transfer[] {TextTransfer.getInstance()}); + } + + }; + Action leaveSession = new Action("Leave session", ImageDescriptor.createFromFile(ShareView.class, "/icons/stop.png")) { @@ -133,7 +150,6 @@ public class ShareView extends ViewPart { } }; - if(ShareClient.getDefault().getActiveConnection() != null) toolbars.add(leaveSession); Action showSettings = new Action("Settings", ImageDescriptor.createFromFile(ShareView.class, "/icons/cog.png")) { @@ -146,6 +162,14 @@ public class ShareView extends ViewPart { } }; + + if(ShareClient.getDefault().getActiveSession() == null) { + toolbars.add(joinSession); + }else { + toolbars.add(copySessionID); + toolbars.add(leaveSession); + } + toolbars.add(showSettings); bars.updateActionBars();