# Copying objects

`CopyObject` duplicates an object server-side, so the bytes never travel back to your process. Use it to move a key,
branch a version, change metadata, or apply encryption on the copy. For objects too large to copy in one request, the S3
`UploadPartCopy` assembles a new object from ranges of existing ones.

Copies stay within the tenant. On the native path the source resolves under the token's tenant, so a cross-tenant copy
is impossible by construction.

For the operation matrix, see the [native data-plane reference](/reference/native-api) and the
[S3 operations reference](/reference/s3-operations#multipart-uploads).

## Copy within a bucket

Name the destination (bucket, key) and the source (bucket, key, and an optional source version id). Leave the source
version unset to copy the current version.

:::: native

::: code-group

```ts [Node]
const res = await native.copyObject("reports", "q1/summary-copy.txt", {
  sourceBucket: "reports",
  sourceKey: "q1/summary.txt",
});
console.log(res.etag, res.versionId);
```

```go [Go]
res, err := native.CopyObject(ctx, lockwellnative.CopyObjectInput{
    Bucket:       "reports",
    Key:          "q1/summary-copy.txt",
    SourceBucket: "reports",
    SourceKey:    "q1/summary.txt",
})
fmt.Println(res.ETag, res.VersionID)
```

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

// copyObject(destBucket, destKey, sourceBucket, sourceKey, opts)
var res = nativeClient.copyObject("reports", "q1/summary-copy.txt",
    "reports", "q1/summary.txt", new CopyOptions());
System.out.println(res.etag() + " " + res.versionId());
```

:::

::::

:::: s3

The S3 client names the source first, then the destination:

::: code-group

```ts [Node]
const res = await s3.copyObject("reports", "q1/summary.txt", "", "reports", "q1/summary-copy.txt");
console.log(res.etag, res.versionId);
```

```go [Go]
// CopyObject(srcBucket, srcKey, srcVersionID, dstBucket, dstKey, opts...)
res, err := s3.CopyObject(ctx, "reports", "q1/summary.txt", "", "reports", "q1/summary-copy.txt")
fmt.Println(res.ETag, res.VersionID)
```

```java [Java]
// copyObject(srcBucket, srcKey, srcVersionId, dstBucket, dstKey, ifMatch)
var res = s3.copyObject("reports", "q1/summary.txt", null, "reports", "q1/summary-copy.txt", null);
System.out.println(res.etag() + " " + res.versionId());
```

:::

::::

## Copy across buckets

The destination bucket can differ from the source bucket. Both must belong to the same tenant.

:::: native

::: code-group

```ts [Node]
await native.copyObject("archive", "imports/upload.csv", {
  sourceBucket: "inbox",
  sourceKey: "upload.csv",
});
```

```go [Go]
_, err := native.CopyObject(ctx, lockwellnative.CopyObjectInput{
    Bucket: "archive", Key: "imports/upload.csv",
    SourceBucket: "inbox", SourceKey: "upload.csv",
})
```

```java [Java]
nativeClient.copyObject("archive", "imports/upload.csv", "inbox", "upload.csv", new CopyOptions());
```

:::

::::

:::: s3

::: code-group

```ts [Node]
await s3.copyObject("inbox", "upload.csv", "", "archive", "imports/upload.csv");
```

```go [Go]
_, err := s3.CopyObject(ctx, "inbox", "upload.csv", "", "archive", "imports/upload.csv")
```

```java [Java]
s3.copyObject("inbox", "upload.csv", null, "archive", "imports/upload.csv", null);
```

:::

::::

## Copy-source conditionals

Gate the copy on the state of the **source** object. The copy proceeds only if the condition holds; otherwise it fails
with a `412 PreconditionFailed`.

| Condition                         | Effect                                                  |
| --------------------------------- | ------------------------------------------------------- |
| If-Match `<etag>`                 | Copy only if the source ETag matches.                   |
| If-None-Match `<etag>`            | Copy only if the source ETag does not match.            |
| If-Modified-Since `<http-date>`   | Copy only if the source changed since the date.         |
| If-Unmodified-Since `<http-date>` | Copy only if the source has not changed since the date. |

The native client takes all four as `ifMatch` / `ifNoneMatch` / `ifModifiedSince` / `ifUnmodifiedSince`. The S3 client
takes them as `WithCopyIf*` options.

::: warning The Java S3 `copyObject` accepts only an `ifMatch` argument. For the other three source conditionals on
Java, use the native client. :::

:::: native

::: code-group

```ts [Node]
await native.copyObject("reports", "q1/pinned.txt", {
  sourceBucket: "reports",
  sourceKey: "q1/summary.txt",
  ifUnmodifiedSince: "Wed, 01 Jan 2026 00:00:00 GMT",
});
```

```go [Go]
_, err := native.CopyObject(ctx, lockwellnative.CopyObjectInput{
    Bucket: "reports", Key: "q1/pinned.txt",
    SourceBucket: "reports", SourceKey: "q1/summary.txt",
    IfUnmodifiedSince: "Wed, 01 Jan 2026 00:00:00 GMT",
})
```

```java [Java]
// The native client covers all four source conditionals:
nativeClient.copyObject("reports", "q1/pinned.txt", "reports", "q1/summary.txt",
    new CopyOptions().ifUnmodifiedSince("Wed, 01 Jan 2026 00:00:00 GMT"));
```

:::

::::

:::: s3

::: code-group

```ts [Node]
// Copy only if the source still has the ETag we expect:
await s3.copyObject("reports", "q1/summary.txt", "", "reports", "q1/pinned.txt", {
  ifMatch: '"d41d8cd98f00b204e9800998ecf8427e"',
});
```

```go [Go]
_, err := s3.CopyObject(ctx, "reports", "q1/summary.txt", "", "reports", "q1/pinned.txt",
    lockwellsdk.WithCopyIfMatch(`"d41d8cd98f00b204e9800998ecf8427e"`),
)
if lockwellsdk.IsNotFound(err) { /* ... */ }
```

```java [Java]
// S3 takes ifMatch directly (the last argument):
var res = s3.copyObject("reports", "q1/summary.txt", null, "reports", "q1/pinned.txt",
    "\"d41d8cd98f00b204e9800998ecf8427e\"");
```

:::

::::

### Destination conditionals (native client)

The native copy also gates on the **destination** atomically at the commit: `requireAbsent` copies only when the
destination key does not exist (a copy-time create-only), and `requireMatchEtag` copies only when the destination's
current ETag matches (a copy-time overwrite-only). These pair naturally with the
[conditional write](/guide/conditional-writes) patterns.

::: code-group

```ts [Node]
// Branch a snapshot, but never clobber an existing one:
await native.copyObject("snapshots", "latest.json", {
  sourceBucket: "live",
  sourceKey: "state.json",
  requireAbsent: true, // 412 if snapshots/latest.json already exists
});
```

```go [Go]
_, err := native.CopyObject(ctx, lockwellnative.CopyObjectInput{
    Bucket: "snapshots", Key: "latest.json",
    SourceBucket: "live", SourceKey: "state.json",
    RequireAbsent: true, // 412 if snapshots/latest.json already exists
})
```

```java [Java]
// ifAbsent() sets the destination require-absent precondition:
nativeClient.copyObject("snapshots", "latest.json", "live", "state.json",
    new CopyOptions().ifAbsent()); // 412 if snapshots/latest.json already exists
```

:::

## Metadata directive: COPY vs REPLACE

By default a copy inherits the source's user metadata and content type (`metadataDirective: "COPY"`). Pass `REPLACE` to
set fresh metadata and content type from the request instead.

On the native client, set the directive explicitly. On the S3 client, supplying copy metadata implies `REPLACE` (the SDK
sets the directive for you when you pass metadata).

:::: native

::: code-group

```ts [Node]
// Explicit REPLACE with a new content type + metadata:
await native.copyObject("reports", "q1/tagged.txt", {
  sourceBucket: "reports",
  sourceKey: "q1/summary.txt",
  metadataDirective: "REPLACE",
  contentType: "text/plain; charset=utf-8",
  metadata: { reviewed: "true" },
});
```

```go [Go]
_, err := native.CopyObject(ctx, lockwellnative.CopyObjectInput{
    Bucket: "reports", Key: "q1/tagged.txt",
    SourceBucket: "reports", SourceKey: "q1/summary.txt",
    MetadataDirective: "REPLACE",
    ContentType:       "text/plain; charset=utf-8",
    Metadata:          map[string]string{"reviewed": "true"},
})
```

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

// replaceMetadata sets the directive to REPLACE and the new values:
nativeClient.copyObject("reports", "q1/tagged.txt", "reports", "q1/summary.txt",
    new CopyOptions().replaceMetadata("text/plain; charset=utf-8", Map.of("reviewed", "true")));
```

:::

::::

:::: s3

::: code-group

```ts [Node]
// Passing metadata replaces the destination's metadata (sets REPLACE):
await s3.copyObject("reports", "q1/summary.txt", "", "reports", "q1/tagged.txt", {
  metadata: { reviewed: "true" },
});
```

```go [Go]
// WithCopyMetadata replaces the destination metadata (sets REPLACE):
_, err := s3.CopyObject(ctx, "reports", "q1/summary.txt", "", "reports", "q1/tagged.txt",
    lockwellsdk.WithCopyMetadata(map[string]string{"reviewed": "true"}),
)
```

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

// The S3 copyObject does not take a metadata argument; use the native client for
// a metadata-replacing copy.
```

:::

::::

With `COPY` (the default), the source's content type and every `X-Lockwell-Meta-*` / `x-amz-meta-*` entry carry over
unchanged.

## Encryption on the copy (S3 client)

The native store is always encrypted at rest, so the native copy has no SSE option. On the S3 client
`WithCopyServerSideEncryption` / `serverSideEncryption: true` requests SSE-S3 for the destination object, so you can
encrypt a previously unencrypted object by copying it onto itself or to a new key.

:::: s3

```go
_, err := s3.CopyObject(ctx, "vault", "ledger.json", "", "vault", "ledger.json",
    lockwellsdk.WithCopyServerSideEncryption(),
)
```

::::

## Large objects: UploadPartCopy (S3 client)

A single `CopyObject` copies the whole object in one request. For an object too large for that, or to assemble a new
object from ranges of existing ones, copy **parts**: start a multipart upload on the destination, then call
`UploadPartCopy` for each part, naming a source object and an optional byte range. Complete the upload with the returned
part ETags.

`UploadPartCopy` lives on the S3 client. The native multipart path uploads part bytes rather than copying ranges; see
[multipart uploads](/guide/multipart-uploads). Each part except the last must meet the multipart minimum part size.

:::: s3

::: code-group

```ts [Node]
const { uploadId } = await s3.createMultipartUpload("archive", "merged.bin");

// Each part copies a byte range from a source object (no bytes through your app):
const p1 = await s3.uploadPartCopy("inbox", "a.bin", "", "archive", "merged.bin", uploadId, 1, "bytes=0-5242879");
const p2 = await s3.uploadPartCopy("inbox", "b.bin", "", "archive", "merged.bin", uploadId, 2, ""); // whole object

await s3.completeMultipartUpload("archive", "merged.bin", uploadId, [
  { partNumber: 1, etag: p1.etag },
  { partNumber: 2, etag: p2.etag },
]);
```

```go [Go]
mu, _ := s3.CreateMultipartUpload(ctx, "archive", "merged.bin")

// UploadPartCopy(srcBucket, srcKey, srcVersionID, dstBucket, dstKey, uploadID, partNumber, byteRange)
e1, _ := s3.UploadPartCopy(ctx,
    "inbox", "a.bin", "", "archive", "merged.bin", mu.UploadID, 1, "bytes=0-5242879")
e2, _ := s3.UploadPartCopy(ctx,
    "inbox", "b.bin", "", "archive", "merged.bin", mu.UploadID, 2, "") // whole object

_, _ = s3.CompleteMultipartUpload(ctx, "archive", "merged.bin", mu.UploadID,
    []lockwellsdk.CompletedPart{{PartNumber: 1, ETag: e1.ETag}, {PartNumber: 2, ETag: e2.ETag}})
```

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

var mu = s3.createMultipartUpload("archive", "merged.bin", null);

// uploadPartCopy(bucket, key, uploadId, partNumber, srcBucket, srcKey, srcVersionId, copySourceRange)
String e1 = s3.uploadPartCopy("archive", "merged.bin", mu.uploadId(), 1,
    "inbox", "a.bin", null, "bytes=0-5242879");
String e2 = s3.uploadPartCopy("archive", "merged.bin", mu.uploadId(), 2,
    "inbox", "b.bin", null, null);

s3.completeMultipartUpload("archive", "merged.bin", mu.uploadId(),
    null, List.of(e1, e2));
```

:::

::::

## Next steps

- [Multipart uploads](/guide/multipart-uploads): the full upload-by-parts flow.
- [Conditional writes](/guide/conditional-writes): the create-only / overwrite-only patterns the destination
  conditionals mirror.
- [Object tags](/guide/object-tags): tag the copies you make.
- [Deleting objects](/guide/deleting-objects): remove the source after a move.
