Skip to content

Commit 6705021

Browse files
committed
Support the new API GetObjectAttributes
Issue: CLDSRV-817
1 parent 70af4b6 commit 6705021

File tree

10 files changed

+1475
-4
lines changed

10 files changed

+1475
-4
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+
// Metadata allowed to be returned by getObjectAttributes API
283+
allowedObjectAttributes: 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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const { errorInstances } = require('arsenal');
2+
const { allowedObjectAttributes } = require('../../../../constants');
3+
4+
/**
5+
* parseAttributesHeaders - Parse and validate the x-amz-object-attributes header
6+
* @param {object} headers - request headers
7+
* @returns {string[]} - array of valid attribute names
8+
* @throws {Error} - InvalidRequest if header is missing/empty, InvalidArgument if attribute is invalid
9+
*/
10+
function parseAttributesHeaders(headers) {
11+
const raw = headers['x-amz-object-attributes'] || '';
12+
13+
const attributes = raw
14+
.split(',')
15+
.map(s => s.trim())
16+
.filter(s => s !== '');
17+
18+
if (attributes.length === 0) {
19+
throw errorInstances.InvalidRequest.customizeDescription(
20+
'The x-amz-object-attributes header specifying the attributes to be retrieved is either missing or empty',
21+
);
22+
}
23+
24+
const invalids = attributes.filter(s => !allowedObjectAttributes.has(s));
25+
if (invalids.length > 0) {
26+
throw errorInstances.InvalidArgument.customizeDescription('Invalid attribute name specified.');
27+
}
28+
29+
return attributes;
30+
}
31+
32+
module.exports = parseAttributesHeaders;

lib/api/objectGetAttributes.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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 OBJECT_GET_ATTRIBUTES = 'objectGetAttributes';
13+
14+
const checkExpectedBucketOwnerPromise = promisify(checkExpectedBucketOwner);
15+
16+
/**
17+
* validateBucketAndObjPromise - Promisified wrapper for standardMetadataValidateBucketAndObj
18+
* @param {object} params - validation parameters
19+
* @param {boolean} actionImplicitDenies - whether action has implicit denies
20+
* @param {object} log - Werelogs logger
21+
* @returns {Promise<{bucket: BucketInfo, objMD: object}>} - bucket and object metadata
22+
* @throws {Error} - rejects with error from standardMetadataValidateBucketAndObj
23+
*/
24+
function validateBucketAndObjPromise(params, actionImplicitDenies, log) {
25+
return new Promise((resolve, reject) => {
26+
standardMetadataValidateBucketAndObj(params, actionImplicitDenies, log, (err, bucket, objMD) => {
27+
if (err) {
28+
return reject(err);
29+
}
30+
return resolve({ bucket, objMD });
31+
});
32+
});
33+
}
34+
35+
/**
36+
* buildXmlResponse - Build XML response for GetObjectAttributes
37+
* @param {object} objMD - object metadata
38+
* @param {array} attributes - requested attributes
39+
* @returns {string} XML response
40+
*/
41+
function buildXmlResponse(objMD, attributes) {
42+
const attrResp = {};
43+
44+
if (attributes.includes('ETag')) {
45+
attrResp.ETag = objMD['content-md5'];
46+
}
47+
48+
// NOTE: Checksum is not implemented
49+
if (attributes.includes('Checksum')) {
50+
attrResp.Checksum = {};
51+
}
52+
53+
if (attributes.includes('ObjectParts')) {
54+
const partCount = getPartCountFromMd5(objMD);
55+
if (partCount) {
56+
attrResp.ObjectParts = { PartsCount: partCount };
57+
}
58+
}
59+
60+
if (attributes.includes('StorageClass')) {
61+
attrResp.StorageClass = objMD['x-amz-storage-class'];
62+
}
63+
64+
if (attributes.includes('ObjectSize')) {
65+
attrResp.ObjectSize = objMD['content-length'];
66+
}
67+
68+
const builder = new xml2js.Builder();
69+
return builder.buildObject({ GetObjectAttributesResponse: attrResp });
70+
}
71+
72+
/**
73+
* objectGetAttributes - Retrieves all metadata from an object without returning the object itself
74+
* @param {AuthInfo} authInfo - Instance of AuthInfo class with requester's info
75+
* @param {object} request - http request object
76+
* @param {object} log - Werelogs logger
77+
* @param {function} callback - callback to server
78+
* @return {undefined}
79+
*/
80+
async function objectGetAttributes(authInfo, request, log, callback) {
81+
log.trace('processing request', { method: OBJECT_GET_ATTRIBUTES });
82+
const { bucketName, objectKey, headers, actionImplicitDenies } = request;
83+
84+
let responseHeaders = {};
85+
86+
const versionId = decodeVersionId(request.query);
87+
if (versionId instanceof Error) {
88+
log.debug('invalid versionId query', { versionId: request.query.versionId, error: versionId });
89+
throw versionId;
90+
}
91+
92+
const metadataValParams = {
93+
authInfo,
94+
bucketName,
95+
objectKey,
96+
versionId,
97+
getDeleteMarker: true,
98+
requestType: request.apiMethods || OBJECT_GET_ATTRIBUTES,
99+
request,
100+
};
101+
102+
try {
103+
const { bucket, objMD } = await validateBucketAndObjPromise(metadataValParams, actionImplicitDenies, log);
104+
await checkExpectedBucketOwnerPromise(headers, bucket, log);
105+
106+
responseHeaders = collectCorsHeaders(headers.origin, request.method, bucket);
107+
if (objMD) {
108+
responseHeaders['x-amz-version-id'] = getVersionIdResHeader(bucket.getVersioningConfiguration(), objMD);
109+
responseHeaders['Last-Modified'] = objMD['last-modified'] && new Date(objMD['last-modified']).toUTCString();
110+
}
111+
112+
if (!objMD) {
113+
const err = versionId ? errors.NoSuchVersion : errors.NoSuchKey;
114+
log.debug('object not found', { bucket: bucketName, key: objectKey, versionId });
115+
throw err;
116+
}
117+
118+
if (objMD.isDeleteMarker) {
119+
log.debug('attempt to get attributes of a delete marker', { bucket: bucketName, key: objectKey, versionId });
120+
responseHeaders['x-amz-delete-marker'] = true;
121+
throw errors.MethodNotAllowed;
122+
}
123+
124+
const attributes = parseAttributesHeaders(headers);
125+
126+
pushMetric(OBJECT_GET_ATTRIBUTES, log, {
127+
authInfo,
128+
bucket: bucketName,
129+
keys: [objectKey],
130+
versionId: objMD?.versionId,
131+
location: objMD?.dataStoreName,
132+
});
133+
134+
const xml = buildXmlResponse(objMD, attributes);
135+
return callback(null, xml, responseHeaders);
136+
} catch (err) {
137+
log.debug('error processing request', {
138+
error: err,
139+
method: OBJECT_GET_ATTRIBUTES,
140+
});
141+
142+
return callback(err, null, responseHeaders);
143+
}
144+
}
145+
146+
module.exports = objectGetAttributes;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"dependencies": {
2222
"@azure/storage-blob": "^12.28.0",
2323
"@hapi/joi": "^17.1.1",
24-
"arsenal": "git+https://github.com/scality/Arsenal#8.2.43",
24+
"arsenal": "git+https://github.com/scality/Arsenal#feature/ARSN-549/get-object-attributes",
2525
"async": "2.6.4",
2626
"aws-sdk": "^2.1692.0",
2727
"bucketclient": "scality/bucketclient#8.2.7",

0 commit comments

Comments
 (0)