├── .github └── workflows │ ├── build.yml │ └── codestyle.yml ├── .gitignore ├── .pre-commit-config.yaml ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── checkstyle.xml ├── pom.xml └── src ├── main └── java │ └── me │ └── desair │ └── tus │ └── server │ ├── HttpHeader.java │ ├── HttpMethod.java │ ├── RequestHandler.java │ ├── RequestValidator.java │ ├── TusExtension.java │ ├── TusFileUploadService.java │ ├── checksum │ ├── ChecksumAlgorithm.java │ ├── ChecksumExtension.java │ ├── ChecksumOptionsRequestHandler.java │ ├── ChecksumPatchRequestHandler.java │ └── validation │ │ └── ChecksumAlgorithmValidator.java │ ├── concatenation │ ├── ConcatenationExtension.java │ ├── ConcatenationHeadRequestHandler.java │ ├── ConcatenationOptionsRequestHandler.java │ ├── ConcatenationPostRequestHandler.java │ └── validation │ │ ├── NoUploadLengthOnFinalValidator.java │ │ ├── PartialUploadsExistValidator.java │ │ └── PatchFinalUploadValidator.java │ ├── core │ ├── CoreDefaultResponseHeadersHandler.java │ ├── CoreHeadRequestHandler.java │ ├── CoreOptionsRequestHandler.java │ ├── CorePatchRequestHandler.java │ ├── CoreProtocol.java │ └── validation │ │ ├── ContentLengthValidator.java │ │ ├── ContentTypeValidator.java │ │ ├── HttpMethodValidator.java │ │ ├── IdExistsValidator.java │ │ ├── TusResumableValidator.java │ │ └── UploadOffsetValidator.java │ ├── creation │ ├── CreationExtension.java │ ├── CreationHeadRequestHandler.java │ ├── CreationOptionsRequestHandler.java │ ├── CreationPatchRequestHandler.java │ ├── CreationPostRequestHandler.java │ └── validation │ │ ├── PostEmptyRequestValidator.java │ │ ├── PostUriValidator.java │ │ ├── UploadDeferLengthValidator.java │ │ └── UploadLengthValidator.java │ ├── download │ ├── DownloadExtension.java │ ├── DownloadGetRequestHandler.java │ └── DownloadOptionsRequestHandler.java │ ├── exception │ ├── ChecksumAlgorithmNotSupportedException.java │ ├── InvalidContentLengthException.java │ ├── InvalidContentTypeException.java │ ├── InvalidPartialUploadIdException.java │ ├── InvalidTusResumableException.java │ ├── InvalidUploadLengthException.java │ ├── InvalidUploadOffsetException.java │ ├── MaxUploadLengthExceededException.java │ ├── PatchOnFinalUploadNotAllowedException.java │ ├── PostOnInvalidRequestURIException.java │ ├── TusException.java │ ├── UnsupportedMethodException.java │ ├── UploadAlreadyLockedException.java │ ├── UploadChecksumMismatchException.java │ ├── UploadInProgressException.java │ ├── UploadLengthNotAllowedOnConcatenationException.java │ ├── UploadNotFoundException.java │ └── UploadOffsetMismatchException.java │ ├── expiration │ ├── ExpirationExtension.java │ ├── ExpirationOptionsRequestHandler.java │ └── ExpirationRequestHandler.java │ ├── termination │ ├── TerminationDeleteRequestHandler.java │ ├── TerminationExtension.java │ └── TerminationOptionsRequestHandler.java │ ├── upload │ ├── TimeBasedUploadIdFactory.java │ ├── UploadId.java │ ├── UploadIdFactory.java │ ├── UploadInfo.java │ ├── UploadLock.java │ ├── UploadLockingService.java │ ├── UploadStorageService.java │ ├── UploadType.java │ ├── UuidUploadIdFactory.java │ ├── cache │ │ └── ThreadLocalCachedStorageAndLockingService.java │ ├── concatenation │ │ ├── UploadConcatenationService.java │ │ ├── UploadInputStreamEnumeration.java │ │ └── VirtualConcatenationService.java │ └── disk │ │ ├── AbstractDiskBasedService.java │ │ ├── DiskLockingService.java │ │ ├── DiskStorageService.java │ │ ├── ExpiredUploadFilter.java │ │ ├── FileBasedLock.java │ │ └── StoragePathNotAvailableException.java │ └── util │ ├── AbstractExtensionRequestHandler.java │ ├── AbstractRequestHandler.java │ ├── AbstractTusExtension.java │ ├── HttpChunkedEncodingInputStream.java │ ├── TusServletRequest.java │ ├── TusServletResponse.java │ └── Utils.java └── test ├── java └── me │ └── desair │ └── tus │ └── server │ ├── AbstractTusExtensionIntegrationTest.java │ ├── HttpMethodTest.java │ ├── ITTusFileUploadService.java │ ├── ITTusFileUploadServiceCached.java │ ├── checksum │ ├── ChecksumAlgorithmTest.java │ ├── ChecksumOptionsRequestHandlerTest.java │ ├── ChecksumPatchRequestHandlerTest.java │ ├── ITChecksumExtension.java │ └── validation │ │ └── ChecksumAlgorithmValidatorTest.java │ ├── concatenation │ ├── ConcatenationHeadRequestHandlerTest.java │ ├── ConcatenationOptionsRequestHandlerTest.java │ ├── ConcatenationPostRequestHandlerTest.java │ └── validation │ │ ├── NoUploadLengthOnFinalValidatorTest.java │ │ ├── PartialUploadsExistValidatorTest.java │ │ └── PatchFinalUploadValidatorTest.java │ ├── core │ ├── CoreDefaultResponseHeadersHandlerTest.java │ ├── CoreHeadRequestHandlerTest.java │ ├── CoreOptionsRequestHandlerTest.java │ ├── CorePatchRequestHandlerTest.java │ ├── ITCoreProtocol.java │ └── validation │ │ ├── ContentLengthValidatorTest.java │ │ ├── ContentTypeValidatorTest.java │ │ ├── HttpMethodValidatorTest.java │ │ ├── IdExistsValidatorTest.java │ │ ├── TusResumableValidatorTest.java │ │ └── UploadOffsetValidatorTest.java │ ├── creation │ ├── CreationHeadRequestHandlerTest.java │ ├── CreationOptionsRequestHandlerTest.java │ ├── CreationPatchRequestHandlerTest.java │ ├── CreationPostRequestHandlerTest.java │ ├── ITCreationExtension.java │ └── validation │ │ ├── PostEmptyRequestValidatorTest.java │ │ ├── PostUriValidatorTest.java │ │ ├── UploadDeferLengthValidatorTest.java │ │ └── UploadLengthValidatorTest.java │ ├── download │ ├── DownloadGetRequestHandlerTest.java │ └── DownloadOptionsRequestHandlerTest.java │ ├── expiration │ ├── ExpirationOptionsRequestHandlerTest.java │ └── ExpirationRequestHandlerTest.java │ ├── termination │ └── TerminationDeleteRequestHandlerTest.java │ ├── upload │ ├── TimeBasedUploadIdFactoryTest.java │ ├── UploadIdTest.java │ ├── UploadInfoTest.java │ ├── UuidUploadIdFactoryTest.java │ ├── concatenation │ │ ├── UploadInputStreamEnumerationTest.java │ │ └── VirtualConcatenationServiceTest.java │ └── disk │ │ ├── DiskLockingServiceTest.java │ │ ├── DiskStorageServiceTest.java │ │ ├── ExpiredUploadFilterTest.java │ │ └── FileBasedLockTest.java │ └── util │ ├── HttpChunkedEncodingInputStreamTest.java │ ├── MapMatcher.java │ └── TusServletResponseTest.java └── resources └── simplelogger.properties /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Build and Tests 10 | 11 | on: 12 | push: 13 | branches: ["master"] 14 | paths: ["**.java", ".github/workflows/build.yml", "pom.xml"] 15 | pull_request: 16 | branches: ["master"] 17 | schedule: 18 | - cron: "30 5 11 * *" 19 | workflow_dispatch: 20 | 21 | jobs: 22 | mvn-install: 23 | strategy: 24 | matrix: 25 | java: [17] 26 | os: [ubuntu-latest, windows-latest] 27 | include: 28 | - os: ubuntu-latest 29 | java: 17 30 | upload-dependency-graph: true 31 | run-sonarcloud: true 32 | run-coveralls: true 33 | 34 | runs-on: ${{ matrix.os }} 35 | 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v3 39 | - name: Set up JDK ${{ matrix.java }} 40 | uses: actions/setup-java@v3 41 | with: 42 | java-version: ${{ matrix.java }} 43 | distribution: "temurin" 44 | cache: maven 45 | - name: Build with Maven 46 | run: mvn install 47 | 48 | # Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive 49 | - if: ${{ matrix.upload-dependency-graph }} 50 | name: Update dependency graph 51 | uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 52 | 53 | - if: ${{ matrix.run-sonarcloud }} 54 | name: SonarCloud 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 58 | run: mvn sonar:sonar -P sonarcloud 59 | 60 | - if: ${{ matrix.run-coveralls }} 61 | name: Coveralls 62 | uses: coverallsapp/github-action@v2 63 | with: 64 | # *base-path* is prepended to all paths in order to correctly reference source files on coveralls.io 65 | base-path: src/main/java 66 | 67 | # *file* is optional, but good to have. By default coveralls will try to find the report automatically. 68 | files: target/site/jacoco-it/jacoco.xml target/site/jacoco-ut/jacoco.xml 69 | -------------------------------------------------------------------------------- /.github/workflows/codestyle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Google Java Codestyle 10 | 11 | on: 12 | push: 13 | branches: ["master"] 14 | paths: ["**.java", ".github/workflows/build.yml", "pom.xml"] 15 | pull_request: 16 | branches: ["master"] 17 | workflow_dispatch: 18 | 19 | jobs: 20 | check-codestyle: 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | - name: Set up JDK 17 27 | uses: actions/setup-java@v3 28 | with: 29 | java-version: "17" 30 | distribution: "temurin" 31 | cache: maven 32 | - name: Check codestyle with Maven 33 | run: mvn -P codestyle com.spotify.fmt:fmt-maven-plugin:check 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/java,maven,intellij,visualstudiocode 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=java,maven,intellij,visualstudiocode 4 | 5 | ### Intellij ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # AWS User-specific 17 | .idea/**/aws.xml 18 | 19 | # Generated files 20 | .idea/**/contentModel.xml 21 | 22 | # Sensitive or high-churn files 23 | .idea/**/dataSources/ 24 | .idea/**/dataSources.ids 25 | .idea/**/dataSources.local.xml 26 | .idea/**/sqlDataSources.xml 27 | .idea/**/dynamic.xml 28 | .idea/**/uiDesigner.xml 29 | .idea/**/dbnavigator.xml 30 | 31 | # Gradle 32 | .idea/**/gradle.xml 33 | .idea/**/libraries 34 | 35 | # Gradle and Maven with auto-import 36 | # When using Gradle or Maven with auto-import, you should exclude module files, 37 | # since they will be recreated, and may cause churn. Uncomment if using 38 | # auto-import. 39 | # .idea/artifacts 40 | # .idea/compiler.xml 41 | # .idea/jarRepositories.xml 42 | # .idea/modules.xml 43 | # .idea/*.iml 44 | # .idea/modules 45 | # *.iml 46 | # *.ipr 47 | 48 | # CMake 49 | cmake-build-*/ 50 | 51 | # Mongo Explorer plugin 52 | .idea/**/mongoSettings.xml 53 | 54 | # File-based project format 55 | *.iws 56 | 57 | # IntelliJ 58 | out/ 59 | 60 | # mpeltonen/sbt-idea plugin 61 | .idea_modules/ 62 | 63 | # JIRA plugin 64 | atlassian-ide-plugin.xml 65 | 66 | # Cursive Clojure plugin 67 | .idea/replstate.xml 68 | 69 | # Crashlytics plugin (for Android Studio and IntelliJ) 70 | com_crashlytics_export_strings.xml 71 | crashlytics.properties 72 | crashlytics-build.properties 73 | fabric.properties 74 | 75 | # Editor-based Rest Client 76 | .idea/httpRequests 77 | 78 | # Android studio 3.1+ serialized cache file 79 | .idea/caches/build_file_checksums.ser 80 | 81 | ### Intellij Patch ### 82 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 83 | 84 | # *.iml 85 | # modules.xml 86 | # .idea/misc.xml 87 | # *.ipr 88 | 89 | # Sonarlint plugin 90 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 91 | .idea/**/sonarlint/ 92 | 93 | # SonarQube Plugin 94 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 95 | .idea/**/sonarIssues.xml 96 | 97 | # Markdown Navigator plugin 98 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 99 | .idea/**/markdown-navigator.xml 100 | .idea/**/markdown-navigator-enh.xml 101 | .idea/**/markdown-navigator/ 102 | 103 | # Cache file creation bug 104 | # See https://youtrack.jetbrains.com/issue/JBR-2257 105 | .idea/$CACHE_FILE$ 106 | 107 | # CodeStream plugin 108 | # https://plugins.jetbrains.com/plugin/12206-codestream 109 | .idea/codestream.xml 110 | 111 | ### Java ### 112 | # Compiled class file 113 | *.class 114 | 115 | # Log file 116 | *.log 117 | 118 | # BlueJ files 119 | *.ctxt 120 | 121 | # Mobile Tools for Java (J2ME) 122 | .mtj.tmp/ 123 | 124 | # Package Files # 125 | *.jar 126 | *.war 127 | *.nar 128 | *.ear 129 | *.zip 130 | *.tar.gz 131 | *.rar 132 | 133 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 134 | hs_err_pid* 135 | 136 | ### Maven ### 137 | target/ 138 | pom.xml.tag 139 | pom.xml.releaseBackup 140 | pom.xml.versionsBackup 141 | pom.xml.next 142 | release.properties 143 | dependency-reduced-pom.xml 144 | buildNumber.properties 145 | .mvn/timing.properties 146 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 147 | .mvn/wrapper/maven-wrapper.jar 148 | 149 | ### VisualStudioCode ### 150 | .vscode/* 151 | !.vscode/settings.json 152 | !.vscode/tasks.json 153 | !.vscode/launch.json 154 | !.vscode/extensions.json 155 | *.code-workspace 156 | 157 | .settings 158 | .project 159 | .classpath 160 | 161 | # Local History for Visual Studio Code 162 | .history/ 163 | 164 | ### VisualStudioCode Patch ### 165 | # Ignore all local history of files 166 | .history 167 | .ionide 168 | .vscode 169 | 170 | # End of https://www.toptal.com/developers/gitignore/api/java,maven,intellij,visualstudiocode 171 | .vscode/ 172 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: check-added-large-files 6 | - id: check-json 7 | - id: check-merge-conflict 8 | - id: check-xml 9 | - id: check-yaml 10 | - id: end-of-file-fixer 11 | - id: trailing-whitespace 12 | - repo: https://github.com/dustinsand/pre-commit-jvm 13 | rev: v0.8.0 14 | hooks: 15 | - id: google-java-formatter-jdk11 16 | args: [--replace, --set-exit-if-changed] 17 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Expected Behaviour 2 | (Please describe the behaviour you expected to see. If possible please refer to paragraphs of the official tus protocol specification on https://tus.io/.) 3 | 4 | ### Actual Behaviour 5 | (What faulty behaviour does the implementation have?) 6 | 7 | ### Steps to reproduce 8 | (Ideally, you specify detailed steps using the curl command.) 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tom Desair (http://tom.desair.me) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/HttpMethod.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import java.util.Set; 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.apache.commons.lang3.Validate; 7 | 8 | /** 9 | * Class that represents a HTTP method. The X-HTTP-Method-Override request header MUST be a string 10 | * which MUST be interpreted as the request’s method by the Server, if the header is presented. The 11 | * actual method of the request MUST be ignored. The Client SHOULD use this header if its 12 | * environment does not support the PATCH or DELETE methods. 13 | * (https://tus.io/protocols/resumable-upload.html#x-http-method-override) 14 | */ 15 | public enum HttpMethod { 16 | GET, 17 | HEAD, 18 | POST, 19 | PUT, 20 | DELETE, 21 | CONNECT, 22 | OPTIONS, 23 | TRACE, 24 | PATCH; 25 | 26 | /** Get the {@link HttpMethod} instance that matches the provided name. */ 27 | public static HttpMethod forName(String name) { 28 | for (HttpMethod method : HttpMethod.values()) { 29 | if (StringUtils.equalsIgnoreCase(method.name(), name)) { 30 | return method; 31 | } 32 | } 33 | 34 | return null; 35 | } 36 | 37 | /** 38 | * Get the {@link HttpMethod} instance of this request if it is present in the provided 39 | * supportedHttpMethods set. 40 | */ 41 | public static HttpMethod getMethodIfSupported( 42 | HttpServletRequest request, Set supportedHttpMethods) { 43 | Validate.notNull(request, "The HttpServletRequest cannot be null"); 44 | 45 | String requestMethod = request.getHeader(HttpHeader.METHOD_OVERRIDE); 46 | if (StringUtils.isBlank(requestMethod) || forName(requestMethod) == null) { 47 | requestMethod = request.getMethod(); 48 | } 49 | 50 | HttpMethod httpMethod = forName(requestMethod); 51 | return httpMethod != null && supportedHttpMethods.contains(httpMethod) ? httpMethod : null; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/RequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server; 2 | 3 | import java.io.IOException; 4 | import me.desair.tus.server.exception.TusException; 5 | import me.desair.tus.server.upload.UploadStorageService; 6 | import me.desair.tus.server.util.TusServletRequest; 7 | import me.desair.tus.server.util.TusServletResponse; 8 | 9 | public interface RequestHandler { 10 | 11 | boolean supports(HttpMethod method); 12 | 13 | void process( 14 | HttpMethod method, 15 | TusServletRequest servletRequest, 16 | TusServletResponse servletResponse, 17 | UploadStorageService uploadStorageService, 18 | String ownerKey) 19 | throws IOException, TusException; 20 | 21 | boolean isErrorHandler(); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/RequestValidator.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import java.io.IOException; 5 | import me.desair.tus.server.exception.TusException; 6 | import me.desair.tus.server.upload.UploadStorageService; 7 | 8 | /** Interface for request validators */ 9 | public interface RequestValidator { 10 | 11 | /** 12 | * Validate if the request should be processed 13 | * 14 | * @param method The HTTP method of this request (do not use {@link 15 | * HttpServletRequest#getMethod()}!) 16 | * @param request The {@link HttpServletRequest} to validate 17 | * @param uploadStorageService The current upload storage service 18 | * @param ownerKey A key representing the owner of the upload 19 | * @throws TusException When validation fails and the request should not be processed 20 | */ 21 | void validate( 22 | HttpMethod method, 23 | HttpServletRequest request, 24 | UploadStorageService uploadStorageService, 25 | String ownerKey) 26 | throws TusException, IOException; 27 | 28 | /** 29 | * Test if this validator supports the given HTTP method 30 | * 31 | * @param method The current HTTP method 32 | * @return true if supported, false otherwise 33 | */ 34 | boolean supports(HttpMethod method); 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/TusExtension.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import java.io.IOException; 5 | import java.util.Collection; 6 | import me.desair.tus.server.exception.TusException; 7 | import me.desair.tus.server.upload.UploadStorageService; 8 | import me.desair.tus.server.util.TusServletRequest; 9 | import me.desair.tus.server.util.TusServletResponse; 10 | 11 | /** Interface that represents an extension in the tus protocol. */ 12 | public interface TusExtension { 13 | 14 | /** 15 | * The name of the Tus extension that can be used to disable or enable the extension. 16 | * 17 | * @return The name of the extension 18 | */ 19 | String getName(); 20 | 21 | /** 22 | * Validate the given request. 23 | * 24 | * @param method The HTTP method of this request (taking into account overrides) 25 | * @param servletRequest The HTTP request 26 | * @param uploadStorageService The current upload storage service 27 | * @param ownerKey Identifier of the owner of this upload 28 | * @throws TusException When the request is invalid 29 | * @throws IOException When unable to read upload information 30 | */ 31 | void validate( 32 | HttpMethod method, 33 | HttpServletRequest servletRequest, 34 | UploadStorageService uploadStorageService, 35 | String ownerKey) 36 | throws TusException, IOException; 37 | 38 | /** 39 | * Process the given request. 40 | * 41 | * @param method The HTTP method of this request (taking into account overrides) 42 | * @param servletRequest The HTTP request 43 | * @param servletResponse The HTTP response 44 | * @param uploadStorageService The current upload storage service 45 | * @param ownerKey Identifier of the owner of this upload 46 | * @throws TusException When processing the request fails 47 | * @throws IOException When unable to read upload information 48 | */ 49 | void process( 50 | HttpMethod method, 51 | TusServletRequest servletRequest, 52 | TusServletResponse servletResponse, 53 | UploadStorageService uploadStorageService, 54 | String ownerKey) 55 | throws IOException, TusException; 56 | 57 | /** 58 | * If a request is invalid, or when processing the request fails, it might be necessary to react 59 | * to this failure. This method allows extensions to react to validation or processing failures. 60 | * 61 | * @param method The HTTP method of this request (taking into account overrides) 62 | * @param servletRequest The HTTP request 63 | * @param servletResponse The HTTP response 64 | * @param uploadStorageService The current upload storage service 65 | * @param ownerKey Identifier of the owner of this upload 66 | * @throws TusException When handling the error fails 67 | * @throws IOException When unable to read upload information 68 | */ 69 | void handleError( 70 | HttpMethod method, 71 | TusServletRequest servletRequest, 72 | TusServletResponse servletResponse, 73 | UploadStorageService uploadStorageService, 74 | String ownerKey) 75 | throws IOException, TusException; 76 | 77 | /** 78 | * The minimal list of HTTP methods that this extension needs to function properly. 79 | * 80 | * @return The list of HTTP methods required by this extension 81 | */ 82 | Collection getMinimalSupportedHttpMethods(); 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/checksum/ChecksumAlgorithm.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.checksum; 2 | 3 | import java.security.MessageDigest; 4 | import java.security.NoSuchAlgorithmException; 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | /** 10 | * Enum that contains all supported checksum algorithms The names of the checksum algorithms MUST 11 | * only consist of ASCII characters with the modification that uppercase characters are excluded. 12 | */ 13 | public enum ChecksumAlgorithm { 14 | MD5("MD5", "md5"), 15 | SHA1("SHA-1", "sha1"), 16 | SHA256("SHA-256", "sha256"), 17 | SHA384("SHA-384", "sha384"), 18 | SHA512("SHA-512", "sha512"); 19 | 20 | public static final String CHECKSUM_VALUE_SEPARATOR = " "; 21 | 22 | private static final Logger log = LoggerFactory.getLogger(ChecksumAlgorithm.class); 23 | 24 | private String javaName; 25 | private String tusName; 26 | 27 | ChecksumAlgorithm(String javaName, String tusName) { 28 | this.javaName = javaName; 29 | this.tusName = tusName; 30 | } 31 | 32 | public String getJavaName() { 33 | return javaName; 34 | } 35 | 36 | public String getTusName() { 37 | return tusName; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return getTusName(); 43 | } 44 | 45 | public MessageDigest getMessageDigest() { 46 | try { 47 | return MessageDigest.getInstance(getJavaName()); 48 | } catch (NoSuchAlgorithmException e) { 49 | log.error("We are trying the use an algorithm that is not supported by this JVM", e); 50 | return null; 51 | } 52 | } 53 | 54 | public static ChecksumAlgorithm forTusName(String name) { 55 | for (ChecksumAlgorithm alg : ChecksumAlgorithm.values()) { 56 | if (alg.getTusName().equals(name)) { 57 | return alg; 58 | } 59 | } 60 | return null; 61 | } 62 | 63 | public static ChecksumAlgorithm forUploadChecksumHeader(String uploadChecksumHeader) { 64 | String algorithm = StringUtils.substringBefore(uploadChecksumHeader, CHECKSUM_VALUE_SEPARATOR); 65 | return forTusName(algorithm); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/checksum/ChecksumExtension.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.checksum; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collection; 5 | import java.util.List; 6 | import me.desair.tus.server.HttpMethod; 7 | import me.desair.tus.server.RequestHandler; 8 | import me.desair.tus.server.RequestValidator; 9 | import me.desair.tus.server.checksum.validation.ChecksumAlgorithmValidator; 10 | import me.desair.tus.server.util.AbstractTusExtension; 11 | 12 | /** 13 | * The Client and the Server MAY implement and use this extension to verify data integrity of each 14 | * PATCH request. If supported, the Server MUST add checksum to the Tus-Extension header. 15 | */ 16 | public class ChecksumExtension extends AbstractTusExtension { 17 | 18 | @Override 19 | public String getName() { 20 | return "checksum"; 21 | } 22 | 23 | @Override 24 | public Collection getMinimalSupportedHttpMethods() { 25 | return Arrays.asList(HttpMethod.OPTIONS, HttpMethod.PATCH); 26 | } 27 | 28 | @Override 29 | protected void initValidators(List requestValidators) { 30 | requestValidators.add(new ChecksumAlgorithmValidator()); 31 | } 32 | 33 | @Override 34 | protected void initRequestHandlers(List requestHandlers) { 35 | requestHandlers.add(new ChecksumOptionsRequestHandler()); 36 | requestHandlers.add(new ChecksumPatchRequestHandler()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/checksum/ChecksumOptionsRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.checksum; 2 | 3 | import me.desair.tus.server.HttpHeader; 4 | import me.desair.tus.server.HttpMethod; 5 | import me.desair.tus.server.upload.UploadStorageService; 6 | import me.desair.tus.server.util.AbstractExtensionRequestHandler; 7 | import me.desair.tus.server.util.TusServletRequest; 8 | import me.desair.tus.server.util.TusServletResponse; 9 | import org.apache.commons.lang3.StringUtils; 10 | 11 | /** 12 | * The Tus-Checksum-Algorithm header MUST be included in the response to an OPTIONS request. The 13 | * Tus-Checksum-Algorithm response header MUST be a comma-separated list of the checksum algorithms 14 | * supported by the server. 15 | */ 16 | public class ChecksumOptionsRequestHandler extends AbstractExtensionRequestHandler { 17 | 18 | @Override 19 | public void process( 20 | HttpMethod method, 21 | TusServletRequest servletRequest, 22 | TusServletResponse servletResponse, 23 | UploadStorageService uploadStorageService, 24 | String ownerKey) { 25 | 26 | super.process(method, servletRequest, servletResponse, uploadStorageService, ownerKey); 27 | 28 | servletResponse.setHeader( 29 | HttpHeader.TUS_CHECKSUM_ALGORITHM, StringUtils.join(ChecksumAlgorithm.values(), ",")); 30 | } 31 | 32 | @Override 33 | protected void appendExtensions(StringBuilder extensionBuilder) { 34 | addExtension(extensionBuilder, "checksum"); 35 | addExtension(extensionBuilder, "checksum-trailer"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/checksum/ChecksumPatchRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.checksum; 2 | 3 | import static me.desair.tus.server.checksum.ChecksumAlgorithm.CHECKSUM_VALUE_SEPARATOR; 4 | 5 | import java.io.IOException; 6 | import me.desair.tus.server.HttpHeader; 7 | import me.desair.tus.server.HttpMethod; 8 | import me.desair.tus.server.checksum.validation.ChecksumAlgorithmValidator; 9 | import me.desair.tus.server.exception.TusException; 10 | import me.desair.tus.server.exception.UploadChecksumMismatchException; 11 | import me.desair.tus.server.upload.UploadStorageService; 12 | import me.desair.tus.server.util.AbstractRequestHandler; 13 | import me.desair.tus.server.util.TusServletRequest; 14 | import me.desair.tus.server.util.TusServletResponse; 15 | import org.apache.commons.lang3.StringUtils; 16 | 17 | public class ChecksumPatchRequestHandler extends AbstractRequestHandler { 18 | 19 | @Override 20 | public boolean supports(HttpMethod method) { 21 | return HttpMethod.PATCH.equals(method); 22 | } 23 | 24 | @Override 25 | public void process( 26 | HttpMethod method, 27 | TusServletRequest servletRequest, 28 | TusServletResponse servletResponse, 29 | UploadStorageService uploadStorageService, 30 | String ownerKey) 31 | throws IOException, TusException { 32 | 33 | String uploadChecksumHeader = servletRequest.getHeader(HttpHeader.UPLOAD_CHECKSUM); 34 | 35 | if (servletRequest.hasCalculatedChecksum() && StringUtils.isNotBlank(uploadChecksumHeader)) { 36 | 37 | // The Upload-Checksum header can be a trailing header which is only present after 38 | // reading the 39 | // full content. 40 | // Therefor we need to revalidate that header here 41 | new ChecksumAlgorithmValidator() 42 | .validate(method, servletRequest, uploadStorageService, ownerKey); 43 | 44 | // Everything is valid, check if the checksum matches 45 | String expectedValue = 46 | StringUtils.substringAfter(uploadChecksumHeader, CHECKSUM_VALUE_SEPARATOR); 47 | 48 | ChecksumAlgorithm checksumAlgorithm = 49 | ChecksumAlgorithm.forUploadChecksumHeader(uploadChecksumHeader); 50 | String calculatedValue = servletRequest.getCalculatedChecksum(checksumAlgorithm); 51 | 52 | if (!StringUtils.equals(expectedValue, calculatedValue)) { 53 | // throw an exception if the checksum is invalid. This will also trigger the removal 54 | // of any 55 | // bytes that were already saved 56 | throw new UploadChecksumMismatchException( 57 | "Expected checksum " 58 | + expectedValue 59 | + " but was " 60 | + calculatedValue 61 | + " with algorithm " 62 | + checksumAlgorithm); 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/checksum/validation/ChecksumAlgorithmValidator.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.checksum.validation; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import java.io.IOException; 5 | import me.desair.tus.server.HttpHeader; 6 | import me.desair.tus.server.HttpMethod; 7 | import me.desair.tus.server.RequestValidator; 8 | import me.desair.tus.server.checksum.ChecksumAlgorithm; 9 | import me.desair.tus.server.exception.ChecksumAlgorithmNotSupportedException; 10 | import me.desair.tus.server.exception.TusException; 11 | import me.desair.tus.server.upload.UploadStorageService; 12 | import org.apache.commons.lang3.StringUtils; 13 | 14 | /** 15 | * The Server MAY respond with one of the following status code: 400 Bad Request if the checksum 16 | * algorithm is not supported by the server 17 | */ 18 | public class ChecksumAlgorithmValidator implements RequestValidator { 19 | 20 | @Override 21 | public void validate( 22 | HttpMethod method, 23 | HttpServletRequest request, 24 | UploadStorageService uploadStorageService, 25 | String ownerKey) 26 | throws TusException, IOException { 27 | 28 | String uploadChecksum = request.getHeader(HttpHeader.UPLOAD_CHECKSUM); 29 | 30 | // If the client provided a checksum header, check that we support the algorithm 31 | if (StringUtils.isNotBlank(uploadChecksum) 32 | && ChecksumAlgorithm.forUploadChecksumHeader(uploadChecksum) == null) { 33 | 34 | throw new ChecksumAlgorithmNotSupportedException( 35 | "The " 36 | + HttpHeader.UPLOAD_CHECKSUM 37 | + " header value " 38 | + uploadChecksum 39 | + " is not supported"); 40 | } 41 | } 42 | 43 | @Override 44 | public boolean supports(HttpMethod method) { 45 | return HttpMethod.PATCH.equals(method); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/concatenation/ConcatenationExtension.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.concatenation; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collection; 5 | import java.util.List; 6 | import me.desair.tus.server.HttpMethod; 7 | import me.desair.tus.server.RequestHandler; 8 | import me.desair.tus.server.RequestValidator; 9 | import me.desair.tus.server.concatenation.validation.NoUploadLengthOnFinalValidator; 10 | import me.desair.tus.server.concatenation.validation.PartialUploadsExistValidator; 11 | import me.desair.tus.server.concatenation.validation.PatchFinalUploadValidator; 12 | import me.desair.tus.server.util.AbstractTusExtension; 13 | 14 | /** 15 | * This extension can be used to concatenate multiple uploads into a single one enabling Clients to 16 | * perform parallel uploads and to upload non-contiguous chunks. If the Server supports this 17 | * extension, it MUST add concatenation to the Tus-Extension header. 18 | */ 19 | public class ConcatenationExtension extends AbstractTusExtension { 20 | 21 | @Override 22 | public String getName() { 23 | return "concatenation"; 24 | } 25 | 26 | @Override 27 | public Collection getMinimalSupportedHttpMethods() { 28 | return Arrays.asList(HttpMethod.OPTIONS, HttpMethod.POST, HttpMethod.PATCH, HttpMethod.HEAD); 29 | } 30 | 31 | @Override 32 | protected void initValidators(List requestValidators) { 33 | requestValidators.add(new PatchFinalUploadValidator()); 34 | requestValidators.add(new NoUploadLengthOnFinalValidator()); 35 | requestValidators.add(new PartialUploadsExistValidator()); 36 | } 37 | 38 | @Override 39 | protected void initRequestHandlers(List requestHandlers) { 40 | requestHandlers.add(new ConcatenationOptionsRequestHandler()); 41 | requestHandlers.add(new ConcatenationPostRequestHandler()); 42 | requestHandlers.add(new ConcatenationHeadRequestHandler()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/concatenation/ConcatenationHeadRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.concatenation; 2 | 3 | import java.io.IOException; 4 | import java.util.Objects; 5 | import me.desair.tus.server.HttpHeader; 6 | import me.desair.tus.server.HttpMethod; 7 | import me.desair.tus.server.exception.TusException; 8 | import me.desair.tus.server.upload.UploadInfo; 9 | import me.desair.tus.server.upload.UploadStorageService; 10 | import me.desair.tus.server.upload.UploadType; 11 | import me.desair.tus.server.util.AbstractRequestHandler; 12 | import me.desair.tus.server.util.TusServletRequest; 13 | import me.desair.tus.server.util.TusServletResponse; 14 | 15 | /** 16 | * The response to a HEAD request for a upload SHOULD NOT contain the Upload-Offset header unless 17 | * the concatenation has been successfully finished. After successful concatenation, the 18 | * Upload-Offset and Upload-Length MUST be set and their values MUST be equal. The value of the 19 | * Upload-Offset header before concatenation is not defined for a upload.
20 | * The response to a HEAD request for a partial upload MUST contain the Upload-Offset header. 21 | * Response to HEAD request against partial or upload MUST include the Upload-Concat header and its 22 | * value as received in the upload creation request. 23 | */ 24 | public class ConcatenationHeadRequestHandler extends AbstractRequestHandler { 25 | 26 | @Override 27 | public boolean supports(HttpMethod method) { 28 | return HttpMethod.HEAD.equals(method); 29 | } 30 | 31 | @Override 32 | public void process( 33 | HttpMethod method, 34 | TusServletRequest servletRequest, 35 | TusServletResponse servletResponse, 36 | UploadStorageService uploadStorageService, 37 | String ownerKey) 38 | throws IOException, TusException { 39 | 40 | UploadInfo uploadInfo = 41 | uploadStorageService.getUploadInfo(servletRequest.getRequestURI(), ownerKey); 42 | 43 | if (!UploadType.REGULAR.equals(uploadInfo.getUploadType())) { 44 | servletResponse.setHeader(HttpHeader.UPLOAD_CONCAT, uploadInfo.getUploadConcatHeaderValue()); 45 | } 46 | 47 | if (UploadType.CONCATENATED.equals(uploadInfo.getUploadType())) { 48 | if (uploadInfo.isUploadInProgress()) { 49 | // Execute the merge function again to update our upload data 50 | uploadStorageService.getUploadConcatenationService().merge(uploadInfo); 51 | } 52 | 53 | if (uploadInfo.hasLength()) { 54 | servletResponse.setHeader( 55 | HttpHeader.UPLOAD_LENGTH, Objects.toString(uploadInfo.getLength())); 56 | } 57 | 58 | if (!uploadInfo.isUploadInProgress()) { 59 | servletResponse.setHeader( 60 | HttpHeader.UPLOAD_OFFSET, Objects.toString(uploadInfo.getOffset())); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/concatenation/ConcatenationOptionsRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.concatenation; 2 | 3 | import me.desair.tus.server.util.AbstractExtensionRequestHandler; 4 | 5 | /** 6 | * If the Server supports this extension, it MUST add concatenation to the Tus-Extension header. The 7 | * Client MAY send the concatenation request while the partial uploads are still in progress. This 8 | * feature MUST be explicitly announced by the Server by adding concatenation-unfinished to the 9 | * Tus-Extension header. 10 | */ 11 | public class ConcatenationOptionsRequestHandler extends AbstractExtensionRequestHandler { 12 | 13 | @Override 14 | protected void appendExtensions(StringBuilder extensionBuilder) { 15 | addExtension(extensionBuilder, "concatenation"); 16 | addExtension(extensionBuilder, "concatenation-unfinished"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/concatenation/ConcatenationPostRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.concatenation; 2 | 3 | import java.io.IOException; 4 | import me.desair.tus.server.HttpHeader; 5 | import me.desair.tus.server.HttpMethod; 6 | import me.desair.tus.server.exception.TusException; 7 | import me.desair.tus.server.upload.UploadInfo; 8 | import me.desair.tus.server.upload.UploadStorageService; 9 | import me.desair.tus.server.upload.UploadType; 10 | import me.desair.tus.server.util.AbstractRequestHandler; 11 | import me.desair.tus.server.util.TusServletRequest; 12 | import me.desair.tus.server.util.TusServletResponse; 13 | import me.desair.tus.server.util.Utils; 14 | import org.apache.commons.lang3.StringUtils; 15 | 16 | /** 17 | * The Server MUST acknowledge a successful upload creation with the 201 Created status. The Server 18 | * MUST set the Location header to the URL of the created resource. This URL MAY be absolute or 19 | * relative. 20 | */ 21 | public class ConcatenationPostRequestHandler extends AbstractRequestHandler { 22 | 23 | @Override 24 | public boolean supports(HttpMethod method) { 25 | return HttpMethod.POST.equals(method); 26 | } 27 | 28 | @Override 29 | public void process( 30 | HttpMethod method, 31 | TusServletRequest servletRequest, 32 | TusServletResponse servletResponse, 33 | UploadStorageService uploadStorageService, 34 | String ownerKey) 35 | throws IOException, TusException { 36 | 37 | // For post requests, the upload URI is part of the response 38 | String uploadUri = servletResponse.getHeader(HttpHeader.LOCATION); 39 | UploadInfo uploadInfo = uploadStorageService.getUploadInfo(uploadUri, ownerKey); 40 | 41 | if (uploadInfo != null) { 42 | 43 | String uploadConcatValue = servletRequest.getHeader(HttpHeader.UPLOAD_CONCAT); 44 | if (StringUtils.equalsIgnoreCase(uploadConcatValue, "partial")) { 45 | uploadInfo.setUploadType(UploadType.PARTIAL); 46 | 47 | } else if (StringUtils.startsWithIgnoreCase(uploadConcatValue, "final")) { 48 | // reset the length, just to be sure 49 | uploadInfo.setLength(null); 50 | uploadInfo.setUploadType(UploadType.CONCATENATED); 51 | uploadInfo.setConcatenationPartIds( 52 | Utils.parseConcatenationIDsFromHeader(uploadConcatValue)); 53 | 54 | uploadStorageService.getUploadConcatenationService().merge(uploadInfo); 55 | 56 | } else { 57 | uploadInfo.setUploadType(UploadType.REGULAR); 58 | } 59 | 60 | uploadInfo.setUploadConcatHeaderValue(uploadConcatValue); 61 | 62 | uploadStorageService.update(uploadInfo); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/concatenation/validation/NoUploadLengthOnFinalValidator.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.concatenation.validation; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import java.io.IOException; 5 | import me.desair.tus.server.HttpHeader; 6 | import me.desair.tus.server.HttpMethod; 7 | import me.desair.tus.server.RequestValidator; 8 | import me.desair.tus.server.exception.TusException; 9 | import me.desair.tus.server.exception.UploadLengthNotAllowedOnConcatenationException; 10 | import me.desair.tus.server.upload.UploadStorageService; 11 | import org.apache.commons.lang3.StringUtils; 12 | 13 | /** The Client MUST NOT include the Upload-Length header in the upload creation. */ 14 | public class NoUploadLengthOnFinalValidator implements RequestValidator { 15 | 16 | @Override 17 | public void validate( 18 | HttpMethod method, 19 | HttpServletRequest request, 20 | UploadStorageService uploadStorageService, 21 | String ownerKey) 22 | throws IOException, TusException { 23 | 24 | String uploadConcatValue = request.getHeader(HttpHeader.UPLOAD_CONCAT); 25 | 26 | if (StringUtils.startsWithIgnoreCase(uploadConcatValue, "final") 27 | && StringUtils.isNotBlank(request.getHeader(HttpHeader.UPLOAD_LENGTH))) { 28 | 29 | throw new UploadLengthNotAllowedOnConcatenationException( 30 | "The upload length of a concatenated upload cannot be set"); 31 | } 32 | } 33 | 34 | @Override 35 | public boolean supports(HttpMethod method) { 36 | return HttpMethod.POST.equals(method); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/concatenation/validation/PartialUploadsExistValidator.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.concatenation.validation; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import java.io.IOException; 5 | import me.desair.tus.server.HttpHeader; 6 | import me.desair.tus.server.HttpMethod; 7 | import me.desair.tus.server.RequestValidator; 8 | import me.desair.tus.server.exception.InvalidPartialUploadIdException; 9 | import me.desair.tus.server.exception.TusException; 10 | import me.desair.tus.server.upload.UploadInfo; 11 | import me.desair.tus.server.upload.UploadStorageService; 12 | import me.desair.tus.server.util.Utils; 13 | import org.apache.commons.lang3.StringUtils; 14 | 15 | /** Validate that the IDs specified in the Upload-Concat header map to an existing upload */ 16 | public class PartialUploadsExistValidator implements RequestValidator { 17 | 18 | @Override 19 | public void validate( 20 | HttpMethod method, 21 | HttpServletRequest request, 22 | UploadStorageService uploadStorageService, 23 | String ownerKey) 24 | throws IOException, TusException { 25 | 26 | String uploadConcatValue = request.getHeader(HttpHeader.UPLOAD_CONCAT); 27 | 28 | if (StringUtils.startsWithIgnoreCase(uploadConcatValue, "final")) { 29 | 30 | for (String uploadUri : Utils.parseConcatenationIDsFromHeader(uploadConcatValue)) { 31 | 32 | UploadInfo uploadInfo = uploadStorageService.getUploadInfo(uploadUri, ownerKey); 33 | if (uploadInfo == null) { 34 | throw new InvalidPartialUploadIdException( 35 | "The URI " 36 | + uploadUri 37 | + " in Upload-Concat header does not match an existing upload"); 38 | } 39 | } 40 | } 41 | } 42 | 43 | @Override 44 | public boolean supports(HttpMethod method) { 45 | return HttpMethod.POST.equals(method); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/concatenation/validation/PatchFinalUploadValidator.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.concatenation.validation; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import java.io.IOException; 5 | import me.desair.tus.server.HttpMethod; 6 | import me.desair.tus.server.RequestValidator; 7 | import me.desair.tus.server.exception.PatchOnFinalUploadNotAllowedException; 8 | import me.desair.tus.server.exception.TusException; 9 | import me.desair.tus.server.upload.UploadInfo; 10 | import me.desair.tus.server.upload.UploadStorageService; 11 | import me.desair.tus.server.upload.UploadType; 12 | 13 | /** 14 | * The Server MUST respond with the 403 Forbidden status to PATCH requests against a upload URL and 15 | * MUST NOT modify the or its partial uploads. 16 | */ 17 | public class PatchFinalUploadValidator implements RequestValidator { 18 | 19 | @Override 20 | public void validate( 21 | HttpMethod method, 22 | HttpServletRequest request, 23 | UploadStorageService uploadStorageService, 24 | String ownerKey) 25 | throws IOException, TusException { 26 | 27 | UploadInfo uploadInfo = uploadStorageService.getUploadInfo(request.getRequestURI(), ownerKey); 28 | 29 | if (uploadInfo != null && UploadType.CONCATENATED.equals(uploadInfo.getUploadType())) { 30 | throw new PatchOnFinalUploadNotAllowedException( 31 | "You cannot send a PATCH request for a " + "concatenated upload URI"); 32 | } 33 | } 34 | 35 | @Override 36 | public boolean supports(HttpMethod method) { 37 | return HttpMethod.PATCH.equals(method); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/core/CoreDefaultResponseHeadersHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.core; 2 | 3 | import me.desair.tus.server.HttpHeader; 4 | import me.desair.tus.server.HttpMethod; 5 | import me.desair.tus.server.RequestHandler; 6 | import me.desair.tus.server.TusFileUploadService; 7 | import me.desair.tus.server.upload.UploadStorageService; 8 | import me.desair.tus.server.util.TusServletRequest; 9 | import me.desair.tus.server.util.TusServletResponse; 10 | 11 | /** 12 | * The Tus-Resumable header MUST be included in every request and response except for OPTIONS 13 | * requests. The value MUST be the version of the protocol used by the Client or the Server. 14 | */ 15 | public class CoreDefaultResponseHeadersHandler implements RequestHandler { 16 | 17 | @Override 18 | public boolean supports(HttpMethod method) { 19 | return true; 20 | } 21 | 22 | @Override 23 | public void process( 24 | HttpMethod method, 25 | TusServletRequest servletRequest, 26 | TusServletResponse servletResponse, 27 | UploadStorageService uploadStorageService, 28 | String ownerKey) { 29 | 30 | // Always set Tus-Resumable header 31 | servletResponse.setHeader(HttpHeader.TUS_RESUMABLE, TusFileUploadService.TUS_API_VERSION); 32 | // By default, set the Content-Length to 0 33 | servletResponse.setHeader(HttpHeader.CONTENT_LENGTH, "0"); 34 | } 35 | 36 | @Override 37 | public boolean isErrorHandler() { 38 | return true; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/core/CoreHeadRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.core; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | import java.io.IOException; 5 | import java.util.Objects; 6 | import me.desair.tus.server.HttpHeader; 7 | import me.desair.tus.server.HttpMethod; 8 | import me.desair.tus.server.upload.UploadInfo; 9 | import me.desair.tus.server.upload.UploadStorageService; 10 | import me.desair.tus.server.upload.UploadType; 11 | import me.desair.tus.server.util.AbstractRequestHandler; 12 | import me.desair.tus.server.util.TusServletRequest; 13 | import me.desair.tus.server.util.TusServletResponse; 14 | 15 | /** 16 | * A HEAD request is used to determine the offset at which the upload should be continued.
17 | * The Server MUST always include the Upload-Offset header in the response for a HEAD request, even 18 | * if the offset is 0, or the upload is already considered completed. If the size of the upload is 19 | * known, the Server MUST include the Upload-Length header in the response.
20 | * The Server MUST prevent the client and/or proxies from caching the response by adding the 21 | * Cache-Control: no-store header to the response. 22 | */ 23 | public class CoreHeadRequestHandler extends AbstractRequestHandler { 24 | 25 | @Override 26 | public boolean supports(HttpMethod method) { 27 | return HttpMethod.HEAD.equals(method); 28 | } 29 | 30 | @Override 31 | public void process( 32 | HttpMethod method, 33 | TusServletRequest servletRequest, 34 | TusServletResponse servletResponse, 35 | UploadStorageService uploadStorageService, 36 | String ownerKey) 37 | throws IOException { 38 | 39 | UploadInfo uploadInfo = 40 | uploadStorageService.getUploadInfo(servletRequest.getRequestURI(), ownerKey); 41 | 42 | if (!UploadType.CONCATENATED.equals(uploadInfo.getUploadType())) { 43 | 44 | if (uploadInfo.hasLength()) { 45 | servletResponse.setHeader( 46 | HttpHeader.UPLOAD_LENGTH, Objects.toString(uploadInfo.getLength())); 47 | } 48 | servletResponse.setHeader(HttpHeader.UPLOAD_OFFSET, Objects.toString(uploadInfo.getOffset())); 49 | } 50 | 51 | servletResponse.setHeader(HttpHeader.CACHE_CONTROL, "no-store"); 52 | 53 | servletResponse.setStatus(HttpServletResponse.SC_NO_CONTENT); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/core/CoreOptionsRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.core; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | import java.util.Objects; 5 | import me.desair.tus.server.HttpHeader; 6 | import me.desair.tus.server.HttpMethod; 7 | import me.desair.tus.server.TusFileUploadService; 8 | import me.desair.tus.server.upload.UploadStorageService; 9 | import me.desair.tus.server.util.AbstractRequestHandler; 10 | import me.desair.tus.server.util.TusServletRequest; 11 | import me.desair.tus.server.util.TusServletResponse; 12 | 13 | /** 14 | * An OPTIONS request MAY be used to gather information about the Server’s current configuration. A 15 | * successful response indicated by the 204 No Content or 200 OK status MUST contain the Tus-Version 16 | * header. It MAY include the Tus-Extension and Tus-Max-Size headers. 17 | */ 18 | public class CoreOptionsRequestHandler extends AbstractRequestHandler { 19 | 20 | @Override 21 | public boolean supports(HttpMethod method) { 22 | return HttpMethod.OPTIONS.equals(method); 23 | } 24 | 25 | @Override 26 | public void process( 27 | HttpMethod method, 28 | TusServletRequest servletRequest, 29 | TusServletResponse servletResponse, 30 | UploadStorageService uploadStorageService, 31 | String ownerKey) { 32 | 33 | if (uploadStorageService.getMaxUploadSize() > 0) { 34 | servletResponse.setHeader( 35 | HttpHeader.TUS_MAX_SIZE, Objects.toString(uploadStorageService.getMaxUploadSize())); 36 | } 37 | 38 | servletResponse.setHeader(HttpHeader.TUS_VERSION, TusFileUploadService.TUS_API_VERSION); 39 | 40 | servletResponse.setStatus(HttpServletResponse.SC_NO_CONTENT); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/core/CorePatchRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.core; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | import java.io.IOException; 5 | import java.util.Objects; 6 | import me.desair.tus.server.HttpHeader; 7 | import me.desair.tus.server.HttpMethod; 8 | import me.desair.tus.server.exception.TusException; 9 | import me.desair.tus.server.exception.UploadNotFoundException; 10 | import me.desair.tus.server.upload.UploadInfo; 11 | import me.desair.tus.server.upload.UploadStorageService; 12 | import me.desair.tus.server.util.AbstractRequestHandler; 13 | import me.desair.tus.server.util.TusServletRequest; 14 | import me.desair.tus.server.util.TusServletResponse; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | 18 | /** 19 | * The Server SHOULD accept PATCH requests against any upload URL and apply the bytes contained in 20 | * the message at the given offset specified by the Upload-Offset header.
21 | * The Server MUST acknowledge successful PATCH requests with the 204 No Content status. It MUST 22 | * include the Upload-Offset header containing the new offset. The new offset MUST be the sum of the 23 | * offset before the PATCH request and the number of bytes received and processed or stored during 24 | * the current PATCH request. 25 | */ 26 | public class CorePatchRequestHandler extends AbstractRequestHandler { 27 | 28 | private static final Logger log = LoggerFactory.getLogger(CorePatchRequestHandler.class); 29 | 30 | @Override 31 | public boolean supports(HttpMethod method) { 32 | return HttpMethod.PATCH.equals(method); 33 | } 34 | 35 | @Override 36 | public void process( 37 | HttpMethod method, 38 | TusServletRequest servletRequest, 39 | TusServletResponse servletResponse, 40 | UploadStorageService uploadStorageService, 41 | String ownerKey) 42 | throws IOException, TusException { 43 | 44 | boolean found = true; 45 | UploadInfo uploadInfo = 46 | uploadStorageService.getUploadInfo(servletRequest.getRequestURI(), ownerKey); 47 | 48 | if (uploadInfo == null) { 49 | found = false; 50 | } else if (uploadInfo.isUploadInProgress()) { 51 | try { 52 | uploadInfo = 53 | uploadStorageService.append(uploadInfo, servletRequest.getContentInputStream()); 54 | } catch (UploadNotFoundException e) { 55 | found = false; 56 | } 57 | } 58 | 59 | if (found) { 60 | servletResponse.setHeader(HttpHeader.UPLOAD_OFFSET, Objects.toString(uploadInfo.getOffset())); 61 | servletResponse.setStatus(HttpServletResponse.SC_NO_CONTENT); 62 | 63 | if (!uploadInfo.isUploadInProgress()) { 64 | log.info( 65 | "Upload with ID {} at location {} finished successfully", 66 | uploadInfo.getId(), 67 | servletRequest.getRequestURI()); 68 | } 69 | } else { 70 | log.error( 71 | "The patch request handler could not find the upload for URL {0}. " 72 | + "This means something is really wrong the request validators!", 73 | servletRequest.getRequestURI()); 74 | servletResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/core/CoreProtocol.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.core; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collection; 5 | import java.util.List; 6 | import me.desair.tus.server.HttpMethod; 7 | import me.desair.tus.server.RequestHandler; 8 | import me.desair.tus.server.RequestValidator; 9 | import me.desair.tus.server.core.validation.ContentLengthValidator; 10 | import me.desair.tus.server.core.validation.ContentTypeValidator; 11 | import me.desair.tus.server.core.validation.HttpMethodValidator; 12 | import me.desair.tus.server.core.validation.IdExistsValidator; 13 | import me.desair.tus.server.core.validation.TusResumableValidator; 14 | import me.desair.tus.server.core.validation.UploadOffsetValidator; 15 | import me.desair.tus.server.util.AbstractTusExtension; 16 | 17 | /** 18 | * The core protocol describes how to resume an interrupted upload. It assumes that you already have 19 | * a URL for the upload, usually created via the Creation extension. All Clients and Servers MUST 20 | * implement the core protocol. 21 | */ 22 | public class CoreProtocol extends AbstractTusExtension { 23 | 24 | @Override 25 | public String getName() { 26 | return "core"; 27 | } 28 | 29 | @Override 30 | public Collection getMinimalSupportedHttpMethods() { 31 | return Arrays.asList(HttpMethod.OPTIONS, HttpMethod.HEAD, HttpMethod.PATCH); 32 | } 33 | 34 | @Override 35 | protected void initValidators(List validators) { 36 | validators.add(new HttpMethodValidator()); 37 | validators.add(new TusResumableValidator()); 38 | validators.add(new IdExistsValidator()); 39 | validators.add(new ContentTypeValidator()); 40 | validators.add(new UploadOffsetValidator()); 41 | validators.add(new ContentLengthValidator()); 42 | } 43 | 44 | @Override 45 | protected void initRequestHandlers(List requestHandlers) { 46 | requestHandlers.add(new CoreDefaultResponseHeadersHandler()); 47 | requestHandlers.add(new CoreHeadRequestHandler()); 48 | requestHandlers.add(new CorePatchRequestHandler()); 49 | requestHandlers.add(new CoreOptionsRequestHandler()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/core/validation/ContentLengthValidator.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.core.validation; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import java.io.IOException; 5 | import me.desair.tus.server.HttpHeader; 6 | import me.desair.tus.server.HttpMethod; 7 | import me.desair.tus.server.RequestValidator; 8 | import me.desair.tus.server.exception.InvalidContentLengthException; 9 | import me.desair.tus.server.exception.TusException; 10 | import me.desair.tus.server.upload.UploadInfo; 11 | import me.desair.tus.server.upload.UploadStorageService; 12 | import me.desair.tus.server.util.Utils; 13 | 14 | /** 15 | * Validate that the given upload length in combination with the bytes we already received, does not 16 | * exceed the declared initial length on upload creation. 17 | */ 18 | public class ContentLengthValidator implements RequestValidator { 19 | 20 | @Override 21 | public void validate( 22 | HttpMethod method, 23 | HttpServletRequest request, 24 | UploadStorageService uploadStorageService, 25 | String ownerKey) 26 | throws TusException, IOException { 27 | 28 | Long contentLength = Utils.getLongHeader(request, HttpHeader.CONTENT_LENGTH); 29 | 30 | UploadInfo uploadInfo = uploadStorageService.getUploadInfo(request.getRequestURI(), ownerKey); 31 | 32 | if (contentLength != null 33 | && uploadInfo != null 34 | && uploadInfo.hasLength() 35 | && (uploadInfo.getOffset() + contentLength > uploadInfo.getLength())) { 36 | 37 | throw new InvalidContentLengthException( 38 | "The " 39 | + HttpHeader.CONTENT_LENGTH 40 | + " value " 41 | + contentLength 42 | + " in combination with the current offset " 43 | + uploadInfo.getOffset() 44 | + " exceeds the declared upload length " 45 | + uploadInfo.getLength()); 46 | } 47 | } 48 | 49 | @Override 50 | public boolean supports(HttpMethod method) { 51 | return HttpMethod.PATCH.equals(method); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/core/validation/ContentTypeValidator.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.core.validation; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import me.desair.tus.server.HttpHeader; 5 | import me.desair.tus.server.HttpMethod; 6 | import me.desair.tus.server.RequestValidator; 7 | import me.desair.tus.server.exception.InvalidContentTypeException; 8 | import me.desair.tus.server.exception.TusException; 9 | import me.desair.tus.server.upload.UploadStorageService; 10 | import me.desair.tus.server.util.Utils; 11 | 12 | /** All PATCH requests MUST use Content-Type: application/offset+octet-stream. */ 13 | public class ContentTypeValidator implements RequestValidator { 14 | 15 | static final String APPLICATION_OFFSET_OCTET_STREAM = "application/offset+octet-stream"; 16 | 17 | @Override 18 | public void validate( 19 | HttpMethod method, 20 | HttpServletRequest request, 21 | UploadStorageService uploadStorageService, 22 | String ownerKey) 23 | throws TusException { 24 | 25 | String contentType = Utils.getHeader(request, HttpHeader.CONTENT_TYPE); 26 | if (!APPLICATION_OFFSET_OCTET_STREAM.equals(contentType)) { 27 | throw new InvalidContentTypeException( 28 | "The " 29 | + HttpHeader.CONTENT_TYPE 30 | + " header must contain value " 31 | + APPLICATION_OFFSET_OCTET_STREAM); 32 | } 33 | } 34 | 35 | @Override 36 | public boolean supports(HttpMethod method) { 37 | return HttpMethod.PATCH.equals(method); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/core/validation/HttpMethodValidator.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.core.validation; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import me.desair.tus.server.HttpMethod; 5 | import me.desair.tus.server.RequestValidator; 6 | import me.desair.tus.server.exception.TusException; 7 | import me.desair.tus.server.exception.UnsupportedMethodException; 8 | import me.desair.tus.server.upload.UploadStorageService; 9 | 10 | /** Class to validate if the current HTTP method is valid */ 11 | public class HttpMethodValidator implements RequestValidator { 12 | 13 | @Override 14 | public void validate( 15 | HttpMethod method, 16 | HttpServletRequest request, 17 | UploadStorageService uploadStorageService, 18 | String ownerKey) 19 | throws TusException { 20 | 21 | if (method == null) { 22 | throw new UnsupportedMethodException( 23 | "The HTTP method " + request.getMethod() + " is not supported"); 24 | } 25 | } 26 | 27 | @Override 28 | public boolean supports(HttpMethod method) { 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/core/validation/IdExistsValidator.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.core.validation; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import java.io.IOException; 5 | import me.desair.tus.server.HttpMethod; 6 | import me.desair.tus.server.RequestValidator; 7 | import me.desair.tus.server.exception.TusException; 8 | import me.desair.tus.server.exception.UploadNotFoundException; 9 | import me.desair.tus.server.upload.UploadStorageService; 10 | 11 | /** 12 | * If the resource is not found, the Server SHOULD return either the 404 Not Found, 410 Gone or 403 13 | * Forbidden status without the Upload-Offset header. 14 | */ 15 | public class IdExistsValidator implements RequestValidator { 16 | 17 | @Override 18 | public void validate( 19 | HttpMethod method, 20 | HttpServletRequest request, 21 | UploadStorageService uploadStorageService, 22 | String ownerKey) 23 | throws TusException, IOException { 24 | 25 | if (uploadStorageService.getUploadInfo(request.getRequestURI(), ownerKey) == null) { 26 | throw new UploadNotFoundException( 27 | "The upload for path " 28 | + request.getRequestURI() 29 | + " and owner " 30 | + ownerKey 31 | + " was not found."); 32 | } 33 | } 34 | 35 | @Override 36 | public boolean supports(HttpMethod method) { 37 | return method != null 38 | && (HttpMethod.HEAD.equals(method) 39 | || HttpMethod.PATCH.equals(method) 40 | || HttpMethod.DELETE.equals(method) 41 | || HttpMethod.GET.equals(method)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/core/validation/TusResumableValidator.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.core.validation; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import me.desair.tus.server.HttpHeader; 5 | import me.desair.tus.server.HttpMethod; 6 | import me.desair.tus.server.RequestValidator; 7 | import me.desair.tus.server.TusFileUploadService; 8 | import me.desair.tus.server.exception.InvalidTusResumableException; 9 | import me.desair.tus.server.exception.TusException; 10 | import me.desair.tus.server.upload.UploadStorageService; 11 | import me.desair.tus.server.util.Utils; 12 | import org.apache.commons.lang3.StringUtils; 13 | 14 | /** 15 | * Class that will validate if the tus version in the request corresponds to our implementation 16 | * version
17 | * The Tus-Resumable header MUST be included in every request and response except for OPTIONS 18 | * requests. The value MUST be the version of the protocol used by the Client or the Server. If the 19 | * the version specified by the Client is not supported by the Server, it MUST respond with the 412 20 | * Precondition Failed status and MUST include the Tus-Version header into the response. In 21 | * addition, the Server MUST NOT process the request.
22 | * (https://tus.io/protocols/resumable-upload.html#tus-resumable) 23 | */ 24 | public class TusResumableValidator implements RequestValidator { 25 | 26 | /** Validate tus protocol version. */ 27 | public void validate( 28 | HttpMethod method, 29 | HttpServletRequest request, 30 | UploadStorageService uploadStorageService, 31 | String ownerKey) 32 | throws TusException { 33 | 34 | String requestVersion = Utils.getHeader(request, HttpHeader.TUS_RESUMABLE); 35 | if (!StringUtils.equals(requestVersion, TusFileUploadService.TUS_API_VERSION)) { 36 | throw new InvalidTusResumableException( 37 | "This server does not support tus protocol version " + requestVersion); 38 | } 39 | } 40 | 41 | @Override 42 | public boolean supports(HttpMethod method) { 43 | return !HttpMethod.OPTIONS.equals(method) && !HttpMethod.GET.equals(method); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/core/validation/UploadOffsetValidator.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.core.validation; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import java.io.IOException; 5 | import java.util.Objects; 6 | import me.desair.tus.server.HttpHeader; 7 | import me.desair.tus.server.HttpMethod; 8 | import me.desair.tus.server.RequestValidator; 9 | import me.desair.tus.server.exception.TusException; 10 | import me.desair.tus.server.exception.UploadOffsetMismatchException; 11 | import me.desair.tus.server.upload.UploadInfo; 12 | import me.desair.tus.server.upload.UploadStorageService; 13 | import me.desair.tus.server.util.Utils; 14 | import org.apache.commons.lang3.StringUtils; 15 | 16 | /** 17 | * The Upload-Offset header’s value MUST be equal to the current offset of the resource. If the 18 | * offsets do not match, the Server MUST respond with the 409 Conflict status without modifying the 19 | * upload resource. 20 | */ 21 | public class UploadOffsetValidator implements RequestValidator { 22 | 23 | @Override 24 | public void validate( 25 | HttpMethod method, 26 | HttpServletRequest request, 27 | UploadStorageService uploadStorageService, 28 | String ownerKey) 29 | throws IOException, TusException { 30 | 31 | String uploadOffset = Utils.getHeader(request, HttpHeader.UPLOAD_OFFSET); 32 | 33 | UploadInfo uploadInfo = uploadStorageService.getUploadInfo(request.getRequestURI(), ownerKey); 34 | 35 | if (uploadInfo != null) { 36 | String expectedOffset = Objects.toString(uploadInfo.getOffset()); 37 | if (!StringUtils.equals(expectedOffset, uploadOffset)) { 38 | throw new UploadOffsetMismatchException( 39 | "The Upload-Offset was " 40 | + StringUtils.trimToNull(uploadOffset) 41 | + " but expected " 42 | + expectedOffset); 43 | } 44 | } 45 | } 46 | 47 | @Override 48 | public boolean supports(HttpMethod method) { 49 | return HttpMethod.PATCH.equals(method); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/creation/CreationExtension.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.creation; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collection; 5 | import java.util.List; 6 | import me.desair.tus.server.HttpMethod; 7 | import me.desair.tus.server.RequestHandler; 8 | import me.desair.tus.server.RequestValidator; 9 | import me.desair.tus.server.creation.validation.PostEmptyRequestValidator; 10 | import me.desair.tus.server.creation.validation.PostUriValidator; 11 | import me.desair.tus.server.creation.validation.UploadDeferLengthValidator; 12 | import me.desair.tus.server.creation.validation.UploadLengthValidator; 13 | import me.desair.tus.server.util.AbstractTusExtension; 14 | 15 | /** 16 | * The Client and the Server SHOULD implement the upload creation extension. If the Server supports 17 | * this extension. 18 | */ 19 | public class CreationExtension extends AbstractTusExtension { 20 | 21 | @Override 22 | public String getName() { 23 | return "creation"; 24 | } 25 | 26 | @Override 27 | public Collection getMinimalSupportedHttpMethods() { 28 | return Arrays.asList(HttpMethod.OPTIONS, HttpMethod.HEAD, HttpMethod.PATCH, HttpMethod.POST); 29 | } 30 | 31 | @Override 32 | protected void initValidators(List requestValidators) { 33 | requestValidators.add(new PostUriValidator()); 34 | requestValidators.add(new PostEmptyRequestValidator()); 35 | requestValidators.add(new UploadDeferLengthValidator()); 36 | requestValidators.add(new UploadLengthValidator()); 37 | } 38 | 39 | @Override 40 | protected void initRequestHandlers(List requestHandlers) { 41 | requestHandlers.add(new CreationHeadRequestHandler()); 42 | requestHandlers.add(new CreationPatchRequestHandler()); 43 | requestHandlers.add(new CreationPostRequestHandler()); 44 | requestHandlers.add(new CreationOptionsRequestHandler()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/creation/CreationHeadRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.creation; 2 | 3 | import java.io.IOException; 4 | import me.desair.tus.server.HttpHeader; 5 | import me.desair.tus.server.HttpMethod; 6 | import me.desair.tus.server.upload.UploadInfo; 7 | import me.desair.tus.server.upload.UploadStorageService; 8 | import me.desair.tus.server.upload.UploadType; 9 | import me.desair.tus.server.util.AbstractRequestHandler; 10 | import me.desair.tus.server.util.TusServletRequest; 11 | import me.desair.tus.server.util.TusServletResponse; 12 | 13 | /** 14 | * A HEAD request can be used to retrieve the metadata that was supplied at creation.
15 | * If an upload contains additional metadata, responses to HEAD requests MUST include the 16 | * Upload-Metadata header and its value as specified by the Client during the creation.
17 | * As long as the length of the upload is not known, the Server MUST set Upload-Defer-Length: 1 in 18 | * all responses to HEAD requests. 19 | */ 20 | public class CreationHeadRequestHandler extends AbstractRequestHandler { 21 | 22 | @Override 23 | public boolean supports(HttpMethod method) { 24 | return HttpMethod.HEAD.equals(method); 25 | } 26 | 27 | @Override 28 | public void process( 29 | HttpMethod method, 30 | TusServletRequest servletRequest, 31 | TusServletResponse servletResponse, 32 | UploadStorageService uploadStorageService, 33 | String ownerKey) 34 | throws IOException { 35 | 36 | UploadInfo uploadInfo = 37 | uploadStorageService.getUploadInfo(servletRequest.getRequestURI(), ownerKey); 38 | 39 | if (uploadInfo.hasMetadata()) { 40 | servletResponse.setHeader(HttpHeader.UPLOAD_METADATA, uploadInfo.getEncodedMetadata()); 41 | } 42 | 43 | if (!uploadInfo.hasLength() && !UploadType.CONCATENATED.equals(uploadInfo.getUploadType())) { 44 | servletResponse.setHeader(HttpHeader.UPLOAD_DEFER_LENGTH, "1"); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/creation/CreationOptionsRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.creation; 2 | 3 | import me.desair.tus.server.util.AbstractExtensionRequestHandler; 4 | 5 | /** 6 | * The Client and the Server SHOULD implement the upload creation extension. If the Server supports 7 | * this extension, it MUST add creation to the Tus-Extension header.
8 | * If the Server supports deferring length, it MUST add creation-defer-length to the Tus-Extension 9 | * header. 10 | */ 11 | public class CreationOptionsRequestHandler extends AbstractExtensionRequestHandler { 12 | 13 | @Override 14 | protected void appendExtensions(StringBuilder extensionBuilder) { 15 | addExtension(extensionBuilder, "creation"); 16 | addExtension(extensionBuilder, "creation-defer-length"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/creation/CreationPatchRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.creation; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | import java.io.IOException; 5 | import me.desair.tus.server.HttpHeader; 6 | import me.desair.tus.server.HttpMethod; 7 | import me.desair.tus.server.exception.UploadNotFoundException; 8 | import me.desair.tus.server.upload.UploadInfo; 9 | import me.desair.tus.server.upload.UploadStorageService; 10 | import me.desair.tus.server.util.AbstractRequestHandler; 11 | import me.desair.tus.server.util.TusServletRequest; 12 | import me.desair.tus.server.util.TusServletResponse; 13 | import me.desair.tus.server.util.Utils; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | /** 18 | * Upload-Defer-Length: 1 if upload size is not known at the time. Once it is known the Client MUST 19 | * set the Upload-Length header in the next PATCH request. Once set the length MUST NOT be changed. 20 | */ 21 | public class CreationPatchRequestHandler extends AbstractRequestHandler { 22 | 23 | private static final Logger log = LoggerFactory.getLogger(CreationPatchRequestHandler.class); 24 | 25 | @Override 26 | public boolean supports(HttpMethod method) { 27 | return HttpMethod.PATCH.equals(method); 28 | } 29 | 30 | @Override 31 | public void process( 32 | HttpMethod method, 33 | TusServletRequest servletRequest, 34 | TusServletResponse servletResponse, 35 | UploadStorageService uploadStorageService, 36 | String ownerKey) 37 | throws IOException { 38 | 39 | UploadInfo uploadInfo = 40 | uploadStorageService.getUploadInfo(servletRequest.getRequestURI(), ownerKey); 41 | 42 | if (uploadInfo != null && !uploadInfo.hasLength()) { 43 | Long uploadLength = Utils.getLongHeader(servletRequest, HttpHeader.UPLOAD_LENGTH); 44 | if (uploadLength != null) { 45 | uploadInfo.setLength(uploadLength); 46 | try { 47 | uploadStorageService.update(uploadInfo); 48 | } catch (UploadNotFoundException e) { 49 | log.error( 50 | "The patch request handler could not find the upload for URL " 51 | + servletRequest.getRequestURI() 52 | + ". This means something is really wrong the request validators!", 53 | e); 54 | servletResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/creation/CreationPostRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.creation; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import java.io.IOException; 6 | import me.desair.tus.server.HttpHeader; 7 | import me.desair.tus.server.HttpMethod; 8 | import me.desair.tus.server.upload.UploadInfo; 9 | import me.desair.tus.server.upload.UploadStorageService; 10 | import me.desair.tus.server.util.AbstractRequestHandler; 11 | import me.desair.tus.server.util.TusServletRequest; 12 | import me.desair.tus.server.util.TusServletResponse; 13 | import me.desair.tus.server.util.Utils; 14 | import org.apache.commons.lang3.StringUtils; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | 18 | /** 19 | * The Server MUST acknowledge a successful upload creation with the 201 Created status. The Server 20 | * MUST set the Location header to the URL of the created resource. This URL MAY be absolute or 21 | * relative. 22 | */ 23 | public class CreationPostRequestHandler extends AbstractRequestHandler { 24 | 25 | private static final Logger log = LoggerFactory.getLogger(CreationPostRequestHandler.class); 26 | 27 | @Override 28 | public boolean supports(HttpMethod method) { 29 | return HttpMethod.POST.equals(method); 30 | } 31 | 32 | @Override 33 | public void process( 34 | HttpMethod method, 35 | TusServletRequest servletRequest, 36 | TusServletResponse servletResponse, 37 | UploadStorageService uploadStorageService, 38 | String ownerKey) 39 | throws IOException { 40 | 41 | UploadInfo info = buildUploadInfo(servletRequest); 42 | info = uploadStorageService.create(info, ownerKey); 43 | 44 | // We've already validated that the current request URL matches our upload URL so we can 45 | // safely 46 | // use it. 47 | String uploadUri = servletRequest.getRequestURI(); 48 | 49 | // It's important to return relative UPLOAD URLs in the Location header in order to support 50 | // HTTPS proxies 51 | // that sit in front of the web app 52 | String url = uploadUri + (StringUtils.endsWith(uploadUri, "/") ? "" : "/") + info.getId(); 53 | servletResponse.setHeader(HttpHeader.LOCATION, url); 54 | servletResponse.setStatus(HttpServletResponse.SC_CREATED); 55 | 56 | log.info( 57 | "Created upload with ID {} at {} for ip address {} with location {}", 58 | info.getId(), 59 | info.getCreationTimestamp(), 60 | info.getCreatorIpAddresses(), 61 | url); 62 | } 63 | 64 | private UploadInfo buildUploadInfo(HttpServletRequest servletRequest) { 65 | UploadInfo info = new UploadInfo(servletRequest); 66 | 67 | Long length = Utils.getLongHeader(servletRequest, HttpHeader.UPLOAD_LENGTH); 68 | if (length != null) { 69 | info.setLength(length); 70 | } 71 | 72 | String metadata = Utils.getHeader(servletRequest, HttpHeader.UPLOAD_METADATA); 73 | if (StringUtils.isNotBlank(metadata)) { 74 | info.setEncodedMetadata(metadata); 75 | } 76 | 77 | return info; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/creation/validation/PostEmptyRequestValidator.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.creation.validation; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import me.desair.tus.server.HttpHeader; 5 | import me.desair.tus.server.HttpMethod; 6 | import me.desair.tus.server.RequestValidator; 7 | import me.desair.tus.server.exception.InvalidContentLengthException; 8 | import me.desair.tus.server.exception.TusException; 9 | import me.desair.tus.server.upload.UploadStorageService; 10 | import me.desair.tus.server.util.Utils; 11 | 12 | /** An empty POST request is used to create a new upload resource. */ 13 | public class PostEmptyRequestValidator implements RequestValidator { 14 | 15 | @Override 16 | public void validate( 17 | HttpMethod method, 18 | HttpServletRequest request, 19 | UploadStorageService uploadStorageService, 20 | String ownerKey) 21 | throws TusException { 22 | 23 | Long contentLength = Utils.getLongHeader(request, HttpHeader.CONTENT_LENGTH); 24 | if (contentLength != null && contentLength > 0) { 25 | throw new InvalidContentLengthException( 26 | "A POST request should have a Content-Length header with value " + "0 and no content"); 27 | } 28 | } 29 | 30 | @Override 31 | public boolean supports(HttpMethod method) { 32 | return HttpMethod.POST.equals(method); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/creation/validation/PostUriValidator.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.creation.validation; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import java.util.regex.Matcher; 5 | import java.util.regex.Pattern; 6 | import me.desair.tus.server.HttpMethod; 7 | import me.desair.tus.server.RequestValidator; 8 | import me.desair.tus.server.exception.PostOnInvalidRequestURIException; 9 | import me.desair.tus.server.exception.TusException; 10 | import me.desair.tus.server.upload.UploadStorageService; 11 | 12 | /** 13 | * The Client MUST send a POST request against a known upload creation URL to request a new upload 14 | * resource. 15 | */ 16 | public class PostUriValidator implements RequestValidator { 17 | 18 | private Pattern uploadUriPattern = null; 19 | 20 | @Override 21 | public void validate( 22 | HttpMethod method, 23 | HttpServletRequest request, 24 | UploadStorageService uploadStorageService, 25 | String ownerKey) 26 | throws TusException { 27 | 28 | Matcher uploadUriMatcher = 29 | getUploadUriPattern(uploadStorageService).matcher(request.getRequestURI()); 30 | 31 | if (!uploadUriMatcher.matches()) { 32 | throw new PostOnInvalidRequestURIException( 33 | "POST requests have to be sent to '" + uploadStorageService.getUploadUri() + "'. "); 34 | } 35 | } 36 | 37 | @Override 38 | public boolean supports(HttpMethod method) { 39 | return HttpMethod.POST.equals(method); 40 | } 41 | 42 | private Pattern getUploadUriPattern(UploadStorageService uploadStorageService) { 43 | if (uploadUriPattern == null) { 44 | // A POST request should match the full URI 45 | uploadUriPattern = Pattern.compile("^" + uploadStorageService.getUploadUri() + "$"); 46 | } 47 | return uploadUriPattern; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/creation/validation/UploadDeferLengthValidator.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.creation.validation; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import me.desair.tus.server.HttpHeader; 5 | import me.desair.tus.server.HttpMethod; 6 | import me.desair.tus.server.RequestValidator; 7 | import me.desair.tus.server.exception.InvalidUploadLengthException; 8 | import me.desair.tus.server.exception.TusException; 9 | import me.desair.tus.server.upload.UploadStorageService; 10 | import me.desair.tus.server.util.Utils; 11 | import org.apache.commons.lang3.StringUtils; 12 | 13 | /** 14 | * The request MUST include one of the following headers: a) Upload-Length to indicate the size of 15 | * an entire upload in bytes. b) Upload-Defer-Length: 1 if upload size is not known at the time. 16 | */ 17 | public class UploadDeferLengthValidator implements RequestValidator { 18 | 19 | @Override 20 | public void validate( 21 | HttpMethod method, 22 | HttpServletRequest request, 23 | UploadStorageService uploadStorageService, 24 | String ownerKey) 25 | throws TusException { 26 | 27 | boolean uploadLength = false; 28 | boolean deferredLength = false; 29 | boolean concatenatedUpload = false; 30 | 31 | if (StringUtils.isNumeric(Utils.getHeader(request, HttpHeader.UPLOAD_LENGTH))) { 32 | uploadLength = true; 33 | } 34 | 35 | if (Utils.getHeader(request, HttpHeader.UPLOAD_DEFER_LENGTH).equals("1")) { 36 | deferredLength = true; 37 | } 38 | 39 | String uploadConcatValue = request.getHeader(HttpHeader.UPLOAD_CONCAT); 40 | if (StringUtils.startsWithIgnoreCase(uploadConcatValue, "final")) { 41 | concatenatedUpload = true; 42 | } 43 | 44 | if (!concatenatedUpload && !uploadLength && !deferredLength) { 45 | throw new InvalidUploadLengthException( 46 | "No valid value was found in headers " 47 | + HttpHeader.UPLOAD_LENGTH 48 | + " and " 49 | + HttpHeader.UPLOAD_DEFER_LENGTH); 50 | } else if (uploadLength && deferredLength) { 51 | throw new InvalidUploadLengthException( 52 | "A POST request cannot contain both " 53 | + HttpHeader.UPLOAD_LENGTH 54 | + " and " 55 | + HttpHeader.UPLOAD_DEFER_LENGTH 56 | + " headers."); 57 | } 58 | } 59 | 60 | @Override 61 | public boolean supports(HttpMethod method) { 62 | return HttpMethod.POST.equals(method); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/creation/validation/UploadLengthValidator.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.creation.validation; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import me.desair.tus.server.HttpHeader; 5 | import me.desair.tus.server.HttpMethod; 6 | import me.desair.tus.server.RequestValidator; 7 | import me.desair.tus.server.exception.MaxUploadLengthExceededException; 8 | import me.desair.tus.server.exception.TusException; 9 | import me.desair.tus.server.upload.UploadStorageService; 10 | import me.desair.tus.server.util.Utils; 11 | 12 | /** 13 | * If the length of the upload exceeds the maximum, which MAY be specified using the Tus-Max-Size 14 | * header, the Server MUST respond with the 413 Request Entity Too Large status. 15 | */ 16 | public class UploadLengthValidator implements RequestValidator { 17 | 18 | @Override 19 | public void validate( 20 | HttpMethod method, 21 | HttpServletRequest request, 22 | UploadStorageService uploadStorageService, 23 | String ownerKey) 24 | throws TusException { 25 | 26 | Long uploadLength = Utils.getLongHeader(request, HttpHeader.UPLOAD_LENGTH); 27 | if (uploadLength != null 28 | && uploadStorageService.getMaxUploadSize() > 0 29 | && uploadLength > uploadStorageService.getMaxUploadSize()) { 30 | 31 | throw new MaxUploadLengthExceededException( 32 | "Upload requests can have a maximum size of " + uploadStorageService.getMaxUploadSize()); 33 | } 34 | } 35 | 36 | @Override 37 | public boolean supports(HttpMethod method) { 38 | return HttpMethod.POST.equals(method); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/download/DownloadExtension.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.download; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collection; 5 | import java.util.List; 6 | import me.desair.tus.server.HttpMethod; 7 | import me.desair.tus.server.RequestHandler; 8 | import me.desair.tus.server.RequestValidator; 9 | import me.desair.tus.server.util.AbstractTusExtension; 10 | 11 | /** 12 | * Some Tus clients also send GET request to retrieve the uploaded content. We consider this as an 13 | * unofficial extension. 14 | */ 15 | public class DownloadExtension extends AbstractTusExtension { 16 | 17 | @Override 18 | public String getName() { 19 | return "download"; 20 | } 21 | 22 | @Override 23 | public Collection getMinimalSupportedHttpMethods() { 24 | return Arrays.asList(HttpMethod.OPTIONS, HttpMethod.GET); 25 | } 26 | 27 | @Override 28 | protected void initValidators(List requestValidators) { 29 | // All validation is all read done by the Core protocol 30 | } 31 | 32 | @Override 33 | protected void initRequestHandlers(List requestHandlers) { 34 | requestHandlers.add(new DownloadGetRequestHandler()); 35 | requestHandlers.add(new DownloadOptionsRequestHandler()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/download/DownloadGetRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.download; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | import java.io.IOException; 5 | import java.net.URLEncoder; 6 | import java.nio.charset.StandardCharsets; 7 | import java.util.Objects; 8 | import me.desair.tus.server.HttpHeader; 9 | import me.desair.tus.server.HttpMethod; 10 | import me.desair.tus.server.exception.TusException; 11 | import me.desair.tus.server.exception.UploadInProgressException; 12 | import me.desair.tus.server.upload.UploadInfo; 13 | import me.desair.tus.server.upload.UploadStorageService; 14 | import me.desair.tus.server.util.AbstractRequestHandler; 15 | import me.desair.tus.server.util.TusServletRequest; 16 | import me.desair.tus.server.util.TusServletResponse; 17 | 18 | /** Send the uploaded bytes of finished uploads. */ 19 | public class DownloadGetRequestHandler extends AbstractRequestHandler { 20 | 21 | private static final String CONTENT_DISPOSITION_FORMAT = 22 | "attachment; filename=\"%s\"; filename*=UTF-8''%s"; 23 | 24 | @Override 25 | public boolean supports(HttpMethod method) { 26 | return HttpMethod.GET.equals(method); 27 | } 28 | 29 | @Override 30 | public void process( 31 | HttpMethod method, 32 | TusServletRequest servletRequest, 33 | TusServletResponse servletResponse, 34 | UploadStorageService uploadStorageService, 35 | String ownerKey) 36 | throws IOException, TusException { 37 | 38 | UploadInfo info = uploadStorageService.getUploadInfo(servletRequest.getRequestURI(), ownerKey); 39 | if (info == null || info.isUploadInProgress()) { 40 | throw new UploadInProgressException( 41 | "Upload " 42 | + servletRequest.getRequestURI() 43 | + " is still in progress " 44 | + "and cannot be downloaded yet"); 45 | } else { 46 | 47 | servletResponse.setHeader(HttpHeader.CONTENT_LENGTH, Objects.toString(info.getLength())); 48 | 49 | servletResponse.setHeader( 50 | HttpHeader.CONTENT_DISPOSITION, 51 | String.format( 52 | CONTENT_DISPOSITION_FORMAT, 53 | info.getFileName(), 54 | URLEncoder.encode(info.getFileName(), StandardCharsets.UTF_8.toString()) 55 | .replace("+", "%20"))); 56 | 57 | servletResponse.setHeader(HttpHeader.CONTENT_TYPE, info.getFileMimeType()); 58 | 59 | if (info.hasMetadata()) { 60 | servletResponse.setHeader(HttpHeader.UPLOAD_METADATA, info.getEncodedMetadata()); 61 | } 62 | 63 | uploadStorageService.copyUploadTo(info, servletResponse.getOutputStream()); 64 | } 65 | 66 | servletResponse.setStatus(HttpServletResponse.SC_OK); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/download/DownloadOptionsRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.download; 2 | 3 | import me.desair.tus.server.util.AbstractExtensionRequestHandler; 4 | 5 | /** Add our download extension the Tus-Extension header */ 6 | public class DownloadOptionsRequestHandler extends AbstractExtensionRequestHandler { 7 | 8 | @Override 9 | protected void appendExtensions(StringBuilder extensionBuilder) { 10 | addExtension(extensionBuilder, "download"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/exception/ChecksumAlgorithmNotSupportedException.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.exception; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | 5 | /** Exception thrown when the client sends a request for a checksum algorithm we do not support */ 6 | public class ChecksumAlgorithmNotSupportedException extends TusException { 7 | public ChecksumAlgorithmNotSupportedException(String message) { 8 | super(HttpServletResponse.SC_BAD_REQUEST, message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/exception/InvalidContentLengthException.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.exception; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | 5 | public class InvalidContentLengthException extends TusException { 6 | public InvalidContentLengthException(String message) { 7 | super(HttpServletResponse.SC_BAD_REQUEST, message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/exception/InvalidContentTypeException.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.exception; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | 5 | /** Exception thrown when the request has an invalid content type. */ 6 | public class InvalidContentTypeException extends TusException { 7 | public InvalidContentTypeException(String message) { 8 | super(HttpServletResponse.SC_NOT_ACCEPTABLE, message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/exception/InvalidPartialUploadIdException.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.exception; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | 5 | /** Exception thrown when the Upload-Concat header contains an ID which is not valid. */ 6 | public class InvalidPartialUploadIdException extends TusException { 7 | public InvalidPartialUploadIdException(String message) { 8 | super(HttpServletResponse.SC_PRECONDITION_FAILED, message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/exception/InvalidTusResumableException.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.exception; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | 5 | /** 6 | * Exception thrown when receiving a request with a tus protocol version we do not support
7 | * The Tus-Resumable header MUST be included in every request and response except for OPTIONS 8 | * requests. The value MUST be the version of the protocol used by the Client or the Server. If the 9 | * the version specified by the Client is not supported by the Server, it MUST respond with the 412 10 | * Precondition Failed status and MUST include the Tus-Version header into the response. In 11 | * addition, the Server MUST NOT process the request.
12 | * (https://tus.io/protocols/resumable-upload.html#tus-resumable) 13 | */ 14 | public class InvalidTusResumableException extends TusException { 15 | 16 | public InvalidTusResumableException(String message) { 17 | super(HttpServletResponse.SC_PRECONDITION_FAILED, message); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/exception/InvalidUploadLengthException.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.exception; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | 5 | /** Exception thrown when no valid Upload-Length or Upload-Defer-Length header is found */ 6 | public class InvalidUploadLengthException extends TusException { 7 | 8 | public InvalidUploadLengthException(String message) { 9 | super(HttpServletResponse.SC_BAD_REQUEST, message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/exception/InvalidUploadOffsetException.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.exception; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | 5 | public class InvalidUploadOffsetException extends TusException { 6 | public InvalidUploadOffsetException(String message) { 7 | super(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/exception/MaxUploadLengthExceededException.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.exception; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | 5 | /** Exception thrown when the given upload length exceeds or internally defined maximum */ 6 | public class MaxUploadLengthExceededException extends TusException { 7 | public MaxUploadLengthExceededException(String message) { 8 | super(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/exception/PatchOnFinalUploadNotAllowedException.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.exception; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | 5 | /** The Server MUST respond with the 403 Forbidden status to PATCH requests against a upload URL */ 6 | public class PatchOnFinalUploadNotAllowedException extends TusException { 7 | 8 | public PatchOnFinalUploadNotAllowedException(String message) { 9 | super(HttpServletResponse.SC_FORBIDDEN, message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/exception/PostOnInvalidRequestURIException.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.exception; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | 5 | /** Exception thrown when a POST request was received on an invalid URI */ 6 | public class PostOnInvalidRequestURIException extends TusException { 7 | 8 | public PostOnInvalidRequestURIException(String message) { 9 | super(HttpServletResponse.SC_METHOD_NOT_ALLOWED, message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/exception/TusException.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.exception; 2 | 3 | /** Super class for exception in the tus protocol */ 4 | public class TusException extends Exception { 5 | 6 | private int status; 7 | 8 | public TusException(int status, String message) { 9 | this(status, message, null); 10 | } 11 | 12 | public TusException(int status, String message, Throwable e) { 13 | super(message, e); 14 | this.status = status; 15 | } 16 | 17 | public int getStatus() { 18 | return status; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/exception/UnsupportedMethodException.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.exception; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | 5 | /** Exception thrown when we receive a HTTP request with a method name that we do not support */ 6 | public class UnsupportedMethodException extends TusException { 7 | public UnsupportedMethodException(String message) { 8 | super(HttpServletResponse.SC_METHOD_NOT_ALLOWED, message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/exception/UploadAlreadyLockedException.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.exception; 2 | 3 | public class UploadAlreadyLockedException extends TusException { 4 | public UploadAlreadyLockedException(String message) { 5 | // 423 is LOCKED (WebDAV rfc 4918) 6 | super(423, message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/exception/UploadChecksumMismatchException.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.exception; 2 | 3 | /** 4 | * Exception thrown when the client provided checksum does not match the checksum calculated by the 5 | * server 6 | */ 7 | public class UploadChecksumMismatchException extends TusException { 8 | public UploadChecksumMismatchException(String message) { 9 | super(460, message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/exception/UploadInProgressException.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.exception; 2 | 3 | /** 4 | * Exception thrown when accessing an upload that is still in progress and this is not supported by 5 | * the operation. 6 | */ 7 | public class UploadInProgressException extends TusException { 8 | /** Constructor. */ 9 | public UploadInProgressException(String message) { 10 | // 422 Unprocessable Entity 11 | // The request was well-formed but was unable to be followed due to semantic errors. 12 | super(422, message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/exception/UploadLengthNotAllowedOnConcatenationException.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.exception; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | 5 | /** Exception thrown when the Client includes the Upload-Length header in the upload creation. */ 6 | public class UploadLengthNotAllowedOnConcatenationException extends TusException { 7 | public UploadLengthNotAllowedOnConcatenationException(String message) { 8 | super(HttpServletResponse.SC_BAD_REQUEST, message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/exception/UploadNotFoundException.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.exception; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | 5 | /** 6 | * Exception thrown when the given upload ID was not found
7 | * If the resource is not found, the Server SHOULD return either the 404 Not Found, 410 Gone or 403 8 | * Forbidden status without the Upload-Offset header. 9 | */ 10 | public class UploadNotFoundException extends TusException { 11 | public UploadNotFoundException(String message) { 12 | super(HttpServletResponse.SC_NOT_FOUND, message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/exception/UploadOffsetMismatchException.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.exception; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | 5 | /** 6 | * If the offsets do not match, the Server MUST respond with the 409 Conflict status without 7 | * modifying the upload resource. 8 | */ 9 | public class UploadOffsetMismatchException extends TusException { 10 | public UploadOffsetMismatchException(String message) { 11 | super(HttpServletResponse.SC_CONFLICT, message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/expiration/ExpirationExtension.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.expiration; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collection; 5 | import java.util.List; 6 | import me.desair.tus.server.HttpMethod; 7 | import me.desair.tus.server.RequestHandler; 8 | import me.desair.tus.server.RequestValidator; 9 | import me.desair.tus.server.util.AbstractTusExtension; 10 | 11 | /** The Server MAY remove unfinished uploads once they expire. */ 12 | public class ExpirationExtension extends AbstractTusExtension { 13 | 14 | @Override 15 | public String getName() { 16 | return "expiration"; 17 | } 18 | 19 | @Override 20 | public Collection getMinimalSupportedHttpMethods() { 21 | return Arrays.asList(HttpMethod.OPTIONS, HttpMethod.PATCH, HttpMethod.POST); 22 | } 23 | 24 | @Override 25 | protected void initValidators(List requestValidators) { 26 | // No validators 27 | } 28 | 29 | @Override 30 | protected void initRequestHandlers(List requestHandlers) { 31 | requestHandlers.add(new ExpirationOptionsRequestHandler()); 32 | requestHandlers.add(new ExpirationRequestHandler()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/expiration/ExpirationOptionsRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.expiration; 2 | 3 | import me.desair.tus.server.util.AbstractExtensionRequestHandler; 4 | 5 | /** 6 | * The Server MAY remove unfinished uploads once they expire. In order to indicate this behavior to 7 | * the Client, the Server MUST add expiration to the Tus-Extension header. 8 | */ 9 | public class ExpirationOptionsRequestHandler extends AbstractExtensionRequestHandler { 10 | 11 | @Override 12 | protected void appendExtensions(StringBuilder extensionBuilder) { 13 | addExtension(extensionBuilder, "expiration"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/expiration/ExpirationRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.expiration; 2 | 3 | import java.io.IOException; 4 | import me.desair.tus.server.HttpHeader; 5 | import me.desair.tus.server.HttpMethod; 6 | import me.desair.tus.server.exception.TusException; 7 | import me.desair.tus.server.upload.UploadInfo; 8 | import me.desair.tus.server.upload.UploadStorageService; 9 | import me.desair.tus.server.util.AbstractRequestHandler; 10 | import me.desair.tus.server.util.TusServletRequest; 11 | import me.desair.tus.server.util.TusServletResponse; 12 | import org.apache.commons.lang3.StringUtils; 13 | 14 | /** 15 | * The Upload-Expires response header indicates the time after which the unfinished upload expires. 16 | * This header MUST be included in every PATCH response if the upload is going to expire. Its value 17 | * MAY change over time. If the expiration is known at the creation, the Upload-Expires header MUST 18 | * be included in the response to the initial POST request. Its value MAY change over time. The 19 | * value of the Upload-Expires header MUST be in RFC 7231 20 | * (https://tools.ietf.org/html/rfc7231#section-7.1.1.1) datetime format. 21 | */ 22 | public class ExpirationRequestHandler extends AbstractRequestHandler { 23 | 24 | @Override 25 | public boolean supports(HttpMethod method) { 26 | return HttpMethod.PATCH.equals(method) || HttpMethod.POST.equals(method); 27 | } 28 | 29 | @Override 30 | public void process( 31 | HttpMethod method, 32 | TusServletRequest servletRequest, 33 | TusServletResponse servletResponse, 34 | UploadStorageService uploadStorageService, 35 | String ownerKey) 36 | throws IOException, TusException { 37 | 38 | // For post requests, the upload URI is part of the response 39 | String uploadUri = servletResponse.getHeader(HttpHeader.LOCATION); 40 | if (StringUtils.isBlank(uploadUri)) { 41 | // For patch request, our upload URI is the URI of the request 42 | uploadUri = servletRequest.getRequestURI(); 43 | } 44 | 45 | Long expirationPeriod = uploadStorageService.getUploadExpirationPeriod(); 46 | UploadInfo uploadInfo = uploadStorageService.getUploadInfo(uploadUri, ownerKey); 47 | 48 | // The Upload-Expires response header MUST be included in every PATCH response if the upload 49 | // is 50 | // going to expire. 51 | // If the expiration is known at the creation, the Upload-Expires header MUST be included in 52 | // the 53 | // response to 54 | // the initial POST request. Its value MAY change over time. 55 | 56 | if (expirationPeriod != null && expirationPeriod > 0 && uploadInfo != null) { 57 | 58 | uploadInfo.updateExpiration(expirationPeriod); 59 | uploadStorageService.update(uploadInfo); 60 | 61 | servletResponse.setDateHeader(HttpHeader.UPLOAD_EXPIRES, uploadInfo.getExpirationTimestamp()); 62 | } 63 | } 64 | 65 | @Override 66 | public boolean isErrorHandler() { 67 | return true; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/termination/TerminationDeleteRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.termination; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | import java.io.IOException; 5 | import me.desair.tus.server.HttpMethod; 6 | import me.desair.tus.server.exception.TusException; 7 | import me.desair.tus.server.upload.UploadInfo; 8 | import me.desair.tus.server.upload.UploadStorageService; 9 | import me.desair.tus.server.util.AbstractRequestHandler; 10 | import me.desair.tus.server.util.TusServletRequest; 11 | import me.desair.tus.server.util.TusServletResponse; 12 | 13 | /** 14 | * When receiving a DELETE request for an existing upload the Server SHOULD free associated 15 | * resources and MUST respond with the 204 No Content status confirming that the upload was 16 | * terminated. For all future requests to this URL the Server SHOULD respond with the 404 Not Found 17 | * or 410 Gone status. 18 | */ 19 | public class TerminationDeleteRequestHandler extends AbstractRequestHandler { 20 | 21 | @Override 22 | public boolean supports(HttpMethod method) { 23 | return HttpMethod.DELETE.equals(method); 24 | } 25 | 26 | @Override 27 | public void process( 28 | HttpMethod method, 29 | TusServletRequest servletRequest, 30 | TusServletResponse servletResponse, 31 | UploadStorageService uploadStorageService, 32 | String ownerKey) 33 | throws IOException, TusException { 34 | 35 | UploadInfo uploadInfo = 36 | uploadStorageService.getUploadInfo(servletRequest.getRequestURI(), ownerKey); 37 | 38 | if (uploadInfo != null) { 39 | uploadStorageService.terminateUpload(uploadInfo); 40 | } 41 | 42 | servletResponse.setStatus(HttpServletResponse.SC_NO_CONTENT); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/termination/TerminationExtension.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.termination; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collection; 5 | import java.util.List; 6 | import me.desair.tus.server.HttpMethod; 7 | import me.desair.tus.server.RequestHandler; 8 | import me.desair.tus.server.RequestValidator; 9 | import me.desair.tus.server.util.AbstractTusExtension; 10 | 11 | /** 12 | * This extension defines a way for the Client to terminate completed and unfinished uploads 13 | * allowing the Server to free up used resources.
14 | * If this extension is supported by the Server, it MUST be announced by adding "termination" to the 15 | * Tus-Extension header. 16 | */ 17 | public class TerminationExtension extends AbstractTusExtension { 18 | 19 | @Override 20 | public String getName() { 21 | return "termination"; 22 | } 23 | 24 | @Override 25 | public Collection getMinimalSupportedHttpMethods() { 26 | return Arrays.asList(HttpMethod.OPTIONS, HttpMethod.DELETE); 27 | } 28 | 29 | @Override 30 | protected void initValidators(List requestValidators) { 31 | // All validation is all read done by the Core protocol 32 | } 33 | 34 | @Override 35 | protected void initRequestHandlers(List requestHandlers) { 36 | requestHandlers.add(new TerminationDeleteRequestHandler()); 37 | requestHandlers.add(new TerminationOptionsRequestHandler()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/termination/TerminationOptionsRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.termination; 2 | 3 | import me.desair.tus.server.util.AbstractExtensionRequestHandler; 4 | 5 | /** Add our download extension the Tus-Extension header */ 6 | public class TerminationOptionsRequestHandler extends AbstractExtensionRequestHandler { 7 | 8 | @Override 9 | protected void appendExtensions(StringBuilder extensionBuilder) { 10 | addExtension(extensionBuilder, "termination"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/upload/TimeBasedUploadIdFactory.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.upload; 2 | 3 | import java.io.Serializable; 4 | import org.apache.commons.lang3.StringUtils; 5 | 6 | /** 7 | * Alternative {@link UploadIdFactory} implementation that uses the current system time to generate 8 | * ID's. Since time is not unique, this upload ID factory should not be used in busy, clustered 9 | * production systems. 10 | */ 11 | public class TimeBasedUploadIdFactory extends UploadIdFactory { 12 | 13 | @Override 14 | protected Serializable getIdValueIfValid(String extractedUrlId) { 15 | Long id = null; 16 | 17 | if (StringUtils.isNotBlank(extractedUrlId)) { 18 | try { 19 | id = Long.parseLong(extractedUrlId); 20 | } catch (NumberFormatException ex) { 21 | id = null; 22 | } 23 | } 24 | 25 | return id; 26 | } 27 | 28 | @Override 29 | public synchronized UploadId createId() { 30 | return new UploadId(System.currentTimeMillis()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/upload/UploadId.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.upload; 2 | 3 | import java.io.Serializable; 4 | import java.io.UnsupportedEncodingException; 5 | import java.util.Objects; 6 | import org.apache.commons.codec.DecoderException; 7 | import org.apache.commons.codec.net.URLCodec; 8 | import org.apache.commons.lang3.Validate; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | /** The unique identifier of an upload process in the tus protocol */ 13 | public class UploadId implements Serializable { 14 | 15 | private static final String UPLOAD_ID_CHARSET = "UTF-8"; 16 | private static final Logger log = LoggerFactory.getLogger(UploadId.class); 17 | 18 | private String urlSafeValue; 19 | private Serializable originalObject; 20 | 21 | /** 22 | * Create a new {@link UploadId} instance based on the provided object using it's toString method. 23 | * 24 | * @param inputObject The object to use for constructing the ID 25 | */ 26 | public UploadId(Serializable inputObject) { 27 | String inputValue = (inputObject == null ? null : inputObject.toString()); 28 | Validate.notBlank(inputValue, "The upload ID value cannot be blank"); 29 | 30 | this.originalObject = inputObject; 31 | URLCodec codec = new URLCodec(); 32 | // Check if value is not encoded already 33 | try { 34 | if (inputValue != null && inputValue.equals(codec.decode(inputValue, UPLOAD_ID_CHARSET))) { 35 | this.urlSafeValue = codec.encode(inputValue, UPLOAD_ID_CHARSET); 36 | } else { 37 | // value is already encoded, use as is 38 | this.urlSafeValue = inputValue; 39 | } 40 | } catch (DecoderException | UnsupportedEncodingException e) { 41 | log.warn("Unable to URL encode upload ID value", e); 42 | this.urlSafeValue = inputValue; 43 | } 44 | } 45 | 46 | /** 47 | * The original input object that was provided when constructing this upload ID 48 | * 49 | * @return The original object used to create this ID 50 | */ 51 | public Serializable getOriginalObject() { 52 | return this.originalObject; 53 | } 54 | 55 | @Override 56 | public String toString() { 57 | return urlSafeValue; 58 | } 59 | 60 | @Override 61 | public boolean equals(Object o) { 62 | if (this == o) { 63 | return true; 64 | } 65 | if (!(o instanceof UploadId)) { 66 | return false; 67 | } 68 | 69 | UploadId uploadId = (UploadId) o; 70 | // Upload IDs with the same URL-safe value should be considered equal 71 | return Objects.equals(urlSafeValue, uploadId.urlSafeValue); 72 | } 73 | 74 | @Override 75 | public int hashCode() { 76 | // Upload IDs with the same URL-safe value should be considered equal 77 | return Objects.hash(urlSafeValue); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/upload/UploadIdFactory.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.upload; 2 | 3 | import java.io.Serializable; 4 | import java.util.regex.Matcher; 5 | import java.util.regex.Pattern; 6 | import org.apache.commons.lang3.StringUtils; 7 | import org.apache.commons.lang3.Validate; 8 | 9 | /** 10 | * Interface for a factory that can create unique upload IDs. This factory can also parse the upload 11 | * identifier from a given upload URL. 12 | */ 13 | public abstract class UploadIdFactory { 14 | 15 | private String uploadUri = "/"; 16 | private Pattern uploadUriPattern = null; 17 | 18 | /** 19 | * Set the URI under which the main tus upload endpoint is hosted. Optionally, this URI may 20 | * contain regex parameters in order to support endpoints that contain URL parameters, for example 21 | * /users/[0-9]+/files/upload 22 | * 23 | * @param uploadUri The URI of the main tus upload endpoint 24 | */ 25 | public void setUploadUri(String uploadUri) { 26 | Validate.notBlank(uploadUri, "The upload URI pattern cannot be blank"); 27 | Validate.isTrue(StringUtils.startsWith(uploadUri, "/"), "The upload URI should start with /"); 28 | Validate.isTrue(!StringUtils.endsWith(uploadUri, "$"), "The upload URI should not end with $"); 29 | this.uploadUri = uploadUri; 30 | this.uploadUriPattern = null; 31 | } 32 | 33 | /** 34 | * Return the URI of the main tus upload endpoint. Note that this value possibly contains regex 35 | * parameters. 36 | * 37 | * @return The URI of the main tus upload endpoint. 38 | */ 39 | public String getUploadUri() { 40 | return uploadUri; 41 | } 42 | 43 | /** 44 | * Read the upload identifier from the given URL.
45 | * Clients will send requests to upload URLs or provided URLs of completed uploads. This method is 46 | * able to parse those URLs and provide the user with the corresponding upload ID. 47 | * 48 | * @param url The URL provided by the client 49 | * @return The corresponding Upload identifier 50 | */ 51 | public UploadId readUploadId(String url) { 52 | Matcher uploadUriMatcher = getUploadUriPattern().matcher(StringUtils.trimToEmpty(url)); 53 | String pathId = uploadUriMatcher.replaceFirst(""); 54 | 55 | Serializable idValue = null; 56 | if (StringUtils.isNotBlank(pathId)) { 57 | idValue = getIdValueIfValid(pathId); 58 | } 59 | 60 | return idValue == null ? null : new UploadId(idValue); 61 | } 62 | 63 | /** 64 | * Create a new unique upload ID. 65 | * 66 | * @return A new unique upload ID 67 | */ 68 | public abstract UploadId createId(); 69 | 70 | /** 71 | * Transform the extracted path ID value to a value to use for the upload ID object. If the 72 | * extracted value is not valid, null is returned 73 | * 74 | * @param extractedUrlId The ID extracted from the URL 75 | * @return Value to use in the UploadId object, null if the extracted URL value was not valid 76 | */ 77 | protected abstract Serializable getIdValueIfValid(String extractedUrlId); 78 | 79 | /** 80 | * Build and retrieve the Upload URI regex pattern. 81 | * 82 | * @return A (cached) Pattern to match upload URI's 83 | */ 84 | protected Pattern getUploadUriPattern() { 85 | if (uploadUriPattern == null) { 86 | // We will extract the upload ID's by removing the upload URI from the start of the 87 | // request URI 88 | uploadUriPattern = 89 | Pattern.compile("^.*" + uploadUri + (StringUtils.endsWith(uploadUri, "/") ? "" : "/?")); 90 | } 91 | return uploadUriPattern; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/upload/UploadLock.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.upload; 2 | 3 | import java.io.IOException; 4 | 5 | /** Interface that represents a lock on an upload */ 6 | public interface UploadLock extends AutoCloseable { 7 | 8 | /** 9 | * Get the upload URI of the upload that is locked by this lock 10 | * 11 | * @return The URI of the locked upload 12 | */ 13 | String getUploadUri(); 14 | 15 | /** 16 | * Method to release the lock on an upload when done processing it. It's possible that this method 17 | * is called multiple times within the same request 18 | */ 19 | void release(); 20 | 21 | @Override 22 | void close() throws IOException; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/upload/UploadLockingService.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.upload; 2 | 3 | import java.io.IOException; 4 | import me.desair.tus.server.exception.TusException; 5 | 6 | /** 7 | * Service interface that can lock a specific upload so that it cannot be modified by other 8 | * requests/threads. 9 | */ 10 | public interface UploadLockingService { 11 | 12 | /** 13 | * If the given URI represents a valid upload, lock that upload for processing. 14 | * 15 | * @param requestUri The URI that potentially represents an upload 16 | * @return The lock on the upload, or null if not lock was applied 17 | * @throws TusException If the upload is already locked 18 | */ 19 | UploadLock lockUploadByUri(String requestUri) throws TusException, IOException; 20 | 21 | /** 22 | * Clean up any stale locks that are still present. 23 | * 24 | * @throws IOException When cleaning a stale lock fails 25 | */ 26 | void cleanupStaleLocks() throws IOException; 27 | 28 | /** 29 | * Check if the upload with the given ID is currently locked. 30 | * 31 | * @param id The ID of the upload to check 32 | * @return True if the upload is locked, false otherwise 33 | */ 34 | boolean isLocked(UploadId id); 35 | 36 | /** 37 | * Set an instance if IdFactory to be used for creating identities and extracting them from 38 | * uploadUris. 39 | * 40 | * @param idFactory The {@link UploadIdFactory} to use within this locking service 41 | */ 42 | void setIdFactory(UploadIdFactory idFactory); 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/upload/UploadType.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.upload; 2 | 3 | /** Enum that lists all the possible upload types in the tus protocol */ 4 | public enum UploadType { 5 | /** REGULAR indicates a normal upload */ 6 | REGULAR, 7 | 8 | /** PARTIAL indicates an upload that is part of a concatenated upload */ 9 | PARTIAL, 10 | 11 | /** CONCATENATED is the upload that combines different partial uploads */ 12 | CONCATENATED 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/upload/UuidUploadIdFactory.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.upload; 2 | 3 | import java.io.Serializable; 4 | import java.util.UUID; 5 | 6 | /** 7 | * Factory to create unique upload IDs. This factory can also parse the upload identifier from a 8 | * given upload URL. 9 | */ 10 | public class UuidUploadIdFactory extends UploadIdFactory { 11 | 12 | @Override 13 | protected Serializable getIdValueIfValid(String extractedUrlId) { 14 | UUID id = null; 15 | try { 16 | id = UUID.fromString(extractedUrlId); 17 | } catch (IllegalArgumentException ex) { 18 | id = null; 19 | } 20 | 21 | return id; 22 | } 23 | 24 | @Override 25 | public synchronized UploadId createId() { 26 | return new UploadId(UUID.randomUUID()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/upload/concatenation/UploadConcatenationService.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.upload.concatenation; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.util.List; 6 | import me.desair.tus.server.exception.UploadNotFoundException; 7 | import me.desair.tus.server.upload.UploadInfo; 8 | 9 | /** 10 | * Interface for a service that is able to concatenate partial uploads into a concatenated upload 11 | */ 12 | public interface UploadConcatenationService { 13 | 14 | /** 15 | * Merge the given concatenated upload if all the underlying partial uploads are completed. If the 16 | * underlying partial uploads are still in-progress, this method does nothing. Otherwise the 17 | * upload information of the concatenated upload is updated. 18 | * 19 | * @param uploadInfo The concatenated upload 20 | * @throws IOException If merging the upload fails 21 | * @throws UploadNotFoundException When one of the partial uploads cannot be found 22 | */ 23 | void merge(UploadInfo uploadInfo) throws IOException, UploadNotFoundException; 24 | 25 | /** 26 | * Get the concatenated bytes of this concatenated upload 27 | * 28 | * @param uploadInfo The concatenated upload 29 | * @return The concatenated bytes, or null if this upload is still in progress 30 | * @throws IOException When return the concatenated bytes fails 31 | * @throws UploadNotFoundException When the or one of the partial uploads cannot be found 32 | */ 33 | InputStream getConcatenatedBytes(UploadInfo uploadInfo) 34 | throws IOException, UploadNotFoundException; 35 | 36 | /** 37 | * Get all underlying partial uploads associated with the given concatenated upload 38 | * 39 | * @param info The concatenated upload 40 | * @return The underlying partial uploads 41 | * @throws IOException When retrieving the underlying partial uploads fails 42 | * @throws UploadNotFoundException When one of the partial uploads cannot be found 43 | */ 44 | List getPartialUploads(UploadInfo info) throws IOException, UploadNotFoundException; 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/upload/concatenation/UploadInputStreamEnumeration.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.upload.concatenation; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.util.ArrayList; 6 | import java.util.Enumeration; 7 | import java.util.Iterator; 8 | import java.util.List; 9 | import me.desair.tus.server.exception.UploadNotFoundException; 10 | import me.desair.tus.server.upload.UploadInfo; 11 | import me.desair.tus.server.upload.UploadStorageService; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | /** 16 | * Enumeration class that enumerates all input streams associated with with given list of uploads 17 | */ 18 | public class UploadInputStreamEnumeration implements Enumeration { 19 | 20 | private static final Logger log = LoggerFactory.getLogger(UploadInputStreamEnumeration.class); 21 | 22 | private List uploads; 23 | private UploadStorageService uploadStorageService; 24 | private Iterator uploadIterator; 25 | private InputStream currentInputStream = null; 26 | 27 | public UploadInputStreamEnumeration( 28 | List uploadList, UploadStorageService uploadStorageService) { 29 | this.uploads = new ArrayList<>(uploadList); 30 | this.uploadStorageService = uploadStorageService; 31 | this.uploadIterator = this.uploads.iterator(); 32 | } 33 | 34 | @Override 35 | public boolean hasMoreElements() { 36 | if (uploadIterator != null && uploadIterator.hasNext()) { 37 | currentInputStream = getNextInputStream(); 38 | } else { 39 | currentInputStream = null; 40 | } 41 | 42 | // if we could not get a next upload stream, set the iterator to null 43 | // to make sure repeated calls give the same result 44 | if (currentInputStream == null) { 45 | uploadIterator = null; 46 | return false; 47 | } else { 48 | return true; 49 | } 50 | } 51 | 52 | @Override 53 | public InputStream nextElement() { 54 | return currentInputStream; 55 | } 56 | 57 | private InputStream getNextInputStream() { 58 | InputStream is = null; 59 | UploadInfo info = uploadIterator.next(); 60 | if (info != null) { 61 | try { 62 | is = uploadStorageService.getUploadedBytes(info.getId()); 63 | } catch (IOException | UploadNotFoundException ex) { 64 | log.error("Error while retrieving input stream for upload with ID " + info.getId(), ex); 65 | is = null; 66 | } 67 | } 68 | return is; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/upload/disk/AbstractDiskBasedService.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.upload.disk; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | import java.nio.file.Paths; 7 | import me.desair.tus.server.TusFileUploadService; 8 | import me.desair.tus.server.upload.UploadId; 9 | import org.apache.commons.lang3.Validate; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | /** Common abstract super class to implement service that use the disk file system */ 14 | public class AbstractDiskBasedService { 15 | 16 | private static final Logger log = LoggerFactory.getLogger(TusFileUploadService.class); 17 | 18 | private Path storagePath; 19 | 20 | public AbstractDiskBasedService(String path) { 21 | Validate.notBlank(path, "The storage path cannot be blank"); 22 | this.storagePath = Paths.get(path); 23 | } 24 | 25 | protected Path getStoragePath() { 26 | return storagePath; 27 | } 28 | 29 | protected Path getPathInStorageDirectory(UploadId id) { 30 | if (!Files.exists(storagePath)) { 31 | init(); 32 | } 33 | 34 | if (id == null) { 35 | return null; 36 | } else { 37 | return storagePath.resolve(id.toString()); 38 | } 39 | } 40 | 41 | private synchronized void init() { 42 | if (!Files.exists(storagePath)) { 43 | try { 44 | Files.createDirectories(storagePath); 45 | } catch (IOException e) { 46 | String message = 47 | "Unable to create the directory specified by the storage path " + storagePath; 48 | log.error(message, e); 49 | throw new StoragePathNotAvailableException(message, e); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/upload/disk/DiskLockingService.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.upload.disk; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.file.DirectoryStream; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.nio.file.attribute.FileTime; 9 | import me.desair.tus.server.exception.TusException; 10 | import me.desair.tus.server.exception.UploadAlreadyLockedException; 11 | import me.desair.tus.server.upload.UploadId; 12 | import me.desair.tus.server.upload.UploadIdFactory; 13 | import me.desair.tus.server.upload.UploadLock; 14 | import me.desair.tus.server.upload.UploadLockingService; 15 | import org.apache.commons.lang3.Validate; 16 | 17 | /** 18 | * {@link UploadLockingService} implementation that uses the file system for implementing locking 19 | *
20 | * File locking can also apply to shared network drives. This way the framework supports clustering 21 | * as long as the upload storage directory is mounted as a shared (network) drive.
22 | * File locks are also automatically released on application (JVM) shutdown. This means the file 23 | * locking is not persistent and prevents cleanup and stale lock issues. 24 | */ 25 | public class DiskLockingService extends AbstractDiskBasedService implements UploadLockingService { 26 | 27 | private static final String LOCK_SUB_DIRECTORY = "locks"; 28 | 29 | private UploadIdFactory idFactory; 30 | 31 | public DiskLockingService(String storagePath) { 32 | super(storagePath + File.separator + LOCK_SUB_DIRECTORY); 33 | } 34 | 35 | /** Constructor to use custom UploadIdFactory. */ 36 | public DiskLockingService(UploadIdFactory idFactory, String storagePath) { 37 | this(storagePath); 38 | Validate.notNull(idFactory, "The IdFactory cannot be null"); 39 | this.idFactory = idFactory; 40 | } 41 | 42 | @Override 43 | public UploadLock lockUploadByUri(String requestUri) throws TusException, IOException { 44 | 45 | UploadId id = idFactory.readUploadId(requestUri); 46 | 47 | UploadLock lock = null; 48 | 49 | Path lockPath = getLockPath(id); 50 | // If lockPath is not null, we know this is a valid Upload URI 51 | if (lockPath != null) { 52 | lock = new FileBasedLock(requestUri, lockPath); 53 | } 54 | return lock; 55 | } 56 | 57 | @Override 58 | public void cleanupStaleLocks() throws IOException { 59 | try (DirectoryStream locksStream = Files.newDirectoryStream(getStoragePath())) { 60 | for (Path path : locksStream) { 61 | 62 | FileTime lastModifiedTime = Files.getLastModifiedTime(path); 63 | if (lastModifiedTime.toMillis() < System.currentTimeMillis() - 10000L) { 64 | UploadId id = new UploadId(path.getFileName().toString()); 65 | 66 | if (!isLocked(id)) { 67 | Files.deleteIfExists(path); 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | @Override 75 | public boolean isLocked(UploadId id) { 76 | boolean locked = false; 77 | Path lockPath = getLockPath(id); 78 | 79 | if (lockPath != null) { 80 | // Try to obtain a lock to see if the upload is currently locked 81 | try (UploadLock lock = new FileBasedLock(id.toString(), lockPath)) { 82 | 83 | // We got the lock, so it means no one else is locking it. 84 | locked = false; 85 | 86 | } catch (UploadAlreadyLockedException | IOException e) { 87 | // There was already a lock 88 | locked = true; 89 | } 90 | } 91 | 92 | return locked; 93 | } 94 | 95 | @Override 96 | public void setIdFactory(UploadIdFactory idFactory) { 97 | Validate.notNull(idFactory, "The IdFactory cannot be null"); 98 | this.idFactory = idFactory; 99 | } 100 | 101 | private Path getLockPath(UploadId id) { 102 | return getPathInStorageDirectory(id); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/upload/disk/ExpiredUploadFilter.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.upload.disk; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.DirectoryStream; 5 | import java.nio.file.Path; 6 | import java.util.Objects; 7 | import me.desair.tus.server.upload.UploadId; 8 | import me.desair.tus.server.upload.UploadInfo; 9 | import me.desair.tus.server.upload.UploadLockingService; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | /** Directory stream filter that only accepts uploads that are still in progress and expired */ 14 | public class ExpiredUploadFilter implements DirectoryStream.Filter { 15 | 16 | private static final Logger log = LoggerFactory.getLogger(ExpiredUploadFilter.class); 17 | 18 | private DiskStorageService diskStorageService; 19 | private UploadLockingService uploadLockingService; 20 | 21 | ExpiredUploadFilter( 22 | DiskStorageService diskStorageService, UploadLockingService uploadLockingService) { 23 | this.diskStorageService = diskStorageService; 24 | this.uploadLockingService = uploadLockingService; 25 | } 26 | 27 | @Override 28 | public boolean accept(Path upload) throws IOException { 29 | UploadId id = null; 30 | try { 31 | id = new UploadId(upload.getFileName().toString()); 32 | UploadInfo info = diskStorageService.getUploadInfo(id); 33 | 34 | if (info != null && info.isExpired() && !uploadLockingService.isLocked(id)) { 35 | return true; 36 | } 37 | 38 | } catch (Exception ex) { 39 | if (log.isDebugEnabled()) { 40 | log.debug("Not able to determine state of upload " + Objects.toString(id), ex); 41 | } 42 | } 43 | 44 | return false; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/upload/disk/FileBasedLock.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.upload.disk; 2 | 3 | import static java.nio.file.StandardOpenOption.CREATE; 4 | import static java.nio.file.StandardOpenOption.WRITE; 5 | 6 | import java.io.IOException; 7 | import java.nio.channels.FileChannel; 8 | import java.nio.channels.FileLock; 9 | import java.nio.channels.OverlappingFileLockException; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import me.desair.tus.server.exception.UploadAlreadyLockedException; 13 | import me.desair.tus.server.upload.UploadLock; 14 | import me.desair.tus.server.util.Utils; 15 | import org.apache.commons.lang3.Validate; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | /** 20 | * Upload locking implementation using the file system file locking mechanism. File locking can also 21 | * apply to shared network drives. This way the framework supports clustering as long as the upload 22 | * storage directory is mounted as a shared (network) drive.
23 | * File locks are also automatically released on application (JVM) shutdown. This means the file 24 | * locking is not persistent and prevents cleanup and stale lock issues. 25 | */ 26 | public class FileBasedLock implements UploadLock { 27 | 28 | private static final Logger log = LoggerFactory.getLogger(FileBasedLock.class); 29 | 30 | private String uploadUri; 31 | private FileChannel fileChannel = null; 32 | protected Path lockPath; 33 | 34 | /** Constructor. */ 35 | public FileBasedLock(String uploadUri, Path lockPath) 36 | throws UploadAlreadyLockedException, IOException { 37 | Validate.notBlank(uploadUri, "The upload URI cannot be blank"); 38 | Validate.notNull(lockPath, "The path to the lock cannot be null"); 39 | this.uploadUri = uploadUri; 40 | this.lockPath = lockPath; 41 | 42 | tryToObtainFileLock(); 43 | } 44 | 45 | private void tryToObtainFileLock() throws UploadAlreadyLockedException, IOException { 46 | String message = "The upload " + getUploadUri() + " is already locked"; 47 | 48 | try { 49 | // Try to acquire a lock 50 | fileChannel = createFileChannel(); 51 | FileLock fileLock = Utils.lockFileExclusively(fileChannel); 52 | 53 | // If the upload is already locked, our lock will be null 54 | if (fileLock == null) { 55 | fileChannel.close(); 56 | throw new UploadAlreadyLockedException(message); 57 | } 58 | 59 | } catch (OverlappingFileLockException e) { 60 | if (fileChannel != null) { 61 | try { 62 | fileChannel.close(); 63 | } catch (IOException e1) { 64 | // Should not happen 65 | } 66 | } 67 | throw new UploadAlreadyLockedException(message); 68 | } catch (IOException e) { 69 | throw new IOException( 70 | "Unable to create or open file required to implement file-based locking", e); 71 | } 72 | } 73 | 74 | @Override 75 | public String getUploadUri() { 76 | return uploadUri; 77 | } 78 | 79 | @Override 80 | public void release() { 81 | try { 82 | // Closing the channel will also release the lock 83 | fileChannel.close(); 84 | Files.deleteIfExists(lockPath); 85 | } catch (IOException e) { 86 | log.warn("Unable to release file lock for URI " + getUploadUri(), e); 87 | } 88 | } 89 | 90 | @Override 91 | public void close() throws IOException { 92 | release(); 93 | } 94 | 95 | protected FileChannel createFileChannel() throws IOException { 96 | return FileChannel.open(lockPath, CREATE, WRITE); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/upload/disk/StoragePathNotAvailableException.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.upload.disk; 2 | 3 | /** Exception thrown when the disk storage path cannot be read or created. */ 4 | public class StoragePathNotAvailableException extends RuntimeException { 5 | public StoragePathNotAvailableException(String message, Throwable e) { 6 | super(message, e); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/util/AbstractExtensionRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.util; 2 | 3 | import me.desair.tus.server.HttpHeader; 4 | import me.desair.tus.server.HttpMethod; 5 | import me.desair.tus.server.upload.UploadStorageService; 6 | import org.apache.commons.lang3.StringUtils; 7 | 8 | /** Abstract request handler to add tus extension values to the correct header */ 9 | public abstract class AbstractExtensionRequestHandler extends AbstractRequestHandler { 10 | 11 | @Override 12 | public boolean supports(HttpMethod method) { 13 | return HttpMethod.OPTIONS.equals(method); 14 | } 15 | 16 | @Override 17 | public void process( 18 | HttpMethod method, 19 | TusServletRequest servletRequest, 20 | TusServletResponse servletResponse, 21 | UploadStorageService uploadStorageService, 22 | String ownerKey) { 23 | 24 | StringBuilder extensionBuilder = 25 | new StringBuilder( 26 | StringUtils.trimToEmpty(servletResponse.getHeader(HttpHeader.TUS_EXTENSION))); 27 | 28 | appendExtensions(extensionBuilder); 29 | 30 | servletResponse.setHeader(HttpHeader.TUS_EXTENSION, extensionBuilder.toString()); 31 | } 32 | 33 | protected abstract void appendExtensions(StringBuilder extensionBuilder); 34 | 35 | protected void addExtension(StringBuilder stringBuilder, String extension) { 36 | if (stringBuilder.length() > 0) { 37 | stringBuilder.append(","); 38 | } 39 | stringBuilder.append(extension); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/util/AbstractRequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.util; 2 | 3 | import me.desair.tus.server.RequestHandler; 4 | 5 | /** 6 | * Abstract {@link me.desair.tus.server.RequestHandler} implementation that contains the common 7 | * functionality. 8 | */ 9 | public abstract class AbstractRequestHandler implements RequestHandler { 10 | 11 | @Override 12 | public boolean isErrorHandler() { 13 | return false; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/util/AbstractTusExtension.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.util; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import java.io.IOException; 5 | import java.util.LinkedList; 6 | import java.util.List; 7 | import me.desair.tus.server.HttpMethod; 8 | import me.desair.tus.server.RequestHandler; 9 | import me.desair.tus.server.RequestValidator; 10 | import me.desair.tus.server.TusExtension; 11 | import me.desair.tus.server.exception.TusException; 12 | import me.desair.tus.server.upload.UploadStorageService; 13 | 14 | /** Abstract class to implement a tus extension using validators and request handlers. */ 15 | public abstract class AbstractTusExtension implements TusExtension { 16 | 17 | private List requestValidators = new LinkedList<>(); 18 | private List requestHandlers = new LinkedList<>(); 19 | 20 | protected AbstractTusExtension() { 21 | initValidators(requestValidators); 22 | initRequestHandlers(requestHandlers); 23 | } 24 | 25 | protected abstract void initValidators(List requestValidators); 26 | 27 | protected abstract void initRequestHandlers(List requestHandlers); 28 | 29 | @Override 30 | public void validate( 31 | HttpMethod method, 32 | HttpServletRequest servletRequest, 33 | UploadStorageService uploadStorageService, 34 | String ownerKey) 35 | throws TusException, IOException { 36 | 37 | for (RequestValidator requestValidator : requestValidators) { 38 | if (requestValidator.supports(method)) { 39 | requestValidator.validate(method, servletRequest, uploadStorageService, ownerKey); 40 | } 41 | } 42 | } 43 | 44 | @Override 45 | public void process( 46 | HttpMethod method, 47 | TusServletRequest servletRequest, 48 | TusServletResponse servletResponse, 49 | UploadStorageService uploadStorageService, 50 | String ownerKey) 51 | throws IOException, TusException { 52 | 53 | for (RequestHandler requestHandler : requestHandlers) { 54 | if (requestHandler.supports(method)) { 55 | requestHandler.process( 56 | method, servletRequest, servletResponse, uploadStorageService, ownerKey); 57 | } 58 | } 59 | } 60 | 61 | @Override 62 | public void handleError( 63 | HttpMethod method, 64 | TusServletRequest request, 65 | TusServletResponse response, 66 | UploadStorageService uploadStorageService, 67 | String ownerKey) 68 | throws IOException, TusException { 69 | 70 | for (RequestHandler requestHandler : requestHandlers) { 71 | if (requestHandler.supports(method) && requestHandler.isErrorHandler()) { 72 | requestHandler.process(method, request, response, uploadStorageService, ownerKey); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/me/desair/tus/server/util/TusServletResponse.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.util; 2 | 3 | import jakarta.servlet.http.HttpServletResponse; 4 | import jakarta.servlet.http.HttpServletResponseWrapper; 5 | import java.util.HashMap; 6 | import java.util.LinkedList; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.Objects; 10 | import org.apache.commons.lang3.StringUtils; 11 | 12 | /** 13 | * {@link HttpServletResponseWrapper} to capture header values set on the current {@link 14 | * HttpServletResponse}. 15 | */ 16 | public class TusServletResponse extends HttpServletResponseWrapper { 17 | 18 | private Map> headers = new HashMap<>(); 19 | 20 | /** 21 | * Constructs a response adaptor wrapping the given response. 22 | * 23 | * @param response The response that has to be wrapped 24 | * @throws IllegalArgumentException if the response is null 25 | */ 26 | public TusServletResponse(HttpServletResponse response) { 27 | super(response); 28 | } 29 | 30 | @Override 31 | public void setDateHeader(String name, long date) { 32 | super.setDateHeader(name, date); 33 | overwriteHeader(name, Objects.toString(date)); 34 | } 35 | 36 | @Override 37 | public void addDateHeader(String name, long date) { 38 | super.addDateHeader(name, date); 39 | recordHeader(name, Objects.toString(date)); 40 | } 41 | 42 | @Override 43 | public void setHeader(String name, String value) { 44 | super.setHeader(name, value); 45 | overwriteHeader(name, value); 46 | } 47 | 48 | @Override 49 | public void addHeader(String name, String value) { 50 | super.addHeader(name, value); 51 | recordHeader(name, value); 52 | } 53 | 54 | @Override 55 | public void setIntHeader(String name, int value) { 56 | super.setIntHeader(name, value); 57 | overwriteHeader(name, Objects.toString(value)); 58 | } 59 | 60 | @Override 61 | public void addIntHeader(String name, int value) { 62 | super.addIntHeader(name, value); 63 | recordHeader(name, Objects.toString(value)); 64 | } 65 | 66 | @Override 67 | public String getHeader(String name) { 68 | String value; 69 | if (headers.containsKey(name)) { 70 | value = headers.get(name).get(0); 71 | } else { 72 | value = super.getHeader(name); 73 | } 74 | return StringUtils.trimToNull(value); 75 | } 76 | 77 | private void recordHeader(String name, String value) { 78 | List values = headers.computeIfAbsent(name, k -> new LinkedList<>()); 79 | values.add(value); 80 | } 81 | 82 | private void overwriteHeader(String name, String value) { 83 | List values = new LinkedList<>(); 84 | values.add(value); 85 | headers.put(name, values); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/AbstractTusExtensionIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.containsInAnyOrder; 6 | import static org.mockito.ArgumentMatchers.any; 7 | import static org.mockito.ArgumentMatchers.nullable; 8 | import static org.mockito.Mockito.when; 9 | 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.io.StringWriter; 13 | import java.nio.charset.StandardCharsets; 14 | import java.util.Arrays; 15 | import me.desair.tus.server.exception.TusException; 16 | import me.desair.tus.server.upload.UploadInfo; 17 | import me.desair.tus.server.upload.UploadStorageService; 18 | import me.desair.tus.server.util.AbstractTusExtension; 19 | import me.desair.tus.server.util.TusServletRequest; 20 | import me.desair.tus.server.util.TusServletResponse; 21 | import org.apache.commons.io.IOUtils; 22 | import org.junit.runner.RunWith; 23 | import org.mockito.Mock; 24 | import org.mockito.junit.MockitoJUnitRunner; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | import org.springframework.mock.web.MockHttpServletRequest; 28 | import org.springframework.mock.web.MockHttpServletResponse; 29 | 30 | @RunWith(MockitoJUnitRunner.Silent.class) 31 | public abstract class AbstractTusExtensionIntegrationTest { 32 | 33 | private static final Logger log = 34 | LoggerFactory.getLogger(AbstractTusExtensionIntegrationTest.class); 35 | 36 | protected AbstractTusExtension tusFeature; 37 | 38 | protected MockHttpServletRequest servletRequest; 39 | 40 | protected MockHttpServletResponse servletResponse; 41 | 42 | @Mock protected UploadStorageService uploadStorageService; 43 | 44 | protected UploadInfo uploadInfo; 45 | 46 | protected void prepareUploadInfo(Long offset, Long length) throws IOException, TusException { 47 | uploadInfo = new UploadInfo(); 48 | uploadInfo.setOffset(offset); 49 | uploadInfo.setLength(length); 50 | when(uploadStorageService.getUploadInfo(nullable(String.class), nullable(String.class))) 51 | .thenReturn(uploadInfo); 52 | when(uploadStorageService.append(any(UploadInfo.class), any(InputStream.class))) 53 | .thenReturn(uploadInfo); 54 | } 55 | 56 | protected void setRequestHeaders(String... headers) { 57 | if (headers != null && headers.length > 0) { 58 | for (String header : headers) { 59 | switch (header) { 60 | case HttpHeader.TUS_RESUMABLE: 61 | servletRequest.addHeader(HttpHeader.TUS_RESUMABLE, "1.0.0"); 62 | break; 63 | case HttpHeader.CONTENT_TYPE: 64 | servletRequest.addHeader(HttpHeader.CONTENT_TYPE, "application/offset+octet-stream"); 65 | break; 66 | case HttpHeader.UPLOAD_OFFSET: 67 | servletRequest.addHeader(HttpHeader.UPLOAD_OFFSET, uploadInfo.getOffset()); 68 | break; 69 | case HttpHeader.CONTENT_LENGTH: 70 | servletRequest.addHeader( 71 | HttpHeader.CONTENT_LENGTH, uploadInfo.getLength() - uploadInfo.getOffset()); 72 | break; 73 | default: 74 | log.warn("Undefined HTTP header " + header); 75 | break; 76 | } 77 | } 78 | } 79 | } 80 | 81 | protected void executeCall(HttpMethod method, boolean readContent) 82 | throws TusException, IOException { 83 | tusFeature.validate(method, servletRequest, uploadStorageService, null); 84 | TusServletRequest tusServletRequest = new TusServletRequest(this.servletRequest, true); 85 | 86 | if (readContent) { 87 | StringWriter writer = new StringWriter(); 88 | IOUtils.copy(tusServletRequest.getContentInputStream(), writer, StandardCharsets.UTF_8); 89 | } 90 | 91 | tusFeature.process( 92 | method, 93 | tusServletRequest, 94 | new TusServletResponse(servletResponse), 95 | uploadStorageService, 96 | null); 97 | } 98 | 99 | protected void assertResponseHeader(String header, String value) { 100 | assertThat(servletResponse.getHeader(header), is(value)); 101 | } 102 | 103 | protected void assertResponseHeader(String header, String... values) { 104 | assertThat( 105 | Arrays.asList(servletResponse.getHeader(header).split(",")), containsInAnyOrder(values)); 106 | } 107 | 108 | protected void assertResponseStatus(int httpStatus) { 109 | assertThat(servletResponse.getStatus(), is(httpStatus)); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/HttpMethodTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.util.EnumSet; 6 | import org.junit.Test; 7 | import org.springframework.mock.web.MockHttpServletRequest; 8 | 9 | public class HttpMethodTest { 10 | 11 | @Test 12 | public void forName() throws Exception { 13 | assertEquals(HttpMethod.DELETE, HttpMethod.forName("delete")); 14 | assertEquals(HttpMethod.GET, HttpMethod.forName("get")); 15 | assertEquals(HttpMethod.HEAD, HttpMethod.forName("head")); 16 | assertEquals(HttpMethod.PATCH, HttpMethod.forName("patch")); 17 | assertEquals(HttpMethod.POST, HttpMethod.forName("post")); 18 | assertEquals(HttpMethod.PUT, HttpMethod.forName("put")); 19 | assertEquals(HttpMethod.OPTIONS, HttpMethod.forName("options")); 20 | assertEquals(null, HttpMethod.forName("test")); 21 | } 22 | 23 | @Test 24 | public void getMethodNormal() throws Exception { 25 | MockHttpServletRequest servletRequest = new MockHttpServletRequest(); 26 | servletRequest.setMethod("patch"); 27 | 28 | assertEquals( 29 | HttpMethod.PATCH, 30 | HttpMethod.getMethodIfSupported(servletRequest, EnumSet.allOf(HttpMethod.class))); 31 | } 32 | 33 | @Test 34 | public void getMethodOverridden() throws Exception { 35 | MockHttpServletRequest servletRequest = new MockHttpServletRequest(); 36 | servletRequest.setMethod("post"); 37 | servletRequest.addHeader(HttpHeader.METHOD_OVERRIDE, "patch"); 38 | 39 | assertEquals( 40 | HttpMethod.PATCH, 41 | HttpMethod.getMethodIfSupported(servletRequest, EnumSet.allOf(HttpMethod.class))); 42 | } 43 | 44 | @Test 45 | public void getMethodOverriddenDoesNotExist() throws Exception { 46 | MockHttpServletRequest servletRequest = new MockHttpServletRequest(); 47 | servletRequest.setMethod("post"); 48 | servletRequest.addHeader(HttpHeader.METHOD_OVERRIDE, "test"); 49 | 50 | assertEquals( 51 | HttpMethod.POST, 52 | HttpMethod.getMethodIfSupported(servletRequest, EnumSet.allOf(HttpMethod.class))); 53 | } 54 | 55 | @Test(expected = NullPointerException.class) 56 | public void getMethodNull() throws Exception { 57 | HttpMethod.getMethodIfSupported(null, EnumSet.allOf(HttpMethod.class)); 58 | } 59 | 60 | @Test 61 | public void getMethodNotSupported() throws Exception { 62 | MockHttpServletRequest servletRequest = new MockHttpServletRequest(); 63 | servletRequest.setMethod("put"); 64 | 65 | assertEquals( 66 | null, HttpMethod.getMethodIfSupported(servletRequest, EnumSet.noneOf(HttpMethod.class))); 67 | } 68 | 69 | @Test 70 | public void getMethodRequestNotExists() throws Exception { 71 | MockHttpServletRequest servletRequest = new MockHttpServletRequest(); 72 | servletRequest.setMethod("test"); 73 | 74 | assertEquals( 75 | null, HttpMethod.getMethodIfSupported(servletRequest, EnumSet.noneOf(HttpMethod.class))); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/checksum/ChecksumAlgorithmTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.checksum; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertNotNull; 5 | 6 | import org.junit.Test; 7 | 8 | public class ChecksumAlgorithmTest { 9 | 10 | @Test 11 | public void getMessageDigest() throws Exception { 12 | assertNotNull(ChecksumAlgorithm.MD5.getMessageDigest()); 13 | assertNotNull(ChecksumAlgorithm.SHA1.getMessageDigest()); 14 | assertNotNull(ChecksumAlgorithm.SHA256.getMessageDigest()); 15 | assertNotNull(ChecksumAlgorithm.SHA384.getMessageDigest()); 16 | assertNotNull(ChecksumAlgorithm.SHA512.getMessageDigest()); 17 | } 18 | 19 | @Test 20 | public void forTusName() throws Exception { 21 | assertEquals(ChecksumAlgorithm.MD5, ChecksumAlgorithm.forTusName("md5")); 22 | assertEquals(ChecksumAlgorithm.SHA1, ChecksumAlgorithm.forTusName("sha1")); 23 | assertEquals(ChecksumAlgorithm.SHA256, ChecksumAlgorithm.forTusName("sha256")); 24 | assertEquals(ChecksumAlgorithm.SHA384, ChecksumAlgorithm.forTusName("sha384")); 25 | assertEquals(ChecksumAlgorithm.SHA512, ChecksumAlgorithm.forTusName("sha512")); 26 | assertEquals(null, ChecksumAlgorithm.forTusName("test")); 27 | } 28 | 29 | @Test 30 | public void forUploadChecksumHeader() throws Exception { 31 | assertEquals( 32 | ChecksumAlgorithm.MD5, ChecksumAlgorithm.forUploadChecksumHeader("md5 1234567890")); 33 | assertEquals( 34 | ChecksumAlgorithm.SHA1, ChecksumAlgorithm.forUploadChecksumHeader("sha1 1234567890")); 35 | assertEquals( 36 | ChecksumAlgorithm.SHA256, ChecksumAlgorithm.forUploadChecksumHeader("sha256 1234567890")); 37 | assertEquals( 38 | ChecksumAlgorithm.SHA384, ChecksumAlgorithm.forUploadChecksumHeader("sha384 1234567890")); 39 | assertEquals( 40 | ChecksumAlgorithm.SHA512, ChecksumAlgorithm.forUploadChecksumHeader("sha512 1234567890")); 41 | assertEquals(null, ChecksumAlgorithm.forUploadChecksumHeader("test 1234567890")); 42 | } 43 | 44 | @Test 45 | public void testToString() throws Exception { 46 | assertEquals("md5", ChecksumAlgorithm.MD5.toString()); 47 | assertEquals("sha1", ChecksumAlgorithm.SHA1.toString()); 48 | assertEquals("sha256", ChecksumAlgorithm.SHA256.toString()); 49 | assertEquals("sha384", ChecksumAlgorithm.SHA384.toString()); 50 | assertEquals("sha512", ChecksumAlgorithm.SHA512.toString()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/checksum/ChecksumOptionsRequestHandlerTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.checksum; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.containsInAnyOrder; 6 | 7 | import java.util.Arrays; 8 | import me.desair.tus.server.HttpHeader; 9 | import me.desair.tus.server.HttpMethod; 10 | import me.desair.tus.server.util.TusServletRequest; 11 | import me.desair.tus.server.util.TusServletResponse; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.springframework.mock.web.MockHttpServletRequest; 15 | import org.springframework.mock.web.MockHttpServletResponse; 16 | 17 | public class ChecksumOptionsRequestHandlerTest { 18 | 19 | private ChecksumOptionsRequestHandler handler; 20 | 21 | private MockHttpServletRequest servletRequest; 22 | 23 | private MockHttpServletResponse servletResponse; 24 | 25 | @Before 26 | public void setUp() { 27 | servletRequest = new MockHttpServletRequest(); 28 | servletResponse = new MockHttpServletResponse(); 29 | handler = new ChecksumOptionsRequestHandler(); 30 | } 31 | 32 | @Test 33 | public void processListExtensions() throws Exception { 34 | 35 | handler.process( 36 | HttpMethod.OPTIONS, 37 | new TusServletRequest(servletRequest), 38 | new TusServletResponse(servletResponse), 39 | null, 40 | null); 41 | 42 | assertThat( 43 | Arrays.asList(servletResponse.getHeader(HttpHeader.TUS_EXTENSION).split(",")), 44 | containsInAnyOrder("checksum", "checksum-trailer")); 45 | 46 | assertThat( 47 | Arrays.asList(servletResponse.getHeader(HttpHeader.TUS_CHECKSUM_ALGORITHM).split(",")), 48 | containsInAnyOrder("md5", "sha1", "sha256", "sha384", "sha512")); 49 | } 50 | 51 | @Test 52 | public void supports() throws Exception { 53 | assertThat(handler.supports(HttpMethod.GET), is(false)); 54 | assertThat(handler.supports(HttpMethod.POST), is(false)); 55 | assertThat(handler.supports(HttpMethod.PUT), is(false)); 56 | assertThat(handler.supports(HttpMethod.DELETE), is(false)); 57 | assertThat(handler.supports(HttpMethod.HEAD), is(false)); 58 | assertThat(handler.supports(HttpMethod.OPTIONS), is(true)); 59 | assertThat(handler.supports(HttpMethod.PATCH), is(false)); 60 | assertThat(handler.supports(null), is(false)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/checksum/ChecksumPatchRequestHandlerTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.checksum; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.mockito.ArgumentMatchers.any; 6 | import static org.mockito.ArgumentMatchers.nullable; 7 | import static org.mockito.Mockito.never; 8 | import static org.mockito.Mockito.times; 9 | import static org.mockito.Mockito.verify; 10 | import static org.mockito.Mockito.when; 11 | 12 | import me.desair.tus.server.HttpHeader; 13 | import me.desair.tus.server.HttpMethod; 14 | import me.desair.tus.server.exception.ChecksumAlgorithmNotSupportedException; 15 | import me.desair.tus.server.exception.UploadChecksumMismatchException; 16 | import me.desair.tus.server.upload.UploadInfo; 17 | import me.desair.tus.server.upload.UploadStorageService; 18 | import me.desair.tus.server.util.TusServletRequest; 19 | import org.junit.Before; 20 | import org.junit.Test; 21 | import org.junit.runner.RunWith; 22 | import org.mockito.ArgumentMatchers; 23 | import org.mockito.Mock; 24 | import org.mockito.junit.MockitoJUnitRunner; 25 | 26 | @RunWith(MockitoJUnitRunner.Silent.class) 27 | public class ChecksumPatchRequestHandlerTest { 28 | 29 | private ChecksumPatchRequestHandler handler; 30 | 31 | @Mock private TusServletRequest servletRequest; 32 | 33 | @Mock private UploadStorageService uploadStorageService; 34 | 35 | @Before 36 | public void setUp() throws Exception { 37 | handler = new ChecksumPatchRequestHandler(); 38 | 39 | UploadInfo info = new UploadInfo(); 40 | info.setOffset(2L); 41 | info.setLength(10L); 42 | when(uploadStorageService.getUploadInfo(nullable(String.class), nullable(String.class))) 43 | .thenReturn(info); 44 | } 45 | 46 | @Test 47 | public void supports() throws Exception { 48 | assertThat(handler.supports(HttpMethod.GET), is(false)); 49 | assertThat(handler.supports(HttpMethod.POST), is(false)); 50 | assertThat(handler.supports(HttpMethod.PUT), is(false)); 51 | assertThat(handler.supports(HttpMethod.DELETE), is(false)); 52 | assertThat(handler.supports(HttpMethod.HEAD), is(false)); 53 | assertThat(handler.supports(HttpMethod.OPTIONS), is(false)); 54 | assertThat(handler.supports(HttpMethod.PATCH), is(true)); 55 | assertThat(handler.supports(null), is(false)); 56 | } 57 | 58 | @Test 59 | public void testValidHeaderAndChecksum() throws Exception { 60 | when(servletRequest.getHeader(HttpHeader.UPLOAD_CHECKSUM)).thenReturn("sha1 1234567890"); 61 | when(servletRequest.getCalculatedChecksum(ArgumentMatchers.any(ChecksumAlgorithm.class))) 62 | .thenReturn("1234567890"); 63 | when(servletRequest.hasCalculatedChecksum()).thenReturn(true); 64 | 65 | handler.process(HttpMethod.PATCH, servletRequest, null, uploadStorageService, null); 66 | 67 | verify(servletRequest, times(1)).getCalculatedChecksum(any(ChecksumAlgorithm.class)); 68 | } 69 | 70 | @Test(expected = UploadChecksumMismatchException.class) 71 | public void testValidHeaderAndInvalidChecksum() throws Exception { 72 | when(servletRequest.getHeader(HttpHeader.UPLOAD_CHECKSUM)).thenReturn("sha1 1234567890"); 73 | when(servletRequest.getCalculatedChecksum(ArgumentMatchers.any(ChecksumAlgorithm.class))) 74 | .thenReturn("0123456789"); 75 | when(servletRequest.hasCalculatedChecksum()).thenReturn(true); 76 | 77 | handler.process(HttpMethod.PATCH, servletRequest, null, uploadStorageService, null); 78 | } 79 | 80 | @Test 81 | public void testNoHeader() throws Exception { 82 | when(servletRequest.getHeader(HttpHeader.UPLOAD_CHECKSUM)).thenReturn(null); 83 | 84 | handler.process(HttpMethod.PATCH, servletRequest, null, uploadStorageService, null); 85 | 86 | verify(servletRequest, never()).getCalculatedChecksum(any(ChecksumAlgorithm.class)); 87 | } 88 | 89 | @Test(expected = ChecksumAlgorithmNotSupportedException.class) 90 | public void testInvalidHeader() throws Exception { 91 | when(servletRequest.getHeader(HttpHeader.UPLOAD_CHECKSUM)).thenReturn("test 1234567890"); 92 | when(servletRequest.hasCalculatedChecksum()).thenReturn(true); 93 | 94 | handler.process(HttpMethod.PATCH, servletRequest, null, uploadStorageService, null); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/checksum/ITChecksumExtension.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.checksum; 2 | 3 | import static org.junit.Assert.fail; 4 | import static org.mockito.Mockito.atLeastOnce; 5 | import static org.mockito.Mockito.spy; 6 | import static org.mockito.Mockito.verify; 7 | 8 | import me.desair.tus.server.AbstractTusExtensionIntegrationTest; 9 | import me.desair.tus.server.HttpHeader; 10 | import me.desair.tus.server.HttpMethod; 11 | import me.desair.tus.server.exception.ChecksumAlgorithmNotSupportedException; 12 | import me.desair.tus.server.exception.UploadChecksumMismatchException; 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | import org.springframework.mock.web.MockHttpServletRequest; 16 | import org.springframework.mock.web.MockHttpServletResponse; 17 | 18 | public class ITChecksumExtension extends AbstractTusExtensionIntegrationTest { 19 | 20 | @Before 21 | public void setUp() throws Exception { 22 | servletRequest = spy(new MockHttpServletRequest()); 23 | servletResponse = new MockHttpServletResponse(); 24 | tusFeature = new ChecksumExtension(); 25 | uploadInfo = null; 26 | } 27 | 28 | @Test 29 | public void testOptions() throws Exception { 30 | setRequestHeaders(); 31 | 32 | executeCall(HttpMethod.OPTIONS, false); 33 | 34 | assertResponseHeader(HttpHeader.TUS_EXTENSION, "checksum", "checksum-trailer"); 35 | assertResponseHeader( 36 | HttpHeader.TUS_CHECKSUM_ALGORITHM, "md5", "sha1", "sha256", "sha384", "sha512"); 37 | } 38 | 39 | @Test(expected = ChecksumAlgorithmNotSupportedException.class) 40 | public void testInvalidAlgorithm() throws Exception { 41 | servletRequest.addHeader(HttpHeader.UPLOAD_CHECKSUM, "test 1234567890"); 42 | servletRequest.setContent("Test content".getBytes()); 43 | 44 | executeCall(HttpMethod.PATCH, false); 45 | } 46 | 47 | @Test 48 | public void testValidChecksumTrailerHeader() throws Exception { 49 | String content = 50 | "8\r\n" 51 | + "Mozilla \r\n" 52 | + "A\r\n" 53 | + "Developer \r\n" 54 | + "7\r\n" 55 | + "Network\r\n" 56 | + "0\r\n" 57 | + "Upload-Checksum: sha1 zYR9iS5Rya+WoH1fEyfKqqdPWWE=\r\n" 58 | + "\r\n"; 59 | 60 | servletRequest.addHeader(HttpHeader.TRANSFER_ENCODING, "chunked"); 61 | servletRequest.setContent(content.getBytes()); 62 | 63 | try { 64 | executeCall(HttpMethod.PATCH, true); 65 | } catch (Exception ex) { 66 | fail(); 67 | } 68 | } 69 | 70 | @Test 71 | public void testValidChecksumNormalHeader() throws Exception { 72 | String content = "Mozilla Developer Network"; 73 | 74 | servletRequest.addHeader(HttpHeader.UPLOAD_CHECKSUM, "sha1 zYR9iS5Rya+WoH1fEyfKqqdPWWE="); 75 | servletRequest.setContent(content.getBytes()); 76 | 77 | executeCall(HttpMethod.PATCH, true); 78 | 79 | verify(servletRequest, atLeastOnce()).getHeader(HttpHeader.UPLOAD_CHECKSUM); 80 | } 81 | 82 | @Test(expected = UploadChecksumMismatchException.class) 83 | public void testInvalidChecksumTrailerHeader() throws Exception { 84 | String content = 85 | "8\r\n" 86 | + "Mozilla \r\n" 87 | + "A\r\n" 88 | + "Developer \r\n" 89 | + "7\r\n" 90 | + "Network\r\n" 91 | + "0\r\n" 92 | + "Upload-Checksum: sha1 zYR9iS5Rya+WoH1fEyfKqqdPWW=\r\n" 93 | + "\r\n"; 94 | 95 | servletRequest.addHeader(HttpHeader.TRANSFER_ENCODING, "chunked"); 96 | servletRequest.setContent(content.getBytes()); 97 | 98 | executeCall(HttpMethod.PATCH, true); 99 | } 100 | 101 | @Test(expected = UploadChecksumMismatchException.class) 102 | public void testInvalidChecksumNormalHeader() throws Exception { 103 | String content = "Mozilla Developer Network"; 104 | 105 | servletRequest.addHeader(HttpHeader.UPLOAD_CHECKSUM, "sha1 zYR9iS5Rya+WoH1fEyfKqqdPWW="); 106 | servletRequest.setContent(content.getBytes()); 107 | 108 | executeCall(HttpMethod.PATCH, true); 109 | } 110 | 111 | @Test 112 | public void testNoChecksum() throws Exception { 113 | String content = "Mozilla Developer Network"; 114 | 115 | servletRequest.setContent(content.getBytes()); 116 | 117 | try { 118 | executeCall(HttpMethod.PATCH, true); 119 | } catch (Exception ex) { 120 | fail(); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/checksum/validation/ChecksumAlgorithmValidatorTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.checksum.validation; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.junit.Assert.fail; 6 | import static org.mockito.Mockito.spy; 7 | import static org.mockito.Mockito.times; 8 | import static org.mockito.Mockito.verify; 9 | 10 | import me.desair.tus.server.HttpHeader; 11 | import me.desair.tus.server.HttpMethod; 12 | import me.desair.tus.server.exception.ChecksumAlgorithmNotSupportedException; 13 | import me.desair.tus.server.upload.UploadStorageService; 14 | import org.junit.Before; 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | import org.mockito.Mock; 18 | import org.mockito.junit.MockitoJUnitRunner; 19 | import org.springframework.mock.web.MockHttpServletRequest; 20 | 21 | @RunWith(MockitoJUnitRunner.Silent.class) 22 | public class ChecksumAlgorithmValidatorTest { 23 | 24 | private ChecksumAlgorithmValidator validator; 25 | 26 | private MockHttpServletRequest servletRequest; 27 | 28 | @Mock private UploadStorageService uploadStorageService; 29 | 30 | @Before 31 | public void setUp() { 32 | servletRequest = spy(new MockHttpServletRequest()); 33 | validator = new ChecksumAlgorithmValidator(); 34 | } 35 | 36 | @Test 37 | public void supports() throws Exception { 38 | assertThat(validator.supports(HttpMethod.GET), is(false)); 39 | assertThat(validator.supports(HttpMethod.POST), is(false)); 40 | assertThat(validator.supports(HttpMethod.PUT), is(false)); 41 | assertThat(validator.supports(HttpMethod.DELETE), is(false)); 42 | assertThat(validator.supports(HttpMethod.HEAD), is(false)); 43 | assertThat(validator.supports(HttpMethod.OPTIONS), is(false)); 44 | assertThat(validator.supports(HttpMethod.PATCH), is(true)); 45 | assertThat(validator.supports(null), is(false)); 46 | } 47 | 48 | @Test 49 | public void testValid() throws Exception { 50 | servletRequest.addHeader(HttpHeader.UPLOAD_CHECKSUM, "sha1 1234567890"); 51 | 52 | validator.validate(HttpMethod.PATCH, servletRequest, uploadStorageService, null); 53 | 54 | verify(servletRequest, times(1)).getHeader(HttpHeader.UPLOAD_CHECKSUM); 55 | } 56 | 57 | @Test 58 | public void testNoHeader() throws Exception { 59 | // servletRequest.addHeader(HttpHeader.UPLOAD_CHECKSUM, null); 60 | 61 | try { 62 | validator.validate(HttpMethod.PATCH, servletRequest, uploadStorageService, null); 63 | } catch (Exception ex) { 64 | fail(); 65 | } 66 | } 67 | 68 | @Test(expected = ChecksumAlgorithmNotSupportedException.class) 69 | public void testInvalidHeader() throws Exception { 70 | servletRequest.addHeader(HttpHeader.UPLOAD_CHECKSUM, "test 1234567890"); 71 | 72 | validator.validate(HttpMethod.PATCH, servletRequest, uploadStorageService, null); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/concatenation/ConcatenationOptionsRequestHandlerTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.concatenation; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.containsInAnyOrder; 6 | 7 | import java.util.Arrays; 8 | import me.desair.tus.server.HttpHeader; 9 | import me.desair.tus.server.HttpMethod; 10 | import me.desair.tus.server.util.TusServletRequest; 11 | import me.desair.tus.server.util.TusServletResponse; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.springframework.mock.web.MockHttpServletRequest; 15 | import org.springframework.mock.web.MockHttpServletResponse; 16 | 17 | public class ConcatenationOptionsRequestHandlerTest { 18 | 19 | private ConcatenationOptionsRequestHandler handler; 20 | 21 | private MockHttpServletRequest servletRequest; 22 | 23 | private MockHttpServletResponse servletResponse; 24 | 25 | @Before 26 | public void setUp() { 27 | servletRequest = new MockHttpServletRequest(); 28 | servletResponse = new MockHttpServletResponse(); 29 | handler = new ConcatenationOptionsRequestHandler(); 30 | } 31 | 32 | @Test 33 | public void processListExtensions() throws Exception { 34 | 35 | handler.process( 36 | HttpMethod.OPTIONS, 37 | new TusServletRequest(servletRequest), 38 | new TusServletResponse(servletResponse), 39 | null, 40 | null); 41 | 42 | assertThat( 43 | Arrays.asList(servletResponse.getHeader(HttpHeader.TUS_EXTENSION).split(",")), 44 | containsInAnyOrder("concatenation", "concatenation-unfinished")); 45 | } 46 | 47 | @Test 48 | public void supports() throws Exception { 49 | assertThat(handler.supports(HttpMethod.GET), is(false)); 50 | assertThat(handler.supports(HttpMethod.POST), is(false)); 51 | assertThat(handler.supports(HttpMethod.PUT), is(false)); 52 | assertThat(handler.supports(HttpMethod.DELETE), is(false)); 53 | assertThat(handler.supports(HttpMethod.HEAD), is(false)); 54 | assertThat(handler.supports(HttpMethod.OPTIONS), is(true)); 55 | assertThat(handler.supports(HttpMethod.PATCH), is(false)); 56 | assertThat(handler.supports(null), is(false)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/concatenation/validation/NoUploadLengthOnFinalValidatorTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.concatenation.validation; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.junit.Assert.fail; 6 | 7 | import me.desair.tus.server.HttpHeader; 8 | import me.desair.tus.server.HttpMethod; 9 | import me.desair.tus.server.exception.UploadLengthNotAllowedOnConcatenationException; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.springframework.mock.web.MockHttpServletRequest; 13 | 14 | public class NoUploadLengthOnFinalValidatorTest { 15 | 16 | private NoUploadLengthOnFinalValidator validator; 17 | 18 | private MockHttpServletRequest servletRequest; 19 | 20 | @Before 21 | public void setUp() { 22 | servletRequest = new MockHttpServletRequest(); 23 | validator = new NoUploadLengthOnFinalValidator(); 24 | } 25 | 26 | @Test 27 | public void supports() throws Exception { 28 | assertThat(validator.supports(HttpMethod.GET), is(false)); 29 | assertThat(validator.supports(HttpMethod.POST), is(true)); 30 | assertThat(validator.supports(HttpMethod.PUT), is(false)); 31 | assertThat(validator.supports(HttpMethod.DELETE), is(false)); 32 | assertThat(validator.supports(HttpMethod.HEAD), is(false)); 33 | assertThat(validator.supports(HttpMethod.OPTIONS), is(false)); 34 | assertThat(validator.supports(HttpMethod.PATCH), is(false)); 35 | assertThat(validator.supports(null), is(false)); 36 | } 37 | 38 | @Test 39 | public void validateFinalUploadValid() throws Exception { 40 | servletRequest.addHeader(HttpHeader.UPLOAD_CONCAT, "final;12345 235235 253523"); 41 | // servletRequest.addHeader(HttpHeader.UPLOAD_LENGTH, "10L"); 42 | 43 | // When we validate the request 44 | try { 45 | validator.validate(HttpMethod.POST, servletRequest, null, null); 46 | } catch (Exception ex) { 47 | fail(); 48 | } 49 | 50 | // No Exception is thrown 51 | } 52 | 53 | @Test(expected = UploadLengthNotAllowedOnConcatenationException.class) 54 | public void validateFinalUploadInvalid() throws Exception { 55 | servletRequest.addHeader(HttpHeader.UPLOAD_CONCAT, "final;12345 235235 253523"); 56 | servletRequest.addHeader(HttpHeader.UPLOAD_LENGTH, "10L"); 57 | 58 | // When we validate the request 59 | validator.validate(HttpMethod.POST, servletRequest, null, null); 60 | } 61 | 62 | @Test 63 | public void validateNotFinal1() throws Exception { 64 | servletRequest.addHeader(HttpHeader.UPLOAD_CONCAT, "partial"); 65 | servletRequest.addHeader(HttpHeader.UPLOAD_LENGTH, "10L"); 66 | 67 | // When we validate the request 68 | try { 69 | validator.validate(HttpMethod.POST, servletRequest, null, null); 70 | } catch (Exception ex) { 71 | fail(); 72 | } 73 | 74 | // No Exception is thrown 75 | } 76 | 77 | @Test 78 | public void validateNotFinal2() throws Exception { 79 | // servletRequest.addHeader(HttpHeader.UPLOAD_CONCAT, "partial"); 80 | // servletRequest.addHeader(HttpHeader.UPLOAD_LENGTH, "10L"); 81 | 82 | // When we validate the request 83 | try { 84 | validator.validate(HttpMethod.POST, servletRequest, null, null); 85 | } catch (Exception ex) { 86 | fail(); 87 | } 88 | 89 | // No Exception is thrown 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/core/CoreDefaultResponseHeadersHandlerTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.core; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | 6 | import me.desair.tus.server.HttpHeader; 7 | import me.desair.tus.server.HttpMethod; 8 | import me.desair.tus.server.TusFileUploadService; 9 | import me.desair.tus.server.util.TusServletRequest; 10 | import me.desair.tus.server.util.TusServletResponse; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.springframework.mock.web.MockHttpServletRequest; 14 | import org.springframework.mock.web.MockHttpServletResponse; 15 | 16 | public class CoreDefaultResponseHeadersHandlerTest { 17 | 18 | private MockHttpServletRequest servletRequest; 19 | 20 | private MockHttpServletResponse servletResponse; 21 | 22 | private CoreDefaultResponseHeadersHandler handler; 23 | 24 | @Before 25 | public void setUp() { 26 | servletRequest = new MockHttpServletRequest(); 27 | servletResponse = new MockHttpServletResponse(); 28 | handler = new CoreDefaultResponseHeadersHandler(); 29 | } 30 | 31 | @Test 32 | public void supports() throws Exception { 33 | assertThat(handler.supports(HttpMethod.GET), is(true)); 34 | assertThat(handler.supports(HttpMethod.POST), is(true)); 35 | assertThat(handler.supports(HttpMethod.PUT), is(true)); 36 | assertThat(handler.supports(HttpMethod.DELETE), is(true)); 37 | assertThat(handler.supports(HttpMethod.HEAD), is(true)); 38 | assertThat(handler.supports(HttpMethod.OPTIONS), is(true)); 39 | assertThat(handler.supports(HttpMethod.PATCH), is(true)); 40 | assertThat(handler.supports(null), is(true)); 41 | } 42 | 43 | @Test 44 | public void process() throws Exception { 45 | handler.process( 46 | HttpMethod.PATCH, 47 | new TusServletRequest(servletRequest), 48 | new TusServletResponse(servletResponse), 49 | null, 50 | null); 51 | 52 | assertThat( 53 | servletResponse.getHeader(HttpHeader.TUS_RESUMABLE), 54 | is(TusFileUploadService.TUS_API_VERSION)); 55 | assertThat(servletResponse.getHeader(HttpHeader.CONTENT_LENGTH), is("0")); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/core/CoreOptionsRequestHandlerTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.core; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.CoreMatchers.nullValue; 5 | import static org.hamcrest.MatcherAssert.assertThat; 6 | import static org.mockito.Mockito.when; 7 | 8 | import jakarta.servlet.http.HttpServletResponse; 9 | import me.desair.tus.server.HttpHeader; 10 | import me.desair.tus.server.HttpMethod; 11 | import me.desair.tus.server.TusFileUploadService; 12 | import me.desair.tus.server.upload.UploadStorageService; 13 | import me.desair.tus.server.util.TusServletRequest; 14 | import me.desair.tus.server.util.TusServletResponse; 15 | import org.junit.Before; 16 | import org.junit.Test; 17 | import org.junit.runner.RunWith; 18 | import org.mockito.Mock; 19 | import org.mockito.junit.MockitoJUnitRunner; 20 | import org.springframework.mock.web.MockHttpServletRequest; 21 | import org.springframework.mock.web.MockHttpServletResponse; 22 | 23 | @RunWith(MockitoJUnitRunner.Silent.class) 24 | public class CoreOptionsRequestHandlerTest { 25 | 26 | private CoreOptionsRequestHandler handler; 27 | 28 | private MockHttpServletRequest servletRequest; 29 | 30 | private MockHttpServletResponse servletResponse; 31 | 32 | @Mock private UploadStorageService uploadStorageService; 33 | 34 | @Before 35 | public void setUp() { 36 | servletRequest = new MockHttpServletRequest(); 37 | servletResponse = new MockHttpServletResponse(); 38 | handler = new CoreOptionsRequestHandler(); 39 | } 40 | 41 | @Test 42 | public void processWithMaxSize() throws Exception { 43 | when(uploadStorageService.getMaxUploadSize()).thenReturn(5368709120L); 44 | 45 | handler.process( 46 | HttpMethod.OPTIONS, 47 | new TusServletRequest(servletRequest), 48 | new TusServletResponse(servletResponse), 49 | uploadStorageService, 50 | null); 51 | 52 | assertThat( 53 | servletResponse.getHeader(HttpHeader.TUS_VERSION), 54 | is(TusFileUploadService.TUS_API_VERSION)); 55 | assertThat(servletResponse.getHeader(HttpHeader.TUS_MAX_SIZE), is("5368709120")); 56 | assertThat(servletResponse.getStatus(), is(HttpServletResponse.SC_NO_CONTENT)); 57 | } 58 | 59 | @Test 60 | public void processWithoutMaxSize() throws Exception { 61 | when(uploadStorageService.getMaxUploadSize()).thenReturn(0L); 62 | 63 | handler.process( 64 | HttpMethod.OPTIONS, 65 | new TusServletRequest(servletRequest), 66 | new TusServletResponse(servletResponse), 67 | uploadStorageService, 68 | null); 69 | 70 | assertThat(servletResponse.getHeader(HttpHeader.TUS_VERSION), is("1.0.0")); 71 | assertThat(servletResponse.getHeader(HttpHeader.TUS_MAX_SIZE), is(nullValue())); 72 | assertThat(servletResponse.getStatus(), is(HttpServletResponse.SC_NO_CONTENT)); 73 | } 74 | 75 | @Test 76 | public void supports() throws Exception { 77 | assertThat(handler.supports(HttpMethod.GET), is(false)); 78 | assertThat(handler.supports(HttpMethod.POST), is(false)); 79 | assertThat(handler.supports(HttpMethod.PUT), is(false)); 80 | assertThat(handler.supports(HttpMethod.DELETE), is(false)); 81 | assertThat(handler.supports(HttpMethod.HEAD), is(false)); 82 | assertThat(handler.supports(HttpMethod.OPTIONS), is(true)); 83 | assertThat(handler.supports(HttpMethod.PATCH), is(false)); 84 | assertThat(handler.supports(null), is(false)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/core/validation/ContentTypeValidatorTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.core.validation; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.junit.Assert.fail; 6 | 7 | import me.desair.tus.server.HttpHeader; 8 | import me.desair.tus.server.HttpMethod; 9 | import me.desair.tus.server.exception.InvalidContentTypeException; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.springframework.mock.web.MockHttpServletRequest; 13 | 14 | public class ContentTypeValidatorTest { 15 | 16 | private ContentTypeValidator validator; 17 | 18 | private MockHttpServletRequest servletRequest; 19 | 20 | @Before 21 | public void setUp() { 22 | servletRequest = new MockHttpServletRequest(); 23 | validator = new ContentTypeValidator(); 24 | } 25 | 26 | @Test 27 | public void validateValid() throws Exception { 28 | servletRequest.addHeader( 29 | HttpHeader.CONTENT_TYPE, ContentTypeValidator.APPLICATION_OFFSET_OCTET_STREAM); 30 | 31 | try { 32 | validator.validate(HttpMethod.PATCH, servletRequest, null, null); 33 | } catch (Exception ex) { 34 | fail(); 35 | } 36 | 37 | // No exception is thrown 38 | } 39 | 40 | @Test(expected = InvalidContentTypeException.class) 41 | public void validateInvalidHeader() throws Exception { 42 | servletRequest.addHeader(HttpHeader.CONTENT_TYPE, "application/octet-stream"); 43 | 44 | validator.validate(HttpMethod.PATCH, servletRequest, null, null); 45 | 46 | // Expect a InvalidContentTypeException exception 47 | } 48 | 49 | @Test(expected = InvalidContentTypeException.class) 50 | public void validateMissingHeader() throws Exception { 51 | // We don't set the header 52 | // servletRequest.addHeader(HttpHeader.CONTENT_TYPE, 53 | // ContentTypeValidator.APPLICATION_OFFSET_OCTET_STREAM); 54 | 55 | validator.validate(HttpMethod.PATCH, servletRequest, null, null); 56 | 57 | // Expect a InvalidContentTypeException exception 58 | } 59 | 60 | @Test 61 | public void supports() throws Exception { 62 | assertThat(validator.supports(HttpMethod.GET), is(false)); 63 | assertThat(validator.supports(HttpMethod.POST), is(false)); 64 | assertThat(validator.supports(HttpMethod.PUT), is(false)); 65 | assertThat(validator.supports(HttpMethod.DELETE), is(false)); 66 | assertThat(validator.supports(HttpMethod.HEAD), is(false)); 67 | assertThat(validator.supports(HttpMethod.OPTIONS), is(false)); 68 | assertThat(validator.supports(HttpMethod.PATCH), is(true)); 69 | assertThat(validator.supports(null), is(false)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/core/validation/HttpMethodValidatorTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.core.validation; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.junit.Assert.fail; 6 | 7 | import me.desair.tus.server.HttpMethod; 8 | import me.desair.tus.server.exception.UnsupportedMethodException; 9 | import me.desair.tus.server.upload.UploadStorageService; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.springframework.mock.web.MockHttpServletRequest; 13 | 14 | /** Test cases for the {@link HttpMethodValidator}. */ 15 | public class HttpMethodValidatorTest { 16 | 17 | private MockHttpServletRequest servletRequest; 18 | private HttpMethodValidator validator; 19 | private UploadStorageService uploadStorageService; 20 | 21 | @Before 22 | public void setUp() { 23 | servletRequest = new MockHttpServletRequest(); 24 | validator = new HttpMethodValidator(); 25 | } 26 | 27 | @Test 28 | public void validateValid() throws Exception { 29 | try { 30 | validator.validate(HttpMethod.POST, servletRequest, uploadStorageService, null); 31 | } catch (Exception ex) { 32 | fail(); 33 | } 34 | } 35 | 36 | @Test(expected = UnsupportedMethodException.class) 37 | public void validateInvalid() throws Exception { 38 | validator.validate(null, servletRequest, uploadStorageService, null); 39 | } 40 | 41 | @Test 42 | public void supports() throws Exception { 43 | assertThat(validator.supports(HttpMethod.GET), is(true)); 44 | assertThat(validator.supports(HttpMethod.POST), is(true)); 45 | assertThat(validator.supports(HttpMethod.PUT), is(true)); 46 | assertThat(validator.supports(HttpMethod.DELETE), is(true)); 47 | assertThat(validator.supports(HttpMethod.HEAD), is(true)); 48 | assertThat(validator.supports(HttpMethod.OPTIONS), is(true)); 49 | assertThat(validator.supports(HttpMethod.PATCH), is(true)); 50 | assertThat(validator.supports(null), is(true)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/core/validation/IdExistsValidatorTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.core.validation; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.junit.Assert.fail; 6 | import static org.mockito.ArgumentMatchers.nullable; 7 | import static org.mockito.Mockito.when; 8 | 9 | import me.desair.tus.server.HttpMethod; 10 | import me.desair.tus.server.exception.UploadNotFoundException; 11 | import me.desair.tus.server.upload.UploadInfo; 12 | import me.desair.tus.server.upload.UploadStorageService; 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.mockito.Mock; 17 | import org.mockito.junit.MockitoJUnitRunner; 18 | import org.springframework.mock.web.MockHttpServletRequest; 19 | 20 | @RunWith(MockitoJUnitRunner.Silent.class) 21 | public class IdExistsValidatorTest { 22 | 23 | private IdExistsValidator validator; 24 | 25 | private MockHttpServletRequest servletRequest; 26 | 27 | @Mock private UploadStorageService uploadStorageService; 28 | 29 | @Before 30 | public void setUp() { 31 | servletRequest = new MockHttpServletRequest(); 32 | validator = new IdExistsValidator(); 33 | } 34 | 35 | @Test 36 | public void validateValid() throws Exception { 37 | UploadInfo info = new UploadInfo(); 38 | info.setOffset(0L); 39 | info.setLength(10L); 40 | when(uploadStorageService.getUploadInfo(nullable(String.class), nullable(String.class))) 41 | .thenReturn(info); 42 | 43 | // When we validate the request 44 | try { 45 | validator.validate(HttpMethod.PATCH, servletRequest, uploadStorageService, null); 46 | } catch (Exception ex) { 47 | fail(); 48 | } 49 | 50 | // No Exception is thrown 51 | } 52 | 53 | @Test(expected = UploadNotFoundException.class) 54 | public void validateInvalid() throws Exception { 55 | when(uploadStorageService.getUploadInfo(nullable(String.class), nullable(String.class))) 56 | .thenReturn(null); 57 | 58 | // When we validate the request 59 | validator.validate(HttpMethod.PATCH, servletRequest, uploadStorageService, null); 60 | 61 | // Expect a UploadNotFoundException 62 | } 63 | 64 | @Test 65 | public void supports() throws Exception { 66 | assertThat(validator.supports(HttpMethod.GET), is(true)); 67 | assertThat(validator.supports(HttpMethod.POST), is(false)); 68 | assertThat(validator.supports(HttpMethod.PUT), is(false)); 69 | assertThat(validator.supports(HttpMethod.DELETE), is(true)); 70 | assertThat(validator.supports(HttpMethod.HEAD), is(true)); 71 | assertThat(validator.supports(HttpMethod.OPTIONS), is(false)); 72 | assertThat(validator.supports(HttpMethod.PATCH), is(true)); 73 | assertThat(validator.supports(null), is(false)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/core/validation/TusResumableValidatorTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.core.validation; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.junit.Assert.fail; 6 | 7 | import me.desair.tus.server.HttpHeader; 8 | import me.desair.tus.server.HttpMethod; 9 | import me.desair.tus.server.exception.InvalidTusResumableException; 10 | import me.desair.tus.server.upload.UploadStorageService; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.springframework.mock.web.MockHttpServletRequest; 14 | 15 | public class TusResumableValidatorTest { 16 | 17 | private MockHttpServletRequest servletRequest; 18 | private TusResumableValidator validator; 19 | private UploadStorageService uploadStorageService; 20 | 21 | @Before 22 | public void setUp() { 23 | servletRequest = new MockHttpServletRequest(); 24 | validator = new TusResumableValidator(); 25 | } 26 | 27 | @Test(expected = InvalidTusResumableException.class) 28 | public void validateNoVersion() throws Exception { 29 | validator.validate(HttpMethod.POST, servletRequest, uploadStorageService, null); 30 | } 31 | 32 | @Test(expected = InvalidTusResumableException.class) 33 | public void validateInvalidVersion() throws Exception { 34 | servletRequest.addHeader(HttpHeader.TUS_RESUMABLE, "2.0.0"); 35 | validator.validate(HttpMethod.POST, servletRequest, uploadStorageService, null); 36 | } 37 | 38 | @Test 39 | public void validateValid() throws Exception { 40 | servletRequest.addHeader(HttpHeader.TUS_RESUMABLE, "1.0.0"); 41 | try { 42 | validator.validate(HttpMethod.POST, servletRequest, uploadStorageService, null); 43 | } catch (Exception ex) { 44 | fail(); 45 | } 46 | } 47 | 48 | @Test 49 | public void validateNullMethod() throws Exception { 50 | servletRequest.addHeader(HttpHeader.TUS_RESUMABLE, "1.0.0"); 51 | try { 52 | validator.validate(null, servletRequest, uploadStorageService, null); 53 | } catch (Exception ex) { 54 | fail(); 55 | } 56 | } 57 | 58 | @Test 59 | public void supports() throws Exception { 60 | assertThat(validator.supports(HttpMethod.GET), is(false)); 61 | assertThat(validator.supports(HttpMethod.POST), is(true)); 62 | assertThat(validator.supports(HttpMethod.PUT), is(true)); 63 | assertThat(validator.supports(HttpMethod.DELETE), is(true)); 64 | assertThat(validator.supports(HttpMethod.HEAD), is(true)); 65 | assertThat(validator.supports(HttpMethod.OPTIONS), is(false)); 66 | assertThat(validator.supports(HttpMethod.PATCH), is(true)); 67 | assertThat(validator.supports(null), is(true)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/creation/CreationOptionsRequestHandlerTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.creation; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.containsInAnyOrder; 6 | 7 | import java.util.Arrays; 8 | import me.desair.tus.server.HttpHeader; 9 | import me.desair.tus.server.HttpMethod; 10 | import me.desair.tus.server.util.TusServletRequest; 11 | import me.desair.tus.server.util.TusServletResponse; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.springframework.mock.web.MockHttpServletRequest; 15 | import org.springframework.mock.web.MockHttpServletResponse; 16 | 17 | public class CreationOptionsRequestHandlerTest { 18 | 19 | private CreationOptionsRequestHandler handler; 20 | 21 | private MockHttpServletRequest servletRequest; 22 | 23 | private MockHttpServletResponse servletResponse; 24 | 25 | @Before 26 | public void setUp() { 27 | servletRequest = new MockHttpServletRequest(); 28 | servletResponse = new MockHttpServletResponse(); 29 | handler = new CreationOptionsRequestHandler(); 30 | } 31 | 32 | @Test 33 | public void processListExtensions() throws Exception { 34 | 35 | handler.process( 36 | HttpMethod.OPTIONS, 37 | new TusServletRequest(servletRequest), 38 | new TusServletResponse(servletResponse), 39 | null, 40 | null); 41 | 42 | assertThat( 43 | Arrays.asList(servletResponse.getHeader(HttpHeader.TUS_EXTENSION).split(",")), 44 | containsInAnyOrder("creation", "creation-defer-length")); 45 | } 46 | 47 | @Test 48 | public void supports() throws Exception { 49 | assertThat(handler.supports(HttpMethod.GET), is(false)); 50 | assertThat(handler.supports(HttpMethod.POST), is(false)); 51 | assertThat(handler.supports(HttpMethod.PUT), is(false)); 52 | assertThat(handler.supports(HttpMethod.DELETE), is(false)); 53 | assertThat(handler.supports(HttpMethod.HEAD), is(false)); 54 | assertThat(handler.supports(HttpMethod.OPTIONS), is(true)); 55 | assertThat(handler.supports(HttpMethod.PATCH), is(false)); 56 | assertThat(handler.supports(null), is(false)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/creation/validation/PostEmptyRequestValidatorTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.creation.validation; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.junit.Assert.fail; 6 | 7 | import me.desair.tus.server.HttpHeader; 8 | import me.desair.tus.server.HttpMethod; 9 | import me.desair.tus.server.exception.InvalidContentLengthException; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.springframework.mock.web.MockHttpServletRequest; 13 | 14 | public class PostEmptyRequestValidatorTest { 15 | 16 | private PostEmptyRequestValidator validator; 17 | 18 | private MockHttpServletRequest servletRequest; 19 | 20 | @Before 21 | public void setUp() { 22 | servletRequest = new MockHttpServletRequest(); 23 | validator = new PostEmptyRequestValidator(); 24 | } 25 | 26 | @Test 27 | public void supports() throws Exception { 28 | assertThat(validator.supports(HttpMethod.GET), is(false)); 29 | assertThat(validator.supports(HttpMethod.POST), is(true)); 30 | assertThat(validator.supports(HttpMethod.PUT), is(false)); 31 | assertThat(validator.supports(HttpMethod.DELETE), is(false)); 32 | assertThat(validator.supports(HttpMethod.HEAD), is(false)); 33 | assertThat(validator.supports(HttpMethod.OPTIONS), is(false)); 34 | assertThat(validator.supports(HttpMethod.PATCH), is(false)); 35 | assertThat(validator.supports(null), is(false)); 36 | } 37 | 38 | @Test 39 | public void validateMissingContentLength() throws Exception { 40 | // We don't set a content length header 41 | // servletRequest.addHeader(HttpHeader.CONTENT_LENGTH, 3L); 42 | 43 | // When we validate the request 44 | try { 45 | validator.validate(HttpMethod.POST, servletRequest, null, null); 46 | } catch (Exception ex) { 47 | fail(); 48 | } 49 | 50 | // No Exception is thrown 51 | } 52 | 53 | @Test 54 | public void validateContentLengthZero() throws Exception { 55 | servletRequest.addHeader(HttpHeader.CONTENT_LENGTH, 0L); 56 | 57 | // When we validate the request 58 | try { 59 | validator.validate(HttpMethod.POST, servletRequest, null, null); 60 | } catch (Exception ex) { 61 | fail(); 62 | } 63 | 64 | // No Exception is thrown 65 | } 66 | 67 | @Test(expected = InvalidContentLengthException.class) 68 | public void validateContentLengthNotZero() throws Exception { 69 | servletRequest.addHeader(HttpHeader.CONTENT_LENGTH, 10L); 70 | 71 | // When we validate the request 72 | validator.validate(HttpMethod.POST, servletRequest, null, null); 73 | 74 | // Expect a InvalidContentLengthException 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/creation/validation/PostUriValidatorTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.creation.validation; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.junit.Assert.fail; 6 | import static org.mockito.Mockito.when; 7 | 8 | import me.desair.tus.server.HttpMethod; 9 | import me.desair.tus.server.exception.PostOnInvalidRequestURIException; 10 | import me.desair.tus.server.upload.UploadStorageService; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.mockito.Mock; 15 | import org.mockito.junit.MockitoJUnitRunner; 16 | import org.springframework.mock.web.MockHttpServletRequest; 17 | 18 | @RunWith(MockitoJUnitRunner.Silent.class) 19 | public class PostUriValidatorTest { 20 | 21 | private PostUriValidator validator; 22 | 23 | private MockHttpServletRequest servletRequest; 24 | 25 | @Mock private UploadStorageService uploadStorageService; 26 | 27 | @Before 28 | public void setUp() { 29 | servletRequest = new MockHttpServletRequest(); 30 | validator = new PostUriValidator(); 31 | } 32 | 33 | @Test 34 | public void supports() throws Exception { 35 | assertThat(validator.supports(HttpMethod.GET), is(false)); 36 | assertThat(validator.supports(HttpMethod.POST), is(true)); 37 | assertThat(validator.supports(HttpMethod.PUT), is(false)); 38 | assertThat(validator.supports(HttpMethod.DELETE), is(false)); 39 | assertThat(validator.supports(HttpMethod.HEAD), is(false)); 40 | assertThat(validator.supports(HttpMethod.OPTIONS), is(false)); 41 | assertThat(validator.supports(HttpMethod.PATCH), is(false)); 42 | assertThat(validator.supports(null), is(false)); 43 | } 44 | 45 | @Test 46 | public void validateMatchingUrl() throws Exception { 47 | servletRequest.setRequestURI("/test/upload"); 48 | when(uploadStorageService.getUploadUri()).thenReturn("/test/upload"); 49 | 50 | try { 51 | validator.validate(HttpMethod.POST, servletRequest, uploadStorageService, null); 52 | } catch (Exception ex) { 53 | fail(); 54 | } 55 | 56 | // No Exception is thrown 57 | } 58 | 59 | @Test(expected = PostOnInvalidRequestURIException.class) 60 | public void validateInvalidUrl() throws Exception { 61 | servletRequest.setRequestURI("/test/upload/12"); 62 | when(uploadStorageService.getUploadUri()).thenReturn("/test/upload"); 63 | 64 | validator.validate(HttpMethod.POST, servletRequest, uploadStorageService, null); 65 | 66 | // Expect PostOnInvalidRequestURIException 67 | } 68 | 69 | @Test 70 | public void validateMatchingRegexUrl() throws Exception { 71 | servletRequest.setRequestURI("/users/1234/files/upload"); 72 | when(uploadStorageService.getUploadUri()).thenReturn("/users/[0-9]+/files/upload"); 73 | 74 | try { 75 | validator.validate(HttpMethod.POST, servletRequest, uploadStorageService, null); 76 | } catch (Exception ex) { 77 | fail(); 78 | } 79 | 80 | // No Exception is thrown 81 | } 82 | 83 | @Test(expected = PostOnInvalidRequestURIException.class) 84 | public void validateInvalidRegexUrl() throws Exception { 85 | servletRequest.setRequestURI("/users/abc123/files/upload"); 86 | when(uploadStorageService.getUploadUri()).thenReturn("/users/[0-9]+/files/upload"); 87 | 88 | validator.validate(HttpMethod.POST, servletRequest, uploadStorageService, null); 89 | 90 | // Expect PostOnInvalidRequestURIException 91 | } 92 | 93 | @Test(expected = PostOnInvalidRequestURIException.class) 94 | public void validateInvalidRegexUrlPatchUrl() throws Exception { 95 | servletRequest.setRequestURI("/users/1234/files/upload/7669c72a-3f2a-451f-a3b9-9210e7a4c02f"); 96 | when(uploadStorageService.getUploadUri()).thenReturn("/users/[0-9]+/files/upload"); 97 | 98 | validator.validate(HttpMethod.POST, servletRequest, uploadStorageService, null); 99 | 100 | // Expect PostOnInvalidRequestURIException 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/creation/validation/UploadLengthValidatorTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.creation.validation; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.junit.Assert.fail; 6 | import static org.mockito.Mockito.when; 7 | 8 | import me.desair.tus.server.HttpHeader; 9 | import me.desair.tus.server.HttpMethod; 10 | import me.desair.tus.server.exception.MaxUploadLengthExceededException; 11 | import me.desair.tus.server.upload.UploadStorageService; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.mockito.Mock; 16 | import org.mockito.junit.MockitoJUnitRunner; 17 | import org.springframework.mock.web.MockHttpServletRequest; 18 | 19 | @RunWith(MockitoJUnitRunner.Silent.class) 20 | public class UploadLengthValidatorTest { 21 | 22 | private UploadLengthValidator validator; 23 | 24 | private MockHttpServletRequest servletRequest; 25 | 26 | @Mock private UploadStorageService uploadStorageService; 27 | 28 | @Before 29 | public void setUp() { 30 | servletRequest = new MockHttpServletRequest(); 31 | validator = new UploadLengthValidator(); 32 | } 33 | 34 | @Test 35 | public void supports() throws Exception { 36 | assertThat(validator.supports(HttpMethod.GET), is(false)); 37 | assertThat(validator.supports(HttpMethod.POST), is(true)); 38 | assertThat(validator.supports(HttpMethod.PUT), is(false)); 39 | assertThat(validator.supports(HttpMethod.DELETE), is(false)); 40 | assertThat(validator.supports(HttpMethod.HEAD), is(false)); 41 | assertThat(validator.supports(HttpMethod.OPTIONS), is(false)); 42 | assertThat(validator.supports(HttpMethod.PATCH), is(false)); 43 | assertThat(validator.supports(null), is(false)); 44 | } 45 | 46 | @Test 47 | public void validateNoMaxUploadLength() throws Exception { 48 | when(uploadStorageService.getMaxUploadSize()).thenReturn(0L); 49 | servletRequest.addHeader(HttpHeader.UPLOAD_LENGTH, 300L); 50 | 51 | try { 52 | validator.validate(HttpMethod.POST, servletRequest, uploadStorageService, null); 53 | } catch (Exception ex) { 54 | fail(); 55 | } 56 | 57 | // No Exception is thrown 58 | } 59 | 60 | @Test 61 | public void validateBelowMaxUploadLength() throws Exception { 62 | when(uploadStorageService.getMaxUploadSize()).thenReturn(400L); 63 | servletRequest.addHeader(HttpHeader.UPLOAD_LENGTH, 300L); 64 | 65 | try { 66 | validator.validate(HttpMethod.POST, servletRequest, uploadStorageService, null); 67 | } catch (Exception ex) { 68 | fail(); 69 | } 70 | 71 | // No Exception is thrown 72 | } 73 | 74 | @Test 75 | public void validateEqualMaxUploadLength() throws Exception { 76 | when(uploadStorageService.getMaxUploadSize()).thenReturn(300L); 77 | servletRequest.addHeader(HttpHeader.UPLOAD_LENGTH, 300L); 78 | 79 | try { 80 | validator.validate(HttpMethod.POST, servletRequest, uploadStorageService, null); 81 | } catch (Exception ex) { 82 | fail(); 83 | } 84 | 85 | // No Exception is thrown 86 | } 87 | 88 | @Test 89 | public void validateNoUploadLength() throws Exception { 90 | when(uploadStorageService.getMaxUploadSize()).thenReturn(300L); 91 | // servletRequest.addHeader(HttpHeader.UPLOAD_LENGTH, 300L); 92 | 93 | try { 94 | validator.validate(HttpMethod.POST, servletRequest, uploadStorageService, null); 95 | } catch (Exception ex) { 96 | fail(); 97 | } 98 | 99 | // No Exception is thrown 100 | } 101 | 102 | @Test(expected = MaxUploadLengthExceededException.class) 103 | public void validateAboveMaxUploadLength() throws Exception { 104 | when(uploadStorageService.getMaxUploadSize()).thenReturn(200L); 105 | servletRequest.addHeader(HttpHeader.UPLOAD_LENGTH, 300L); 106 | 107 | validator.validate(HttpMethod.POST, servletRequest, uploadStorageService, null); 108 | 109 | // Expect a MaxUploadLengthExceededException 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/download/DownloadOptionsRequestHandlerTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.download; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.containsInAnyOrder; 6 | 7 | import java.util.Arrays; 8 | import me.desair.tus.server.HttpHeader; 9 | import me.desair.tus.server.HttpMethod; 10 | import me.desair.tus.server.util.TusServletRequest; 11 | import me.desair.tus.server.util.TusServletResponse; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.springframework.mock.web.MockHttpServletRequest; 15 | import org.springframework.mock.web.MockHttpServletResponse; 16 | 17 | public class DownloadOptionsRequestHandlerTest { 18 | 19 | private DownloadOptionsRequestHandler handler; 20 | 21 | private MockHttpServletRequest servletRequest; 22 | 23 | private MockHttpServletResponse servletResponse; 24 | 25 | @Before 26 | public void setUp() { 27 | servletRequest = new MockHttpServletRequest(); 28 | servletResponse = new MockHttpServletResponse(); 29 | handler = new DownloadOptionsRequestHandler(); 30 | } 31 | 32 | @Test 33 | public void processListExtensions() throws Exception { 34 | 35 | handler.process( 36 | HttpMethod.OPTIONS, 37 | new TusServletRequest(servletRequest), 38 | new TusServletResponse(servletResponse), 39 | null, 40 | null); 41 | 42 | assertThat( 43 | Arrays.asList(servletResponse.getHeader(HttpHeader.TUS_EXTENSION).split(",")), 44 | containsInAnyOrder("download")); 45 | } 46 | 47 | @Test 48 | public void supports() throws Exception { 49 | assertThat(handler.supports(HttpMethod.GET), is(false)); 50 | assertThat(handler.supports(HttpMethod.POST), is(false)); 51 | assertThat(handler.supports(HttpMethod.PUT), is(false)); 52 | assertThat(handler.supports(HttpMethod.DELETE), is(false)); 53 | assertThat(handler.supports(HttpMethod.HEAD), is(false)); 54 | assertThat(handler.supports(HttpMethod.OPTIONS), is(true)); 55 | assertThat(handler.supports(HttpMethod.PATCH), is(false)); 56 | assertThat(handler.supports(null), is(false)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/expiration/ExpirationOptionsRequestHandlerTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.expiration; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.containsInAnyOrder; 6 | 7 | import java.util.Arrays; 8 | import me.desair.tus.server.HttpHeader; 9 | import me.desair.tus.server.HttpMethod; 10 | import me.desair.tus.server.util.TusServletRequest; 11 | import me.desair.tus.server.util.TusServletResponse; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.springframework.mock.web.MockHttpServletRequest; 15 | import org.springframework.mock.web.MockHttpServletResponse; 16 | 17 | public class ExpirationOptionsRequestHandlerTest { 18 | 19 | private ExpirationOptionsRequestHandler handler; 20 | 21 | private MockHttpServletRequest servletRequest; 22 | 23 | private MockHttpServletResponse servletResponse; 24 | 25 | @Before 26 | public void setUp() { 27 | servletRequest = new MockHttpServletRequest(); 28 | servletResponse = new MockHttpServletResponse(); 29 | handler = new ExpirationOptionsRequestHandler(); 30 | } 31 | 32 | @Test 33 | public void processListExtensions() throws Exception { 34 | 35 | handler.process( 36 | HttpMethod.OPTIONS, 37 | new TusServletRequest(servletRequest), 38 | new TusServletResponse(servletResponse), 39 | null, 40 | null); 41 | 42 | assertThat( 43 | Arrays.asList(servletResponse.getHeader(HttpHeader.TUS_EXTENSION).split(",")), 44 | containsInAnyOrder("expiration")); 45 | } 46 | 47 | @Test 48 | public void supports() throws Exception { 49 | assertThat(handler.supports(HttpMethod.GET), is(false)); 50 | assertThat(handler.supports(HttpMethod.POST), is(false)); 51 | assertThat(handler.supports(HttpMethod.PUT), is(false)); 52 | assertThat(handler.supports(HttpMethod.DELETE), is(false)); 53 | assertThat(handler.supports(HttpMethod.HEAD), is(false)); 54 | assertThat(handler.supports(HttpMethod.OPTIONS), is(true)); 55 | assertThat(handler.supports(HttpMethod.PATCH), is(false)); 56 | assertThat(handler.supports(null), is(false)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/termination/TerminationDeleteRequestHandlerTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.termination; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.mockito.ArgumentMatchers.any; 6 | import static org.mockito.ArgumentMatchers.nullable; 7 | import static org.mockito.Mockito.never; 8 | import static org.mockito.Mockito.times; 9 | import static org.mockito.Mockito.verify; 10 | import static org.mockito.Mockito.when; 11 | 12 | import jakarta.servlet.http.HttpServletResponse; 13 | import java.util.UUID; 14 | import me.desair.tus.server.HttpMethod; 15 | import me.desair.tus.server.upload.UploadId; 16 | import me.desair.tus.server.upload.UploadInfo; 17 | import me.desair.tus.server.upload.UploadStorageService; 18 | import me.desair.tus.server.util.TusServletRequest; 19 | import me.desair.tus.server.util.TusServletResponse; 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | import org.junit.runner.RunWith; 23 | import org.mockito.Mock; 24 | import org.mockito.junit.MockitoJUnitRunner; 25 | import org.springframework.mock.web.MockHttpServletRequest; 26 | import org.springframework.mock.web.MockHttpServletResponse; 27 | 28 | @RunWith(MockitoJUnitRunner.Silent.class) 29 | public class TerminationDeleteRequestHandlerTest { 30 | 31 | private TerminationDeleteRequestHandler handler; 32 | 33 | private MockHttpServletRequest servletRequest; 34 | 35 | private MockHttpServletResponse servletResponse; 36 | 37 | @Mock private UploadStorageService uploadStorageService; 38 | 39 | @Before 40 | public void setUp() { 41 | servletRequest = new MockHttpServletRequest(); 42 | servletResponse = new MockHttpServletResponse(); 43 | handler = new TerminationDeleteRequestHandler(); 44 | } 45 | 46 | @Test 47 | public void supports() throws Exception { 48 | assertThat(handler.supports(HttpMethod.GET), is(false)); 49 | assertThat(handler.supports(HttpMethod.POST), is(false)); 50 | assertThat(handler.supports(HttpMethod.PUT), is(false)); 51 | assertThat(handler.supports(HttpMethod.DELETE), is(true)); 52 | assertThat(handler.supports(HttpMethod.HEAD), is(false)); 53 | assertThat(handler.supports(HttpMethod.OPTIONS), is(false)); 54 | assertThat(handler.supports(HttpMethod.PATCH), is(false)); 55 | assertThat(handler.supports(null), is(false)); 56 | } 57 | 58 | @Test 59 | public void testWithNotExistingUpload() throws Exception { 60 | when(uploadStorageService.getUploadInfo(nullable(String.class), nullable(String.class))) 61 | .thenReturn(null); 62 | 63 | handler.process( 64 | HttpMethod.DELETE, 65 | new TusServletRequest(servletRequest), 66 | new TusServletResponse(servletResponse), 67 | uploadStorageService, 68 | null); 69 | 70 | verify(uploadStorageService, never()).terminateUpload(any(UploadInfo.class)); 71 | assertThat(servletResponse.getStatus(), is(HttpServletResponse.SC_NO_CONTENT)); 72 | } 73 | 74 | @Test 75 | public void testWithExistingUpload() throws Exception { 76 | final UploadId id = new UploadId(UUID.randomUUID()); 77 | 78 | UploadInfo info = new UploadInfo(); 79 | info.setId(id); 80 | info.setOffset(2L); 81 | info.setLength(10L); 82 | when(uploadStorageService.getUploadInfo(nullable(String.class), nullable(String.class))) 83 | .thenReturn(info); 84 | 85 | handler.process( 86 | HttpMethod.DELETE, 87 | new TusServletRequest(servletRequest), 88 | new TusServletResponse(servletResponse), 89 | uploadStorageService, 90 | null); 91 | 92 | verify(uploadStorageService, times(1)).terminateUpload(info); 93 | assertThat(servletResponse.getStatus(), is(HttpServletResponse.SC_NO_CONTENT)); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/upload/TimeBasedUploadIdFactoryTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.upload; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.greaterThan; 5 | import static org.hamcrest.Matchers.hasToString; 6 | import static org.hamcrest.Matchers.is; 7 | import static org.hamcrest.Matchers.lessThan; 8 | import static org.hamcrest.Matchers.not; 9 | import static org.hamcrest.Matchers.nullValue; 10 | 11 | import me.desair.tus.server.util.Utils; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | 15 | public class TimeBasedUploadIdFactoryTest { 16 | 17 | private UploadIdFactory idFactory; 18 | 19 | @Before 20 | public void setUp() { 21 | idFactory = new TimeBasedUploadIdFactory(); 22 | } 23 | 24 | @Test(expected = NullPointerException.class) 25 | public void setUploadUriNull() throws Exception { 26 | idFactory.setUploadUri(null); 27 | } 28 | 29 | @Test 30 | public void setUploadUriNoTrailingSlash() throws Exception { 31 | idFactory.setUploadUri("/test/upload"); 32 | assertThat(idFactory.getUploadUri(), is("/test/upload")); 33 | } 34 | 35 | @Test 36 | public void setUploadUriWithTrailingSlash() throws Exception { 37 | idFactory.setUploadUri("/test/upload/"); 38 | assertThat(idFactory.getUploadUri(), is("/test/upload/")); 39 | } 40 | 41 | @Test(expected = IllegalArgumentException.class) 42 | public void setUploadUriBlank() throws Exception { 43 | idFactory.setUploadUri(" "); 44 | } 45 | 46 | @Test(expected = IllegalArgumentException.class) 47 | public void setUploadUriNoStartingSlash() throws Exception { 48 | idFactory.setUploadUri("test/upload/"); 49 | } 50 | 51 | @Test(expected = IllegalArgumentException.class) 52 | public void setUploadUriEndsWithDollar() throws Exception { 53 | idFactory.setUploadUri("/test/upload$"); 54 | } 55 | 56 | @Test 57 | public void readUploadId() throws Exception { 58 | idFactory.setUploadUri("/test/upload"); 59 | 60 | assertThat(idFactory.readUploadId("/test/upload/1546152320043"), hasToString("1546152320043")); 61 | } 62 | 63 | @Test 64 | public void readUploadIdRegex() throws Exception { 65 | idFactory.setUploadUri("/users/[0-9]+/files/upload"); 66 | 67 | assertThat( 68 | idFactory.readUploadId("/users/1337/files/upload/1546152320043"), 69 | hasToString("1546152320043")); 70 | } 71 | 72 | @Test 73 | public void readUploadIdTrailingSlash() throws Exception { 74 | idFactory.setUploadUri("/test/upload/"); 75 | 76 | assertThat(idFactory.readUploadId("/test/upload/1546152320043"), hasToString("1546152320043")); 77 | } 78 | 79 | @Test 80 | public void readUploadIdRegexTrailingSlash() throws Exception { 81 | idFactory.setUploadUri("/users/[0-9]+/files/upload/"); 82 | 83 | assertThat( 84 | idFactory.readUploadId("/users/123456789/files/upload/1546152320043"), 85 | hasToString("1546152320043")); 86 | } 87 | 88 | @Test 89 | public void readUploadIdNoUuid() throws Exception { 90 | idFactory.setUploadUri("/test/upload"); 91 | 92 | assertThat(idFactory.readUploadId("/test/upload/not-a-time-value"), is(nullValue())); 93 | } 94 | 95 | @Test 96 | public void readUploadIdRegexNoMatch() throws Exception { 97 | idFactory.setUploadUri("/users/[0-9]+/files/upload"); 98 | 99 | assertThat(idFactory.readUploadId("/users/files/upload/1546152320043"), is(nullValue())); 100 | } 101 | 102 | @Test 103 | public void createId() throws Exception { 104 | UploadId id = idFactory.createId(); 105 | assertThat(id, not(nullValue())); 106 | Utils.sleep(10); 107 | assertThat( 108 | Long.parseLong(id.getOriginalObject().toString()), 109 | greaterThan(System.currentTimeMillis() - 1000L)); 110 | assertThat( 111 | Long.parseLong(id.getOriginalObject().toString()), lessThan(System.currentTimeMillis())); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/upload/UploadIdTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.upload; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertNotEquals; 5 | 6 | import java.util.UUID; 7 | import org.junit.Test; 8 | 9 | /** Test class for the UploadId class. */ 10 | public class UploadIdTest { 11 | 12 | @Test 13 | public void getOriginalObjectUuid() { 14 | UUID id = UUID.randomUUID(); 15 | UploadId uploadId = new UploadId(id); 16 | assertEquals(id.toString(), uploadId.toString()); 17 | assertEquals(id, uploadId.getOriginalObject()); 18 | } 19 | 20 | @Test 21 | public void getOriginalObjectLong() { 22 | UploadId uploadId = new UploadId(1337L); 23 | assertEquals("1337", uploadId.toString()); 24 | assertEquals(1337L, uploadId.getOriginalObject()); 25 | } 26 | 27 | @Test(expected = NullPointerException.class) 28 | public void testNullConstructor() { 29 | new UploadId(null); 30 | } 31 | 32 | @Test(expected = IllegalArgumentException.class) 33 | public void testBlankConstructor() { 34 | new UploadId(" \t"); 35 | } 36 | 37 | @Test 38 | public void toStringNotYetUrlSafe() { 39 | UploadId uploadId = new UploadId("my test id/1"); 40 | assertEquals("my+test+id%2F1", uploadId.toString()); 41 | } 42 | 43 | @Test 44 | public void toStringNotYetUrlSafe2() { 45 | UploadId uploadId = new UploadId("id+%2F1+/+1"); 46 | assertEquals("id+%2F1+/+1", uploadId.toString()); 47 | } 48 | 49 | @Test 50 | public void toStringAlreadyUrlSafe() { 51 | UploadId uploadId = new UploadId("my+test+id%2F1"); 52 | assertEquals("my+test+id%2F1", uploadId.toString()); 53 | } 54 | 55 | @Test 56 | public void toStringWithInternalDecoderException() { 57 | String test = "Invalid % value"; 58 | UploadId id = new UploadId(test); 59 | assertEquals("Invalid % value", id.toString()); 60 | } 61 | 62 | @Test 63 | public void equalsSameUrlSafeValue() { 64 | UploadId id1 = new UploadId("id%2F1"); 65 | UploadId id2 = new UploadId("id/1"); 66 | UploadId id3 = new UploadId("id/1"); 67 | 68 | assertEquals(id1, id2); 69 | assertEquals(id2, id3); 70 | assertEquals(id1, id1); 71 | assertNotEquals(id1, null); 72 | assertNotEquals(id1, UUID.randomUUID()); 73 | } 74 | 75 | @Test 76 | public void hashCodeSameUrlSafeValue() { 77 | UploadId id1 = new UploadId("id%2F1"); 78 | UploadId id2 = new UploadId("id/1"); 79 | UploadId id3 = new UploadId("id/1"); 80 | 81 | assertEquals(id1.hashCode(), id2.hashCode()); 82 | assertEquals(id2.hashCode(), id3.hashCode()); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/upload/UuidUploadIdFactoryTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.upload; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.hasToString; 5 | import static org.hamcrest.Matchers.is; 6 | import static org.hamcrest.Matchers.not; 7 | import static org.hamcrest.Matchers.nullValue; 8 | 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | 12 | /** Test cases for the UuidUploadIdFactory. */ 13 | public class UuidUploadIdFactoryTest { 14 | 15 | private UploadIdFactory idFactory; 16 | 17 | @Before 18 | public void setUp() { 19 | idFactory = new UuidUploadIdFactory(); 20 | } 21 | 22 | @Test(expected = NullPointerException.class) 23 | public void setUploadUriNull() throws Exception { 24 | idFactory.setUploadUri(null); 25 | } 26 | 27 | @Test 28 | public void setUploadUriNoTrailingSlash() throws Exception { 29 | idFactory.setUploadUri("/test/upload"); 30 | assertThat(idFactory.getUploadUri(), is("/test/upload")); 31 | } 32 | 33 | @Test 34 | public void setUploadUriWithTrailingSlash() throws Exception { 35 | idFactory.setUploadUri("/test/upload/"); 36 | assertThat(idFactory.getUploadUri(), is("/test/upload/")); 37 | } 38 | 39 | @Test(expected = IllegalArgumentException.class) 40 | public void setUploadUriBlank() throws Exception { 41 | idFactory.setUploadUri(" "); 42 | } 43 | 44 | @Test(expected = IllegalArgumentException.class) 45 | public void setUploadUriNoStartingSlash() throws Exception { 46 | idFactory.setUploadUri("test/upload/"); 47 | } 48 | 49 | @Test(expected = IllegalArgumentException.class) 50 | public void setUploadUriEndsWithDollar() throws Exception { 51 | idFactory.setUploadUri("/test/upload$"); 52 | } 53 | 54 | @Test 55 | public void readUploadId() throws Exception { 56 | idFactory.setUploadUri("/test/upload"); 57 | 58 | assertThat( 59 | idFactory.readUploadId("/test/upload/1911e8a4-6939-490c-b58b-a5d70f8d91fb"), 60 | hasToString("1911e8a4-6939-490c-b58b-a5d70f8d91fb")); 61 | } 62 | 63 | @Test 64 | public void readUploadIdRegex() throws Exception { 65 | idFactory.setUploadUri("/users/[0-9]+/files/upload"); 66 | 67 | assertThat( 68 | idFactory.readUploadId("/users/1337/files/upload/1911e8a4-6939-490c-b58b-a5d70f8d91fb"), 69 | hasToString("1911e8a4-6939-490c-b58b-a5d70f8d91fb")); 70 | } 71 | 72 | @Test 73 | public void readUploadIdTrailingSlash() throws Exception { 74 | idFactory.setUploadUri("/test/upload/"); 75 | 76 | assertThat( 77 | idFactory.readUploadId("/test/upload/1911e8a4-6939-490c-b58b-a5d70f8d91fb"), 78 | hasToString("1911e8a4-6939-490c-b58b-a5d70f8d91fb")); 79 | } 80 | 81 | @Test 82 | public void readUploadIdRegexTrailingSlash() throws Exception { 83 | idFactory.setUploadUri("/users/[0-9]+/files/upload/"); 84 | 85 | assertThat( 86 | idFactory.readUploadId( 87 | "/users/123456789/files/upload/1911e8a4-6939-490c-b58b-a5d70f8d91fb"), 88 | hasToString("1911e8a4-6939-490c-b58b-a5d70f8d91fb")); 89 | } 90 | 91 | @Test 92 | public void readUploadIdNoUuid() throws Exception { 93 | idFactory.setUploadUri("/test/upload"); 94 | 95 | assertThat(idFactory.readUploadId("/test/upload/not-a-uuid-value"), is(nullValue())); 96 | } 97 | 98 | @Test 99 | public void readUploadIdRegexNoMatch() throws Exception { 100 | idFactory.setUploadUri("/users/[0-9]+/files/upload"); 101 | 102 | assertThat( 103 | idFactory.readUploadId("/users/files/upload/1911e8a4-6939-490c-b58b-a5d70f8d91fb"), 104 | is(nullValue())); 105 | } 106 | 107 | @Test 108 | public void createId() throws Exception { 109 | assertThat(idFactory.createId(), not(nullValue())); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/upload/disk/FileBasedLockTest.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.upload.disk; 2 | 3 | import static org.junit.Assert.assertFalse; 4 | import static org.junit.Assert.fail; 5 | import static org.mockito.Mockito.anyBoolean; 6 | import static org.mockito.Mockito.anyLong; 7 | import static org.mockito.Mockito.doReturn; 8 | import static org.mockito.Mockito.spy; 9 | 10 | import java.io.IOException; 11 | import java.nio.channels.FileChannel; 12 | import java.nio.file.Files; 13 | import java.nio.file.Path; 14 | import java.nio.file.Paths; 15 | import java.util.UUID; 16 | import me.desair.tus.server.exception.UploadAlreadyLockedException; 17 | import org.junit.BeforeClass; 18 | import org.junit.Test; 19 | 20 | public class FileBasedLockTest { 21 | 22 | private static Path storagePath; 23 | 24 | @BeforeClass 25 | public static void setupDataFolder() throws IOException { 26 | storagePath = Paths.get("target", "tus", "locks").toAbsolutePath(); 27 | Files.createDirectories(storagePath); 28 | } 29 | 30 | @Test 31 | public void testLockRelease() throws UploadAlreadyLockedException, IOException { 32 | UUID test = UUID.randomUUID(); 33 | FileBasedLock lock = 34 | new FileBasedLock("/test/upload/" + test.toString(), storagePath.resolve(test.toString())); 35 | lock.close(); 36 | assertFalse(Files.exists(storagePath.resolve(test.toString()))); 37 | } 38 | 39 | @Test(expected = UploadAlreadyLockedException.class) 40 | public void testOverlappingLock() throws Exception { 41 | UUID test = UUID.randomUUID(); 42 | Path path = storagePath.resolve(test.toString()); 43 | try (FileBasedLock lock1 = new FileBasedLock("/test/upload/" + test.toString(), path)) { 44 | FileBasedLock lock2 = new FileBasedLock("/test/upload/" + test.toString(), path); 45 | lock2.close(); 46 | } 47 | } 48 | 49 | @Test(expected = UploadAlreadyLockedException.class) 50 | public void testAlreadyLocked() throws Exception { 51 | UUID test1 = UUID.randomUUID(); 52 | Path path1 = storagePath.resolve(test1.toString()); 53 | try (FileBasedLock lock1 = new FileBasedLock("/test/upload/" + test1.toString(), path1)) { 54 | FileBasedLock lock2 = 55 | new FileBasedLock("/test/upload/" + test1.toString(), path1) { 56 | @Override 57 | protected FileChannel createFileChannel() throws IOException { 58 | FileChannel channel = createFileChannelMock(); 59 | doReturn(null).when(channel).tryLock(anyLong(), anyLong(), anyBoolean()); 60 | return channel; 61 | } 62 | }; 63 | lock2.close(); 64 | } 65 | } 66 | 67 | @Test 68 | public void testLockReleaseLockRelease() throws UploadAlreadyLockedException, IOException { 69 | UUID test = UUID.randomUUID(); 70 | Path path = storagePath.resolve(test.toString()); 71 | FileBasedLock lock = new FileBasedLock("/test/upload/" + test.toString(), path); 72 | lock.close(); 73 | assertFalse(Files.exists(path)); 74 | lock = new FileBasedLock("/test/upload/" + test.toString(), path); 75 | lock.close(); 76 | assertFalse(Files.exists(path)); 77 | } 78 | 79 | @Test(expected = IOException.class) 80 | public void testLockIoException() throws UploadAlreadyLockedException, IOException { 81 | // Create directory on place where lock file will be 82 | UUID test = UUID.randomUUID(); 83 | Path path = storagePath.resolve(test.toString()); 84 | try { 85 | Files.createDirectories(path); 86 | } catch (IOException e) { 87 | fail(); 88 | } 89 | 90 | FileBasedLock lock = new FileBasedLock("/test/upload/" + test.toString(), path); 91 | lock.close(); 92 | } 93 | 94 | private FileChannel createFileChannelMock() throws IOException { 95 | return spy(FileChannel.class); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/test/java/me/desair/tus/server/util/MapMatcher.java: -------------------------------------------------------------------------------- 1 | package me.desair.tus.server.util; 2 | 3 | import java.util.Map; 4 | import org.hamcrest.Description; 5 | import org.hamcrest.Matcher; 6 | import org.hamcrest.TypeSafeMatcher; 7 | 8 | /** Custom Matcher class used in tests */ 9 | public class MapMatcher { 10 | 11 | private MapMatcher() {} 12 | 13 | public static Matcher> hasSize(final int size) { 14 | return new TypeSafeMatcher>() { 15 | @Override 16 | public boolean matchesSafely(Map kvMap) { 17 | return kvMap.size() == size; 18 | } 19 | 20 | @Override 21 | public void describeTo(Description description) { 22 | description.appendText(" has ").appendValue(size).appendText(" key/value pairs"); 23 | } 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | # SLF4J's SimpleLogger configuration file 2 | # Simple implementation of Logger that sends all enabled log messages, for all defined loggers, to System.err. 3 | 4 | # Default logging detail level for all instances of SimpleLogger. 5 | # Must be one of ("trace", "debug", "info", "warn", or "error"). 6 | # If not specified, defaults to "info". 7 | org.slf4j.simpleLogger.defaultLogLevel=info 8 | 9 | # Logging detail level for a SimpleLogger instance named "xxxxx". 10 | # Must be one of ("trace", "debug", "info", "warn", or "error"). 11 | # If not specified, the default logging detail level is used. 12 | #org.slf4j.simpleLogger.log.xxxxx= 13 | 14 | # Set to true if you want the current date and time to be included in output messages. 15 | # Default is false, and will output the number of milliseconds elapsed since startup. 16 | org.slf4j.simpleLogger.showDateTime=true 17 | 18 | # The date and time format to be used in the output messages. 19 | # The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat. 20 | # If the format is not specified or is invalid, the default format is used. 21 | # The default format is yyyy-MM-dd HH:mm:ss:SSS Z. 22 | #org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS Z 23 | 24 | # Set to true if you want to output the current thread name. 25 | # Defaults to true. 26 | #org.slf4j.simpleLogger.showThreadName=true 27 | 28 | # Set to true if you want the Logger instance name to be included in output messages. 29 | # Defaults to true. 30 | org.slf4j.simpleLogger.showLogName=true 31 | 32 | # Set to true if you want the last component of the name to be included in output messages. 33 | # Defaults to false. 34 | org.slf4j.simpleLogger.showShortLogName=true --------------------------------------------------------------------------------