@@ -671,20 +671,53 @@ function set_amz_headers(req, res) {
671
671
*
672
672
* @param {Object } req
673
673
* @param {http.ServerResponse } res
674
+ * @param {Object } object_md
674
675
*/
675
- async function set_expiration_header ( req , res ) {
676
- const rules = req . params . bucket && await req . object_sdk . read_bucket_lifecycle_config_info ( req . params . bucket ) ;
677
- const object_md = {
678
- bucket : req . params . bucket ,
679
- key : req . params . key ,
680
- size : req . headers [ 'x-amz-decoded-content-length' ] || req . headers [ 'content-length' ] ? parse_content_length ( req , {
681
- ErrorClass : S3Error ,
682
- error_missing_content_length : S3Error . MissingContentLength
683
- } ) : undefined ,
684
- tagging : req . body && req . body . Tagging ? s3_utils . parse_body_tagging_xml ( req ) : undefined ,
685
- } ;
676
+ async function set_expiration_header ( req , res , object_md ) {
677
+ const rules = req . params . bucket && await req . object_sdk . get_bucket_lifecycle_configuration_rules ( { name : req . params . bucket } ) ;
678
+ if ( ! object_md ) { // calculating object_md for putObject
679
+ object_md = {
680
+ bucket : req . params . bucket ,
681
+ key : req . params . key ,
682
+ create_time : new Date ( Date . UTC ( new Date ( ) . getUTCFullYear ( ) , new Date ( ) . getUTCMonth ( ) , new Date ( ) . getUTCDate ( ) ) ) . getTime ( ) ,
683
+ size : req . headers [ 'x-amz-decoded-content-length' ] || req . headers [ 'content-length' ] ? parse_content_length ( req , {
684
+ ErrorClass : S3Error ,
685
+ error_missing_content_length : S3Error . MissingContentLength
686
+ } ) : undefined ,
687
+ tagging : req . body && req . body . Tagging ? s3_utils . parse_body_tagging_xml ( req ) : undefined ,
688
+ } ;
689
+ }
686
690
687
- if ( object_md . key && rules ?. length > 0 ) {
691
+ const matched_rule = get_lifecycle_rule_for_object ( rules , object_md ) ;
692
+ if ( matched_rule ) {
693
+ const expiration_header = parse_expiration_header ( matched_rule , object_md . create_time ) ;
694
+ if ( expiration_header ) {
695
+ dbg . log1 ( 'set x_amz_expiration header from applied rule: ' , matched_rule ) ;
696
+ res . setHeader ( 'x-amz-expiration' , expiration_header ) ;
697
+ }
698
+ }
699
+ }
700
+
701
+ /**
702
+ * get_lifecycle_rule_for_object determines the most specific matching lifecycle rule for the given object metadata
703
+ *
704
+ * priority is based on:
705
+ * - longest matching prefix
706
+ * - most matching tags
707
+ * - narrowest object size range
708
+ *
709
+ * @param {Array<Object> } rules
710
+ * @param {Object } object_md
711
+ * @returns {Object|undefined }
712
+ */
713
+ function get_lifecycle_rule_for_object ( rules , object_md ) {
714
+ let matched_rule ;
715
+ let rule_priority = {
716
+ prefix_len : - 1 ,
717
+ tag_count : - 1 ,
718
+ size_span : Infinity ,
719
+ } ;
720
+ if ( object_md ?. key && rules ?. length > 0 ) {
688
721
for ( const rule of rules ) {
689
722
if ( rule ?. status !== 'Enabled' ) continue ;
690
723
@@ -705,37 +738,53 @@ async function set_expiration_header(req, res) {
705
738
if ( ! matches_all_tags ) continue ;
706
739
}
707
740
708
- const expiration_header = parse_expiration_header ( rule ?. expiration , rule ?. id ) ;
709
- if ( expiration_header ) {
710
- dbg . log1 ( 'set x_amz_expiration header from applied rule: ' , rule ) ;
711
- res . setHeader ( 'x-amz-expiration' , expiration_header ) ;
712
- break ; // apply only for first matching rule
741
+ const priority = {
742
+ prefix_len : ( filter ?. prefix || '' ) . length ,
743
+ tag_count : Array . isArray ( filter ?. tagging ) ? filter ?. tagging . length : 0 ,
744
+ size_span : ( filter ?. object_size_less_than ?? Infinity ) - ( filter ?. object_size_greater_than ?? 0 )
745
+ } ;
746
+
747
+ // compare prefix length
748
+ const is_more_specific_prefix = priority . prefix_len > rule_priority . prefix_len ;
749
+
750
+ // compare tag count (if prefixes are equal)
751
+ const is_more_specific_tags = priority . prefix_len === rule_priority . prefix_len &&
752
+ priority . tag_count > rule_priority . tag_count ;
753
+
754
+ // compare size span (if prefixes and tags are equal)
755
+ const is_more_specific_size = priority . prefix_len === rule_priority . prefix_len &&
756
+ priority . tag_count === rule_priority . tag_count &&
757
+ priority . size_span < rule_priority . size_span ;
758
+
759
+ if ( is_more_specific_prefix || is_more_specific_tags || is_more_specific_size ) {
760
+ matched_rule = rule ;
761
+ rule_priority = priority ;
713
762
}
714
763
}
715
764
}
765
+ return matched_rule ;
716
766
}
717
767
718
768
/**
719
769
* parse_expiration_header converts an expiration rule (either with `date` or `days`)
720
770
* into an s3 style `x-amz-expiration` header value
721
771
*
722
- * @param {Object } expiration - expiration object from lifecycle config
723
- * @param {string } rule_id - id of the lifecycle rule
772
+ * @param {Object } rule
773
+ * @param {Object } create_time
724
774
* @returns {string|undefined }
725
775
*
726
776
* Example output:
727
777
* expiry-date="Thu, 10 Apr 2025 00:00:00 GMT", rule-id="rule_id"
728
778
*/
729
- function parse_expiration_header ( expiration , rule_id ) {
779
+ function parse_expiration_header ( rule , create_time ) {
780
+ const expiration = rule . expiration ;
781
+ const rule_id = rule . rule_id ;
782
+
730
783
if ( ! expiration || ( ! expiration . date && ! expiration . days ) ) return undefined ;
731
784
732
785
const expiration_date = expiration . date ?
733
786
new Date ( expiration . date ) :
734
- new Date ( Date . UTC (
735
- new Date ( ) . getUTCFullYear ( ) ,
736
- new Date ( ) . getUTCMonth ( ) ,
737
- new Date ( ) . getUTCDate ( ) + expiration . days
738
- ) ) ;
787
+ new Date ( create_time + expiration . days * 24 * 60 * 60 * 1000 ) ;
739
788
740
789
return `expiry-date="${ expiration_date . toUTCString ( ) } ", rule-id="${ rule_id } "` ;
741
790
}
0 commit comments