Initial commit (demo as distributed to Cloudy)
This commit is contained in:
commit
fa687c2968
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
|
@ -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
|
5
.idea/.gitignore
vendored
Normal file
5
.idea/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Environment-dependent path to Maven home directory
|
||||
/mavenHomeManager.xml
|
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
7
.idea/encodings.xml
Normal file
7
.idea/encodings.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
20
.idea/misc.xml
Normal file
20
.idea/misc.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EntryPointsManager">
|
||||
<writeAnnotations>
|
||||
<writeAnnotation name="Parameter" />
|
||||
<writeAnnotation name="com.beust.jcommander.Parameter" />
|
||||
</writeAnnotations>
|
||||
</component>
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
142
pom.xml
Normal file
142
pom.xml
Normal file
|
@ -0,0 +1,142 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>moe.nekojimi.friendcloud</groupId>
|
||||
<artifactId>FriendCloudProto</artifactId>
|
||||
<version>0.0.69</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<protobuf.version>3.9.2</protobuf.version>
|
||||
<generated.sourceDirectory>target/generated-sources</generated.sourceDirectory>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.github.serceman</groupId>
|
||||
<artifactId>jnr-fuse</artifactId>
|
||||
<version>0.5.8</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
<version>${protobuf.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.offbynull.portmapper</groupId>
|
||||
<artifactId>portmapper</artifactId>
|
||||
<version>2.0.7-NEKOJIMI-PATCH</version>
|
||||
</dependency>
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.bitlet</groupId>-->
|
||||
<!-- <artifactId>weupnp</artifactId>-->
|
||||
<!-- <version>RELEASE</version>-->
|
||||
<!-- </dependency>-->
|
||||
<dependency>
|
||||
<groupId>org.jcommander</groupId>
|
||||
<artifactId>jcommander</artifactId>
|
||||
<version>2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.github.ascopes</groupId>
|
||||
<artifactId>protobuf-maven-plugin</artifactId>
|
||||
<version>3.7.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
<version>16.0.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<version>2.0.17</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>2.0.17</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kstruct</groupId>
|
||||
<artifactId>gethostname4j</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.github.ascopes</groupId>
|
||||
<artifactId>protobuf-maven-plugin</artifactId>
|
||||
|
||||
<configuration>
|
||||
<protocVersion>${protobuf.version}</protocVersion>
|
||||
</configuration>
|
||||
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>generate</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<!-- <plugin>-->
|
||||
<!-- <groupId>com.github.os72</groupId>-->
|
||||
<!-- <artifactId>protoc-jar-maven-plugin</artifactId>-->
|
||||
<!-- <version>3.9.2</version>-->
|
||||
<!-- <executions>-->
|
||||
<!-- <execution>-->
|
||||
<!-- <phase>generate-sources</phase>-->
|
||||
<!-- <goals>-->
|
||||
<!-- <goal>run</goal>-->
|
||||
<!-- </goals>-->
|
||||
<!-- <configuration>-->
|
||||
<!-- <protocVersion>${protobuf.version}</protocVersion>-->
|
||||
<!-- <inputDirectories>-->
|
||||
<!-- <include>src/main/protobuf</include>-->
|
||||
<!-- </inputDirectories>-->
|
||||
<!-- </configuration>-->
|
||||
<!-- </execution>-->
|
||||
<!-- </executions>-->
|
||||
<!-- </plugin>-->
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<configuration>
|
||||
<mainClass>moe.nekojimi.friendcloud.Main</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>moe.nekojimi.friendcloud.Main</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
139
src/main/java/moe/nekojimi/friendcloud/ConnectionManager.java
Normal file
139
src/main/java/moe/nekojimi/friendcloud/ConnectionManager.java
Normal file
|
@ -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<PeerConnection> activeConnections = new HashSet<>();
|
||||
|
||||
private final Set<Consumer<PeerConnection>> 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<PeerConnection> 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<PeerConnection> deadConnections = new HashSet<>();
|
||||
for (PeerConnection peerConnection: activeConnections)
|
||||
{
|
||||
if (!peerConnection.isAlive())
|
||||
deadConnections.add(peerConnection);
|
||||
}
|
||||
activeConnections.removeAll(deadConnections);
|
||||
}
|
||||
|
||||
public void addNewConnectionConsumer(Consumer<PeerConnection> consumer)
|
||||
{
|
||||
newConnectionConsumers.add(consumer);
|
||||
}
|
||||
|
||||
public void removeNewConnectionConsumer(Consumer<PeerConnection> consumer)
|
||||
{
|
||||
newConnectionConsumers.remove(consumer);
|
||||
}
|
||||
|
||||
}
|
138
src/main/java/moe/nekojimi/friendcloud/FileDownloadTask.java
Normal file
138
src/main/java/moe/nekojimi/friendcloud/FileDownloadTask.java
Normal file
|
@ -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<File>
|
||||
{
|
||||
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<Integer> 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<Integer> 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<Peer, PeerFileState> fileStates = file.getFileStates();
|
||||
|
||||
// determine what nodes we can connect to
|
||||
List<PeerConnection> 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<CompletableFuture<List<Integer>>> fileFutures = new ArrayList<>();
|
||||
int offset = 0;
|
||||
for (PeerConnection connection : connections)
|
||||
{
|
||||
CompletableFuture<List<Integer>> 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<List<Integer>> future : fileFutures)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<Integer> 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();
|
||||
}
|
||||
}
|
75
src/main/java/moe/nekojimi/friendcloud/FilePieceAccess.java
Normal file
75
src/main/java/moe/nekojimi/friendcloud/FilePieceAccess.java
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
85
src/main/java/moe/nekojimi/friendcloud/FileRemoteAccess.java
Normal file
85
src/main/java/moe/nekojimi/friendcloud/FileRemoteAccess.java
Normal file
|
@ -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<Integer> 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<File> 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;
|
||||
}
|
||||
|
||||
}
|
286
src/main/java/moe/nekojimi/friendcloud/Main.java
Normal file
286
src/main/java/moe/nekojimi/friendcloud/Main.java
Normal file
|
@ -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<String> sharedFiles = new ArrayList<>();
|
||||
|
||||
@Parameter(names="-known-peer")
|
||||
private List<String> 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<List<NetworkObject>> 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<PortMapper> 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;
|
||||
}
|
||||
}
|
139
src/main/java/moe/nekojimi/friendcloud/Model.java
Normal file
139
src/main/java/moe/nekojimi/friendcloud/Model.java
Normal file
|
@ -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<NetworkObject.ObjectID, NetworkObject> objects = new HashMap<>();
|
||||
private final int systemID;
|
||||
|
||||
private Peer selfPeer = null;
|
||||
private ObjectChangeRecord changeHead;
|
||||
private final Map<Long, ObjectChangeRecord> 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<Long, Node> 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<NetworkObject.ObjectID> listObjects(Set<ObjectStatements.ObjectType> 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<NetworkFSNode> listFSNodes(String path)
|
||||
{
|
||||
//TODO: dumbest algorithm in the world
|
||||
|
||||
List<NetworkFSNode> 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!");
|
||||
}
|
||||
}
|
|
@ -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<ObjectChangeRecord> 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<ObjectChangeRecord> getChangeHeads()
|
||||
{
|
||||
return Collections.unmodifiableSet(changeHeads);
|
||||
}
|
||||
}
|
|
@ -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<Long, FileRemoteAccess> fileAccessors = new HashMap<>();
|
||||
private final Map<Long, Integer> 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);
|
||||
}
|
||||
}
|
|
@ -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<Long, Request<?, ?>> 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 <T> CompletableFuture<T> makeRequest(Request<?, T> 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<NetworkObject.ObjectID> 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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<PieceMessages.FilePiecesRequestMessage, List<Integer>>
|
||||
{
|
||||
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<Long> expectedPieces = new ArrayList<>();
|
||||
private final List<Integer> 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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package moe.nekojimi.friendcloud.network.requests;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class Response<T> extends CompletableFuture<T>
|
||||
{
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
233
src/main/java/moe/nekojimi/friendcloud/objects/NetworkFile.java
Normal file
233
src/main/java/moe/nekojimi/friendcloud/objects/NetworkFile.java
Normal file
|
@ -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<Peer, PeerFileState> fileStates = new HashMap<>();
|
||||
private BitSet pieces = new BitSet();
|
||||
|
||||
private static File tempDirectory = null;
|
||||
// private List<FilePiece> 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<FilePiece> 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<FilePiece> getPieces()
|
||||
// {
|
||||
// return pieces;
|
||||
// }
|
||||
|
||||
// private void setPieces(List<FilePiece> 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<Peer, PeerFileState> getFileStates()
|
||||
{
|
||||
return fileStates;
|
||||
}
|
||||
}
|
|
@ -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<ObjectID> 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
123
src/main/java/moe/nekojimi/friendcloud/objects/Peer.java
Normal file
123
src/main/java/moe/nekojimi/friendcloud/objects/Peer.java
Normal file
|
@ -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<URI> addresses = new ArrayList<>();
|
||||
private String userName = "";
|
||||
private String systemName = "";
|
||||
|
||||
private Map<NetworkFile, PeerFileState> 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<String,String> 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<NetworkFile, PeerFileState> getFileStates()
|
||||
{
|
||||
return fileStates;
|
||||
}
|
||||
|
||||
public List<URI> 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
43
src/main/protobuf/CommonMessages.proto
Normal file
43
src/main/protobuf/CommonMessages.proto
Normal file
|
@ -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 {
|
||||
}
|
49
src/main/protobuf/ObjectStatements.proto
Normal file
49
src/main/protobuf/ObjectStatements.proto
Normal file
|
@ -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<string,string> 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;
|
||||
|
||||
}
|
46
src/main/protobuf/PieceMessages.proto
Normal file
46
src/main/protobuf/PieceMessages.proto
Normal file
|
@ -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;
|
||||
}
|
9
src/main/protobuf/TestMessage.proto
Normal file
9
src/main/protobuf/TestMessage.proto
Normal file
|
@ -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;
|
||||
}
|
Loading…
Reference in a new issue