Skip to content

Commit caf1848

Browse files
committed
Support the new API GetObjectAttributes
Issue: CLDSRV-817
1 parent 10f7494 commit caf1848

File tree

8 files changed

+1287
-0
lines changed

8 files changed

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

0 commit comments

Comments
 (0)