TUS Uploads#
Gradle module: source/extn/tusmgmt. Maven coordinate (placeholder, pending extraction): com.palmyralabs.palmyra.extn:palmyra-ext-tusmgmt:<version>.
Implements the TUS resumable-upload protocol for Spring Boot 3 / Jakarta EE 10. Wraps me.desair.tus:tus-java-server with:
- A relative-path
Locationheader on create (survives reverse proxies). - A metadata SPI (
IndexStorageService) so the app owns its own attachment table — the extension doesn’t impose one. - A per-subtype handler registry (
@TusUpload("subType")+FileUploadHandler) so each domain module overrides file-path layout and on-completion behaviour without touching the controller. - A scheduled expiration sweeper that reaps orphaned uploads and stale disk locks.
This extension does not depend on
palmyra-spring,palmyra-dbpwd-mgmt, orpalmyra-dbacl-mgmt— it slots into any Spring Boot 3 app.
Pages#
| Page | Role |
|---|---|
| Integration guide | Step-by-step consumer walkthrough — from dependency add to first successful PATCH |
Public URL shape#
{prefix}/tusupload/{subType}/{ciId} POST (create), HEAD (status), OPTIONS
{prefix}/tusupload/{subType}/{ciId}/{fid} PATCH (chunk), HEAD, DELETE, OPTIONS{prefix}=${palmyra.servlet.prefix-path:/api}— TUS URLs sit alongside your palmyra CRUD URLs.{subType}= a string key the app registered via@TusUpload("...").{ciId}= the row id on the owning table (e.g. exam id, gateway id).{fid}= UUID string returned in theLocationheader of the POST response.
The ownerKey handed to the TUS library internally is the colon-joined reference:ciType:citId:ciId from FileOwnerInfo. The stock controller sets reference = ciType = subType and citId = 0; apps that need a distinct reference can override TusUploadController or set the ownerKey out-of-band.
Request lifecycle#
┌─────────────────────────────────────────────────────────────────────────────┐
│ 1. POST /api/tusupload/{subType}/{ciId} │
│ headers: Tus-Resumable, Upload-Length, Upload-Metadata │
│ │
│ TusUploadController.dispatch → TusUploadService.process │
│ → ExtCreationPostRequestHandler.process │
│ → DirectoryStorageService.create │
│ → IndexStorageService.create(info, ownerKey) │
│ • registry.resolve(subType) → FileUploadHandler │
│ • handler.generateFilePath(owner, filename, metadata) │
│ • persist row: fid, path, size, offset=0, status=0 │
│ → create empty file on disk at {root}/{relativePath} │
│ │
│ ← 201 Created, Location: {requestURI}/{fid} │
├─────────────────────────────────────────────────────────────────────────────┤
│ 2. PATCH /api/tusupload/{subType}/{ciId}/{fid} │
│ headers: Tus-Resumable, Upload-Offset, Content-Type │
│ body: raw chunk bytes │
│ │
│ → AbstractDiskStorageService.append → writes bytes to disk │
│ → DirectoryStorageService.update │
│ → IndexStorageService.update │
│ • updates file_offset, status (percent complete) │
│ • when offset == length → handler.onComplete(owner, ..., attachment) │
│ │
│ ← 204 No Content, Upload-Offset: {new offset} │
├─────────────────────────────────────────────────────────────────────────────┤
│ 3. HEAD /api/tusupload/{subType}/{ciId}/{fid} │
│ ← 200 + Upload-Offset + Upload-Length │
├─────────────────────────────────────────────────────────────────────────────┤
│ 4. @Scheduled sweeper (every sweep-interval-ms) │
│ TusExpirationSweeper.sweep → TusUploadService.cleanup │
│ → DirectoryStorageService.cleanupExpiredUploads │
│ → IndexStorageService.forEachExpiredUpload │
│ → terminateUpload per row (delete DB row + on-disk file) │
└─────────────────────────────────────────────────────────────────────────────┘Properties — palmyra.tus.*#
Bound to TusMgmtProperties:
| Property | Default | Purpose |
|---|---|---|
palmyra.tus.upload-location |
/opt/palmyra/upload |
Root dir for uploaded bytes + TUS lock files |
palmyra.tus.expiration-ms |
604800000 (7 days) |
Age past which an in-progress upload is reaped |
palmyra.tus.sweep-interval-ms |
900000 (15 min) |
TusExpirationSweeper poll interval |
palmyra.tus.sweep-initial-delay-ms |
60000 (1 min) |
Delay before the first sweep after boot |
palmyra.tus.url-pattern |
/.*/tusupload/[a-zA-Z_]+/[0-9]+/? |
Regex the TUS lib uses to extract fid from request URIs |
SPIs the consuming app implements#
IndexStorageService (exactly one bean)#
DB-backed metadata operations: create, update, getUploadInfo, delete, isUploadIdExists, getRelativePath, forEachExpiredUpload, forEachUpload.
create(info, ownerKey) is the hook that wires the FileUploadHandler.generateFilePath callback into the persisted row; update(info) fires handler.onComplete when offset reaches length. See Integration guide § 3 for the skeleton.
FileUploadHandler (one bean per subtype)#
@Component
@TusUpload("Order")
public class OrderUploadHandler implements FileUploadHandler {
@Override
public String generateFilePath(FileOwnerInfo owner, String filename, Map<String,String> metadata) {
return owner.getDefaultFilePath(filename); // default: {subType}/{ciId}/{filename}
}
@Override
public void onComplete(FileOwnerInfo owner, String filename, Map<String,String> metadata, Object attachment) {
// fire domain event, trigger indexer, publish SQS message, …
}
}Multiple handlers co-exist — the registry keys by @TusUpload.value(). Missing subtype → IllegalArgumentException on first request.
Layout (reference)#
tusmgmt/
└── src/main/java/com/palmyralabs/palmyra/ext/tusmgmt/
├── FileOwnerInfo.java immutable reference:ciType:citId:ciId parser
├── FileUploadHandler.java per-domain SPI (generateFilePath + onComplete)
├── TusUpload.java @TusUpload("subType") marker (meta-@Component)
├── FileUploadHandlerRegistry.java resolves subType → handler
├── IndexStorageService.java metadata-storage contract (app implements)
├── TusUploadService.java TusFileUploadService subclass with extension stack
├── TusUploadController.java Spring @Controller dispatcher
├── TusUploadConfig.java @EnableConfigurationProperties + bean wiring
├── TusMgmtProperties.java @ConfigurationProperties("palmyra.tus")
├── TusExpirationSweeper.java @Scheduled cleanup
└── storage/
├── AbstractDiskStorageService.java disk I/O base
├── DirectoryStorageService.java disk + IndexStorageService bridge
├── ExtCreationExtension.java swaps in relative-Location POST handler
└── ExtCreationPostRequestHandler.javaFramework dependencies#
Pulls in: me.desair.tus:tus-java-server:1.0.0-3.0, spring-web, spring-context, spring-boot, commons-lang3, slf4j-api, jakarta.servlet-api. Lombok is compile/test only.
Does NOT depend on: palmyra-spring / palmyra-rest / palmyra-standard, any app-specific JPA entities / repos, palmyra-dbacl-mgmt / palmyra-dbpwd-mgmt. ACL and auth are the host application’s concern — the controller method is gated by whatever Spring Security chain the app wires up.
Related#
- TusUploadHandler — the
tus-java-serverupload interface used internally. - TusDownloadHandler — download-side counterpart.