diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..835e8dc --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.gradle +.idea +build \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2066b7c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: java + +jdk: +- oraclejdk8 + +sudo: false +addons: + apt: + packages: + - oracle-java8-installer \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..98839f4 --- /dev/null +++ b/build.gradle @@ -0,0 +1,48 @@ +group 'ru.spbau.shevchenko' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile group: 'org.jetbrains', name: 'annotations', version: '13.0' + testCompile group: 'junit', name: 'junit', version: '4.11' + compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.0' +} + +task serverJar(type: Jar) { + manifest { + attributes 'Implementation-Title': 'Ftp server jar file', + 'Implementation-Version': version, + 'Main-Class': 'server.ServerConsoleApp' + } + baseName = 'ftp-server' + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } + with jar +} + +task consoleClientJar(type: Jar) { + manifest { + attributes 'Implementation-Title': 'Ftp console client jar file', + 'Implementation-Version': version, + 'Main-Class': 'client.ClientConsoleApp' + } + baseName = 'ftp-client-console' + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } + with jar +} +task guiClientJar(type: Jar) { + manifest { + attributes 'Implementation-Title': 'Ftp gui client jar file', + 'Implementation-Version': version, + 'Main-Class': 'client.gui.ClientGuiApp' + } + baseName = 'ftp-client-gui' + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } + with jar +} \ No newline at end of file diff --git a/build/classes/main/console/Main.class b/build/classes/main/console/Main.class new file mode 100644 index 0000000..90f3af8 Binary files /dev/null and b/build/classes/main/console/Main.class differ diff --git a/build/classes/main/index b/build/classes/main/index new file mode 100644 index 0000000..e69de29 diff --git a/build/classes/main/staged b/build/classes/main/staged new file mode 100644 index 0000000..e69de29 diff --git a/build/classes/main/vcs/Blob.class b/build/classes/main/vcs/Blob.class new file mode 100644 index 0000000..b16682c Binary files /dev/null and b/build/classes/main/vcs/Blob.class differ diff --git a/build/classes/main/vcs/BlobSHARef.class b/build/classes/main/vcs/BlobSHARef.class new file mode 100644 index 0000000..d65cca9 Binary files /dev/null and b/build/classes/main/vcs/BlobSHARef.class differ diff --git a/build/classes/main/vcs/Branch.class b/build/classes/main/vcs/Branch.class new file mode 100644 index 0000000..d88cab5 Binary files /dev/null and b/build/classes/main/vcs/Branch.class differ diff --git a/build/classes/main/vcs/BranchAlreadyExistsException.class b/build/classes/main/vcs/BranchAlreadyExistsException.class new file mode 100644 index 0000000..cdfc546 Binary files /dev/null and b/build/classes/main/vcs/BranchAlreadyExistsException.class differ diff --git a/build/classes/main/vcs/BranchNotFoundException.class b/build/classes/main/vcs/BranchNotFoundException.class new file mode 100644 index 0000000..f4813a4 Binary files /dev/null and b/build/classes/main/vcs/BranchNotFoundException.class differ diff --git a/build/classes/main/vcs/Branches.class b/build/classes/main/vcs/Branches.class new file mode 100644 index 0000000..fdd1a49 Binary files /dev/null and b/build/classes/main/vcs/Branches.class differ diff --git a/build/classes/main/vcs/CheckoutStagedNotEmptyException.class b/build/classes/main/vcs/CheckoutStagedNotEmptyException.class new file mode 100644 index 0000000..446de44 Binary files /dev/null and b/build/classes/main/vcs/CheckoutStagedNotEmptyException.class differ diff --git a/build/classes/main/vcs/Commit.class b/build/classes/main/vcs/Commit.class new file mode 100644 index 0000000..73ffdee Binary files /dev/null and b/build/classes/main/vcs/Commit.class differ diff --git a/build/classes/main/vcs/CommitRef.class b/build/classes/main/vcs/CommitRef.class new file mode 100644 index 0000000..cb719ef Binary files /dev/null and b/build/classes/main/vcs/CommitRef.class differ diff --git a/build/classes/main/vcs/CommitSHARef.class b/build/classes/main/vcs/CommitSHARef.class new file mode 100644 index 0000000..a0dbbf3 Binary files /dev/null and b/build/classes/main/vcs/CommitSHARef.class differ diff --git a/build/classes/main/vcs/ContentfulBlob.class b/build/classes/main/vcs/ContentfulBlob.class new file mode 100644 index 0000000..ab1465b Binary files /dev/null and b/build/classes/main/vcs/ContentfulBlob.class differ diff --git a/build/classes/main/vcs/ContentlessBlob.class b/build/classes/main/vcs/ContentlessBlob.class new file mode 100644 index 0000000..8117851 Binary files /dev/null and b/build/classes/main/vcs/ContentlessBlob.class differ diff --git a/build/classes/main/vcs/DeleteActiveBranchException.class b/build/classes/main/vcs/DeleteActiveBranchException.class new file mode 100644 index 0000000..816e8f4 Binary files /dev/null and b/build/classes/main/vcs/DeleteActiveBranchException.class differ diff --git a/build/classes/main/vcs/EmptyCommitMessageException.class b/build/classes/main/vcs/EmptyCommitMessageException.class new file mode 100644 index 0000000..ca6aed1 Binary files /dev/null and b/build/classes/main/vcs/EmptyCommitMessageException.class differ diff --git a/build/classes/main/vcs/GitObject.class b/build/classes/main/vcs/GitObject.class new file mode 100644 index 0000000..9926128 Binary files /dev/null and b/build/classes/main/vcs/GitObject.class differ diff --git a/build/classes/main/vcs/MergeWhenStagedNotEmptyException.class b/build/classes/main/vcs/MergeWhenStagedNotEmptyException.class new file mode 100644 index 0000000..1664648 Binary files /dev/null and b/build/classes/main/vcs/MergeWhenStagedNotEmptyException.class differ diff --git a/build/classes/main/vcs/NothingToCommitException.class b/build/classes/main/vcs/NothingToCommitException.class new file mode 100644 index 0000000..13a5347 Binary files /dev/null and b/build/classes/main/vcs/NothingToCommitException.class differ diff --git a/build/classes/main/vcs/RepoState.class b/build/classes/main/vcs/RepoState.class new file mode 100644 index 0000000..d5e6fbb Binary files /dev/null and b/build/classes/main/vcs/RepoState.class differ diff --git a/build/classes/main/vcs/SHARef.class b/build/classes/main/vcs/SHARef.class new file mode 100644 index 0000000..d4da28b Binary files /dev/null and b/build/classes/main/vcs/SHARef.class differ diff --git a/build/classes/main/vcs/VCS.class b/build/classes/main/vcs/VCS.class new file mode 100644 index 0000000..bd3832e Binary files /dev/null and b/build/classes/main/vcs/VCS.class differ diff --git a/build/classes/main/vcs/VCSException.class b/build/classes/main/vcs/VCSException.class new file mode 100644 index 0000000..96d13c4 Binary files /dev/null and b/build/classes/main/vcs/VCSException.class differ diff --git a/build/classes/main/vcs/VCSFiles.class b/build/classes/main/vcs/VCSFiles.class new file mode 100644 index 0000000..07b729a Binary files /dev/null and b/build/classes/main/vcs/VCSFiles.class differ diff --git a/build/classes/main/vcs/VCSFilesCorruptedException.class b/build/classes/main/vcs/VCSFilesCorruptedException.class new file mode 100644 index 0000000..0fbf3b7 Binary files /dev/null and b/build/classes/main/vcs/VCSFilesCorruptedException.class differ diff --git a/build/classes/main/vcs/WrongArgumentsException.class b/build/classes/main/vcs/WrongArgumentsException.class new file mode 100644 index 0000000..3d5b17d Binary files /dev/null and b/build/classes/main/vcs/WrongArgumentsException.class differ diff --git a/build/classes/test/vcs/TestFile.class b/build/classes/test/vcs/TestFile.class new file mode 100644 index 0000000..369ff1b Binary files /dev/null and b/build/classes/test/vcs/TestFile.class differ diff --git a/build/classes/test/vcs/VCSTest.class b/build/classes/test/vcs/VCSTest.class new file mode 100644 index 0000000..857b448 Binary files /dev/null and b/build/classes/test/vcs/VCSTest.class differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..ca78035 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e842cab --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Apr 27 12:04:40 MSK 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..27309d9 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..832fdb6 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..8af3acb --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'FTP' + diff --git a/src/main/java/client/Client.java b/src/main/java/client/Client.java new file mode 100644 index 0000000..c64a749 --- /dev/null +++ b/src/main/java/client/Client.java @@ -0,0 +1,83 @@ +package client; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import utils.SimpleFile; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +/** + * Interface of a simple FTP client. + */ + +public interface Client { + /** + * Enum of all possible client queries. + */ + enum Query { + /** + * Query to list all files in directory + */ + LIST, + /** + * Query to download file. + */ + GET + } + + /** + * Tries to establish connection with server hostname:port. + * @param hostname the hostname of server + * @param port the port number + * @throws IOException If some I/O error during connection occurs + */ + void connect(@NotNull String hostname, int port) throws IOException; + + /** + * Closes connection with server. + * @throws IOException If an I/O error during connection close occurs + */ + void disconnect() throws IOException; + + /** + * Return list of files located in the specified directory. + * @param path The path to the directory + * @return List with all files in the directory. + * @throws IOException If an I/O error occurs + */ + @NotNull + List executeList(@NotNull String path) throws IOException; + + /** + * Downloads file. + * @param path path to file on server + * @return path to downloaded file on client + * @throws IOException If an I/O error occurs + */ + @NotNull + default Path executeGet(@NotNull String path) throws IOException { + return executeGet(path, null); + } + + /** + * Downloads file to a specified directory. + * @param path path to file on server + * @param destinationFolder path to destination folder on client + * @return path to downloaded file on client + * @throws IOException If an I/O error occurs + */ + @NotNull + Path executeGet(@NotNull String path, @Nullable String destinationFolder) throws IOException ; + + /** + * Creates non-blocking FTP client. + * @return A new FTP client + * @throws IOException If an I/O error occurs + */ + @NotNull + static Client getNonBlocking() throws IOException { + return new NonBlockingClient(); + } +} diff --git a/src/main/java/client/ClientConsoleApp.java b/src/main/java/client/ClientConsoleApp.java new file mode 100644 index 0000000..665d786 --- /dev/null +++ b/src/main/java/client/ClientConsoleApp.java @@ -0,0 +1,42 @@ +package client; + +import utils.SimpleFile; + +import java.io.IOException; +import java.nio.file.Path; + +/** + * Console application for communication with FTP server. + */ + +public class ClientConsoleApp { + /** + * Main method. + * @param args command line args + * @throws IOException If an I/O error occurs + */ + public static void main(String[] args) throws IOException { + Client client = Client.getNonBlocking(); + System.out.println("heh"); + client.connect(args[1], Integer.valueOf(args[2])); + System.out.println("meh"); + switch (args[0]) { + case "get": { + Path path = client.executeGet(args[3]); + System.out.println("Downloaded file to " + path); + break; + } + case "list": { + for (SimpleFile file : client.executeList(args[3])) { + System.out.println(file.name + " " + file.isDirectory); + } + break; + } + default: { + System.out.println("Usage is: get PATH or list PATH"); + } + + } + client.disconnect(); + } +} diff --git a/src/main/java/client/NonBlockingClient.java b/src/main/java/client/NonBlockingClient.java new file mode 100644 index 0000000..b02c617 --- /dev/null +++ b/src/main/java/client/NonBlockingClient.java @@ -0,0 +1,105 @@ +package client; + +import org.apache.commons.lang3.ArrayUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import utils.*; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.net.InetSocketAddress; +import java.nio.channels.NotYetConnectedException; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +/** + * Class that implements non-blocking FTP client. + */ + +class NonBlockingClient implements Client{ + + private final SocketChannel channel; + + NonBlockingClient() throws IOException { + channel = SocketChannel.open(); + channel.configureBlocking(false); + } + + @Override + public void connect(@NotNull String hostname, int port) throws IOException { + if (channel.isConnected()) return; + channel.connect(new InetSocketAddress(hostname, port)); + + //noinspection StatementWithEmptyBody + while(!channel.finishConnect()) { + //System.out.println("not connected"); + } + } + @Override + public void disconnect() throws IOException { + channel.close(); + } + + @NotNull + @Override + public List executeList(@NotNull String path) throws IOException { + sendRequest(createSentData((byte) Query.LIST.ordinal(), path.getBytes(StandardCharsets.UTF_8))); + byte[] response = getSmallResponse(); + ObjectInputStream objectStream = new ObjectInputStream(new ByteArrayInputStream(response)); + try { + //noinspection unchecked + return (List) objectStream.readObject(); + } catch (ClassNotFoundException e) { + // TODO: probably change to custom exception + throw new RuntimeException("server.NonBlockingServer fucked up!"); + } + } + + + @NotNull + @Override + public Path executeGet(@NotNull String path, @Nullable String destination) throws IOException { + sendRequest(createSentData((byte) Query.GET.ordinal(), path.getBytes(StandardCharsets.UTF_8))); + Path destinationPath = (destination == null ? null : Paths.get(destination)); + return getBigResponse(destinationPath).getPath(); + } + + @NotNull + private byte[] getSmallResponse() throws IOException { + SmallReadableMessage message = new SmallReadableMessage(channel); + getResponse(message); + return message.getData(); + } + + @NotNull + private BigReadableMessage getBigResponse(@Nullable Path destination) throws IOException { + BigReadableMessage message = new BigReadableMessage(channel, destination); + getResponse(message); + return message; + } + + private void getResponse(@NotNull ReadableMessage message) throws IOException { + //noinspection StatementWithEmptyBody + while (!message.read()); + } + + private void sendRequest(@NotNull byte[] data) throws IOException { + //noinspection StatementWithEmptyBody + if (!channel.isConnected()) { + throw new NotYetConnectedException(); + } + WritableMessage message = new SmallWritableMessage(channel, data); + //noinspection StatementWithEmptyBody + while (!message.write()); + } + + @NotNull + private byte[] createSentData(byte first, @NotNull byte[] rest) { + return ArrayUtils.addAll(ByteUtils.byteToBytes(first), rest); + } + +} diff --git a/src/main/java/client/gui/ClientGuiApp.java b/src/main/java/client/gui/ClientGuiApp.java new file mode 100644 index 0000000..69b5c0a --- /dev/null +++ b/src/main/java/client/gui/ClientGuiApp.java @@ -0,0 +1,26 @@ +package client.gui; + +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.stage.Stage; + +/** + * Gui application for communication with FTP server. + */ +public class ClientGuiApp extends Application { + @Override + public void start(Stage stage) throws Exception { + Scene scene = new SceneFactory().create(); + stage.setScene(scene); + stage.show(); + } + + /** + * Main method to launch GUI app. + * @param args console arguments + */ + public static void main(String[] args) { + launch(args); + } + +} \ No newline at end of file diff --git a/src/main/java/client/gui/SceneFactory.java b/src/main/java/client/gui/SceneFactory.java new file mode 100644 index 0000000..294c796 --- /dev/null +++ b/src/main/java/client/gui/SceneFactory.java @@ -0,0 +1,132 @@ +package client.gui; + +import client.Client; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import utils.SimpleFile; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +class SceneFactory { + private TextField hostnameTextField; + private TextField portTextField; + private TextField filePathTextField; + private TextArea resultTextArea; + private TextField destinationTextField; + + private static class VBoxBuilder { + private List nodeList; + + VBoxBuilder() { + this.nodeList = new ArrayList<>(); + } + + private VBoxBuilder addNodesHorizontally(Node... nodes) { + nodeList.add(new HBox(10, nodes)); + return this; + } + + private VBox build() { + Node[] nodes = new Node[nodeList.size()]; + nodes = nodeList.toArray(nodes); + VBox vbox = new VBox(nodes); + vbox.setMinWidth(MIN_WIDTH); + vbox.setSpacing(10); + return vbox; + } + } + + private static final double MIN_WIDTH = 400; + + Scene create() { + VBoxBuilder vBoxBuilder = new VBoxBuilder(); + + + TableView table = new TableView<>(); + + + ObservableList teamMembers = FXCollections.observableArrayList(); + table.setItems(teamMembers); + + hostnameTextField = new TextField(); + portTextField = new TextField(); + destinationTextField = new TextField(); + filePathTextField = new TextField(); + Button getRequestButton = new Button("Get file"); + getRequestButton.setOnMouseClicked(this::performGetRequest); + Button listRequestButton = new Button("List content"); + listRequestButton.setOnMouseClicked(this::performListRequest); + resultTextArea = new TextArea("hey\nmay"); + + + TableColumn firstNameCol = new TableColumn<>("Filename"); + firstNameCol.setCellValueFactory(new PropertyValueFactory("name")); + TableColumn lastNameCol = new TableColumn<>("Is directory"); + lastNameCol.setCellValueFactory(new PropertyValueFactory("isDirectory")); + + table.getColumns().setAll(firstNameCol, lastNameCol); + + VBox root = vBoxBuilder.addNodesHorizontally(new Label("Hostname:"), hostnameTextField, new Label("Port:"), portTextField) + .addNodesHorizontally(new Label("Download directory:"), destinationTextField) + .addNodesHorizontally(filePathTextField, getRequestButton, listRequestButton) + .addNodesHorizontally(resultTextArea) + .build(); + return new Scene(root); + } + + private void performListRequest(MouseEvent mouseEvent) { + System.err.println("listing"); + try { + Client client = Client.getNonBlocking(); + System.err.println("connecting"); + client.connect(hostnameTextField.getText(), Integer.parseInt(portTextField.getText())); + System.err.println("requesting"); + List result = client.executeList(filePathTextField.getText()); + StringBuilder resultString = new StringBuilder(); + for (SimpleFile file : result) { + //noinspection StringConcatenationInsideStringBufferAppend + resultString.append(file.name + " " + (file.isDirectory ? "directory" : "file") + "\n"); + } + System.err.println("result:"); + System.err.println(resultString.toString()); + resultTextArea.setText(resultString.toString()); + client.disconnect(); + } catch (IOException e) { + showAlert(Alert.AlertType.ERROR, "Error while performing list request: " + e.getMessage()); + } + } + + private void showAlert(Alert.AlertType alertType, String message) { + Alert alert = new Alert(alertType, message); + alert.show(); + } + + private void performGetRequest(MouseEvent mouseEvent) { + System.err.println("listing"); + try { + Client client = Client.getNonBlocking(); + System.err.println("connecting"); + client.connect(hostnameTextField.getText(), Integer.parseInt(portTextField.getText())); + System.err.println("requesting"); + String destination = destinationTextField.getText(); + Path result = client.executeGet(filePathTextField.getText(), + destination.isEmpty() ? null : destination); + System.err.println("result:"); + System.err.println(result.toAbsolutePath().toString()); + showAlert(Alert.AlertType.INFORMATION, result.toAbsolutePath().toString()); + client.disconnect(); + } catch (IOException e) { + showAlert(Alert.AlertType.ERROR, "Error while performing list request: " + e.getMessage()); + } + } +} diff --git a/src/main/java/client/gui/package-info.java b/src/main/java/client/gui/package-info.java new file mode 100644 index 0000000..28d971e --- /dev/null +++ b/src/main/java/client/gui/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides GUI application for FTP client. + */ +package client.gui; \ No newline at end of file diff --git a/src/main/java/client/package-info.java b/src/main/java/client/package-info.java new file mode 100644 index 0000000..dfaa115 --- /dev/null +++ b/src/main/java/client/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides client console app and interface for FTP clients. + */ + +package client; \ No newline at end of file diff --git a/src/main/java/server/FTPServer.java b/src/main/java/server/FTPServer.java new file mode 100644 index 0000000..b3cc902 --- /dev/null +++ b/src/main/java/server/FTPServer.java @@ -0,0 +1,38 @@ +package server; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.nio.file.Path; + +/** + * Interface of a simple FTP server. + */ + +public interface FTPServer { + /** + * Launches server. + * + * After launch it will loop indefinitely until its thread will be interrupted. + * @param hostname the hostname to listen to + * @param port the port to listen to + * @throws IOException If an I/O error occurs + */ + void run(@NotNull String hostname, int port) throws IOException; + + /** + * Creates non-blocking FTP server. + * @param root path to root directory for server + * @return A new FTP server + */ + @NotNull + static FTPServer getNonBlocking(@NotNull Path root) { + return new NonBlockingServer(root); + } + + /** + * Returns whether server is ready to accept connections. + * @return true if server is ready to accept incoming connections and false otherwise + */ + boolean isReady(); +} diff --git a/src/main/java/server/FileSystem.java b/src/main/java/server/FileSystem.java new file mode 100644 index 0000000..4cde354 --- /dev/null +++ b/src/main/java/server/FileSystem.java @@ -0,0 +1,65 @@ +package server; + +import org.jetbrains.annotations.NotNull; +import utils.SimpleFile; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.stream.Collectors; + +/** + * Class that provides means to interact with file system. + */ +public class FileSystem { + @NotNull + private final Path root; + + /** + * Creates file system manager with specified root. + * @param root path to desired root directory + */ + @SuppressWarnings("WeakerAccess") + public FileSystem(@NotNull Path root) { + this.root = root; + } + + /** + * Returns list of entries in specified directory. + * @param path path to directory + * @return list of entries in specified directory + * @throws IOException If an I/O error occurs + */ + @SuppressWarnings("WeakerAccess") + @NotNull + public ArrayList list(@NotNull Path path) throws IOException { + assertChild(path, root); + return new ArrayList<>(Files.list(path).map(p -> new SimpleFile(p.toString(), Files.isDirectory(p))).collect(Collectors.toList())); + } + + /** + * Returns {@link InputStream} to read from the file. + * @param path path to the file + * @return a new input stream + * @throws IOException If an I/O error occurs + */ + @SuppressWarnings("WeakerAccess") + @NotNull + public InputStream getOutputStream(@NotNull Path path) throws IOException { + assertChild(path, root); + return Files.newInputStream(path); + } + + private static void assertChild(@NotNull Path path, @NotNull Path root) { + if (!isChild(path, root)) { + throw new SecurityException("Trying to go out of set root"); + } + } + + + private static boolean isChild(@NotNull Path path, @NotNull Path root) { + return path.toAbsolutePath().startsWith(root.toAbsolutePath()); + } + +} \ No newline at end of file diff --git a/src/main/java/server/NonBlockingServer.java b/src/main/java/server/NonBlockingServer.java new file mode 100644 index 0000000..6a16dd5 --- /dev/null +++ b/src/main/java/server/NonBlockingServer.java @@ -0,0 +1,71 @@ +package server; + +import org.jetbrains.annotations.NotNull; +import utils.SmallReadableMessage; +import utils.WritableMessage; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.nio.file.Path; +import java.util.Iterator; + +class NonBlockingServer implements FTPServer { + private final Path root; + + public boolean isReady() { + return ready; + } + + private Boolean ready = false; + + NonBlockingServer(Path root) { + this.root = root; + } + + @Override + public void run(@NotNull String hostname, int port) throws IOException { + FileSystem fileSystem = new FileSystem(root); + Selector selector = Selector.open(); + ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); + serverSocketChannel.bind(new InetSocketAddress(hostname, port)); + serverSocketChannel.configureBlocking(false); + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + QueryProcessor queryProcessor = new QueryProcessor(); + synchronized (this) { + ready = true; + } + while (!Thread.interrupted()) { + selector.selectNow(); + Iterator keyIterator = selector.selectedKeys().iterator(); + while (keyIterator.hasNext()) { + SelectionKey selectionKey = keyIterator.next(); + if (selectionKey.isAcceptable()) { + ServerSocketChannel serverChannel = (ServerSocketChannel) selectionKey.channel(); + SocketChannel socketChannel = serverChannel.accept(); + socketChannel.configureBlocking(false); + SelectionKey newSelectionKey = socketChannel.register(selector, SelectionKey.OP_READ); + newSelectionKey.attach(new SmallReadableMessage(socketChannel)); + } + if (selectionKey.isReadable()) { + SmallReadableMessage message = (SmallReadableMessage) selectionKey.attachment(); + if (message.read()) { + SelectionKey newSelectionKey = selectionKey.channel().register(selector, SelectionKey.OP_WRITE); + newSelectionKey.attach(queryProcessor.process(message.getData(), fileSystem).generateMessage(message.getChannel())); + } + } + if (selectionKey.isWritable()) { + WritableMessage message = (WritableMessage) selectionKey.attachment(); + if (message.write()) { + selectionKey.channel().close(); + selectionKey.cancel(); + } + } + keyIterator.remove(); + } + } + } +} diff --git a/src/main/java/server/QueryProcessor.java b/src/main/java/server/QueryProcessor.java new file mode 100644 index 0000000..d58aef4 --- /dev/null +++ b/src/main/java/server/QueryProcessor.java @@ -0,0 +1,39 @@ +package server; + +import client.Client; +import org.jetbrains.annotations.NotNull; +import utils.GetResponse; +import utils.ListResponse; +import utils.Response; +import utils.SimpleFile; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.ArrayList; + +class QueryProcessor { + Response process(@NotNull byte[] data, @NotNull FileSystem fileSystem) throws IOException { + Client.Query queryType = Client.Query.values()[data[0]]; + String path = new String(data, 1, data.length - 1, StandardCharsets.UTF_8); + + switch (queryType) { + case GET: { + return new GetResponse(fileSystem.getOutputStream(Paths.get(path))); + } + case LIST: { + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + ObjectOutputStream objectStream = new ObjectOutputStream(byteStream); + ArrayList files = fileSystem.list(Paths.get(path)); + objectStream.writeObject(files); + objectStream.flush(); + return new ListResponse(byteStream.toByteArray()); + } + default: { + throw new UnsupportedOperationException(); + } + } + } +} diff --git a/src/main/java/server/ServerConsoleApp.java b/src/main/java/server/ServerConsoleApp.java new file mode 100644 index 0000000..03147de --- /dev/null +++ b/src/main/java/server/ServerConsoleApp.java @@ -0,0 +1,67 @@ +package server; + + +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Scanner; + +/** + * Console application providing start/stop methods for FTP server. + */ + +public class ServerConsoleApp { + /** + * Main method. + * @param args command line args + * @throws IOException If an I/O error occurs + */ + + public static void main(String[] args) throws IOException { + Scanner scanner = new Scanner(System.in); + + @Nullable Thread serverThread = null; + + while (true) { + System.out.println("cycling"); + String command = scanner.next(); + boolean shutdown = false; + switch(command) { + case "start": { + String hostname = scanner.next(); + int port = scanner.nextInt(); + serverThread = new Thread(() -> { + FTPServer server = FTPServer.getNonBlocking(Paths.get(".")); + try { + server.run(hostname, port); + } catch (IOException e) { + System.err.println("NonBlockingServer run failed: " + e.getMessage()); + } + }); + serverThread.start(); + break; + } + case "stop": { + if (serverThread == null) { + System.err.println("NonBlockingServer not started!"); + } + else { + serverThread.interrupt(); + } + break; + } + case "shutdown": { + shutdown = true; + break; + } + default: { + System.out.println("Usage: start HOST PORT | stop | shutdown"); + } + } + if (shutdown) { + break; + } + } + } +} diff --git a/src/main/java/server/package-info.java b/src/main/java/server/package-info.java new file mode 100644 index 0000000..bdff820 --- /dev/null +++ b/src/main/java/server/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides server console app and interface for FTP servers. + */ + +package server; \ No newline at end of file diff --git a/src/main/java/utils/BigReadableMessage.java b/src/main/java/utils/BigReadableMessage.java new file mode 100644 index 0000000..ab8a458 --- /dev/null +++ b/src/main/java/utils/BigReadableMessage.java @@ -0,0 +1,39 @@ +package utils; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.channels.SocketChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Implementation of {@link ReadableMessage} suitable for reading big messages. + */ +public class BigReadableMessage extends ReadableMessage { + @NotNull + private String path; + + /** + * Creates new message that reads its content from provided channel. + * @param channel channel to read data from + * @throws IOException If an I/O error occurs + */ + public BigReadableMessage(@NotNull SocketChannel channel, @Nullable Path destination) throws IOException { + super(channel, (destination != null ? + new KnownFileOutputStream(Files.createTempFile(destination, "dwnl", "").toFile()) : + new KnownFileOutputStream(Files.createTempFile("dwnl", "").toFile()))); + path = ((KnownFileOutputStream) getDestination()).getFile().getAbsolutePath(); + } + + /** + * Returns path to downloaded file. + * @return path to downloaded file + */ + @NotNull + public Path getPath() { + return Paths.get(path); + } +} diff --git a/src/main/java/utils/BigWritableMessage.java b/src/main/java/utils/BigWritableMessage.java new file mode 100644 index 0000000..96cfc9c --- /dev/null +++ b/src/main/java/utils/BigWritableMessage.java @@ -0,0 +1,26 @@ +package utils; + + +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.nio.channels.SocketChannel; + +class BigWritableMessage extends WritableMessage { + BigWritableMessage(@NotNull SocketChannel channel, @NotNull InputStream source) throws IOException { + super(channel, createMessageInputStream(source), getSize(source)); + } + + @NotNull + private static InputStream createMessageInputStream(@NotNull InputStream source) throws IOException { + InputStream stream = new ByteArrayInputStream(ByteUtils.longToBytes(getSize(source))); + return new SequenceInputStream(stream, source); + } + + private static long getSize(@NotNull InputStream source) throws IOException { + return Long.BYTES + source.available(); + } +} diff --git a/src/main/java/utils/ByteUtils.java b/src/main/java/utils/ByteUtils.java new file mode 100644 index 0000000..df2dbc9 --- /dev/null +++ b/src/main/java/utils/ByteUtils.java @@ -0,0 +1,38 @@ +package utils; + +import org.jetbrains.annotations.NotNull; + +import java.nio.ByteBuffer; + +/** + * Class that provides utility functions for work with byte arrays. + */ +public class ByteUtils { + private static final ByteBuffer longBuffer = ByteBuffer.allocate(Long.BYTES); + private static final ByteBuffer byteBuffer = ByteBuffer.allocate(Byte.BYTES); + + /** + * Converts long to byte array. + * @param x number to convert + * @return byte representation of given number + */ + @SuppressWarnings("WeakerAccess") + @NotNull + public static byte[] longToBytes(long x) { + longBuffer.putLong(0, x); + longBuffer.clear(); + return longBuffer.array().clone(); + } + + /** + * Converts byte to byte array. + * @param x byte to convert + * @return byte array consisting of single given byte + */ + @NotNull + public static byte[] byteToBytes(byte x) { + byteBuffer.put(x); + byteBuffer.clear(); + return byteBuffer.array().clone(); + } +} \ No newline at end of file diff --git a/src/main/java/utils/GetResponse.java b/src/main/java/utils/GetResponse.java new file mode 100644 index 0000000..e9faa58 --- /dev/null +++ b/src/main/java/utils/GetResponse.java @@ -0,0 +1,28 @@ +package utils; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.SocketChannel; + +/** + * Class encapsulating response to Query.GET query. + */ +public class GetResponse implements Response { + private final InputStream source; + + /** + * Creates new response encapsulating given data. + * @param source {@link InputStream} to read response data from + */ + public GetResponse(@NotNull InputStream source) { + this.source = source; + } + + + @Override + public WritableMessage generateMessage(@NotNull SocketChannel channel) throws IOException { + return new BigWritableMessage(channel, source); + } +} diff --git a/src/main/java/utils/KnownFileOutputStream.java b/src/main/java/utils/KnownFileOutputStream.java new file mode 100644 index 0000000..7342f13 --- /dev/null +++ b/src/main/java/utils/KnownFileOutputStream.java @@ -0,0 +1,21 @@ +package utils; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; + +class KnownFileOutputStream extends FileOutputStream { + @NotNull + private final File file; + + KnownFileOutputStream(@NotNull File file) throws FileNotFoundException { + super(file); + this.file = file; + } + @NotNull + File getFile() { + return file; + } +} diff --git a/src/main/java/utils/ListResponse.java b/src/main/java/utils/ListResponse.java new file mode 100644 index 0000000..fdb2615 --- /dev/null +++ b/src/main/java/utils/ListResponse.java @@ -0,0 +1,25 @@ +package utils; + +import org.jetbrains.annotations.NotNull; + +import java.nio.channels.SocketChannel; + +/** + * Class encapsulating response to Query.LIST query. + */ +public class ListResponse implements Response { + @NotNull private byte[] data; + + /** + * Creates new response encapsulating given data. + * @param data response data + */ + public ListResponse(@NotNull byte[] data) { + this.data = data; + } + + @Override + public WritableMessage generateMessage(@NotNull SocketChannel channel) { + return new SmallWritableMessage(channel, data); + } +} diff --git a/src/main/java/utils/ReadableMessage.java b/src/main/java/utils/ReadableMessage.java new file mode 100644 index 0000000..36337d9 --- /dev/null +++ b/src/main/java/utils/ReadableMessage.java @@ -0,0 +1,71 @@ +package utils; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; + +/** + * Object that can read message data from specified channel. + */ +public class ReadableMessage { + private static final int BUFFER_SIZE = 4096; + @NotNull + private final OutputStream destination; + @NotNull + private SocketChannel channel; + private long size = 0; + private int read = 0; + @NotNull + private ByteBuffer buffer; + + ReadableMessage(@NotNull SocketChannel channel, @NotNull OutputStream destination) { + this.channel = channel; + // TODO: add buffered + this.destination = destination; + buffer = ByteBuffer.allocate(BUFFER_SIZE); + } + + /** + * Returns associated channel. + * @return channel from which data is being read + */ + @NotNull + public SocketChannel getChannel() { + return channel; + } + + /** + * Calls read() on associated channel + * @return whether there is data left to read + * @throws IOException If an I/O error occurs. + */ + public boolean read() throws IOException { + if (read < Long.BYTES) { + read += channel.read(buffer); + if (read >= Long.BYTES) { + buffer.flip(); + size = buffer.getLong(); + } + else { + return false; + } + } + while (buffer.hasRemaining()) { + destination.write(buffer.get()); + } + buffer.clear(); + if (read < size) { + read += channel.read(buffer); + } + buffer.flip(); + return read == size && !buffer.hasRemaining(); + } + + @NotNull + OutputStream getDestination() { + return destination; + } +} diff --git a/src/main/java/utils/Response.java b/src/main/java/utils/Response.java new file mode 100644 index 0000000..2136f44 --- /dev/null +++ b/src/main/java/utils/Response.java @@ -0,0 +1,18 @@ +package utils; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.nio.channels.SocketChannel; + +/** + * Response to client query based on which message could be generated. + */ +public interface Response { + /** + * Returns {@link WritableMessage} based on this response + * @param channel channel to associate message with + * @return message with this response + */ + WritableMessage generateMessage(@NotNull SocketChannel channel) throws IOException; +} diff --git a/src/main/java/utils/SimpleFile.java b/src/main/java/utils/SimpleFile.java new file mode 100644 index 0000000..2c2a6f9 --- /dev/null +++ b/src/main/java/utils/SimpleFile.java @@ -0,0 +1,31 @@ +package utils; + +import org.jetbrains.annotations.NotNull; + +import java.io.Serializable; + +/** + * Object containing pair (name, isDirectory) for file. + */ +public class SimpleFile implements Serializable{ + /** + * Filename + */ + @NotNull + public final String name; + /** + * Whether this file is a directory + */ + public final boolean isDirectory; + + /** + * Creates new file info with given data. + * @param name filename + * @param isDirectory whether given file is a directory + */ + public SimpleFile(@NotNull String name, boolean isDirectory) { + this.name = name; + this.isDirectory = isDirectory; + } + +} diff --git a/src/main/java/utils/SmallReadableMessage.java b/src/main/java/utils/SmallReadableMessage.java new file mode 100644 index 0000000..650019a --- /dev/null +++ b/src/main/java/utils/SmallReadableMessage.java @@ -0,0 +1,29 @@ +package utils; + +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayOutputStream; +import java.nio.channels.SocketChannel; + +/** + * Implementation of {@link ReadableMessage} suitable for reading small messages. + */ +public class SmallReadableMessage extends ReadableMessage { + /** + * Creates new message that reads its content from provided channel. + * @param channel channel to read data from + */ + public SmallReadableMessage(@NotNull SocketChannel channel) { + super(channel, new ByteArrayOutputStream()); + } + + /** + * Return read data. + * @return data byte array + */ + @NotNull + public byte[] getData() { + return ((ByteArrayOutputStream) getDestination()).toByteArray(); + } + +} diff --git a/src/main/java/utils/SmallWritableMessage.java b/src/main/java/utils/SmallWritableMessage.java new file mode 100644 index 0000000..ad49376 --- /dev/null +++ b/src/main/java/utils/SmallWritableMessage.java @@ -0,0 +1,30 @@ +package utils; + + +import org.apache.commons.lang3.ArrayUtils; +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayInputStream; +import java.nio.channels.SocketChannel; + +/** + * Implementation of {@link WritableMessage} suitable for writing small messages. + */ +public class SmallWritableMessage extends WritableMessage { + /** + * Creates new message that writes its content to provided channel. + * @param channel channel to write data to + */ + public SmallWritableMessage(@NotNull SocketChannel channel, @NotNull byte[] data) { + super(channel, new ByteArrayInputStream(addHeader(data)), getSize(data)); + } + + private static long getSize(@NotNull byte[] data) { + return Long.BYTES + data.length; + } + + @NotNull + private static byte[] addHeader(@NotNull byte[] data) { + return ArrayUtils.addAll(ByteUtils.longToBytes(getSize(data)), data); + } +} diff --git a/src/main/java/utils/WritableMessage.java b/src/main/java/utils/WritableMessage.java new file mode 100644 index 0000000..629fd51 --- /dev/null +++ b/src/main/java/utils/WritableMessage.java @@ -0,0 +1,49 @@ +package utils; + + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; + +/** + * Object that can write message data to specified channel. + */ +public class WritableMessage { + private static final int BUFFER_SIZE = 4096; + @NotNull + private final ByteBuffer buffer; + private long leftSource; + @NotNull + private SocketChannel channel; + @NotNull + private InputStream source; + WritableMessage(@NotNull SocketChannel channel, @NotNull InputStream source, long size) { + this.leftSource = size; + this.channel = channel; + // TODO: add buffered + this.source = source; + this.buffer = ByteBuffer.allocate(BUFFER_SIZE); + this.buffer.limit(0); + } + + /** + * Calls write() on associated channel with remaining data. + * @return whether it has completed message writing + */ + public boolean write() throws IOException { + if (buffer.hasRemaining()) { + channel.write(buffer); + return false; + } + else if (leftSource > 0){ + buffer.clear(); + int read = source.read(buffer.array()); + buffer.limit(read); + leftSource -= read; + } + return leftSource == 0 && !buffer.hasRemaining(); + } +} diff --git a/src/main/java/utils/package-info.java b/src/main/java/utils/package-info.java new file mode 100644 index 0000000..9e0ba5a --- /dev/null +++ b/src/main/java/utils/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides utility classes for client/server communication and internal needs. + */ +package utils; \ No newline at end of file diff --git a/src/test/java/FileSystemTest.java b/src/test/java/FileSystemTest.java new file mode 100644 index 0000000..7c43013 --- /dev/null +++ b/src/test/java/FileSystemTest.java @@ -0,0 +1,4 @@ +package PACKAGE_NAME; + +public class FileSystemTest { +} diff --git a/src/test/java/IntegrationTest.java b/src/test/java/IntegrationTest.java new file mode 100644 index 0000000..3f60dd9 --- /dev/null +++ b/src/test/java/IntegrationTest.java @@ -0,0 +1,103 @@ + +import client.Client; +import org.junit.Assert; +import org.junit.Test; +import server.FTPServer; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class IntegrationTest { + private static final String HOSTNAME = "localhost"; + private static final int PORT = 1234; + private static final TestFile A = new TestFile("A.txt", "A text"); + private static final TestFile B = new TestFile("B.txt", "B text"); + private static final TestFile A_B = new TestFile("A/B.txt", "A/B.txt text"); + private static final TestFile A_B_C = new TestFile("A/B/C.txt", "A/B/C.txt text"); + + private Client client; + private Thread serverThread; + + @Test + public void processGet() throws Exception { + prepare(); + + A.create(); + String path = A.getPath().toString(); + + Path downloadedPath = client.executeGet(A.getPath().toString()); + + Assert.assertArrayEquals(Files.readAllBytes(downloadedPath), A.getContent()); + Files.delete(downloadedPath); + + finish(); + } + + + + private void finish() throws IOException { + client.disconnect(); + serverThread.interrupt(); + } + + private void prepare() throws IOException { + Path serverRoot = TestFile.getTestDir(); + FTPServer server = FTPServer.getNonBlocking(serverRoot); + client = Client.getNonBlocking(); + serverThread = new Thread(() -> { + try { + server.run(HOSTNAME, PORT); + } catch (IOException e) { + throw new RuntimeException(e.getMessage()); + } + }); + serverThread.start(); + + //noinspection StatementWithEmptyBody + while(!server.isReady()); + client.connect(HOSTNAME, PORT); + } +} + +class TestFile { + private static final Path TEST_DIR; + + static Path getTestDir() { + return TEST_DIR; + } + + static { + try { + TEST_DIR = Files.createTempDirectory("FTP_root"); + } catch (IOException e) { + throw new RuntimeException("Failed to create test directory"); + } + } + + private String content; + private Path path; + + TestFile(String path, String content){ + this.content = content; + this.path = TEST_DIR.resolve(path); + } + void create() throws IOException { + Path parent = path.getParent(); + if (parent != null && Files.notExists(parent)) { + Files.createDirectories(parent); + } + if (Files.notExists(path)) { + Files.createFile(path); + } + Files.write(path, content.getBytes()); + } + + Path getPath() { + return path; + } + + byte[] getContent() { + return content.getBytes(); + } +} \ No newline at end of file diff --git a/src/test/java/TestFile.java b/src/test/java/TestFile.java new file mode 100644 index 0000000..c91a5c7 --- /dev/null +++ b/src/test/java/TestFile.java @@ -0,0 +1,4 @@ +package PACKAGE_NAME; + +public class TestFile { +} diff --git a/src/test/java/server/QueryProcessorTest.java b/src/test/java/server/QueryProcessorTest.java new file mode 100644 index 0000000..a44d8b8 --- /dev/null +++ b/src/test/java/server/QueryProcessorTest.java @@ -0,0 +1,5 @@ +import static org.junit.Assert.*; + +public class QueryProcessorTest { + +} \ No newline at end of file