Resumable Uploads
The upload-session protocol. Use it for large files, flaky networks, or any scenario where a multipart POST is too fragile. Clients split the file into fixed-size chunks, upload them in parallel and out of order, then finalize.
Base path: /api/v1/shares/{share_id}/files/upload-session/
The endpoints are nested under the destination share — share_id is part of the URL, not the body.
Protocol overview
| 1. POST /api/v1/shares/{share_id}/files/upload-session
→ session_id, total_chunks, expires_at
2. PUT /api/v1/shares/{share_id}/files/upload-session/{session_id}/chunks/{i}
→ raw chunk body
(repeat for each chunk; parallel fine)
3. GET /api/v1/shares/{share_id}/files/upload-session/{session_id}
→ inspect progress
4. POST /api/v1/shares/{share_id}/files/upload-session/{session_id}/complete
→ finalize into a File (server verifies content_sha256 if provided)
|
1. Create a session
| curl -X POST $SCAIDRIVE_URL/api/v1/shares/shr_01J3H/files/upload-session \
-H "Authorization: Bearer $SCAIDRIVE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "big-video.mp4",
"size": 2147483648,
"chunks": 512,
"folder_id": "fld_01J3I",
"content_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}'
|
| resp = httpx.post(
f"{url}/api/v1/shares/shr_01J3H/files/upload-session",
headers={"Authorization": f"Bearer {token}"},
json={
"name": "big-video.mp4",
"size": 2147483648,
"chunks": 512,
"folder_id": "fld_01J3I",
},
)
session = resp.json()
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | const resp = await fetch(`${url}/api/v1/shares/shr_01J3H/files/upload-session`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "big-video.mp4",
size: 2147483648,
chunks: 512,
folder_id: "fld_01J3I",
}),
});
const session = await resp.json();
|
Body fields:
| Field |
Required |
Notes |
name |
Yes |
Destination filename |
size |
Yes |
Total size in bytes |
chunks |
Yes |
Total number of chunks the client will upload |
folder_id |
No |
Omit or null for share root |
content_sha256 |
No |
Hex sha256 of the assembled bytes. If provided, server verifies on complete and 422s on mismatch |
mime_type |
No |
Default detected from extension |
replace_file_id |
No |
Upload a new version of an existing file |
Response:
| {
"session_id": "ups_01J5T",
"total_chunks": 512,
"uploaded_chunks": 0,
"expires_at": "2026-04-24T10:15:00Z"
}
|
Sessions expire after 24 hours.
2. Upload chunks
| curl -X PUT "$SCAIDRIVE_URL/api/v1/shares/shr_01J3H/files/upload-session/ups_01J5T/chunks/0" \
-H "Authorization: Bearer $SCAIDRIVE_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @./chunk_000
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | async def upload_chunk(client, share_id, session_id, index, data):
r = await client.put(
f"/api/v1/shares/{share_id}/files/upload-session/{session_id}/chunks/{index}",
headers={"Content-Type": "application/octet-stream"},
content=data,
)
r.raise_for_status()
async def upload_file(client, share_id, session, path, chunk_size):
with open(path, "rb") as f:
tasks = []
index = 0
while True:
data = f.read(chunk_size)
if not data:
break
tasks.append(upload_chunk(client, share_id, session["session_id"], index, data))
if len(tasks) >= 8:
await asyncio.gather(*tasks)
tasks = []
index += 1
if tasks:
await asyncio.gather(*tasks)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 | async function uploadChunks(shareId: string, session: any, filePath: string, chunkSize: number) {
const fd = await open(filePath, "r");
const buf = Buffer.alloc(chunkSize);
const tasks: Promise<Response>[] = [];
let index = 0;
while (true) {
const { bytesRead } = await fd.read(buf, 0, chunkSize, null);
if (!bytesRead) break;
const data = buf.subarray(0, bytesRead);
tasks.push(
fetch(`${url}/api/v1/shares/${shareId}/files/upload-session/${session.session_id}/chunks/${index}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/octet-stream",
},
body: data,
}),
);
if (tasks.length >= 8) {
await Promise.all(tasks);
tasks.length = 0;
}
index++;
}
await Promise.all(tasks);
await fd.close();
}
|
Chunks carry no per-chunk metadata. The chunk body is raw bytes; Content-Type: application/octet-stream is recommended but optional.
Response: 200 with {chunk_index, uploaded_chunks, total_chunks}.
3. Inspect progress
| curl -H "Authorization: Bearer $SCAIDRIVE_TOKEN" \
$SCAIDRIVE_URL/api/v1/shares/shr_01J3H/files/upload-session/ups_01J5T
|
Response:
| {
"session_id": "ups_01J5T",
"total_chunks": 512,
"uploaded_chunks": 340,
"received_chunks": [0, 1, 2, "…", 339],
"expires_at": "2026-04-24T10:15:00Z"
}
|
Use this to resume after a crash — compare received_chunks to what you intended to upload, push what's missing.
4. Finalize
When all chunks are uploaded:
| curl -X POST $SCAIDRIVE_URL/api/v1/shares/shr_01J3H/files/upload-session/ups_01J5T/complete \
-H "Authorization: Bearer $SCAIDRIVE_TOKEN"
|
Response:
| {
"file_id": "fil_01J5Z",
"name": "big-video.mp4",
"size": 2147483648,
"version_id": "ver_01J5Z",
"checksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
|
Finalize:
- Verifies all expected chunk indices are present (else 400).
- Reassembles chunks in order.
- Computes sha256 over the assembled bytes.
- If
content_sha256 was supplied on session create, compares — mismatch returns 422 CHECKSUM_MISMATCH and the upload is rejected.
- Creates the
File row (or a new FileVersion if replace_file_id was used).
- Writes a
ChangeLog entry.
- Deletes the upload session.
The checksum_sha256 in the response is the server-computed value — usable as an integrity probe even when the client didn't supply one upfront.
Cancel
| curl -X DELETE $SCAIDRIVE_URL/api/v1/shares/shr_01J3H/files/upload-session/ups_01J5T \
-H "Authorization: Bearer $SCAIDRIVE_TOKEN"
|
Returns 204. Uploaded chunks are reference-dropped and GC'd.
Error handling during upload
| Code |
HTTP |
Meaning |
| Session not found |
404 |
Session expired, cancelled, or never existed |
| Session expired |
410 |
Expired during upload; create a new session |
| Missing chunks |
400 |
complete called before all expected indices arrived |
| Checksum mismatch |
422 |
Server-computed sha256 didn't match the client-supplied content_sha256 |
| Validation error |
422 |
Chunk index out of range, or session field invalid |
| Quota exceeded |
507 |
Quota check happens on finalize; space must be available |
Quota is checked both at session-create (pre-flight) and at finalize (authoritative — usage may change during upload).
Parallelism guidance
- 4–8 parallel chunk uploads saturate most home broadband.
- 16+ parallel uploads help only on symmetric 1 Gbit+ links.
- Larger chunk size reduces request overhead but increases retransmit cost. 8 MB is a sensible default matching what the storage backend buffers internally; 16–32 MB for fat pipes. The server doesn't impose a hard min or max — pick what suits your network.
What's next