├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── dependabot.yaml ├── linters │ └── sun_checks.xml └── workflows │ ├── lintChanges.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .well-known └── funding-manifest-urls ├── LICENSE ├── README.md ├── build.gradle ├── example ├── assets │ └── prairie.jpg ├── build.gradle └── src │ └── main │ └── java │ └── io │ └── tus │ └── java │ └── example │ ├── Main.java │ └── package-info.java ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── io │ │ └── tus │ │ └── java │ │ └── client │ │ ├── FingerprintNotFoundException.java │ │ ├── ProtocolException.java │ │ ├── ResumingNotEnabledException.java │ │ ├── TusClient.java │ │ ├── TusExecutor.java │ │ ├── TusInputStream.java │ │ ├── TusURLMemoryStore.java │ │ ├── TusURLStore.java │ │ ├── TusUpload.java │ │ ├── TusUploader.java │ │ └── package-info.java └── resources │ └── tus-java-client-version │ └── version.properties └── test └── java └── io └── tus └── java └── client ├── MockServerProvider.java ├── TestTusClient.java ├── TestTusExecutor.java ├── TestTusURLMemoryStore.java ├── TestTusUpload.java ├── TestTusUploader.java └── package-info.java /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug or incorrect behavior 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Execute command '...' 16 | 2. Start upload '...' 17 | 3. See error 18 | 19 | Please try to reproduce the problem when uploading to our public tus instance at [https://tusd.tusdemo.net/files/](https://tusd.tusdemo.net/files/), if applicable to your situation. This helps us to determine whether the problem may be caused by the server component. 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Setup details** 25 | Please provide following details, if applicable to your situation: 26 | - Runtime environment: [e.g. Android, Java version, etc.] 27 | - Used tus-java-client version: [e.g. v0.4.2] 28 | - Used tus server software: [e.g. tusd, tus-node-server etc.] 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea or improvement 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Can you provide help with implementing this feature?** 20 | The tus team is always happy to improve your situation but our time is also sadly limited, so it may take some time until we can work on your request. If you have the resources and knowledge to take a shot at your idea, we are more than eager to assist you. 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a general question about using tus-java-client 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Question** 11 | Please place your question here. 12 | 13 | **Setup details** 14 | Please provide following details, if applicable to your situation: 15 | - Runtime environment: [e.g. Android, Java version, etc.] 16 | - Used tus-java-client version: [e.g. v0.4.2] 17 | - Used tus server software: [e.g. tusd, tus-node-server etc.] 18 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | 8 | - package-ecosystem: gradle 9 | directory: / 10 | schedule: 11 | interval: weekly 12 | -------------------------------------------------------------------------------- /.github/linters/sun_checks.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 33 | 34 | 35 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 204 | 205 | 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /.github/workflows/lintChanges.yml: -------------------------------------------------------------------------------- 1 | name: Lint Java Code 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | types: 8 | - opened 9 | - synchronize 10 | - unlabeled 11 | jobs: 12 | Lint_Java: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repository 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Lint_Java 21 | uses: github/super-linter@v7 22 | env: 23 | VALIDATE_ALL_CODEBASE: true # lint all files 24 | VALIDATE_JAVA: true # only lint Java files 25 | DEFAULT_BRANCH: main 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Enables better overview of runs 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Maven Repository 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | publish_to_maven: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout Repository 11 | uses: actions/checkout@v4 12 | - name: Set up JDK 13 | uses: actions/setup-java@v4 14 | with: 15 | java-version: '17' 16 | distribution: 'adopt' 17 | - name: Grant execute permission for gradlew 18 | run: chmod +x gradlew 19 | - name: Publish package to Maven Repository 20 | run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository 21 | env: 22 | SONATYPE_USER: ${{ secrets.SONATYPE_USER }} 23 | SONATYPE_KEY: ${{ secrets.SONATYPE_KEY }} 24 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} 25 | SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} 26 | SIGNING_KEY_AMORED: ${{ secrets.SIGNING_KEY_AMORED }} 27 | SONATYPE_STAGING_PROFILE: ${{ secrets.SONATYPE_STAGING_PROFILE }} 28 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build and test a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Tests 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | types: 12 | - opened 13 | - synchronize 14 | - unlabeled 15 | jobs: 16 | Java: 17 | strategy: 18 | matrix: 19 | java: ['17','21'] 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Checkout Repository 25 | uses: actions/checkout@v4 26 | - name: Set up JDK 27 | uses: actions/setup-java@v4 28 | with: 29 | java-version: ${{ matrix.java }} 30 | distribution: 'adopt' 31 | - name: Grant execute permission for gradlew 32 | run: chmod +x gradlew 33 | - name: Build with Gradle 34 | run: ./gradlew assemble 35 | - name: Run Tests 36 | run: ./gradlew check 37 | 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.war 8 | *.ear 9 | 10 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 11 | hs_err_pid* 12 | 13 | # Eclipse and other IDEs 14 | .idea/ 15 | .eclipse/ 16 | .classpath 17 | .project 18 | .settings/ 19 | *.iml 20 | local.properties 21 | build/ 22 | 23 | # Gradle 24 | .gradle/ 25 | -------------------------------------------------------------------------------- /.well-known/funding-manifest-urls: -------------------------------------------------------------------------------- 1 | https://tus.io/funding.json 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 tus - Resumable File Uploads 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tus-java-client [![Tests](https://github.com/tus/tus-java-client/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/tus/tus-java-client/actions/workflows/tests.yml) 2 | 3 | > **tus** is a protocol based on HTTP for *resumable file uploads*. Resumable 4 | > means that an upload can be interrupted at any moment and can be resumed without 5 | > re-uploading the previous data again. An interruption may happen willingly, if 6 | > the user wants to pause, or by accident in case of a network issue or server 7 | > outage. 8 | 9 | **tus-java-client** is a library for uploading files using the *tus* protocol to any remote server supporting it. 10 | 11 | This library is also compatible with the Android platform and can be used without any modifications using the API. The [tus-android-client](https://github.com/tus/tus-android-client) provides additional classes which can be used in addition the Java library. 12 | 13 | ## Usage 14 | 15 | ```java 16 | // Create a new TusClient instance 17 | TusClient client = new TusClient(); 18 | 19 | // Configure tus HTTP endpoint. This URL will be used for creating new uploads 20 | // using the Creation extension 21 | client.setUploadCreationURL(new URL("https://tusd.tusdemo.net/files")); 22 | 23 | // Enable resumable uploads by storing the upload URL in memory 24 | client.enableResuming(new TusURLMemoryStore()); 25 | 26 | // Open a file using which we will then create a TusUpload. If you do not have 27 | // a File object, you can manually construct a TusUpload using an InputStream. 28 | // See the documentation for more information. 29 | File file = new File("./cute_kitten.png"); 30 | final TusUpload upload = new TusUpload(file); 31 | 32 | System.out.println("Starting upload..."); 33 | 34 | // We wrap our uploading code in the TusExecutor class which will automatically catch 35 | // exceptions and issue retries with small delays between them and take fully 36 | // advantage of tus' resumability to offer more reliability. 37 | // This step is optional but highly recommended. 38 | TusExecutor executor = new TusExecutor() { 39 | @Override 40 | protected void makeAttempt() throws ProtocolException, IOException { 41 | // First try to resume an upload. If that's not possible we will create a new 42 | // upload and get a TusUploader in return. This class is responsible for opening 43 | // a connection to the remote server and doing the uploading. 44 | TusUploader uploader = client.resumeOrCreateUpload(upload); 45 | 46 | // Alternatively, if your tus server does not support the Creation extension 47 | // and you obtained an upload URL from another service, you can instruct 48 | // tus-java-client to upload to a specific URL. Please note that this is usually 49 | // _not_ necessary and only if the tus server does not support the Creation 50 | // extension. The Vimeo API would be an example where this method is needed. 51 | // TusUploader uploader = client.beginOrResumeUploadFromURL(upload, new URL("https://tus.server.net/files/my_file")); 52 | 53 | // Upload the file in chunks of 1KB sizes. 54 | uploader.setChunkSize(1024); 55 | 56 | // Upload the file as long as data is available. Once the 57 | // file has been fully uploaded the method will return -1 58 | do { 59 | // Calculate the progress using the total size of the uploading file and 60 | // the current offset. 61 | long totalBytes = upload.getSize(); 62 | long bytesUploaded = uploader.getOffset(); 63 | double progress = (double) bytesUploaded / totalBytes * 100; 64 | 65 | System.out.printf("Upload at %06.2f%%.\n", progress); 66 | } while(uploader.uploadChunk() > -1); 67 | 68 | // Allow the HTTP connection to be closed and cleaned up 69 | uploader.finish(); 70 | 71 | System.out.println("Upload finished."); 72 | System.out.format("Upload available at: %s", uploader.getUploadURL().toString()); 73 | } 74 | }; 75 | executor.makeAttempts(); 76 | 77 | ``` 78 | 79 | ## Installation 80 | 81 | The JARs can be downloaded manually from our [Maven Central Repo](https://central.sonatype.com/namespace/io.tus.java.client). 82 | 83 | **Gradle:** 84 | 85 | ```groovy 86 | implementation 'io.tus.java.client:tus-java-client:0.5.1' 87 | ``` 88 | 89 | **Maven:** 90 | 91 | ```xml 92 | 93 | io.tus.java.client 94 | tus-java-client 95 | 0.5.1 96 | 97 | ``` 98 | 99 | ## Documentation 100 | 101 | The documentation of the latest versions can be found online at [javadoc.io](https://javadoc.io/doc/io.tus.java.client/tus-java-client). 102 | 103 | ## FAQ 104 | 105 | ### Can I use my own custom SSLSocketFactory? 106 | 107 | Yes, you can! Create a subclass of `TusClient` and override the `prepareConnection` method to attach your `SSLSocketFactory`. After this use your custom `TusClient` subclass as you would normally use it. Here is an example: 108 | 109 | ```java 110 | @Override 111 | public void prepareConnection(@NotNull HttpURLConnection connection) { 112 | super.prepareConnection(connection); 113 | 114 | if(connection instanceof HttpsURLConnection) { 115 | HttpsURLConnection secureConnection = (HttpsURLConnection) connection; 116 | secureConnection.setSSLSocketFactory(mySSLSocketFactory); 117 | } 118 | } 119 | ``` 120 | 121 | ### Can I use a proxy that will be used for uploading files? 122 | 123 | Yes, just add a proxy to the TusClient as shown below (1 line added to the above [usage](#usage)): 124 | 125 | ```java 126 | TusClient client = new TusClient(); 127 | Proxy myProxy = new Proxy(...); 128 | client.setProxy(myProxy); 129 | ``` 130 | 131 | ## License 132 | 133 | MIT 134 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'signing' 3 | id('io.github.gradle-nexus.publish-plugin') version '2.0.0' 4 | } 5 | 6 | apply plugin: 'java' 7 | apply plugin: 'maven-publish' 8 | 9 | group 'io.tus.java.client' 10 | 11 | allprojects { 12 | repositories { 13 | mavenCentral() 14 | } 15 | } 16 | 17 | // We compile the library using Java 1.7 compatibility 18 | // in order to ensure interoperability with older Android platforms. 19 | sourceCompatibility = 1.8 20 | targetCompatibility = 1.8 21 | 22 | // load version number from file 23 | def config = new ConfigSlurper().parse(new File("${projectDir}/src/main/resources/tus-java-client-version/version.properties").toURI().toURL()) 24 | version = config.versionNumber 25 | 26 | dependencies { 27 | implementation 'org.jetbrains:annotations:26.0.2' 28 | testImplementation 'junit:junit:4.13.2' 29 | testImplementation 'org.mock-server:mockserver-junit-rule:5.15.0' 30 | testImplementation 'org.mockito:mockito-core:5.18.0' 31 | } 32 | 33 | tasks.register('sourcesJar', Jar) { 34 | dependsOn classes 35 | archiveClassifier.set('sources') 36 | from sourceSets.main.allSource 37 | } 38 | 39 | tasks.register('javadocJar', Jar) { 40 | dependsOn javadoc 41 | archiveClassifier.set('javadoc') 42 | from javadoc.destinationDir 43 | } 44 | 45 | artifacts { 46 | archives sourcesJar, javadocJar 47 | } 48 | 49 | 50 | def pomConfig = { 51 | name 'tus-java-client' 52 | url 'https://tus.io' 53 | 54 | scm { 55 | url 'https://github.com/tus/tus-java-client' 56 | connection 'https://github.com/tus/tus-java-client' 57 | developerConnection 'https://github.com/tus/tus-java-client' 58 | } 59 | 60 | developers { 61 | developer { 62 | id 'acconut' 63 | name 'Marius Kleidl' 64 | email 'maerious@gmail.com' 65 | } 66 | developer { 67 | id 'cdr-chakotay' 68 | name 'Florian Kuenzig' 69 | email 'florian@transloadit.com' 70 | } 71 | } 72 | 73 | inceptionYear '2015' 74 | licenses { 75 | license { 76 | name 'The MIT License (MIT)' 77 | url 'http://opensource.org/licenses/MIT' 78 | } 79 | } 80 | } 81 | 82 | publishing { 83 | publications { 84 | mavenJava(MavenPublication) { 85 | from components.java 86 | groupId = 'io.tus.java.client' 87 | artifactId = 'tus-java-client' 88 | version project.getVersion() 89 | artifact sourcesJar 90 | artifact javadocJar 91 | 92 | pom.withXml { 93 | def root = asNode() 94 | root.appendNode('description', 'Java client for tus, the resumable file uploading protocol.') 95 | root.children().last() + pomConfig 96 | } 97 | } 98 | } 99 | } 100 | 101 | signing { 102 | def signingKeyId = System.getenv("SIGNING_KEY_ID") 103 | def signingPassword = System.getenv("SIGNING_KEY_PASSWORD") 104 | def signingKey = System.getenv("SIGNING_KEY_AMORED") 105 | useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) 106 | sign publishing.publications.mavenJava 107 | } 108 | 109 | nexusPublishing { 110 | repositories { 111 | sonatype { 112 | username = System.getenv("SONATYPE_USER") 113 | password = System.getenv("SONATYPE_KEY") 114 | stagingProfileId = System.getenv('SONATYPE_STAGING_PROFILE') 115 | 116 | // See https://central.sonatype.org/publish/publish-portal-ossrh-staging-api/#configuration 117 | nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) 118 | snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /example/assets/prairie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tus/tus-java-client/1554733185cdc66ffe7877dc323713f6bf87c601/example/assets/prairie.jpg -------------------------------------------------------------------------------- /example/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | 3 | dependencies { 4 | implementation fileTree(dir: 'libs', include: ['*.jar']) 5 | implementation rootProject 6 | } 7 | -------------------------------------------------------------------------------- /example/src/main/java/io/tus/java/example/Main.java: -------------------------------------------------------------------------------- 1 | package io.tus.java.example; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.net.URL; 6 | 7 | import io.tus.java.client.ProtocolException; 8 | import io.tus.java.client.TusClient; 9 | import io.tus.java.client.TusExecutor; 10 | import io.tus.java.client.TusURLMemoryStore; 11 | import io.tus.java.client.TusUpload; 12 | import io.tus.java.client.TusUploader; 13 | 14 | /** 15 | * A representative Example class to show an usual usecase. 16 | */ 17 | public final class Main { 18 | /** 19 | * Main method to run a standard upload task. 20 | * @param args 21 | */ 22 | public static void main(String[] args) { 23 | try { 24 | // When Java's HTTP client follows a redirect for a POST request, it will change the 25 | // method from POST to GET which can be disabled using following system property. 26 | // If you do not enable strict redirects, the tus-java-client will not follow any 27 | // redirects but still work correctly. 28 | System.setProperty("http.strictPostRedirect", "true"); 29 | 30 | // Create a new TusClient instance 31 | final TusClient client = new TusClient(); 32 | 33 | // Configure tus HTTP endpoint. This URL will be used for creating new uploads 34 | // using the Creation extension 35 | client.setUploadCreationURL(new URL("https://tusd.tusdemo.net/files/")); 36 | 37 | // Enable resumable uploads by storing the upload URL in memory 38 | client.enableResuming(new TusURLMemoryStore()); 39 | 40 | // Open a file using which we will then create a TusUpload. If you do not have 41 | // a File object, you can manually construct a TusUpload using an InputStream. 42 | // See the documentation for more information. 43 | File file = new File("./example/assets/prairie.jpg"); 44 | final TusUpload upload = new TusUpload(file); 45 | 46 | // You can also upload from an InputStream directly using a bit more work: 47 | // InputStream stream = …; 48 | // TusUpload upload = new TusUpload(); 49 | // upload.setInputStream(stream); 50 | // upload.setSize(sizeOfStream); 51 | // upload.setFingerprint("stream"); 52 | 53 | 54 | System.out.println("Starting upload..."); 55 | 56 | // We wrap our uploading code in the TusExecutor class which will automatically catch 57 | // exceptions and issue retries with small delays between them and take fully 58 | // advantage of tus' resumability to offer more reliability. 59 | // This step is optional but highly recommended. 60 | TusExecutor executor = new TusExecutor() { 61 | @Override 62 | protected void makeAttempt() throws ProtocolException, IOException { 63 | // First try to resume an upload. If that's not possible we will create a new 64 | // upload and get a TusUploader in return. This class is responsible for opening 65 | // a connection to the remote server and doing the uploading. 66 | TusUploader uploader = client.resumeOrCreateUpload(upload); 67 | 68 | // Upload the file in chunks of 1KB sizes. 69 | uploader.setChunkSize(1024); 70 | 71 | // Upload the file as long as data is available. Once the 72 | // file has been fully uploaded the method will return -1 73 | do { 74 | // Calculate the progress using the total size of the uploading file and 75 | // the current offset. 76 | long totalBytes = upload.getSize(); 77 | long bytesUploaded = uploader.getOffset(); 78 | double progress = (double) bytesUploaded / totalBytes * 100; 79 | 80 | System.out.printf("Upload at %06.2f%%.\n", progress); 81 | } while (uploader.uploadChunk() > -1); 82 | 83 | // Allow the HTTP connection to be closed and cleaned up 84 | uploader.finish(); 85 | 86 | System.out.println("Upload finished."); 87 | System.out.format("Upload available at: %s", uploader.getUploadURL().toString()); 88 | } 89 | }; 90 | executor.makeAttempts(); 91 | } catch (Exception e) { 92 | e.printStackTrace(); 93 | } 94 | 95 | } 96 | private Main() { 97 | throw new IllegalStateException("Utility class"); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /example/src/main/java/io/tus/java/example/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This package contains example classes for TUS-Client usage. 3 | */ 4 | package io.tus.java.example; 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tus/tus-java-client/1554733185cdc66ffe7877dc323713f6bf87c601/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':example' 2 | rootProject.name = 'tus-java-client' 3 | -------------------------------------------------------------------------------- /src/main/java/io/tus/java/client/FingerprintNotFoundException.java: -------------------------------------------------------------------------------- 1 | package io.tus.java.client; 2 | 3 | /** 4 | * This exception is thrown by {@link TusClient#resumeUpload(TusUpload)} if no upload URL 5 | * has been stored in the {@link TusURLStore}. 6 | */ 7 | public class FingerprintNotFoundException extends Exception { 8 | /** 9 | * Instantiates a new Object of type {@link FingerprintNotFoundException}. 10 | * @param fingerprint 11 | */ 12 | public FingerprintNotFoundException(String fingerprint) { 13 | super("fingerprint not in storage found: " + fingerprint); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/tus/java/client/ProtocolException.java: -------------------------------------------------------------------------------- 1 | package io.tus.java.client; 2 | 3 | import java.io.IOException; 4 | import java.net.HttpURLConnection; 5 | 6 | /** 7 | * This exception is thrown if the server sends a request with an unexpected status code or 8 | * missing/invalid headers. 9 | */ 10 | public class ProtocolException extends Exception { 11 | private HttpURLConnection connection; 12 | 13 | /** 14 | * Instantiates a new Object of type {@link ProtocolException}. 15 | * @param message Message to be thrown with the exception. 16 | */ 17 | public ProtocolException(String message) { 18 | super(message); 19 | } 20 | 21 | /** 22 | Instantiates a new Object of type {@link ProtocolException}. 23 | * @param message Message to be thrown with the exception. 24 | * @param connection {@link HttpURLConnection}, where the error occurred. 25 | */ 26 | public ProtocolException(String message, HttpURLConnection connection) { 27 | super(message); 28 | this.connection = connection; 29 | } 30 | 31 | /** 32 | * Returns the {@link HttpURLConnection} instances, which caused the error. 33 | * @return {@link HttpURLConnection} 34 | */ 35 | public HttpURLConnection getCausingConnection() { 36 | return connection; 37 | } 38 | 39 | /** 40 | * Determines whether a retry attempt should be made after a {@link ProtocolException} or not. 41 | * @return {@code true} if there should be a retry attempt. 42 | */ 43 | public boolean shouldRetry() { 44 | if (connection == null) { 45 | return false; 46 | } 47 | 48 | try { 49 | int responseCode = connection.getResponseCode(); 50 | 51 | // 5XX and 423 Resource Locked status codes should be retried. 52 | return (responseCode >= 500 && responseCode < 600) || responseCode == 423; 53 | } catch (IOException e) { 54 | return false; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/io/tus/java/client/ResumingNotEnabledException.java: -------------------------------------------------------------------------------- 1 | package io.tus.java.client; 2 | 3 | /** 4 | * This exception is thrown when you try to resume an upload using 5 | * {@link TusClient#resumeUpload(TusUpload)} without enabling it first. 6 | */ 7 | public class ResumingNotEnabledException extends Exception { 8 | /** 9 | * Instantiates a new Object of Type {@link ResumingNotEnabledException}. 10 | */ 11 | public ResumingNotEnabledException() { 12 | super("resuming not enabled for this client. use enableResuming() to do so"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/tus/java/client/TusClient.java: -------------------------------------------------------------------------------- 1 | package io.tus.java.client; 2 | 3 | import java.net.Proxy; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import java.io.IOException; 8 | import java.net.HttpURLConnection; 9 | import java.net.URL; 10 | import java.util.Map; 11 | 12 | /** 13 | * This class is used for creating or resuming uploads. 14 | */ 15 | public class TusClient { 16 | /** 17 | * Version of the tus protocol used by the client. The remote server needs to support this 18 | * version, too. 19 | */ 20 | public static final String TUS_VERSION = "1.0.0"; 21 | 22 | private URL uploadCreationURL; 23 | private Proxy proxy; 24 | private boolean resumingEnabled; 25 | private boolean removeFingerprintOnSuccessEnabled; 26 | private TusURLStore urlStore; 27 | private Map headers; 28 | private int connectTimeout = 5000; 29 | 30 | /** 31 | * Create a new tus client. 32 | */ 33 | public TusClient() { 34 | 35 | } 36 | 37 | /** 38 | * Set the URL used for creating new uploads. This is required if you want to initiate new 39 | * uploads using {@link #createUpload} or {@link #resumeOrCreateUpload} but is not used if you 40 | * only resume existing uploads. 41 | * 42 | * @param uploadCreationURL Absolute upload creation URL 43 | */ 44 | public void setUploadCreationURL(URL uploadCreationURL) { 45 | this.uploadCreationURL = uploadCreationURL; 46 | } 47 | 48 | /** 49 | * Get the current upload creation URL. 50 | * 51 | * @return Current upload creation URL 52 | */ 53 | public URL getUploadCreationURL() { 54 | return uploadCreationURL; 55 | } 56 | 57 | /** 58 | * Set the proxy that will be used for all requests. 59 | * 60 | * @param proxy Proxy to use 61 | */ 62 | public void setProxy(Proxy proxy) { 63 | this.proxy = proxy; 64 | } 65 | 66 | /** 67 | * Get the current proxy used for all requests. 68 | * 69 | * @return Current proxy 70 | */ 71 | public Proxy getProxy() { 72 | return proxy; 73 | } 74 | 75 | /** 76 | * Enable resuming already started uploads. This step is required if you want to use 77 | * {@link #resumeUpload(TusUpload)}. 78 | * 79 | * @param urlStore Storage used to save and retrieve upload URLs by its fingerprint. 80 | */ 81 | public void enableResuming(@NotNull TusURLStore urlStore) { 82 | resumingEnabled = true; 83 | this.urlStore = urlStore; 84 | } 85 | 86 | /** 87 | * Disable resuming started uploads. 88 | * 89 | * @see #enableResuming(TusURLStore) 90 | */ 91 | public void disableResuming() { 92 | resumingEnabled = false; 93 | this.urlStore = null; 94 | } 95 | 96 | /** 97 | * Get the current status if resuming. 98 | * 99 | * @see #enableResuming(TusURLStore) 100 | * @see #disableResuming() 101 | * 102 | * @return True if resuming has been enabled using {@link #enableResuming(TusURLStore)} 103 | */ 104 | public boolean resumingEnabled() { 105 | return resumingEnabled; 106 | } 107 | 108 | /** 109 | * Enable removing fingerprints after a successful upload. 110 | * 111 | * @see #disableRemoveFingerprintOnSuccess() 112 | */ 113 | public void enableRemoveFingerprintOnSuccess() { 114 | removeFingerprintOnSuccessEnabled = true; 115 | } 116 | 117 | /** 118 | * Disable removing fingerprints after a successful upload. 119 | * 120 | * @see #enableRemoveFingerprintOnSuccess() 121 | */ 122 | public void disableRemoveFingerprintOnSuccess() { 123 | removeFingerprintOnSuccessEnabled = false; 124 | } 125 | 126 | /** 127 | * Get the current status if removing fingerprints after a successful upload. 128 | * 129 | * @see #enableRemoveFingerprintOnSuccess() 130 | * @see #disableRemoveFingerprintOnSuccess() 131 | * 132 | * @return True if resuming has been enabled using {@link #enableResuming(TusURLStore)} 133 | */ 134 | public boolean removeFingerprintOnSuccessEnabled() { 135 | return removeFingerprintOnSuccessEnabled; 136 | } 137 | 138 | 139 | /** 140 | * Set headers which will be added to every HTTP requestes made by this TusClient instance. 141 | * These may to overwrite tus-specific headers, which can be identified by their Tus-* 142 | * prefix, and can cause unexpected behavior. 143 | * 144 | * @see #getHeaders() 145 | * @see #prepareConnection(HttpURLConnection) 146 | * 147 | * @param headers The map of HTTP headers 148 | */ 149 | public void setHeaders(@Nullable Map headers) { 150 | this.headers = headers; 151 | } 152 | 153 | /** 154 | * Get the HTTP headers which should be contained in every request and were configured using 155 | * {@link #setHeaders(Map)}. 156 | * 157 | * @see #setHeaders(Map) 158 | * @see #prepareConnection(HttpURLConnection) 159 | * 160 | * @return The map of configured HTTP headers 161 | */ 162 | @Nullable 163 | public Map getHeaders() { 164 | return headers; 165 | } 166 | 167 | /** 168 | * Sets the timeout for a Connection. 169 | * @param timeout in milliseconds 170 | */ 171 | public void setConnectTimeout(int timeout) { 172 | connectTimeout = timeout; 173 | } 174 | 175 | /** 176 | * Returns the Connection Timeout. 177 | * @return Timeout in milliseconds. 178 | */ 179 | public int getConnectTimeout() { 180 | return connectTimeout; 181 | } 182 | 183 | /** 184 | * Create a new upload using the Creation extension. Before calling this function, an "upload 185 | * creation URL" must be defined using {@link #setUploadCreationURL(URL)} or else this 186 | * function will fail. 187 | * In order to create the upload a POST request will be issued. The file's chunks must be 188 | * uploaded manually using the returned {@link TusUploader} object. 189 | * 190 | * @param upload The file for which a new upload will be created 191 | * @return Use {@link TusUploader} to upload the file's chunks. 192 | * @throws ProtocolException Thrown if the remote server sent an unexpected response, e.g. 193 | * wrong status codes or missing/invalid headers. 194 | * @throws IOException Thrown if an exception occurs while issuing the HTTP request. 195 | */ 196 | public TusUploader createUpload(@NotNull TusUpload upload) throws ProtocolException, IOException { 197 | HttpURLConnection connection = openConnection(uploadCreationURL); 198 | connection.setRequestMethod("POST"); 199 | prepareConnection(connection); 200 | 201 | String encodedMetadata = upload.getEncodedMetadata(); 202 | if (encodedMetadata.length() > 0) { 203 | connection.setRequestProperty("Upload-Metadata", encodedMetadata); 204 | } 205 | 206 | connection.addRequestProperty("Upload-Length", Long.toString(upload.getSize())); 207 | connection.connect(); 208 | 209 | int responseCode = connection.getResponseCode(); 210 | if (!(responseCode >= 200 && responseCode < 300)) { 211 | throw new ProtocolException( 212 | "unexpected status code (" + responseCode + ") while creating upload", connection); 213 | } 214 | 215 | String urlStr = connection.getHeaderField("Location"); 216 | if (urlStr == null || urlStr.length() == 0) { 217 | throw new ProtocolException("missing upload URL in response for creating upload", connection); 218 | } 219 | 220 | // The upload URL must be relative to the URL of the request by which is was returned, 221 | // not the upload creation URL. In most cases, there is no difference between those two 222 | // but there may be cases in which the POST request is redirected. 223 | URL uploadURL = new URL(connection.getURL(), urlStr); 224 | 225 | if (resumingEnabled) { 226 | urlStore.set(upload.getFingerprint(), uploadURL); 227 | } 228 | 229 | return createUploader(upload, uploadURL, 0L); 230 | } 231 | 232 | @NotNull 233 | private HttpURLConnection openConnection(@NotNull URL uploadURL) throws IOException { 234 | if (proxy != null) { 235 | return (HttpURLConnection) uploadURL.openConnection(proxy); 236 | } 237 | return (HttpURLConnection) uploadURL.openConnection(); 238 | } 239 | 240 | @NotNull 241 | private TusUploader createUploader(@NotNull TusUpload upload, @NotNull URL uploadURL, long offset) 242 | throws IOException { 243 | TusUploader uploader = new TusUploader(this, upload, uploadURL, upload.getTusInputStream(), offset); 244 | uploader.setProxy(proxy); 245 | return uploader; 246 | } 247 | 248 | /** 249 | * Try to resume an already started upload. Before call this function, resuming must be 250 | * enabled using {@link #enableResuming(TusURLStore)}. This method will look up the URL for this 251 | * upload in the {@link TusURLStore} using the upload's fingerprint (see 252 | * {@link TusUpload#getFingerprint()}). After a successful lookup a HEAD request will be issued 253 | * to find the current offset without uploading the file, yet. 254 | * 255 | * @param upload The file for which an upload will be resumed 256 | * @return Use {@link TusUploader} to upload the remaining file's chunks. 257 | * @throws FingerprintNotFoundException Thrown if no matching fingerprint has been found in 258 | * {@link TusURLStore}. Use {@link #createUpload(TusUpload)} to create a new upload. 259 | * @throws ResumingNotEnabledException Throw if resuming has not been enabled using {@link 260 | * #enableResuming(TusURLStore)}. 261 | * @throws ProtocolException Thrown if the remote server sent an unexpected response, e.g. 262 | * wrong status codes or missing/invalid headers. 263 | * @throws IOException Thrown if an exception occurs while issuing the HTTP request. 264 | */ 265 | public TusUploader resumeUpload(@NotNull TusUpload upload) throws 266 | FingerprintNotFoundException, ResumingNotEnabledException, ProtocolException, IOException { 267 | if (!resumingEnabled) { 268 | throw new ResumingNotEnabledException(); 269 | } 270 | 271 | URL uploadURL = urlStore.get(upload.getFingerprint()); 272 | if (uploadURL == null) { 273 | throw new FingerprintNotFoundException(upload.getFingerprint()); 274 | } 275 | 276 | return beginOrResumeUploadFromURL(upload, uploadURL); 277 | } 278 | 279 | /** 280 | * Begin an upload or alternatively resume it if the upload has already been started before. In contrast to 281 | * {@link #createUpload(TusUpload)} and {@link #resumeOrCreateUpload(TusUpload)} this method will not create a new 282 | * upload. The user must obtain the upload location URL on their own as this method will not send the POST request 283 | * which is normally used to create a new upload. 284 | * Therefore, this method is only useful if you are uploading to a service which takes care of creating the tus 285 | * upload for yourself. One example of such a service is the Vimeo API. 286 | * When called a HEAD request will be issued to find the current offset without uploading the file, yet. 287 | * The uploading can be started by using the returned {@link TusUploader} object. 288 | * 289 | * @param upload The file for which an upload will be resumed 290 | * @param uploadURL The upload location URL at which has already been created and this file should be uploaded to. 291 | * @return Use {@link TusUploader} to upload the remaining file's chunks. 292 | * @throws ProtocolException Thrown if the remote server sent an unexpected response, e.g. 293 | * wrong status codes or missing/invalid headers. 294 | * @throws IOException Thrown if an exception occurs while issuing the HTTP request. 295 | */ 296 | public TusUploader beginOrResumeUploadFromURL(@NotNull TusUpload upload, @NotNull URL uploadURL) throws 297 | ProtocolException, IOException { 298 | HttpURLConnection connection = openConnection(uploadURL); 299 | connection.setRequestMethod("HEAD"); 300 | prepareConnection(connection); 301 | 302 | connection.connect(); 303 | 304 | int responseCode = connection.getResponseCode(); 305 | if (!(responseCode >= 200 && responseCode < 300)) { 306 | throw new ProtocolException( 307 | "unexpected status code (" + responseCode + ") while resuming upload", connection); 308 | } 309 | 310 | String offsetStr = connection.getHeaderField("Upload-Offset"); 311 | if (offsetStr == null || offsetStr.length() == 0) { 312 | throw new ProtocolException("missing upload offset in response for resuming upload", connection); 313 | } 314 | long offset = Long.parseLong(offsetStr); 315 | 316 | return createUploader(upload, uploadURL, offset); 317 | } 318 | 319 | /** 320 | * Try to resume an upload using {@link #resumeUpload(TusUpload)}. If the method call throws 321 | * an {@link ResumingNotEnabledException} or {@link FingerprintNotFoundException}, a new upload 322 | * will be created using {@link #createUpload(TusUpload)}. 323 | * 324 | * @param upload The file for which an upload will be resumed 325 | * @throws ProtocolException Thrown if the remote server sent an unexpected response, e.g. 326 | * wrong status codes or missing/invalid headers. 327 | * @throws IOException Thrown if an exception occurs while issuing the HTTP request. 328 | * @return {@link TusUploader} instance. 329 | */ 330 | public TusUploader resumeOrCreateUpload(@NotNull TusUpload upload) throws ProtocolException, IOException { 331 | try { 332 | return resumeUpload(upload); 333 | } catch (FingerprintNotFoundException e) { 334 | return createUpload(upload); 335 | } catch (ResumingNotEnabledException e) { 336 | return createUpload(upload); 337 | } catch (ProtocolException e) { 338 | // If the attempt to resume returned a 404 Not Found, we immediately try to create a new 339 | // one since TusExectuor would not retry this operation. 340 | HttpURLConnection connection = e.getCausingConnection(); 341 | if (connection != null && connection.getResponseCode() == 404) { 342 | return createUpload(upload); 343 | } 344 | 345 | throw e; 346 | } 347 | } 348 | 349 | /** 350 | * Set headers used for every HTTP request. Currently, this will add the Tus-Resumable header 351 | * and any custom header which can be configured using {@link #setHeaders(Map)}, 352 | * 353 | * @param connection The connection whose headers will be modified. 354 | */ 355 | public void prepareConnection(@NotNull HttpURLConnection connection) { 356 | // Only follow redirects, if the POST methods is preserved. If http.strictPostRedirect is 357 | // disabled, a POST request will be transformed into a GET request which is not wanted by us. 358 | 359 | // CHECKSTYLE:OFF 360 | // LineLength - Necessary because of length of the link 361 | // See:https://github.com/openjdk/jdk/blob/jdk7-b43/jdk/src/share/classes/sun/net/www/protocol/http/HttpURLConnection.java#L2020-L2035 362 | // CHECKSTYLE:ON 363 | connection.setInstanceFollowRedirects(Boolean.getBoolean("http.strictPostRedirect")); 364 | 365 | connection.setConnectTimeout(connectTimeout); 366 | connection.addRequestProperty("Tus-Resumable", TUS_VERSION); 367 | 368 | if (headers != null) { 369 | for (Map.Entry entry : headers.entrySet()) { 370 | connection.addRequestProperty(entry.getKey(), entry.getValue()); 371 | } 372 | } 373 | } 374 | 375 | /** 376 | * Actions to be performed after a successful upload completion. 377 | * Manages URL removal from the URL store if remove fingerprint on success is enabled 378 | * 379 | * @param upload that has been finished 380 | */ 381 | protected void uploadFinished(@NotNull TusUpload upload) { 382 | if (resumingEnabled && removeFingerprintOnSuccessEnabled) { 383 | urlStore.remove(upload.getFingerprint()); 384 | } 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /src/main/java/io/tus/java/client/TusExecutor.java: -------------------------------------------------------------------------------- 1 | package io.tus.java.client; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * TusExecutor is a wrapper class which you can build around your uploading mechanism and any 7 | * exception thrown by it will be caught and may result in a retry. This way you can easily add 8 | * retrying functionality to your application with defined delays between them. 9 | * 10 | * This can be achieved by extending TusExecutor and implementing the abstract makeAttempt() method: 11 | *
 12 |  * {@code
 13 |  *  TusExecutor executor = new TusExecutor() {
 14 |  *      {@literal @}Override
 15 |  *      protected void makeAttempt() throws ProtocolException, IOException {
 16 |  *          TusUploader uploader = client.resumeOrCreateUpload(upload);
 17 |  *          while(uploader.uploadChunk() > -1) {}
 18 |  *          uploader.finish();
 19 |  *      }
 20 |  *  };
 21 |  *  executor.makeAttempts();
 22 |  * }
 23 |  * 
24 | * 25 | * The retries are basically just calling the {@link #makeAttempt()} method which should then 26 | * retrieve an {@link TusUploader} using {@link TusClient#resumeOrCreateUpload(TusUpload)} and then 27 | * invoke {@link TusUploader#uploadChunk()} as long as possible without catching 28 | * {@link ProtocolException}s or {@link IOException}s as this is taken over by this class. 29 | * 30 | * The current attempt can be interrupted using {@link Thread#interrupt()} which will cause the 31 | * {@link #makeAttempts()} method to return false immediately. 32 | */ 33 | public abstract class TusExecutor { 34 | private int[] delays = new int[]{500, 1000, 2000, 3000}; 35 | 36 | /** 37 | * Set the delays at which TusExecutor will issue a retry if {@link #makeAttempt()} throws an 38 | * exception. If the methods call fails for the first time it will wait delays[0]ms 39 | * before calling it again. If this second calls also does not return normally 40 | * delays[1]ms will be waited on so on. 41 | * It total delays.length retries may be issued, resulting in up to 42 | * delays.length + 1 calls to {@link #makeAttempt()}. 43 | * The default delays are set to 500ms, 1s, 2s and 3s. 44 | * 45 | * @see #getDelays() 46 | * 47 | * @param delays The desired delay values to be used 48 | */ 49 | public void setDelays(int[] delays) { 50 | this.delays = delays; 51 | } 52 | 53 | /** 54 | * Get the delays which will be used for waiting before attempting retries. 55 | * 56 | * @see #setDelays(int[]) 57 | * 58 | * @return The dalys previously set 59 | */ 60 | public int[] getDelays() { 61 | return delays; 62 | } 63 | 64 | /** 65 | * This method is basically just calling the {@link #makeAttempt()} method which should then 66 | * retrieve an {@link TusUploader} using {@link TusClient#resumeOrCreateUpload(TusUpload)} and then 67 | * invoke {@link TusUploader#uploadChunk()} as long as possible without catching 68 | * {@link ProtocolException}s or {@link IOException}s as this is taken over by this class. 69 | * 70 | * The current attempt can be interrupted using {@link Thread#interrupt()} which will cause the 71 | * method to return false immediately. 72 | * 73 | * @return true if the {@link #makeAttempt()} method returned normally and 74 | * false if the thread was interrupted while sleeping until the next attempt. 75 | * 76 | * @throws ProtocolException 77 | * @throws IOException 78 | */ 79 | public boolean makeAttempts() throws ProtocolException, IOException { 80 | int attempt = -1; 81 | while (true) { 82 | attempt++; 83 | 84 | try { 85 | makeAttempt(); 86 | // Returning true is the signal that the makeAttempt() function exited without 87 | // throwing an error. 88 | return true; 89 | } catch (ProtocolException e) { 90 | // Do not attempt a retry, if the Exception suggests so. 91 | if (!e.shouldRetry()) { 92 | throw e; 93 | } 94 | 95 | if (attempt >= delays.length) { 96 | // We exceeds the number of maximum retries. In this case the latest exception 97 | // is thrown. 98 | throw e; 99 | } 100 | } catch (IOException e) { 101 | if (attempt >= delays.length) { 102 | // We exceeds the number of maximum retries. In this case the latest exception 103 | // is thrown. 104 | throw e; 105 | } 106 | } 107 | 108 | try { 109 | // Sleep for the specified delay before attempting the next retry. 110 | Thread.sleep(delays[attempt]); 111 | } catch (InterruptedException e) { 112 | // If we get interrupted while waiting for the next retry, the user has cancelled 113 | // the upload willingly and we return false as a signal. 114 | return false; 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * This method must be implemented by the specific caller. It will be invoked once or multiple 121 | * times by the {@link #makeAttempts()} method. 122 | * A proper implementation should retrieve an {@link TusUploader} using 123 | * {@link TusClient#resumeOrCreateUpload(TusUpload)} and then invoke 124 | * {@link TusUploader#uploadChunk()} as long as possible without catching 125 | * {@link ProtocolException}s or {@link IOException}s as this is taken over by this class. 126 | * 127 | * @throws ProtocolException 128 | * @throws IOException 129 | */ 130 | protected abstract void makeAttempt() throws ProtocolException, IOException; 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/io/tus/java/client/TusInputStream.java: -------------------------------------------------------------------------------- 1 | package io.tus.java.client; 2 | 3 | import java.io.BufferedInputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | 7 | /** 8 | * TusInputStream is an internal abstraction above an InputStream which allows seeking to a 9 | * position relative to the beginning of the stream. In comparision {@link InputStream#skip(long)} 10 | * only supports skipping bytes relative to the current position. 11 | */ 12 | class TusInputStream { 13 | private InputStream stream; 14 | private long bytesRead; 15 | private long lastMark = -1; 16 | 17 | /** 18 | * Create a new TusInputStream which reads from and operates on the supplied stream. 19 | * 20 | * @param stream The stream to read from 21 | */ 22 | TusInputStream(InputStream stream) { 23 | if (!stream.markSupported()) { 24 | stream = new BufferedInputStream(stream); 25 | } 26 | 27 | this.stream = stream; 28 | } 29 | 30 | /** 31 | * Read a specific amount of bytes from the stream and write them to the start of the supplied 32 | * buffer. 33 | * See {@link InputStream#read(byte[], int, int)} for more details. 34 | * 35 | * @param buffer The array to write the bytes to 36 | * @param length Number of bytes to read at most 37 | * @return Actual number of bytes read 38 | * @throws IOException 39 | */ 40 | public int read(byte[] buffer, int length) throws IOException { 41 | int bytesReadNow = stream.read(buffer, 0, length); 42 | bytesRead += bytesReadNow; 43 | return bytesReadNow; 44 | } 45 | 46 | /** 47 | * Seek to the position relative to the start of the stream. 48 | * 49 | * @param position Absolute position to seek to 50 | * @throws IOException 51 | */ 52 | public void seekTo(long position) throws IOException { 53 | if (lastMark != -1) { 54 | stream.reset(); 55 | stream.skip(position - lastMark); 56 | lastMark = -1; 57 | } else { 58 | stream.skip(position); 59 | } 60 | 61 | bytesRead = position; 62 | } 63 | 64 | /** 65 | * Mark the current position to allow seeking to a position after this mark. 66 | * See {@link InputStream#mark(int)} for details. 67 | * 68 | * @param readLimit Number of bytes to read before this mark gets invalidated 69 | */ 70 | public void mark(int readLimit) { 71 | lastMark = bytesRead; 72 | stream.mark(readLimit); 73 | } 74 | 75 | /** 76 | * Close the underlying instance of InputStream. 77 | * 78 | * @throws IOException 79 | */ 80 | public void close() throws IOException { 81 | stream.close(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/io/tus/java/client/TusURLMemoryStore.java: -------------------------------------------------------------------------------- 1 | package io.tus.java.client; 2 | 3 | import java.net.URL; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | /** 8 | * This class is used to map an upload's fingerprint with the corresponding upload URL by storing 9 | * the entries in a {@link HashMap}. This functionality is used to allow resuming uploads. The 10 | * fingerprint is usually retrieved using {@link TusUpload#getFingerprint()}. 11 | *
12 | * The values will only be stored as long as the application is running. This store will not 13 | * keep the values after your application crashes or restarts. 14 | */ 15 | public class TusURLMemoryStore implements TusURLStore { 16 | private Map store = new HashMap(); 17 | 18 | /** 19 | * Stores the upload's fingerprint and url. 20 | * @param fingerprint An upload's fingerprint. 21 | * @param url The corresponding upload URL. 22 | */ 23 | @Override 24 | public void set(String fingerprint, URL url) { 25 | store.put(fingerprint, url); 26 | } 27 | 28 | /** 29 | * Returns the corresponding Upload URL to a given fingerprint. 30 | * @param fingerprint An upload's fingerprint. 31 | * @return The corresponding upload URL. 32 | */ 33 | @Override 34 | public URL get(String fingerprint) { 35 | return store.get(fingerprint); 36 | } 37 | 38 | /** 39 | * Removes the corresponding entry to a fingerprint from the {@link TusURLMemoryStore}. 40 | * @param fingerprint An upload's fingerprint. 41 | */ 42 | @Override 43 | public void remove(String fingerprint) { 44 | store.remove(fingerprint); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/io/tus/java/client/TusURLStore.java: -------------------------------------------------------------------------------- 1 | package io.tus.java.client; 2 | 3 | import java.net.URL; 4 | 5 | /** 6 | * Implementations of this interface are used to map an upload's fingerprint with the corresponding 7 | * upload URL. This functionality is used to allow resuming uploads. The fingerprint is usually 8 | * retrieved using {@link TusUpload#getFingerprint()}. 9 | */ 10 | public interface TusURLStore { 11 | /** 12 | * Store a new fingerprint and its upload URL. 13 | * 14 | * @param fingerprint An upload's fingerprint. 15 | * @param url The corresponding upload URL. 16 | */ 17 | void set(String fingerprint, URL url); 18 | 19 | /** 20 | * Retrieve an upload's URL for a fingerprint. If no matching entry is found this method will 21 | * return null. 22 | * 23 | * @param fingerprint An upload's fingerprint. 24 | * @return The corresponding upload URL. 25 | */ 26 | URL get(String fingerprint); 27 | 28 | /** 29 | * Remove an entry from the store. Calling {@link #get(String)} with the same fingerprint will 30 | * return null. If no entry exists for this fingerprint no exception should be 31 | * thrown. 32 | * 33 | * @param fingerprint An upload's fingerprint. 34 | */ 35 | void remove(String fingerprint); 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/tus/java/client/TusUpload.java: -------------------------------------------------------------------------------- 1 | package io.tus.java.client; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.io.File; 6 | import java.io.FileInputStream; 7 | import java.io.FileNotFoundException; 8 | import java.io.InputStream; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | /** 13 | * This class contains information about a file which will be uploaded later. Uploading is not 14 | * done using this class but using {@link TusUploader} whose instances are returned by 15 | * {@link TusClient#createUpload(TusUpload)}, {@link TusClient#createUpload(TusUpload)} and 16 | * {@link TusClient#resumeOrCreateUpload(TusUpload)}. 17 | */ 18 | public class TusUpload { 19 | private long size; 20 | private InputStream input; 21 | private TusInputStream tusInputStream; 22 | private String fingerprint; 23 | private Map metadata; 24 | 25 | /** 26 | * Create a new TusUpload object. 27 | */ 28 | public TusUpload() { 29 | } 30 | 31 | /** 32 | * Create a new TusUpload object using the supplied file object. The corresponding {@link 33 | * InputStream}, size and fingerprint will be automatically set. 34 | * 35 | * @param file The file whose content should be later uploaded. 36 | * @throws FileNotFoundException Thrown if the file cannot be found. 37 | */ 38 | public TusUpload(@NotNull File file) throws FileNotFoundException { 39 | size = file.length(); 40 | setInputStream(new FileInputStream(file)); 41 | 42 | fingerprint = String.format("%s-%d", file.getAbsolutePath(), size); 43 | 44 | metadata = new HashMap(); 45 | metadata.put("filename", file.getName()); 46 | } 47 | 48 | /** 49 | * Returns the file size of the upload. 50 | * @return File size in bytes 51 | */ 52 | public long getSize() { 53 | return size; 54 | } 55 | 56 | /** 57 | * Set the file's size in bytes. 58 | * 59 | * @param size File's size in bytes. 60 | */ 61 | public void setSize(long size) { 62 | this.size = size; 63 | } 64 | 65 | /** 66 | * Returns the file specific fingerprint. 67 | * @return Fingerprint as String. 68 | */ 69 | public String getFingerprint() { 70 | return fingerprint; 71 | } 72 | 73 | /** 74 | * Sets a fingerprint for this upload. This fingerprint must be unique and file specific, because it is used 75 | * for upload identification. 76 | * @param fingerprint String of fingerprint information. 77 | */ 78 | public void setFingerprint(String fingerprint) { 79 | this.fingerprint = fingerprint; 80 | } 81 | 82 | /** 83 | * Returns the input stream of the file to upload. 84 | * @return {@link InputStream} 85 | */ 86 | public InputStream getInputStream() { 87 | return input; 88 | } 89 | 90 | /** 91 | * This method returns the {@link TusInputStream}, which was derived from the file's {@link InputStream}. 92 | * @return {@link TusInputStream} 93 | */ 94 | TusInputStream getTusInputStream() { 95 | return tusInputStream; 96 | } 97 | 98 | /** 99 | * Set the source from which will be read if the file will be later uploaded. 100 | * 101 | * @param inputStream The stream which will be read. 102 | */ 103 | public void setInputStream(InputStream inputStream) { 104 | input = inputStream; 105 | tusInputStream = new TusInputStream(inputStream); 106 | } 107 | 108 | /** 109 | * This methods allows it to send Metadata alongside with the upload. The Metadata must be provided as 110 | * a Map with Key - Value pairs of Type String. 111 | * @param metadata Key-value pairs of Type String 112 | */ 113 | public void setMetadata(Map metadata) { 114 | this.metadata = metadata; 115 | } 116 | 117 | /** 118 | * This method returns the upload's metadata as Map. 119 | * @return {@link Map} of metadata Key - Value pairs. 120 | */ 121 | public Map getMetadata() { 122 | return metadata; 123 | } 124 | 125 | /** 126 | * Encode the metadata into a string according to the specification, so it can be 127 | * used as the value for the Upload-Metadata header. 128 | * 129 | * @return Encoded metadata 130 | */ 131 | public String getEncodedMetadata() { 132 | if (metadata == null || metadata.size() == 0) { 133 | return ""; 134 | } 135 | 136 | String encoded = ""; 137 | 138 | boolean firstElement = true; 139 | for (Map.Entry entry : metadata.entrySet()) { 140 | if (!firstElement) { 141 | encoded += ","; 142 | } 143 | encoded += entry.getKey() + " " + base64Encode(entry.getValue().getBytes()); 144 | 145 | firstElement = false; 146 | } 147 | 148 | return encoded; 149 | } 150 | 151 | /** 152 | * Encode a byte-array using Base64. This is a sligtly modified version from an implementation 153 | * published on Wikipedia (https://en.wikipedia.org/wiki/Base64#Sample_Implementation_in_Java) 154 | * under the Creative Commons Attribution-ShareAlike License. 155 | * @param in input Byte array for Base64 encoding. 156 | * @return Base64 encoded String derived from input Bytes. 157 | */ 158 | static String base64Encode(byte[] in) { 159 | StringBuilder out = new StringBuilder((in.length * 4) / 3); 160 | String codes = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 161 | 162 | int b; 163 | for (int i = 0; i < in.length; i += 3) { 164 | b = (in[i] & 0xFC) >> 2; 165 | out.append(codes.charAt(b)); 166 | b = (in[i] & 0x03) << 4; 167 | if (i + 1 < in.length) { 168 | b |= (in[i + 1] & 0xF0) >> 4; 169 | out.append(codes.charAt(b)); 170 | b = (in[i + 1] & 0x0F) << 2; 171 | if (i + 2 < in.length) { 172 | b |= (in[i + 2] & 0xC0) >> 6; 173 | out.append(codes.charAt(b)); 174 | b = in[i + 2] & 0x3F; 175 | out.append(codes.charAt(b)); 176 | } else { 177 | out.append(codes.charAt(b)); 178 | out.append('='); 179 | } 180 | } else { 181 | out.append(codes.charAt(b)); 182 | out.append("=="); 183 | } 184 | } 185 | 186 | return out.toString(); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/main/java/io/tus/java/client/TusUploader.java: -------------------------------------------------------------------------------- 1 | package io.tus.java.client; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | import java.net.HttpURLConnection; 6 | import java.net.Proxy; 7 | import java.net.URL; 8 | import java.net.URLConnection; 9 | 10 | /** 11 | * This class is used for doing the actual upload of the files. Instances are returned by 12 | * {@link TusClient#createUpload(TusUpload)}, {@link TusClient#createUpload(TusUpload)} and 13 | * {@link TusClient#resumeOrCreateUpload(TusUpload)}. 14 | *
15 | * After obtaining an instance you can upload a file by following these steps: 16 | *
    17 | *
  1. Upload a chunk using {@link #uploadChunk()}
  2. 18 | *
  3. Optionally get the new offset ({@link #getOffset()} to calculate the progress
  4. 19 | *
  5. Repeat step 1 until the {@link #uploadChunk()} returns -1
  6. 20 | *
  7. Close HTTP connection and InputStream using {@link #finish()} to free resources
  8. 21 | *
22 | */ 23 | public class TusUploader { 24 | private URL uploadURL; 25 | private Proxy proxy; 26 | private TusInputStream input; 27 | private long offset; 28 | private TusClient client; 29 | private TusUpload upload; 30 | private byte[] buffer; 31 | private int requestPayloadSize = 10 * 1024 * 1024; 32 | private int bytesRemainingForRequest; 33 | 34 | private HttpURLConnection connection; 35 | private OutputStream output; 36 | 37 | /** 38 | * Begin a new upload request by opening a PATCH request to specified upload URL. After this 39 | * method returns a connection will be ready and you can upload chunks of the file. 40 | * 41 | * @param client Used for preparing a request ({@link TusClient#prepareConnection(HttpURLConnection)} 42 | * @param upload {@link TusUpload} to be uploaded. 43 | * @param uploadURL URL to send the request to 44 | * @param input Stream to read (and seek) from and upload to the remote server 45 | * @param offset Offset to read from 46 | * @throws IOException Thrown if an exception occurs while issuing the HTTP request. 47 | */ 48 | public TusUploader(TusClient client, TusUpload upload, URL uploadURL, TusInputStream input, long offset) 49 | throws IOException { 50 | this.uploadURL = uploadURL; 51 | this.input = input; 52 | this.offset = offset; 53 | this.client = client; 54 | this.upload = upload; 55 | 56 | input.seekTo(offset); 57 | 58 | setChunkSize(2 * 1024 * 1024); 59 | } 60 | 61 | private void openConnection() throws IOException, ProtocolException { 62 | // Only open a connection, if we have none open. 63 | if (connection != null) { 64 | return; 65 | } 66 | 67 | bytesRemainingForRequest = requestPayloadSize; 68 | input.mark(requestPayloadSize); 69 | 70 | if (proxy != null) { 71 | connection = (HttpURLConnection) uploadURL.openConnection(proxy); 72 | } else { 73 | connection = (HttpURLConnection) uploadURL.openConnection(); 74 | } 75 | client.prepareConnection(connection); 76 | connection.setRequestProperty("Upload-Offset", Long.toString(offset)); 77 | connection.setRequestProperty("Content-Type", "application/offset+octet-stream"); 78 | connection.setRequestProperty("Expect", "100-continue"); 79 | 80 | try { 81 | connection.setRequestMethod("PATCH"); 82 | // Check whether we are running on a buggy JRE 83 | } catch (java.net.ProtocolException pe) { 84 | connection.setRequestMethod("POST"); 85 | connection.setRequestProperty("X-HTTP-Method-Override", "PATCH"); 86 | } 87 | 88 | connection.setDoOutput(true); 89 | connection.setChunkedStreamingMode(0); 90 | try { 91 | output = connection.getOutputStream(); 92 | } catch (java.net.ProtocolException pe) { 93 | // If we already have a response code available, our expectation using the "Expect: 100- 94 | // continue" header failed and we should handle this response. 95 | if (connection.getResponseCode() != -1) { 96 | finish(); 97 | } 98 | 99 | throw pe; 100 | } 101 | } 102 | 103 | /** 104 | * Sets the used chunk size. This number is used by {@link #uploadChunk()} to indicate how 105 | * much data is uploaded in a single take. When choosing a value for this parameter you need to 106 | * consider that uploadChunk() will only return once the specified number of bytes has been 107 | * sent. For slow internet connections this may take a long time. In addition, a buffer with 108 | * the chunk size is allocated and kept in memory. 109 | * 110 | * @param size The new chunk size 111 | */ 112 | public void setChunkSize(int size) { 113 | buffer = new byte[size]; 114 | } 115 | 116 | /** 117 | * Returns the current chunk size set using {@link #setChunkSize(int)}. 118 | * 119 | * @return Current chunk size 120 | */ 121 | public int getChunkSize() { 122 | return buffer.length; 123 | } 124 | 125 | /** 126 | * Set the maximum payload size for a single request counted in bytes. This is useful for splitting 127 | * bigger uploads into multiple requests. For example, if you have a resource of 2MB and 128 | * the payload size set to 1MB, the upload will be transferred by two requests of 1MB each. 129 | * 130 | * The default value for this setting is 10 * 1024 * 1024 bytes (10 MiB). 131 | * 132 | * Be aware that setting a low maximum payload size (in the low megabytes or even less range) will result in 133 | * decreased performance since more requests need to be used for an upload. Each request will come with its overhead 134 | * in terms of longer upload times. 135 | * 136 | * Be aware that setting a high maximum payload size may result in a high memory usage since 137 | * tus-java-client usually allocates a buffer with the maximum payload size (this buffer is used 138 | * to allow retransmission of lost data if necessary). If the client is running on a memory- 139 | * constrained device (e.g. mobile app) and the maximum payload size is too high, it might 140 | * result in an {@link OutOfMemoryError}. 141 | * 142 | * This method must not be called when the uploader has currently an open connection to the 143 | * remote server. In general, try to set the payload size before invoking {@link #uploadChunk()} 144 | * the first time. 145 | * 146 | * @see #getRequestPayloadSize() 147 | * 148 | * @param size Number of bytes for a single payload 149 | * @throws IllegalStateException Thrown if the uploader currently has a connection open 150 | */ 151 | public void setRequestPayloadSize(int size) throws IllegalStateException { 152 | if (connection != null) { 153 | throw new IllegalStateException("payload size for a single request must not be " 154 | + "modified as long as a request is in progress"); 155 | } 156 | 157 | requestPayloadSize = size; 158 | } 159 | 160 | /** 161 | * Get the current maximum payload size for a single request. 162 | * 163 | * @see #setChunkSize(int) 164 | * 165 | * @return Number of bytes for a single payload 166 | */ 167 | public int getRequestPayloadSize() { 168 | return requestPayloadSize; 169 | } 170 | 171 | /** 172 | * Upload a part of the file by reading a chunk from the InputStream and writing 173 | * it to the HTTP request's body. If the number of available bytes is lower than the chunk's 174 | * size, all available bytes will be uploaded and nothing more. 175 | * No new connection will be established when calling this method, instead the connection opened 176 | * in the previous calls will be used. 177 | * The size of the read chunk can be obtained using {@link #getChunkSize()} and changed 178 | * using {@link #setChunkSize(int)}. 179 | * In order to obtain the new offset, use {@link #getOffset()} after this method returns. 180 | * 181 | * @return Number of bytes read and written. 182 | * @throws IOException Thrown if an exception occurs while reading from the source or writing 183 | * to the HTTP request. 184 | */ 185 | public int uploadChunk() throws IOException, ProtocolException { 186 | openConnection(); 187 | 188 | int bytesToRead = Math.min(getChunkSize(), bytesRemainingForRequest); 189 | 190 | int bytesRead = input.read(buffer, bytesToRead); 191 | if (bytesRead == -1) { 192 | // No bytes were read since the input stream is empty 193 | return -1; 194 | } 195 | 196 | // Do not write the entire buffer to the stream since the array will 197 | // be filled up with 0x00s if the number of read bytes is lower then 198 | // the chunk's size. 199 | output.write(buffer, 0, bytesRead); 200 | output.flush(); 201 | 202 | offset += bytesRead; 203 | bytesRemainingForRequest -= bytesRead; 204 | 205 | if (bytesRemainingForRequest <= 0) { 206 | finishConnection(); 207 | } 208 | 209 | return bytesRead; 210 | } 211 | 212 | /** 213 | * Upload a part of the file by read a chunks specified size from the InputStream and writing 214 | * it to the HTTP request's body. If the number of available bytes is lower than the chunk's 215 | * size, all available bytes will be uploaded and nothing more. 216 | * No new connection will be established when calling this method, instead the connection opened 217 | * in the previous calls will be used. 218 | * In order to obtain the new offset, use {@link #getOffset()} after this method returns. 219 | * 220 | * This method ignored the payload size per request, which may be set using 221 | * {@link #setRequestPayloadSize(int)}. Please, use {@link #uploadChunk()} instead. 222 | * 223 | * @deprecated This method is inefficient and has been replaced by {@link #setChunkSize(int)} 224 | * and {@link #uploadChunk()} and should not be used anymore. The reason is, that 225 | * this method allocates a new buffer with the supplied chunk size for each time 226 | * it's called without reusing it. This results in a high number of memory 227 | * allocations and should be avoided. The new methods do not have this issue. 228 | * 229 | * @param chunkSize Maximum number of bytes which will be uploaded. When choosing a value 230 | * for this parameter you need to consider that the method call will only 231 | * return once the specified number of bytes have been sent. For slow 232 | * internet connections this may take a long time. 233 | * @return Number of bytes read and written. 234 | * @throws IOException Thrown if an exception occurs while reading from the source or writing 235 | * to the HTTP request. 236 | */ 237 | @Deprecated public int uploadChunk(int chunkSize) throws IOException, ProtocolException { 238 | openConnection(); 239 | 240 | byte[] buf = new byte[chunkSize]; 241 | int bytesRead = input.read(buf, chunkSize); 242 | if (bytesRead == -1) { 243 | // No bytes were read since the input stream is empty 244 | return -1; 245 | } 246 | 247 | // Do not write the entire buffer to the stream since the array will 248 | // be filled up with 0x00s if the number of read bytes is lower then 249 | // the chunk's size. 250 | output.write(buf, 0, bytesRead); 251 | output.flush(); 252 | 253 | offset += bytesRead; 254 | 255 | return bytesRead; 256 | } 257 | 258 | /** 259 | * Get the current offset for the upload. This is the number of all bytes uploaded in total and 260 | * in all requests (not only this one). You can use it in conjunction with 261 | * {@link TusUpload#getSize()} to calculate the progress. 262 | * 263 | * @return The upload's current offset. 264 | */ 265 | public long getOffset() { 266 | return offset; 267 | } 268 | 269 | /** 270 | * This methods returns the destination {@link URL} of the upload. 271 | * @return The {@link URL} of the upload. 272 | */ 273 | public URL getUploadURL() { 274 | return uploadURL; 275 | } 276 | 277 | /** 278 | * Set the proxy that will be used when uploading. 279 | * 280 | * @param proxy Proxy to use 281 | */ 282 | public void setProxy(Proxy proxy) { 283 | this.proxy = proxy; 284 | } 285 | 286 | /** 287 | * This methods returns the proxy used when uploading. 288 | * 289 | * @return The {@link Proxy} used for the upload or null when not set. 290 | */ 291 | public Proxy getProxy() { 292 | return proxy; 293 | } 294 | 295 | /** 296 | * Finish the request by closing the HTTP connection and the InputStream. 297 | * You can call this method even before the entire file has been uploaded. Use this behavior to 298 | * enable pausing uploads. 299 | * This method is equivalent to calling {@code finish(false)}. 300 | * 301 | * @throws ProtocolException Thrown if the server sends an unexpected status 302 | * code 303 | * @throws IOException Thrown if an exception occurs while cleaning up. 304 | */ 305 | public void finish() throws ProtocolException, IOException { 306 | finish(true); 307 | } 308 | 309 | /** 310 | * Finish the request by closing the HTTP connection. You can choose whether to close the InputStream or not. 311 | * You can call this method even before the entire file has been uploaded. Use this behavior to 312 | * enable pausing uploads. 313 | * Be aware that it doesn't automatically release local resources if {@code closeStream == false} and you do 314 | * not close the InputStream on your own. To be safe use {@link TusUploader#finish()}. 315 | * @param closeInputStream Determines whether the InputStream is closed with the HTTP connection. Not closing the 316 | * Input Stream may be useful for future upload a future continuation of the upload. 317 | * @throws ProtocolException Thrown if the server sends an unexpected status code 318 | * @throws IOException Thrown if an exception occurs while cleaning up. 319 | */ 320 | public void finish(boolean closeInputStream) throws ProtocolException, IOException { 321 | finishConnection(); 322 | if (upload.getSize() == offset) { 323 | client.uploadFinished(upload); 324 | } 325 | 326 | // Close the TusInputStream after checking the response and closing the connection to ensure 327 | // that we will not need to read from it again in the future. 328 | if (closeInputStream) { 329 | input.close(); 330 | } 331 | } 332 | 333 | private void finishConnection() throws ProtocolException, IOException { 334 | if (output != null) { 335 | output.close(); 336 | } 337 | 338 | if (connection != null) { 339 | int responseCode = connection.getResponseCode(); 340 | connection.disconnect(); 341 | 342 | if (!(responseCode >= 200 && responseCode < 300)) { 343 | throw new ProtocolException("unexpected status code (" + responseCode + ") while uploading chunk", 344 | connection); 345 | } 346 | 347 | // TODO detect changes and seek accordingly 348 | long serverOffset = getHeaderFieldLong(connection, "Upload-Offset"); 349 | if (serverOffset == -1) { 350 | throw new ProtocolException("response to PATCH request contains no or invalid Upload-Offset header", 351 | connection); 352 | } 353 | if (offset != serverOffset) { 354 | throw new ProtocolException( 355 | String.format("response contains different Upload-Offset value (%d) than expected (%d)", 356 | serverOffset, 357 | offset), 358 | connection); 359 | } 360 | 361 | connection = null; 362 | } 363 | } 364 | 365 | private long getHeaderFieldLong(URLConnection connection, String field) { 366 | String value = connection.getHeaderField(field); 367 | if (value == null) { 368 | return -1; 369 | } 370 | 371 | try { 372 | return Long.parseLong(value); 373 | } catch (NumberFormatException e) { 374 | return -1; 375 | } 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /src/main/java/io/tus/java/client/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This package provides all necessary classes for the TUS - Upload Client. 3 | */ 4 | package io.tus.java.client; 5 | -------------------------------------------------------------------------------- /src/main/resources/tus-java-client-version/version.properties: -------------------------------------------------------------------------------- 1 | versionNumber='0.5.1' 2 | -------------------------------------------------------------------------------- /src/test/java/io/tus/java/client/MockServerProvider.java: -------------------------------------------------------------------------------- 1 | package io.tus.java.client; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | import org.mockserver.client.MockServerClient; 6 | import org.mockserver.socket.PortFactory; 7 | 8 | import java.net.URL; 9 | 10 | import static org.mockserver.integration.ClientAndServer.startClientAndServer; 11 | 12 | /** 13 | * This class provides a MockServer. 14 | */ 15 | public class MockServerProvider { 16 | protected MockServerClient mockServer; 17 | protected URL mockServerURL; 18 | protected URL creationUrl; 19 | 20 | /** 21 | * Test configuration before running. 22 | * @throws Exception 23 | */ 24 | @Before 25 | public void setUp() throws Exception { 26 | creationUrl = new URL("http://tusd.tusdemo.net"); 27 | int port = PortFactory.findFreePort(); 28 | mockServerURL = new URL("http://localhost:" + port + "/files"); 29 | mockServer = startClientAndServer(port); 30 | } 31 | 32 | /** 33 | * Clean up after finishing the test-run. 34 | */ 35 | @After 36 | public void tearDown() { 37 | mockServer.stop(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/io/tus/java/client/TestTusClient.java: -------------------------------------------------------------------------------- 1 | package io.tus.java.client; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.IOException; 5 | import java.net.HttpURLConnection; 6 | import java.net.InetSocketAddress; 7 | import java.net.MalformedURLException; 8 | import java.net.Proxy; 9 | import java.net.Proxy.Type; 10 | import java.net.URL; 11 | import java.util.HashMap; 12 | import java.util.LinkedHashMap; 13 | import java.util.Map; 14 | 15 | import org.junit.Test; 16 | import org.mockserver.model.HttpRequest; 17 | import org.mockserver.model.HttpResponse; 18 | 19 | import static org.junit.Assert.assertEquals; 20 | import static org.junit.Assert.assertFalse; 21 | import static org.junit.Assert.assertNull; 22 | import static org.junit.Assert.assertTrue; 23 | import static org.junit.Assert.fail; 24 | 25 | /** 26 | * Class to test the tus-Client. 27 | */ 28 | public class TestTusClient extends MockServerProvider { 29 | 30 | /** 31 | * Tests if the client object is set up correctly. 32 | */ 33 | @Test 34 | public void testTusClient() { 35 | TusClient client = new TusClient(); 36 | assertNull(client.getUploadCreationURL()); 37 | } 38 | 39 | /** 40 | * Checks if upload URLS are set correctly. 41 | * @throws MalformedURLException if the provided URL is malformed. 42 | */ 43 | @Test 44 | public void testTusClientURL() throws MalformedURLException { 45 | TusClient client = new TusClient(); 46 | client.setUploadCreationURL(creationUrl); 47 | assertEquals(client.getUploadCreationURL(), creationUrl); 48 | } 49 | 50 | /** 51 | * Checks if upload URLS are set correctly. 52 | * @throws MalformedURLException if the provided URL is malformed. 53 | */ 54 | @Test 55 | public void testSetUploadCreationURL() throws MalformedURLException { 56 | TusClient client = new TusClient(); 57 | client.setUploadCreationURL(new URL("http://tusd.tusdemo.net")); 58 | assertEquals(client.getUploadCreationURL(), new URL("http://tusd.tusdemo.net")); 59 | } 60 | 61 | /** 62 | * Tests if resumable uploads can be turned off and on. 63 | */ 64 | @Test 65 | public void testEnableResuming() { 66 | TusClient client = new TusClient(); 67 | assertFalse(client.resumingEnabled()); 68 | 69 | TusURLStore store = new TusURLMemoryStore(); 70 | client.enableResuming(store); 71 | assertTrue(client.resumingEnabled()); 72 | 73 | client.disableResuming(); 74 | assertFalse(client.resumingEnabled()); 75 | } 76 | 77 | /** 78 | * Verifies if uploads can be created with the tus client. 79 | * @throws IOException if upload data cannot be read. 80 | * @throws ProtocolException if the upload cannot be constructed. 81 | */ 82 | @Test 83 | public void testCreateUpload() throws IOException, ProtocolException { 84 | mockServer.when(new HttpRequest() 85 | .withMethod("POST") 86 | .withPath("/files") 87 | .withHeader("Connection", "keep-alive") 88 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 89 | .withHeader("Upload-Metadata", "foo aGVsbG8=,bar d29ybGQ=") 90 | .withHeader("Upload-Length", "10")) 91 | .respond(new HttpResponse() 92 | .withStatusCode(201) 93 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 94 | .withHeader("Location", mockServerURL + "/foo")); 95 | 96 | Map metadata = new LinkedHashMap(); 97 | metadata.put("foo", "hello"); 98 | metadata.put("bar", "world"); 99 | 100 | TusClient client = new TusClient(); 101 | client.setUploadCreationURL(mockServerURL); 102 | TusUpload upload = new TusUpload(); 103 | upload.setSize(10); 104 | upload.setInputStream(new ByteArrayInputStream(new byte[10])); 105 | upload.setMetadata(metadata); 106 | TusUploader uploader = client.createUpload(upload); 107 | 108 | assertEquals(uploader.getUploadURL(), new URL(mockServerURL + "/foo")); 109 | } 110 | /** 111 | * Verifies if uploads can be created with the tus client through a proxy. 112 | * @throws IOException if upload data cannot be read. 113 | * @throws ProtocolException if the upload cannot be constructed. 114 | */ 115 | @Test 116 | public void testCreateUploadWithProxy() throws IOException, ProtocolException { 117 | mockServer.when(new HttpRequest() 118 | .withMethod("POST") 119 | .withPath("/files") 120 | .withHeader("Proxy-Connection", "keep-alive") 121 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 122 | .withHeader("Upload-Metadata", "foo aGVsbG8=,bar d29ybGQ=") 123 | .withHeader("Upload-Length", "11")) 124 | .respond(new HttpResponse() 125 | .withStatusCode(201) 126 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 127 | .withHeader("Location", mockServerURL + "/foo")); 128 | 129 | Map metadata = new LinkedHashMap(); 130 | metadata.put("foo", "hello"); 131 | metadata.put("bar", "world"); 132 | 133 | TusClient client = new TusClient(); 134 | client.setUploadCreationURL(mockServerURL); 135 | client.setProxy(new Proxy(Type.HTTP, new InetSocketAddress("localhost", mockServer.getPort()))); 136 | TusUpload upload = new TusUpload(); 137 | upload.setSize(11); 138 | upload.setInputStream(new ByteArrayInputStream(new byte[11])); 139 | upload.setMetadata(metadata); 140 | TusUploader uploader = client.createUpload(upload); 141 | 142 | assertEquals(uploader.getUploadURL(), new URL(mockServerURL + "/foo")); 143 | } 144 | 145 | /** 146 | * Tests if a missing location header causes an exception as expected. 147 | * @throws Exception if unreachable code has been reached. 148 | */ 149 | @Test 150 | public void testCreateUploadWithMissingLocationHeader() throws Exception { 151 | mockServer.when(new HttpRequest() 152 | .withMethod("POST") 153 | .withPath("/files") 154 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 155 | .withHeader("Upload-Length", "10")) 156 | .respond(new HttpResponse() 157 | .withStatusCode(201) 158 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)); 159 | 160 | TusClient client = new TusClient(); 161 | client.setUploadCreationURL(mockServerURL); 162 | TusUpload upload = new TusUpload(); 163 | upload.setSize(10); 164 | upload.setInputStream(new ByteArrayInputStream(new byte[10])); 165 | try { 166 | TusUploader uploader = client.createUpload(upload); 167 | throw new Exception("unreachable code reached"); 168 | } catch (ProtocolException e) { 169 | assertEquals(e.getMessage(), "missing upload URL in response for creating upload"); 170 | } 171 | } 172 | 173 | /** 174 | * Tests if uploads with relative upload destinations are working. 175 | * @throws Exception 176 | */ 177 | @Test 178 | public void testCreateUploadWithRelativeLocation() throws Exception { 179 | // We need to enable strict following for POST requests first 180 | System.setProperty("http.strictPostRedirect", "true"); 181 | 182 | // Attempt a real redirect 183 | mockServer.when(new HttpRequest() 184 | .withMethod("POST") 185 | .withPath("/filesRedirect") 186 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 187 | .withHeader("Upload-Length", "10")) 188 | .respond(new HttpResponse() 189 | .withStatusCode(301) 190 | .withHeader("Location", mockServerURL + "Redirected/")); 191 | 192 | mockServer.when(new HttpRequest() 193 | .withMethod("POST") 194 | .withPath("/filesRedirected/") 195 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 196 | .withHeader("Upload-Length", "10")) 197 | .respond(new HttpResponse() 198 | .withStatusCode(201) 199 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 200 | .withHeader("Location", "foo")); 201 | 202 | TusClient client = new TusClient(); 203 | client.setUploadCreationURL(new URL(mockServerURL + "Redirect")); 204 | TusUpload upload = new TusUpload(); 205 | upload.setSize(10); 206 | upload.setInputStream(new ByteArrayInputStream(new byte[10])); 207 | TusUploader uploader = client.createUpload(upload); 208 | 209 | // The upload URL must be relative to the URL of the request by which it was returned, 210 | // not the upload creation URL. In most cases, there is no difference between those two, 211 | // but it's still important to be correct here. 212 | assertEquals(uploader.getUploadURL(), new URL(mockServerURL + "Redirected/foo")); 213 | } 214 | 215 | /** 216 | * Tests if {@link TusClient#resumeUpload(TusUpload)} works. 217 | * @throws ResumingNotEnabledException 218 | * @throws FingerprintNotFoundException 219 | * @throws IOException 220 | * @throws ProtocolException 221 | */ 222 | @Test 223 | public void testResumeUpload() throws ResumingNotEnabledException, FingerprintNotFoundException, IOException, 224 | ProtocolException { 225 | mockServer.when(new HttpRequest() 226 | .withMethod("HEAD") 227 | .withPath("/files/foo") 228 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)) 229 | .respond(new HttpResponse() 230 | .withStatusCode(204) 231 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 232 | .withHeader("Upload-Offset", "3")); 233 | 234 | TusClient client = new TusClient(); 235 | client.setUploadCreationURL(mockServerURL); 236 | client.enableResuming(new TestResumeUploadStore()); 237 | 238 | TusUpload upload = new TusUpload(); 239 | upload.setSize(10); 240 | upload.setInputStream(new ByteArrayInputStream(new byte[10])); 241 | upload.setFingerprint("test-fingerprint"); 242 | 243 | TusUploader uploader = client.resumeUpload(upload); 244 | 245 | assertEquals(uploader.getUploadURL(), new URL(mockServerURL.toString() + "/foo")); 246 | assertEquals(uploader.getOffset(), 3); 247 | } 248 | 249 | /** 250 | * Test Implementation for a {@link TusURLStore}. 251 | */ 252 | private final class TestResumeUploadStore implements TusURLStore { 253 | public void set(String fingerprint, URL url) { 254 | fail("set method must not be called"); 255 | } 256 | 257 | public URL get(String fingerprint) { 258 | assertEquals(fingerprint, "test-fingerprint"); 259 | 260 | try { 261 | return new URL(mockServerURL.toString() + "/foo"); 262 | } catch (Exception ignored) { } 263 | return null; 264 | } 265 | 266 | public void remove(String fingerprint) { 267 | fail("remove method must not be called"); 268 | } 269 | } 270 | 271 | /** 272 | * Tests if an upload gets started if {@link TusClient#resumeOrCreateUpload(TusUpload)} gets called. 273 | * @throws IOException 274 | * @throws ProtocolException 275 | */ 276 | @Test 277 | public void testResumeOrCreateUpload() throws IOException, ProtocolException { 278 | mockServer.when(new HttpRequest() 279 | .withMethod("POST") 280 | .withPath("/files") 281 | .withHeader("Connection", "keep-alive") 282 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 283 | .withHeader("Upload-Length", "10")) 284 | .respond(new HttpResponse() 285 | .withStatusCode(201) 286 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 287 | .withHeader("Location", mockServerURL + "/foo")); 288 | 289 | TusClient client = new TusClient(); 290 | client.setUploadCreationURL(mockServerURL); 291 | TusUpload upload = new TusUpload(); 292 | upload.setSize(10); 293 | upload.setInputStream(new ByteArrayInputStream(new byte[10])); 294 | TusUploader uploader = client.resumeOrCreateUpload(upload); 295 | 296 | assertEquals(uploader.getUploadURL(), new URL(mockServerURL + "/foo")); 297 | } 298 | 299 | /** 300 | * Tests if an upload gets started when {@link TusClient#resumeOrCreateUpload(TusUpload)} gets called with 301 | * a proxy set. 302 | * @throws IOException 303 | * @throws ProtocolException 304 | */ 305 | @Test 306 | public void testResumeOrCreateUploadWithProxy() throws IOException, ProtocolException { 307 | mockServer.when(new HttpRequest() 308 | .withMethod("POST") 309 | .withPath("/files") 310 | .withHeader("Proxy-Connection", "keep-alive") 311 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 312 | .withHeader("Upload-Length", "11")) 313 | .respond(new HttpResponse() 314 | .withStatusCode(201) 315 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 316 | .withHeader("Location", mockServerURL + "/foo")); 317 | 318 | TusClient client = new TusClient(); 319 | client.setUploadCreationURL(mockServerURL); 320 | Proxy proxy = new Proxy(Type.HTTP, new InetSocketAddress("localhost", mockServer.getPort())); 321 | client.setProxy(proxy); 322 | TusUpload upload = new TusUpload(); 323 | upload.setSize(11); 324 | upload.setInputStream(new ByteArrayInputStream(new byte[11])); 325 | TusUploader uploader = client.resumeOrCreateUpload(upload); 326 | 327 | assertEquals(proxy, client.getProxy()); 328 | assertEquals(uploader.getUploadURL(), new URL(mockServerURL + "/foo")); 329 | } 330 | 331 | /** 332 | * Checks if a new upload attempt is started in case of a serverside 404-error, without having an Exception thrown. 333 | * @throws IOException 334 | * @throws ProtocolException 335 | */ 336 | @Test 337 | public void testResumeOrCreateUploadNotFound() throws IOException, ProtocolException { 338 | mockServer.when(new HttpRequest() 339 | .withMethod("HEAD") 340 | .withPath("/files/not_found") 341 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)) 342 | .respond(new HttpResponse() 343 | .withStatusCode(404)); 344 | 345 | mockServer.when(new HttpRequest() 346 | .withMethod("POST") 347 | .withPath("/files") 348 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 349 | .withHeader("Upload-Length", "10")) 350 | .respond(new HttpResponse() 351 | .withStatusCode(201) 352 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 353 | .withHeader("Location", mockServerURL + "/foo")); 354 | 355 | TusClient client = new TusClient(); 356 | client.setUploadCreationURL(mockServerURL); 357 | 358 | TusURLStore store = new TusURLMemoryStore(); 359 | store.set("fingerprint", new URL(mockServerURL + "/not_found")); 360 | client.enableResuming(store); 361 | 362 | TusUpload upload = new TusUpload(); 363 | upload.setSize(10); 364 | upload.setInputStream(new ByteArrayInputStream(new byte[10])); 365 | upload.setFingerprint("fingerprint"); 366 | TusUploader uploader = client.resumeOrCreateUpload(upload); 367 | 368 | assertEquals(uploader.getUploadURL(), new URL(mockServerURL + "/foo")); 369 | } 370 | 371 | /** 372 | * Tests if {@link TusClient#beginOrResumeUploadFromURL(TusUpload, URL)} works. 373 | * @throws IOException 374 | * @throws ProtocolException 375 | */ 376 | @Test 377 | public void testBeginOrResumeUploadFromURL() throws IOException, ProtocolException { 378 | mockServer.when(new HttpRequest() 379 | .withMethod("HEAD") 380 | .withPath("/files/fooFromURL") 381 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)) 382 | .respond(new HttpResponse() 383 | .withStatusCode(204) 384 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 385 | .withHeader("Upload-Offset", "3")); 386 | 387 | TusClient client = new TusClient(); 388 | URL uploadURL = new URL(mockServerURL.toString() + "/fooFromURL"); 389 | 390 | TusUpload upload = new TusUpload(); 391 | upload.setSize(10); 392 | upload.setInputStream(new ByteArrayInputStream(new byte[10])); 393 | 394 | TusUploader uploader = client.beginOrResumeUploadFromURL(upload, uploadURL); 395 | 396 | assertEquals(uploader.getUploadURL(), uploadURL); 397 | assertEquals(uploader.getOffset(), 3); 398 | } 399 | 400 | /** 401 | * Tests if connections are prepared correctly, which means all header are getting set. 402 | * @throws IOException 403 | */ 404 | @Test 405 | public void testPrepareConnection() throws IOException { 406 | HttpURLConnection connection = (HttpURLConnection) mockServerURL.openConnection(); 407 | TusClient client = new TusClient(); 408 | client.prepareConnection(connection); 409 | 410 | assertEquals(connection.getRequestProperty("Tus-Resumable"), TusClient.TUS_VERSION); 411 | } 412 | 413 | /** 414 | * Tests if HTTP - Headers are set correctly. 415 | * @throws IOException 416 | */ 417 | @Test 418 | public void testSetHeaders() throws IOException { 419 | HttpURLConnection connection = (HttpURLConnection) mockServerURL.openConnection(); 420 | TusClient client = new TusClient(); 421 | 422 | Map headers = new HashMap(); 423 | headers.put("Greeting", "Hello"); 424 | headers.put("Important", "yes"); 425 | headers.put("Tus-Resumable", "evil"); 426 | 427 | assertNull(client.getHeaders()); 428 | client.setHeaders(headers); 429 | assertEquals(headers, client.getHeaders()); 430 | 431 | client.prepareConnection(connection); 432 | 433 | assertEquals(connection.getRequestProperty("Greeting"), "Hello"); 434 | assertEquals(connection.getRequestProperty("Important"), "yes"); 435 | } 436 | 437 | /** 438 | * Tests if connection timeouts are set correctly. 439 | * @throws IOException 440 | */ 441 | @Test 442 | public void testSetConnectionTimeout() throws IOException { 443 | HttpURLConnection connection = (HttpURLConnection) mockServerURL.openConnection(); 444 | TusClient client = new TusClient(); 445 | 446 | assertEquals(client.getConnectTimeout(), 5000); 447 | client.setConnectTimeout(3000); 448 | assertEquals(client.getConnectTimeout(), 3000); 449 | 450 | client.prepareConnection(connection); 451 | 452 | assertEquals(connection.getConnectTimeout(), 3000); 453 | } 454 | 455 | /** 456 | * Tests whether the connection follows redirects only after explicitly enabling this feature. 457 | * @throws Exception 458 | */ 459 | @Test 460 | public void testFollowRedirects() throws Exception { 461 | HttpURLConnection connection = (HttpURLConnection) mockServerURL.openConnection(); 462 | TusClient client = new TusClient(); 463 | 464 | // Should not follow by default 465 | client.prepareConnection(connection); 466 | assertFalse(connection.getInstanceFollowRedirects()); 467 | 468 | // Only follow if we enable strict redirects 469 | System.setProperty("http.strictPostRedirect", "true"); 470 | client.prepareConnection(connection); 471 | assertTrue(connection.getInstanceFollowRedirects()); 472 | 473 | // Attempt a real redirect 474 | mockServer.when(new HttpRequest() 475 | .withMethod("POST") 476 | .withPath("/filesRedirect") 477 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 478 | .withHeader("Upload-Length", "10")) 479 | .respond(new HttpResponse() 480 | .withStatusCode(301) 481 | .withHeader("Location", mockServerURL + "Redirected")); 482 | 483 | mockServer.when(new HttpRequest() 484 | .withMethod("POST") 485 | .withPath("/filesRedirected") 486 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 487 | .withHeader("Upload-Length", "10")) 488 | .respond(new HttpResponse() 489 | .withStatusCode(201) 490 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 491 | .withHeader("Location", mockServerURL + "/foo")); 492 | 493 | client.setUploadCreationURL(new URL(mockServerURL + "Redirect")); 494 | TusUpload upload = new TusUpload(); 495 | upload.setSize(10); 496 | upload.setInputStream(new ByteArrayInputStream(new byte[10])); 497 | TusUploader uploader = client.createUpload(upload); 498 | 499 | assertEquals(uploader.getUploadURL(), new URL(mockServerURL + "/foo")); 500 | } 501 | 502 | /** 503 | * Tests if the fingerprint in the {@link TusURLStore} does not get removed after upload success. 504 | * @throws IOException 505 | * @throws ProtocolException 506 | */ 507 | @Test 508 | public void testRemoveFingerprintOnSuccessDisabled() throws IOException, ProtocolException { 509 | 510 | TusClient client = new TusClient(); 511 | 512 | TusURLStore store = new TusURLMemoryStore(); 513 | URL dummyURL = new URL("http://dummy-url/files/dummy"); 514 | store.set("fingerprint", dummyURL); 515 | client.enableResuming(store); 516 | 517 | assertFalse(client.removeFingerprintOnSuccessEnabled()); 518 | 519 | TusUpload upload = new TusUpload(); 520 | upload.setFingerprint("fingerprint"); 521 | 522 | client.uploadFinished(upload); 523 | 524 | assertEquals(dummyURL, store.get("fingerprint")); 525 | 526 | } 527 | 528 | /** 529 | * Tests if the fingerprint in the {@link TusURLStore} does get removed after upload success, 530 | * after this feature has been enabled via the {@link TusClient#enableRemoveFingerprintOnSuccess()} - method. 531 | * @throws IOException 532 | * @throws ProtocolException 533 | */ 534 | @Test 535 | public void testRemoveFingerprintOnSuccessEnabled() throws IOException, ProtocolException { 536 | 537 | TusClient client = new TusClient(); 538 | 539 | TusURLStore store = new TusURLMemoryStore(); 540 | URL dummyURL = new URL("http://dummy-url/files/dummy"); 541 | store.set("fingerprint", dummyURL); 542 | client.enableResuming(store); 543 | client.enableRemoveFingerprintOnSuccess(); 544 | 545 | assertTrue(client.removeFingerprintOnSuccessEnabled()); 546 | 547 | TusUpload upload = new TusUpload(); 548 | upload.setFingerprint("fingerprint"); 549 | 550 | client.uploadFinished(upload); 551 | 552 | assertNull(store.get("fingerprint")); 553 | 554 | } 555 | } 556 | -------------------------------------------------------------------------------- /src/test/java/io/tus/java/client/TestTusExecutor.java: -------------------------------------------------------------------------------- 1 | package io.tus.java.client; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.IOException; 6 | import java.net.HttpURLConnection; 7 | import java.net.MalformedURLException; 8 | import java.net.URL; 9 | 10 | import static org.junit.Assert.assertArrayEquals; 11 | import static org.junit.Assert.assertEquals; 12 | import static org.junit.Assert.assertFalse; 13 | import static org.junit.Assert.assertTrue; 14 | 15 | /** 16 | * Test class for a {@link TusExecutor}. 17 | */ 18 | public class TestTusExecutor { 19 | 20 | /** 21 | * Tests if the delays for connection attempts are set in the right manner. 22 | */ 23 | @Test 24 | public void testSetDelays() { 25 | CountingExecutor exec = new CountingExecutor(); 26 | 27 | assertArrayEquals(exec.getDelays(), new int[]{500, 1000, 2000, 3000}); 28 | exec.setDelays(new int[]{1, 2, 3}); 29 | assertArrayEquals(exec.getDelays(), new int[]{1, 2, 3}); 30 | assertEquals(exec.getCalls(), 0); 31 | } 32 | 33 | /** 34 | * Tests if a running execution is interuptable. 35 | * @throws Exception 36 | */ 37 | @Test 38 | public void testInterrupting() throws Exception { 39 | TusExecutor exec = new TusExecutor() { 40 | @Override 41 | protected void makeAttempt() throws ProtocolException, IOException { 42 | throw new IOException(); 43 | } 44 | }; 45 | 46 | exec.setDelays(new int[]{100000}); 47 | 48 | final Thread executorThread = Thread.currentThread(); 49 | Thread waiterThread = new Thread(new Runnable() { 50 | @Override 51 | public void run() { 52 | try { 53 | Thread.sleep(100); 54 | executorThread.interrupt(); 55 | } catch (InterruptedException e) { 56 | e.printStackTrace(); 57 | } 58 | } 59 | }); 60 | waiterThread.start(); 61 | 62 | assertFalse(exec.makeAttempts()); 63 | } 64 | 65 | 66 | /** 67 | * Tests if {@link TusExecutor#makeAttempts()} actually makes attempts. 68 | * @throws Exception 69 | */ 70 | @Test 71 | public void testMakeAttempts() throws Exception { 72 | CountingExecutor exec = new CountingExecutor(); 73 | 74 | exec.setDelays(new int[]{1, 2, 3}); 75 | assertTrue(exec.makeAttempts()); 76 | assertEquals(exec.getCalls(), 1); 77 | } 78 | 79 | 80 | /** 81 | * Tests if every attempt can throw an IOException. 82 | * @throws Exception 83 | */ 84 | @Test(expected = IOException.class) 85 | public void testMakeAllAttemptsThrowIOException() throws Exception { 86 | CountingExecutor exec = new CountingExecutor() { 87 | @Override 88 | protected void makeAttempt() throws ProtocolException, IOException { 89 | super.makeAttempt(); 90 | throw new IOException(); 91 | } 92 | }; 93 | 94 | exec.setDelays(new int[]{1, 2, 3}); 95 | try { 96 | exec.makeAttempts(); 97 | } finally { 98 | assertEquals(exec.getCalls(), 4); 99 | } 100 | } 101 | 102 | /** 103 | * Tests if every attempt can throw a {@link ProtocolException}. 104 | * @throws Exception 105 | */ 106 | @Test(expected = ProtocolException.class) 107 | public void testMakeAllAttemptsThrowProtocolException() throws Exception { 108 | CountingExecutor exec = new CountingExecutor() { 109 | @Override 110 | protected void makeAttempt() throws ProtocolException, IOException { 111 | super.makeAttempt(); 112 | throw new ProtocolException("something happened", new MockHttpURLConnection(500)); 113 | } 114 | }; 115 | 116 | exec.setDelays(new int[]{1, 2, 3}); 117 | try { 118 | exec.makeAttempts(); 119 | } finally { 120 | assertEquals(exec.getCalls(), 4); 121 | } 122 | } 123 | 124 | /** 125 | * Tests if an Exception can be thrown also at single attempts. 126 | * @throws Exception 127 | */ 128 | @Test(expected = ProtocolException.class) 129 | public void testMakeOneAttempt() throws Exception { 130 | CountingExecutor exec = new CountingExecutor() { 131 | @Override 132 | protected void makeAttempt() throws ProtocolException, IOException { 133 | super.makeAttempt(); 134 | throw new ProtocolException("something happened", new MockHttpURLConnection(404)); 135 | } 136 | }; 137 | 138 | exec.setDelays(new int[]{1, 2, 3}); 139 | try { 140 | exec.makeAttempts(); 141 | } finally { 142 | assertEquals(exec.getCalls(), 1); 143 | } 144 | } 145 | 146 | /** 147 | * A mocked HttpURLConnection which always returns the specified response code. 148 | */ 149 | private class MockHttpURLConnection extends HttpURLConnection { 150 | private int statusCode; 151 | 152 | MockHttpURLConnection(int statusCode) throws MalformedURLException { 153 | super(new URL("http://localhost/")); 154 | this.statusCode = statusCode; 155 | } 156 | 157 | @Override 158 | public int getResponseCode() { 159 | return statusCode; 160 | } 161 | 162 | @Override 163 | public boolean usingProxy() { 164 | return false; 165 | } 166 | 167 | @Override 168 | public void disconnect() { } 169 | 170 | @Override 171 | public void connect() { } 172 | } 173 | 174 | /** 175 | * A TusExecutor implementation which counts the calls to makeAttempt(). 176 | */ 177 | private class CountingExecutor extends TusExecutor { 178 | private int calls; 179 | 180 | @Override 181 | protected void makeAttempt() throws ProtocolException, IOException { 182 | calls++; 183 | } 184 | 185 | public int getCalls() { 186 | return calls; 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/test/java/io/tus/java/client/TestTusURLMemoryStore.java: -------------------------------------------------------------------------------- 1 | package io.tus.java.client; 2 | 3 | import java.net.MalformedURLException; 4 | import java.net.URL; 5 | 6 | import org.junit.Test; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | 10 | /** 11 | * Test class for {@link TusURLMemoryStore}. 12 | */ 13 | public class TestTusURLMemoryStore { 14 | 15 | /** 16 | * Tests if setting and deleting of an url in the {@link TusURLMemoryStore} works. 17 | * @throws MalformedURLException 18 | */ 19 | @Test 20 | public void test() throws MalformedURLException { 21 | TusURLStore store = new TusURLMemoryStore(); 22 | URL url = new URL("https://tusd.tusdemo.net/files/hello"); 23 | String fingerprint = "foo"; 24 | store.set(fingerprint, url); 25 | 26 | assertEquals(store.get(fingerprint), url); 27 | 28 | store.remove(fingerprint); 29 | 30 | assertEquals(store.get(fingerprint), null); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/io/tus/java/client/TestTusUpload.java: -------------------------------------------------------------------------------- 1 | package io.tus.java.client; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | import static org.junit.Assert.assertNotSame; 7 | 8 | import java.io.File; 9 | import java.io.FileOutputStream; 10 | import java.io.IOException; 11 | import java.io.OutputStream; 12 | import java.util.LinkedHashMap; 13 | import java.util.Map; 14 | 15 | /** 16 | * Test class for {@link TusUpload}. 17 | */ 18 | public class TestTusUpload { 19 | /** 20 | * Tests if uploading a file works. 21 | * @throws IOException 22 | */ 23 | @Test 24 | public void testTusUploadFile() throws IOException { 25 | String content = "hello world"; 26 | 27 | File file = File.createTempFile("tus-upload-test", ".tmp"); 28 | OutputStream output = new FileOutputStream(file); 29 | output.write(content.getBytes()); 30 | output.close(); 31 | 32 | TusUpload upload = new TusUpload(file); 33 | 34 | Map metadata = new LinkedHashMap(); 35 | metadata.put("foo", "hello"); 36 | metadata.put("bar", "world"); 37 | metadata.putAll(upload.getMetadata()); 38 | 39 | assertEquals(metadata.get("filename"), file.getName()); 40 | 41 | upload.setMetadata(metadata); 42 | assertEquals(upload.getMetadata(), metadata); 43 | assertEquals( 44 | upload.getEncodedMetadata(), 45 | "foo aGVsbG8=,bar d29ybGQ=,filename " + TusUpload.base64Encode(file.getName().getBytes())); 46 | 47 | assertEquals(upload.getSize(), content.length()); 48 | assertNotSame(upload.getFingerprint(), ""); 49 | byte[] readContent = new byte[content.length()]; 50 | assertEquals(upload.getInputStream().read(readContent), content.length()); 51 | assertEquals(new String(readContent), content); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/io/tus/java/client/TestTusUploader.java: -------------------------------------------------------------------------------- 1 | package io.tus.java.client; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertTrue; 5 | import static org.mockito.Mockito.mock; 6 | import static org.mockito.Mockito.verify; 7 | import static org.mockito.Mockito.times; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.ByteArrayInputStream; 11 | import java.io.IOException; 12 | import java.io.InputStreamReader; 13 | import java.io.OutputStream; 14 | import java.net.InetSocketAddress; 15 | import java.net.MalformedURLException; 16 | import java.net.Proxy; 17 | import java.net.Proxy.Type; 18 | import java.net.ServerSocket; 19 | import java.net.Socket; 20 | import java.net.URL; 21 | import java.util.Arrays; 22 | 23 | import org.junit.Assume; 24 | import org.junit.Test; 25 | import org.mockserver.model.HttpRequest; 26 | import org.mockserver.model.HttpResponse; 27 | import org.mockserver.socket.PortFactory; 28 | 29 | /** 30 | * Test class for {@link TusUploader}. 31 | */ 32 | public class TestTusUploader extends MockServerProvider { 33 | private boolean isOpenJDK6 = System.getProperty("java.version").startsWith("1.6") 34 | && System.getProperty("java.vm.name").contains("OpenJDK"); 35 | 36 | /** 37 | * Tests if the {@link TusUploader} actually uploads files and fixed chunk sizes. 38 | * @throws IOException 39 | * @throws ProtocolException 40 | */ 41 | @Test 42 | public void testTusUploader() throws IOException, ProtocolException { 43 | byte[] content = "hello world".getBytes(); 44 | 45 | mockServer.when(new HttpRequest() 46 | .withPath("/files/foo") 47 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 48 | .withHeader("Upload-Offset", "3") 49 | .withHeader("Content-Type", "application/offset+octet-stream") 50 | .withHeader("Connection", "keep-alive") 51 | .withBody(Arrays.copyOfRange(content, 3, 11))) 52 | .respond(new HttpResponse() 53 | .withStatusCode(204) 54 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 55 | .withHeader("Upload-Offset", "11")); 56 | 57 | TusClient client = new TusClient(); 58 | URL uploadUrl = new URL(mockServerURL + "/foo"); 59 | TusInputStream input = new TusInputStream(new ByteArrayInputStream(content)); 60 | long offset = 3; 61 | 62 | TusUpload upload = new TusUpload(); 63 | 64 | TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, offset); 65 | 66 | uploader.setChunkSize(5); 67 | assertEquals(uploader.getChunkSize(), 5); 68 | 69 | assertEquals(5, uploader.uploadChunk()); 70 | assertEquals(3, uploader.uploadChunk(5)); 71 | assertEquals(-1, uploader.uploadChunk()); 72 | assertEquals(-1, uploader.uploadChunk(5)); 73 | assertEquals(11, uploader.getOffset()); 74 | uploader.finish(); 75 | } 76 | 77 | /** 78 | * Tests if the {@link TusUploader} actually uploads files through a proxy. 79 | * @throws IOException 80 | * @throws ProtocolException 81 | */ 82 | @Test 83 | public void testTusUploaderWithProxy() throws IOException, ProtocolException { 84 | byte[] content = "hello world with proxy".getBytes(); 85 | 86 | mockServer.when(new HttpRequest() 87 | .withPath("/files/foo") 88 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 89 | .withHeader("Upload-Offset", "0") 90 | .withHeader("Content-Type", "application/offset+octet-stream") 91 | .withHeader("Proxy-Connection", "keep-alive") 92 | .withBody(Arrays.copyOf(content, content.length))) 93 | .respond(new HttpResponse() 94 | .withStatusCode(204) 95 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 96 | .withHeader("Upload-Offset", "22")); 97 | 98 | TusClient client = new TusClient(); 99 | URL uploadUrl = new URL(mockServerURL + "/foo"); 100 | Proxy proxy = new Proxy(Type.HTTP, new InetSocketAddress("localhost", mockServer.getPort())); 101 | TusInputStream input = new TusInputStream(new ByteArrayInputStream(content)); 102 | long offset = 0; 103 | 104 | TusUpload upload = new TusUpload(); 105 | 106 | TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, offset); 107 | uploader.setProxy(proxy); 108 | 109 | assertEquals(proxy, uploader.getProxy()); 110 | assertEquals(22, uploader.uploadChunk()); 111 | uploader.finish(); 112 | } 113 | 114 | /** 115 | * Verifies, that {@link TusClient#uploadFinished(TusUpload)} gets called after a proper upload has been finished. 116 | * @throws IOException 117 | * @throws ProtocolException 118 | */ 119 | @Test 120 | public void testTusUploaderClientUploadFinishedCalled() throws IOException, ProtocolException { 121 | 122 | TusClient client = mock(TusClient.class); 123 | 124 | byte[] content = "hello world".getBytes(); 125 | 126 | URL uploadUrl = new URL("http://dummy-url/foo"); 127 | TusInputStream input = new TusInputStream(new ByteArrayInputStream(content)); 128 | long offset = 10; 129 | 130 | TusUpload upload = new TusUpload(); 131 | upload.setSize(10); 132 | 133 | TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, offset); 134 | uploader.finish(); 135 | 136 | // size and offset are the same, so uploadfinished() should be called 137 | verify(client).uploadFinished(upload); 138 | } 139 | 140 | /** 141 | * Verifies, that {@link TusClient#uploadFinished(TusUpload)} doesn't get called if the actual upload size is 142 | * greater than the offset. 143 | * @throws IOException 144 | * @throws ProtocolException 145 | */ 146 | @Test 147 | public void testTusUploaderClientUploadFinishedNotCalled() throws IOException, ProtocolException { 148 | 149 | TusClient client = mock(TusClient.class); 150 | 151 | byte[] content = "hello world".getBytes(); 152 | 153 | URL uploadUrl = new URL("http://dummy-url/foo"); 154 | TusInputStream input = new TusInputStream(new ByteArrayInputStream(content)); 155 | long offset = 0; 156 | 157 | TusUpload upload = new TusUpload(); 158 | upload.setSize(10); 159 | 160 | TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, offset); 161 | uploader.finish(); 162 | 163 | // size is greater than offset, so uploadfinished() should not be called 164 | verify(client, times(0)).uploadFinished(upload); 165 | } 166 | 167 | /** 168 | * Verifies, that an Exception gets thrown, if the upload server isn't satisfied with the client's headers. 169 | * @throws IOException 170 | * @throws ProtocolException 171 | */ 172 | @Test 173 | public void testTusUploaderFailedExpectation() throws IOException, ProtocolException { 174 | Assume.assumeFalse(isOpenJDK6); 175 | 176 | FailingExpectationServer server = new FailingExpectationServer(); 177 | server.start(); 178 | 179 | byte[] content = "hello world".getBytes(); 180 | 181 | TusClient client = new TusClient(); 182 | URL uploadUrl = new URL(server.getURL() + "/expect"); 183 | TusInputStream input = new TusInputStream(new ByteArrayInputStream(content)); 184 | long offset = 3; 185 | TusUpload upload = new TusUpload(); 186 | boolean exceptionThrown = false; 187 | TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, offset); 188 | try { 189 | uploader.uploadChunk(); 190 | } catch (ProtocolException e) { 191 | assertTrue(e.getMessage().contains("500")); 192 | exceptionThrown = true; 193 | } finally { 194 | assertTrue(exceptionThrown); 195 | } 196 | } 197 | 198 | /** 199 | * FailingExpectationServer is a HTTP/1.1 server which will always respond with a 500 Internal 200 | * Error. This is meant to simulate failing expectations when the request contains the 201 | * expected header. The org.mockserver packages do not support this and will always send the 202 | * 100 Continue status code. therefore, we built our own stupid mocking server. 203 | */ 204 | private class FailingExpectationServer extends Thread { 205 | private final byte[] response = "HTTP/1.1 500 Internal Server Error\r\n\r\n".getBytes(); 206 | private ServerSocket serverSocket; 207 | private int port; 208 | 209 | FailingExpectationServer() throws IOException { 210 | port = PortFactory.findFreePort(); 211 | 212 | serverSocket = new ServerSocket(port); 213 | } 214 | 215 | @Override 216 | public void run() { 217 | try { 218 | Socket socket = serverSocket.accept(); 219 | 220 | OutputStream output = socket.getOutputStream(); 221 | BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); 222 | while (!input.readLine().isEmpty()) { 223 | output.write(response); 224 | break; 225 | } 226 | 227 | socket.close(); 228 | } catch (IOException e) { 229 | e.printStackTrace(); 230 | } 231 | } 232 | 233 | public URL getURL() { 234 | try { 235 | return new URL("http://localhost:" + port); 236 | } catch (MalformedURLException e) { 237 | return null; 238 | } 239 | } 240 | } 241 | 242 | /** 243 | * Verifies, that {@link TusUploader#setRequestPayloadSize(int)} effectively limits the size a payload. 244 | * @throws Exception 245 | */ 246 | @Test 247 | public void testSetRequestPayloadSize() throws Exception { 248 | byte[] content = "hello world".getBytes(); 249 | 250 | mockServer.when(new HttpRequest() 251 | .withPath("/files/payload") 252 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 253 | .withHeader("Upload-Offset", "0") 254 | .withHeader("Content-Type", "application/offset+octet-stream") 255 | .withBody(Arrays.copyOfRange(content, 0, 5))) 256 | .respond(new HttpResponse() 257 | .withStatusCode(204) 258 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 259 | .withHeader("Upload-Offset", "5")); 260 | 261 | mockServer.when(new HttpRequest() 262 | .withPath("/files/payload") 263 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 264 | .withHeader("Upload-Offset", "5") 265 | .withHeader("Content-Type", "application/offset+octet-stream") 266 | .withBody(Arrays.copyOfRange(content, 5, 10))) 267 | .respond(new HttpResponse() 268 | .withStatusCode(204) 269 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 270 | .withHeader("Upload-Offset", "10")); 271 | 272 | mockServer.when(new HttpRequest() 273 | .withPath("/files/payload") 274 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 275 | .withHeader("Upload-Offset", "10") 276 | .withHeader("Content-Type", "application/offset+octet-stream") 277 | .withBody(Arrays.copyOfRange(content, 10, 11))) 278 | .respond(new HttpResponse() 279 | .withStatusCode(204) 280 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 281 | .withHeader("Upload-Offset", "11")); 282 | 283 | TusClient client = new TusClient(); 284 | URL uploadUrl = new URL(mockServerURL + "/payload"); 285 | TusInputStream input = new TusInputStream(new ByteArrayInputStream(content)); 286 | TusUpload upload = new TusUpload(); 287 | 288 | TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, 0); 289 | 290 | assertEquals(uploader.getRequestPayloadSize(), 10 * 1024 * 1024); 291 | uploader.setRequestPayloadSize(5); 292 | assertEquals(uploader.getRequestPayloadSize(), 5); 293 | 294 | uploader.setChunkSize(4); 295 | 296 | // First request 297 | assertEquals(4, uploader.uploadChunk()); 298 | assertEquals(1, uploader.uploadChunk()); 299 | 300 | // Second request 301 | uploader.setChunkSize(100); 302 | assertEquals(5, uploader.uploadChunk()); 303 | 304 | // Third request 305 | assertEquals(1, uploader.uploadChunk()); 306 | uploader.finish(); 307 | } 308 | 309 | 310 | /** 311 | * Verifies, that an exception is thrown if {@link TusUploader#setRequestPayloadSize(int)} is called while the 312 | * client has already an upload connection opened. 313 | * @throws Exception 314 | */ 315 | @Test(expected = IllegalStateException.class) 316 | public void testSetRequestPayloadSizeThrows() throws Exception { 317 | byte[] content = "hello world".getBytes(); 318 | 319 | TusClient client = new TusClient(); 320 | URL uploadUrl = new URL(mockServerURL + "/payloadException"); 321 | TusInputStream input = new TusInputStream(new ByteArrayInputStream(content)); 322 | TusUpload upload = new TusUpload(); 323 | 324 | TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, 0); 325 | 326 | uploader.setChunkSize(4); 327 | uploader.uploadChunk(); 328 | 329 | // Throws IllegalStateException 330 | uploader.setRequestPayloadSize(100); 331 | } 332 | 333 | /** 334 | * Verifies, that an Exception is thrown if the UploadOffsetHeader is missing. 335 | * @throws Exception 336 | */ 337 | @Test 338 | public void testMissingUploadOffsetHeader() throws Exception { 339 | byte[] content = "hello world".getBytes(); 340 | 341 | mockServer.when(new HttpRequest() 342 | .withPath("/files/missingHeader")) 343 | .respond(new HttpResponse() 344 | .withStatusCode(204) 345 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)); 346 | 347 | TusClient client = new TusClient(); 348 | URL uploadUrl = new URL(mockServerURL + "/missingHeader"); 349 | TusInputStream input = new TusInputStream(new ByteArrayInputStream(content)); 350 | TusUpload upload = new TusUpload(); 351 | 352 | TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, 0); 353 | 354 | boolean exceptionThrown = false; 355 | try { 356 | assertEquals(11, uploader.uploadChunk()); 357 | uploader.finish(); 358 | } catch (ProtocolException e) { 359 | assertTrue(e.getMessage().contains("no or invalid Upload-Offset header")); 360 | exceptionThrown = true; 361 | } finally { 362 | assertTrue(exceptionThrown); 363 | } 364 | } 365 | 366 | /** 367 | * Verifies, that an Exception is thrown if the UploadOffsetHeader of the server's response does not match the 368 | * clients upload offset value. 369 | * @throws Exception 370 | */ 371 | @Test 372 | public void testUnmatchingUploadOffsetHeader() throws Exception { 373 | byte[] content = "hello world".getBytes(); 374 | 375 | mockServer.when(new HttpRequest() 376 | .withPath("/files/unmatchingHeader")) 377 | .respond(new HttpResponse() 378 | .withStatusCode(204) 379 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION) 380 | .withHeader("Upload-Offset", "44")); 381 | 382 | TusClient client = new TusClient(); 383 | URL uploadUrl = new URL(mockServerURL + "/unmatchingHeader"); 384 | TusInputStream input = new TusInputStream(new ByteArrayInputStream(content)); 385 | TusUpload upload = new TusUpload(); 386 | 387 | TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, 0); 388 | 389 | boolean exceptionThrown = false; 390 | try { 391 | assertEquals(11, uploader.uploadChunk()); 392 | uploader.finish(); 393 | } catch (ProtocolException e) { 394 | assertTrue(e.getMessage().contains("different Upload-Offset value (44) than expected (11)")); 395 | exceptionThrown = true; 396 | } finally { 397 | assertTrue(exceptionThrown); 398 | } 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /src/test/java/io/tus/java/client/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This package contains methods for unit testing. 3 | **/ 4 | package io.tus.java.client; 5 | --------------------------------------------------------------------------------