diff --git a/.changeset/preserve-gcs-listing-failures.md b/.changeset/preserve-gcs-listing-failures.md new file mode 100644 index 000000000..371bf2dd2 --- /dev/null +++ b/.changeset/preserve-gcs-listing-failures.md @@ -0,0 +1,5 @@ +--- +'oc-gs-storage-adapter': patch +--- + +Preserve provider listing failures instead of reporting them as missing directories. diff --git a/packages/oc-gs-storage-adapter/__mocks__/@google-cloud/storage.js b/packages/oc-gs-storage-adapter/__mocks__/@google-cloud/storage.js index 037786628..883f51bfb 100644 --- a/packages/oc-gs-storage-adapter/__mocks__/@google-cloud/storage.js +++ b/packages/oc-gs-storage-adapter/__mocks__/@google-cloud/storage.js @@ -18,6 +18,21 @@ const _Storage = class { return Promise.resolve([]); } + if (bucket === 'failing-bucket') { + return Promise.reject({ code: 503, message: 'Service unavailable' }); + } + + if (bucket === 'missing-bucket') { + return Promise.reject({ code: 404, message: 'Bucket not found' }); + } + + if (bucket === 'unchanged-page-token-bucket') { + return Promise.resolve([ + [{ name: 'components/a/1.0.0/app.js' }], + { pageToken: 'unchanged-token' } + ]); + } + if (bucket === 'paginated-bucket') { const makeFile = (name) => ({ name, diff --git a/packages/oc-gs-storage-adapter/src/index.ts b/packages/oc-gs-storage-adapter/src/index.ts index 7370723ad..e3e38252b 100644 --- a/packages/oc-gs-storage-adapter/src/index.ts +++ b/packages/oc-gs-storage-adapter/src/index.ts @@ -119,62 +119,58 @@ export default function gsAdapter(conf: GsConfig): StorageAdapter { ? dir : dir + '/'; - try { - const collected: { name: string }[] = []; - let pageToken: string | undefined; - - do { - const requestPageToken = pageToken; - const options: { - prefix: string; - autoPaginate: false; - pageToken?: string; - } = { - prefix: normalisedPath, - autoPaginate: false - }; - if (requestPageToken) { - options.pageToken = requestPageToken; - } - - const results = await getClient().bucket(bucketName).getFiles(options); - const files = results[0] ?? []; - const nextQuery = results[1] as { pageToken?: string } | undefined; + const collected: { name: string }[] = []; + let pageToken: string | undefined; + + do { + const requestPageToken = pageToken; + const options: { + prefix: string; + autoPaginate: false; + pageToken?: string; + } = { + prefix: normalisedPath, + autoPaginate: false + }; + if (requestPageToken) { + options.pageToken = requestPageToken; + } - for (const file of files) { - collected.push(file); - } + const results = await getClient().bucket(bucketName).getFiles(options); + const files = results[0] ?? []; + const nextQuery = results[1] as { pageToken?: string } | undefined; - const nextPageToken = nextQuery?.pageToken; - if (nextPageToken && nextPageToken === requestPageToken) { - throw new Error( - 'GCS getFiles returned an unchanged pageToken; aborting to avoid an infinite loop' - ); - } - pageToken = nextPageToken; - } while (pageToken); - - if (collected.length === 0) { - throw 'no files'; + for (const file of files) { + collected.push(file); } - const result = collected - //remove prefix - .map((file) => file.name.replace(normalisedPath, '')) - // only get files that aren't in root directory - .filter((file) => file.split('/').length > 1) - //get directory names - .map((file) => file.split('/')[0]) - // reduce to unique directories - .filter((item, i, ar) => ar.indexOf(item) === i); + const nextPageToken = nextQuery?.pageToken; + if (nextPageToken && nextPageToken === requestPageToken) { + throw new Error( + 'GCS getFiles returned an unchanged pageToken; aborting to avoid an infinite loop' + ); + } + pageToken = nextPageToken; + } while (pageToken); - return result; - } catch (_err) { + if (collected.length === 0) { throw { code: strings.errors.STORAGE.DIR_NOT_FOUND_CODE, msg: strings.errors.STORAGE.DIR_NOT_FOUND(dir) }; } + + const result = collected + //remove prefix + .map((file) => file.name.replace(normalisedPath, '')) + // only get files that aren't in root directory + .filter((file) => file.split('/').length > 1) + //get directory names + .map((file) => file.split('/')[0]) + // reduce to unique directories + .filter((item, i, ar) => ar.indexOf(item) === i); + + return result; }; const putDir = async ( diff --git a/packages/oc-gs-storage-adapter/test/gs.test.ts b/packages/oc-gs-storage-adapter/test/gs.test.ts index ef361a1c2..928110e83 100644 --- a/packages/oc-gs-storage-adapter/test/gs.test.ts +++ b/packages/oc-gs-storage-adapter/test/gs.test.ts @@ -195,6 +195,35 @@ test('listSubDirectories follows pageToken across pages', async () => { expect(data).toEqual(['1.0.0', '2.0.0']); }); +test('listSubDirectories preserves provider errors', () => { + const client = gs({ ...validOptions, bucket: 'failing-bucket' }); + + return expect(client.listSubDirectories('components/a')).rejects.toEqual({ + code: 503, + message: 'Service unavailable' + }); +}); + +test('listSubDirectories preserves missing bucket errors', () => { + const client = gs({ ...validOptions, bucket: 'missing-bucket' }); + + return expect(client.listSubDirectories('components/a')).rejects.toEqual({ + code: 404, + message: 'Bucket not found' + }); +}); + +test('listSubDirectories preserves unchanged pageToken errors', () => { + const client = gs({ + ...validOptions, + bucket: 'unchanged-page-token-bucket' + }); + + return expect(client.listSubDirectories('components/a')).rejects.toThrow( + 'GCS getFiles returned an unchanged pageToken; aborting to avoid an infinite loop' + ); +}); + test('removeDir follows pageToken across pages and deletes all files', async () => { const client = gs({ ...validOptions, bucket: 'paginated-bucket' });