Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/algos/list/MPU.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ class MultipartUploads {
},
StorageClass: tmp['x-amz-storage-class'],
Initiated: tmp.initiated,
ChecksumAlgorithm: tmp.checksumAlgorithm,
ChecksumType: tmp.checksumType,
ChecksumIsDefault: tmp.checksumIsDefault,
},
});
this.NextKeyMarker = tmp.key;
Expand Down
11 changes: 11 additions & 0 deletions lib/s3middleware/convertToXml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export type ListParams = {
};
StorageClass: string;
Initiated: string;
ChecksumAlgorithm?: string;
ChecksumType?: string;
ChecksumIsDefault?: boolean;
};
}>;
};
Expand Down Expand Up @@ -116,6 +119,14 @@ export const listMultipartUploads = (xmlParams: ListParams) => {
xml.push(
'<Upload>',
`<Key>${escapeForXml(key)}</Key>`,
);
if (!val.ChecksumIsDefault && val.ChecksumAlgorithm && val.ChecksumType) {
xml.push(
`<ChecksumAlgorithm>${escapeForXml(val.ChecksumAlgorithm.toUpperCase())}</ChecksumAlgorithm>`,
`<ChecksumType>${escapeForXml(val.ChecksumType)}</ChecksumType>`,
);
}
xml.push(
`<UploadId>${escapeForXml(val.UploadId)}</UploadId>`,
'<Initiator>',
`<ID>${escapeForXml(val.Initiator.ID)}</ID>`,
Expand Down
3 changes: 3 additions & 0 deletions lib/storage/metadata/in_memory/ListMultipartUploadsResult.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ class ListMultipartUploadsResult extends ListResult {
},
StorageClass: uploadInfo.storageClass,
Initiated: uploadInfo.initiated,
ChecksumAlgorithm: uploadInfo.checksumAlgorithm,
ChecksumType: uploadInfo.checksumType,
ChecksumIsDefault: uploadInfo.checksumIsDefault,
},
});
this.MaxKeys += 1;
Expand Down
3 changes: 3 additions & 0 deletions lib/storage/metadata/in_memory/getMultipartUploadListing.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ function getMultipartUploadListing(bucket, params, callback) {
ownerDisplayName: storedMD['owner-display-name'],
storageClass: storedMD['x-amz-storage-class'],
initiated: storedMD.initiated,
checksumAlgorithm: storedMD.checksumAlgorithm,
checksumType: storedMD.checksumType,
checksumIsDefault: storedMD.checksumIsDefault,
};
});
// If keyMarker specified, edit the uploads array so it
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"engines": {
"node": ">=20"
},
"version": "8.2.50",
"version": "8.2.51",
"description": "Common utilities for the S3 project components",
"main": "build/index.js",
"repository": {
Expand Down
83 changes: 83 additions & 0 deletions tests/unit/algos/list/MPU.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,64 @@ describe('Multipart Uploads listing algorithm', () => {
},
StorageClass: tmp['x-amz-storage-class'],
Initiated: tmp.initiated,
ChecksumAlgorithm: tmp.checksumAlgorithm,
ChecksumType: tmp.checksumType,
ChecksumIsDefault: tmp.checksumIsDefault,
},
};
});
done();
});

const checksumValues = [
JSON.stringify({
'key': 'test/1',
'uploadId': 'uploadId1',
'initiator': initiator1,
'owner-id': '1',
'owner-display-name': 'owner1',
'x-amz-storage-class': storageClass,
'initiated': '',
'checksumAlgorithm': 'CRC32',
'checksumType': 'FULL_OBJECT',
'checksumIsDefault': false,
}),
JSON.stringify({
'key': 'test/2',
'uploadId': 'uploadId2',
'initiator': initiator2,
'owner-id': '1',
'owner-display-name': 'owner2',
'x-amz-storage-class': storageClass,
'initiated': '',
'checksumAlgorithm': 'SHA256',
'checksumType': 'COMPOSITE',
'checksumIsDefault': true,
}),
JSON.stringify({
'key': 'test/3',
'uploadId': 'uploadId3',
'initiator': initiator1,
'owner-id': '1',
'owner-display-name': 'owner1',
'x-amz-storage-class': storageClass,
'initiated': '',
}),
];

const checksumKeys = {
v0: [
`${overviewPrefix}test/1${splitter}uploadId1`,
`${overviewPrefix}test/2${splitter}uploadId2`,
`${overviewPrefix}test/3${splitter}uploadId3`,
],
v1: [
`${DbPrefixes.Master}${overviewPrefix}test/1${splitter}uploadId1`,
`${DbPrefixes.Master}${overviewPrefix}test/2${splitter}uploadId2`,
`${DbPrefixes.Master}${overviewPrefix}test/3${splitter}uploadId3`,
],
};

['v0', 'v1'].forEach(vFormat => {
const dbListing = keys[vFormat].map((key, i) => ({
key,
Expand Down Expand Up @@ -170,5 +222,36 @@ describe('Multipart Uploads listing algorithm', () => {
listingParams, logger, vFormat);
assert.deepStrictEqual(listingResult, expectedResult);
});

it(`should pass through checksum fields ` +
`(vFormat=${vFormat})`, () => {
const checksumDbListing = checksumKeys[vFormat].map((key, i) => ({
key,
value: checksumValues[i],
}));
const result = performListing(checksumDbListing, MultipartUploads,
listingParams, logger, vFormat);
// First upload: explicit checksum
assert.strictEqual(
result.Uploads[0].value.ChecksumAlgorithm, 'CRC32');
assert.strictEqual(
result.Uploads[0].value.ChecksumType, 'FULL_OBJECT');
assert.strictEqual(
result.Uploads[0].value.ChecksumIsDefault, false);
// Second upload: default checksum
assert.strictEqual(
result.Uploads[1].value.ChecksumAlgorithm, 'SHA256');
assert.strictEqual(
result.Uploads[1].value.ChecksumType, 'COMPOSITE');
assert.strictEqual(
result.Uploads[1].value.ChecksumIsDefault, true);
// Third upload: no checksum fields
assert.strictEqual(
result.Uploads[2].value.ChecksumAlgorithm, undefined);
assert.strictEqual(
result.Uploads[2].value.ChecksumType, undefined);
assert.strictEqual(
result.Uploads[2].value.ChecksumIsDefault, undefined);
});
});
});
133 changes: 133 additions & 0 deletions tests/unit/s3middleware/convertToXml.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import assert from 'assert';
import { listMultipartUploads, ListParams } from
'../../../lib/s3middleware/convertToXml';

function makeUpload(key: string, uploadId: string, overrides?: {
ChecksumAlgorithm?: string;
ChecksumType?: string;
ChecksumIsDefault?: boolean;
}) {
return {
key,
value: {
UploadId: uploadId,
Initiator: { ID: 'initiator1', DisplayName: 'Initiator 1' },
Owner: { ID: 'owner1', DisplayName: 'Owner 1' },
StorageClass: 'STANDARD',
Initiated: '2026-01-01T00:00:00.000Z',
...overrides,
},
};
}

function makeParams(uploads: ListParams['list']['Uploads']): ListParams {
return {
list: {
MaxKeys: '1000',
IsTruncated: 'false',
CommonPrefixes: [],
Uploads: uploads,
},
encoding: 'url',
bucketName: 'test-bucket',
keyMarker: '',
uploadIdMarker: '',
};
}

describe('convertToXml listMultipartUploads', () => {
it('should uppercase ChecksumAlgorithm in XML output', () => {
const params = makeParams([
makeUpload('key1', 'id1', {
ChecksumAlgorithm: 'crc32',
ChecksumType: 'FULL_OBJECT',
ChecksumIsDefault: false,
}),
]);
const xml = listMultipartUploads(params);
assert(xml.includes(
'<ChecksumAlgorithm>CRC32</ChecksumAlgorithm>'));
assert(xml.includes(
'<ChecksumType>FULL_OBJECT</ChecksumType>'));
});

it('should not include checksum fields when ChecksumIsDefault is true',
() => {
const params = makeParams([
makeUpload('key1', 'id1', {
ChecksumAlgorithm: 'sha256',
ChecksumType: 'COMPOSITE',
ChecksumIsDefault: true,
}),
]);
const xml = listMultipartUploads(params);
assert(!xml.includes('<ChecksumAlgorithm>'));
assert(!xml.includes('<ChecksumType>'));
});

it('should not include checksum fields when absent', () => {
const params = makeParams([
makeUpload('key1', 'id1'),
]);
const xml = listMultipartUploads(params);
assert(!xml.includes('<ChecksumAlgorithm>'));
assert(!xml.includes('<ChecksumType>'));
});

it('should place ChecksumAlgorithm after Key and before UploadId', () => {
const params = makeParams([
makeUpload('key1', 'id1', {
ChecksumAlgorithm: 'sha256',
ChecksumType: 'COMPOSITE',
ChecksumIsDefault: false,
}),
]);
const xml = listMultipartUploads(params);
const keyIdx = xml.indexOf('<Key>');
const checksumAlgoIdx = xml.indexOf('<ChecksumAlgorithm>');
const checksumTypeIdx = xml.indexOf('<ChecksumType>');
const uploadIdIdx = xml.indexOf('<UploadId>');
assert(keyIdx < checksumAlgoIdx,
'ChecksumAlgorithm should come after Key');
assert(checksumAlgoIdx < checksumTypeIdx,
'ChecksumType should come after ChecksumAlgorithm');
assert(checksumTypeIdx < uploadIdIdx,
'UploadId should come after ChecksumType');
});

it('should not include checksum fields when only one is present', () => {
const params = makeParams([
makeUpload('key1', 'id1', {
ChecksumAlgorithm: 'crc32',
ChecksumIsDefault: false,
}),
]);
const xml = listMultipartUploads(params);
assert(!xml.includes('<ChecksumAlgorithm>'));
assert(!xml.includes('<ChecksumType>'));
});

it('should handle mixed uploads with and without checksum fields', () => {
const params = makeParams([
makeUpload('key1', 'id1', {
ChecksumAlgorithm: 'crc32c',
ChecksumType: 'FULL_OBJECT',
ChecksumIsDefault: false,
}),
makeUpload('key2', 'id2'),
makeUpload('key3', 'id3', {
ChecksumAlgorithm: 'sha256',
ChecksumType: 'COMPOSITE',
ChecksumIsDefault: true,
}),
]);
const xml = listMultipartUploads(params);
assert(xml.includes(
'<ChecksumAlgorithm>CRC32C</ChecksumAlgorithm>'));
// Only the first upload should emit checksum elements
const algoMatches = xml.match(/<ChecksumAlgorithm>/g);
assert.strictEqual(algoMatches?.length, 1);
const typeMatches = xml.match(/<ChecksumType>/g);
assert.strictEqual(typeMatches?.length, 1);
});
});
Loading