diff --git a/src/storage/index.ts b/src/storage/index.ts index e8e98c0c..f3f25b2e 100644 --- a/src/storage/index.ts +++ b/src/storage/index.ts @@ -577,80 +577,86 @@ export function getAppBucketUrl(gaiaHubUrl: string, appPrivateKey: string) { * @returns {Promise} that resolves to the number of files listed. * @private */ -function listFilesLoop(hubConfig: GaiaHubConfig, - page: string | null, - callCount: number, - fileCount: number, - callback: (name: string) => boolean): Promise { +async function listFilesLoop( + caller: UserSession, + hubConfig: GaiaHubConfig | null, + page: string | null, + callCount: number, + fileCount: number, + callback: (name: string) => boolean +): Promise { if (callCount > 65536) { // this is ridiculously huge, and probably indicates // a faulty Gaia hub anyway (e.g. on that serves endless data) throw new Error('Too many entries to list') } - let httpStatus - const pageRequest = JSON.stringify({ page }) - - const fetchOptions = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Content-Length': `${pageRequest.length}`, - Authorization: `bearer ${hubConfig.token}` - }, - body: pageRequest + hubConfig = hubConfig || await caller.getOrSetLocalGaiaHubConnection() + let response: Response + try { + const pageRequest = JSON.stringify({ page }) + const fetchOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': `${pageRequest.length}`, + Authorization: `bearer ${hubConfig.token}` + }, + body: pageRequest + } + response = await fetch(`${hubConfig.server}/list-files/${hubConfig.address}`, fetchOptions) + if (!response.ok) { + throw new Error(`listFiles failed with HTTP status ${response.status}`) + } + } catch (error) { + // If error occurs on the first call, perform a gaia re-connection and retry. + // Same logic as other gaia requests (putFile, getFile, etc). + if (callCount === 0) { + const freshHubConfig = await caller.setLocalGaiaHubConnection() + return listFilesLoop(caller, freshHubConfig, page, callCount + 1, 0, callback) + } + throw error } - return fetch(`${hubConfig.server}/list-files/${hubConfig.address}`, fetchOptions) - .then((response) => { - httpStatus = response.status - if (httpStatus >= 400) { - throw new Error(`listFiles failed with HTTP status ${httpStatus}`) - } - return response.text() - }) - .then(responseText => JSON.parse(responseText)) - .then((responseJSON) => { - const entries = responseJSON.entries - const nextPage = responseJSON.page - if (entries === null || entries === undefined) { - // indicates a misbehaving Gaia hub or a misbehaving driver - // (i.e. the data is malformed) - throw new Error('Bad listFiles response: no entries') - } - for (let i = 0; i < entries.length; i++) { - const rc = callback(entries[i]) - if (!rc) { - // callback indicates that we're done - return Promise.resolve(fileCount + i) - } - } - if (nextPage && entries.length > 0) { - // keep going -- have more entries - return listFilesLoop( - hubConfig, nextPage, callCount + 1, fileCount + entries.length, callback - ) - } else { - // no more entries -- end of data - return Promise.resolve(fileCount + entries.length) - } - }) + const responseText = await response.text() + const responseJSON = JSON.parse(responseText) + const entries = responseJSON.entries + const nextPage = responseJSON.page + if (entries === null || entries === undefined) { + // indicates a misbehaving Gaia hub or a misbehaving driver + // (i.e. the data is malformed) + throw new Error('Bad listFiles response: no entries') + } + for (let i = 0; i < entries.length; i++) { + const rc = callback(entries[i]) + if (!rc) { + // callback indicates that we're done + return fileCount + i + } + } + if (nextPage && entries.length > 0) { + // keep going -- have more entries + return listFilesLoop( + caller, hubConfig, nextPage, callCount + 1, fileCount + entries.length, callback + ) + } else { + // no more entries -- end of data + return fileCount + entries.length + } } /** * List the set of files in this application's Gaia storage bucket. - * @param {UserSession} caller - instance calling this method * @param {function} callback - a callback to invoke on each named file that * returns `true` to continue the listing operation or `false` to end it * @return {Promise} that resolves to the number of files listed */ -export async function listFiles( +export function listFiles( callback: (name: string) => boolean, caller?: UserSession ): Promise { - const userSession = caller || new UserSession() - const gaiaHubConfig = await userSession.getOrSetLocalGaiaHubConnection() - return listFilesLoop(gaiaHubConfig, null, 0, 0, callback) + caller = caller || new UserSession() + return listFilesLoop(caller, null, null, 0, 0, callback) } export { connectToGaiaHub, uploadToGaiaHub, BLOCKSTACK_GAIA_HUB_LABEL } diff --git a/tests/unitTests/src/unitTestsStorage.ts b/tests/unitTests/src/unitTestsStorage.ts index b0f800c0..c58b9dc2 100644 --- a/tests/unitTests/src/unitTestsStorage.ts +++ b/tests/unitTests/src/unitTestsStorage.ts @@ -1032,4 +1032,68 @@ export function runStorageTests() { t.equal(count, 1, 'Count matches number of files') }) }) + + test('listFiles gets a new gaia config and tries again', (t) => { + t.plan(4) + + const path = 'file.json' + const listFilesUrl = 'https://hub.testblockstack.org/list-files/1NZNxhoxobqwsNvTb16pdeiqvFvce3Yabc' + const invalidHubConfig = { + address: '1NZNxhoxobqwsNvTb16pdeiqvFvce3Yabc', + server: 'https://hub.testblockstack.org', + token: '', + url_prefix: 'https://gaia.testblockstack.org/hub/' + } + const validHubConfig = Object.assign({}, invalidHubConfig, { + token: 'valid' + }) + const connectToGaiaHub = sinon.stub().resolves(validHubConfig) + + const privateKey = 'a5c61c6ca7b3e7e55edee68566aeab22e4da26baa285c7bd10e8d2218aa3b229' + const UserSessionClass = proxyquire('../../../src/auth/userSession', { + '../storage/hub': { + connectToGaiaHub + } + }).UserSession as typeof UserSession + + const appConfig = new AppConfig(['store_write'], 'http://localhost:3000') + const blockstack = new UserSessionClass({ appConfig }) + blockstack.store.getSessionData().userData = { + appPrivateKey: privateKey, + gaiaHubConfig: invalidHubConfig + } + + const { listFiles } = proxyquire('../../../src/storage', { + './hub': { + connectToGaiaHub + } + }) + + let callCount = 0 + FetchMock.post(listFilesUrl, (url, { headers }) => { + if ((headers).Authorization === 'bearer ') { + t.ok(true, 'tries with invalid token') + return 401 + } + callCount += 1 + if (callCount === 1) { + return { entries: [path], page: callCount } + } else if (callCount === 2) { + return { entries: [], page: callCount } + } else { + throw new Error('Called too many times') + } + }) + + const files = [] + listFiles((name) => { + files.push(name) + return true + }, blockstack) + .then((count) => { + t.equal(files.length, 1, 'Got one file back') + t.equal(files[0], 'file.json', 'Got the right file back') + t.equal(count, 1, 'Count matches number of files') + }) + }) }