Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions components/lifecycle/new-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { isMissingBucketConfiguration } from "@/lib/bucket-configuration"
import {
buildCurrentVersionExpirationRules,
buildLifecycleFilter,
buildNoncurrentVersionExpirationRule,
findIncompleteLifecycleTag,
getBucketVersioningMode,
hasCompleteLifecycleTags,
Expand Down Expand Up @@ -127,10 +128,6 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:
}
}, [open, loadTiers, loadVersioningStatus, tiersReloadVersion, versioningReloadVersion])

useEffect(() => {
if (activeTab !== "expire" || versionType !== "current") setExpiredDeleteMark(false)
}, [activeTab, versionType])

useEffect(() => {
if (!hasVersionHistory) {
setVersionType("current")
Expand Down Expand Up @@ -186,7 +183,7 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:
if (incompleteTagIndex >= 0) {
errors.tags = tags[incompleteTagIndex].key.trim() ? t("Please enter tag value") : t("Please enter key name")
}
if (activeTab === "expire" && versionType === "current" && expiredDeleteMark && hasCompleteLifecycleTags(tags)) {
if (activeTab === "expire" && expiredDeleteMark && hasCompleteLifecycleTags(tags)) {
errors.deleteMarker = `${t("Delete Marker Handling")}: ${t("Tags")} ${t("Unsupported")}`
}

Expand Down Expand Up @@ -243,8 +240,7 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:

if (activeTab === "expire") {
if (versionType === "non-current") {
baseRule.NoncurrentVersionExpiration = { NoncurrentDays: daysValue }
newRules = [baseRule]
newRules = [buildNoncurrentVersionExpirationRule(baseId, daysValue, filter, expiredDeleteMark)]
} else {
newRules = buildCurrentVersionExpirationRules(baseId, daysValue, filter, expiredDeleteMark)
}
Expand Down Expand Up @@ -490,7 +486,7 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:
</div>
</details>

{hasVersionHistory && versionType === "current" && (
{hasVersionHistory && (
<details>
<summary className="cursor-pointer text-sm font-medium text-primary">
{t("Advanced Settings")}
Expand Down Expand Up @@ -539,7 +535,7 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:
</Alert>
) : null}

{activeTab === "expire" && expiredDeleteMark ? (
{activeTab === "expire" && versionType === "current" && expiredDeleteMark ? (
<p className="text-sm text-muted-foreground">{t("2 lifecycle rules will be created.")}</p>
) : null}
</TabsContent>
Expand Down
15 changes: 15 additions & 0 deletions lib/bucket-lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,18 @@ export function buildCurrentVersionExpirationRules(

return rules
}

export function buildNoncurrentVersionExpirationRule(
id: string,
days: number,
filter: Record<string, unknown>,
cleanupExpiredDeleteMarkers: boolean,
): Record<string, unknown> {
return {
ID: id,
Status: "Enabled",
Filter: filter,
NoncurrentVersionExpiration: { NoncurrentDays: days },
...(cleanupExpiredDeleteMarkers ? { Expiration: { ExpiredObjectDeleteMarker: true } } : {}),
}
}
44 changes: 43 additions & 1 deletion tests/lib/bucket-settings-safety.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import {
buildCurrentVersionExpirationRules,
buildLifecycleFilter,
buildNoncurrentVersionExpirationRule,
findIncompleteLifecycleTag,
getBucketVersioningMode,
hasCompleteLifecycleTags,
Expand Down Expand Up @@ -155,6 +156,39 @@ test("AWS SDK serializes expiration days and delete-marker cleanup as two valid
assert.deepEqual(expirations, ["<Days>30</Days>", "<ExpiredObjectDeleteMarker>true</ExpiredObjectDeleteMarker>"])
})

test("noncurrent expiration and delete-marker cleanup serialize as one rule without current expiry", async () => {
let requestBody = ""
const client = new S3Client({
region: "us-east-1",
endpoint: "http://127.0.0.1:9000",
credentials: { accessKeyId: "test", secretAccessKey: "test" },
requestHandler: {
async handle(request) {
requestBody = typeof request.body === "string" ? request.body : new TextDecoder().decode(request.body)
return { response: { statusCode: 200, headers: {}, body: new Uint8Array() } }
},
},
})

await client.send(
new PutBucketLifecycleConfigurationCommand({
Bucket: "test-bucket",
LifecycleConfiguration: {
Rules: [buildNoncurrentVersionExpirationRule("rule", 1, { Prefix: "" }, true)],
},
}),
)
client.destroy()

assert.equal((requestBody.match(/<Rule>/g) ?? []).length, 1)
assert.match(
requestBody,
/<NoncurrentVersionExpiration><NoncurrentDays>1<\/NoncurrentDays><\/NoncurrentVersionExpiration>/,
)
assert.match(requestBody, /<Expiration><ExpiredObjectDeleteMarker>true<\/ExpiredObjectDeleteMarker><\/Expiration>/)
assert.doesNotMatch(requestBody, /<Expiration><(?:Days|Date)>/)
})

test("lifecycle helpers preserve suspended versioning and reject partial tag pairs", () => {
assert.equal(MAX_LIFECYCLE_RULES, 1000)
assert.equal(getBucketVersioningMode("Enabled"), "enabled")
Expand All @@ -171,11 +205,19 @@ test("lifecycle helpers preserve suspended versioning and reject partial tag pai
lifecycleFormSource,
/buildCurrentVersionExpirationRules\(baseId, daysValue, filter, expiredDeleteMark\)/,
)
assert.match(
lifecycleFormSource,
/buildNoncurrentVersionExpirationRule\(baseId, daysValue, filter, expiredDeleteMark\)/,
)
assert.match(lifecycleFormSource, /Rules: \[\.\.\.existingRules, \.\.\.newRules\]/)
assert.match(lifecycleFormSource, /existingRules\.length \+ newRules\.length > MAX_LIFECYCLE_RULES/)
assert.match(lifecycleFormSource, /const hasVersionHistory = versioningMode !== "unversioned"/)
assert.match(lifecycleFormSource, /expiredDeleteMark && hasCompleteLifecycleTags\(tags\)/)
assert.match(lifecycleFormSource, /activeTab === "expire" && expiredDeleteMark && hasCompleteLifecycleTags\(tags\)/)
assert.match(lifecycleFormSource, /setExpiredDeleteMark\(checked === true\)/)
assert.doesNotMatch(
lifecycleFormSource,
/activeTab !== "expire" \|\| versionType !== "current"\) setExpiredDeleteMark\(false\)/,
)
})

test("bucket reads fail closed and stale responses cannot update the active bucket", () => {
Expand Down