# Multipart uploads

A multipart upload splits a large object into parts that upload independently, then assembles them server-side into one
object. Use it for big files (so a failed part retries without re-sending the whole object), for resumable uploads, and
for uploading parts in parallel.

The lifecycle is always the same:

1. Create the upload and get an `uploadId`.
2. Upload each part (1-based `partNumber`, 1 to 10000), keeping the ETag each part returns.
3. Complete the upload with the ordered list of part numbers and ETags.
4. If anything goes wrong, abort the upload to release the parts.

Every part except the last must be at least 5 MiB; the last part can be any size. An upload that is never completed or
aborted is swept by the lifecycle job, so a crashed client does not leak parts forever.

::: warning Every part except the last must be at least 5 MiB. A too-small non-final part is rejected at completion, so
size your chunks before you start uploading. :::

## A complete large-file upload

The native client mirrors the lifecycle over JSON. Parts stream with no buffering, and the bearer token is refreshed
proactively before each streaming part (a streaming body cannot be replayed). This reads a file in fixed-size chunks,
uploads each as a part, then completes. On any failure it aborts so no orphaned parts remain.

:::: native

::: code-group

```ts [Node]
import { createReadStream } from "node:fs";

const bucket = "reports";
const key = "big.bin";

const mpu = await native.createMultipartUpload(bucket, key, {
  contentType: "application/octet-stream",
});

try {
  const parts = [];
  let partNumber = 1;
  for (const chunk of chunks) {
    const p = await native.uploadPart(bucket, key, mpu.uploadId, partNumber, chunk);
    parts.push({ partNumber, etag: p.etag });
    partNumber++;
  }
  const done = await native.completeMultipartUpload(bucket, key, mpu.uploadId, parts);
  console.log(done.etag, done.versionId);
} catch (err) {
  await native.abortMultipartUpload(bucket, key, mpu.uploadId);
  throw err;
}
```

```go [Go]
mpu, err := native.CreateMultipartUpload(ctx, "reports", "big.bin")
if err != nil {
    return err
}

var parts []lockwellnative.CompleteMultipartPart
for n, chunk := range chunks {
    uploaded, err := native.UploadPart(ctx, lockwellnative.UploadPartInput{
        Bucket: "reports", Key: "big.bin", UploadID: mpu.UploadID,
        PartNumber: n + 1, Body: bytes.NewReader(chunk),
    })
    if err != nil {
        _ = native.AbortMultipartUpload(ctx, "reports", "big.bin", mpu.UploadID)
        return err
    }
    parts = append(parts, lockwellnative.CompleteMultipartPart{PartNumber: uploaded.PartNumber, ETag: uploaded.ETag})
}

done, err := native.CompleteMultipartUpload(ctx, lockwellnative.CompleteMultipartInput{
    Bucket: "reports", Key: "big.bin", UploadID: mpu.UploadID,
    Parts: parts,
})
if err != nil {
    return err
}
fmt.Println(done.ETag, done.VersionID)
```

```java [Java]
var mpu = nativeClient.createMultipartUpload("reports", "big.bin", "application/octet-stream");

try {
    var parts = new java.util.ArrayList<com.lockwell.sdk.nativeapi.NativeTypes.CompleteMultipartPart>();
    for (int n = 0; n < chunks.size(); n++) {
        var uploaded = nativeClient.uploadPart("reports", "big.bin", mpu.uploadId(), n + 1, chunks.get(n));
        parts.add(new com.lockwell.sdk.nativeapi.NativeTypes.CompleteMultipartPart(n + 1, uploaded.etag()));
    }
    var done = nativeClient.completeMultipartUpload("reports", "big.bin", mpu.uploadId(), parts);
    System.out.println(done.etag());
} catch (Exception e) {
    nativeClient.abortMultipartUpload("reports", "big.bin", mpu.uploadId());
    throw e;
}
```

:::

::::

All native clients send an explicit ordered `{partNumber, etag}` completion manifest. The server validates that every
referenced part exists and its ETag matches, then assembles exactly the selected parts; duplicate, out-of-order,
missing, or mismatched entries are rejected.

:::: s3

The S3 client follows the same shape; the body goes up as an `aws-chunked` stream and `completeMultipartUpload` takes
the ordered ETag list:

::: code-group

```ts [Node]
import { createReadStream } from "node:fs";

const bucket = "reports";
const key = "big.bin";
const PART = 8 * 1024 * 1024; // 8 MiB

const mpu = await s3.createMultipartUpload(bucket, key, {
  contentType: "application/octet-stream",
});

try {
  const parts = [];
  let partNumber = 1;
  for await (const chunk of chunked(createReadStream(key), PART)) {
    const p = await s3.uploadPart(bucket, key, mpu.uploadId, partNumber, chunk);
    parts.push({ partNumber, etag: p.etag });
    partNumber++;
  }
  const done = await s3.completeMultipartUpload(bucket, key, mpu.uploadId, parts);
  console.log(done.etag, done.versionId);
} catch (err) {
  await s3.abortMultipartUpload(bucket, key, mpu.uploadId);
  throw err;
}
```

```go [Go]
const part = 8 << 20 // 8 MiB

f, err := os.Open("big.bin")
if err != nil {
    return err
}
defer f.Close()

mpu, err := s3.CreateMultipartUpload(ctx, "reports", "big.bin",
    lockwellsdk.WithContentType("application/octet-stream"))
if err != nil {
    return err
}

var parts []lockwellsdk.CompletedPart
buf := make([]byte, part)
for n := 1; ; n++ {
    read, rerr := io.ReadFull(f, buf)
    if read > 0 {
        p, err := s3.UploadPart(ctx, "reports", "big.bin", mpu.UploadID, n, buf[:read])
        if err != nil {
            _ = s3.AbortMultipartUpload(ctx, "reports", "big.bin", mpu.UploadID)
            return err
        }
        parts = append(parts, lockwellsdk.CompletedPart{PartNumber: n, ETag: p.ETag})
    }
    if rerr == io.EOF || rerr == io.ErrUnexpectedEOF {
        break
    }
    if rerr != nil {
        _ = s3.AbortMultipartUpload(ctx, "reports", "big.bin", mpu.UploadID)
        return rerr
    }
}

done, err := s3.CompleteMultipartUpload(ctx, "reports", "big.bin", mpu.UploadID, parts)
if err != nil {
    return err
}
fmt.Println(done.ETag, done.VersionID)
```

```java [Java]
int part = 8 * 1024 * 1024; // 8 MiB
var mpu = s3.createMultipartUpload("reports", "big.bin", "application/octet-stream");

try (var in = new java.io.FileInputStream("big.bin")) {
    var etags = new java.util.ArrayList<String>();
    byte[] buf = new byte[part];
    int read, n = 1;
    while ((read = in.readNBytes(buf, 0, buf.length)) > 0) {
        byte[] chunk = read == buf.length ? buf : java.util.Arrays.copyOf(buf, read);
        etags.add(s3.uploadPart("reports", "big.bin", mpu.uploadId(), n++, chunk));
    }
    var done = s3.completeMultipartUpload("reports", "big.bin", mpu.uploadId(), null, etags);
    System.out.println(done.etag());
} catch (Exception e) {
    s3.abortMultipartUpload("reports", "big.bin", mpu.uploadId());
    throw e;
}
```

:::

The Java `completeMultipartUpload(bucket, key, uploadId, null, etags)` infers part numbers from the ETag list order
(part 1 is the first ETag). The variant that returns a composite checksum takes explicit part numbers.

::::

## Conditional completion (native)

The native `completeMultipartUpload` accepts `If-None-Match: *` / `If-Match: "<etag>"` to gate the assembled object
atomically at the commit. This is the multipart equivalent of a conditional create or overwrite, applied at the moment
the parts become one object. The S3 client does not gate completion this way.

```go
done, err := native.CompleteMultipartUpload(ctx, lockwellnative.CompleteMultipartInput{
    Bucket: "reports", Key: "big.bin", UploadID: mpu.UploadID,
    IfNoneMatch: "*", // assemble only if the key is still absent (412 otherwise)
})
```

## Per-part checksums

Supply a per-part digest on every `uploadPart` and read the server-computed composite off the assembled object. Each
part is then verified end to end and folded into the composite. This is covered in full on the
[checksums](/guide/checksums#per-part-checksums-on-multipart-uploads) page; in short, on the native path:

```ts [Node]
import { computeChecksumBase64 } from "@kelphect/sdk";

const mpu = await native.createMultipartUpload("reports", "big.bin");
const p = await native.uploadPart("reports", "big.bin", mpu.uploadId, 1, chunk, {
  checksums: { crc32c: computeChecksumBase64("CRC32C", chunk) },
});
await native.completeMultipartUpload("reports", "big.bin", mpu.uploadId, [{ partNumber: 1, etag: p.etag }]);
const head = await native.headObject("reports", "big.bin");
console.log(head.checksums.crc32c); // composite over all parts
```

## List in-progress uploads

`listMultipartUploads` shows every upload that has been created but not yet completed or aborted in a bucket. Use it to
find and clean up stale uploads.

:::: native

::: code-group

```ts [Node]
const page = await native.listMultipartUploads("reports", { prefix: "big" });
for (const u of page.uploads) console.log(u.key, u.uploadId, u.initiated);

// Or every upload across pages:
for await (const page of native.paginateMultipartUploads("reports")) {
  for (const u of page.uploads) console.log(u.key, u.uploadId);
}
```

```go [Go]
page, err := native.ListMultipartUploads(ctx, "reports")
for _, u := range page.Uploads {
    fmt.Println(u.Key, u.UploadID, u.Initiated)
}
```

```java [Java]
var page = nativeClient.listMultipartUploads("reports");
page.uploads().forEach(u -> System.out.println(u.key() + " " + u.uploadId()));
```

:::

::::

:::: s3

The S3 `ListMultipartUploads` pages by a `(keyMarker, uploadIdMarker)` pair:

::: code-group

```ts [Node]
const page = await s3.listMultipartUploads("reports", { prefix: "big", maxUploads: 100 });
for (const u of page.uploads) console.log(u.key, u.uploadId, u.initiated);

// Or every upload across pages:
for await (const page of s3.paginateMultipartUploads("reports")) {
  for (const u of page.uploads) console.log(u.key, u.uploadId);
}
```

```go [Go]
page, err := s3.ListMultipartUploads(ctx, "reports",
    lockwellsdk.WithUploadsPrefix("big"),
    lockwellsdk.WithMaxUploads(100))
for _, u := range page.Uploads {
    fmt.Println(u.Key, u.UploadID, u.Initiated)
}
```

```java [Java]
import com.lockwell.sdk.LockwellClient.ListUploadsOptions;

var pager = s3.listMultipartUploadsPaginator("reports",
    new ListUploadsOptions().prefix("big"));
while (pager.hasMorePages()) {
    pager.nextPage().uploads().forEach(u ->
        System.out.println(u.key() + " " + u.uploadId()));
}
```

:::

::::

## List the parts of an upload

`listParts` returns the parts uploaded so far for one in-progress upload, with each part's number, ETag, and size. Use
it to resume an interrupted upload (skip the parts already present) or to rebuild the completion list.

:::: native

::: code-group

```ts [Node]
const page = await native.listParts("reports", "big.bin", mpu.uploadId);
for (const p of page.parts) console.log(p.partNumber, p.etag, p.size);
```

```go [Go]
page, err := native.ListParts(ctx, "reports", "big.bin", mpu.UploadID)
for _, p := range page.Parts {
    fmt.Println(p.PartNumber, p.ETag, p.Size)
}
```

```java [Java]
var page = nativeClient.listParts("reports", "big.bin", mpu.uploadId());
page.parts().forEach(p -> System.out.println(p.partNumber() + " " + p.etag()));
```

:::

::::

:::: s3

The S3 `ListParts` pages by `partNumberMarker`:

::: code-group

```ts [Node]
const page = await s3.listParts("reports", "big.bin", mpu.uploadId, { maxParts: 100 });
for (const p of page.parts) console.log(p.partNumber, p.etag, p.size);

// Every part across pages:
for await (const page of s3.paginateParts("reports", "big.bin", mpu.uploadId)) {
  for (const p of page.parts) console.log(p.partNumber, p.etag);
}
```

```go [Go]
page, err := s3.ListParts(ctx, "reports", "big.bin", mpu.UploadID,
    lockwellsdk.WithMaxParts(100))
for _, p := range page.Parts {
    fmt.Println(p.PartNumber, p.ETag, p.Size)
}
```

```java [Java]
import com.lockwell.sdk.LockwellClient.ListPartsOptions;

var pager = s3.listPartsPaginator("reports", "big.bin", mpu.uploadId(),
    new ListPartsOptions().maxParts(100));
while (pager.hasMorePages()) {
    pager.nextPage().parts().forEach(p ->
        System.out.println(p.partNumber() + " " + p.etag()));
}
```

:::

::::

## UploadPartCopy: build a part from an existing object

`UploadPartCopy` fills a part by server-side-copying bytes from an existing object instead of re-uploading them. Pass a
byte range to copy a slice; pass an empty range to copy the whole source object. This is how you stitch or re-chunk
objects without moving data through the client. Supply a source version id to copy from a specific version. This is an
S3-client operation; on the native path, use [copyObject](/guide/copying-objects) for server-side copies.

:::: s3

::: code-group

```ts [Node]
const cp = await s3.uploadPartCopy(
  "reports",
  "source.bin",
  "", // src bucket, key, version (''=current)
  "reports",
  "assembled.bin",
  mpu.uploadId,
  1, // destination part number
  "bytes=0-5242879", // copy the first 5 MiB ('' = whole object)
);
parts.push({ partNumber: 1, etag: cp.etag });
```

```go [Go]
cp, err := s3.UploadPartCopy(ctx,
    "reports", "source.bin", "", // src bucket, key, version
    "reports", "assembled.bin", mpu.UploadID,
    1,                  // destination part number
    "bytes=0-5242879")  // "" copies the whole object
parts = append(parts, lockwellsdk.CompletedPart{PartNumber: 1, ETag: cp.ETag})
```

```java [Java]
String etag = s3.uploadPartCopy("reports", "assembled.bin", mpu.uploadId(), 1,
    "reports", "source.bin", null, "bytes=0-5242879");
```

:::

::::

## Complete or abort

`completeMultipartUpload` assembles the parts in part-number order into the final object and returns its ETag and
version id. `abortMultipartUpload` discards the upload and frees its parts. Always abort on failure; an upload left
dangling holds its parts until the lifecycle sweep expires it.

The native completion accepts an idempotency key so a retried completion replays the original result instead of failing
or double-assembling (Node passes it as the `idempotencyKey` option).

:::: native

```ts [Node]
const done = await native.completeMultipartUpload("reports", "big.bin", mpu.uploadId, parts, {
  idempotencyKey: "assemble-big-bin-2026-001",
});

// Or give up on the upload entirely:
await native.abortMultipartUpload("reports", "big.bin", mpu.uploadId);
```

::::

:::: s3

```ts [Node]
const done = await s3.completeMultipartUpload("reports", "big.bin", mpu.uploadId, parts, {
  idempotencyKey: "assemble-big-bin-2026-001",
});

await s3.abortMultipartUpload("reports", "big.bin", mpu.uploadId);
```

::::

## Related

- [Checksums](/guide/checksums#per-part-checksums-on-multipart-uploads) for per-part and composite verification.
- [Conditional writes](/guide/conditional-writes) for the idempotency key and `If-None-Match` at completion.
- [Errors and retries](/guide/errors-and-retries) for which calls retry automatically.
- [S3 operations reference](/reference/s3-operations) for the full operation matrix.
