From 5b7c6a857af27d2c674cab14a2316145836e46ca Mon Sep 17 00:00:00 2001 From: Nekojimi Date: Thu, 18 Sep 2025 11:01:12 +0100 Subject: [PATCH] First version of data persistence architecture --- pom.xml | 24 ++ .../java/moe/nekojimi/friendcloud/Main.java | 100 ++++-- .../java/moe/nekojimi/friendcloud/Model.java | 167 --------- .../friendcloud/ObjectChangeRecord.java | 150 ++++---- .../friendcloud/ObjectChangeTransaction.java | 6 +- .../java/moe/nekojimi/friendcloud/Util.java | 1 + .../friendcloud/filesystem/FUSEAccess.java | 8 +- .../friendcloud/network/PeerConnection.java | 17 +- .../network/requests/ObjectChangeRequest.java | 5 +- .../network/requests/ObjectListRequest.java | 6 +- .../friendcloud/objects/NetworkFSNode.java | 22 +- .../friendcloud/objects/NetworkFile.java | 36 +- .../friendcloud/objects/NetworkFolder.java | 14 +- .../friendcloud/objects/NetworkObject.java | 20 +- .../nekojimi/friendcloud/objects/Peer.java | 26 +- .../friendcloud/objects/PeerFileState.java | 14 +- .../friendcloud/storage/CachingDataStore.java | 92 +++++ .../friendcloud/storage/DataStore.java | 89 +++++ .../friendcloud/storage/LocalData.java | 40 +++ .../nekojimi/friendcloud/storage/Model.java | 184 ++++++++++ .../friendcloud/storage/Storable.java | 29 ++ .../storage/StupidJSONFileStore.java | 334 ++++++++++++++++++ .../friendcloud/tasks/JoinNetworkTask.java | 5 +- .../tasks/PropagateMessageTask.java | 4 +- .../tasks/SyncWithNetworkTask.java | 5 +- 25 files changed, 1078 insertions(+), 320 deletions(-) delete mode 100644 src/main/java/moe/nekojimi/friendcloud/Model.java create mode 100644 src/main/java/moe/nekojimi/friendcloud/storage/CachingDataStore.java create mode 100644 src/main/java/moe/nekojimi/friendcloud/storage/DataStore.java create mode 100644 src/main/java/moe/nekojimi/friendcloud/storage/LocalData.java create mode 100644 src/main/java/moe/nekojimi/friendcloud/storage/Model.java create mode 100644 src/main/java/moe/nekojimi/friendcloud/storage/Storable.java create mode 100644 src/main/java/moe/nekojimi/friendcloud/storage/StupidJSONFileStore.java diff --git a/pom.xml b/pom.xml index 8281a29..052e69d 100644 --- a/pom.xml +++ b/pom.xml @@ -69,6 +69,30 @@ gethostname4j 1.0.0 + + es.blackleg + jlibnotify + 1.1.0 + + + com.github.hypfvieh + dbus-java-core + 5.1.0-SNAPSHOT + + + + com.github.hypfvieh + dbus-java-transport-native-unixsocket + 5.1.0-SNAPSHOT + + + + + com.github.hypfvieh + dbus-java-transport-tcp + 5.1.0-SNAPSHOT + + diff --git a/src/main/java/moe/nekojimi/friendcloud/Main.java b/src/main/java/moe/nekojimi/friendcloud/Main.java index bf99255..0bcbce1 100644 --- a/src/main/java/moe/nekojimi/friendcloud/Main.java +++ b/src/main/java/moe/nekojimi/friendcloud/Main.java @@ -11,6 +11,12 @@ import com.offbynull.portmapper.gateways.process.ProcessGateway; import com.offbynull.portmapper.mapper.MappedPort; import com.offbynull.portmapper.mapper.PortMapper; import com.offbynull.portmapper.mapper.PortType; +import es.blackleg.jlibnotify.JLibnotify; +import es.blackleg.jlibnotify.JLibnotifyNotification; +import es.blackleg.jlibnotify.core.DefaultJLibnotify; +import es.blackleg.jlibnotify.core.DefaultJLibnotifyLoader; +import es.blackleg.jlibnotify.exception.JLibnotifyInitException; +import es.blackleg.jlibnotify.exception.JLibnotifyLoadException; import jnr.ffi.Platform; import moe.nekojimi.friendcloud.filesystem.FUSEAccess; import moe.nekojimi.friendcloud.network.PeerConnection; @@ -18,8 +24,10 @@ import moe.nekojimi.friendcloud.network.requests.ObjectListRequest; import moe.nekojimi.friendcloud.objects.NetworkFile; import moe.nekojimi.friendcloud.objects.NetworkObject; import moe.nekojimi.friendcloud.objects.Peer; -import moe.nekojimi.friendcloud.objects.PeerFileState; import moe.nekojimi.friendcloud.protos.ObjectStatements; +import moe.nekojimi.friendcloud.storage.Model; +import moe.nekojimi.friendcloud.storage.StupidJSONFileStore; +import moe.nekojimi.friendcloud.tasks.JoinNetworkTask; import org.slf4j.simple.SimpleLogger; import java.io.File; @@ -28,6 +36,7 @@ import java.net.*; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.*; @@ -37,7 +46,7 @@ public class Main private static Main instance; @Parameter(names="-share") - private List sharedFiles = new ArrayList<>(); + private List sharedFilePaths = new ArrayList<>(); @Parameter(names="-known-peer") private List knownPeers = new ArrayList<>(); @@ -48,13 +57,15 @@ public class Main @Parameter(names="-no-upnp") private boolean noUpnp = false; + @Parameter(names="-create-network") + private boolean createNetwork = false; + // @Parameter(names="-file") private ConnectionManager connectionManager; - private final ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(16); - private final FUSEAccess fuseAccess = new FUSEAccess(); + private final Model model = new Model(new StupidJSONFileStore(new File("storage"))); public static void main(String[] args) { @@ -66,7 +77,7 @@ public class Main try { instance.run(); - } catch (IOException | InterruptedException e) + } catch (IOException | InterruptedException | JLibnotifyLoadException | JLibnotifyInitException e) { e.printStackTrace(System.err); try @@ -81,7 +92,7 @@ public class Main // TestMessage.SearchRequest request = TestMessage.SearchRequest.newBuilder().setQuery("bees!").setPageNumber(316).setResultsPerPage(42069).build(); } - private void run() throws IOException, InterruptedException + private void run() throws IOException, InterruptedException, JLibnotifyLoadException, JLibnotifyInitException { connectionManager = new ConnectionManager(tcpPort); @@ -111,14 +122,20 @@ public class Main } })); - connectionManager.addNewConnectionConsumer(this::resquestCompleteState); + DefaultJLibnotify libnotify = (DefaultJLibnotify) DefaultJLibnotifyLoader.init().load(); + libnotify.init("FriendCloud"); + JLibnotifyNotification notification = libnotify.createNotification("Holy balls a notification!", "Woah!!!", "dialog-information"); + notification.show(); + + connectionManager.addNewConnectionConsumer(this::requestCompleteState); connectionManager.start(); String hostname = Hostname.getHostname(); - Model.getInstance().getSelfPeer().setSystemName(hostname); - Model.getInstance().getSelfPeer().setUserName(System.getProperty("user.name") + "-" + tcpPort); + model.getSelfPeer().setSystemName(hostname); + model.getSelfPeer().setUserName(System.getProperty("user.name") + "-" + tcpPort); addHostAddress(InetAddress.getLocalHost()); + model.objectChanged(model.getSelfPeer()); /* Startup procedure: @@ -133,23 +150,43 @@ public class Main if (!noUpnp) setupIGP(); - for (String sharedFilePath: sharedFiles) + Set sharedFiles = new HashSet<>(); + for (String sharedFilePath: sharedFilePaths) + { + sharedFiles.add(new File(sharedFilePath)); + } + + List knownFiles = model.listObjects(Set.of(ObjectStatements.ObjectType.OBJECT_TYPE_FILE)); + + for (NetworkObject knownFile: knownFiles) + { + NetworkFile f = (NetworkFile) knownFile; + boolean removed = sharedFiles.remove(f.getLocalFile()); + if (removed) + System.out.println("Identified known local file " + f.getObjectID() + " = " + f.getLocalFile()); + } + + for (File sharedFile: sharedFiles) { - File file = new File(sharedFilePath); - if (file.exists()) + if (sharedFile.exists()) { - System.out.println("Adding shared network file: " + file.getAbsolutePath()); + System.out.println("Adding shared network file: " + sharedFile.getAbsolutePath()); - NetworkFile networkFile = (NetworkFile) Model.getInstance().createObjectByType(ObjectStatements.ObjectType.OBJECT_TYPE_FILE); - networkFile.updateFromLocalFile(file); + NetworkFile networkFile = (NetworkFile) model.createObjectByType(ObjectStatements.ObjectType.OBJECT_TYPE_FILE); + networkFile.updateFromLocalFile(sharedFile); + model.objectChanged(networkFile); - PeerFileState peerFileState = (PeerFileState) Model.getInstance().createObjectByType(ObjectStatements.ObjectType.OBJECT_TYPE_PEER_FILE_STATE); - peerFileState.setNode(Model.getInstance().getSelfPeer()); - peerFileState.setFile(networkFile); - peerFileState.setProgress(100); +// PeerFileState peerFileState = (PeerFileState) model.createObjectByType(ObjectStatements.ObjectType.OBJECT_TYPE_PEER_FILE_STATE); +// peerFileState.setNode(model.getSelfPeer()); +// peerFileState.setFile(networkFile); +// peerFileState.setProgress(100); +// model.objectChanged(peerFileState); } } + JoinNetworkTask joinNetworkTask = new JoinNetworkTask(); + executor.submit(joinNetworkTask); + for (String knownPeerAddress : knownPeers) { String[] split = knownPeerAddress.split(":"); @@ -165,21 +202,7 @@ public class Main URI uri = new URI("tcp", null, address.getHostString(), address.getPort(), null, null, null); PeerConnection nodeConnection = connectionManager.getNodeConnection(uri); - resquestCompleteState(nodeConnection); - -// objectListFuture.whenComplete((networkObjects, throwable) -> { -// for (NetworkObject networkObject: networkObjects) -// { -// if (networkObject instanceof NetworkFile) -// { -// System.out.println("Heard about NetworkFile " + networkObject + ", creating download task!"); -// FileDownloadTask fileDownloadTask = new FileDownloadTask((NetworkFile) networkObject, connectionManager); -// executor.submit(fileDownloadTask); -// } -// } -// }); - - + requestCompleteState(nodeConnection); } catch (ConnectException ex) { System.out.println("Couldn't connect to host " + address); @@ -191,7 +214,7 @@ public class Main } } - private void resquestCompleteState(PeerConnection nodeConnection) + private void requestCompleteState(PeerConnection nodeConnection) { CompletableFuture> objectListFuture = nodeConnection.makeRequest(new ObjectListRequest(Set.of( ObjectStatements.ObjectType.OBJECT_TYPE_FILE, @@ -202,7 +225,7 @@ public class Main private void addHostAddress(InetAddress address) { String host = address.getCanonicalHostName(); - Peer selfNode = Model.getInstance().getSelfPeer(); + Peer selfNode = model.getSelfPeer(); try { URI uri = new URI("tcp", null, host, tcpPort, null, null, null); @@ -281,4 +304,9 @@ public class Main { return connectionManager; } + + public Model getModel() + { + return model; + } } \ No newline at end of file diff --git a/src/main/java/moe/nekojimi/friendcloud/Model.java b/src/main/java/moe/nekojimi/friendcloud/Model.java deleted file mode 100644 index a77caaa..0000000 --- a/src/main/java/moe/nekojimi/friendcloud/Model.java +++ /dev/null @@ -1,167 +0,0 @@ -package moe.nekojimi.friendcloud; - -import moe.nekojimi.friendcloud.objects.*; -import moe.nekojimi.friendcloud.protos.ObjectStatements; - -import java.util.*; - -public class Model -{ - private static Model instance = null; - public static Model getInstance() - { - if (instance == null) - instance = new Model(); - return instance; - } - - private final Map objects = new HashMap<>(); - private final int systemID; - - private Peer selfPeer = null; - private ObjectChangeRecord currentChange; - private final Map changeRecords = new HashMap<>(); - - private Model() - { - Random ran = new Random(); - systemID = ran.nextInt() & 0x00FFFFFF; - } - - public void setSelfPeer(Peer selfPeer) - { - this.selfPeer = selfPeer; - } - - public synchronized Peer getSelfPeer() - { -// if (selfPeer == null) -// selfPeer = (Peer) createObjectByType(ObjectStatements.ObjectType.OBJECT_TYPE_PEER); - return selfPeer; - } - // private Map nodes = new HashMap<>(); - - public synchronized NetworkObject.ObjectID getNextObjectID(ObjectStatements.ObjectType type) - { - Random ran = new Random(); - int randomNumber = ran.nextInt(); - NetworkObject.ObjectID objectID = new NetworkObject.ObjectID(type, systemID, randomNumber); - System.out.println("Assigned new object ID: " + objectID); - return objectID; - } - - public synchronized NetworkObject createObjectByID(NetworkObject.ObjectID id) - { - ObjectStatements.ObjectType type = id.getType(); - System.out.println("Creating new object with type: " + type.name()); - NetworkObject ret = switch (type) - { -// case UNRECOGNIZED -> ; - case OBJECT_TYPE_FILE -> new NetworkFile(id); - case OBJECT_TYPE_UNSPECIFIED -> throw new IllegalArgumentException(); -// case OBJECT_TYPE_USER -> null; - case OBJECT_TYPE_FOLDER -> new NetworkFolder(id); - case OBJECT_TYPE_PEER -> new Peer(id); - case OBJECT_TYPE_PEER_FILE_STATE -> new PeerFileState(id); - default -> throw new UnsupportedOperationException("NYI"); - }; - objects.put(id, ret); - return ret; - } - - public synchronized NetworkObject createObjectByType(ObjectStatements.ObjectType type) - { - return createObjectByID(getNextObjectID(type)); - } - - public synchronized NetworkObject getOrCreateObject(NetworkObject.ObjectID id) - { - if (!objects.containsKey(id)) - { - objects.put(id, createObjectByID(id)); - } - return objects.get(id); - } - - public synchronized List listObjects(Set types) - { - return objects.keySet().stream().filter((id)->(types.contains(id.getType()))).toList(); - } - - public synchronized NetworkObject getObject(NetworkObject.ObjectID objectID) - { - return objects.get(objectID); - } - - public synchronized List listFSNodes(String path) - { - //TODO: dumbest algorithm in the world - - List ret = new ArrayList<>(); - for (NetworkObject.ObjectID nodeID : listObjects(Set.of(ObjectStatements.ObjectType.OBJECT_TYPE_FILE, ObjectStatements.ObjectType.OBJECT_TYPE_FOLDER))) - { - NetworkFSNode fsNode = (NetworkFSNode) getObject(nodeID); - String networkPath = fsNode.getNetworkPath(); - if (networkPath.substring(0, networkPath.lastIndexOf("/")+1).equals(path)) - ret.add(fsNode); - } - return ret; - } - - public synchronized NetworkFSNode getFSNode(String path) - { - for (NetworkObject.ObjectID nodeID : listObjects(Set.of(ObjectStatements.ObjectType.OBJECT_TYPE_FILE, ObjectStatements.ObjectType.OBJECT_TYPE_FOLDER))) - { - NetworkFSNode fsNode = (NetworkFSNode) getObject(nodeID); - String networkPath = fsNode.getNetworkPath(); - if (networkPath.equals(path)) - return fsNode; - } - return null; - } - - public synchronized void addChangeRecord(ObjectChangeRecord record) - { - changeRecords.put(record.getChangeID(), record); - } - - public ObjectChangeRecord getChangeRecord(long id) - { - return changeRecords.get(id); - } - - public void applyChangeRecord(ObjectChangeRecord record) - { - if (!record.getChangeHeads().contains(currentChange)) - throw new IllegalStateException("Change does not apply! Valid change heads=" + record.getChangeHeads() + ", we are in state " + currentChange.getChangeID()); - if (!changeRecords.containsKey(record.getChangeID())) - addChangeRecord(record); - - -// if (record == null) -// throw new IllegalArgumentException("Cannot apply unknown change!"); - } - - public Set getChangeHeads() - { - // stupid algorithm - start with all of the changes, then remove the ones that are referenced by something - // TODO: better algorithm - Set ret = new HashSet<>(changeRecords.values()); - for (ObjectChangeRecord record : changeRecords.values()) - { - - } - } - - public Set listOtherPeers() - { - Set ret = new HashSet<>(); - for (NetworkObject.ObjectID peerID : listObjects(Set.of(ObjectStatements.ObjectType.OBJECT_TYPE_PEER))) - { - Peer peer = (Peer) getObject(peerID); - if (peer != getSelfPeer()) - ret.add(peer); - } - return ret; - } -} diff --git a/src/main/java/moe/nekojimi/friendcloud/ObjectChangeRecord.java b/src/main/java/moe/nekojimi/friendcloud/ObjectChangeRecord.java index 637567b..973e010 100644 --- a/src/main/java/moe/nekojimi/friendcloud/ObjectChangeRecord.java +++ b/src/main/java/moe/nekojimi/friendcloud/ObjectChangeRecord.java @@ -2,19 +2,20 @@ package moe.nekojimi.friendcloud; import moe.nekojimi.friendcloud.objects.NetworkObject; import moe.nekojimi.friendcloud.protos.ObjectStatements; +import moe.nekojimi.friendcloud.storage.Storable; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; -public class ObjectChangeRecord +public class ObjectChangeRecord implements Storable { // private final long changeID; - private final NetworkObject.ObjectID creatorPeer; - private final Set changeHeads = new HashSet<>(); - private final Set changes = new HashSet<>(); + private NetworkObject.ObjectID creatorPeer; + private Set changeHeads = new HashSet<>(); + private Set changes = new HashSet<>(); public ObjectChangeRecord(NetworkObject.ObjectID creatorPeer) { @@ -69,6 +70,22 @@ public class ObjectChangeRecord return builder; } + @Override + public Map getStateMap() + { + return Map.of("changeHeads", changeHeads, + "changes", changes, + "creator", creatorPeer.toLong()); + } + + @Override + public void updateFromStateMap(Map map) + { + changeHeads = new HashSet<>((Collection) map.get("changeHeads")); + changes = new HashSet<>((Collection) map.get("changes")); + creatorPeer = new NetworkObject.ObjectID((Long) map.get("creator")); + } + public String toString() { StringBuilder sb = new StringBuilder(); @@ -103,68 +120,69 @@ public class ObjectChangeRecord return creatorPeer; } - public static class Change + @Override + public long getStorageID() { - private final NetworkObject.ObjectID objectID; - private final Map beforeValues; - private final Map afterValues; - - public Change(NetworkObject.ObjectID objectID, Map before, Map after) - { - this.objectID = objectID; - this.beforeValues = before; - this.afterValues = after; - } - - public static Change createFromObjectChange(ObjectStatements.ObjectChange change) - { - return new Change(new NetworkObject.ObjectID(change.getObjectId()), change.getBeforeMap(), change.getAfterMap()); - } - - public static Change createFromObjectStates(ObjectStatements.ObjectState before, ObjectStatements.ObjectState after) - { - Map beforeValues = new HashMap<>(); - Map afterValues = new HashMap<>(); - for (String key: after.getValuesMap().keySet()) - { - String beforeValue = before.getValuesOrDefault(key, null); - String afterValue = after.getValuesOrDefault(key, null); - if (!afterValue.equals(beforeValue)) - { - beforeValues.put(key,beforeValue); - afterValues.put(key,afterValue); - } - } - if (!afterValues.isEmpty()) - { - return new Change(new NetworkObject.ObjectID(before.getObjectId()), beforeValues, afterValues); - } - return null; - } - - public String toString() - { - StringBuilder sb = new StringBuilder(); - sb.append(objectID.toLong()).append(";"); // The object ID, then ; - // now all key-value pairs in alphabetical order - List keys = new ArrayList<>(beforeValues.keySet()); - Collections.sort(keys); - for (String key : keys) - { - sb.append(key).append(":").append(afterValues.get(key)); - } - sb.append(";"); - return sb.toString(); - } - - - public ObjectStatements.ObjectChange.Builder buildObjectChange() - { - ObjectStatements.ObjectChange.Builder builder = ObjectStatements.ObjectChange.newBuilder(); - builder.putAllBefore(beforeValues); - builder.putAllAfter(afterValues); - builder.setObjectId(objectID.toLong()); - return builder; - } + return getChangeID(); } + + public Set getChangeHeads() + { + return changeHeads; + } + + public record Change(NetworkObject.ObjectID objectID, Map beforeValues, Map afterValues) + { + + public static Change createFromObjectChange(ObjectStatements.ObjectChange change) + { + return new Change(new NetworkObject.ObjectID(change.getObjectId()), change.getBeforeMap(), change.getAfterMap()); + } + + public static Change createFromObjectStates(ObjectStatements.ObjectState before, ObjectStatements.ObjectState after) + { + Map beforeValues = new HashMap<>(); + Map afterValues = new HashMap<>(); + for (String key : after.getValuesMap().keySet()) + { + String beforeValue = before.getValuesOrDefault(key, null); + String afterValue = after.getValuesOrDefault(key, null); + if (!afterValue.equals(beforeValue)) + { + beforeValues.put(key, beforeValue); + afterValues.put(key, afterValue); + } + } + if (!afterValues.isEmpty()) + { + return new Change(new NetworkObject.ObjectID(before.getObjectId()), beforeValues, afterValues); + } + return null; + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append(objectID.toLong()).append(";"); // The object ID, then ; + // now all key-value pairs in alphabetical order + List keys = new ArrayList<>(beforeValues.keySet()); + Collections.sort(keys); + for (String key : keys) + { + sb.append(key).append(":").append(afterValues.get(key)); + } + sb.append(";"); + return sb.toString(); + } + + + public ObjectStatements.ObjectChange.Builder buildObjectChange() + { + ObjectStatements.ObjectChange.Builder builder = ObjectStatements.ObjectChange.newBuilder(); + builder.putAllBefore(beforeValues); + builder.putAllAfter(afterValues); + builder.setObjectId(objectID.toLong()); + return builder; + } + } } diff --git a/src/main/java/moe/nekojimi/friendcloud/ObjectChangeTransaction.java b/src/main/java/moe/nekojimi/friendcloud/ObjectChangeTransaction.java index fb7596f..5788394 100644 --- a/src/main/java/moe/nekojimi/friendcloud/ObjectChangeTransaction.java +++ b/src/main/java/moe/nekojimi/friendcloud/ObjectChangeTransaction.java @@ -37,7 +37,7 @@ public class ObjectChangeTransaction implements Closeable public ObjectChangeTransaction addObjectBeforeChange(NetworkObject.ObjectID id) { - NetworkObject object = Model.getInstance().getObject(id); + NetworkObject object = Main.getInstance().getModel().getObject(id); if (object != null) beforeStates.put(id, object.buildObjectState().build()); return this; @@ -52,7 +52,7 @@ public class ObjectChangeTransaction implements Closeable for (Map.Entry entry : beforeStates.entrySet()) { - ObjectStatements.ObjectState afterState = Model.getInstance().getObject(entry.getKey()).buildObjectState().build(); + ObjectStatements.ObjectState afterState = Main.getInstance().getModel().getObject(entry.getKey()).buildObjectState().build(); ObjectChangeRecord.Change change = ObjectChangeRecord.Change.createFromObjectStates(entry.getValue(), afterState); changes.add(change); } @@ -66,7 +66,7 @@ public class ObjectChangeTransaction implements Closeable // end the transaction and get the change object ObjectChangeRecord objectChangeRecord = endTransaction(); // add the new change to the model - Model.getInstance().addChangeRecord(objectChangeRecord); + Main.getInstance().getModel().addChangeRecord(objectChangeRecord); // create a task to propagate the change to other peers Main.getInstance().getExecutor().submit(new PropagateMessageTask(objectChangeRecord.buildObjectChangeMessage().build())); } diff --git a/src/main/java/moe/nekojimi/friendcloud/Util.java b/src/main/java/moe/nekojimi/friendcloud/Util.java index 5e8a9ab..4534dbe 100644 --- a/src/main/java/moe/nekojimi/friendcloud/Util.java +++ b/src/main/java/moe/nekojimi/friendcloud/Util.java @@ -16,4 +16,5 @@ public class Util } return ret; } + } diff --git a/src/main/java/moe/nekojimi/friendcloud/filesystem/FUSEAccess.java b/src/main/java/moe/nekojimi/friendcloud/filesystem/FUSEAccess.java index 4ca96be..aeca1c3 100644 --- a/src/main/java/moe/nekojimi/friendcloud/filesystem/FUSEAccess.java +++ b/src/main/java/moe/nekojimi/friendcloud/filesystem/FUSEAccess.java @@ -2,7 +2,7 @@ package moe.nekojimi.friendcloud.filesystem; import jnr.ffi.Pointer; import moe.nekojimi.friendcloud.FileRemoteAccess; -import moe.nekojimi.friendcloud.Model; +import moe.nekojimi.friendcloud.Main; import moe.nekojimi.friendcloud.objects.NetworkFSNode; import moe.nekojimi.friendcloud.objects.NetworkFile; import moe.nekojimi.friendcloud.objects.NetworkFolder; @@ -34,7 +34,7 @@ public class FUSEAccess extends FuseStubFS filter.apply(buf, "..", null, 0); // filter.apply(buf,"hello", null, 0); - for (NetworkFSNode fsNode : Model.getInstance().listFSNodes(path)) + for (NetworkFSNode fsNode : Main.getInstance().getModel().listFSNodes(path)) { filter.apply(buf, fsNode.getName(), null, 0); } @@ -54,7 +54,7 @@ public class FUSEAccess extends FuseStubFS } else { - NetworkFSNode fsNode = Model.getInstance().getFSNode(path); + NetworkFSNode fsNode = Main.getInstance().getModel().getFSNode(path); switch (fsNode) { case null -> @@ -85,7 +85,7 @@ public class FUSEAccess extends FuseStubFS public int open(String path, FuseFileInfo fi) { System.out.println("FUSE: Opening file " + path); - NetworkFSNode fsNode = Model.getInstance().getFSNode(path); + NetworkFSNode fsNode = Main.getInstance().getModel().getFSNode(path); if (fsNode == null) { System.err.println("FUSE: Failed to open file " + path + ": not found"); diff --git a/src/main/java/moe/nekojimi/friendcloud/network/PeerConnection.java b/src/main/java/moe/nekojimi/friendcloud/network/PeerConnection.java index bdc5d04..574eff1 100644 --- a/src/main/java/moe/nekojimi/friendcloud/network/PeerConnection.java +++ b/src/main/java/moe/nekojimi/friendcloud/network/PeerConnection.java @@ -6,7 +6,7 @@ import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; import moe.nekojimi.friendcloud.FilePieceAccess; -import moe.nekojimi.friendcloud.Model; +import moe.nekojimi.friendcloud.Main; import moe.nekojimi.friendcloud.objects.NetworkFile; import moe.nekojimi.friendcloud.objects.NetworkObject; import moe.nekojimi.friendcloud.objects.Peer; @@ -86,7 +86,7 @@ public abstract class PeerConnection extends Thread { CommonMessages.MessageHeader.Builder headerBuilder = CommonMessages.MessageHeader.newBuilder() .setMessageId(nextMessageId) - .setSenderId(Model.getInstance().getSelfPeer().getObjectID().toLong()); + .setSenderId(Main.getInstance().getModel().getSelfPeer().getObjectID().toLong()); if (inReplyTo != null) headerBuilder.setReplyToMessageId(inReplyTo.getMessageId()); @@ -131,7 +131,7 @@ public abstract class PeerConnection extends Thread NetworkObject.ObjectID senderID = new NetworkObject.ObjectID(header.getSenderId()); if (peer == null) - peer = (Peer) Model.getInstance().getOrCreateObject(senderID); + peer = (Peer) Main.getInstance().getModel().getOrCreateObject(senderID); else { if (!senderID.equals(peer.getObjectID())) @@ -187,15 +187,12 @@ public abstract class PeerConnection extends Thread if (body.is(ObjectStatements.ObjectListRequest.class)) { ObjectStatements.ObjectListRequest objectListRequest = body.unpack(ObjectStatements.ObjectListRequest.class); - List objectIDS = Model.getInstance().listObjects(new HashSet<>(objectListRequest.getTypesList())); + List objects = Main.getInstance().getModel().listObjects(new HashSet<>(objectListRequest.getTypesList())); ObjectStatements.ObjectList.Builder objectList = ObjectStatements.ObjectList.newBuilder(); - for (NetworkObject.ObjectID objectID : objectIDS) + for (NetworkObject object : objects) { - NetworkObject networkObject = Model.getInstance().getOrCreateObject(objectID); - objectList.addStates(networkObject.buildObjectState()); -// networkObject.updateFromStateMessage(); -// objectList.addState(networkObject.buildObjectState()); + objectList.addStates(object.buildObjectState()); } System.out.println("Replying to ObjectListRequest with ObjectList, objects=" + objectList.getStatesList()); sendMessage(wrapMessage(objectList.build(), header)); @@ -208,7 +205,7 @@ public abstract class PeerConnection extends Thread replyWithError(CommonMessages.Error.ERROR_INVALID_ARGUMENT, header); } - NetworkFile networkFile = (NetworkFile) Model.getInstance().getObject(new NetworkObject.ObjectID(filePiecesRequestMessage.getFileId())); + NetworkFile networkFile = (NetworkFile) Main.getInstance().getModel().getObject(new NetworkObject.ObjectID(filePiecesRequestMessage.getFileId())); if (networkFile == null) { replyWithError(CommonMessages.Error.ERROR_OBJECT_NOT_FOUND, header); diff --git a/src/main/java/moe/nekojimi/friendcloud/network/requests/ObjectChangeRequest.java b/src/main/java/moe/nekojimi/friendcloud/network/requests/ObjectChangeRequest.java index 5135b66..5dade64 100644 --- a/src/main/java/moe/nekojimi/friendcloud/network/requests/ObjectChangeRequest.java +++ b/src/main/java/moe/nekojimi/friendcloud/network/requests/ObjectChangeRequest.java @@ -2,8 +2,7 @@ package moe.nekojimi.friendcloud.network.requests; import com.google.protobuf.Any; import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.Message; -import moe.nekojimi.friendcloud.Model; +import moe.nekojimi.friendcloud.Main; import moe.nekojimi.friendcloud.ObjectChangeRecord; import moe.nekojimi.friendcloud.protos.ObjectStatements; @@ -34,7 +33,7 @@ public class ObjectChangeRequest extends Request> +public class ObjectListRequest extends Request> { private final Set types; @@ -44,7 +44,7 @@ public class ObjectListRequest extends Request getStateMap() + { + Map ret = super.getStateMap(); + ret.put("name", name); + ret.put("parent", parent != null ? parent.getStorageID() : 0L); + return ret; + } + + @Override + public void updateFromStateMap(Map map) + { + name = map.get("name").toString(); + parent = (NetworkFolder) Main.getInstance().getModel().getObject(new ObjectID(((Number)map.get("parent")).longValue())); + } + public String getName() { return name; diff --git a/src/main/java/moe/nekojimi/friendcloud/objects/NetworkFile.java b/src/main/java/moe/nekojimi/friendcloud/objects/NetworkFile.java index 5b6846a..9555359 100644 --- a/src/main/java/moe/nekojimi/friendcloud/objects/NetworkFile.java +++ b/src/main/java/moe/nekojimi/friendcloud/objects/NetworkFile.java @@ -9,10 +9,7 @@ import java.nio.file.Files; import java.nio.file.attribute.FileAttribute; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.BitSet; -import java.util.HashMap; -import java.util.HexFormat; -import java.util.Map; +import java.util.*; import java.util.concurrent.TimeUnit; public class NetworkFile extends NetworkFSNode @@ -20,6 +17,7 @@ public class NetworkFile extends NetworkFSNode private static final int MIN_PIECE_SIZE = 0x400; // 1KiB private static final int MAX_PIECE_SIZE = 0x100000; // 1 MiB private static final int IDEAL_PIECE_COUNT = 1024; + private static File tempDirectory = null; private long size = 0; private long pieceSize = 0; @@ -31,7 +29,6 @@ public class NetworkFile extends NetworkFSNode private final Map fileStates = new HashMap<>(); private BitSet pieces = new BitSet(); - private static File tempDirectory = null; // private List pieces = new ArrayList<>(); public NetworkFile(ObjectID objectID) @@ -118,7 +115,6 @@ public class NetworkFile extends NetworkFSNode hash = HexFormat.of().parseHex(state.getValuesOrThrow("hash")); if (state.containsValues("pieceSize")) pieceSize = Long.parseLong(state.getValuesOrThrow("pieceSize")); - } @Override @@ -131,6 +127,34 @@ public class NetworkFile extends NetworkFSNode .putValues("pieceSize", Long.toString(pieceSize)); } + @Override + public Map getStateMap() + { + Map ret = super.getStateMap(); + ret.put("size", size); + ret.put("hash", HexFormat.of().formatHex(hash)); + ret.put("pieceSize", pieceSize); + ret.put("pieces", Arrays.stream(pieces.toLongArray()).boxed().toList()); + ret.put("localFile", localFile != null ? localFile.getAbsolutePath() : ""); + return ret; + } + + @Override + public void updateFromStateMap(Map map) + { + super.updateFromStateMap(map); + size = ((Number) map.get("size")).longValue(); + hash = HexFormat.of().parseHex((CharSequence) map.get("hash")); + pieceSize = ((Number) map.get("pieceSize")).longValue(); + ArrayList pieces1 = (ArrayList) map.get("pieces"); + pieces = BitSet.valueOf(pieces1.stream().mapToLong(Number::longValue).toArray()); + String localFilePath = (String) map.get("localFile"); + if (localFilePath.isEmpty()) + localFile = null; + else + localFile = new File(localFilePath); + } + public File getLocalFile() { return localFile; diff --git a/src/main/java/moe/nekojimi/friendcloud/objects/NetworkFolder.java b/src/main/java/moe/nekojimi/friendcloud/objects/NetworkFolder.java index 9e6da28..b8f02c3 100644 --- a/src/main/java/moe/nekojimi/friendcloud/objects/NetworkFolder.java +++ b/src/main/java/moe/nekojimi/friendcloud/objects/NetworkFolder.java @@ -2,13 +2,13 @@ package moe.nekojimi.friendcloud.objects; import moe.nekojimi.friendcloud.protos.ObjectStatements; +import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; import java.util.WeakHashMap; public class NetworkFolder extends NetworkFSNode { -// private final SortedSet children = new TreeSet<>((a,b)->Long.compare(a.getId(),b.getId())); public NetworkFolder(ObjectID objectID) { @@ -18,8 +18,18 @@ public class NetworkFolder extends NetworkFSNode @Override public ObjectStatements.ObjectState.Builder buildObjectState() { - return super.buildObjectState().putValues("name", name); + return super.buildObjectState(); } + @Override + public Map getStateMap() + { + return super.getStateMap(); + } + @Override + public void updateFromStateMap(Map map) + { + super.updateFromStateMap(map); + } } diff --git a/src/main/java/moe/nekojimi/friendcloud/objects/NetworkObject.java b/src/main/java/moe/nekojimi/friendcloud/objects/NetworkObject.java index f0897ef..07fcc1c 100644 --- a/src/main/java/moe/nekojimi/friendcloud/objects/NetworkObject.java +++ b/src/main/java/moe/nekojimi/friendcloud/objects/NetworkObject.java @@ -1,10 +1,13 @@ package moe.nekojimi.friendcloud.objects; import moe.nekojimi.friendcloud.protos.ObjectStatements; +import moe.nekojimi.friendcloud.storage.Storable; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; -public abstract class NetworkObject +public abstract class NetworkObject implements Storable { private final ObjectID objectID; @@ -18,6 +21,21 @@ public abstract class NetworkObject return objectID; } + @Override + public long getStorageID() + { + return getObjectID().toLong(); + } + + + @Override + public Map getStateMap() + { + Map ret = new HashMap<>(); + ret.put("id", objectID.toLong()); + return ret; + } + public synchronized void updateFromStateMessage(ObjectStatements.ObjectState state) { if (state.getObjectId() != objectID.toLong()) diff --git a/src/main/java/moe/nekojimi/friendcloud/objects/Peer.java b/src/main/java/moe/nekojimi/friendcloud/objects/Peer.java index 9e67ee5..ca67e8d 100644 --- a/src/main/java/moe/nekojimi/friendcloud/objects/Peer.java +++ b/src/main/java/moe/nekojimi/friendcloud/objects/Peer.java @@ -4,10 +4,7 @@ import moe.nekojimi.friendcloud.protos.ObjectStatements; import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; public class Peer extends NetworkObject @@ -73,7 +70,26 @@ public class Peer extends NetworkObject return builder; } - public void addAddress(URI address) + @Override + public Map getStateMap() + { + Map ret = super.getStateMap(); + ret.put("userName", userName); + ret.put("systemName", systemName); + ret.put("addresses", addresses); + return ret; + } + + @Override + public void updateFromStateMap(Map map) + { + userName = map.get("userName").toString(); + systemName = map.get("systemName").toString(); + addresses.clear(); + addresses.addAll((Collection) map.get("addresses")); + } + + public void addAddress(URI address) { addresses.add(address); } diff --git a/src/main/java/moe/nekojimi/friendcloud/objects/PeerFileState.java b/src/main/java/moe/nekojimi/friendcloud/objects/PeerFileState.java index 813c557..79b80c6 100644 --- a/src/main/java/moe/nekojimi/friendcloud/objects/PeerFileState.java +++ b/src/main/java/moe/nekojimi/friendcloud/objects/PeerFileState.java @@ -1,8 +1,10 @@ package moe.nekojimi.friendcloud.objects; -import moe.nekojimi.friendcloud.Model; +import moe.nekojimi.friendcloud.Main; import moe.nekojimi.friendcloud.protos.ObjectStatements; +import java.util.Map; + public class PeerFileState extends NetworkObject { private Peer peer; @@ -19,8 +21,8 @@ public class PeerFileState extends NetworkObject public void updateFromStateMessage(ObjectStatements.ObjectState state) { super.updateFromStateMessage(state); - peer = (Peer) Model.getInstance().getOrCreateObject(new ObjectID(Long.parseLong(state.getValuesOrThrow("peer")))); - file = (NetworkFile) Model.getInstance().getOrCreateObject(new ObjectID(Long.parseLong(state.getValuesOrThrow("file")))); + peer = (Peer) Main.getInstance().getModel().getOrCreateObject(new ObjectID(Long.parseLong(state.getValuesOrThrow("peer")))); + file = (NetworkFile) Main.getInstance().getModel().getOrCreateObject(new ObjectID(Long.parseLong(state.getValuesOrThrow("file")))); if (state.containsValues("progress")) progress = Double.parseDouble(state.getValuesOrThrow("progress")); @@ -72,4 +74,10 @@ public class PeerFileState extends NetworkObject { return peer; } + + @Override + public void updateFromStateMap(Map map) + { + + } } diff --git a/src/main/java/moe/nekojimi/friendcloud/storage/CachingDataStore.java b/src/main/java/moe/nekojimi/friendcloud/storage/CachingDataStore.java new file mode 100644 index 0000000..6beb1ba --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/storage/CachingDataStore.java @@ -0,0 +1,92 @@ +package moe.nekojimi.friendcloud.storage; + +import java.lang.reflect.Modifier; +import java.util.*; + +public class CachingDataStore extends DataStore +{ + private final DataStore backend; + + public CachingDataStore(DataStore backend) + { + this.backend = backend; + } + + @Override + public synchronized DAO getDAOForClass(Class clazz) + { + if (daos.containsKey(clazz)) + return (DAO) daos.get(clazz); + else + { + CachingDAO ret = new CachingDAO<>(clazz); + daos.put(clazz, ret); + return ret; + } + } + + @Override + public FSNodeDAO getFSDAO() + { + return backend.getFSDAO(); + } + + private Map, CachingDAO> daos = new HashMap<>(); + + public class CachingDAO implements DAO + { + private final DAO backendDao; + private WeakHashMap cache = new WeakHashMap<>(); + + public CachingDAO(Class clazz) + { + this.backendDao = backend.getDAOForClass(clazz); + } + + @Override + public synchronized List getAll() + { + List ret = new ArrayList<>(); + ret.addAll(cache.values()); + for (T t : backendDao.getAll()) + { + if (!cache.containsKey(t.getStorageID())) + { + ret.add(t); + cache.put(t.getStorageID(), t); + } + } + return ret; + } + + @Override + public boolean exists(long id) + { + return backendDao.exists(id); + } + + @Override + public T create(long id) + { + T t = backendDao.create(id); + cache.put(id, t); + return t; + } + + @Override + public T get(long id) + { + T t = backendDao.get(id); + cache.put(id, t); + return t; + } + + @Override + public void update(T object) + { + long id = object.getStorageID(); + backendDao.update(object); + cache.put(id, object); + } + } +} diff --git a/src/main/java/moe/nekojimi/friendcloud/storage/DataStore.java b/src/main/java/moe/nekojimi/friendcloud/storage/DataStore.java new file mode 100644 index 0000000..2b33099 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/storage/DataStore.java @@ -0,0 +1,89 @@ +package moe.nekojimi.friendcloud.storage; + +import moe.nekojimi.friendcloud.objects.NetworkFSNode; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public abstract class DataStore +{ + public abstract DAO getDAOForClass(Class clazz); + public abstract FSNodeDAO getFSDAO(); + + public interface DAO + { + List getAll(); + boolean exists(long id); + T create(long id); + T get(long id); + default T getOrCreate(long id) + { + T old = get(id); + if (old != null) + return old; + return create(id); + } + void update(T object); + } + + public interface FSNodeDAO extends DAO + { + default List getPathChildren(String path) + { + System.err.println("WARNING: using violently cursed implementation of getPathChildren, make a better one now!!!"); + return getAll().stream().filter(networkFSNode -> networkFSNode.getNetworkPath().startsWith(path)).toList(); + } + } + + public class ClassFusionDAO implements DAO + { + private final Set> subclasses; + + @SafeVarargs + public ClassFusionDAO(Class... subclasses) + { + this.subclasses = Set.of(subclasses); + } + + @Override + public List getAll() + { + List ret = new ArrayList<>(); + for (Class subclass : subclasses) + ret.addAll(getDAOForClass(subclass).getAll()); + return ret; + } + + @Override + public boolean exists(long id) + { + for (Class subclass : subclasses) + if (getDAOForClass(subclass).exists(id)) + return true; + return false; + } + + @Override + public T create(long id) + { + throw new UnsupportedOperationException("That type's abstract, can't create it"); + } + + @Override + public T get(long id) + { + for (Class subclass : subclasses) + if (getDAOForClass(subclass).exists(id)) + return getDAOForClass(subclass).get(id); + return null; + } + + @Override + public void update(T object) + { + throw new UnsupportedOperationException("That type's abstract, can't update it"); + } + } +} diff --git a/src/main/java/moe/nekojimi/friendcloud/storage/LocalData.java b/src/main/java/moe/nekojimi/friendcloud/storage/LocalData.java new file mode 100644 index 0000000..e159629 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/storage/LocalData.java @@ -0,0 +1,40 @@ +package moe.nekojimi.friendcloud.storage; + +import moe.nekojimi.friendcloud.Main; +import moe.nekojimi.friendcloud.objects.NetworkObject; +import moe.nekojimi.friendcloud.objects.Peer; + +import java.util.Map; + +public class LocalData implements Storable +{ + private Peer localPeer; + + public Peer getLocalPeer() + { + return localPeer; + } + + public void setLocalPeer(Peer localPeer) + { + this.localPeer = localPeer; + } + + @Override + public long getStorageID() + { + return 0; + } + + @Override + public Map getStateMap() + { + return Map.of("localPeer", localPeer.getObjectID().toLong()); + } + + @Override + public void updateFromStateMap(Map map) + { + localPeer = (Peer) Main.getInstance().getModel().getObject(new NetworkObject.ObjectID((Long) map.get("localPeer"))); + } +} diff --git a/src/main/java/moe/nekojimi/friendcloud/storage/Model.java b/src/main/java/moe/nekojimi/friendcloud/storage/Model.java new file mode 100644 index 0000000..1078689 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/storage/Model.java @@ -0,0 +1,184 @@ +package moe.nekojimi.friendcloud.storage; + +import moe.nekojimi.friendcloud.ObjectChangeRecord; +import moe.nekojimi.friendcloud.objects.*; +import moe.nekojimi.friendcloud.protos.ObjectStatements; + +import java.util.*; +import java.util.stream.Collectors; + +public class Model +{ + + private final CachingDataStore dataStore; + + private final int systemID; + + private Peer selfPeer = null; + private ObjectChangeRecord currentChange; + + public Model(DataStore dataStore) + { + this.dataStore = new CachingDataStore(dataStore); + Random ran = new Random(); + systemID = ran.nextInt() & 0x00FFFFFF; + } + + public void setSelfPeer(Peer selfPeer) + { + this.selfPeer = selfPeer; + } + + public synchronized Peer getSelfPeer() + { + if (selfPeer == null) + selfPeer = createObjectByType(ObjectStatements.ObjectType.OBJECT_TYPE_PEER); + return selfPeer; + } + // private Map nodes = new HashMap<>(); + + public synchronized NetworkObject.ObjectID getNextObjectID(ObjectStatements.ObjectType type) + { + Random ran = new Random(); + int randomNumber = ran.nextInt(); + NetworkObject.ObjectID objectID = new NetworkObject.ObjectID(type, systemID, randomNumber); + System.out.println("Assigned new object ID: " + objectID); + return objectID; + } + + public static Class getNetworkObjectClassByType(ObjectStatements.ObjectType type) + { + return switch (type) + { + case OBJECT_TYPE_FILE -> NetworkFile.class; + case OBJECT_TYPE_FOLDER -> NetworkFolder.class; + case OBJECT_TYPE_PEER -> Peer.class; + case OBJECT_TYPE_PEER_FILE_STATE -> PeerFileState.class; + case OBJECT_TYPE_UNSPECIFIED, UNRECOGNIZED -> throw new IllegalArgumentException("???"); + default -> throw new UnsupportedOperationException("NYI"); + }; + } + + public synchronized T createObjectByID(NetworkObject.ObjectID id) + { + if (id.toLong() == 0) + throw new IllegalArgumentException("Cannot create an object with ID=0!"); + ObjectStatements.ObjectType type = id.getType(); + System.out.println("Creating new object with type: " + type.name()); + if (type == ObjectStatements.ObjectType.OBJECT_TYPE_UNSPECIFIED) + throw new IllegalArgumentException(); + T ret = (T) dataStore.getDAOForClass(getNetworkObjectClassByType(type)).create(id.toLong()); + return ret; + } + + public synchronized T createObjectByType(ObjectStatements.ObjectType type) + { + return createObjectByID(getNextObjectID(type)); + } + + public synchronized T getObject(NetworkObject.ObjectID id) + { + if (id.toLong() == 0) + return null; + Class clazz = (Class) getNetworkObjectClassByType(id.getType()); + return dataStore.getDAOForClass(clazz).get(id.toLong()); + } + + public synchronized T getOrCreateObject(NetworkObject.ObjectID id) + { + if (id.toLong() == 0) + return null; + Class clazz = (Class) getNetworkObjectClassByType(id.getType()); + return dataStore.getDAOForClass(clazz).getOrCreate(id.toLong()); + } + + public synchronized List listObjects(Set types) + { + List ret = new ArrayList<>(); + Set> classes = types.stream().map(Model::getNetworkObjectClassByType).collect(Collectors.toSet()); + for (Class clazz: classes) + { + List list = dataStore.getDAOForClass(clazz).getAll(); + ret.addAll(list); + } + return ret; + } + + public synchronized List listFSNodes(String path) + { + //TODO: dumbest algorithm in the world + + List ret = new ArrayList<>(); + for (NetworkObject object : listObjects(Set.of(ObjectStatements.ObjectType.OBJECT_TYPE_FILE, ObjectStatements.ObjectType.OBJECT_TYPE_FOLDER))) + { + NetworkFSNode fsNode = (NetworkFSNode) object; + String networkPath = fsNode.getNetworkPath(); + if (networkPath.substring(0, networkPath.lastIndexOf("/")+1).equals(path)) + ret.add(fsNode); + } + return ret; + } + + public synchronized NetworkFSNode getFSNode(String path) + { + for (NetworkObject object : listObjects(Set.of(ObjectStatements.ObjectType.OBJECT_TYPE_FILE, ObjectStatements.ObjectType.OBJECT_TYPE_FOLDER))) + { + NetworkFSNode fsNode = (NetworkFSNode) object; + String networkPath = fsNode.getNetworkPath(); + if (networkPath.equals(path)) + return fsNode; + } + return null; + } + + public synchronized void addChangeRecord(ObjectChangeRecord record) + { + dataStore.getDAOForClass(ObjectChangeRecord.class).update(record); + } + + public ObjectChangeRecord getChangeRecord(long id) + { + return dataStore.getDAOForClass(ObjectChangeRecord.class).get(id); + } + + public void applyChangeRecord(ObjectChangeRecord record) + { + if (!record.getChangeHeads().contains(currentChange.getChangeID())) + throw new IllegalStateException("Change does not apply! Valid change heads=" + record.getChangeHeads() + ", we are in state " + currentChange.getChangeID()); + addChangeRecord(record); + + +// if (record == null) +// throw new IllegalArgumentException("Cannot apply unknown change!"); + } + + public Set getChangeHeads() + { + // stupid algorithm - start with all of the changes, then remove the ones that are referenced by something + // TODO: better algorithm + Set ret = new HashSet<>(dataStore.getDAOForClass(ObjectChangeRecord.class).getAll()); +// for (ObjectChangeRecord record : changeRecords.values()) +// { +// throw new UnsupportedOperationException("NYI"); +// } + throw new UnsupportedOperationException("NYI"); + } + + public Set listOtherPeers() + { + Set ret = new HashSet<>(); + for (NetworkObject object : listObjects(Set.of(ObjectStatements.ObjectType.OBJECT_TYPE_PEER))) + { + Peer peer = (Peer) object; + if (peer != getSelfPeer()) + ret.add(peer); + } + return ret; + } + + public void objectChanged(T storable) + { + Class clazz = (Class) storable.getClass(); + dataStore.getDAOForClass(clazz).update(storable); + } +} diff --git a/src/main/java/moe/nekojimi/friendcloud/storage/Storable.java b/src/main/java/moe/nekojimi/friendcloud/storage/Storable.java new file mode 100644 index 0000000..372a5a1 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/storage/Storable.java @@ -0,0 +1,29 @@ +package moe.nekojimi.friendcloud.storage; + +import java.util.Map; + +public interface Storable +{ + /** + * Gets the unique ID used for persisting the object on disk. Must remain constant for an object's lifetime. + * @return the unique ID. + */ + long getStorageID(); + +// /** +// * Gets a constant string specifying the namespace that the ID numbers returned by getStorageID() are unique within. Defaults to the class name. +// * @return the namespace name. +// */ +// default String getStorageNamespace() +// { +// return this.getClass().getName().toLowerCase(); +// } + + /** + * Gets a map representing all serializable (i.e. to be saved) fields of this object, by name. + * Fields may be any of the following types: String, Integer, Long, Double, Collection\, Boolean, Map\ + */ + Map getStateMap(); + + void updateFromStateMap(Map map); +} diff --git a/src/main/java/moe/nekojimi/friendcloud/storage/StupidJSONFileStore.java b/src/main/java/moe/nekojimi/friendcloud/storage/StupidJSONFileStore.java new file mode 100644 index 0000000..37c1a9c --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/storage/StupidJSONFileStore.java @@ -0,0 +1,334 @@ +package moe.nekojimi.friendcloud.storage; + +import moe.nekojimi.friendcloud.objects.*; +import org.jetbrains.annotations.NotNull; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.util.*; + +public class StupidJSONFileStore extends DataStore +{ + private final File storageDirectory; + + private final Map, DAO> daos = new HashMap<>(); + + public StupidJSONFileStore(File storageDirectory) + { + this.storageDirectory = storageDirectory; + if (!storageDirectory.exists()) + { + if (!storageDirectory.mkdirs()) + throw new RuntimeException("Unable to create storage directory " + storageDirectory.getAbsolutePath()); + } + } + + @Override + public DAO getDAOForClass(Class clazz) + { + if (daos.containsKey(clazz)) + return (DAO) daos.get(clazz); + + DAO ret; + if (clazz.equals(NetworkFile.class)) + ret = (DAO) new NetworkFileDAO(); + else if (clazz.equals(NetworkFolder.class)) + ret = (DAO) new NetworkFolderDAO(); + else if (clazz.equals(Peer.class)) + ret = (DAO) new PeerDAO(); + else if (clazz.equals(PeerFileState.class)) + ret = (DAO) new PeerFileStateDAO(); + else if (clazz.equals(NetworkFSNode.class)) + ret = (DAO) new NetworkFSNodeDAO(); + else + throw new UnsupportedOperationException("Requested DAO for unsupported type " + clazz.getCanonicalName()); + + daos.put(clazz, ret); + return (DAO) ret; + } + + @Override + public FSNodeDAO getFSDAO() + { + return new NetworkFSNodeDAO(); + } + + private abstract class JSONObjectDAO implements DAO + { + protected final Set<@NotNull Class> VALID_JSON_CLASSES = Set.of(new Class[]{Boolean.class, Double.class, Integer.class, Long.class, String.class, JSONArray.class, JSONObject.class, JSONObject.NULL.getClass()}); + + protected abstract String getNamespace(); + protected File getNamespaceDirectory() + { + File ret = new File(storageDirectory, getNamespace()); + if (!ret.exists()) + ret.mkdir(); + return ret; + } + protected abstract T makeBlank(long id); + protected T jsonToObject(JSONObject json) + { + long id = json.getLong("storageID"); + T ret = makeBlank(id); + ret.updateFromStateMap(jsonToMap(json)); + return ret; + } + + protected JSONObject objectToJson(T object) + { + Map stateMap = object.getStateMap(); + return mapToJson(stateMap).put("storageID", object.getStorageID()); + } + + private JSONObject mapToJson(Map stateMap) + { + JSONObject ret = new JSONObject(); + for (Map.Entry e : stateMap.entrySet()) + { + Class valueClass = e.getValue().getClass(); + if (VALID_JSON_CLASSES.contains(valueClass)) + ret.put(e.getKey(), e.getValue()); + else if (Collection.class.isAssignableFrom(valueClass)) + { + Collection valueCollection = (Collection) e.getValue(); + List writeList = new ArrayList<>(valueCollection.size()); + for (Object item: valueCollection) + if (VALID_JSON_CLASSES.contains(item.getClass())) + writeList.add(item); + else + writeList.add(serialiseWeirdObject(item)); + ret.put(e.getKey(), writeList); + } + else if (Map.class.isAssignableFrom(valueClass)) + { + Map valueMap = (Map) e.getValue(); + return mapToJson(valueMap); + } + else + { + ret.put(e.getKey(), serialiseWeirdObject(e.getValue())); + } + } + return ret; + } + private Map jsonToMap(JSONObject json) + { + Map ret = new HashMap<>(); + for (String key : json.keySet()) + { + ret.put(key,deserialiseSomething(json.get(key))); + } + return ret; + } + + protected Object deserialiseSomething(Object value) + { + Object ret = value; + if (value instanceof JSONObject) + { + JSONObject valueJsonObject = (JSONObject) value; + if (valueJsonObject.has("weirdObjectClass")) + ret = deserialiseWeirdObject(valueJsonObject); + else + ret = jsonToMap(valueJsonObject); + } + else if (value instanceof JSONArray) + { + JSONArray valueJsonArray = (JSONArray) value; + List readList = new ArrayList(valueJsonArray.length()); + for (int i = 0; i < valueJsonArray.length(); i++) + { + Object item = deserialiseSomething(valueJsonArray.get(i)); + readList.add(item); + } + return readList; + } + return ret; + } + + protected JSONObject serialiseWeirdObject(Object value) throws IllegalArgumentException + { + throw new IllegalArgumentException("Don't know how to serialise a " + value.getClass().getCanonicalName() ); + } + protected Object deserialiseWeirdObject(JSONObject json) + { + throw new IllegalArgumentException("Don't know how to deserialise that."); + } + + @Override + public boolean exists(long id) + { + File file = new File(getNamespaceDirectory(), Long.toHexString(id) + ".json"); + return file.exists(); + } + + @Override + public List getAll() + { + List ret = new ArrayList<>(); + + // get all files in the storage directory + for (File file : Objects.requireNonNull(getNamespaceDirectory().listFiles())) + { + try + { + JSONObject json = new JSONObject(Files.readString(file.toPath())); + ret.add(jsonToObject(json)); + } catch (IOException e) + { + e.printStackTrace(System.err); + } + } + + return ret; + } + + @Override + public T create(long id) + { + T ret = makeBlank(id); + update(ret); + return ret; + } + + @Override + public T get(long id) + { + File file = new File(getNamespaceDirectory(), Long.toHexString(id) + ".json"); + try + { + JSONObject json = new JSONObject(Files.readString(file.toPath())); + return jsonToObject(json); + } catch (IOException e) + { + throw new RuntimeException(e); + } + } + + @Override + public void update(T object) + { + File file = new File(getNamespaceDirectory(), Long.toHexString(object.getStorageID()) + ".json"); + try(FileWriter writer = new FileWriter(file, false)) + { + objectToJson(object).write(writer); + writer.flush(); + } catch (IOException e) + { + throw new RuntimeException(e); + } + } + } + + private abstract class NetworkObjectDAO extends JSONObjectDAO + { + @Override + protected String getNamespace() + { + return "networkObjects"; + } + } + + private class NetworkFSNodeDAO extends ClassFusionDAO implements FSNodeDAO + { + public NetworkFSNodeDAO() + { + super(NetworkFile.class, NetworkFolder.class); + } + } + + private class NetworkFileDAO extends NetworkObjectDAO + { + @Override + protected String getNamespace() + { + return super.getNamespace() + "/files"; + } + + @Override + protected NetworkFile makeBlank(long id) + { + return new NetworkFile(new NetworkObject.ObjectID(id)); + } + } + + private class NetworkFolderDAO extends NetworkObjectDAO + { + @Override + protected String getNamespace() + { + return super.getNamespace() + "/folders"; + } + @Override + protected NetworkFolder makeBlank(long id) + { + return new NetworkFolder(new NetworkObject.ObjectID(id)); + } + } + + private class PeerDAO extends NetworkObjectDAO + { + @Override + protected String getNamespace() + { + return super.getNamespace() + "/peers"; + } + @Override + protected Peer makeBlank(long id) + { + return new Peer(new NetworkObject.ObjectID(id)); + } + + @Override + protected JSONObject serialiseWeirdObject(Object value) throws IllegalArgumentException + { + if (value instanceof URI) + { + URI uri = (URI) value; + return new JSONObject().put("weirdObjectClass", URI.class.getCanonicalName()).put("uri",uri.toString()); + } + return super.serialiseWeirdObject(value); + } + + @Override + protected Object deserialiseWeirdObject(JSONObject json) + { + String weirdType = json.getString("weirdObjectClass"); + try + { + Class weirdClass = Class.forName(weirdType); + if (weirdClass == URI.class) + return new URI(json.getString("uri")); + } catch (ClassNotFoundException e) + { + throw new IllegalArgumentException(e); + } catch (URISyntaxException e) + { + throw new RuntimeException(e); + } + + return super.deserialiseWeirdObject(json); + } + } + + private class PeerFileStateDAO extends NetworkObjectDAO + { + @Override + protected String getNamespace() + { + return super.getNamespace() + "/peerFileStates"; + } + @Override + protected PeerFileState makeBlank(long id) + { + return new PeerFileState(new NetworkObject.ObjectID(id)); + } + } +} diff --git a/src/main/java/moe/nekojimi/friendcloud/tasks/JoinNetworkTask.java b/src/main/java/moe/nekojimi/friendcloud/tasks/JoinNetworkTask.java index 857653a..a6b1bd2 100644 --- a/src/main/java/moe/nekojimi/friendcloud/tasks/JoinNetworkTask.java +++ b/src/main/java/moe/nekojimi/friendcloud/tasks/JoinNetworkTask.java @@ -1,7 +1,6 @@ package moe.nekojimi.friendcloud.tasks; import moe.nekojimi.friendcloud.Main; -import moe.nekojimi.friendcloud.Model; import moe.nekojimi.friendcloud.ObjectChangeTransaction; import moe.nekojimi.friendcloud.objects.NetworkObject; import moe.nekojimi.friendcloud.objects.Peer; @@ -19,11 +18,11 @@ public class JoinNetworkTask implements Runnable NetworkObject.ObjectID peerID = null; try (ObjectChangeTransaction builder = ObjectChangeTransaction.startTransaction(Main.getInstance().getConnectionManager(), peerID)) { - Peer selfPeer = Model.getInstance().getSelfPeer(); + Peer selfPeer = Main.getInstance().getModel().getSelfPeer(); if (selfPeer != null) peerID = selfPeer.getObjectID(); else - peerID = Model.getInstance().getNextObjectID(ObjectStatements.ObjectType.OBJECT_TYPE_PEER); + peerID = Main.getInstance().getModel().getNextObjectID(ObjectStatements.ObjectType.OBJECT_TYPE_PEER); // synchronise with the network SyncWithNetworkTask syncWithNetworkTask = new SyncWithNetworkTask(); diff --git a/src/main/java/moe/nekojimi/friendcloud/tasks/PropagateMessageTask.java b/src/main/java/moe/nekojimi/friendcloud/tasks/PropagateMessageTask.java index 1253510..48a52f8 100644 --- a/src/main/java/moe/nekojimi/friendcloud/tasks/PropagateMessageTask.java +++ b/src/main/java/moe/nekojimi/friendcloud/tasks/PropagateMessageTask.java @@ -3,10 +3,8 @@ package moe.nekojimi.friendcloud.tasks; import com.google.protobuf.Message; import moe.nekojimi.friendcloud.ConnectionManager; import moe.nekojimi.friendcloud.Main; -import moe.nekojimi.friendcloud.Model; import moe.nekojimi.friendcloud.network.PeerConnection; import moe.nekojimi.friendcloud.objects.Peer; -import moe.nekojimi.friendcloud.protos.CommonMessages; import java.io.IOException; @@ -23,7 +21,7 @@ public class PropagateMessageTask implements Runnable public void run() { ConnectionManager connectionManager = Main.getInstance().getConnectionManager(); - for (Peer peer: Model.getInstance().listOtherPeers()) + for (Peer peer: Main.getInstance().getModel().listOtherPeers()) { try { diff --git a/src/main/java/moe/nekojimi/friendcloud/tasks/SyncWithNetworkTask.java b/src/main/java/moe/nekojimi/friendcloud/tasks/SyncWithNetworkTask.java index ff5d66f..a396b94 100644 --- a/src/main/java/moe/nekojimi/friendcloud/tasks/SyncWithNetworkTask.java +++ b/src/main/java/moe/nekojimi/friendcloud/tasks/SyncWithNetworkTask.java @@ -1,7 +1,6 @@ package moe.nekojimi.friendcloud.tasks; import moe.nekojimi.friendcloud.Main; -import moe.nekojimi.friendcloud.Model; import moe.nekojimi.friendcloud.ObjectChangeRecord; import moe.nekojimi.friendcloud.network.PeerConnection; import moe.nekojimi.friendcloud.network.requests.ObjectChangeRequest; @@ -20,14 +19,14 @@ public class SyncWithNetworkTask implements Runnable public void run() { // for each other peer: - for (Peer peer : Model.getInstance().listOtherPeers()) + for (Peer peer : Main.getInstance().getModel().listOtherPeers()) { // open a connection try { PeerConnection connection = Main.getInstance().getConnectionManager().getNodeConnection(peer); // send a ObjectChangeRequest - ObjectChangeRequest objectChangeRequest = new ObjectChangeRequest(Model.getInstance().getChangeHeads().stream().map(ObjectChangeRecord::getChangeID).collect(Collectors.toSet())); + ObjectChangeRequest objectChangeRequest = new ObjectChangeRequest(Main.getInstance().getModel().getChangeHeads().stream().map(ObjectChangeRecord::getChangeID).collect(Collectors.toSet())); CompletableFuture> future = connection.makeRequest(objectChangeRequest); // integrate the returned changes with our change graph