From 4157f8ca00efee46347b6843c81abc079fa80697 Mon Sep 17 00:00:00 2001 From: Andrew Rumble Date: Wed, 19 Mar 2025 10:57:53 +0000 Subject: [PATCH] Add a list directory method to the S3 persistor GitOrigin-RevId: 6ecff3eb457dc2168ca49ff9409bb09fa932781c --- .../src/PerProjectEncryptedS3Persistor.js | 18 ++++- libraries/object-persistor/src/S3Persistor.js | 68 +++++++++++++------ libraries/object-persistor/src/types.d.ts | 6 ++ 3 files changed, 70 insertions(+), 22 deletions(-) create mode 100644 libraries/object-persistor/src/types.d.ts diff --git a/libraries/object-persistor/src/PerProjectEncryptedS3Persistor.js b/libraries/object-persistor/src/PerProjectEncryptedS3Persistor.js index 7bd4bb93e5..a0230128fe 100644 --- a/libraries/object-persistor/src/PerProjectEncryptedS3Persistor.js +++ b/libraries/object-persistor/src/PerProjectEncryptedS3Persistor.js @@ -33,6 +33,10 @@ const AES256_KEY_LENGTH = 32 * @property {() => Promise>} getRootKeyEncryptionKeys */ +/** + * @typedef {import('./types').ListDirectoryResult} ListDirectoryResult + */ + /** * Helper function to make TS happy when accessing error properties * AWSError is not an actual class, so we cannot use instanceof. @@ -391,9 +395,9 @@ class PerProjectEncryptedS3Persistor extends S3Persistor { * A general "cache" for project keys is another alternative. For now, use a helper class. */ class CachedPerProjectEncryptedS3Persistor { - /** @type SSECOptions */ + /** @type SSECOptions */ #projectKeyOptions - /** @type PerProjectEncryptedS3Persistor */ + /** @type PerProjectEncryptedS3Persistor */ #parent /** @@ -424,6 +428,16 @@ class CachedPerProjectEncryptedS3Persistor { return await this.#parent.getObjectSize(bucketName, path) } + /** + * + * @param {string} bucketName + * @param {string} path + * @return {Promise} + */ + async listDirectory(bucketName, path) { + return await this.#parent.listDirectory(bucketName, path) + } + /** * @param {string} bucketName * @param {string} path diff --git a/libraries/object-persistor/src/S3Persistor.js b/libraries/object-persistor/src/S3Persistor.js index 2835a271ff..1849838ba4 100644 --- a/libraries/object-persistor/src/S3Persistor.js +++ b/libraries/object-persistor/src/S3Persistor.js @@ -20,6 +20,18 @@ const { URL } = require('node:url') const { WriteError, ReadError, NotFoundError } = require('./Errors') const zlib = require('node:zlib') +/** + * @typedef {import('aws-sdk/clients/s3').ListObjectsV2Output} ListObjectsV2Output + */ + +/** + * @typedef {import('aws-sdk/clients/s3').Object} S3Object + */ + +/** + * @typedef {import('./types').ListDirectoryResult} ListDirectoryResult + */ + /** * Wrapper with private fields to avoid revealing them on console, JSON.stringify or similar. */ @@ -266,26 +278,12 @@ class S3Persistor extends AbstractPersistor { * @return {Promise} */ async deleteDirectory(bucketName, key, continuationToken) { - let response - const options = { Bucket: bucketName, Prefix: key } - if (continuationToken) { - options.ContinuationToken = continuationToken - } - - try { - response = await this._getClientForBucket(bucketName) - .listObjectsV2(options) - .promise() - } catch (err) { - throw PersistorHelper.wrapError( - err, - 'failed to list objects in S3', - { bucketName, key }, - ReadError - ) - } - - const objects = response.Contents?.map(item => ({ Key: item.Key || '' })) + const { contents, response } = await this.listDirectory( + bucketName, + key, + continuationToken + ) + const objects = contents.map(item => ({ Key: item.Key || '' })) if (objects?.length) { try { await this._getClientForBucket(bucketName) @@ -316,6 +314,36 @@ class S3Persistor extends AbstractPersistor { } } + /** + * + * @param {string} bucketName + * @param {string} key + * @param {string} [continuationToken] + * @return {Promise} + */ + async listDirectory(bucketName, key, continuationToken) { + let response + const options = { Bucket: bucketName, Prefix: key } + if (continuationToken) { + options.ContinuationToken = continuationToken + } + + try { + response = await this._getClientForBucket(bucketName) + .listObjectsV2(options) + .promise() + } catch (err) { + throw PersistorHelper.wrapError( + err, + 'failed to list objects in S3', + { bucketName, key }, + ReadError + ) + } + + return { contents: response.Contents ?? [], response } + } + /** * @param {string} bucketName * @param {string} key diff --git a/libraries/object-persistor/src/types.d.ts b/libraries/object-persistor/src/types.d.ts new file mode 100644 index 0000000000..5640685a5f --- /dev/null +++ b/libraries/object-persistor/src/types.d.ts @@ -0,0 +1,6 @@ +import type { ListObjectsV2Output, Object } from 'aws-sdk/clients/s3' + +export type ListDirectoryResult = { + contents: Array + response: ListObjectsV2Output +}