-
Notifications
You must be signed in to change notification settings - Fork 255
Return user metadata in GetObjectAttributes API #6070
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/CLDSRV-817/get-object-attributes
Are you sure you want to change the base?
Changes from all commits
b818779
0dc49e8
e2a4a81
1a3d966
45e8e2f
e040894
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,21 +1,35 @@ | ||||||||||||||||||||
| const { errorInstances } = require('arsenal'); | ||||||||||||||||||||
| const { supportedGetObjectAttributes } = require('../../../../constants'); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * parseAttributesHeaders - Parse and validate the x-amz-object-attributes header | ||||||||||||||||||||
| * @param {object} headers - request headers | ||||||||||||||||||||
| * @returns {string[]} - array of valid attribute names | ||||||||||||||||||||
| * @throws {Error} - InvalidRequest if header is missing/empty, InvalidArgument if attribute is invalid | ||||||||||||||||||||
| * Parse and validate attribute headers from a request. | ||||||||||||||||||||
maeldonn marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||
| * @param {object} headers - Request headers object | ||||||||||||||||||||
| * @param {string} headerName - Name of the header to parse (e.g., 'x-amz-object-attributes') | ||||||||||||||||||||
| * @param {Set<string>} supportedAttributes - Set of valid attribute names | ||||||||||||||||||||
| * @param {boolean} [isRequired=false] - If true, throws when header is missing/empty | ||||||||||||||||||||
| * @returns {string[]} Array of validated attribute names | ||||||||||||||||||||
| * @throws {arsenal.errors.InvalidRequest} When header is required but missing/empty | ||||||||||||||||||||
| * @throws {arsenal.errors.InvalidArgument} When an invalid attribute name is specified | ||||||||||||||||||||
| * @example | ||||||||||||||||||||
| * // Input headers: | ||||||||||||||||||||
| * { 'headerName': 'ETag, ObjectSize, x-amz-meta-custom' } | ||||||||||||||||||||
| * | ||||||||||||||||||||
| * // Parsed result: | ||||||||||||||||||||
| * ['ETag', 'ObjectSize', 'x-amz-meta-custom'] | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| function parseAttributesHeaders(headers) { | ||||||||||||||||||||
| const attributes = headers['x-amz-object-attributes']?.split(',').map(attr => attr.trim()) ?? []; | ||||||||||||||||||||
| if (attributes.length === 0) { | ||||||||||||||||||||
| function parseAttributesHeaders(headers, headerName, supportedAttributes, isRequired = false) { | ||||||||||||||||||||
| const attributes = | ||||||||||||||||||||
| headers[headerName] | ||||||||||||||||||||
| ?.split(',') | ||||||||||||||||||||
| .map(attr => attr.trim()) | ||||||||||||||||||||
| .map(attr => (supportedAttributes.has(attr) ? attr : attr.toLowerCase())) ?? []; | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wait so it means even if the attribute is not in supportedAttribute, it will be returned as lower cased ? I mean, ok I guess the overall function works because later there is the attributes.some() and has() thing, but could it be clearer
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Anyways, maybe the funciton is good enough as it is, but if you're able to make it a bit clearer it's nice, cause it's doing multiple thing that aren't completely obvious from the function name, it looks like it's gonna do some generic parsing, then ends up doing lowercasing and having a special case for x-amz-meta. Maybe renaming function name and intermediates variables help
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it's not a supported attribute:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seems uselessly innefficient: we traverse the list 3 times to do the same thing...
Suggested change
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| if (isRequired && attributes.length === 0) { | ||||||||||||||||||||
| throw errorInstances.InvalidRequest.customizeDescription( | ||||||||||||||||||||
| 'The x-amz-object-attributes header specifying the attributes to be retrieved is either missing or empty', | ||||||||||||||||||||
| `The ${headerName} header specifying the attributes to be retrieved is either missing or empty`, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (attributes.some(attr => !supportedGetObjectAttributes.has(attr))) { | ||||||||||||||||||||
| if (attributes.some(attr => !attr.startsWith('x-amz-meta-') && !supportedAttributes.has(attr))) { | ||||||||||||||||||||
| throw errorInstances.InvalidArgument.customizeDescription('Invalid attribute name specified.'); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ const versionIdUtils = versioning.VersionID; | |
| const monitoring = require('../utilities/monitoringHandler'); | ||
| const { generateToken, decryptToken } | ||
| = require('../api/apiUtils/object/continueToken'); | ||
| const parseAttributesHeaders = require('./apiUtils/object/parseAttributesHeader'); | ||
|
|
||
| // do not url encode the continuation tokens | ||
| const skipUrlEncoding = new Set([ | ||
|
|
@@ -332,16 +333,12 @@ function bucketGet(authInfo, request, log, callback) { | |
| const bucketName = request.bucketName; | ||
| const v2 = params['list-type']; | ||
|
|
||
| const optionalAttributes = | ||
| request.headers['x-amz-optional-object-attributes'] | ||
| ?.split(',') | ||
| .map(attr => attr.trim()) | ||
| .map(attr => attr !== 'RestoreStatus' ? attr.toLowerCase() : attr) | ||
| ?? []; | ||
| if (optionalAttributes.some(attr => !attr.startsWith('x-amz-meta-') && attr != 'RestoreStatus')) { | ||
| return callback( | ||
| errorInstances.InvalidArgument.customizeDescription('Invalid attribute name specified') | ||
| ); | ||
| let optionalAttributes; | ||
| try { | ||
| const headerName = 'x-amz-optional-object-attributes'; | ||
| optionalAttributes = parseAttributesHeaders(request.headers, headerName, new Set(['RestoreStatus'])); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| } catch (err) { | ||
| return callback(err); | ||
| } | ||
|
|
||
| if (v2 !== undefined && Number.parseInt(v2, 10) !== 2) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,7 @@ const { decodeVersionId, getVersionIdResHeader } = require('./apiUtils/object/ve | |||||||||||
| const { checkExpectedBucketOwner } = require('./apiUtils/authorization/bucketOwner'); | ||||||||||||
| const { pushMetric } = require('../utapi/utilities'); | ||||||||||||
| const { getPartCountFromMd5 } = require('./apiUtils/object/partInfo'); | ||||||||||||
| const { supportedGetObjectAttributes } = require('../../constants'); | ||||||||||||
|
|
||||||||||||
| const OBJECT_GET_ATTRIBUTES = 'objectGetAttributes'; | ||||||||||||
|
|
||||||||||||
|
|
@@ -47,10 +48,37 @@ function buildXmlResponse(objMD, attributes) { | |||||||||||
| attrResp.ObjectSize = objMD['content-length']; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| if (attributes.some(attr => attr.startsWith('x-amz-meta-'))) { | ||||||||||||
| const userMetadata = extractUserMetadata(objMD, attributes); | ||||||||||||
| Object.assign(attrResp, userMetadata); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| const builder = new xml2js.Builder(); | ||||||||||||
| return builder.buildObject({ GetObjectAttributesResponse: attrResp }); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * extractUserMetadata - Extract requested user metadata from object metadata | ||||||||||||
| * @param {object} objMD - object metadata | ||||||||||||
| * @param {string[]} attributes - requested attributes | ||||||||||||
| * @returns {object} - object containing requested user metadata key-value pairs | ||||||||||||
| */ | ||||||||||||
| function extractUserMetadata(objMD, attributes) { | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there is already this logic in ListObjectv2, must be factorized... |
||||||||||||
| const result = {}; | ||||||||||||
|
|
||||||||||||
| const isWildcard = attributes.includes('x-amz-meta-*'); | ||||||||||||
| const sourceKeys = isWildcard ? Object.keys(objMD) : attributes; | ||||||||||||
|
|
||||||||||||
| for (const key of sourceKeys) { | ||||||||||||
| const isValidKey = isWildcard ? key.startsWith('x-amz-meta-') : true; | ||||||||||||
| if (isValidKey && objMD[key]) { | ||||||||||||
| result[key] = objMD[key]; | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| return result; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * objectGetAttributes - Retrieves all metadata from an object without returning the object itself | ||||||||||||
| * @param {AuthInfo} authInfo - Instance of AuthInfo class with requester's info | ||||||||||||
|
|
@@ -137,7 +165,7 @@ async function objectGetAttributes(authInfo, request, log, callback) { | |||||||||||
| throw err; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| const attributes = parseAttributesHeaders(headers); | ||||||||||||
| const attributes = parseAttributesHeaders(headers, 'x-amz-object-attributes', supportedGetObjectAttributes, true); | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this
Suggested change
|
||||||||||||
|
|
||||||||||||
| pushMetric(OBJECT_GET_ATTRIBUTES, log, { | ||||||||||||
| authInfo, | ||||||||||||
|
|
||||||||||||
Uh oh!
There was an error while loading. Please reload this page.