Initial sync

This commit is contained in:
MrLetsplay 2023-12-11 13:28:39 +01:00
parent 2541fdbd58
commit 050b09d1aa
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
7 changed files with 225 additions and 51 deletions

View File

@ -10,8 +10,9 @@ Require-Bundle: org.eclipse.ui,
org.eclipse.ui.editors;bundle-version="3.17.0", org.eclipse.ui.editors;bundle-version="3.17.0",
org.eclipse.text, org.eclipse.text,
Java-WebSocket;bundle-version="1.5.4", 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 Bundle-RequiredExecutionEnvironment: JavaSE-17
Automatic-Module-Name: ShareClient Automatic-Module-Name: ShareClient
Bundle-ActivationPolicy: lazy Bundle-ActivationPolicy: lazy

BIN
icons/content-copy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

BIN
icons/content-copy@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

View File

@ -4,13 +4,20 @@ import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.HashMap;
import java.util.List; import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.UUID; import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.plugin.AbstractUIPlugin; import org.eclipse.ui.plugin.AbstractUIPlugin;
@ -18,6 +25,7 @@ import org.osgi.framework.BundleContext;
import me.mrletsplay.shareclient.util.Peer; import me.mrletsplay.shareclient.util.Peer;
import me.mrletsplay.shareclient.util.ProjectRelativePath; import me.mrletsplay.shareclient.util.ProjectRelativePath;
import me.mrletsplay.shareclient.util.ShareSession;
import me.mrletsplay.shareclient.views.ShareView; import me.mrletsplay.shareclient.views.ShareView;
import me.mrletsplay.shareclientcore.connection.ConnectionException; import me.mrletsplay.shareclientcore.connection.ConnectionException;
import me.mrletsplay.shareclientcore.connection.MessageListener; import me.mrletsplay.shareclientcore.connection.MessageListener;
@ -26,6 +34,7 @@ import me.mrletsplay.shareclientcore.connection.WebSocketConnection;
import me.mrletsplay.shareclientcore.connection.message.FullSyncMessage; import me.mrletsplay.shareclientcore.connection.message.FullSyncMessage;
import me.mrletsplay.shareclientcore.connection.message.Message; import me.mrletsplay.shareclientcore.connection.message.Message;
import me.mrletsplay.shareclientcore.connection.message.PeerJoinMessage; import me.mrletsplay.shareclientcore.connection.message.PeerJoinMessage;
import me.mrletsplay.shareclientcore.connection.message.RequestFullSyncMessage;
/** /**
* The activator class controls the plug-in life cycle * The activator class controls the plug-in life cycle
@ -38,15 +47,11 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener {
// The shared instance // The shared instance
private static ShareClient plugin; private static ShareClient plugin;
private RemoteConnection activeConnection;
private ShareView view; private ShareView view;
private List<Peer> peers; private ShareSession activeSession;
/**
* The constructor
*/
public ShareClient() { public ShareClient() {
this.peers = new ArrayList<>();
} }
@Override @Override
@ -55,6 +60,7 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener {
plugin = this; plugin = this;
getPreferenceStore().setDefault(ShareClientPreferences.SERVER_URI, "ws://localhost:5473"); getPreferenceStore().setDefault(ShareClientPreferences.SERVER_URI, "ws://localhost:5473");
getPreferenceStore().setDefault(ShareClientPreferences.SHOW_CURSORS, true);
// new ShareWSClient(URI.create("ws://localhost:5473")).connect(); // new ShareWSClient(URI.create("ws://localhost:5473")).connect();
} }
@ -73,52 +79,56 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener {
return plugin; return plugin;
} }
private void updateView() { public void updateView() {
if (view == null) return; if (view == null) return;
Display.getDefault().asyncExec(() -> view.getViewer().setInput(peers.stream().map(p -> p.name()).toArray(String[]::new))); 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); String serverURI = getPreferenceStore().getString(ShareClientPreferences.SERVER_URI);
if (serverURI == null) return null; if (serverURI == null) return null;
String username = getPreferenceStore().getString(ShareClientPreferences.USERNAME); 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); WebSocketConnection connection = new WebSocketConnection(URI.create(serverURI), username);
try { try {
connection.connect(sessionID); // TODO: connect to existing session connection.connect(sessionID); // TODO: connect to existing session
} catch (ConnectionException e) { } catch (ConnectionException e) {
MessageDialog.openInformation( e.printStackTrace();
null, MessageDialog.openError(null, "Share Client", "Failed to connect to server: " + e);
"Share Client",
"Failed to connect to server: " + e);
return null; return null;
} }
connection.addListener(this); connection.addListener(this);
return activeConnection = connection; updateView();
return activeSession = new ShareSession(connection, sessionID);
} }
public RemoteConnection getOrOpenConnection() { public ShareSession getOrStartSession() {
if(activeConnection == null) { if (activeSession == null) {
openConnection(UUID.randomUUID().toString()); startSession(UUID.randomUUID().toString());
} }
return activeConnection; return activeSession;
} }
public void closeConnection() { public void closeConnection() {
if(activeConnection == null) return; if (activeSession == null) return;
activeConnection.disconnect(); ShareSession session = activeSession;
activeConnection = null; activeSession = null;
peers.clear(); session.stop();
updateView(); updateView();
} }
public RemoteConnection getActiveConnection() { public ShareSession getActiveSession() {
return activeConnection; return activeSession;
} }
public void setView(ShareView view) { public void setView(ShareView view) {
@ -133,7 +143,7 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener {
public void onMessage(Message message) { public void onMessage(Message message) {
System.out.println("Got: " + message); System.out.println("Got: " + message);
if (message instanceof PeerJoinMessage join) { if (message instanceof PeerJoinMessage join) {
peers.add(new Peer(join.peerName(), join.peerSiteID())); activeSession.getPeers().add(new Peer(join.peerName(), join.peerSiteID()));
updateView(); updateView();
} }
@ -143,11 +153,27 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener {
try { try {
path = ProjectRelativePath.of(sync.documentPath()); path = ProjectRelativePath.of(sync.documentPath());
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
e.printStackTrace();
return; return;
} }
IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(path.projectName()); IWorkspace workspace = ResourcesPlugin.getWorkspace();
if(project == null) return; // TODO: create project 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()); Path filePath = project.getLocation().toPath().resolve(path.relativePath());
try { try {
@ -157,10 +183,76 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener {
} }
Files.write(filePath, sync.content()); Files.write(filePath, sync.content());
} catch (IOException e) { project.refreshLocal(IResource.DEPTH_INFINITE, null);
// TODO: handle exception } catch (IOException | CoreException e) {
e.printStackTrace();
MessageDialog.openError(null, "Share Client", "Failed to update file: " + e.toString());
return;
}
}
if (message instanceof RequestFullSyncMessage req) {
Map<ProjectRelativePath, Path> 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<ProjectRelativePath, Path> 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;
}
}
} }

View File

@ -12,7 +12,7 @@ import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.handlers.HandlerUtil; import org.eclipse.ui.handlers.HandlerUtil;
import me.mrletsplay.shareclient.ShareClient; import me.mrletsplay.shareclient.ShareClient;
import me.mrletsplay.shareclientcore.connection.RemoteConnection; import me.mrletsplay.shareclient.util.ShareSession;
public class ShareProjectHandler extends AbstractHandler { public class ShareProjectHandler extends AbstractHandler {
@ -32,10 +32,12 @@ public class ShareProjectHandler extends AbstractHandler {
IPath path = project.getLocation(); IPath path = project.getLocation();
if(path == null) return null; if(path == null) return null;
RemoteConnection con = ShareClient.getDefault().getOrOpenConnection(); ShareSession session = ShareClient.getDefault().getOrStartSession();
if(con == null) return null; 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(); // IEditorPart editor = window.getActivePage().getActiveEditor();
// if(!(editor instanceof ITextEditor)) return null; // if(!(editor instanceof ITextEditor)) return null;

View File

@ -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<IProject> sharedProjects;
private List<Peer> 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<IProject> getSharedProjects() {
return sharedProjects;
}
public List<Peer> 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();
}
}

View File

@ -18,8 +18,12 @@ import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.swt.SWT; 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.graphics.Image;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IActionBars; import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IViewSite; import org.eclipse.ui.IViewSite;
import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbench;
@ -28,6 +32,7 @@ import org.eclipse.ui.dialogs.PreferencesUtil;
import org.eclipse.ui.part.ViewPart; import org.eclipse.ui.part.ViewPart;
import me.mrletsplay.shareclient.ShareClient; import me.mrletsplay.shareclient.ShareClient;
import me.mrletsplay.shareclient.util.ShareSession;
import me.mrletsplay.shareclientcore.connection.ConnectionException; import me.mrletsplay.shareclientcore.connection.ConnectionException;
import me.mrletsplay.shareclientcore.connection.RemoteConnection; import me.mrletsplay.shareclientcore.connection.RemoteConnection;
import me.mrletsplay.shareclientcore.connection.message.RequestFullSyncMessage; import me.mrletsplay.shareclientcore.connection.message.RequestFullSyncMessage;
@ -90,7 +95,7 @@ public class ShareView extends ViewPart {
updateActionBars(); updateActionBars();
} }
private void updateActionBars() { public void updateActionBars() {
IActionBars bars = getViewSite().getActionBars(); IActionBars bars = getViewSite().getActionBars();
IToolBarManager toolbars = bars.getToolBarManager(); IToolBarManager toolbars = bars.getToolBarManager();
@ -106,11 +111,13 @@ public class ShareView extends ViewPart {
input.setBlockOnOpen(true); input.setBlockOnOpen(true);
if(input.open() != InputDialog.OK) return; if(input.open() != InputDialog.OK) return;
RemoteConnection connection = ShareClient.getDefault().getActiveConnection(); ShareSession session = ShareClient.getDefault().getActiveSession();
if(connection != null) connection.disconnect(); if(session != null) session.stop();
connection = ShareClient.getDefault().openConnection(input.getValue()); session = ShareClient.getDefault().startSession(input.getValue());
if(connection == null) return; if(session == null) return;
RemoteConnection connection = session.getConnection();
try { try {
connection.send(new RequestFullSyncMessage(connection.getSiteID(), null)); 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")) { 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")) { 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); toolbars.add(showSettings);
bars.updateActionBars(); bars.updateActionBars();