# Upload & download

This page covers writing and reading object bytes. It walks `PutObject` (buffered and streaming), `GetObject`
(whole-object and streaming, ranges, versions, response overrides), and `HeadObject`.

The examples for the legacy HTTP/JSON surface lead with the native client and show the S3 client right after, behind the
surface toggle. New Go applications should use the direct binary `lockwellwire` client (in `lockwell-sdk-go`)
described in the
[Go SDK guide](/sdks/go#lockwellwire-lockwell-sdk-go-the-lnw1-native-wire-client). Two clients
write to the same encrypted, per-tenant store:

- the **legacy native client** uses an auto-managed JSON bearer token (`NativeClient` / `lockwellnative` /
  `LockwellNativeClient`);
- the **S3 client** signs every request with SigV4 (`Client` / `lockwellsdk` / `LockwellClient`).

For the operation matrix at a glance, see the [native data-plane reference](/reference/native-api) and the
[S3 operations reference](/reference/s3-operations). Copy, list, delete, and conditional writes have their own pages,
linked at the bottom.

## Constructing a client

The native client takes an endpoint plus an access-key id and secret, then mints and refreshes its own bearer token
(single-flight, thread-safe). The S3 client takes the same credentials and signs each request directly.

::: code-group

```ts [Node]
import { NativeClient, Client } from "@kelphect/sdk";

// Native JSON data plane (token managed for you):
const native = new NativeClient({
  endpoint: "https://objects.example.com", // public listener; /api/v1 is added for you
  accessKeyId: process.env.LOCKWELL_ACCESS_KEY_ID,
  secretKey: process.env.LOCKWELL_SECRET_KEY,
});

// S3 data plane (SigV4):
const s3 = new Client({
  endpoint: "https://objects.example.com",
  accessKeyId: process.env.LOCKWELL_ACCESS_KEY_ID,
  secretKey: process.env.LOCKWELL_SECRET_KEY,
});
```

```go [Go]
import (
    "github.com/RusticStack/lockwell-sdk-go/lockwellnative"
    "github.com/RusticStack/lockwell-sdk-go/lockwellsdk"
)

native, err := lockwellnative.New(
    "https://objects.example.com",
    os.Getenv("LOCKWELL_ACCESS_KEY_ID"),
    os.Getenv("LOCKWELL_SECRET_KEY"),
)

s3, err := lockwellsdk.New(
    "https://objects.example.com",
    lockwellsdk.Credentials{
        AccessKeyID: os.Getenv("LOCKWELL_ACCESS_KEY_ID"),
        SecretKey:   os.Getenv("LOCKWELL_SECRET_KEY"),
    },
)
```

```java [Java]
import com.lockwell.sdk.nativeapi.LockwellNativeClient;
import com.lockwell.sdk.LockwellClient;
import com.lockwell.sdk.Credentials;

var nativeClient = LockwellNativeClient.builder()
    .endpoint("https://objects.example.com")
    .accessKeyId(System.getenv("LOCKWELL_ACCESS_KEY_ID"))
    .secretKey(System.getenv("LOCKWELL_SECRET_KEY"))
    .build();

var s3 = LockwellClient.builder()
    .endpoint("https://objects.example.com")
    .credentials(new Credentials(System.getenv("LOCKWELL_ACCESS_KEY_ID"),
                                 System.getenv("LOCKWELL_SECRET_KEY")))
    .build();
```

:::

The rest of this page uses `native` for the native client and `s3` for the S3 client.

## Put an object (buffered)

Hand the SDK a byte buffer and it does one request. The native client returns the stored object's ETag and, on a
versioned bucket, a version id.

:::: native

::: code-group

```ts [Node]
const put = await native.putObject("reports", "q1/summary.txt", "hello world", {
  contentType: "text/plain",
  metadata: { author: "finance", quarter: "Q1" },
});
console.log(put.etag, put.versionId);
```

```go [Go]
put, err := native.PutObject(ctx, lockwellnative.PutObjectInput{
    Bucket:      "reports",
    Key:         "q1/summary.txt",
    Body:        strings.NewReader("hello world"),
    ContentType: "text/plain",
    Metadata:    map[string]string{"author": "finance", "quarter": "Q1"},
})
fmt.Println(put.ETag, put.VersionID)
```

```java [Java]
import com.lockwell.sdk.nativeapi.NativeTypes.PutOptions;

var put = nativeClient.putObject("reports", "q1/summary.txt", "hello world".getBytes(),
    new PutOptions()
        .contentType("text/plain")
        .metadata("author", "finance")
        .metadata("quarter", "Q1"));
System.out.println(put.etag() + " " + put.versionId());
```

:::

::::

:::: s3

The same with the S3 client:

::: code-group

```ts [Node]
const put = await s3.putObject("reports", "q1/summary.txt", Buffer.from("hello world"), {
  contentType: "text/plain",
  metadata: { author: "finance", quarter: "Q1" },
});
console.log(put.etag, put.versionId);
```

```go [Go]
put, err := s3.PutObject(ctx, "reports", "q1/summary.txt", []byte("hello world"),
    lockwellsdk.WithContentType("text/plain"),
    lockwellsdk.WithMetadata(map[string]string{"author": "finance", "quarter": "Q1"}),
)
fmt.Println(put.ETag, put.VersionID)
```

```java [Java]
var put = s3.putObject("reports", "q1/summary.txt", "hello world".getBytes(),
    new LockwellClient.PutOptions()
        .contentType("text/plain")
        .metadata("author", "finance")
        .metadata("quarter", "Q1"));
System.out.println(put.etag() + " " + put.versionId());
```

:::

::::

User metadata is stored alongside the object and returned on every read. On the native path it travels as
`X-Lockwell-Meta-*` headers; on the S3 path as `x-amz-meta-*`. Both SDKs surface it as a plain key-value map (the prefix
is stripped for you).

### PutObject options

| Option               | Native (Go / Node / Java)                               | S3 (Go / Node / Java)                                                         | Effect                                                                                               |
| -------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| Content type         | `ContentType` / `contentType` / `.contentType`          | `WithContentType` / `contentType` / `.contentType`                            | Sets the stored media type.                                                                          |
| User metadata        | `Metadata` / `metadata` / `.metadata`                   | `WithMetadata` / `metadata` / `.metadata`                                     | Arbitrary key-value pairs.                                                                           |
| Idempotency key      | `IdempotencyKey` / `idempotencyKey` / `.idempotencyKey` | `WithIdempotencyKey` / `idempotencyKey` / `.idempotencyKey`                   | Makes a retried write replay the stored result. See [conditional writes](/guide/conditional-writes). |
| Checksum             | `Checksums` / `checksums` / `.checksum`                 | `WithChecksumAlgorithm` / `checksumAlgorithm` / `.checksum`                   | Server verifies and persists an end-to-end digest. See [checksums](/guide/checksums).                |
| Create-only          | `IfNoneMatch:"*"` / `ifNoneMatch:'*'` / `.ifAbsent()`   | `WithPutIfNoneMatch("*")` / `ifNoneMatch: "*"` / `.ifNoneMatch("*")`          | Write only when the key is absent. See [conditional writes](/guide/conditional-writes).              |
| Overwrite-only       | `IfMatch` / `ifMatch` / `.ifMatch`                      | (use native or copy)                                                          | Write only when the current ETag matches. See [conditional writes](/guide/conditional-writes).       |
| SSE-S3               | (always on at rest)                                     | `WithServerSideEncryption` / `serverSideEncryption` / `.serverSideEncryption` | Requests server-managed encryption at rest.                                                          |
| Object Lock at write | set with `setObjectRetention` after write               | `WithObjectLock*` headers via `PutOptions`                                    | Apply retention or a legal hold. See [object lock](/guide/object-lock).                              |

Conditional create and overwrite are native-client features. The native `putObject` takes `If-None-Match` / `If-Match`
directly. The S3 `PutObject` does not, so for an existing object reach for a conditional [copy](/guide/copying-objects).
Lockwell issues presigned GET/PUT/HEAD/DELETE URLs on the S3 client; for a browser-direct upload you may also use a
native [signed URL](/guide/signed-urls).

::: info The S3 `PutObject` supports create-only `If-None-Match: *`. Use native `putObject` for overwrite-only
`If-Match`, or a conditional copy for an existing key. :::

### Encryption status and SSE-S3

The native client's encryption posture is a server deployment property. Production configs enable encryption, so native
writes are stored as encrypted chunks under per-tenant data keys and the native client has no SSE option. Native GET and
HEAD responses expose the `X-Lockwell-Encrypted` header; Java surfaces it as `GetResult.encrypted()` and
`HeadResult.encrypted()`. Do not run tenant-handling workloads on an encryption-disabled deployment.

On the S3 client, `WithServerSideEncryption` / `serverSideEncryption: true` / `.serverSideEncryption()` asks for SSE-S3:
a server-managed, per-tenant key encrypts the object at rest. The response echoes `serverSideEncryption: "AES256"`.
SSE-KMS is a non-goal. The S3 wire and all three first-party S3 clients expose typed SSE-C helpers; supply the same raw
32-byte customer key on every applicable read, write, copy, multipart-part, and completion request, and never log or
persist it in application telemetry.

User metadata is a lossless, duplicate-preserving user namespace. The merged metadata contract stores it separately from
internal SSE-C and Object Lock state, including for historical names such as `x-amz-meta-lockwell-sse-customer-key-md5`.
Caller metadata cannot manufacture an internal encryption marker. This is a metadata-boundary guarantee, not permission
to use the native or LNW/1 surfaces for SSE-C; genuine customer-provided keys remain an S3-only capability.

:::: s3

::: code-group

```ts [Node]
const put = await s3.putObject("vault", "ledger.json", Buffer.from(data), {
  serverSideEncryption: true,
});
console.log(put.serverSideEncryption); // "AES256"
```

```go [Go]
put, err := s3.PutObject(ctx, "vault", "ledger.json", data,
    lockwellsdk.WithServerSideEncryption())
fmt.Println(put.ServerSideEncryption) // "AES256"
```

```java [Java]
var put = s3.putObject("vault", "ledger.json", data,
    new LockwellClient.PutOptions().serverSideEncryption());
System.out.println(put.serverSideEncryption()); // "AES256"
```

:::

::::

## Put an object (streaming)

Hand the SDK a reader and the body goes to the server without ever sitting whole in memory. Reach for this when a file
is larger than you want to buffer.

The native client streams the raw bytes. A streaming body cannot be replayed, so the client mints a fresh token before
it starts streaming and a token-expiry 401 retry is never needed. A 401 from a genuinely revoked key still surfaces.

:::: native

::: code-group

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

// A ReadableStream / async-iterable body streams without buffering:
const file = Readable.toWeb(createReadStream("./big.bin"));
await native.putObject("reports", "big.bin", file, {
  contentType: "application/octet-stream",
  contentLength: 1_048_576, // set so the server enforces the size cap + quota up front
});
```

```go [Go]
f, _ := os.Open("./big.bin")
defer f.Close()
info, _ := f.Stat()

_, err := native.PutObject(ctx, lockwellnative.PutObjectInput{
    Bucket:        "reports",
    Key:           "big.bin",
    Body:          f,
    ContentType:   "application/octet-stream",
    ContentLength: info.Size(), // lets the server enforce the size cap + quota up front
})
```

```java [Java]
import java.io.FileInputStream;

// An InputStream supplier streams the body:
nativeClient.putObject("reports", "big.bin",
    () -> {
        try { return new FileInputStream("./big.bin"); }
        catch (Exception e) { throw new RuntimeException(e); }
    },
    new PutOptions().contentType("application/octet-stream"));
```

:::

::::

Set `contentLength` (native) where you know the size up front: it lets the server enforce the per-object size cap and
the tenant quota before it accepts a single byte, and it avoids chunked transfer encoding. For very large or resumable
uploads, use [multipart upload](/guide/multipart-uploads) instead.

:::: s3

The S3 client wraps the stream as an `aws-chunked` body with an end-to-end checksum trailer, so a checksum algorithm is
required on `putObjectStream`. An idempotency key is not supported for the S3 stream (the trailer is not known at
reservation time); use the buffered `PutObject` when you need idempotency.

::: code-group

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

// Checksum required; sent in an aws-chunked trailer:
await s3.putObjectStream("reports", "big.bin", createReadStream("./big.bin"), "CRC64NVME", {
  contentType: "application/octet-stream",
});
```

```go [Go]
g, _ := os.Open("./big.bin")
defer g.Close()
info, _ := g.Stat()

_, err := s3.PutObjectStream(ctx, "reports", "big.bin", g, info.Size(),
    lockwellsdk.ChecksumCRC64NVME,
    lockwellsdk.WithContentType("application/octet-stream"),
)
```

```java [Java]
import java.nio.file.Files;
import java.nio.file.Path;

try (var in = Files.newInputStream(Path.of("big.bin"))) {
    s3.putObjectStream("reports", "big.bin", in, "CRC64NVME",
        new LockwellClient.PutOptions().contentType("application/octet-stream"));
}
```

:::

::::

## Get an object

`GetObject` streams the body. On both clients you own the body and must close it, and the result carries the object's
metadata: content type, length, ETag, version id, and any checksums.

:::: native

::: code-group

```ts [Node]
// Streaming download (no whole-object buffering):
const obj = await native.getObjectStream("reports", "big.bin");
console.log(obj.contentType, obj.contentLength, obj.etag);
for await (const chunk of obj.body) {
  /* ...process chunk... */
}

// Buffered convenience (small objects):
const small = await native.getObject("reports", "q1/summary.txt");
console.log(small.body.toString("utf8"), small.metadata);
```

```go [Go]
// Streaming; close the reader:
obj, err := native.GetObject(ctx, lockwellnative.GetObjectInput{Bucket: "reports", Key: "big.bin"})
if err != nil {
    return err
}
defer obj.Close()
fmt.Println(obj.ContentType, obj.ContentLength, obj.ETag)
io.Copy(dst, obj)
```

```java [Java]
import com.lockwell.sdk.nativeapi.NativeTypes.GetResult;

// Streaming InputStream; try-with-resources closes it:
try (GetResult obj = nativeClient.getObject("reports", "big.bin")) {
    System.out.println(obj.contentType() + " " + obj.contentLength());
    obj.body().transferTo(out);
}
```

:::

::::

:::: s3

The same with the S3 client:

::: code-group

```ts [Node]
const s3obj = await s3.getObjectStream("reports", "big.bin");
const all = await s3obj.readAll(); // or iterate s3obj.body
```

```go [Go]
out, err := s3.GetObject(ctx, "reports", "big.bin")
if err != nil {
    if lockwellsdk.IsNotFound(err) { /* ... */ }
    return err
}
defer out.Body.Close()
io.Copy(dst, out.Body)
fmt.Println(out.ContentType, out.ContentLength, out.Metadata)
```

```java [Java]
import java.util.Map;

// Streaming:
try (var got = s3.getObjectStream("reports", "big.bin", Map.of())) {
    got.body().transferTo(out);
}

// Buffered (small objects):
var small = s3.getObject("reports", "q1/summary.txt", Map.of());
System.out.println(new String(small.body()) + " " + small.metadata());
```

:::

::::

### Byte ranges

Pass a range to fetch part of an object. The server replies `206 Partial Content` with a `Content-Range`. The native
client takes the raw HTTP `Range` value; the S3 client takes a typed range (start, end inclusive, `end < 0` means "to
end").

:::: native

::: code-group

```ts [Node]
const part = await native.getObjectStream("reports", "big.bin", { range: "bytes=0-1023" });
console.log(part.contentRange);
```

```go [Go]
part, _ := native.GetObject(ctx, lockwellnative.GetObjectInput{
    Bucket: "reports", Key: "big.bin", Range: "bytes=0-1023",
})
defer part.Close()
fmt.Println(part.ContentRange)
```

```java [Java]
// range + optional version:
try (var part = nativeClient.getObject("reports", "big.bin", "bytes=0-1023", null)) {
    System.out.println(part.contentRange());
}
```

:::

::::

:::: s3

::: code-group

```ts [Node]
const s3part = await s3.getObjectStream("reports", "big.bin", { range: "bytes=0-1023" });
```

```go [Go]
// Typed range, inclusive:
out, _ := s3.GetObject(ctx, "reports", "big.bin", lockwellsdk.WithRange(0, 1023))
defer out.Body.Close()
fmt.Println(out.ContentRange)
```

```java [Java]
// Range passed in the query/header map:
try (var part = s3.getObjectStream("reports", "big.bin", Map.of("Range", "bytes=0-1023"))) { /* ... */ }
```

:::

::::

### Reading a specific version

On a versioned bucket, pass a `versionId` to read a past version rather than the current one.

:::: native

::: code-group

```ts [Node]
const old = await native.getObject("reports", "q1/summary.txt", { versionId });
```

```go [Go]
old, _ := native.GetObject(ctx, lockwellnative.GetObjectInput{
    Bucket: "reports", Key: "q1/summary.txt", VersionID: versionID,
})
```

```java [Java]
// range + versionId:
try (var old = nativeClient.getObject("reports", "q1/summary.txt", null, versionId)) { /* ... */ }
```

:::

::::

:::: s3

::: code-group

```ts [Node]
const s3old = await s3.getObject("reports", "q1/summary.txt", { versionId });
```

```go [Go]
out, _ := s3.GetObject(ctx, "reports", "q1/summary.txt", lockwellsdk.WithVersionID(versionID))
```

```java [Java]
// versionId in the query map:
var s3old = s3.getObject("reports", "q1/summary.txt", Map.of("versionId", versionId));
```

:::

::::

### Response-header overrides (S3 client)

The S3 `GetObject` can override the headers the server returns for this one read, so you can force a download filename
or a content type without rewriting the object. These are the standard S3 `response-*` query overrides.

:::: s3

::: code-group

```ts [Node]
const dl = await s3.getObjectStream("reports", "q1.csv", {
  responseContentType: "text/csv",
});
```

```go [Go]
out, _ := s3.GetObject(ctx, "reports", "q1.csv",
    lockwellsdk.WithResponseContentType("text/csv"),
    lockwellsdk.WithResponseContentDisposition(`attachment; filename="q1.csv"`),
)
```

```java [Java]
var dl = s3.getObject("reports", "q1.csv", Map.of(
    "response-content-type", "text/csv",
    "response-content-disposition", "attachment; filename=\"q1.csv\""));
```

:::

::::

### Reading one multipart part (S3 client)

`WithPartNumber(n)` on the S3 `GetObject` returns the byte range of a single multipart part (1-based) plus the total
part count in `PartsCount`. This lets a downloader fetch the object part by part with the same boundaries it was
uploaded with.

:::: s3

```go
out, _ := s3.GetObject(ctx, "reports", "big.bin", lockwellsdk.WithPartNumber(1))
fmt.Println(out.PartsCount) // total parts
```

::::

## Head an object

`HeadObject` returns the same metadata as a read with no body: content type, length, ETag, version id, checksums, and
encryption status. A missing object is a not-found error on both clients.

:::: native

::: code-group

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

try {
  const head = await native.headObject("reports", "q1/summary.txt");
  console.log(head.contentLength, head.etag, head.metadata);
} catch (err) {
  if (isNativeNotFound(err)) {
    /* not there */
  } else throw err;
}
```

```go [Go]
info, err := native.HeadObject(ctx, lockwellnative.GetObjectInput{Bucket: "reports", Key: "q1/summary.txt"})
if err != nil {
    if lockwellnative.IsNotFound(err) { /* not there */ }
    return err
}
fmt.Println(info.ContentLength, info.ETag)
```

```java [Java]
var head = nativeClient.headObject("reports", "q1/summary.txt");
System.out.println(head.contentLength() + " " + head.etag());
```

:::

::::

:::: s3

::: code-group

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

try {
  const head = await s3.headObject("reports", "q1/summary.txt");
  console.log(head.contentLength, head.etag, head.metadata);
} catch (err) {
  if (isNotFound(err)) {
    /* not there */
  } else throw err;
}
```

```go [Go]
info, err := s3.HeadObject(ctx, "reports", "q1/summary.txt")
if err != nil {
    if lockwellsdk.IsNotFound(err) { /* not there */ }
    return err
}
fmt.Println(info.ContentLength, info.ETag, info.Metadata)
```

```java [Java]
// Returns a GetResult with an empty body:
var s3head = s3.headObject("reports", "q1/summary.txt");
System.out.println(s3head.contentLength() + " " + s3head.etag());
```

:::

::::

## Errors

Both clients raise a structured error carrying a stable code, the HTTP status, and a request id you can correlate with
an audit row.

| Status | Meaning                          | Native guard                                          | S3 guard                    |
| ------ | -------------------------------- | ----------------------------------------------------- | --------------------------- |
| 404    | no such bucket/key               | `IsNotFound` / `isNativeNotFound`                     | `IsNotFound` / `isNotFound` |
| 401    | bad/expired token or revoked key | `IsUnauthorized` / `isNativeUnauthorized`             | (re-sign)                   |
| 403    | scope or policy denial           | `IsForbidden` / `isNativeForbidden`                   | `APIError` code             |
| 409    | already exists                   | `IsAlreadyExists` / `isNativeConflict`                | `APIError` code             |
| 412    | precondition not met             | `IsPreconditionFailed` / `isNativePreconditionFailed` | `APIError` code             |
| 507    | tenant storage quota exceeded    | `IsQuotaExceeded` / `isNativeQuotaExceeded`           | `APIError` code             |

In Java, catch `NativeException` (native) or `ApiException` (S3) and branch on `statusCode()` / `code()`. Full handling,
including the retry policy, is on [Errors & retries](/guide/errors-and-retries).

## Next steps

- [Listing & pagination](/guide/listing-objects): find the objects you stored.
- [Copying objects](/guide/copying-objects): server-side copy, conditionals, large-object part copy.
- [Deleting objects](/guide/deleting-objects): single, batch, and versioned deletes.
- [Conditional writes](/guide/conditional-writes): create-only, overwrite-only, idempotency.
- [Multipart uploads](/guide/multipart-uploads): resumable uploads for very large objects.
- [Signed URLs](/guide/signed-urls): hand a browser a direct upload or download URL.
