diff --git a/src/internal/type.ts b/src/internal/type.ts index d2f0a193..e2a40ba0 100644 --- a/src/internal/type.ts +++ b/src/internal/type.ts @@ -85,6 +85,10 @@ export interface ItemBucketMetadata { // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any } +export interface ItemBucketTags { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any +} export interface BucketItemFromList { name: string @@ -114,6 +118,7 @@ export type BucketItem = export type BucketItemWithMetadata = BucketItem & { metadata?: ItemBucketMetadata | ItemBucketMetadataList + tags?: ItemBucketTags } export interface BucketStream extends ReadableStream { diff --git a/src/internal/xml-parser.ts b/src/internal/xml-parser.ts index 8b2fc107..d46a2688 100644 --- a/src/internal/xml-parser.ts +++ b/src/internal/xml-parser.ts @@ -14,6 +14,7 @@ import type { CopyObjectResultV1, ObjectLockInfo, ReplicationConfig, + Tags, } from './type.ts' import { RETENTION_VALIDITY_UNITS } from './type.ts' @@ -129,13 +130,24 @@ export function parseListObjectsV2WithMetadata(xml: string) { const lastModified = new Date(content.LastModified) const etag = sanitizeETag(content.ETag) const size = content.Size + + let tags: Tags = {} + if (content.UserTags != null) { + toArray(content.UserTags.split('&')).forEach((tag) => { + const [key, value] = tag.split('=') + tags[key] = value + }) + } else { + tags = {} + } + let metadata if (content.UserMetadata != null) { metadata = toArray(content.UserMetadata)[0] } else { metadata = null } - result.objects.push({ name, lastModified, etag, size, metadata }) + result.objects.push({ name, lastModified, etag, size, metadata, tags }) }) } diff --git a/tests/functional/functional-tests.js b/tests/functional/functional-tests.js index c1eac68f..6ededba7 100644 --- a/tests/functional/functional-tests.js +++ b/tests/functional/functional-tests.js @@ -3620,6 +3620,65 @@ describe('functional tests', function () { ) }) }) + describe('listObjectsV2WithMetadata with tags and metadata', function () { + const bucketName = 'minio-js-test-tags-' + uuid.v4() + const fdObjectName = 'datafile-100-kB' + const fdObject = dataDir ? fs.readFileSync(dataDir + '/' + fdObjectName) : Buffer.alloc(100 * 1024, 0) + const objectName = 'objectwithtags' + const tags = { key1: 'value1', key2: 'value2' } + const metadata = { 'X-Amz-Meta-Test': 'test-value' } + + before(() => { + return client.makeBucket(bucketName, '').then((res) => { + return client.putObject(bucketName, objectName, fdObject, fdObject.length, metadata).then((res) => { + return client.setObjectTagging(bucketName, objectName, tags) + }) + }) + }) + after(() => client.removeObject(bucketName, objectName).then((_) => client.removeBucket(bucketName))) + + step( + `extensions.listObjectsV2WithMetadata(bucketName, prefix, recursive)_bucketName:${bucketName}, prefix:"", recursive:true`, + function (done) { + const listStream = client.extensions.listObjectsV2WithMetadata(bucketName, '', true) + let listedObject = null + + listStream.on('data', function (obj) { + listedObject = obj + }) + + listStream.on('end', function () { + if (!listedObject) { + return done(new Error('No objects were listed')) + } + + if (listedObject.name !== objectName) { + return done(new Error(`Expected object name: ${objectName}, received: ${listedObject.name}`)) + } + + if (!_.isEqual(listedObject.tags, tags)) { + return done( + new Error(`Expected tags: ${JSON.stringify(tags)}, received: ${JSON.stringify(listedObject.tags)}`), + ) + } + + if (!_.isEqual(listedObject.metadata['X-Amz-Meta-Test'], metadata['X-Amz-Meta-Test'])) { + return done( + new Error( + `Expected metadata: ${JSON.stringify(metadata)}, received: ${JSON.stringify(listedObject.metadata)}`, + ), + ) + } + + done() + }) + + listStream.on('error', function (e) { + done(e) + }) + }, + ) + }) describe('Object Name special characters test with a Prefix', () => { // Isolate the bucket/object for easy debugging and tracking. const bucketNameForSpCharObjects = 'minio-js-test-obj-spnpre-' + uuid.v4()