Skip to content

Commit fd17bad

Browse files
committed
Support the new API GetObjectAttributes
Issue: CLDSRV-817
1 parent 6e679a7 commit fd17bad

File tree

8 files changed

+1296
-0
lines changed

8 files changed

+1296
-0
lines changed

constants.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,14 @@ const constants = {
279279
rateLimitDefaultConfigCacheTTL: 30000, // 30 seconds
280280
rateLimitDefaultBurstCapacity: 1,
281281
rateLimitCleanupInterval: 10000, // 10 seconds
282+
// Supported attributes for the GetObjectAttributes 'x-amz-optional-attributes' header.
283+
supportedGetObjectAttributes: new Set([
284+
'StorageClass',
285+
'ObjectSize',
286+
'ObjectParts',
287+
'Checksum',
288+
'ETag',
289+
]),
282290
};
283291

284292
module.exports = constants;

lib/api/api.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const { objectDelete } = require('./objectDelete');
5656
const objectDeleteTagging = require('./objectDeleteTagging');
5757
const objectGet = require('./objectGet');
5858
const objectGetACL = require('./objectGetACL');
59+
const objectGetAttributes = require('./objectGetAttributes.js');
5960
const objectGetLegalHold = require('./objectGetLegalHold');
6061
const objectGetRetention = require('./objectGetRetention');
6162
const objectGetTagging = require('./objectGetTagging');
@@ -471,6 +472,7 @@ const api = {
471472
objectDeleteTagging,
472473
objectGet,
473474
objectGetACL,
475+
objectGetAttributes,
474476
objectGetLegalHold,
475477
objectGetRetention,
476478
objectGetTagging,
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const { errorInstances } = require('arsenal');
2+
const { supportedGetObjectAttributes } = require('../../../../constants');
3+
4+
/**
5+
* parseAttributesHeaders - Parse and validate the x-amz-object-attributes header
6+
* @param {object} headers - request headers
7+
* @returns {Set<string>} - set of requested attribute names
8+
* @throws {Error} - InvalidRequest if header is missing/empty, InvalidArgument if attribute is invalid
9+
*/
10+
function parseAttributesHeaders(headers) {
11+
const attributes = headers['x-amz-object-attributes']?.split(',').map(attr => attr.trim()) ?? [];
12+
if (attributes.length === 0) {
13+
throw errorInstances.InvalidRequest.customizeDescription(
14+
'The x-amz-object-attributes header specifying the attributes to be retrieved is either missing or empty',
15+
);
16+
}
17+
18+
if (attributes.some(attr => !supportedGetObjectAttributes.has(attr))) {
19+
throw errorInstances.InvalidArgument.customizeDescription('Invalid attribute name specified.');
20+
}
21+
22+
return new Set(attributes);
23+
}
24+
25+
module.exports = parseAttributesHeaders;

lib/api/objectGetAttributes.js

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
const { promisify } = require('util');
2+
const { errors } = require('arsenal');
3+
const { standardMetadataValidateBucketAndObj } = require('../metadata/metadataUtils');
4+
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
5+
const parseAttributesHeaders = require('./apiUtils/object/parseAttributesHeader');
6+
const { decodeVersionId, getVersionIdResHeader } = require('./apiUtils/object/versioning');
7+
const { checkExpectedBucketOwner } = require('./apiUtils/authorization/bucketOwner');
8+
const { pushMetric } = require('../utapi/utilities');
9+
const { getPartCountFromMd5 } = require('./apiUtils/object/partInfo');
10+
11+
const checkExpectedBucketOwnerPromise = promisify(checkExpectedBucketOwner);
12+
const validateBucketAndObj = promisify(standardMetadataValidateBucketAndObj);
13+
14+
const OBJECT_GET_ATTRIBUTES = 'objectGetAttributes';
15+
16+
/**
17+
* buildXmlResponse - Build XML response for GetObjectAttributes
18+
* @param {object} objMD - object metadata
19+
* @param {Set<string>} requestedAttrs - set of requested attribute names
20+
* @returns {string} XML response
21+
*/
22+
function buildXmlResponse(objMD, requestedAttrs) {
23+
const xml = [];
24+
xml.push(
25+
'<?xml version="1.0" encoding="UTF-8"?>',
26+
'<GetObjectAttributesResponse>',
27+
);
28+
29+
for (const attribute of requestedAttrs) {
30+
switch (attribute) {
31+
case 'ETag':
32+
xml.push(`<ETag>${objMD['content-md5']}</ETag>`);
33+
break;
34+
case 'ObjectParts': {
35+
const partCount = getPartCountFromMd5(objMD);
36+
if (partCount) {
37+
xml.push(
38+
'<ObjectParts>',
39+
`<PartsCount>${partCount}</PartsCount>`,
40+
'</ObjectParts>',
41+
);
42+
}
43+
break;
44+
}
45+
case 'StorageClass':
46+
xml.push(`<StorageClass>${objMD['x-amz-storage-class']}</StorageClass>`);
47+
break;
48+
case 'ObjectSize':
49+
xml.push(`<ObjectSize>${objMD['content-length']}</ObjectSize>`);
50+
break;
51+
}
52+
}
53+
54+
xml.push('</GetObjectAttributesResponse>');
55+
return xml.join('');
56+
}
57+
58+
/**
59+
* objectGetAttributes - Retrieves all metadata from an object without returning the object itself
60+
* @param {AuthInfo} authInfo - Instance of AuthInfo class with requester's info
61+
* @param {object} request - http request object
62+
* @param {object} log - Werelogs logger
63+
* @param {function} callback - callback optional to keep backward compatibility
64+
* @returns {Promise<object>} - { xml, responseHeaders }
65+
* @throws {ArsenalError} NoSuchVersion - if versionId specified but not found
66+
* @throws {ArsenalError} NoSuchKey - if object not found
67+
* @throws {ArsenalError} MethodNotAllowed - if object is a delete marker
68+
*/
69+
async function objectGetAttributes(authInfo, request, log, callback) {
70+
if (callback) {
71+
return objectGetAttributes(authInfo, request, log)
72+
.then(result => callback(null, result.xml, result.responseHeaders))
73+
.catch(err => callback(err, null, err.responseHeaders ?? {}));
74+
}
75+
76+
log.trace('processing request', { method: OBJECT_GET_ATTRIBUTES });
77+
const { bucketName, objectKey, headers, actionImplicitDenies } = request;
78+
79+
const versionId = decodeVersionId(request.query);
80+
if (versionId instanceof Error) {
81+
log.debug('invalid versionId query', {
82+
method: OBJECT_GET_ATTRIBUTES,
83+
versionId: request.query.versionId,
84+
error: versionId,
85+
});
86+
throw versionId;
87+
}
88+
89+
const metadataValParams = {
90+
authInfo,
91+
bucketName,
92+
objectKey,
93+
versionId,
94+
getDeleteMarker: true,
95+
requestType: request.apiMethods || OBJECT_GET_ATTRIBUTES,
96+
request,
97+
};
98+
99+
let bucket, objectMD;
100+
try {
101+
({ bucket, objectMD } = await validateBucketAndObj(metadataValParams, actionImplicitDenies, log));
102+
await checkExpectedBucketOwnerPromise(headers, bucket, log);
103+
} catch (err) {
104+
log.debug('error validating bucket and object', {
105+
method: OBJECT_GET_ATTRIBUTES,
106+
bucket: bucketName,
107+
key: objectKey,
108+
versionId,
109+
error: err,
110+
});
111+
throw err;
112+
}
113+
114+
const responseHeaders = collectCorsHeaders(headers.origin, request.method, bucket);
115+
116+
if (!objectMD) {
117+
log.debug('object not found', {
118+
method: OBJECT_GET_ATTRIBUTES,
119+
bucket: bucketName,
120+
key: objectKey,
121+
versionId,
122+
});
123+
const err = versionId ? errors.NoSuchVersion : errors.NoSuchKey;
124+
err.responseHeaders = responseHeaders;
125+
throw err;
126+
}
127+
128+
responseHeaders['x-amz-version-id'] = getVersionIdResHeader(bucket.getVersioningConfiguration(), objectMD);
129+
responseHeaders['Last-Modified'] = objectMD['last-modified'] && new Date(objectMD['last-modified']).toUTCString();
130+
131+
if (objectMD.isDeleteMarker) {
132+
log.debug('attempt to get attributes of a delete marker', {
133+
method: OBJECT_GET_ATTRIBUTES,
134+
bucket: bucketName,
135+
key: objectKey,
136+
versionId,
137+
});
138+
responseHeaders['x-amz-delete-marker'] = true;
139+
const err = errors.MethodNotAllowed;
140+
err.responseHeaders = responseHeaders;
141+
throw err;
142+
}
143+
144+
const requestedAttrs = parseAttributesHeaders(headers);
145+
146+
if (requestedAttrs.has('Checksum')) {
147+
log.debug('Checksum attribute requested but not implemented', {
148+
method: OBJECT_GET_ATTRIBUTES,
149+
bucket: bucketName,
150+
key: objectKey,
151+
versionId,
152+
});
153+
const err = errors.NotImplemented.customizeDescription('Checksum attribute is not implemented');
154+
err.responseHeaders = responseHeaders;
155+
throw err;
156+
}
157+
158+
pushMetric(OBJECT_GET_ATTRIBUTES, log, {
159+
authInfo,
160+
bucket: bucketName,
161+
keys: [objectKey],
162+
versionId: objectMD?.versionId,
163+
location: objectMD?.dataStoreName,
164+
});
165+
166+
const xml = buildXmlResponse(objectMD, requestedAttrs);
167+
return { xml, responseHeaders };
168+
}
169+
170+
module.exports = objectGetAttributes;

0 commit comments

Comments
 (0)