Skip to content

Commit c6d02f9

Browse files
committed
Parse optional attributes header with utility function
Issue: CLDSRV-844
1 parent d5d7952 commit c6d02f9

8 files changed

Lines changed: 440 additions & 489 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
const { errorInstances } = require('arsenal');
2+
const { getPartCountFromMd5 } = require('./partInfo');
3+
4+
/**
5+
* Parse and validate attribute headers from a request.
6+
* @param {object} headers - Request headers object
7+
* @param {string} headerName - Name of the header to parse (e.g., 'x-amz-object-attributes')
8+
* @param {Set<string>} supportedAttributes - Set of valid attribute names
9+
* @returns {Set<string>} - set of requested attribute names
10+
* @throws {arsenal.errors.InvalidRequest} When header is required but missing/empty
11+
* @throws {arsenal.errors.InvalidArgument} When an invalid attribute name is specified
12+
* @example
13+
* // Input headers:
14+
* { 'headerName': 'ETag, ObjectSize, x-amz-meta-custom' }
15+
*
16+
* // Parsed result:
17+
* ['ETag', 'ObjectSize', 'x-amz-meta-custom']
18+
*/
19+
function parseAttributesHeaders(headers, headerName, supportedAttributes) {
20+
const result = new Set();
21+
22+
const rawValue = headers[headerName];
23+
if (rawValue === null || rawValue === undefined) {
24+
return result;
25+
}
26+
27+
for (const rawAttr of rawValue.split(',')) {
28+
let attr = rawAttr.trim();
29+
30+
if (!supportedAttributes.has(attr)) {
31+
attr = attr.toLowerCase();
32+
}
33+
34+
if (!attr.startsWith('x-amz-meta-') && !supportedAttributes.has(attr)) {
35+
throw errorInstances.InvalidArgument.customizeDescription('Invalid attribute name specified.');
36+
}
37+
38+
result.add(attr);
39+
}
40+
41+
return result;
42+
}
43+
44+
/**
45+
* buildAttributesXml - Builds XML reponse for requested object attributes
46+
* @param {Object} objectMD - The internal metadata object for the file/object.
47+
* @param {string} [objectMD.content-md5] - The MD5 hash used for the ETag.
48+
* @param {string} [objectMD.x-amz-storage-class] - The storage tier of the object.
49+
* @param {number} [objectMD.content-length] - The size of the object in bytes.
50+
* @param {Object} [objectMD.restoreStatus] - Information regarding the restoration of archived objects.
51+
* @param {boolean} [objectMD.restoreStatus.inProgress] - Whether a restore is currently active.
52+
* @param {string} [objectMD.restoreStatus.expiryDate] - The date after which the restored copy expires.
53+
* @param {Object.<string, any>} userMetadata - Key-value pairs of user-defined metadata.
54+
* @param {string[]} requestedAttrs - A list of specific attributes to include in the output.
55+
* Supports 'ETag', 'ObjectParts', 'StorageClass', 'ObjectSize',
56+
* 'RestoreStatus', and 'x-amz-meta-*' for all user metadata.
57+
* @param {string[]} xml - The string array acting as the output buffer/collector.
58+
* @returns {void} - this function does not return a value, it mutates the `xml` param.
59+
*/
60+
function buildAttributesXml(objectMD, userMetadata, requestedAttrs, xml) {
61+
const customAttributes = new Set();
62+
for (const attribute of requestedAttrs) {
63+
switch (attribute) {
64+
case 'ETag':
65+
xml.push(`<ETag>${objectMD['content-md5']}</ETag>`);
66+
break;
67+
case 'ObjectParts': {
68+
const partCount = getPartCountFromMd5(objectMD);
69+
if (partCount) {
70+
xml.push(
71+
'<ObjectParts>',
72+
`<PartsCount>${partCount}</PartsCount>`,
73+
'</ObjectParts>',
74+
);
75+
}
76+
break;
77+
}
78+
case 'StorageClass':
79+
xml.push(`<StorageClass>${objectMD['x-amz-storage-class']}</StorageClass>`);
80+
break;
81+
case 'ObjectSize':
82+
xml.push(`<ObjectSize>${objectMD['content-length']}</ObjectSize>`);
83+
break;
84+
case 'RestoreStatus':
85+
xml.push('<RestoreStatus>');
86+
xml.push(`<IsRestoreInProgress>${!!objectMD.restoreStatus?.inProgress}</IsRestoreInProgress>`);
87+
88+
if (objectMD.restoreStatus?.expiryDate) {
89+
xml.push(`<RestoreExpiryDate>${objectMD.restoreStatus?.expiryDate}</RestoreExpiryDate>`);
90+
}
91+
92+
xml.push('</RestoreStatus>');
93+
break;
94+
case 'x-amz-meta-*':
95+
for (const key of Object.keys(userMetadata)) {
96+
customAttributes.add(key);
97+
}
98+
break;
99+
default:
100+
if (userMetadata[attribute]) {
101+
customAttributes.add(attribute);
102+
}
103+
}
104+
}
105+
106+
for (const key of customAttributes) {
107+
xml.push(`<${key}>${userMetadata[key]}</${key}>`);
108+
}
109+
}
110+
111+
module.exports = {
112+
parseAttributesHeaders,
113+
buildAttributesXml,
114+
};

lib/api/apiUtils/object/parseAttributesHeader.js

Lines changed: 0 additions & 26 deletions
This file was deleted.

lib/api/bucketGet.js

Lines changed: 14 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ const { pushMetric } = require('../utapi/utilities');
1010
const versionIdUtils = versioning.VersionID;
1111
const monitoring = require('../utilities/monitoringHandler');
1212
const { generateToken, decryptToken } = require('../api/apiUtils/object/continueToken');
13+
const { parseAttributesHeaders, buildAttributesXml } = require('./apiUtils/object/objectAttributes');
14+
15+
const OPTIONAL_ATTRIBUTES = new Set(['RestoreStatus']);
1316

1417
const xmlParamsToSkipUrlEncoding = new Set(['ContinuationToken', 'NextContinuationToken']);
1518

@@ -150,7 +153,11 @@ function processVersions(bucketName, listParams, list) {
150153
`<ID>${v.Owner.ID}</ID>`,
151154
`<DisplayName>${v.Owner.DisplayName}</DisplayName>`,
152155
'</Owner>',
153-
...processOptionalAttributes(v, listParams.optionalAttributes),
156+
);
157+
158+
buildAttributesXml(v, v.userMetadata, listParams.optionalAttributes, xml),
159+
160+
xml.push(
154161
`<StorageClass>${v.StorageClass}</StorageClass>`,
155162
v.IsDeleteMarker ? '</DeleteMarker>' : '</Version>'
156163
);
@@ -231,7 +238,7 @@ function processMasterVersions(bucketName, listParams, list) {
231238
);
232239
}
233240

234-
xml.push(...processOptionalAttributes(v, listParams.optionalAttributes));
241+
buildAttributesXml(v, v.userMetadata, listParams.optionalAttributes, xml);
235242

236243
return xml.push(
237244
`<StorageClass>${v.StorageClass}</StorageClass>`,
@@ -246,41 +253,6 @@ function processMasterVersions(bucketName, listParams, list) {
246253
return xml.join('');
247254
}
248255

249-
function processOptionalAttributes(item, optionalAttributes) {
250-
const xml = [];
251-
const userMetadata = new Set();
252-
253-
for (const attribute of optionalAttributes) {
254-
switch (attribute) {
255-
case 'RestoreStatus':
256-
xml.push('<RestoreStatus>');
257-
xml.push(`<IsRestoreInProgress>${!!item.restoreStatus?.inProgress}</IsRestoreInProgress>`);
258-
259-
if (item.restoreStatus?.expiryDate) {
260-
xml.push(`<RestoreExpiryDate>${item.restoreStatus?.expiryDate}</RestoreExpiryDate>`);
261-
}
262-
263-
xml.push('</RestoreStatus>');
264-
break;
265-
case 'x-amz-meta-*':
266-
for (const key of Object.keys(item.userMetadata)) {
267-
userMetadata.add(key);
268-
}
269-
break;
270-
default:
271-
if (item.userMetadata?.[attribute]) {
272-
userMetadata.add(attribute);
273-
}
274-
}
275-
}
276-
277-
for (const key of userMetadata) {
278-
xml.push(`<${key}>${item.userMetadata[key]}</${key}>`);
279-
}
280-
281-
return xml;
282-
}
283-
284256
function handleResult(listParams, requestMaxKeys, encoding, authInfo, bucketName, list, log) {
285257
// eslint-disable-next-line no-param-reassign
286258
listParams.maxKeys = requestMaxKeys;
@@ -321,15 +293,11 @@ async function bucketGet(authInfo, request, log, callback) {
321293
const bucketName = request.bucketName;
322294
const v2 = params['list-type'];
323295

324-
const optionalAttributes =
325-
request.headers['x-amz-optional-object-attributes']
326-
?.split(',')
327-
.map(attr => attr.trim())
328-
.map(attr => attr !== 'RestoreStatus' ? attr.toLowerCase() : attr)
329-
?? [];
330-
if (optionalAttributes.some(attr => !attr.startsWith('x-amz-meta-') && attr != 'RestoreStatus')) {
331-
throw errorInstances.InvalidArgument.customizeDescription('Invalid attribute name specified');
332-
}
296+
const optionalAttributes = parseAttributesHeaders(
297+
request.headers,
298+
'x-amz-optional-object-attributes',
299+
OPTIONAL_ATTRIBUTES,
300+
);
333301

334302
if (v2 !== undefined && Number.parseInt(v2, 10) !== 2) {
335303
throw errorInstances.InvalidArgument.customizeDescription('Invalid List Type specified in Request');

lib/api/metadataSearch.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ function handleResult(listParams, requestMaxKeys, encoding, authInfo,
2020
// eslint-disable-next-line no-param-reassign
2121
listParams.encoding = encoding;
2222
// eslint-disable-next-line no-param-reassign
23-
listParams.optionalAttributes = [];
23+
listParams.optionalAttributes = new Set();
2424
let res;
2525
if (listParams.listingType === 'DelimiterVersions') {
2626
res = processVersions(bucketName, listParams, list);

0 commit comments

Comments
 (0)