Skip to content

Commit 47f250b

Browse files
committed
Fixes Sync bug
ListALlVersrions can return all objects below a path Modified isBelowPrefix util function
1 parent ec67e12 commit 47f250b

File tree

4 files changed

+20
-187
lines changed

4 files changed

+20
-187
lines changed

app/src/components/utils.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,8 @@ const utils = {
407407
if (typeof prefix !== 'string' || typeof path !== 'string') return false;
408408
else if (path === prefix) return false;
409409
else if (prefix === DELIMITER) return true;
410-
else if (path.startsWith(prefix)) return true;
410+
else if (path.startsWith(prefix + DELIMITER)) return true;
411+
else if (prefix === '') return true;
411412
else return false;
412413
},
413414

app/src/services/storage.js

Lines changed: 13 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -263,47 +263,16 @@ const objectStorageService = {
263263
return this._getS3Client(data).send(new HeadObjectCommand(params));
264264
},
265265

266-
/**
267-
* @function listAllObjects
268-
* Lists all objects in the bucket with the prefix of `filePath`.
269-
* Performs pagination behind the scenes if required.
270-
* @param {string} [options.filePath=undefined] Optional filePath of the objects
271-
* @param {string} [options.bucketId=undefined] Optional bucketId
272-
* @param {boolean} [options.precisePath=true] Optional boolean for filtering results based on the precise path
273-
* @returns {Promise<object[]>} An array of objects matching the criteria
274-
*/
275-
async listAllObjects({ filePath = undefined, bucketId = undefined, precisePath = true } = {}) {
276-
const key = filePath ?? (await utils.getBucket(bucketId)).key;
277-
const path = key !== DELIMITER ? key : '';
278-
279-
const objects = [];
280-
281-
let incomplete = false;
282-
let nextToken = undefined;
283-
do {
284-
const { Contents, IsTruncated, NextContinuationToken } = await this.listObjectsV2({
285-
filePath: path,
286-
continuationToken: nextToken,
287-
bucketId: bucketId
288-
});
289-
290-
if (Contents) objects.push(
291-
...Contents.filter(object => !precisePath || utils.isAtPath(path, object.Key))
292-
);
293-
incomplete = IsTruncated;
294-
nextToken = NextContinuationToken;
295-
} while (incomplete);
296-
297-
return Promise.resolve(objects);
298-
},
299-
300266
/**
301267
* @function listAllObjectVersions
302268
* Lists all objects in the bucket with the prefix of `filePath`.
303269
* Performs pagination behind the scenes if required.
304270
* @param {string} [options.filePath=undefined] Optional filePath of the objects
305271
* @param {string} [options.bucketId=undefined] Optional bucketId
306-
* @param {boolean} [options.precisePath=true] Optional boolean for filtering results based on the precise path
272+
* @param {boolean} [options.precisePath=true] Optional boolean for filtering results based on the precise path.
273+
* If true, only objects that exist directly at the filepath will be included in the results,
274+
* If false, all objects nested in 'subfolders' below the filepath will be included
275+
* match the precise path rather than below the prefix
307276
* @param {boolean} [options.filterLatest=false] Optional boolean for filtering results to only entries
308277
* with IsLatest being true
309278
* @returns {Promise<object>} An object containg an array of DeleteMarkers and Versions
@@ -328,12 +297,18 @@ const objectStorageService = {
328297

329298
if (DeleteMarkers) deleteMarkers.push(
330299
...DeleteMarkers
331-
.filter(object => !precisePath || utils.isAtPath(path, object.Key))
300+
.filter(object => {
301+
if (precisePath) return utils.isAtPath(path, object.Key);
302+
else return utils.isBelowPrefix(path, object.Key);
303+
})
332304
.filter(object => !filterLatest || object.IsLatest === true)
333305
);
334306
if (Versions) versions.push(
335307
...Versions
336-
.filter(object => !precisePath || utils.isAtPath(path, object.Key))
308+
.filter(object => {
309+
if (precisePath) return utils.isAtPath(path, object.Key);
310+
else return utils.isBelowPrefix(path, object.Key);
311+
})
337312
.filter(object => !filterLatest || object.IsLatest === true)
338313
);
339314
incomplete = IsTruncated;
@@ -488,7 +463,7 @@ const objectStorageService = {
488463

489464
// list all objects at and below provided path (key)
490465
if (recursive) {
491-
const s3Response = await this.listAllObjectVersions({ filePath: key, bucketId: bucketId, precisePath: false });
466+
const s3Response = await this.listAllObjectVersions({ filePath: key, bucketId: bucketId });
492467
log.info(`Found ${s3Response.Versions.length} object versions and ${s3Response.DeleteMarkers.length}
493468
delete markers in S3 for bucketId ${data.bucketId}`, { function: 'syncBucketRecursive' });
494469
const s3Keys = [...new Set([

app/tests/unit/components/utils.spec.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,11 @@ describe('isBelowPrefix', () => {
466466
[false, 'a/b', 'a/c'],
467467
[false, 'a/b/c', 'a/c/b'],
468468
[true, 'a/b/c', 'a/b/c/d/e/f'],
469+
[true, 'a/b/c', 'a/b/c/d/e/f/g.jpg'],
470+
[true, 'a/b/c', 'a/b/c/d/e/f/g hij.jpg'],
471+
[false, 'a', 'abcd.jpeg'],
472+
[true, 'a', 'a/bcd.jpeg'],
473+
[true, '', 'abcd.jpeg'],
469474
])('should return %j given prefix %j and path %j', (expected, prefix, path) => {
470475
expect(utils.isBelowPrefix(prefix, path)).toEqual(expected);
471476
});

app/tests/unit/services/storage.spec.js

Lines changed: 0 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -509,154 +509,6 @@ describe('headObject', () => {
509509
});
510510
});
511511

512-
describe('listAllObjects', () => {
513-
const listObjectsV2Mock = jest.spyOn(service, 'listObjectsV2');
514-
515-
beforeEach(() => {
516-
listObjectsV2Mock.mockReset();
517-
});
518-
519-
afterAll(() => {
520-
listObjectsV2Mock.mockRestore();
521-
});
522-
523-
it('should call listObjectsV2 at least once and return an empty array', async () => {
524-
listObjectsV2Mock.mockResolvedValue({ IsTruncated: false });
525-
526-
const result = await service.listAllObjects();
527-
528-
expect(result).toBeTruthy();
529-
expect(Array.isArray(result)).toBeTruthy();
530-
expect(result).toHaveLength(0);
531-
expect(utils.getBucket).toHaveBeenCalledTimes(1);
532-
expect(utils.isAtPath).toHaveBeenCalledTimes(0);
533-
expect(listObjectsV2Mock).toHaveBeenCalledTimes(1);
534-
expect(listObjectsV2Mock).toHaveBeenCalledWith(expect.objectContaining({
535-
filePath: key
536-
}));
537-
});
538-
539-
it('should call listObjectsV2 at least once and return an empty array of objects', async () => {
540-
listObjectsV2Mock.mockResolvedValue({ Contents: [], IsTruncated: false });
541-
542-
const result = await service.listAllObjects();
543-
544-
expect(result).toBeTruthy();
545-
expect(Array.isArray(result)).toBeTruthy();
546-
expect(result).toHaveLength(0);
547-
expect(utils.getBucket).toHaveBeenCalledTimes(1);
548-
expect(utils.isAtPath).toHaveBeenCalledTimes(0);
549-
expect(listObjectsV2Mock).toHaveBeenCalledTimes(1);
550-
expect(listObjectsV2Mock).toHaveBeenCalledWith(expect.objectContaining({
551-
filePath: key
552-
}));
553-
});
554-
555-
it('should call listObjectsV2 multiple times and return an array of precise path objects', async () => {
556-
const continueToken = 'token';
557-
listObjectsV2Mock.mockResolvedValueOnce({
558-
Contents: [{ Key: 'filePath/foo' }],
559-
IsTruncated: true,
560-
NextContinuationToken: continueToken
561-
});
562-
listObjectsV2Mock.mockResolvedValueOnce({
563-
Contents: [{ Key: 'filePath/bar' }],
564-
IsTruncated: false
565-
});
566-
567-
const result = await service.listAllObjects();
568-
569-
expect(result).toBeTruthy();
570-
expect(Array.isArray(result)).toBeTruthy();
571-
expect(result).toHaveLength(2);
572-
expect(result).toEqual(expect.arrayContaining([
573-
{ Key: 'filePath/foo' },
574-
{ Key: 'filePath/bar' }
575-
]));
576-
expect(utils.getBucket).toHaveBeenCalledTimes(1);
577-
expect(utils.isAtPath).toHaveBeenCalledTimes(2);
578-
expect(listObjectsV2Mock).toHaveBeenCalledTimes(2);
579-
expect(listObjectsV2Mock).toHaveBeenNthCalledWith(1, expect.objectContaining({
580-
filePath: key
581-
}));
582-
expect(listObjectsV2Mock).toHaveBeenNthCalledWith(2, expect.objectContaining({
583-
filePath: key,
584-
continuationToken: continueToken
585-
}));
586-
});
587-
588-
it('should call listObjectsV2 multiple times and return an array of all path objects', async () => {
589-
const continueToken = 'token';
590-
listObjectsV2Mock.mockResolvedValueOnce({
591-
Contents: [{ Key: 'filePath/test/foo' }],
592-
IsTruncated: true,
593-
NextContinuationToken: continueToken
594-
});
595-
listObjectsV2Mock.mockResolvedValueOnce({
596-
Contents: [{ Key: 'filePath/test/bar' }],
597-
IsTruncated: false
598-
});
599-
600-
const result = await service.listAllObjects({ precisePath: false });
601-
602-
expect(result).toBeTruthy();
603-
expect(Array.isArray(result)).toBeTruthy();
604-
expect(result).toHaveLength(2);
605-
expect(result).toEqual(expect.arrayContaining([
606-
{ Key: 'filePath/test/foo' },
607-
{ Key: 'filePath/test/bar' }
608-
]));
609-
expect(utils.getBucket).toHaveBeenCalledTimes(1);
610-
expect(utils.isAtPath).toHaveBeenCalledTimes(0);
611-
expect(listObjectsV2Mock).toHaveBeenCalledTimes(2);
612-
expect(listObjectsV2Mock).toHaveBeenNthCalledWith(1, expect.objectContaining({
613-
filePath: key
614-
}));
615-
expect(listObjectsV2Mock).toHaveBeenNthCalledWith(2, expect.objectContaining({
616-
filePath: key,
617-
continuationToken: continueToken
618-
}));
619-
});
620-
621-
it(
622-
'should call listObjectsV2 multiple times with the right bucketId and filePath, returning an array of objects',
623-
async () => {
624-
const continueToken = 'token';
625-
const customPath = 'filePath/test';
626-
listObjectsV2Mock.mockResolvedValueOnce({
627-
Contents: [{ Key: 'filePath/test/foo' }],
628-
IsTruncated: true,
629-
NextContinuationToken: continueToken
630-
});
631-
listObjectsV2Mock.mockResolvedValueOnce({
632-
Contents: [{ Key: 'filePath/test/bar' }],
633-
IsTruncated: false
634-
});
635-
636-
const result = await service.listAllObjects({ filePath: customPath, bucketId: bucket });
637-
638-
expect(result).toBeTruthy();
639-
expect(Array.isArray(result)).toBeTruthy();
640-
expect(result).toHaveLength(2);
641-
expect(result).toEqual(expect.arrayContaining([
642-
{ Key: 'filePath/test/foo' },
643-
{ Key: 'filePath/test/bar' }
644-
]));
645-
expect(utils.getBucket).toHaveBeenCalledTimes(0);
646-
expect(utils.isAtPath).toHaveBeenCalledTimes(2);
647-
expect(listObjectsV2Mock).toHaveBeenCalledTimes(2);
648-
expect(listObjectsV2Mock).toHaveBeenNthCalledWith(1, expect.objectContaining({
649-
filePath: customPath,
650-
bucketId: bucket
651-
}));
652-
expect(listObjectsV2Mock).toHaveBeenNthCalledWith(2, expect.objectContaining({
653-
filePath: customPath,
654-
continuationToken: continueToken,
655-
bucketId: bucket
656-
}));
657-
}
658-
);
659-
});
660512

661513
describe('listAllObjectVersions', () => {
662514
const listObjectVersionMock = jest.spyOn(service, 'listObjectVersion');

0 commit comments

Comments
 (0)