Versioning
Versioning keeps every write of a key instead of overwriting in place. With versioning enabled, an overwrite creates a new version (the previous one stays recoverable), and a delete writes a delete marker (the object reads as gone, but the prior versions are still there). This is how you protect against accidental overwrites and deletes, and how object lock holds an immutable history.
A bucket is in one of three versioning states:
| State | Behavior |
|---|---|
| Disabled (default) | One version per key; overwrites and deletes are permanent. |
| Enabled | Every overwrite is a new version; every delete writes a delete marker. |
| Suspended | New writes get the null version id; existing versions are preserved. |
Versioning can be enabled, then suspended, then enabled again. Suspending never deletes existing versions.
Set and read the versioning state
Enable versioning before you need it: a version history only exists for writes made while versioning was enabled. The native client sets the state with setBucketVersioning and reads it with getBucketVersioning.
await native.setBucketVersioning("reports", "Enabled");
const status = await native.getBucketVersioning("reports"); // 'Enabled' | 'Suspended' | ''// The Go native client uses the lowercase "enabled" / "suspended" spelling:
_, err := native.SetBucketVersioning(ctx, "reports", "enabled")
state, err := native.GetBucketVersioning(ctx, "reports")
fmt.Println(state.Status) // "enabled" / "suspended" / "disabled"native.setBucketVersioning("reports", "enabled");
var state = native.getBucketVersioning("reports");
System.out.println(state.status()); // "enabled" / "suspended" / "disabled"The S3 client takes Enabled or Suspended (there is no Disabled value to set; a bucket that was never enabled reads back as an empty string):
await s3.putBucketVersioning("reports", "Enabled");
const status = await s3.getBucketVersioning("reports");
console.log(status); // "Enabled" (or "" when never enabled)err := s3.PutBucketVersioning(ctx, "reports", lockwellsdk.VersioningEnabled)
status, err := s3.GetBucketVersioning(ctx, "reports")
fmt.Println(status) // "Enabled" (or "" when never enabled)s3.putBucketVersioning("reports", "Enabled");
String status = s3.getBucketVersioning("reports"); // "Enabled" or ""Note the spelling difference: the native client takes enabled / suspended (the lowercase wire form) while the S3 client takes the capitalized Enabled / Suspended.
The two clients spell the versioning state differently. The native client uses lowercase enabled /
suspended; the S3 client uses capitalized Enabled / Suspended. :::
Versions and delete markers
Once versioning is enabled, each PUT to the same key stacks a new version. The latest version is what a plain GET returns.
await native.setBucketVersioning("docs", "Enabled");
const v1 = await native.putObject("docs", "readme.txt", "first");
const v2 = await native.putObject("docs", "readme.txt", "second");
console.log(v1.versionId, v2.versionId); // two distinct ids
const latest = await native.getObject("docs", "readme.txt");
console.log(latest.body.toString()); // "second"::::
await s3.putBucketVersioning("docs", "Enabled");
const v1 = await s3.putObject("docs", "readme.txt", "first");
const v2 = await s3.putObject("docs", "readme.txt", "second");
console.log(v1.versionId, v2.versionId); // two distinct ids
const latest = await s3.getObject("docs", "readme.txt");
console.log(latest.body.toString()); // "second"A delete in a versioned bucket does not remove the data. It writes a delete marker as the new latest version, so a plain GET returns a not-found while the prior versions remain.
await native.deleteObject("docs", "readme.txt"); // writes a delete marker
// A plain read now reports not-found:
try {
await native.getObject("docs", "readme.txt");
} catch (err) {
// isNativeNotFound(err) === true
}
// The prior versions are still readable by version id (see below).await s3.deleteObject("docs", "readme.txt"); // writes a delete marker
try {
await s3.getObject("docs", "readme.txt");
} catch (err) {
// isNotFound(err) === true
}To restore, delete the delete marker by its version id, which makes the previous version the latest again.
List versions and delete markers
listObjectVersions returns both real versions and delete markers in one listing, each carrying its versionId, isLatest flag, and (for versions) size and ETag. It is paginated by a (keyMarker, versionIdMarker) pair. On the native path, delete markers are folded into the versions list and flagged by a deleteMarker field on each entry (Node also surfaces a separate deleteMarkers array).
const page = await native.listObjectVersions("docs", { prefix: "readme", maxKeys: 100 });
for (const v of page.versions) {
console.log("version", v.key, v.versionId, v.isLatest, v.deleteMarker, v.size);
}page, err := native.ListObjectVersions(ctx, lockwellnative.ListObjectVersionsInput{
Bucket: "docs", Prefix: "readme", MaxKeys: 100,
})
for _, v := range page.Versions {
fmt.Println(v.Key, v.VersionID, v.IsLatest, v.DeleteMarker, v.Size)
}import com.lockwell.sdk.nativeapi.NativeTypes.ListVersionsOptions;
var page = native.listObjectVersions("docs",
new ListVersionsOptions("readme", null, null, 100));
page.versions().forEach(v ->
System.out.println(v.key() + " " + v.versionId() + " latest=" + v.isLatest()
+ " marker=" + v.deleteMarker()));The S3 ListObjectVersions splits the two into a versions list and a deleteMarkers list:
const page = await s3.listObjectVersions("docs", { prefix: "readme", maxKeys: 100 });
for (const v of page.versions) {
console.log("version", v.key, v.versionId, v.isLatest, v.size);
}
for (const m of page.deleteMarkers) {
console.log("delete-marker", m.key, m.versionId, m.isLatest);
}page, err := s3.ListObjectVersions(ctx, "docs",
lockwellsdk.WithVersionsPrefix("readme"),
lockwellsdk.WithVersionsMaxKeys(100))
for _, v := range page.Versions {
fmt.Println("version", v.Key, v.VersionID, v.IsLatest, v.Size)
}
for _, m := range page.DeleteMarkers {
fmt.Println("delete-marker", m.Key, m.VersionID, m.IsLatest)
}import com.lockwell.sdk.LockwellClient.ListVersionsOptions;
var page = s3.listObjectVersions("docs",
new ListVersionsOptions().prefix("readme").maxKeys(100));
page.versions().forEach(v ->
System.out.println("version " + v.key() + " " + v.versionId() + " latest=" + v.isLatest()));
page.deleteMarkers().forEach(m ->
System.out.println("delete-marker " + m.key() + " " + m.versionId()));Paginate every version
A version-id marker requires a key marker (both clients reject one without the other). On the native path, the Node client exposes a paginateObjectVersions async iterator that threads both markers for you; in Go and Java, follow IsTruncated with NextKeyMarker and NextVersionIDMarker.
for await (const page of native.paginateObjectVersions("docs", { prefix: "readme" })) {
for (const v of page.versions) console.log(v.key, v.versionId);
}var keyMarker, versionMarker string
for {
page, err := native.ListObjectVersions(ctx, lockwellnative.ListObjectVersionsInput{
Bucket: "docs", Prefix: "readme",
KeyMarker: keyMarker, VersionIDMarker: versionMarker,
})
if err != nil {
return err
}
for _, v := range page.Versions {
fmt.Println(v.Key, v.VersionID)
}
if !page.IsTruncated {
break
}
keyMarker, versionMarker = page.NextKeyMarker, page.NextVersionIDMarker
}String keyMarker = null, versionMarker = null;
for (;;) {
var page = native.listObjectVersions("docs",
new ListVersionsOptions("readme", keyMarker, versionMarker, null));
page.versions().forEach(v -> System.out.println(v.key() + " " + v.versionId()));
if (!page.isTruncated()) break;
keyMarker = page.nextKeyMarker();
versionMarker = page.nextVersionIdMarker();
}The S3 client offers the same manual paging plus a built-in paginator:
// Async iterator that threads both markers for you:
for await (const page of s3.paginateObjectVersions("docs", { prefix: "readme" })) {
for (const v of page.versions) console.log(v.key, v.versionId);
}// Manual paging:
var keyMarker, versionMarker string
for {
page, err := s3.ListObjectVersions(ctx, "docs",
lockwellsdk.WithKeyMarker(keyMarker),
lockwellsdk.WithVersionIDMarker(versionMarker))
if err != nil {
return err
}
for _, v := range page.Versions {
fmt.Println(v.Key, v.VersionID)
}
if !page.IsTruncated {
break
}
keyMarker, versionMarker = page.NextKeyMarker, page.NextVersionIDMarker
}var pager = s3.listObjectVersionsPaginator("docs",
new ListVersionsOptions().prefix("readme"));
while (pager.hasMorePages()) {
var page = pager.nextPage();
page.versions().forEach(v -> System.out.println(v.key() + " " + v.versionId()));
}Read and delete a specific version
Pass a version id to read or delete one exact version instead of the latest. The native client reads with the versionId option on getObject and deletes with deleteObject(bucket, key, { versionId }).
// Read an older version:
const old = await native.getObject("docs", "readme.txt", { versionId: v1.versionId });
console.log(old.body.toString()); // "first"
// Permanently delete one version (not a delete marker; the bytes are gone):
await native.deleteObject("docs", "readme.txt", { versionId: v1.versionId });// Read an older version:
old, err := native.GetObject(ctx, lockwellnative.GetObjectInput{
Bucket: "docs", Key: "readme.txt", VersionID: v1ID,
})
defer old.Close()
// Permanently delete one version:
_, err = native.DeleteObject(ctx, "docs", "readme.txt", v1ID)// Read an older version (range null, versionId set):
try (var old = native.getObject("docs", "readme.txt", null, v1Id)) { /* ... */ }
// Permanently delete one version:
native.deleteObject("docs", "readme.txt", v1Id);// Read an older version:
const old = await s3.getObject("docs", "readme.txt", { versionId: v1.versionId });
console.log(old.body.toString()); // "first"
// Permanently delete one version (not a delete marker; the bytes are gone):
await s3.deleteObject("docs", "readme.txt", { versionId: v1.versionId });// Read an older version:
old, err := s3.GetObject(ctx, "docs", "readme.txt", lockwellsdk.WithVersionID(v1ID))
defer old.Body.Close()
// Permanently delete one version:
err = s3.DeleteObject(ctx, "docs", "readme.txt", lockwellsdk.WithVersionID(v1ID))import java.util.Map;
// Read an older version (Range/versionId go through the query map):
var old = s3.getObject("docs", "readme.txt", Map.of("versionId", v1Id));Deleting a specific version id removes that version's data for good. Deleting without a version id (in a versioned bucket) writes a delete marker instead.
Suspended versioning and the null version
Suspending versioning stops creating new versions. Writes made while suspended take the special null version id ("null"). A later write to the same key while still suspended overwrites that null version in place, so at most one null version exists per key. Existing non-null versions from the enabled period are untouched and stay readable by their version ids.
await native.setBucketVersioning("docs", "Enabled");
const enabled = await native.putObject("docs", "config.json", '{"v":1}'); // real version id
await native.setBucketVersioning("docs", "Suspended");
await native.putObject("docs", "config.json", '{"v":2}'); // takes the "null" version id
await native.putObject("docs", "config.json", '{"v":3}'); // overwrites the same "null" version
// The enabled-period version is still there:
const v1 = await native.getObject("docs", "config.json", { versionId: enabled.versionId });await s3.putBucketVersioning("docs", "Enabled");
const enabled = await s3.putObject("docs", "config.json", '{"v":1}'); // real version id
await s3.putBucketVersioning("docs", "Suspended");
await s3.putObject("docs", "config.json", '{"v":2}'); // takes the "null" version id
await s3.putObject("docs", "config.json", '{"v":3}'); // overwrites the same "null" version
const v1 = await s3.getObject("docs", "config.json", { versionId: enabled.versionId });Re-enabling versioning resumes stacking new versions; it does not retroactively version the writes made while suspended.
Versioning and object lock
Object lock requires versioning, so a bucket created with object lock has versioning enabled and cannot return to a non-versioned state. Each version can carry its own retention deadline and legal hold. See object lock.
Related
- Object lock for per-version WORM retention and legal holds.
- Deleting objects for delete markers and batch deletes.
- Object tags for tagging a specific version.
- S3 operations reference for the full operation matrix.