├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── LICENSE ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml ├── src └── main │ ├── java │ └── tusserver │ │ ├── StorageController.java │ │ ├── TusServerApplication.java │ │ ├── exceptions │ │ ├── TusBadRequestException.java │ │ ├── TusDBFileNotFoundException.java │ │ ├── TusException.java │ │ ├── TusExceptionsCotroller.java │ │ ├── TusPermissionDeniedException.java │ │ └── TusStorageException.java │ │ └── storage │ │ ├── model │ │ ├── TusFile.java │ │ └── TusFilesRepository.java │ │ └── services │ │ ├── StorageService.java │ │ └── StorageServiceImpl.java │ └── resources │ └── application.properties └── web.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ 25 | 26 | ###CUSTOM### 27 | storage/* 28 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasooo/spring-boot-tus/2fe9c462cfa8375c53febe3f6098eb3d882b5ea6/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Tomas Ballon 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TUS PROTOCOL IMPLEMENTATION (JAVA - SPRING BOOT) 2 | 3 | Protocol documentation: https://tus.io/ 4 | 5 | Base implementation of TUS protocol in spring boot, with mongoDB. 6 | 7 | ## Working with official clients 8 | 9 | - tested on js client (https://github.com/tus/tus-js-client/tree/v1.4.4) 10 | 11 | ## Configuration file 12 | 13 | ``` 14 | /src/main/resources/application.propoerties 15 | ``` 16 | 17 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Migwn, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 204 | echo $MAVEN_PROJECTBASEDIR 205 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 206 | 207 | # For Cygwin, switch paths to Windows format before running java 208 | if $cygwin; then 209 | [ -n "$M2_HOME" ] && 210 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 211 | [ -n "$JAVA_HOME" ] && 212 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 213 | [ -n "$CLASSPATH" ] && 214 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 215 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 216 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 217 | fi 218 | 219 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 220 | 221 | exec "$JAVACMD" \ 222 | $MAVEN_OPTS \ 223 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 224 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 225 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 226 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 84 | @REM Fallback to current working directory if not found. 85 | 86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 88 | 89 | set EXEC_DIR=%CD% 90 | set WDIR=%EXEC_DIR% 91 | :findBaseDir 92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 93 | cd .. 94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 95 | set WDIR=%CD% 96 | goto findBaseDir 97 | 98 | :baseDirFound 99 | set MAVEN_PROJECTBASEDIR=%WDIR% 100 | cd "%EXEC_DIR%" 101 | goto endDetectBaseDir 102 | 103 | :baseDirNotFound 104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 105 | cd "%EXEC_DIR%" 106 | 107 | :endDetectBaseDir 108 | 109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 110 | 111 | @setlocal EnableExtensions EnableDelayedExpansion 112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 114 | 115 | :endReadAdditionalConfig 116 | 117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 118 | 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 123 | if ERRORLEVEL 1 goto error 124 | goto end 125 | 126 | :error 127 | set ERROR_CODE=1 128 | 129 | :end 130 | @endlocal & set ERROR_CODE=%ERROR_CODE% 131 | 132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 136 | :skipRcPost 137 | 138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 140 | 141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 142 | 143 | exit /B %ERROR_CODE% 144 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com 7 | tus-server 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | tus-server 12 | tus-spring 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 1.5.7.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-logging 35 | 36 | 37 | 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-test 42 | test 43 | 44 | 45 | org.springframework 46 | spring-web 47 | 4.3.11.RELEASE 48 | 49 | 50 | org.springframework.boot 51 | spring-boot-starter-tomcat 52 | 1.4.1.RELEASE 53 | 54 | 55 | org.springframework 56 | spring-webmvc 57 | 4.3.11.RELEASE 58 | 59 | 60 | com.fasterxml.jackson.core 61 | jackson-databind 62 | 2.8.1 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-starter-log4j2 67 | 68 | 69 | org.springframework.data 70 | spring-data-commons 71 | 1.13.7.RELEASE 72 | 73 | 74 | org.springframework.boot 75 | spring-boot-starter-data-mongodb 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | org.springframework.boot 84 | spring-boot-maven-plugin 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/main/java/tusserver/StorageController.java: -------------------------------------------------------------------------------- 1 | package tusserver; 2 | 3 | import org.springframework.core.env.Environment; 4 | import tusserver.storage.model.TusFile; 5 | import tusserver.exceptions.TusBadRequestException; 6 | import tusserver.exceptions.TusDBFileNotFoundException; 7 | import tusserver.storage.model.TusFilesRepository; 8 | import tusserver.storage.services.StorageService; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.core.io.Resource; 13 | import org.springframework.http.HttpHeaders; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.web.bind.annotation.*; 16 | import org.springframework.web.util.UriComponentsBuilder; 17 | 18 | import javax.servlet.http.HttpServletResponse; 19 | import java.io.InputStream; 20 | import java.util.Objects; 21 | 22 | @CrossOrigin(origins = "*", maxAge = 3600) 23 | @RestController 24 | @RequestMapping("/") 25 | public class StorageController { 26 | 27 | private Logger log = LoggerFactory.getLogger(this.getClass()); 28 | 29 | @Autowired 30 | private Environment environment; 31 | 32 | @Autowired 33 | private TusFilesRepository repo; 34 | 35 | @Autowired 36 | private StorageService storage; 37 | 38 | @RequestMapping(method = RequestMethod.OPTIONS) 39 | ResponseEntity processOptions(HttpServletResponse response) throws Exception { 40 | 41 | log.debug("OPTIONS"); 42 | 43 | response.setHeader("Access-Control-Expose-Headers", "Tus-Resumable, Tus-Version, Tus-Max-Size, Tus-Extension"); 44 | response.setHeader("Tus-Resumable", "1.0.0"); 45 | response.setHeader("Tus-Version", "1.0.0,0.2.2,0.2.1"); 46 | response.setHeader("Tus-Max-Size", environment.getProperty("tusserver.tusmaxsize")); 47 | response.setHeader("Tus-Extension", "creation,expiration"); 48 | response.setHeader("Access-Control-Allow-Origin","*"); 49 | response.setHeader("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE"); 50 | response.setStatus(204); 51 | return null; 52 | } 53 | 54 | @RequestMapping(method = RequestMethod.OPTIONS, value = "/{uuid}") 55 | ResponseEntity processOptionsUuid(@PathVariable String uuid, HttpServletResponse response) throws Exception { 56 | 57 | log.debug("OPTIONS START"); 58 | 59 | TusFile file = repo.findOne(uuid); 60 | if(file == null){ 61 | throw new TusDBFileNotFoundException(uuid); 62 | } 63 | 64 | log.debug("file offset: " + file.getOffset()); 65 | log.debug("OPTIONS END"); 66 | 67 | response.setHeader("Access-Control-Expose-Headers", "Tus-Resumable, Tus-Version, Tus-Max-Size, Tus-Extension"); 68 | response.setHeader("Tus-Resumable", "1.0.0"); 69 | response.setHeader("Tus-Version", "1.0.0,0.2.2,0.2.1"); 70 | response.setHeader("Tus-Max-Size", environment.getProperty("tusserver.tusmaxsize")); 71 | response.setHeader("Tus-Extension", "creation,expiration"); 72 | response.setHeader("Access-Control-Allow-Origin","*"); 73 | response.setHeader("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE"); 74 | response.setHeader("Upload-Offset", Integer.toString(file.getOffset())); 75 | response.setStatus(204); 76 | return null; 77 | } 78 | 79 | @RequestMapping(method = RequestMethod.POST) 80 | ResponseEntity processPost(@RequestHeader("Upload-Length") Integer uploadLength, 81 | UriComponentsBuilder uriComponentsBuilder, 82 | HttpServletResponse response) throws Exception { 83 | 84 | log.debug("POST START"); 85 | log.debug("Final-Length header value: " + Long.toString(uploadLength)); 86 | 87 | if(uploadLength < 1){ 88 | throw new TusBadRequestException("Wrong Final-Length Header"); 89 | } 90 | 91 | if(uploadLength > Long.parseLong(environment.getProperty("tusserver.tusmaxsize"))){ 92 | throw new TusBadRequestException("wrong Final-Length Header, max is: " + environment.getProperty("tusserver.tusmaxsize")); 93 | } 94 | 95 | TusFile file = new TusFile(); 96 | file.setUploadLength(uploadLength); 97 | file.setOffset(0); 98 | file.setCompleted(false); 99 | file = repo.save(file); 100 | 101 | log.debug("POST END"); 102 | 103 | response.setHeader("Access-Control-Expose-Headers", "Location, Tus-Resumable"); 104 | response.setHeader("Location", uriComponentsBuilder.path("/" + file.getUuid()).build().toString()); 105 | response.setHeader("Tus-Resumable", "1.0.0"); 106 | response.setStatus(201); 107 | return null; 108 | } 109 | 110 | @RequestMapping(method = RequestMethod.HEAD, value = "/{uuid}") 111 | ResponseEntity processHead(@PathVariable String uuid, HttpServletResponse response) throws Exception { 112 | 113 | log.debug("HEAD START"); 114 | log.debug("uuid value: " + uuid); 115 | 116 | TusFile file = repo.findOne(uuid); 117 | if(file == null){ 118 | throw new TusDBFileNotFoundException(uuid); 119 | } 120 | 121 | log.debug("file offset: " + file.getOffset()); 122 | log.debug("HEAD END"); 123 | 124 | response.setHeader("Access-Control-Expose-Headers", "Upload-Offset, Upload-Length, Tus-Resumable"); 125 | response.setHeader("Upload-Offset", Integer.toString(file.getOffset())); 126 | response.setHeader("Upload-Length", Integer.toString(file.getUploadLength())); 127 | response.setHeader("Tus-Resumable", "1.0.0"); 128 | response.setStatus(200); 129 | return null; 130 | } 131 | 132 | @RequestMapping(method = RequestMethod.PATCH, value = "/{uuid}") 133 | ResponseEntity processPatch(@RequestHeader("Upload-Offset") Integer uploadOffset, 134 | @RequestHeader("Content-Length") Integer contentLength, 135 | @RequestHeader("Content-Type") String contentType, 136 | @PathVariable String uuid, 137 | InputStream inputStream, 138 | HttpServletResponse response) throws Exception { 139 | 140 | log.debug("PATCH START"); 141 | log.debug("uuid value: " + uuid); 142 | log.debug("Upload-Offset: " + uploadOffset); 143 | log.debug("Content-Length: " + contentLength); 144 | log.debug("Content-Type: " + contentType); 145 | 146 | if(uploadOffset == null || uploadOffset < 0){ 147 | throw new TusBadRequestException("Wrong Offset Header"); 148 | } 149 | 150 | if(contentLength == null || contentLength < 0){ 151 | throw new TusBadRequestException("Wrong Content-Length Header"); 152 | } 153 | 154 | if(!contentType.equals("application/offset+octet-stream")){ 155 | throw new TusBadRequestException("Wrong Content-Type Header"); 156 | } 157 | 158 | TusFile file = repo.findOne(uuid); 159 | if(file == null){ 160 | throw new TusDBFileNotFoundException(uuid); 161 | } 162 | 163 | log.debug("TusFile Offset: " + file.getOffset()); 164 | log.debug("TusFile FinalLength: " + file.getUploadLength()); 165 | 166 | if(!Objects.equals(uploadOffset, file.getOffset())){ 167 | throw new TusBadRequestException("Offsets are not same."); 168 | } 169 | 170 | if(file.getUploadLength() < file.getOffset()){ 171 | throw new TusBadRequestException("Wrong upload length."); 172 | } 173 | 174 | //successful 175 | if(Objects.equals(file.getUploadLength(), file.getOffset())){ 176 | log.debug("Upload-length == Offset"); 177 | log.debug("PATCH END"); 178 | if(!file.isCompleted()){ 179 | file.setCompleted(true); 180 | repo.save(file); 181 | } 182 | response.setStatus(200); 183 | return null; 184 | } 185 | 186 | int newOffset = storage.processStream(uuid, inputStream); 187 | 188 | if(newOffset > file.getUploadLength()){ 189 | throw new TusBadRequestException("File is bigger than uploadLength"); 190 | } 191 | 192 | log.debug("New Offset: " + Integer.toString(newOffset)); 193 | 194 | file.setOffset(newOffset); 195 | repo.save(file); 196 | 197 | log.debug("PATCH END"); 198 | 199 | response.setHeader("Access-Control-Expose-Headers", "Upload-Offset, Tus-Resumable"); 200 | response.setHeader("Tus-Resumable", "1.0.0"); 201 | response.setHeader("Upload-Offset", Integer.toString(newOffset)); 202 | response.setStatus(204); 203 | return null; 204 | } 205 | 206 | @RequestMapping(method = RequestMethod.GET, value = "/{uuid}") 207 | @ResponseBody 208 | ResponseEntity processGet(@PathVariable String uuid) throws Exception { 209 | 210 | log.debug("GET START"); 211 | log.debug("uuid value: " + uuid); 212 | 213 | TusFile file = repo.findOne(uuid); 214 | if(file == null){ 215 | throw new TusDBFileNotFoundException(uuid); 216 | } 217 | 218 | if(!file.isCompleted() && Objects.equals(file.getUploadLength(), file.getOffset())){ 219 | log.debug("Upload-length == Offset"); 220 | file.setCompleted(true); 221 | repo.save(file); 222 | } 223 | 224 | if(!file.isCompleted()){ 225 | throw new TusBadRequestException("File is not completed."); 226 | } 227 | 228 | Resource fileResource = storage.loadResource(uuid); 229 | 230 | log.debug("GET END"); 231 | 232 | return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, 233 | "attachment; filename=\"" + fileResource.getFilename() + "\"").body(fileResource); 234 | } 235 | 236 | } 237 | -------------------------------------------------------------------------------- /src/main/java/tusserver/TusServerApplication.java: -------------------------------------------------------------------------------- 1 | package tusserver; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 5 | import org.springframework.context.annotation.ComponentScan; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | @ComponentScan 10 | @EnableAutoConfiguration 11 | public class TusServerApplication { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(TusServerApplication.class, args); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/tusserver/exceptions/TusBadRequestException.java: -------------------------------------------------------------------------------- 1 | package tusserver.exceptions; 2 | 3 | public class TusBadRequestException extends RuntimeException { 4 | private static final long serialVersionUID = 1L; 5 | 6 | public TusBadRequestException() { 7 | super("Bad request" ); 8 | } 9 | 10 | public TusBadRequestException(String message) { 11 | super("Bad request: " + message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/tusserver/exceptions/TusDBFileNotFoundException.java: -------------------------------------------------------------------------------- 1 | package tusserver.exceptions; 2 | 3 | public class TusDBFileNotFoundException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = 1L; 6 | 7 | public TusDBFileNotFoundException(String uuid) { 8 | super("File with uuid " + uuid + " not found"); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/tusserver/exceptions/TusException.java: -------------------------------------------------------------------------------- 1 | package tusserver.exceptions; 2 | 3 | import java.util.Date; 4 | 5 | @SuppressWarnings("unused") 6 | public class TusException { 7 | 8 | private long timestamp; 9 | private String error; 10 | private String message; 11 | private String exception; 12 | 13 | public TusException(String err, Exception ex){ 14 | this.timestamp = new Date().getTime(); 15 | 16 | this.error = err; 17 | this.message = ex.getMessage(); 18 | this.exception = ex.getClass().getCanonicalName(); 19 | } 20 | 21 | public long getTimestamp() { 22 | return timestamp; 23 | } 24 | 25 | public void setTimestamp(long timestamp) { 26 | this.timestamp = timestamp; 27 | } 28 | 29 | public String getError() { 30 | return error; 31 | } 32 | 33 | public void setError(String error) { 34 | this.error = error; 35 | } 36 | 37 | public String getMessage() { 38 | return message; 39 | } 40 | 41 | public void setMessage(String message) { 42 | this.message = message; 43 | } 44 | 45 | public String getException() { 46 | return exception; 47 | } 48 | 49 | public void setException(String exception) { 50 | this.exception = exception; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/tusserver/exceptions/TusExceptionsCotroller.java: -------------------------------------------------------------------------------- 1 | package tusserver.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ExceptionHandler; 5 | import org.springframework.web.bind.annotation.ResponseStatus; 6 | import org.springframework.web.bind.annotation.RestControllerAdvice; 7 | import org.springframework.web.context.request.WebRequest; 8 | 9 | @RestControllerAdvice 10 | class TusExceptionsController { 11 | 12 | @ExceptionHandler(value = { TusDBFileNotFoundException.class }) 13 | @ResponseStatus(HttpStatus.NOT_FOUND) 14 | public TusException VideoNotFoundExceptionHandler(TusDBFileNotFoundException e, WebRequest req) { 15 | e.printStackTrace(); 16 | 17 | TusException err = new TusException("not found", e); 18 | return err; 19 | } 20 | 21 | @ExceptionHandler(value = { TusStorageException.class }) 22 | @ResponseStatus(HttpStatus.NOT_FOUND) 23 | public TusException TusStorageExceptionHandler(TusStorageException e, WebRequest req) { 24 | e.printStackTrace(); 25 | 26 | TusException err = new TusException("not found", e); 27 | return err; 28 | } 29 | 30 | @ExceptionHandler(value = { TusPermissionDeniedException.class }) 31 | @ResponseStatus(HttpStatus.FORBIDDEN) 32 | public TusException TusPermissionDeniedExceptionHandler(TusPermissionDeniedException e, WebRequest req) { 33 | e.printStackTrace(); 34 | 35 | TusException err = new TusException("permission denied", e); 36 | return err; 37 | } 38 | 39 | @ExceptionHandler(value = { TusBadRequestException.class }) 40 | @ResponseStatus(HttpStatus.FORBIDDEN) 41 | public TusException TusBadRequestExceptionHandler(TusBadRequestException e, WebRequest req) { 42 | e.printStackTrace(); 43 | 44 | TusException err = new TusException("bad request", e); 45 | return err; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/tusserver/exceptions/TusPermissionDeniedException.java: -------------------------------------------------------------------------------- 1 | package tusserver.exceptions; 2 | 3 | public class TusPermissionDeniedException extends RuntimeException { 4 | private static final long serialVersionUID = 1L; 5 | 6 | public TusPermissionDeniedException(String message) { 7 | super("Permission denied: " + message); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/tusserver/exceptions/TusStorageException.java: -------------------------------------------------------------------------------- 1 | package tusserver.exceptions; 2 | 3 | public class TusStorageException extends RuntimeException { 4 | private static final long serialVersionUID = 1L; 5 | 6 | public TusStorageException(String name) { 7 | super("File with name " + name + " not found"); 8 | } 9 | 10 | public TusStorageException(String name, boolean bool) { 11 | super("Directory with name " + name + " not found"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/tusserver/storage/model/TusFile.java: -------------------------------------------------------------------------------- 1 | package tusserver.storage.model; 2 | 3 | import org.springframework.data.mongodb.core.mapping.Document; 4 | import org.springframework.data.annotation.Id; 5 | 6 | @Document 7 | public class TusFile { 8 | 9 | @Id 10 | private String uuid; 11 | 12 | private String fileName; 13 | private Integer uploadLength; 14 | private Integer offset; 15 | 16 | public boolean isCompleted() { 17 | return isCompleted; 18 | } 19 | 20 | public void setCompleted(boolean completed) { 21 | isCompleted = completed; 22 | } 23 | 24 | private boolean isCompleted; 25 | 26 | public String getUuid() { 27 | return uuid; 28 | } 29 | 30 | public void setUuid(String uuid) { 31 | this.uuid = uuid; 32 | } 33 | 34 | public String getFileName() { 35 | return fileName; 36 | } 37 | 38 | public void setFileName(String fileName) { 39 | this.fileName = fileName; 40 | } 41 | 42 | public Integer getUploadLength() { 43 | return uploadLength; 44 | } 45 | 46 | public void setUploadLength(Integer uploadLength) { 47 | this.uploadLength = uploadLength; 48 | } 49 | 50 | public Integer getOffset() { 51 | return offset; 52 | } 53 | 54 | public void setOffset(Integer offset) { 55 | this.offset = offset; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/tusserver/storage/model/TusFilesRepository.java: -------------------------------------------------------------------------------- 1 | package tusserver.storage.model; 2 | 3 | import org.springframework.data.mongodb.repository.MongoRepository; 4 | 5 | public interface TusFilesRepository extends MongoRepository { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/tusserver/storage/services/StorageService.java: -------------------------------------------------------------------------------- 1 | package tusserver.storage.services; 2 | 3 | import org.springframework.core.io.Resource; 4 | 5 | import java.io.InputStream; 6 | 7 | public interface StorageService { 8 | int processStream(String uuid, InputStream inputStream) throws Exception; 9 | Resource loadResource(String uuid) throws Exception; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/tusserver/storage/services/StorageServiceImpl.java: -------------------------------------------------------------------------------- 1 | package tusserver.storage.services; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.core.env.Environment; 5 | import tusserver.exceptions.TusPermissionDeniedException; 6 | import tusserver.exceptions.TusStorageException; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.core.io.Resource; 10 | import org.springframework.core.io.UrlResource; 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.util.StringUtils; 13 | 14 | import java.io.*; 15 | import java.nio.file.Files; 16 | import java.nio.file.Path; 17 | import java.nio.file.Paths; 18 | import java.nio.file.StandardCopyOption;; 19 | 20 | @Component 21 | public class StorageServiceImpl implements StorageService { 22 | 23 | private static final Logger log = LoggerFactory.getLogger(StorageServiceImpl.class.getName()); 24 | private final Path storageDir; 25 | private final String storagePath; 26 | 27 | @Autowired 28 | public StorageServiceImpl(Environment environment){ 29 | log.debug("STARTING STORAGE SERVICE"); 30 | 31 | storagePath = environment.getProperty("tusserver.storagefolder"); 32 | storageDir = Paths.get(this.storagePath); 33 | 34 | File file = new File(storagePath); 35 | 36 | if (!file.isDirectory() && !file.mkdir()){ 37 | throw new TusStorageException(storagePath, true); 38 | } 39 | if (!file.canWrite() || !file.canRead()){ 40 | String message = "Upload directory: " + storageDir + " must be readable and writable"; 41 | throw new TusPermissionDeniedException(message); 42 | } 43 | log.debug("StorageService started SUCCESFULY"); 44 | } 45 | 46 | public Resource loadResource(String uuid) throws Exception { 47 | String filename = StringUtils.cleanPath(uuid); 48 | 49 | Resource resource = new UrlResource(storageDir.resolve(filename).toUri()); 50 | if (resource.exists() || resource.isReadable()) { 51 | return resource; 52 | } 53 | else { 54 | throw new TusStorageException(filename); 55 | 56 | } 57 | } 58 | 59 | public int processStream(String uuid, InputStream inputStream) throws Exception { 60 | String filename = StringUtils.cleanPath(uuid); 61 | File file = new File(storageDir.resolve(filename).toString()); 62 | 63 | if (!file.isFile()){ 64 | new FileOutputStream(file).close(); 65 | if(!file.isFile()){ 66 | log.error("Cannot create new file"); 67 | throw new TusPermissionDeniedException("Cannot create new file"); 68 | } 69 | } 70 | 71 | InputStream storageFile; 72 | try{ 73 | storageFile = new FileInputStream(file); 74 | }catch(IOException e){ 75 | log.error("Cannot read old file"); 76 | throw new TusPermissionDeniedException("Cannot read old file"); 77 | } 78 | 79 | storageFile = new SequenceInputStream(storageFile, inputStream); 80 | Files.copy(storageFile, storageDir.resolve(filename), StandardCopyOption.REPLACE_EXISTING); 81 | file = new File(storageDir.resolve(filename).toString()); 82 | 83 | return (int) file.length(); 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.data.mongodb.uri=mongodb://127.0.0.1:27017/tusServer 2 | 3 | logging.level.root=INFO 4 | logging.level.tusserver=INFO 5 | spring.mvc.dispatch-options-request=true 6 | #logging.file=tusserver.log 7 | 8 | tusserver.tusmaxsize=1073741824 9 | tusserver.storagefolder=storage -------------------------------------------------------------------------------- /web.xml: -------------------------------------------------------------------------------- 1 | 2 | dispatchOptionsRequest 3 | true 4 | --------------------------------------------------------------------------------