Skip to content

Commit 6c5a42e

Browse files
achouhan09Aayush
authored and
Aayush
committed
Added x-amz-expiration, missing HTTP header in the response of object GET/PUT/HEAD
Signed-off-by: Aayush Chouhan <[email protected]>
1 parent 12dc377 commit 6c5a42e

12 files changed

+564
-123
lines changed

src/endpoint/s3/ops/s3_get_object.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ async function get_object(req, res) {
4949
}
5050
}
5151
http_utils.set_response_headers_from_request(req, res);
52+
if (!version_id) await http_utils.set_expiration_header(req, res, object_md); // setting expiration header for bucket lifecycle
5253
const obj_size = object_md.size;
5354
const params = {
5455
object_md,

src/endpoint/s3/ops/s3_head_object.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ async function head_object(req, res) {
2929
s3_utils.set_response_object_md(res, object_md);
3030
s3_utils.set_encryption_response_headers(req, res, object_md.encryption);
3131
http_utils.set_response_headers_from_request(req, res);
32+
if (!params.version_id) await http_utils.set_expiration_header(req, res, object_md); // setting expiration header for bucket lifecycle
3233
}
3334

3435
module.exports = {

src/endpoint/s3/ops/s3_put_object.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ async function put_object(req, res) {
8181
}
8282
res.setHeader('ETag', `"${reply.etag}"`);
8383

84+
const object_info = {
85+
key: req.params.key,
86+
create_time: new Date().getTime(),
87+
size: size,
88+
tagging: tagging,
89+
};
90+
await http_utils.set_expiration_header(req, res, object_info); // setting expiration header for bucket lifecycle
91+
8492
if (reply.seq) {
8593
res.seq = reply.seq;
8694
delete reply.seq;

src/manage_nsfs/nc_lifecycle.js

Lines changed: 5 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ class NCLifecycle {
271271

272272
if (candidates.delete_candidates?.length > 0) {
273273
const expiration = lifecycle_rule.expiration ? this._get_expiration_time(lifecycle_rule.expiration) : 0;
274-
const filter_func = this._build_lifecycle_filter({filter: lifecycle_rule.filter, expiration});
274+
const filter_func = lifecycle_utils.build_lifecycle_filter({filter: lifecycle_rule.filter, expiration});
275275
dbg.log0('process_rule: calling delete_multiple_objects, num of objects to be deleted', candidates.delete_candidates.length);
276276
const delete_res = await this._call_op_and_update_status({
277277
bucket_name,
@@ -478,7 +478,7 @@ class NCLifecycle {
478478
if (rule_state.is_finished) return [];
479479
const expiration = this._get_expiration_time(lifecycle_rule.expiration);
480480
if (expiration < 0) return [];
481-
const filter_func = this._build_lifecycle_filter({filter: lifecycle_rule.filter, expiration});
481+
const filter_func = lifecycle_utils.build_lifecycle_filter({filter: lifecycle_rule.filter, expiration});
482482

483483
const filtered_objects = [];
484484
// TODO list_objects does not accept a filter and works in batch sizes of 1000. should handle batching
@@ -537,7 +537,7 @@ class NCLifecycle {
537537
const versions_list = params.versions_list;
538538
const candidates = [];
539539
const expiration = lifecycle_rule.expiration?.days ? this._get_expiration_time(lifecycle_rule.expiration) : 0;
540-
const filter_func = this._build_lifecycle_filter({filter: lifecycle_rule.filter, expiration});
540+
const filter_func = lifecycle_utils.build_lifecycle_filter({filter: lifecycle_rule.filter, expiration});
541541
for (let i = 0; i < versions_list.objects.length - 1; i++) {
542542
if (this.filter_expired_delete_marker(versions_list.objects[i], versions_list.objects[i + 1], filter_func)) {
543543
candidates.push(versions_list.objects[i]);
@@ -640,7 +640,7 @@ class NCLifecycle {
640640
}
641641
const versions_list = params.versions_list;
642642

643-
const filter_func = this._build_lifecycle_filter({filter: lifecycle_rule.filter, expiration: 0});
643+
const filter_func = lifecycle_utils.build_lifecycle_filter({filter: lifecycle_rule.filter, expiration: 0});
644644
const num_newer_versions = lifecycle_rule.noncurrent_version_expiration.newer_noncurrent_versions;
645645
const num_non_current_days = lifecycle_rule.noncurrent_version_expiration.noncurrent_days;
646646
const delete_candidates = [];
@@ -674,7 +674,7 @@ class NCLifecycle {
674674
const expiration = lifecycle_rule.abort_incomplete_multipart_upload.days_after_initiation;
675675
const res = [];
676676

677-
const filter_func = this._build_lifecycle_filter({filter, expiration});
677+
const filter_func = lifecycle_utils.build_lifecycle_filter({filter, expiration});
678678
let dir_handle;
679679
//TODO this is almost identical to list_uploads except for error handling and support for pagination. should modify list-upload and use it in here instead
680680
try {
@@ -720,29 +720,6 @@ class NCLifecycle {
720720
///////// FILTER HELPERS ////////
721721
////////////////////////////////////
722722

723-
/**
724-
* @typedef {{
725-
* filter: Object
726-
* expiration: Number
727-
* }} filter_params
728-
*
729-
* @param {filter_params} params
730-
* @returns
731-
*/
732-
_build_lifecycle_filter(params) {
733-
/**
734-
* @param {Object} object_info
735-
*/
736-
return function(object_info) {
737-
if (params.filter?.prefix && !object_info.key.startsWith(params.filter.prefix)) return false;
738-
if (params.expiration && object_info.age < params.expiration) return false;
739-
if (params.filter?.tags && !_file_contain_tags(object_info, params.filter.tags)) return false;
740-
if (params.filter?.object_size_greater_than && object_info.size < params.filter.object_size_greater_than) return false;
741-
if (params.filter?.object_size_less_than && object_info.size > params.filter.object_size_less_than) return false;
742-
return true;
743-
};
744-
}
745-
746723
/**
747724
* get the expiration time in days of an object
748725
* if rule is set with date, then rule is applied for all objects after that date
@@ -1468,38 +1445,6 @@ class NCLifecycle {
14681445
}
14691446
}
14701447

1471-
//////////////////
1472-
// TAGS HELPERS //
1473-
//////////////////
1474-
1475-
/**
1476-
* checks if tag query_tag is in the list tag_set
1477-
* @param {Object} query_tag
1478-
* @param {Array<Object>} tag_set
1479-
*/
1480-
function _list_contain_tag(query_tag, tag_set) {
1481-
for (const t of tag_set) {
1482-
if (t.key === query_tag.key && t.value === query_tag.value) return true;
1483-
}
1484-
return false;
1485-
}
1486-
1487-
/**
1488-
* checks if object has all the tags in filter_tags
1489-
* @param {Object} object_info
1490-
* @param {Array<Object>} filter_tags
1491-
* @returns
1492-
*/
1493-
function _file_contain_tags(object_info, filter_tags) {
1494-
if (object_info.tags === undefined) return false;
1495-
for (const tag of filter_tags) {
1496-
if (!_list_contain_tag(tag, object_info.tags)) {
1497-
return false;
1498-
}
1499-
}
1500-
return true;
1501-
}
1502-
15031448
// EXPORTS
15041449
exports.NCLifecycle = NCLifecycle;
15051450
exports.ILM_POLICIES_TMP_DIR = ILM_POLICIES_TMP_DIR;

src/test/system_tests/test_utils.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,80 @@ const run_or_skip_test = cond => {
790790
} else return it.skip;
791791
};
792792

793+
/**
794+
* update_file_mtime updates the mtime of the target path
795+
* Warnings:
796+
* - This operation would change the mtime of the file to 5 days ago - which means that it changes the etag / obj_id of the object
797+
* - Please do not use on versioned objects (version_id will not be changed, but the mtime will be changed) - might cause issues.
798+
* @param {String} target_path
799+
* @returns {Promise<Void>}
800+
*/
801+
async function update_file_mtime(target_path) {
802+
const update_file_mtime_cmp = os_utils.IS_MAC ? `touch -t $(date -v -5d +"%Y%m%d%H%M.%S") ${target_path}` : `touch -d "5 days ago" ${target_path}`;
803+
await os_utils.exec(update_file_mtime_cmp, { return_stdout: true });
804+
}
805+
806+
/////////////////////////////////
807+
////// LIFECYCLE UTILS ///////
808+
/////////////////////////////////
809+
810+
/**
811+
* generate_lifecycle_rule generate an S3 lifecycle rule with optional filters and expiration currently (can be extend to support more lifecycle rule params)
812+
*
813+
* @param {number} expiration_days
814+
* @param {string} id
815+
* @param {string} [prefix]
816+
* @param {Array<Object>} [tags]
817+
* @param {number} [size_gt]
818+
* @param {number} [size_lt]
819+
* @returns {Object}
820+
*/
821+
function generate_lifecycle_rule(expiration_days, id, prefix, tags, size_gt, size_lt) {
822+
const filters = {};
823+
if (prefix) filters.Prefix = prefix;
824+
if (Array.isArray(tags) && tags.length) filters.Tags = tags;
825+
if (size_gt !== undefined) filters.ObjectSizeGreaterThan = size_gt;
826+
if (size_lt !== undefined) filters.ObjectSizeLessThan = size_lt;
827+
828+
const filter = Object.keys(filters).length > 1 ? { And: filters } : filters;
829+
830+
return {
831+
ID: id,
832+
Status: 'Enabled',
833+
Filter: filter,
834+
Expiration: { Days: expiration_days },
835+
};
836+
}
837+
838+
/**
839+
* validate_expiration_header validates the `x-amz-expiration` header against the object creation time, expected rule ID and expiration days
840+
*
841+
* The header is expected to have the format:
842+
* expiry-date="YYYY-MM-DDTHH:MM:SS.SSSZ", rule-id="RULE_ID"
843+
*
844+
* @param {string} expiration_header - expiration header value
845+
* @param {string|Date} start_time - start/create time (string or Date) of the object
846+
* @param {string} expected_rule_id - expected rule ID to match in the header
847+
* @param {number} delta_days - expected number of days between start_time and expiry-date
848+
* @returns {boolean} true if the header is valid and matches the expected_rule_id and delta_days otherwise false
849+
*/
850+
function validate_expiration_header(expiration_header, start_time, expected_rule_id, delta_days) {
851+
const match = expiration_header.match(/expiry-date="(.+)", rule-id="(.+)"/);
852+
if (!match) return false;
853+
854+
const [, expiry_str, rule_id] = match;
855+
const expiration = new Date(expiry_str);
856+
const start = new Date(start_time);
857+
start.setUTCHours(0, 0, 0, 0); // adjusting to midnight UTC otherwise the tests will fail - fix for ceph-s3 tests
858+
859+
const days_diff = Math.floor((expiration.getTime() - start.getTime()) / (24 * 60 * 60 * 1000));
860+
861+
return days_diff === delta_days && rule_id === expected_rule_id;
862+
}
863+
864+
exports.update_file_mtime = update_file_mtime;
865+
exports.generate_lifecycle_rule = generate_lifecycle_rule;
866+
exports.validate_expiration_header = validate_expiration_header;
793867
exports.run_or_skip_test = run_or_skip_test;
794868
exports.blocks_exist_on_cloud = blocks_exist_on_cloud;
795869
exports.create_hosts_pool = create_hosts_pool;

0 commit comments

Comments
 (0)