commit fa687c29688179700dd7a7575c22cd3fb5795dbe Author: Nekojimi Date: Tue Sep 2 12:22:20 2025 +0100 Initial commit (demo as distributed to Cloudy) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..a0ccf77 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Environment-dependent path to Maven home directory +/mavenHomeManager.xml diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..07a8c5e --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..8281a29 --- /dev/null +++ b/pom.xml @@ -0,0 +1,142 @@ + + + 4.0.0 + + moe.nekojimi.friendcloud + FriendCloudProto + 0.0.69 + + + 21 + 21 + UTF-8 + 3.9.2 + target/generated-sources + + + + + com.github.serceman + jnr-fuse + 0.5.8 + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + com.offbynull.portmapper + portmapper + 2.0.7-NEKOJIMI-PATCH + + + + + + + + org.jcommander + jcommander + 2.0 + + + + io.github.ascopes + protobuf-maven-plugin + 3.7.0 + + + org.jetbrains + annotations + 16.0.1 + compile + + + org.slf4j + slf4j-simple + 2.0.17 + + + org.slf4j + slf4j-api + 2.0.17 + + + com.kstruct + gethostname4j + 1.0.0 + + + + + + + + io.github.ascopes + protobuf-maven-plugin + + + ${protobuf.version} + + + + + + generate + + + + + + + + + + + + + + + + + + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.3.0 + + moe.nekojimi.friendcloud.Main + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + + + moe.nekojimi.friendcloud.Main + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/moe/nekojimi/friendcloud/ConnectionManager.java b/src/main/java/moe/nekojimi/friendcloud/ConnectionManager.java new file mode 100644 index 0000000..ff4f0b7 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/ConnectionManager.java @@ -0,0 +1,139 @@ +package moe.nekojimi.friendcloud; + +import moe.nekojimi.friendcloud.network.PeerConnection; +import moe.nekojimi.friendcloud.network.PeerTCPConnection; +import moe.nekojimi.friendcloud.objects.Peer; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URI; +import java.util.*; +import java.util.function.Consumer; + +public class ConnectionManager extends Thread +{ +// private final Executor executor = new ThreadPoolExecutor() + //TODO: move the TCP stuff to it's own thread, which sends NodeTCPConnections to this thread + + private final ServerSocket serverSocket; + + private final Set activeConnections = new HashSet<>(); + + private final Set> newConnectionConsumers = new HashSet<>(); + + public ConnectionManager(int portNumber) throws IOException + { + serverSocket = new ServerSocket(portNumber); +// serverSocket.bind(new InetSocketAddress()); + } + + @Override + public void run() + { + super.run(); + while (!serverSocket.isClosed()) + { + try + { + Socket socket = serverSocket.accept(); + + System.out.println("TCP Connection Manager: accepted connection from " + socket.getRemoteSocketAddress()); + + PeerTCPConnection nodeTCPConnection = new PeerTCPConnection(socket); + activeConnections.add(nodeTCPConnection); + nodeTCPConnection.start(); + + for (Consumer consumer: newConnectionConsumers) + { + consumer.accept(nodeTCPConnection); + } + } catch (IOException e) + { + System.err.println("ConnectionManager TCP experienced exception:" + e.getMessage()); + e.printStackTrace(System.err); + } + } + } + + public PeerConnection getNodeConnection(URI uri) throws IOException + { + return getNodeConnection(uri, null); + } + + public PeerConnection getNodeConnection(URI uri, Peer peer) throws IOException + { + purgeDeadConnections(); + for (PeerConnection peerConnection: activeConnections) + { + if (peerConnection.getUri() == uri) + return peerConnection; + } + + PeerConnection nodeConnection = null; + if (Objects.equals(uri.getScheme(), "tcp")) + { + nodeConnection = new PeerTCPConnection(uri, peer); + nodeConnection.start(); + } + if (nodeConnection != null) + activeConnections.add(nodeConnection); + return nodeConnection; + } + + public PeerConnection getNodeConnection(Peer peer) throws IOException + { + // try to find if we already have an active connection to this peer + purgeDeadConnections(); + for (PeerConnection peerConnection: activeConnections) + { + if (peerConnection.getNode() == peer) + return peerConnection; + } + + for (URI address : peer.getAddresses()) + { + try + { + return getNodeConnection(address); + } + catch (IOException ex) + { + System.err.println("Couldn't create PeerConnection to " + address + " : " + ex.getMessage()); + } + } + System.err.println("Failed to create PeerConnection to " + peer + "!"); + return null; + } + + public void shutdown() throws IOException + { + serverSocket.close(); + for (PeerConnection nc: activeConnections) + { + nc.shutdown(); + } + } + + private void purgeDeadConnections() + { + Set deadConnections = new HashSet<>(); + for (PeerConnection peerConnection: activeConnections) + { + if (!peerConnection.isAlive()) + deadConnections.add(peerConnection); + } + activeConnections.removeAll(deadConnections); + } + + public void addNewConnectionConsumer(Consumer consumer) + { + newConnectionConsumers.add(consumer); + } + + public void removeNewConnectionConsumer(Consumer consumer) + { + newConnectionConsumers.remove(consumer); + } + +} diff --git a/src/main/java/moe/nekojimi/friendcloud/FileDownloadTask.java b/src/main/java/moe/nekojimi/friendcloud/FileDownloadTask.java new file mode 100644 index 0000000..f776e1d --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/FileDownloadTask.java @@ -0,0 +1,138 @@ + +package moe.nekojimi.friendcloud; + +import moe.nekojimi.friendcloud.network.PeerConnection; +import moe.nekojimi.friendcloud.network.requests.FilePiecesRequest; +import moe.nekojimi.friendcloud.objects.NetworkFile; +import moe.nekojimi.friendcloud.objects.Peer; +import moe.nekojimi.friendcloud.objects.PeerFileState; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.*; + +public class FileDownloadTask implements Callable +{ + private final NetworkFile file; + private final ConnectionManager manager; + + private final long timeoutPerPieceMs = 10_000; + private static final int MAX_DOWNLOAD_PIECES_PER_ROUND = 128; + + private final SortedSet missingPieceIndices = new TreeSet<>(); + + public FileDownloadTask(NetworkFile file, ConnectionManager manager) + { + this.file = file; + this.manager = manager; + for (int i = 0; i < file.getPieceCount(); i++) + { + missingPieceIndices.add(i); + } + } + + public FileDownloadTask(NetworkFile file, ConnectionManager manager, List missingPieces) + { + this.file = file; + this.manager = manager; + missingPieceIndices.addAll(missingPieces); + } + + public NetworkFile getFile() + { + return file; + } + + @Override + public File call() throws Exception + { + System.out.println("Starting download of file " + file.getName()); + + while (!missingPieceIndices.isEmpty()) + { + System.out.println("Need to get " + missingPieceIndices.size() + " missing pieces."); + + Map fileStates = file.getFileStates(); + + // determine what nodes we can connect to + List connections = new ArrayList<>(); + for (PeerFileState peerFileState : fileStates.values()) + { + if (peerFileState.getProgress() >= 100.0) + { + try + { + PeerConnection connection = manager.getNodeConnection(peerFileState.getNode()); + System.out.println("FileDownloadTask: Will download from " + peerFileState.getNode().getNodeName()); + connections.add(connection); + } catch (IOException ex) + { + System.err.println("Failed to connect to peer " + peerFileState.getNode().getNodeName() + ": " + ex.getMessage()); + } + } + } + + + if (connections.isEmpty()) + { + System.err.println("FileDownloadTask: No peers have the file, download failed!"); + return null; + } + + // find a continuous run of pieces to download + // TODO: allow for runs with regular gaps (e.g. every 2) to account for previous failed download attempts + int runStart = -1; + int runEnd = -1; + for (int pieceIdx: missingPieceIndices) + { + int runLength = runEnd - runStart; + if (runLength >= MAX_DOWNLOAD_PIECES_PER_ROUND) + break; + else if (runStart == -1) + { + runStart = pieceIdx; + runEnd = pieceIdx; + } + else if (pieceIdx == runEnd + 1) + runEnd = pieceIdx; + else + break; + } + System.out.println("FileDownloadTask: Will download pieces from " + runStart + " to " + runEnd); + + // make one request per connectable peer, striping the needed pieces among them + List>> fileFutures = new ArrayList<>(); + int offset = 0; + for (PeerConnection connection : connections) + { + CompletableFuture> future = connection.makeRequest(new FilePiecesRequest(file, runStart+offset, (runEnd-runStart)+1, connections.size())); + fileFutures.add(future); + offset++; + } + + long timeout = timeoutPerPieceMs * (missingPieceIndices.size() / connections.size()); + + // wait for all the requests to complete + for (CompletableFuture> future : fileFutures) + { + try + { + List receivedPieces = future.get(timeout, TimeUnit.MILLISECONDS); + receivedPieces.forEach(missingPieceIndices::remove); + } catch (InterruptedException e) + { + future.cancel(true); + timeout = 1_000; + System.err.println("FileDownloadTask: Request timed out."); + } catch (ExecutionException | TimeoutException e) + { + e.printStackTrace(System.err); + } + } + } + + System.out.println("FileDownloadTask: finished downloading " + file.getName() + "!"); + return file.getLocalFile(); + } +} diff --git a/src/main/java/moe/nekojimi/friendcloud/FilePieceAccess.java b/src/main/java/moe/nekojimi/friendcloud/FilePieceAccess.java new file mode 100644 index 0000000..6c6caa9 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/FilePieceAccess.java @@ -0,0 +1,75 @@ +package moe.nekojimi.friendcloud; + +import moe.nekojimi.friendcloud.objects.NetworkFile; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; + +public class FilePieceAccess implements Closeable +{ + private final NetworkFile networkFile; + private final File file; + private final RandomAccessFile randomAccessFile; + + public FilePieceAccess(NetworkFile networkFile) throws IOException + { + this.networkFile = networkFile; + this.file = networkFile.getOrCreateLocalFile(); + this.randomAccessFile = new RandomAccessFile(file,"rw"); + randomAccessFile.setLength(file.length()); + } + + public int getPieceOffset(int index) + { + return Math.toIntExact(index * networkFile.getPieceSize()); + } + + public int getPieceSize(int index) + { + if (index != networkFile.getPieceCount()-1) + return Math.toIntExact(networkFile.getPieceSize()); + int ret = Math.toIntExact(networkFile.getSize() % networkFile.getPieceSize()); + if (ret == 0) + ret = Math.toIntExact(networkFile.getPieceSize()); + return ret; + } + + public byte[] readPiece(int index) throws IOException + { + if (index >= networkFile.getPieceCount()) + throw new IllegalArgumentException("Piece index out of range!!"); + if (index < 0) + throw new IllegalArgumentException("Piece index is negative!!"); + if (!networkFile.hasPiece(index)) + return null; + + int pieceSize = getPieceSize(index); + byte[] buffer = new byte[pieceSize]; + int pieceOffset = getPieceOffset(index); + System.out.println("Reading piece " + index + " from file " + file.getName() + " (offset=" + pieceOffset + ", size=" + pieceSize + ")"); + randomAccessFile.seek(pieceOffset); + randomAccessFile.read(buffer); + return buffer; + } + + public void writePiece(int index, byte[] buffer) throws IOException + { + if (buffer.length != getPieceSize(index)) + throw new IllegalArgumentException("Received a file piece that's the wrong size!! Length = " + buffer.length + " != Piece Size = " + getPieceSize(index)); + else if (index >= networkFile.getPieceCount()) + throw new IllegalArgumentException("Received a file piece with an index past the end of the file!!"); + + randomAccessFile.seek(getPieceOffset(index)); + randomAccessFile.write(buffer); + + networkFile.setHasPiece(index, true); + } + + @Override + public void close() throws IOException + { + randomAccessFile.close(); + } +} diff --git a/src/main/java/moe/nekojimi/friendcloud/FileRemoteAccess.java b/src/main/java/moe/nekojimi/friendcloud/FileRemoteAccess.java new file mode 100644 index 0000000..88f26b4 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/FileRemoteAccess.java @@ -0,0 +1,85 @@ +package moe.nekojimi.friendcloud; + +import moe.nekojimi.friendcloud.objects.NetworkFile; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +public class FileRemoteAccess +{ + private final NetworkFile file; + private static final double PREEMPTIVE_DOWNLOAD_THRESHOLD = 5; + +// private FilePieceAccess access; + + public FileRemoteAccess(NetworkFile file) + { + this.file = file; + } + + public synchronized byte[] read(long offset, long size) throws IOException + { + if (offset >= file.getSize()) + return new byte[0]; + List missingPieces = new ArrayList<>(); + long pieceSize = file.getPieceSize(); + int startPieceIdx = Math.toIntExact(Math.floorDiv(offset, pieceSize)); + long endOffset = (offset + size); + if (endOffset >= file.getSize()) + endOffset = file.getSize(); + int endPieceIdx = Math.toIntExact(Math.floorDiv(endOffset - 1, pieceSize)); + for (int pieceIdx = startPieceIdx; pieceIdx <= endPieceIdx; pieceIdx++) + { + if (!file.hasPiece(pieceIdx)) + missingPieces.add(pieceIdx); + } + + System.out.println("FRA: offset=" + offset + ", endOffset=" + endOffset + ", startPieceIdx=" + startPieceIdx + ", endPieceIdx=" + endPieceIdx); + + if (!missingPieces.isEmpty()) + { + System.out.println("FRA: need to get missing pieces " + missingPieces); + FileDownloadTask downloadTask = new FileDownloadTask(file, Main.getInstance().getConnectionManager(), missingPieces); + FutureTask futureTask = new FutureTask<>(downloadTask); + Main.getInstance().getExecutor().submit(futureTask); + + try + { + futureTask.get(); + } catch (InterruptedException | ExecutionException e) + { + e.printStackTrace(System.err); + return null; + } + } + +// if (file.getDownloadPercentage() >= PREEMPTIVE_DOWNLOAD_THRESHOLD) +// { +// FileDownloadTask preemptiveDownloadTask = new FileDownloadTask(file, Main.getInstance().getConnectionManager(), ) +// } + + File localFile = file.getLocalFile(); + if (localFile == null) + { + System.err.println("FRA: No local file, read failed!"); + return null; + } + + long readSize = endOffset - offset; + + byte[] ret; + try (RandomAccessFile randomAccessFile = new RandomAccessFile(localFile, "r")) + { + ret = new byte[Math.toIntExact(readSize)]; + randomAccessFile.seek(offset); + randomAccessFile.read(ret); + } + return ret; + } + +} diff --git a/src/main/java/moe/nekojimi/friendcloud/Main.java b/src/main/java/moe/nekojimi/friendcloud/Main.java new file mode 100644 index 0000000..6b7f723 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/Main.java @@ -0,0 +1,286 @@ +package moe.nekojimi.friendcloud; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.kstruct.gethostname4j.Hostname; +import com.offbynull.portmapper.PortMapperFactory; +import com.offbynull.portmapper.gateway.Bus; +import com.offbynull.portmapper.gateway.Gateway; +import com.offbynull.portmapper.gateways.network.NetworkGateway; +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 jnr.ffi.Platform; +import moe.nekojimi.friendcloud.filesystem.FUSEAccess; +import moe.nekojimi.friendcloud.network.PeerConnection; +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 org.slf4j.simple.SimpleLogger; +import org.xml.sax.SAXException; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.File; +import java.io.IOException; +import java.net.*; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.*; + +public class Main +{ + private static Main instance; + + @Parameter(names="-share") + private List sharedFiles = new ArrayList<>(); + + @Parameter(names="-known-peer") + private List knownPeers = new ArrayList<>(); + + @Parameter(names="-tcp-port") + private int tcpPort = 7777; + + @Parameter(names="-no-upnp") + private boolean noUpnp = false; + +// @Parameter(names="-file") + + private ConnectionManager connectionManager; + + private final ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(16); + + private final FUSEAccess fuseAccess = new FUSEAccess(); + + public static void main(String[] args) + { + instance = new Main(); + JCommander.newBuilder().addObject(instance).build().parse(args); + + System.setProperty(SimpleLogger.DEFAULT_LOG_LEVEL_KEY, "Info"); + + try + { + instance.run(); + } catch (IOException | InterruptedException e) + { + e.printStackTrace(System.err); + try + { + instance.shutdown(); + } catch (IOException f) + { + throw new RuntimeException(f); + } + System.exit(1); + } +// TestMessage.SearchRequest request = TestMessage.SearchRequest.newBuilder().setQuery("bees!").setPageNumber(316).setResultsPerPage(42069).build(); + } + + private void run() throws IOException, InterruptedException + { + connectionManager = new ConnectionManager(tcpPort); + + Path mountPoint; + if (Platform.getNativePlatform().getOS() == Platform.OS.WINDOWS) + { + mountPoint = Paths.get("J:\\"); + } + else + { + mountPoint = Path.of(System.getProperty("user.dir") + "/fuse-mount-" + tcpPort); + boolean created = mountPoint.toFile().mkdirs(); + System.out.println("Created FUSE mount point " + mountPoint); + + } + fuseAccess.mount(mountPoint); + System.out.println("Mounted virtual filesystem at " + mountPoint); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> + { + try + { + shutdown(); + } catch (IOException e) + { + throw new RuntimeException(e); + } + })); + + connectionManager.addNewConnectionConsumer(this::resquestCompleteState); + + connectionManager.start(); + + String hostname = Hostname.getHostname(); + Model.getInstance().getSelfNode().setSystemName(hostname); + Model.getInstance().getSelfNode().setUserName(System.getProperty("user.name") + "-" + tcpPort); + addHostAddress(InetAddress.getLocalHost()); + + /* + Startup procedure: + - Start up UPnP + - Connect to all known nodes + - Request all changes since most recent + - Apply changes to objects + - Scan local files for changes + - Publish local file changes + */ + + if (!noUpnp) + setupIGP(); + + for (String sharedFilePath: sharedFiles) + { + File file = new File(sharedFilePath); + if (file.exists()) + { + System.out.println("Adding shared network file: " + file.getAbsolutePath()); + + NetworkFile networkFile = (NetworkFile) Model.getInstance().createObjectByType(ObjectStatements.ObjectType.OBJECT_TYPE_FILE); + networkFile.updateFromLocalFile(file); + + PeerFileState peerFileState = (PeerFileState) Model.getInstance().createObjectByType(ObjectStatements.ObjectType.OBJECT_TYPE_PEER_FILE_STATE); + peerFileState.setNode(Model.getInstance().getSelfNode()); + peerFileState.setFile(networkFile); + peerFileState.setProgress(100); + } + } + + for (String knownPeerAddress : knownPeers) + { + String[] split = knownPeerAddress.split(":"); + if (split.length != 2) + { + System.err.println("ERROR: " + knownPeerAddress + " isn't a valid address."); + continue; + } + InetSocketAddress address = new InetSocketAddress(split[0],Integer.parseInt(split[1])); + + try + { + 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); +// } +// } +// }); + + + } catch (ConnectException ex) + { + System.out.println("Couldn't connect to host " + address); + } + catch (URISyntaxException e) + { + throw new RuntimeException(e); + } + } + } + + private void resquestCompleteState(PeerConnection nodeConnection) + { + CompletableFuture> objectListFuture = nodeConnection.makeRequest(new ObjectListRequest(Set.of( + ObjectStatements.ObjectType.OBJECT_TYPE_FILE, + ObjectStatements.ObjectType.OBJECT_TYPE_PEER_FILE_STATE, + ObjectStatements.ObjectType.OBJECT_TYPE_PEER))); + } + + private void addHostAddress(InetAddress address) + { + String host = address.getCanonicalHostName(); + Peer selfNode = Model.getInstance().getSelfNode(); + try + { + URI uri = new URI("tcp", null, host, tcpPort, null, null, null); + System.out.println("Added local address " + uri); + selfNode.addAddress(uri); + } catch (URISyntaxException e) + { + throw new RuntimeException(e); + } + } + + private void setupIGP() throws InterruptedException + { + try + { + // Start gateways + Gateway network = NetworkGateway.create(); + Gateway process = ProcessGateway.create(); + Bus networkBus = network.getBus(); + Bus processBus = process.getBus(); + + // Discover port forwarding devices and take the first one found + System.out.println("Discovering port mappers..."); + List mappers = PortMapperFactory.discover(networkBus, processBus);; + PortMapper mapper = mappers.getFirst(); + System.out.println("Got mapper " + mapper + ", mapping port..."); + + MappedPort mappedPort = mapper.mapPort(PortType.TCP, tcpPort, tcpPort, 60); + System.out.println("Port mapping added: " + mappedPort); + + addHostAddress(mappedPort.getExternalAddress()); + } + catch (IllegalStateException ex) + { + System.err.println("Failed to map port! error=" + ex.getMessage()); + ex.printStackTrace(System.err); + } + } + + private void shutdown() throws IOException + { + fuseAccess.umount(); + connectionManager.shutdown(); + executor.shutdown(); + System.out.println("Waiting 10 seconds to complete tasks..."); + boolean terminated = false; + try + { + terminated = executor.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) + { + throw new RuntimeException(e); + } + if (!terminated) + { + System.out.println("Timed out, ending tasks now. Goodbye!"); + executor.shutdownNow(); + } + else + { + System.out.println("Finished everything. Goodbye!"); + } + } + + public static Main getInstance() + { + return instance; + } + + public ScheduledExecutorService getExecutor() + { + return executor; + } + + public ConnectionManager getConnectionManager() + { + return connectionManager; + } +} \ 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 new file mode 100644 index 0000000..6089c89 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/Model.java @@ -0,0 +1,139 @@ +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 changeHead; + private final Map changeRecords = new HashMap<>(); + + private Model() + { + Random ran = new Random(); + systemID = ran.nextInt() & 0x00FFFFFF; + } + + public synchronized Peer getSelfNode() + { + 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(changeHead)) + throw new IllegalStateException("Change does not apply! Valid change heads=" + record.getChangeHeads() + ", we are in state " + changeHead.getChangeID()); + if (!changeRecords.containsKey(record.getChangeID())) + addChangeRecord(record); + + +// if (record == null) +// throw new IllegalArgumentException("Cannot apply unknown change!"); + } +} diff --git a/src/main/java/moe/nekojimi/friendcloud/ObjectChangeRecord.java b/src/main/java/moe/nekojimi/friendcloud/ObjectChangeRecord.java new file mode 100644 index 0000000..d5f6b6d --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/ObjectChangeRecord.java @@ -0,0 +1,39 @@ +package moe.nekojimi.friendcloud; + +import moe.nekojimi.friendcloud.protos.ObjectStatements; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class ObjectChangeRecord +{ + private final long changeID; + + private ObjectChangeRecord(long changeID) + { + this.changeID = changeID; + } + + private final Set changeHeads = new HashSet<>(); + + public static ObjectChangeRecord createFromChangeMessage(ObjectStatements.ObjectChange objectChange) + { + throw new UnsupportedOperationException("NYI!"); + } + + public static ObjectChangeRecord createFromObjectStates(ObjectStatements.ObjectState before, ObjectStatements.ObjectState after) + { + throw new UnsupportedOperationException("NYI!"); + } + + public long getChangeID() + { + return changeID; + } + + public Set getChangeHeads() + { + return Collections.unmodifiableSet(changeHeads); + } +} diff --git a/src/main/java/moe/nekojimi/friendcloud/filesystem/FUSEAccess.java b/src/main/java/moe/nekojimi/friendcloud/filesystem/FUSEAccess.java new file mode 100644 index 0000000..d1c62e8 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/filesystem/FUSEAccess.java @@ -0,0 +1,159 @@ +package moe.nekojimi.friendcloud.filesystem; + +import jnr.ffi.Pointer; +import moe.nekojimi.friendcloud.FileRemoteAccess; +import moe.nekojimi.friendcloud.Model; +import moe.nekojimi.friendcloud.objects.NetworkFSNode; +import moe.nekojimi.friendcloud.objects.NetworkFile; +import moe.nekojimi.friendcloud.objects.NetworkFolder; +import ru.serce.jnrfuse.ErrorCodes; +import ru.serce.jnrfuse.FuseFillDir; +import ru.serce.jnrfuse.FuseStubFS; +import ru.serce.jnrfuse.struct.FileStat; +import ru.serce.jnrfuse.struct.FuseFileInfo; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class FUSEAccess extends FuseStubFS +{ + private static final int DIRECTORY_PERMISSIONS = 0755; + private static final int FILE_PERMISSIONS = 0444; + + private final Object fileLock = new Object(); + private final Map fileAccessors = new HashMap<>(); + private final Map fileOpenCounts = new HashMap<>(); + + @Override + public int readdir(String path, Pointer buf, FuseFillDir filter, long offset, FuseFileInfo fi) + { + System.out.println("FUSE: listing contents of directory " + path); + + int ret = 0; + filter.apply(buf, ".", null, 0); + filter.apply(buf, "..", null, 0); +// filter.apply(buf,"hello", null, 0); + + for (NetworkFSNode fsNode : Model.getInstance().listFSNodes(path)) + { + filter.apply(buf, fsNode.getName(), null, 0); + } + + return ret; + } + + @Override + public int getattr(String path, FileStat stat) + { +// System.out.println("FUSE: reading attributes of file " + path); + if (path.equals("/")) + { + // root directory is special + stat.st_mode.set(FileStat.S_IFDIR | DIRECTORY_PERMISSIONS); + stat.st_nlink.set(2); + } + else + { + NetworkFSNode fsNode = Model.getInstance().getFSNode(path); + switch (fsNode) + { + case null -> + { + return -ErrorCodes.ENOENT(); + } + case NetworkFile networkFile -> + { + stat.st_mode.set(FileStat.S_IFREG | FILE_PERMISSIONS); + stat.st_nlink.set(1); + stat.st_size.set(networkFile.getSize()); + } + case NetworkFolder networkFolder -> + { + stat.st_mode.set(FileStat.S_IFDIR | DIRECTORY_PERMISSIONS); + stat.st_nlink.set(2); + } + default -> + { + return -ErrorCodes.EIO(); + } + } + } + return super.getattr(path, stat); + } + + @Override + public int open(String path, FuseFileInfo fi) + { + System.out.println("FUSE: Opening file " + path); + NetworkFSNode fsNode = Model.getInstance().getFSNode(path); + if (fsNode == null) + { + System.err.println("FUSE: Failed to open file " + path + ": not found"); + return -ErrorCodes.ENOENT(); + } + if (fsNode instanceof NetworkFolder) + { + System.err.println("FUSE: Failed to open file " + path + ": is a directory"); + return -ErrorCodes.EISDIR(); + } + long fileID = fsNode.getObjectID().getId(); + synchronized (fileLock) + { + int openCount = fileOpenCounts.getOrDefault(fileID, 0) + 1; + System.out.println("FUSE: fh: " + fileID + " openCount:" + openCount); + fileOpenCounts.put(fileID, openCount); + if (!fileAccessors.containsKey(fileID)) + fileAccessors.put(fileID, new FileRemoteAccess((NetworkFile) fsNode)); + } + fi.fh.set(fileID); + return 0; + } + + @Override + public int release(String path, FuseFileInfo fi) + { + long fh = fi.fh.longValue(); + synchronized (fileLock) + { + Integer openCount = fileOpenCounts.getOrDefault(fh, 1); + openCount -= 1; + System.out.println("FUSE: releasing file " + path + "(fh:" + fh + ", openCount: " + openCount + ")"); + fileOpenCounts.put(fh, openCount); + if (openCount <= 0) + { + fileAccessors.remove(fh); + } + } + return 0; + } + + @Override + public int read(String path, Pointer buf, long size, long offset, FuseFileInfo fi) + { + System.out.println("FUSE: Reading from file " + path + ", offset=" + offset + ", size=" + size); + synchronized (fileLock) + { + FileRemoteAccess fileRemoteAccess = fileAccessors.get(fi.fh.longValue()); +// System.out.println("Got FRA"); + byte[] bytes = null; + try + { + bytes = fileRemoteAccess.read(offset, size); +// System.out.println("Read from FRA"); + } catch (Exception e) + { + e.printStackTrace(System.err); + return -ErrorCodes.EIO(); + } + if (bytes == null) + { + System.err.println("FUSE: failed to read: buffer empty"); + return -ErrorCodes.EIO(); + } + buf.put(0, bytes, 0, bytes.length); +// System.out.println("FUSE: Read " + bytes.length + " bytes."); + } + return Math.toIntExact(size); + } +} diff --git a/src/main/java/moe/nekojimi/friendcloud/network/PeerConnection.java b/src/main/java/moe/nekojimi/friendcloud/network/PeerConnection.java new file mode 100644 index 0000000..59cf0c2 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/network/PeerConnection.java @@ -0,0 +1,258 @@ +package moe.nekojimi.friendcloud.network; + + +import com.google.protobuf.Any; +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.objects.NetworkFile; +import moe.nekojimi.friendcloud.objects.NetworkObject; +import moe.nekojimi.friendcloud.objects.Peer; +import moe.nekojimi.friendcloud.protos.CommonMessages; +import moe.nekojimi.friendcloud.protos.ObjectStatements; +import moe.nekojimi.friendcloud.network.requests.Request; +import moe.nekojimi.friendcloud.protos.PieceMessages; + +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public abstract class PeerConnection extends Thread +{ + private final Map> pendingRequests = new HashMap<>(); + private Peer peer; + private long nextMessageId = 1; + private final URI uri; + private long artificalDelayMs = 0; + + public PeerConnection() + { + this(null); + } + + public PeerConnection(URI uri) + { + this.uri = uri; + } + + public PeerConnection(URI uri, Peer peer) + { + this(uri); + this.peer = peer; + } + + @Override + public void run() + { + super.run(); + System.out.println("Peer connection to " + uri + " started."); + } + + public synchronized CompletableFuture makeRequest(Request request) + { + if (!isAlive()) + throw new IllegalStateException("Request made to PeerConnection that isn't running!"); + try + { + Message message = request.buildMessage(); + + CommonMessages.FriendCloudMessage wrappedMessage = wrapMessage(message); + pendingRequests.put(wrappedMessage.getHeader().getMessageId(), request); + sendMessage(wrappedMessage); + + return request.getFuture(); + } catch (Exception e) + { + System.err.println("Request failed!"); + e.printStackTrace(System.err); + return CompletableFuture.failedFuture(e); + } + } + + protected abstract void sendMessage(CommonMessages.FriendCloudMessage message) throws IOException; + + private CommonMessages.FriendCloudMessage wrapMessage(Message message, CommonMessages.MessageHeader inReplyTo) + { + CommonMessages.MessageHeader.Builder headerBuilder = CommonMessages.MessageHeader.newBuilder() + .setMessageId(nextMessageId) + .setSenderId(Model.getInstance().getSelfNode().getObjectID().getId()); + + if (inReplyTo != null) + headerBuilder.setReplyToMessageId(inReplyTo.getMessageId()); + + CommonMessages.FriendCloudMessage ret = CommonMessages.FriendCloudMessage.newBuilder() + .setHeader(headerBuilder) + .setBody(Any.pack(message)).build(); + nextMessageId++; + return ret; + } + + private CommonMessages.FriendCloudMessage wrapMessage(Message message) + { + return wrapMessage(message, null); + } + + private void replyWithError(CommonMessages.Error error, CommonMessages.MessageHeader replyHeader) throws IOException + { + System.err.println("Sending error reply: " + error.name() + " to message ID " + replyHeader.getReplyToMessageId()); + CommonMessages.ErrorMessage errorMessage = CommonMessages.ErrorMessage.newBuilder().setError(error).build(); + sendMessage(wrapMessage(errorMessage,replyHeader)); + } + + protected void messageReceived(@org.jetbrains.annotations.NotNull CommonMessages.FriendCloudMessage message) + { + if (artificalDelayMs > 0) + { + try + { + System.err.println("WARNING: artifical lag activated! Waiting " + artificalDelayMs + "ms..."); + Thread.sleep(artificalDelayMs); + } catch (InterruptedException e) + { + // well never mind then + } + } + + CommonMessages.MessageHeader header = message.getHeader(); + + NetworkObject.ObjectID senderID = new NetworkObject.ObjectID(header.getSenderId()); + peer = (Peer) Model.getInstance().getOrCreateObject(senderID); + + Any body = message.getBody(); + + long replyToMessageId = header.getReplyToMessageId(); + System.out.println("Received message! type=" + body.getTypeUrl() + ", ID=" + header.getMessageId() + ", reply_to=" + replyToMessageId ); + + try + { + try + { + if (replyToMessageId != 0) + { + if (pendingRequests.containsKey(replyToMessageId)) + handleReplyMessage(header, body); + else if (body.is(CommonMessages.ErrorMessage.class)) + handleErrorToUnsolicitedMessage(header, body.unpack(CommonMessages.ErrorMessage.class)); + else + replyWithError(CommonMessages.Error.ERROR_NOT_EXPECTING_REPLY, header); + } + else + { + handleUnsolicitedMessage(header, body); + } + } + catch (ReplyWithErrorException ex) + { + ex.printStackTrace(System.err); + replyWithError(ex.getError(), header); + } + catch (IllegalArgumentException ex) + { + ex.printStackTrace(System.err); + replyWithError(CommonMessages.Error.ERROR_INVALID_ARGUMENT, header); + } +// catch (RuntimeException ex) +// { +// ex.printStackTrace(System.err); +// replyWithError(CommonMessages.Error.ERROR_INTERNAL, header); +// } + } catch (IOException ex) + { + ex.printStackTrace(System.err); + } + } + + private void handleErrorToUnsolicitedMessage(CommonMessages.MessageHeader header, CommonMessages.ErrorMessage body) + { + throw new RuntimeException("Our message ID " + header.getReplyToMessageId() + " caused a remote error: " + body.getError().name()); + } + + private void handleUnsolicitedMessage(CommonMessages.MessageHeader header, Any body) throws IOException, ReplyWithErrorException + { + if (body.is(ObjectStatements.ObjectListRequest.class)) + { + ObjectStatements.ObjectListRequest objectListRequest = body.unpack(ObjectStatements.ObjectListRequest.class); + List objectIDS = Model.getInstance().listObjects(new HashSet<>(objectListRequest.getTypesList())); + + ObjectStatements.ObjectList.Builder objectList = ObjectStatements.ObjectList.newBuilder(); + for (NetworkObject.ObjectID objectID : objectIDS) + { + NetworkObject networkObject = Model.getInstance().getOrCreateObject(objectID); + objectList.addStates(networkObject.buildObjectState()); +// networkObject.updateFromStateMessage(); +// objectList.addState(networkObject.buildObjectState()); + } + System.out.println("Replying to ObjectListRequest with ObjectList, objects=" + objectList.getStatesList()); + sendMessage(wrapMessage(objectList.build(), header)); + } + else if (body.is(PieceMessages.FilePiecesRequestMessage.class)) + { + PieceMessages.FilePiecesRequestMessage filePiecesRequestMessage = body.unpack(PieceMessages.FilePiecesRequestMessage.class); + if (filePiecesRequestMessage.getPieceMod() == 0) + { + replyWithError(CommonMessages.Error.ERROR_INVALID_ARGUMENT, header); + } + + NetworkFile networkFile = (NetworkFile) Model.getInstance().getObject(new NetworkObject.ObjectID(filePiecesRequestMessage.getFileId())); + if (networkFile == null) + { + replyWithError(CommonMessages.Error.ERROR_OBJECT_NOT_FOUND, header); + } + assert networkFile != null; + try (FilePieceAccess filePieceAccess = new FilePieceAccess(networkFile)) + { + int startIndex = filePiecesRequestMessage.getStartPieceIndex(); + int endIndex = (filePiecesRequestMessage.getStartPieceIndex() + filePiecesRequestMessage.getPieceCount()) - 1; + System.out.println("Been asked for pieces from " + startIndex + " to " + endIndex); + for (int index = startIndex; index <= endIndex; index += filePiecesRequestMessage.getPieceMod()) + { + byte[] buffer = filePieceAccess.readPiece(index); + if (buffer != null) + { + System.out.println("Replying to file piece request with piece " + index); + PieceMessages.FilePieceMessage filePieceMessage = PieceMessages.FilePieceMessage.newBuilder() + .setPieceIndex(index) + .setFileId(networkFile.getObjectID().getId()) + .setData(ByteString.copyFrom(buffer)) + .build(); + sendMessage(wrapMessage(filePieceMessage, header)); + } + else + { + System.err.println("Don't have requested piece " + index + "!"); + replyWithError(CommonMessages.Error.ERROR_PIECE_NOT_POSSESSED, header); + break; + } + } + } + } + } + + private void handleReplyMessage(CommonMessages.MessageHeader header, Any body) throws InvalidProtocolBufferException, ReplyWithErrorException + { + long replyToMessageId = header.getReplyToMessageId(); + System.out.println("Received reply to message ID " + replyToMessageId); + Request request = pendingRequests.get(replyToMessageId); + boolean doneWithRequest = request.handleReply(body); + if (doneWithRequest) + pendingRequests.remove(replyToMessageId); + } + + public abstract void shutdown() throws IOException; + + public synchronized Peer getNode() + { + return peer; + } + + public synchronized URI getUri() + { + return uri; + } +} diff --git a/src/main/java/moe/nekojimi/friendcloud/network/PeerTCPConnection.java b/src/main/java/moe/nekojimi/friendcloud/network/PeerTCPConnection.java new file mode 100644 index 0000000..d63e071 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/network/PeerTCPConnection.java @@ -0,0 +1,70 @@ +package moe.nekojimi.friendcloud.network; + +import moe.nekojimi.friendcloud.objects.Peer; +import moe.nekojimi.friendcloud.protos.CommonMessages; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.URI; + +public class PeerTCPConnection extends PeerConnection +{ + private final Socket socket; + private final int keepAliveTimeS = 300; + + public PeerTCPConnection(URI tcpURL, Peer peer) throws IOException + { + super(tcpURL, peer); + socket = new Socket(tcpURL.getHost(), tcpURL.getPort()); + System.out.println("TCP Connection: connected to " + tcpURL + " OK!"); + } + + public PeerTCPConnection(Socket openSocket) + { + super(); + socket = openSocket; + } + + @Override + public void run() + { + super.run(); + + try + { + InputStream inputStream = socket.getInputStream(); + while (!socket.isClosed()) + { + CommonMessages.FriendCloudMessage message = CommonMessages.FriendCloudMessage.parseDelimitedFrom(inputStream); +// Any any = Any.parseDelimitedFrom(inputStream); + + if (message != null) + { + System.out.println("TCP Connection: read data"); + + messageReceived(message); + } + } + } catch (IOException ex) + { + // fuck + } + } + + @Override + protected void sendMessage(CommonMessages.FriendCloudMessage message) throws IOException + { + OutputStream outputStream = socket.getOutputStream(); + System.out.println("Sending message " + message.getHeader().getMessageId()); + message.writeDelimitedTo(outputStream); + outputStream.flush(); + } + + @Override + public synchronized void shutdown() throws IOException + { + socket.close(); + } +} diff --git a/src/main/java/moe/nekojimi/friendcloud/network/ReplyWithErrorException.java b/src/main/java/moe/nekojimi/friendcloud/network/ReplyWithErrorException.java new file mode 100644 index 0000000..cbfb3b6 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/network/ReplyWithErrorException.java @@ -0,0 +1,25 @@ +package moe.nekojimi.friendcloud.network; + +import moe.nekojimi.friendcloud.protos.CommonMessages; + +public class ReplyWithErrorException extends Exception +{ + private final CommonMessages.Error error; + + public ReplyWithErrorException(String message, CommonMessages.Error error) + { + super(message); + this.error = error; + } + + public ReplyWithErrorException(CommonMessages.Error error) + { + super("Message raised error:" + error.name()); + this.error = error; + } + + public CommonMessages.Error getError() + { + return error; + } +} diff --git a/src/main/java/moe/nekojimi/friendcloud/network/requests/FilePiecesRequest.java b/src/main/java/moe/nekojimi/friendcloud/network/requests/FilePiecesRequest.java new file mode 100644 index 0000000..02daf24 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/network/requests/FilePiecesRequest.java @@ -0,0 +1,83 @@ +package moe.nekojimi.friendcloud.network.requests; + +import com.google.protobuf.Any; +import com.google.protobuf.InvalidProtocolBufferException; +import moe.nekojimi.friendcloud.FilePieceAccess; +import moe.nekojimi.friendcloud.objects.NetworkFile; +import moe.nekojimi.friendcloud.objects.NetworkObject; +import moe.nekojimi.friendcloud.protos.PieceMessages; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +public class FilePiecesRequest extends Request> +{ + private final NetworkFile file; + private final int startPiece; + private final int pieceCount; + private final int pieceMod; + + private FilePieceAccess filePieceAccess; + private int expectedPieceCount = 0; +// private List expectedPieces = new ArrayList<>(); + private final List receivedPieces = new ArrayList<>(); + + public FilePiecesRequest(NetworkFile file, int startPiece, int pieceCount, int pieceMod) + { + this.file = file; + this.startPiece = startPiece; + this.pieceCount = pieceCount; + this.pieceMod = pieceMod; + } + + @Override + public PieceMessages.FilePiecesRequestMessage buildMessage() + { + expectedPieceCount = Math.toIntExact(pieceCount / pieceMod); + return PieceMessages.FilePiecesRequestMessage.newBuilder() + .setFileId(file.getObjectID().getId()) + .setPieceCount(pieceCount) + .setPieceMod(pieceMod) + .setStartPieceIndex(startPiece) + .build(); + } + + @Override + public boolean handleReply(Any reply) throws InvalidProtocolBufferException + { + if (super.handleReply(reply)) + return true; + try + { + if (reply.is(PieceMessages.FilePieceMessage.class)) + { + expectedPieceCount--; + PieceMessages.FilePieceMessage filePieceMessage = reply.unpack(PieceMessages.FilePieceMessage.class); + byte[] buffer = filePieceMessage.getData().toByteArray(); + int index = Math.toIntExact(filePieceMessage.getPieceIndex()); + + if (filePieceAccess == null) + filePieceAccess = new FilePieceAccess(file); + + filePieceAccess.writePiece((int) index, buffer); + + receivedPieces.add(index); + } + } catch (IOException ex) + { + future.completeExceptionally(ex); + return true; + } + + if (expectedPieceCount <= 0) + { + future.complete(receivedPieces); + } + + return expectedPieceCount == 0; + } +} diff --git a/src/main/java/moe/nekojimi/friendcloud/network/requests/Response.java b/src/main/java/moe/nekojimi/friendcloud/network/requests/Response.java new file mode 100644 index 0000000..5aa699c --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/network/requests/Response.java @@ -0,0 +1,8 @@ +package moe.nekojimi.friendcloud.network.requests; + +import java.util.concurrent.CompletableFuture; + +public class Response extends CompletableFuture +{ + +} diff --git a/src/main/java/moe/nekojimi/friendcloud/objects/FilePiece.java b/src/main/java/moe/nekojimi/friendcloud/objects/FilePiece.java new file mode 100644 index 0000000..fefb85e --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/objects/FilePiece.java @@ -0,0 +1,59 @@ +package moe.nekojimi.friendcloud.objects; + +import com.google.protobuf.Message; + +import java.io.File; +import java.nio.ByteBuffer; + +public class FilePiece +{ + private final byte[] hash; + private final long size; + + private File localFile; + private long fileOffset; + + public FilePiece(byte[] hash, long size, File localFile, long fileOffset) + { + this.hash = hash; + this.size = size; + this.localFile = localFile; + this.fileOffset = fileOffset; + } + + public FilePiece(byte[] hash, long size) + { + this.hash = hash; + this.size = size; + } + + public File getLocalFile() + { + return localFile; + } + + public void setLocalFile(File localFile) + { + this.localFile = localFile; + } + + public long getFileOffset() + { + return fileOffset; + } + + public void setFileOffset(long fileOffset) + { + this.fileOffset = fileOffset; + } + + public byte[] getHash() + { + return hash; + } + + public long getSize() + { + return size; + } +} diff --git a/src/main/java/moe/nekojimi/friendcloud/objects/NetworkFSNode.java b/src/main/java/moe/nekojimi/friendcloud/objects/NetworkFSNode.java new file mode 100644 index 0000000..8bf14c2 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/objects/NetworkFSNode.java @@ -0,0 +1,54 @@ +package moe.nekojimi.friendcloud.objects; + +import moe.nekojimi.friendcloud.Model; +import moe.nekojimi.friendcloud.protos.ObjectStatements; + +public abstract class NetworkFSNode extends NetworkObject +{ + // private String path = ""; + protected NetworkFolder parent = null; + protected String name = ""; + + public NetworkFSNode(ObjectID objectID) + { + super(objectID); + } + + @Override + public synchronized void updateFromStateMessage(ObjectStatements.ObjectState state) + { + super.updateFromStateMessage(state); + if (state.containsValues("name")) + name = state.getValuesOrThrow("name"); + if (state.containsValues("parent")) + { + long parentID = Long.parseLong(state.getValuesOrThrow("parent")); + if (parentID != 0) + parent = (NetworkFolder) Model.getInstance().getOrCreateObject(new ObjectID(parentID)); + else + parent = null; + } + } + + @Override + public ObjectStatements.ObjectState.Builder buildObjectState() + { + return super.buildObjectState() + .putValues("name", getName()); + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getNetworkPath() + { + return (parent != null ? parent.getNetworkPath() : "") + "/" + name; + } +} diff --git a/src/main/java/moe/nekojimi/friendcloud/objects/NetworkFile.java b/src/main/java/moe/nekojimi/friendcloud/objects/NetworkFile.java new file mode 100644 index 0000000..3ddd686 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/objects/NetworkFile.java @@ -0,0 +1,233 @@ +package moe.nekojimi.friendcloud.objects; + +import moe.nekojimi.friendcloud.protos.ObjectStatements; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +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; + +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 long size = 0; + private long pieceSize = 0; + + private byte[] hash = {}; + + private File localFile = null; + + 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) + { + super(objectID); + } + + public void updateFromLocalFile(File localFile) throws IOException + { + this.localFile = localFile; + name = localFile.getName(); + size = localFile.length(); + pieceSize = MIN_PIECE_SIZE; + for (pieceSize = MIN_PIECE_SIZE; pieceSize < MAX_PIECE_SIZE; pieceSize *= 2) + { + long pieceCount = size / pieceSize; + if (pieceCount <= IDEAL_PIECE_COUNT) + break; + } + + pieces = new BitSet(Math.toIntExact(getPieceCount())); + + long offset = 0L; + System.out.println("Calculating hashes for file " + localFile.getName() + "(Piece size: " + pieceSize + ")"); + try (FileInputStream input = new FileInputStream(localFile)) + { + MessageDigest totalDigest = MessageDigest.getInstance("SHA-256"); + byte[] pieceBuf = new byte[Math.toIntExact(pieceSize)]; + int pieceIdx = 0; +// List pieces = new ArrayList<>(); + while(true) + { + int bytesRead = input.read(pieceBuf); + if (bytesRead <= 0) + break; + + // check to see if this piece is just zeroes, if so, assume it's a missing piece + boolean allZero = true; + for (byte b: pieceBuf) + { + if (b != 0) + { + allZero = false; + break; + } + } + pieces.set(pieceIdx, !allZero); + +// MessageDigest pieceDigest = MessageDigest.getInstance("SHA-256"); +// pieceDigest.update(pieceBuf, 0, bytesRead); +// byte[] pieceHash = pieceDigest.digest(); + totalDigest.update(pieceBuf, 0, bytesRead); + +// FilePiece piece = new FilePiece(pieceHash, bytesRead, localFile, offset); +// System.out.print(HexFormat.of().formatHex(pieceHash) + ", "); +// pieces.add(piece); + + pieceIdx++; + } + System.out.println(); + setLocalFile(localFile); + size = localFile.length(); +// setPieces(pieces); + setHash(totalDigest.digest()); + + System.out.println("Total hash: " + HexFormat.of().formatHex(hash)); + System.out.println("Have " + pieces.cardinality() + " of " + getPieceCount() + " pieces."); + + } catch (NoSuchAlgorithmException e) + { + throw new RuntimeException(e); + } + } + + @Override + public void updateFromStateMessage(ObjectStatements.ObjectState state) + { + super.updateFromStateMessage(state); +// if (state.containsValues("path")) +// path = state.getValuesOrThrow("path"); + if (state.containsValues("size")) + size = Long.parseLong(state.getValuesOrThrow("size")); + if (state.containsValues("hash")) + hash = HexFormat.of().parseHex(state.getValuesOrThrow("hash")); + if (state.containsValues("pieceSize")) + pieceSize = Long.parseLong(state.getValuesOrThrow("pieceSize")); + + } + + @Override + public ObjectStatements.ObjectState.Builder buildObjectState() + { + return super.buildObjectState() +// .putValues("path", path) + .putValues("size", Long.toString(size)) + .putValues("hash", HexFormat.of().formatHex(hash)) + .putValues("pieceSize", Long.toString(pieceSize)); + } + + public File getLocalFile() + { + return localFile; + } + + public File getOrCreateLocalFile() throws IOException + { + if (tempDirectory == null) + { + tempDirectory = Files.createTempDirectory("FriendCloud").toFile(); + tempDirectory.mkdirs(); + } + if (localFile == null) + { + localFile = new File(tempDirectory, getName()); +// localFile = File.createTempFile("FriendCloud", getNetworkPath()); + localFile.createNewFile(); + } + return localFile; + } + + public void setLocalFile(File localFile) + { + this.localFile = localFile; + } + + @Override + public String toString() + { + return "NetworkFile{" + + "hash='" + HexFormat.of().formatHex(hash) + '\'' + + ", size=" + size + + ", name='" + name + '\'' + + "} " + super.toString(); + } + + public long getPieceSize() + { + return pieceSize; + } + + private void setPieceSize(long pieceSize) + { + this.pieceSize = pieceSize; + } + +// public List getPieces() +// { +// return pieces; +// } + +// private void setPieces(List pieces) +// { +// this.pieces = pieces; +// } + + public byte[] getHash() + { + return hash; + } + + private void setHash(byte[] hash) + { + this.hash = hash; + } + + public int getPieceCount() + { + return Math.toIntExact(Math.ceilDiv(size, pieceSize)); + } + + public boolean hasPiece(int pieceIdx) + { + return pieces.get(pieceIdx); + } + + public void setHasPiece(int pieceIdx, boolean has) + { + pieces.set(pieceIdx, has); + } + + public double getDownloadPercentage() + { + return (pieces.cardinality() / (double) pieces.size()) * 100.0; + } + + public long getSize() + { + return size; + } + + void addFileState(PeerFileState peerFileState) + { + fileStates.put(peerFileState.getNode(), peerFileState); + } + + public Map getFileStates() + { + return fileStates; + } +} diff --git a/src/main/java/moe/nekojimi/friendcloud/objects/NetworkFolder.java b/src/main/java/moe/nekojimi/friendcloud/objects/NetworkFolder.java new file mode 100644 index 0000000..9e6da28 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/objects/NetworkFolder.java @@ -0,0 +1,25 @@ +package moe.nekojimi.friendcloud.objects; + +import moe.nekojimi.friendcloud.protos.ObjectStatements; + +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) + { + super(objectID); + } + + @Override + public ObjectStatements.ObjectState.Builder buildObjectState() + { + return super.buildObjectState().putValues("name", name); + } + + +} diff --git a/src/main/java/moe/nekojimi/friendcloud/objects/NetworkObject.java b/src/main/java/moe/nekojimi/friendcloud/objects/NetworkObject.java new file mode 100644 index 0000000..b483f38 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/objects/NetworkObject.java @@ -0,0 +1,101 @@ +package moe.nekojimi.friendcloud.objects; + +import moe.nekojimi.friendcloud.protos.ObjectStatements; + +import java.util.Objects; + +public abstract class NetworkObject +{ + private final ObjectID objectID; + + public NetworkObject(ObjectID objectID) + { + this.objectID = objectID; + } + + public ObjectID getObjectID() + { + return objectID; + } + + public synchronized void updateFromStateMessage(ObjectStatements.ObjectState state) + { + if (state.getObjectId() != objectID.getId()) + throw new IllegalArgumentException("Wrong object!"); + } + + public synchronized ObjectStatements.ObjectState mergeChanges(ObjectStatements.ObjectState a, ObjectStatements.ObjectState b) + { + return null; + } + + public ObjectStatements.ObjectState.Builder buildObjectState() + { + return ObjectStatements.ObjectState.newBuilder() + .setObjectId(objectID.getId()); + } + + public static class ObjectID + { + private final ObjectStatements.ObjectType type; + private final int systemID; + private final int uniqueID; + + public ObjectID(long id) + { + uniqueID = (int)(0x00000000_FFFFFFFFL & id); + systemID = Math.toIntExact((0x00FFFFFF_00000000L & id) >>> 32); + type = ObjectStatements.ObjectType.forNumber(Math.toIntExact(((0xFF000000_00000000L & id) >>> 56))); + } + + public ObjectID(ObjectStatements.ObjectType type, int systemID, int uniqueID) + { + this.type = type; + this.systemID = systemID; + this.uniqueID = uniqueID; + } + + public long getId() + { + long uniquePart = Integer.toUnsignedLong(uniqueID); + long systemPart = Integer.toUnsignedLong(systemID) << 32; + long typePart = ((long) type.getNumber()) << 56; + return typePart | systemPart | uniquePart; + } + + public ObjectStatements.ObjectType getType() + { + return type; + } + + public int getSystemID() + { + return systemID; + } + + public int getUniqueID() + { + return uniqueID; + } + + @Override + public String toString() + { + return "OBJ{" + Long.toHexString(getId()) + "}"; + } + + @Override + public boolean equals(Object o) + { + if (o == null || getClass() != o.getClass()) return false; + ObjectID objectID = (ObjectID) o; + return systemID == objectID.systemID && uniqueID == objectID.uniqueID && type == objectID.type; + } + + @Override + public int hashCode() + { + return Objects.hash(type, systemID, uniqueID); + } + } +} diff --git a/src/main/java/moe/nekojimi/friendcloud/objects/Peer.java b/src/main/java/moe/nekojimi/friendcloud/objects/Peer.java new file mode 100644 index 0000000..9e67ee5 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/objects/Peer.java @@ -0,0 +1,123 @@ +package moe.nekojimi.friendcloud.objects; + +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.stream.Collectors; + +public class Peer extends NetworkObject +{ + private final List addresses = new ArrayList<>(); + private String userName = ""; + private String systemName = ""; + + private Map fileStates = new HashMap<>(); + + private volatile int lastTriedAddressIdx = -1; + + public Peer(ObjectID objectID) + { + super(objectID); + } + + public String getNodeName() + { + return userName + "@" + systemName; + } + + @Override + public synchronized void updateFromStateMessage(ObjectStatements.ObjectState state) + { + super.updateFromStateMessage(state); + + Map values = state.getValuesMap(); + if (values.containsKey("userName")) + userName = values.get("userName"); + if (values.containsKey("systemName")) + systemName = values.get("systemName"); + if (values.containsKey("addresses")) + { + addresses.clear(); + String[] split = values.get("addresses").split(","); + for (String s: split) + { + try + { + addresses.add(new URI(s)); + } catch (URISyntaxException e) + { + throw new RuntimeException(e); + } + } + } +// if (values.containsKey("files")) + } + + @Override + public ObjectStatements.ObjectState.Builder buildObjectState() + { + ObjectStatements.ObjectState.Builder builder = super.buildObjectState(); + if (!userName.isEmpty()) + builder.putValues("userName", userName); + if (!systemName.isEmpty()) + builder.putValues("systemName", systemName); + if (!addresses.isEmpty()) + builder.putValues("addresses", + addresses.stream().map(URI::toString).collect(Collectors.joining(",")) + ); + return builder; + } + + public void addAddress(URI address) + { + addresses.add(address); + } + + public URI getNextAddress() + { + lastTriedAddressIdx++; + if (lastTriedAddressIdx >= addresses.size()) + lastTriedAddressIdx = 0; + return addresses.get(lastTriedAddressIdx); + } + + void addFileState(PeerFileState peerFileState) + { + fileStates.put(peerFileState.getFile(), peerFileState); + } + + public Map getFileStates() + { + return fileStates; + } + + public List getAddresses() + { + return addresses; + } + + public String getUserName() + { + return userName; + } + + public void setUserName(String userName) + { + this.userName = userName; + } + + public String getSystemName() + { + return systemName; + } + + public void setSystemName(String systemName) + { + this.systemName = systemName; + } +} diff --git a/src/main/java/moe/nekojimi/friendcloud/objects/PeerFileState.java b/src/main/java/moe/nekojimi/friendcloud/objects/PeerFileState.java new file mode 100644 index 0000000..6936dc4 --- /dev/null +++ b/src/main/java/moe/nekojimi/friendcloud/objects/PeerFileState.java @@ -0,0 +1,75 @@ +package moe.nekojimi.friendcloud.objects; + +import moe.nekojimi.friendcloud.Model; +import moe.nekojimi.friendcloud.protos.ObjectStatements; + +public class PeerFileState extends NetworkObject +{ + private Peer peer; + private NetworkFile file; + + private double progress = 0; + + public PeerFileState(ObjectID objectID) + { + super(objectID); + } + + @Override + 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")))); + if (state.containsValues("progress")) + progress = Double.parseDouble(state.getValuesOrThrow("progress")); + + peer.addFileState(this); + file.addFileState(this); + } + + @Override + public ObjectStatements.ObjectState mergeChanges(ObjectStatements.ObjectState a, ObjectStatements.ObjectState b) + { + return super.mergeChanges(a, b); + } + + @Override + public ObjectStatements.ObjectState.Builder buildObjectState() + { + return super.buildObjectState() + .putValues("peer", Long.toString(peer.getObjectID().getId())) + .putValues("file", Long.toString(file.getObjectID().getId())) + .putValues("progress", Double.toString(progress)); + } + + public void setNode(Peer peer) + { + this.peer = peer; + } + + public void setFile(NetworkFile file) + { + this.file = file; + } + + public double getProgress() + { + return progress; + } + + public void setProgress(double progress) + { + this.progress = progress; + } + + public NetworkFile getFile() + { + return file; + } + + public Peer getNode() + { + return peer; + } +} diff --git a/src/main/protobuf/CommonMessages.proto b/src/main/protobuf/CommonMessages.proto new file mode 100644 index 0000000..d4ecb02 --- /dev/null +++ b/src/main/protobuf/CommonMessages.proto @@ -0,0 +1,43 @@ +syntax = "proto3"; + +package moe.nekojimi.friendcloud.protos; + +import "google/protobuf/any.proto"; + +message MessageHeader { + uint64 sender_id = 1; + uint64 message_id = 2; + uint64 reply_to_message_id = 3; +} + +message FriendCloudMessage { + MessageHeader header = 1; + google.protobuf.Any body = 2; +} + +message LoginMessage { +} + +enum Error { + ERROR_UNSPECIFIED = 0; + ERROR_WHO_THE_FUCK_ARE_YOU = 1; // sender unidentified or unauthenticated + ERROR_PERMISSION_DENIED = 2; // you can't do that + ERROR_OBJECT_NOT_FOUND = 3; // one or more object(s) specified don't exist + ERROR_INTERNAL = 4; // internal error + ERROR_OUT_OF_DATE = 5; // your action is impossible because you have an out-of-date state (in a way that matters) + ERROR_CHECKSUM_FAILURE = 6; // a supplied checksum didn't match the relevant data + ERROR_PIECE_NOT_POSSESSED = 7; // you asked for a file piece I don't have + ERROR_INVALID_ARGUMENT = 8; // an argument specified is outside the expected range + ERROR_NOT_EXPECTING_REPLY = 9; // you sent a reply to a message that I wasn't expecting a reply to + ERROR_INVALID_PROTOBUF = 10; // your message couldn't be decoded at all +} + +message ErrorMessage { + Error error = 1; +} + +message PingMessage { +} + +message PongMessage { +} \ No newline at end of file diff --git a/src/main/protobuf/ObjectStatements.proto b/src/main/protobuf/ObjectStatements.proto new file mode 100644 index 0000000..c097e12 --- /dev/null +++ b/src/main/protobuf/ObjectStatements.proto @@ -0,0 +1,49 @@ +syntax = "proto3"; + +package moe.nekojimi.friendcloud.protos; + +import "CommonMessages.proto"; + +enum ObjectType { + OBJECT_TYPE_UNSPECIFIED = 0; + OBJECT_TYPE_USER = 1; + OBJECT_TYPE_PEER = 2; + OBJECT_TYPE_FILE = 3; + OBJECT_TYPE_FOLDER = 4; + OBJECT_TYPE_PEER_FILE_STATE = 5; +} + +message ObjectState { + uint64 object_id = 1; + map values = 2; +} + +message ObjectStateMessage { + repeated uint64 change_heads = 1; + repeated ObjectState states = 2; +} + +message ObjectStateRequest { + repeated uint64 object_ids = 1; + repeated string keys = 2; +} + +message ObjectChange { + uint64 change_id = 1; + repeated uint64 change_heads = 2; + repeated ObjectState states = 3; +} + +message ObjectChangeRequest { + repeated uint64 changes_since = 1; +} + +message ObjectList { + uint64 change_heads = 1; + repeated ObjectState states = 2; +} + +message ObjectListRequest { + repeated ObjectType types = 1; + +} \ No newline at end of file diff --git a/src/main/protobuf/PieceMessages.proto b/src/main/protobuf/PieceMessages.proto new file mode 100644 index 0000000..7018266 --- /dev/null +++ b/src/main/protobuf/PieceMessages.proto @@ -0,0 +1,46 @@ +syntax = "proto3"; + +package moe.nekojimi.friendcloud.protos; + +import "CommonMessages.proto"; + +message FilePieceChange { + uint64 file_id = 1; + repeated uint64 changed_index = 2; +} + +message FilePieceChangeMessage { + uint64 change_id = 1; + repeated FilePieceChange changes = 2; +} + +message FilePieceChangeRequestMessage { + uint64 change_id = 1; +} + +message FilePiecesRequestMessage { + uint64 file_id = 1; + uint32 start_piece_index = 2; + uint32 piece_count = 3; + uint32 piece_mod = 4; +} + +message FilePiecesQueryMessage { + uint64 file_id = 1; +} + +message FilePiecesQueryResultMessage { + bytes bitfield = 1; +} + +//message PieceRequestMessage { +// bytes piece_hash = 1; +//} +// +message FilePieceMessage +{ + bytes piece_hash = 1; + uint64 file_id = 2; + uint32 piece_index = 3; + bytes data = 4; +} \ No newline at end of file diff --git a/src/main/protobuf/TestMessage.proto b/src/main/protobuf/TestMessage.proto new file mode 100644 index 0000000..a253a7a --- /dev/null +++ b/src/main/protobuf/TestMessage.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option java_package = "moe.nekojimi.friendcloud.protos"; + +message SearchRequest { + string query = 1; + int32 page_number = 2; + int32 results_per_page = 3; +} \ No newline at end of file