
Resumable tus uploads with create/upload flows, pause/resume support, progress callbacks, metadata, configurable chunking and retries, optional file-locking, and persistence hooks for resumed transfers.
tus is a protocol based on HTTP for resumable file uploads. Resumable means that an upload can be interrupted at any moment and can be resumed without re-uploading the previous data again. An interruption may happen willingly, if the user wants to pause, or by accident in case of a network issue or server outage.
Ktus is a multiplatform client library for uploading files using the tus resumable upload protocol to any remote server supporting it. It is build on top of the Ktor client library and supports all platforms supported by Ktor.
Below are several usage examples showing basic and advanced flows.
Ktus has a convenience function that combines both the create and upload phases of a Tus upload into a single call.
// import com.ldartools.ktus.createAndUploadTus
// import com.ldartools.ktus.okio.OkioTusFile
val file = OkioTusFile(filePath)
httpClient.createAndUploadTus(createUrl = url, file = file, metadata = mapOf("filename" to file.name)) {
// optional per-request configuration
setAuthorizationHeader(anonymous = false)
}You can also split the create and upload phases into separate calls. This is useful for persisting the upload URL for later continuation or other advanced use cases where upload might not need to start right away.
// import com.ldartools.ktus.createTus
// import com.ldartools.ktus.uploadTus
// import com.ldartools.ktus.okio.OkioTusFile
val file = OkioTusFile(filePath)
// 1) Create the upload on the server and get the upload URL
val uploadUrl = httpClient.createTus(createUrl = url, file = file, metadata = mapOf("filename" to file.name))
// 2) Upload the file to the returned upload URL
httpClient.uploadTus(uploadUrl = uploadUrl, file = file) {
// optional per-request configuration
setAuthorizationHeader(anonymous = false)
}Customize upload behavior (chunk size, retries, protocol extensions, file locking, etc.) via TusUploadOptions.
// import com.ldartools.ktus.TusUploadOptions
// import com.ldartools.ktus.RetryOptions
val options = TusUploadOptions(
checkServerCapabilities = true,
chunkSize = 4 * 1024 * 1024, // 4 MB
useFileLock = false,
retryOptions = RetryOptions(
maxRetries = 5,
initialDelayMillis = 1_000L,
maxDelayMillis = 60_000L,
factor = 2.0
)
)
httpClient.createAndUploadTus(createUrl = url, file = file, options = options, onProgress = { sent, total ->
println("Uploaded $sent / $total")
})Ktus does not have a built-in persistence mechanism, but it provides the necessary hooks to allow you to persist the upload URL so that uploads can be resumed later.
You can save the returned uploadUrl (for example, to local storage or a database) and later resume the upload by calling uploadTus with that URL.
// Persist the uploadUrl after creation
val uploadUrl = httpClient.createTus(createUrl = url, file = file)
saveToLocalStore("pendingUploadUrl", uploadUrl)
// Later (possibly after app restart) retrieve and resume
val persisted = loadFromLocalStore("pendingUploadUrl")
if (persisted != null) {
httpClient.uploadTus(uploadUrl = persisted, file = file)
}If you prefer a single call that still allows persisting the upload URL immediately after creation, use createAndUploadTus with an onCreate callback.
httpClient.createAndUploadTus(createUrl = url, file = file, onCreate = { uploadUrl ->
// Persist the upload URL so you can resume later if needed
saveToLocalStore("pendingUploadUrl", uploadUrl)
}, onProgress = { sent, total ->
println("Uploaded $sent / $total")
})If you want to pause and resume uploads, you can achieve this by managing the upload coroutine job yourself.
//import kotlinx.coroutines.*
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
var persistedUploadUrl: String? = null
lateinit var uploadJob: Job
// Start (create + upload) and persist uploadUrl immediately via onCreate
fun startUpload(httpClient: HttpClient, createUrl: String, file: ITusFile, options: TusUploadOptions) {
uploadJob = scope.launch {
try {
httpClient.createAndUploadTus(
createUrl = createUrl,
file = file,
options = options,
onProgress = { sent, total -> println("Uploaded $sent / $total") },
onCreate = { url ->
// persist the url to disk/db if you want durable resume across restarts
persistedUploadUrl = url
}
)
} catch (e: CancellationException) {
// paused by caller — safe to ignore or log
} catch (t: Throwable) {
// handle other errors
}
}
}
// Pause (cancel the running job)
suspend fun pauseUpload() {
if (::uploadJob.isInitialized && uploadJob.isActive) {
uploadJob.cancelAndJoin() // stops the upload coroutine and waits for cleanup
}
}
// Resume (use persistedUploadUrl)
fun resumeUpload(httpClient: HttpClient, file: ITusFile, options: TusUploadOptions) {
val url = persistedUploadUrl ?: throw IllegalStateException("No persisted upload URL")
scope.launch {
try {
httpClient.uploadTus(
uploadUrl = url,
file = file,
options = options,
onProgress = { sent, total -> println("Uploaded $sent / $total") }
)
} catch (e: CancellationException) {
// paused again
} catch (t: Throwable) {
// handle other errors
}
}
}Notes:
OkioTusFile is a convenient ITusFile implementation; you can implement ITusFile differently for other platforms.onProgress receives (sent, total) bytes and can be used to update UI progress bars.Add the following libraries to your .toml file.
[versions]
ktus = "1.0.1"
[libraries]
ktus = {module = "com.ldartools.ktus" , version.ref = "ktus"}
ktus-okio = {module = "com.ldartools.ktus-okio" , version.ref = "ktus"}Add the dependencies to your common code.
commonMain.dependencies {
//...
implementation(libs.ktus)
implementation(libs.ktus.okio)
}It is recommended that you used OkioTusFile to get started as this is the easiest approach.
Just understand that this take a dependency on Okio.
If you do not wish to use OkioTusFile, remove the references above and provide an ITusFile implementation.
The following optional Tus protocol extensions have not been implemented.
OkioTusFile because file read locks are not supported by Okio. If you want this feature, please upvote this issue https://github.com/square/okio/issues/1464.Because Ktus is cross platform it must be told how to retrieve the bytes that are being uploaded. This is done via the ITusFile interface.
You must provide an ITusFile implementation(s) that will work for your platform(s).
/**
* An interface abstracting the source of a file to be uploaded via TUS.
* This allows the upload logic to be independent of whether the file is on disk,
* in memory, or from another source.
*/
interface ITusFile {
/** The total size of the file in bytes. */
val size: Long
/** The name of the file, which may be used for metadata. */
val name: String
/**
* Reads a specific range of the file into a ByteReadChannel.
* Implementations should ensure this operation is efficient and does not load
* the entire file into memory, especially for large files.
*
* @param offset The byte offset to start reading from.
* @param length The number of bytes to read.
* @return A ByteReadChannel containing the specified section of the file.
*/
suspend fun readSection(offset: Long, length: Long): ByteReadChannel
/**
* Creates a read lock on the file that will be closed when the upload is complete.
* This is optional and helps prevent the file from being modified during an upload.
* The returned AutoCloseable will be invoked at the end of the upload process.
*/
suspend fun fileReadLock(): AutoCloseable
}Note: While most use cases will be uploading a file from the file system. It is possible to upload from other sources (memory, streams, etc.) by providing a custom ITusFile implementation.
Whatever the source of the ITusFile, it MUST be re-readable. By its nature, Tus will re-request bytes to be read if a chunk fails.
While Ktus does provide an ITusFile implementation built using Okio.
This implementation should work for all platforms supported by Okio.
In order to keep the dependencies of Ktus at minimum, the OkioTusFile is provided in a separate module.
The uploadTus function has a parameter that enables a file lock. This will lock the file, provided the ITusFile implementation supports it, in order to ensure that bytes are not mutated while an upload is in progress.
Ktus was built and validated against the TUS 1.0.0 protocol specification and compared with the official TUS client implementations to ensure correctness and feature parity.
| Feature | tus-js-client | tus-java-client | TUSKit (Swift) | Ktus |
|---|---|---|---|---|
| Core protocol (HEAD/PATCH) | Yes | Yes | Yes | Yes |
| Creation (POST) | Yes | Yes | Yes | Yes |
| OPTIONS capability check | Yes | No | Yes | Yes |
| Chunked uploads | Yes | Yes | Yes | Yes |
| Metadata encoding | Yes | Yes | Yes | Yes |
| Metadata key validation | No | No | Yes | Yes |
| Retry with backoff | Fixed delays | No (caller) | Counter only | Exponential backoff |
| 5xx retry | Yes | No (caller) | Yes (system) | Yes |
| 409 Conflict recovery | No | No | HEAD + retry | HEAD + retry |
| Offset advancement check | No | No | No | Yes |
| PATCH failure loop protection | Retry counter | No | Retry counter | Consecutive failure counter + offset check |
| 404/410 expired handling | Re-creates upload | Exception | Re-creates upload | Exception |
| Upload URL persistence | Built-in | Built-in | Built-in | Caller-managed (see usage) |
| Creation-with-upload | Yes | No | No | No |
| Upload-Defer-Length | Yes | No | No | No |
| Concatenation / parallel | Yes | No | No | No |
| Relative URL resolution | RFC 3986 | RFC 2396 | RFC 3986 | RFC 3986 |
| Cross-platform | Browser + Node | JVM | Apple | JVM, Android, iOS, Linux, + all Ktor targets |
Strengths:
initialDelayMillis, maxDelayMillis, factor) is more robust than fixed delay arrays or no retries at all.Trade-offs:
onCreate callback, separate createTus/uploadTus calls) so callers can persist URLs however they prefer.TusUploadExpiredException so callers can decide how to handle it.ITusFile implementations for common platforms (e.g., Android, iOS, JVM)PRs are welcome! :)
tus is a protocol based on HTTP for resumable file uploads. Resumable means that an upload can be interrupted at any moment and can be resumed without re-uploading the previous data again. An interruption may happen willingly, if the user wants to pause, or by accident in case of a network issue or server outage.
Ktus is a multiplatform client library for uploading files using the tus resumable upload protocol to any remote server supporting it. It is build on top of the Ktor client library and supports all platforms supported by Ktor.
Below are several usage examples showing basic and advanced flows.
Ktus has a convenience function that combines both the create and upload phases of a Tus upload into a single call.
// import com.ldartools.ktus.createAndUploadTus
// import com.ldartools.ktus.okio.OkioTusFile
val file = OkioTusFile(filePath)
httpClient.createAndUploadTus(createUrl = url, file = file, metadata = mapOf("filename" to file.name)) {
// optional per-request configuration
setAuthorizationHeader(anonymous = false)
}You can also split the create and upload phases into separate calls. This is useful for persisting the upload URL for later continuation or other advanced use cases where upload might not need to start right away.
// import com.ldartools.ktus.createTus
// import com.ldartools.ktus.uploadTus
// import com.ldartools.ktus.okio.OkioTusFile
val file = OkioTusFile(filePath)
// 1) Create the upload on the server and get the upload URL
val uploadUrl = httpClient.createTus(createUrl = url, file = file, metadata = mapOf("filename" to file.name))
// 2) Upload the file to the returned upload URL
httpClient.uploadTus(uploadUrl = uploadUrl, file = file) {
// optional per-request configuration
setAuthorizationHeader(anonymous = false)
}Customize upload behavior (chunk size, retries, protocol extensions, file locking, etc.) via TusUploadOptions.
// import com.ldartools.ktus.TusUploadOptions
// import com.ldartools.ktus.RetryOptions
val options = TusUploadOptions(
checkServerCapabilities = true,
chunkSize = 4 * 1024 * 1024, // 4 MB
useFileLock = false,
retryOptions = RetryOptions(
maxRetries = 5,
initialDelayMillis = 1_000L,
maxDelayMillis = 60_000L,
factor = 2.0
)
)
httpClient.createAndUploadTus(createUrl = url, file = file, options = options, onProgress = { sent, total ->
println("Uploaded $sent / $total")
})Ktus does not have a built-in persistence mechanism, but it provides the necessary hooks to allow you to persist the upload URL so that uploads can be resumed later.
You can save the returned uploadUrl (for example, to local storage or a database) and later resume the upload by calling uploadTus with that URL.
// Persist the uploadUrl after creation
val uploadUrl = httpClient.createTus(createUrl = url, file = file)
saveToLocalStore("pendingUploadUrl", uploadUrl)
// Later (possibly after app restart) retrieve and resume
val persisted = loadFromLocalStore("pendingUploadUrl")
if (persisted != null) {
httpClient.uploadTus(uploadUrl = persisted, file = file)
}If you prefer a single call that still allows persisting the upload URL immediately after creation, use createAndUploadTus with an onCreate callback.
httpClient.createAndUploadTus(createUrl = url, file = file, onCreate = { uploadUrl ->
// Persist the upload URL so you can resume later if needed
saveToLocalStore("pendingUploadUrl", uploadUrl)
}, onProgress = { sent, total ->
println("Uploaded $sent / $total")
})If you want to pause and resume uploads, you can achieve this by managing the upload coroutine job yourself.
//import kotlinx.coroutines.*
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
var persistedUploadUrl: String? = null
lateinit var uploadJob: Job
// Start (create + upload) and persist uploadUrl immediately via onCreate
fun startUpload(httpClient: HttpClient, createUrl: String, file: ITusFile, options: TusUploadOptions) {
uploadJob = scope.launch {
try {
httpClient.createAndUploadTus(
createUrl = createUrl,
file = file,
options = options,
onProgress = { sent, total -> println("Uploaded $sent / $total") },
onCreate = { url ->
// persist the url to disk/db if you want durable resume across restarts
persistedUploadUrl = url
}
)
} catch (e: CancellationException) {
// paused by caller — safe to ignore or log
} catch (t: Throwable) {
// handle other errors
}
}
}
// Pause (cancel the running job)
suspend fun pauseUpload() {
if (::uploadJob.isInitialized && uploadJob.isActive) {
uploadJob.cancelAndJoin() // stops the upload coroutine and waits for cleanup
}
}
// Resume (use persistedUploadUrl)
fun resumeUpload(httpClient: HttpClient, file: ITusFile, options: TusUploadOptions) {
val url = persistedUploadUrl ?: throw IllegalStateException("No persisted upload URL")
scope.launch {
try {
httpClient.uploadTus(
uploadUrl = url,
file = file,
options = options,
onProgress = { sent, total -> println("Uploaded $sent / $total") }
)
} catch (e: CancellationException) {
// paused again
} catch (t: Throwable) {
// handle other errors
}
}
}Notes:
OkioTusFile is a convenient ITusFile implementation; you can implement ITusFile differently for other platforms.onProgress receives (sent, total) bytes and can be used to update UI progress bars.Add the following libraries to your .toml file.
[versions]
ktus = "1.0.1"
[libraries]
ktus = {module = "com.ldartools.ktus" , version.ref = "ktus"}
ktus-okio = {module = "com.ldartools.ktus-okio" , version.ref = "ktus"}Add the dependencies to your common code.
commonMain.dependencies {
//...
implementation(libs.ktus)
implementation(libs.ktus.okio)
}It is recommended that you used OkioTusFile to get started as this is the easiest approach.
Just understand that this take a dependency on Okio.
If you do not wish to use OkioTusFile, remove the references above and provide an ITusFile implementation.
The following optional Tus protocol extensions have not been implemented.
OkioTusFile because file read locks are not supported by Okio. If you want this feature, please upvote this issue https://github.com/square/okio/issues/1464.Because Ktus is cross platform it must be told how to retrieve the bytes that are being uploaded. This is done via the ITusFile interface.
You must provide an ITusFile implementation(s) that will work for your platform(s).
/**
* An interface abstracting the source of a file to be uploaded via TUS.
* This allows the upload logic to be independent of whether the file is on disk,
* in memory, or from another source.
*/
interface ITusFile {
/** The total size of the file in bytes. */
val size: Long
/** The name of the file, which may be used for metadata. */
val name: String
/**
* Reads a specific range of the file into a ByteReadChannel.
* Implementations should ensure this operation is efficient and does not load
* the entire file into memory, especially for large files.
*
* @param offset The byte offset to start reading from.
* @param length The number of bytes to read.
* @return A ByteReadChannel containing the specified section of the file.
*/
suspend fun readSection(offset: Long, length: Long): ByteReadChannel
/**
* Creates a read lock on the file that will be closed when the upload is complete.
* This is optional and helps prevent the file from being modified during an upload.
* The returned AutoCloseable will be invoked at the end of the upload process.
*/
suspend fun fileReadLock(): AutoCloseable
}Note: While most use cases will be uploading a file from the file system. It is possible to upload from other sources (memory, streams, etc.) by providing a custom ITusFile implementation.
Whatever the source of the ITusFile, it MUST be re-readable. By its nature, Tus will re-request bytes to be read if a chunk fails.
While Ktus does provide an ITusFile implementation built using Okio.
This implementation should work for all platforms supported by Okio.
In order to keep the dependencies of Ktus at minimum, the OkioTusFile is provided in a separate module.
The uploadTus function has a parameter that enables a file lock. This will lock the file, provided the ITusFile implementation supports it, in order to ensure that bytes are not mutated while an upload is in progress.
Ktus was built and validated against the TUS 1.0.0 protocol specification and compared with the official TUS client implementations to ensure correctness and feature parity.
| Feature | tus-js-client | tus-java-client | TUSKit (Swift) | Ktus |
|---|---|---|---|---|
| Core protocol (HEAD/PATCH) | Yes | Yes | Yes | Yes |
| Creation (POST) | Yes | Yes | Yes | Yes |
| OPTIONS capability check | Yes | No | Yes | Yes |
| Chunked uploads | Yes | Yes | Yes | Yes |
| Metadata encoding | Yes | Yes | Yes | Yes |
| Metadata key validation | No | No | Yes | Yes |
| Retry with backoff | Fixed delays | No (caller) | Counter only | Exponential backoff |
| 5xx retry | Yes | No (caller) | Yes (system) | Yes |
| 409 Conflict recovery | No | No | HEAD + retry | HEAD + retry |
| Offset advancement check | No | No | No | Yes |
| PATCH failure loop protection | Retry counter | No | Retry counter | Consecutive failure counter + offset check |
| 404/410 expired handling | Re-creates upload | Exception | Re-creates upload | Exception |
| Upload URL persistence | Built-in | Built-in | Built-in | Caller-managed (see usage) |
| Creation-with-upload | Yes | No | No | No |
| Upload-Defer-Length | Yes | No | No | No |
| Concatenation / parallel | Yes | No | No | No |
| Relative URL resolution | RFC 3986 | RFC 2396 | RFC 3986 | RFC 3986 |
| Cross-platform | Browser + Node | JVM | Apple | JVM, Android, iOS, Linux, + all Ktor targets |
Strengths:
initialDelayMillis, maxDelayMillis, factor) is more robust than fixed delay arrays or no retries at all.Trade-offs:
onCreate callback, separate createTus/uploadTus calls) so callers can persist URLs however they prefer.TusUploadExpiredException so callers can decide how to handle it.ITusFile implementations for common platforms (e.g., Android, iOS, JVM)PRs are welcome! :)