Skip to content

Commit

Permalink
feat: Handle MAIN_FOLDER_REMOVED error on saveFiles
Browse files Browse the repository at this point in the history
When this even occurs, it means that the main destination folder of the
konnector has been removed during konnector execution.

In this case, we :
 - Regenerate the files index
 - create the destination folder according to the manifest
 - update the trigger with the new message.folder_to_save attribute
 - retry saveFiles

see konnectors/libs#973
  • Loading branch information
doubleface committed Nov 23, 2023
1 parent 5b9a34e commit 9822b7f
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 5 deletions.
70 changes: 65 additions & 5 deletions src/libs/Launcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -402,8 +402,8 @@ export default class Launcher {
*
* @return {Promise<Map<String, import('cozy-client/types/types').FileDocument>>} - index of existing files
*/
async getExistingFilesIndex() {
if (this.existingFilesIndex) {
async getExistingFilesIndex(reset = false) {
if (!reset && this.existingFilesIndex) {
return this.existingFilesIndex
}

Expand Down Expand Up @@ -472,7 +472,7 @@ export default class Launcher {

const existingFilesIndex = await this.getExistingFilesIndex()

const result = await saveFiles(client, entries, folderPath, {
const saveFilesOptions = {
...options,
manifest: konnector,
// @ts-ignore
Expand All @@ -485,10 +485,70 @@ export default class Launcher {
dataUri: await this.worker.call('downloadFileInWorker', entry)
}),
existingFilesIndex
}

try {
const result = await saveFiles(
client,
entries,
folderPath,
saveFilesOptions
)
log.debug(result, 'saveFiles result')
return result
} catch (err) {
if (
(err instanceof Error && err.message !== 'MAIN_FOLDER_REMOVED') ||
!(err instanceof Error) // instanceof Error is here to fix typing error
) {
throw err
}
// main destination folder has been removed during the execution of the konnector. Trying one time to reset all and relaunch saveFiles
return await this.retrySaveFiles(entries, saveFilesOptions)
}
}

/**
* Rerun the saveFiles function after have reindexed files and created the destination folder
* @param {Array<FileDocument>} entries - list of file entries to save
* @param {object} options - options object
* @returns {Promise<Array<object>>} list of saved files
*/
async retrySaveFiles(entries, options) {
const {
launcherClient: client,
account,
trigger,
konnector
} = this.getStartContext() || {}

const folderPath = await this.getFolderPath(trigger.message?.folder_to_save)
this.log({
level: 'warning',
namespace: 'Launcher',
label: 'saveFiles',
message:
'Destination folder removed during konnector execution, trying again'
})
const folder = await models.konnectorFolder.ensureKonnectorFolder(client, {
konnector: { ...konnector, _id: konnector.id }, // _id attribute is missing in konnector object, which causes the reference to the konnector in the destination folder to be null
account,
lang: client.getInstanceOptions().locale
})
log.debug(result, 'saveFiles result')
trigger.message.folder_to_save = folder._id
const { data: triggerResult } = await client.save(trigger)

return result
this.setStartContext({
...this.getStartContext(),
trigger: triggerResult
})

const updatedFilesIndex = await this.getExistingFilesIndex(true) // update files index since the destination folder was removed

return await saveFiles(client, entries, folderPath, {
...options,
existingFilesIndex: updatedFilesIndex
})
}

/**
Expand Down
128 changes: 128 additions & 0 deletions src/libs/Launcher.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,5 +290,133 @@ describe('Launcher', () => {
sourceAccountIdentifier: 'testsourceaccountidentifier'
})
})
it('should retry on MAIN_FOLDER_REMOVED error', async () => {
const launcher = new Launcher()
launcher.setUserData({
sourceAccountIdentifier: 'testsourceaccountidentifier'
})

const konnector = {
slug: 'testkonnectorslug',
id: 'testkonnectorid',
name: 'Test Konnector'
}
const trigger = {
_type: 'io.cozy.triggers',
message: {
folder_to_save: 'testfolderid',
account: 'testaccountid'
}
}
const job = {
message: { account: 'testaccountid', folder_to_save: 'testfolderid' }
}
const client = {
collection: doctype => {
if (doctype === 'io.cozy.permissions') {
return { add: jest.fn() }
}
return client
},
addReferencesTo: jest.fn(),
statByPath: jest
.fn()
.mockImplementation(path => ({ data: { _id: path } })),
findReferencedBy: jest
.fn()
.mockResolvedValue({ included: existingMagicFolder }),
getInstanceOptions: jest.fn().mockReturnValue(() => ({ locale: 'fr' })),
queryAll: jest.fn().mockResolvedValue([
{
_id: 'tokeep',
metadata: {
fileIdAttributes: 'fileidattribute'
}
},
{
_id: 'toignore'
}
]),
query: jest.fn().mockResolvedValue({ data: { path: 'folderPath' } }),
save: jest.fn().mockImplementation(data => ({ _id: 'triggerid' }))

Check failure on line 341 in src/libs/Launcher.spec.js

View workflow job for this annotation

GitHub Actions / Quality Checks

'data' is defined but never used
}
launcher.setStartContext({
konnector,
account: { _id: 'testaccountid' },
client,
launcherClient: client,
trigger,
job
})
saveFiles.mockRejectedValueOnce(new Error('MAIN_FOLDER_REMOVED'))

await launcher.saveFiles([{}], {})

expect(saveFiles).toHaveBeenNthCalledWith(1, client, [{}], 'folderPath', {
downloadAndFormatFile: expect.any(Function),
manifest: expect.any(Object),
existingFilesIndex: new Map([
[
'fileidattribute',
{
_id: 'tokeep',
metadata: { fileIdAttributes: 'fileidattribute' }
}
]
]),
sourceAccount: 'testaccountid',
sourceAccountIdentifier: 'testsourceaccountidentifier'
})
expect(saveFiles).toHaveBeenNthCalledWith(2, client, [{}], 'folderPath', {
downloadAndFormatFile: expect.any(Function),
manifest: expect.any(Object),
existingFilesIndex: new Map([
[
'fileidattribute',
{
_id: 'tokeep',
metadata: { fileIdAttributes: 'fileidattribute' }
}
]
]),
sourceAccount: 'testaccountid',
sourceAccountIdentifier: 'testsourceaccountidentifier'
})
expect(client.save).toHaveBeenCalledTimes(1)
expect(client.save).toHaveBeenNthCalledWith(1, {
_type: 'io.cozy.triggers',
message: {
account: 'testaccountid',
folder_to_save: '/Administratif/Test Konnector/testaccountid'
}
})
expect(client.queryAll).toHaveBeenCalledTimes(2)
expect(client.queryAll).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
doctype: 'io.cozy.files',
selector: {
cozyMetadata: {
createdByApp: 'testkonnectorslug',
sourceAccountIdentifier: 'testsourceaccountidentifier'
},
trashed: false
}
})
)
expect(client.queryAll).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
doctype: 'io.cozy.files',
selector: {
cozyMetadata: {
createdByApp: 'testkonnectorslug',
sourceAccountIdentifier: 'testsourceaccountidentifier'
},
trashed: false
}
})
)
})
})
})

0 comments on commit 9822b7f

Please sign in to comment.