From 2799329ca0158bd978985a4ef3bf4b9df2f4f15e Mon Sep 17 00:00:00 2001 From: Thomas Flament Date: Thu, 18 Jun 2026 13:42:22 +0200 Subject: [PATCH] feat(lifecycle): support Days=0 expiration in RulesReducer Honor Days=0 (and NoncurrentDays=0) lifecycle expiration rules, the explicit "empty the bucket" intent, in the V2 listing optimizer. The reducer used truthy `if (r.Expiration.Days)` gates that dropped a Days=0 rule before any listing was created; switch the current, noncurrent and orphan-delete-marker expiration gates to `!== undefined`. days=0 then yields a listing with no BeforeDate (lists all objects), the same internal state already produced by a past-dated Expiration.Date. Per-object eligibility checks and the MPU-abort path already handle 0, so no other backbeat change is needed. Issue: BB-791 --- extensions/lifecycle/util/RulesReducer.js | 9 ++-- tests/unit/lifecycle/RulesReducer.spec.js | 23 ++++++++ .../lifecycle/RulesReducerWithVersion.spec.js | 46 ++++++++++++++++ tests/unit/lifecycle/rules.spec.js | 25 +++++++++ tests/unit/lifecycle/rulesWithVersion.spec.js | 52 +++++++++++++++++++ 5 files changed, 150 insertions(+), 5 deletions(-) diff --git a/extensions/lifecycle/util/RulesReducer.js b/extensions/lifecycle/util/RulesReducer.js index b79163a0c9..19d2fefd7d 100644 --- a/extensions/lifecycle/util/RulesReducer.js +++ b/extensions/lifecycle/util/RulesReducer.js @@ -142,8 +142,7 @@ class RulesReducer { let storageClass; if (r.Expiration) { - // NOTE: Expiration Days cannot be 0. - if (r.Expiration.Days) { + if (r.Expiration.Days !== undefined) { days = this._decrementExpirationDay(r.Expiration.Days); } else if (r.Expiration.Date) { if (r.Expiration.Date <= this._currentDate) { @@ -200,8 +199,8 @@ class RulesReducer { let storageClass; if (r.NoncurrentVersionExpiration) { - // 'NoncurrentDays' for NoncurrentVersionExpiration action is a positive integer - if (r.NoncurrentVersionExpiration.NoncurrentDays) { + // 'NoncurrentDays' for NoncurrentVersionExpiration action is a nonnegative integer + if (r.NoncurrentVersionExpiration.NoncurrentDays !== undefined) { days = this._decrementExpirationDay(r.NoncurrentVersionExpiration.NoncurrentDays); } } @@ -246,7 +245,7 @@ class RulesReducer { // NOTE: When you specify the Days tag, Amazon S3 automatically performs ExpiredObjectDeleteMarker // cleanup when the delete markers are old enough to satisfy the age criteria. if (r.Expiration) { - if (r.Expiration.Days) { + if (r.Expiration.Days !== undefined) { days = this._decrementExpirationDay(r.Expiration.Days); } else if (r.Expiration.Date) { if (r.Expiration.Date <= this._currentDate) { diff --git a/tests/unit/lifecycle/RulesReducer.spec.js b/tests/unit/lifecycle/RulesReducer.spec.js index 0765fb5349..49d61d4d26 100644 --- a/tests/unit/lifecycle/RulesReducer.spec.js +++ b/tests/unit/lifecycle/RulesReducer.spec.js @@ -143,6 +143,29 @@ describe('RulesReducer with versioning Disabled', () => { assert.deepStrictEqual(result, expected); }); + it('with Expiration rule using Days set to 0', () => { + const currentDate = Date.now(); + const prefix = ''; + const bucketLCRules = [ + { + Expiration: { Days: 0 }, + ID: '123', + Prefix: prefix, + Status: 'Enabled', + } + ]; + const expected = { + currents: [{ + prefix: '', + days: 0, + }] + }; + const rulesReducer = new RulesReducer(versioningStatus, currentDate, bucketLCRules, options); + const result = rulesReducer.toListings(); + + assert.deepStrictEqual(result, expected); + }); + it('with Expiration rule using Days and prefix', () => { const currentDate = Date.now(); const prefix = 'pre'; diff --git a/tests/unit/lifecycle/RulesReducerWithVersion.spec.js b/tests/unit/lifecycle/RulesReducerWithVersion.spec.js index f7d7f5fa5b..b4af027951 100644 --- a/tests/unit/lifecycle/RulesReducerWithVersion.spec.js +++ b/tests/unit/lifecycle/RulesReducerWithVersion.spec.js @@ -121,6 +121,28 @@ describe('RulesReducer with versioning Enabled', () => { assert.deepStrictEqual(result, expected); }); + it('with Expiration rule using Days set to 0', () => { + const currentDate = Date.now(); + const prefix = ''; + const bucketLCRules = [ + { + Expiration: { Days: 0 }, + ID: '123', + Prefix: prefix, + Status: 'Enabled', + } + ]; + const expected = { + currents: [{ prefix: '', days: 0 }], + nonCurrents: [], + orphans: [{ prefix: '', days: 0 }], + }; + const rulesReducer = new RulesReducer(versioningStatus, currentDate, bucketLCRules, options); + const result = rulesReducer.toListings(); + + assert.deepStrictEqual(result, expected); + }); + it('with Expiration rule using Days and prefix', () => { const currentDate = Date.now(); const prefix = 'pre'; @@ -460,6 +482,30 @@ describe('RulesReducer with versioning Enabled', () => { assert.deepStrictEqual(result, expected); }); + it('with NoncurrentVersionExpiration rule using NoncurrentDays set to 0', () => { + const currentDate = Date.now(); + const bucketLCRules = [ + { + NoncurrentVersionExpiration: { NoncurrentDays: 0 }, + ID: '0', + Prefix: '', + Status: 'Enabled', + }, + ]; + const rulesReducer = new RulesReducer(versioningStatus, currentDate, bucketLCRules, options); + const result = rulesReducer.toListings(); + + const expected = { + currents: [], + nonCurrents: [{ + prefix: '', + days: 0, + }], + orphans: [], + }; + assert.deepStrictEqual(result, expected); + }); + it('with NoncurrentVersionTransitions rule', () => { const currentDate = Date.now(); const bucketLCRules = [ diff --git a/tests/unit/lifecycle/rules.spec.js b/tests/unit/lifecycle/rules.spec.js index d31afd9a74..9b9ac31a76 100644 --- a/tests/unit/lifecycle/rules.spec.js +++ b/tests/unit/lifecycle/rules.spec.js @@ -318,6 +318,31 @@ describe('rulesToParams with versioning Disabled', () => { assert.deepStrictEqual(result, expected); }); + it('with Expiration rule using Days set to 0 (no BeforeDate, lists all)', () => { + const currentDate = Date.now(); + const prefix = ''; + const bucketLCRules = [ + { + Expiration: { Days: 0 }, + ID: '123', + Prefix: prefix, + Status: 'Enabled', + } + ]; + const expected = { + params: { + Bucket: bucketName, + Prefix: prefix, + MaxKeys: MAX_KEYS, + }, + listType: 'current', + remainings: [] + }; + + const result = rulesToParams(versioningStatus, currentDate, bucketLCRules, bucketData, options); + assert.deepStrictEqual(result, expected); + }); + it('with Expiration rule using Days and prefix', () => { const currentDate = Date.now(); const prefix = 'pre'; diff --git a/tests/unit/lifecycle/rulesWithVersion.spec.js b/tests/unit/lifecycle/rulesWithVersion.spec.js index c104c22525..537ea53d8d 100644 --- a/tests/unit/lifecycle/rulesWithVersion.spec.js +++ b/tests/unit/lifecycle/rulesWithVersion.spec.js @@ -262,6 +262,33 @@ describe('rulesToParams with versioning Enabled', () => { assert.deepStrictEqual(result, expected); }); + it('with Expiration rule using Days set to 0 (no BeforeDate, lists all)', () => { + const currentDate = Date.now(); + const bucketLCRules = [ + { + Expiration: { Days: 0 }, + ID: '123', + Prefix: '', + Status: 'Enabled', + } + ]; + const expected = { + params: { + Bucket: bucketName, + Prefix: '', + MaxKeys: MAX_KEYS, + }, + listType: 'current', + remainings: [{ + listType: 'orphan', + prefix: '', + }] + }; + + const result = rulesToParams(versioningStatus, currentDate, bucketLCRules, bucketData, options); + assert.deepStrictEqual(result, expected); + }); + it('with Expiration rule using Days', () => { const currentDate = Date.now(); const prefix = ''; @@ -731,6 +758,31 @@ describe('rulesToParams with versioning Enabled', () => { assert.deepStrictEqual(result, expected); }); + it('with NoncurrentVersionExpiration rule using NoncurrentDays set to 0 (no BeforeDate)', () => { + const currentDate = Date.now(); + const bucketLCRules = [ + { + NoncurrentVersionExpiration: { NoncurrentDays: 0 }, + ID: '123', + Prefix: '', + Status: 'Enabled', + } + ]; + + const expected = { + params: { + Bucket: bucketName, + Prefix: '', + MaxKeys: MAX_KEYS, + }, + listType: 'noncurrent', + remainings: [] + }; + + const result = rulesToParams(versioningStatus, currentDate, bucketLCRules, bucketData, options); + assert.deepStrictEqual(result, expected); + }); + it('with NoncurrentVersionExpiration and NoncurrentVersionTransitions rules with different prefix', () => { const currentDate = Date.now(); const expectedBeforeDate = (new Date(currentDate - ONE_DAY_IN_SEC)).toISOString();