Plattform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Modelle Tools & Services
Lösungen
Organisationen Entwickler Internet Service Provider Managed Service Provider AI-in-a-Box
Ressourcen
Support Documentation Blog Downloads
Unternehmen
Über uns Forschung Karriere Investieren Kontakt
Anmelden

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#

text
1
2
3
4
5
6
7
8
9
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#

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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"
  }'
python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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()
typescript
 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:

json
1
2
3
4
5
6
{
  "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#

bash
1
2
3
4
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
python
 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)
typescript
 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#

bash
1
2
curl -H "Authorization: Bearer $SCAIDRIVE_TOKEN" \
     $SCAIDRIVE_URL/api/v1/shares/shr_01J3H/files/upload-session/ups_01J5T

Response:

json
1
2
3
4
5
6
7
{
  "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:

bash
1
2
curl -X POST $SCAIDRIVE_URL/api/v1/shares/shr_01J3H/files/upload-session/ups_01J5T/complete \
  -H "Authorization: Bearer $SCAIDRIVE_TOKEN"

Response:

json
1
2
3
4
5
6
7
{
  "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#

bash
1
2
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#

Updated 2026-06-04 13:17:08 View source (.md) rev 3