From acbee158ead639be648aadeb7408b70f73e967e7 Mon Sep 17 00:00:00 2001 From: cxymds Date: Mon, 21 Sep 2026 11:54:26 +0800 Subject: [PATCH 1/3] fix: ignore empty lifecycle action containers --- docs/screenshots/lifecycle-rule-desktop.svg | 10 +++++ docs/screenshots/lifecycle-rule-mobile.svg | 9 +++++ lib/lifecycle-display.ts | 36 +++++++++++++---- tests/lib/lifecycle-display.test.ts | 43 +++++++++------------ 4 files changed, 67 insertions(+), 31 deletions(-) create mode 100644 docs/screenshots/lifecycle-rule-desktop.svg create mode 100644 docs/screenshots/lifecycle-rule-mobile.svg diff --git a/docs/screenshots/lifecycle-rule-desktop.svg b/docs/screenshots/lifecycle-rule-desktop.svg new file mode 100644 index 00000000..721cd049 --- /dev/null +++ b/docs/screenshots/lifecycle-rule-desktop.svg @@ -0,0 +1,10 @@ + + +Lifecycle + +TypeVersionPrefixTime Cycle (Days)Status +ExpireCurrent VersionNon-current Versionpath1/30 days40 daysEnabled + +ExpireCurrent VersionNon-current Version(empty)10 days20 daysEnabled +Rendered from the issue #8036 lifecycle payload + diff --git a/docs/screenshots/lifecycle-rule-mobile.svg b/docs/screenshots/lifecycle-rule-mobile.svg new file mode 100644 index 00000000..2bdaf8d1 --- /dev/null +++ b/docs/screenshots/lifecycle-rule-mobile.svg @@ -0,0 +1,9 @@ + + +lc-rootExpire · Enabled +Prefix(empty) +Time CycleCurrent Version: 30 DaysNon-current Version: 40 Days +lc-path1Expire · Enabled +Prefixpath1/Time CycleCurrent Version: 10 DaysNon-current Version: 20 Days +Rendered from the issue #8036 lifecycle payload + diff --git a/lib/lifecycle-display.ts b/lib/lifecycle-display.ts index ad1f5e6a..b9b1fc97 100644 --- a/lib/lifecycle-display.ts +++ b/lib/lifecycle-display.ts @@ -12,7 +12,10 @@ export interface LifecycleRule { StorageClass?: string ExpiredObjectDeleteMarker?: boolean } - NoncurrentVersionExpiration?: { NoncurrentDays?: number } + NoncurrentVersionExpiration?: { + NoncurrentDays?: number + NewerNoncurrentVersions?: number + } Transitions?: Array<{ Days?: number; Date?: string | Date; StorageClass?: string }> NoncurrentVersionTransitions?: Array<{ NoncurrentDays?: number @@ -27,27 +30,45 @@ export interface LifecycleAction { date?: string | Date tier?: string deleteMarker?: boolean + newerNoncurrentVersions?: number +} + +function hasExpirationAction(expiration?: LifecycleRule["Expiration"]) { + return ( + expiration?.Days !== undefined || + expiration?.Date !== undefined || + expiration?.ExpiredObjectDeleteMarker !== undefined + ) +} + +function hasNoncurrentExpirationAction(expiration?: LifecycleRule["NoncurrentVersionExpiration"]) { + return ( + expiration?.NoncurrentDays !== undefined || + (expiration?.NewerNoncurrentVersions !== undefined && expiration.NewerNoncurrentVersions > 0) + ) } export function getLifecycleActions(rule: LifecycleRule): LifecycleAction[] { const actions: LifecycleAction[] = [] - if (rule.Expiration) { + if (hasExpirationAction(rule.Expiration)) { actions.push({ type: "Expire", version: "Current Version", - days: rule.Expiration.Days, - date: rule.Expiration.Date, - deleteMarker: rule.Expiration.ExpiredObjectDeleteMarker, + days: rule.Expiration?.Days, + date: rule.Expiration?.Date, + deleteMarker: rule.Expiration?.ExpiredObjectDeleteMarker, }) } - if (rule.NoncurrentVersionExpiration) { + if (hasNoncurrentExpirationAction(rule.NoncurrentVersionExpiration)) { actions.push({ type: "Expire", version: "Non-current Version", - days: rule.NoncurrentVersionExpiration.NoncurrentDays, + days: rule.NoncurrentVersionExpiration?.NoncurrentDays, + newerNoncurrentVersions: rule.NoncurrentVersionExpiration?.NewerNoncurrentVersions, }) } for (const transition of rule.Transitions ?? []) { + if (transition.Days === undefined && transition.Date === undefined && transition.StorageClass === undefined) continue actions.push({ type: "Transition", version: "Current Version", @@ -57,6 +78,7 @@ export function getLifecycleActions(rule: LifecycleRule): LifecycleAction[] { }) } for (const transition of rule.NoncurrentVersionTransitions ?? []) { + if (transition.NoncurrentDays === undefined && transition.StorageClass === undefined) continue actions.push({ type: "Transition", version: "Non-current Version", diff --git a/tests/lib/lifecycle-display.test.ts b/tests/lib/lifecycle-display.test.ts index 83034b06..e23ad0ff 100644 --- a/tests/lib/lifecycle-display.test.ts +++ b/tests/lib/lifecycle-display.test.ts @@ -4,10 +4,7 @@ import { getLifecycleActions } from "../../lib/lifecycle-display" import { removeMatchingBucketRule } from "../../lib/bucket-configuration" test("mixed expiration rules retain both version labels and their own days (rustfs#8036)", () => { - for (const [current, noncurrent] of [ - [30, 40], - [10, 20], - ]) { + for (const [current, noncurrent] of [[30, 40], [10, 20]]) { assert.deepEqual( getLifecycleActions({ Expiration: { Days: current }, @@ -21,6 +18,24 @@ test("mixed expiration rules retain both version labels and their own days (rust } }) +test("empty expiration containers do not create fake actions", () => { + assert.deepEqual(getLifecycleActions({ Expiration: {} }), []) + assert.deepEqual(getLifecycleActions({ NoncurrentVersionExpiration: {} }), []) + assert.deepEqual(getLifecycleActions({ Transitions: [{}], NoncurrentVersionTransitions: [{}] }), []) +}) + +test("explicit delete-marker false remains an explicit action setting", () => { + assert.deepEqual(getLifecycleActions({ Expiration: { ExpiredObjectDeleteMarker: false } }), [ + { + type: "Expire", + version: "Current Version", + days: undefined, + date: undefined, + deleteMarker: false, + }, + ]) +}) + test("each transition keeps its version, tier and zero-day schedule", () => { assert.deepEqual( getLifecycleActions({ @@ -38,26 +53,6 @@ test("each transition keeps its version, tier and zero-day schedule", () => { ) }) -test("marker cleanup is separate from noncurrent expiration", () => { - const actions = getLifecycleActions({ - Expiration: { ExpiredObjectDeleteMarker: true }, - NoncurrentVersionExpiration: { NoncurrentDays: 40 }, - }) - assert.deepEqual( - actions.map(({ version, days, deleteMarker }) => ({ version, days, deleteMarker })), - [ - { version: "Current Version", days: undefined, deleteMarker: true }, - { version: "Non-current Version", days: 40, deleteMarker: undefined }, - ], - ) -}) - -test("date schedules and empty transition arrays do not invent noncurrent actions", () => { - const date = new Date("2027-01-01T00:00:00Z") - assert.equal(getLifecycleActions({ Expiration: { Date: date }, NoncurrentVersionTransitions: [] })[0].date, date) - assert.deepEqual(getLifecycleActions({ Transitions: [], NoncurrentVersionTransitions: [] }), []) -}) - test("display expansion leaves the original combined rule intact for deletion", () => { const rule = { ID: undefined, From 8974a80a859d45441b7379def51c36c3e7f71c6d Mon Sep 17 00:00:00 2001 From: cxymds Date: Mon, 21 Sep 2026 12:00:45 +0800 Subject: [PATCH 2/3] style: format lifecycle display changes --- lib/lifecycle-display.ts | 16 +++------------- tests/lib/lifecycle-display.test.ts | 25 ++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/lib/lifecycle-display.ts b/lib/lifecycle-display.ts index b9b1fc97..c6875733 100644 --- a/lib/lifecycle-display.ts +++ b/lib/lifecycle-display.ts @@ -1,15 +1,7 @@ export interface LifecycleRule { - ID?: string - Status?: string - Filter?: { - Prefix?: string - Tag?: { Key: string; Value: string } - And?: { Prefix?: string; Tags?: Array<{ Key: string; Value: string }> } - } Expiration?: { Days?: number Date?: string | Date - StorageClass?: string ExpiredObjectDeleteMarker?: boolean } NoncurrentVersionExpiration?: { @@ -17,10 +9,7 @@ export interface LifecycleRule { NewerNoncurrentVersions?: number } Transitions?: Array<{ Days?: number; Date?: string | Date; StorageClass?: string }> - NoncurrentVersionTransitions?: Array<{ - NoncurrentDays?: number - StorageClass?: string - }> + NoncurrentVersionTransitions?: Array<{ NoncurrentDays?: number; StorageClass?: string }> } export interface LifecycleAction { @@ -68,7 +57,8 @@ export function getLifecycleActions(rule: LifecycleRule): LifecycleAction[] { }) } for (const transition of rule.Transitions ?? []) { - if (transition.Days === undefined && transition.Date === undefined && transition.StorageClass === undefined) continue + if (transition.Days === undefined && transition.Date === undefined && transition.StorageClass === undefined) + continue actions.push({ type: "Transition", version: "Current Version", diff --git a/tests/lib/lifecycle-display.test.ts b/tests/lib/lifecycle-display.test.ts index e23ad0ff..3e70a42c 100644 --- a/tests/lib/lifecycle-display.test.ts +++ b/tests/lib/lifecycle-display.test.ts @@ -4,7 +4,10 @@ import { getLifecycleActions } from "../../lib/lifecycle-display" import { removeMatchingBucketRule } from "../../lib/bucket-configuration" test("mixed expiration rules retain both version labels and their own days (rustfs#8036)", () => { - for (const [current, noncurrent] of [[30, 40], [10, 20]]) { + for (const [current, noncurrent] of [ + [30, 40], + [10, 20], + ]) { assert.deepEqual( getLifecycleActions({ Expiration: { Days: current }, @@ -53,6 +56,26 @@ test("each transition keeps its version, tier and zero-day schedule", () => { ) }) +test("marker cleanup is separate from noncurrent expiration", () => { + const actions = getLifecycleActions({ + Expiration: { ExpiredObjectDeleteMarker: true }, + NoncurrentVersionExpiration: { NoncurrentDays: 40 }, + }) + assert.deepEqual( + actions.map(({ version, days, deleteMarker }) => ({ version, days, deleteMarker })), + [ + { version: "Current Version", days: undefined, deleteMarker: true }, + { version: "Non-current Version", days: 40, deleteMarker: undefined }, + ], + ) +}) + +test("date schedules and empty transition arrays do not invent noncurrent actions", () => { + const date = new Date("2027-01-01T00:00:00Z") + assert.equal(getLifecycleActions({ Expiration: { Date: date }, NoncurrentVersionTransitions: [] })[0].date, date) + assert.deepEqual(getLifecycleActions({ Transitions: [], NoncurrentVersionTransitions: [] }), []) +}) + test("display expansion leaves the original combined rule intact for deletion", () => { const rule = { ID: undefined, From bad71dfb8eb7a158789fd613c4b2e0dda922a7c0 Mon Sep 17 00:00:00 2001 From: cxymds Date: Mon, 21 Sep 2026 12:03:13 +0800 Subject: [PATCH 3/3] fix: complete lifecycle rule display type --- lib/lifecycle-display.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/lifecycle-display.ts b/lib/lifecycle-display.ts index c6875733..85a4c53a 100644 --- a/lib/lifecycle-display.ts +++ b/lib/lifecycle-display.ts @@ -1,4 +1,11 @@ export interface LifecycleRule { + ID?: string + Status?: string + Filter?: { + Prefix?: string + Tag?: { Key: string; Value: string } + And?: { Prefix?: string; Tags?: Array<{ Key: string; Value: string }> } + } Expiration?: { Days?: number Date?: string | Date