Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Handle MAIN_FOLDER_REMOVED error on saveFiles #1030

Merged
merged 1 commit into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"@sentry/react-native": "3.4.3",
"base-64": "^1.0.0",
"cozy-client": "^44.0.0",
"cozy-clisk": "^0.26.0",
"cozy-clisk": "^0.27.0",
"cozy-device-helper": "^2.7.0",
"cozy-flags": "^2.11.0",
"cozy-intent": "^2.18.0",
Expand Down
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(() => ({ _id: 'triggerid' }))
}
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
}
})
)
})
})
})
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6400,10 +6400,10 @@ cozy-client@^44.0.0:
sift "^6.0.0"
url-search-params-polyfill "^8.0.0"

cozy-clisk@^0.26.0:
version "0.26.0"
resolved "https://registry.yarnpkg.com/cozy-clisk/-/cozy-clisk-0.26.0.tgz#b63734b5678a9ed8c992554da6031c1e6ac1d805"
integrity sha512-E1V3vcrQyrcq6Uxn2B8YqVdufIaI3MTJ//TAwnIhlBYgebo1MRIRoaRYpK8kNXB7EsmQXY5DlgLMOA+1A/YZTQ==
cozy-clisk@^0.27.0:
version "0.27.0"
resolved "https://registry.yarnpkg.com/cozy-clisk/-/cozy-clisk-0.27.0.tgz#49853c67a4e89e950d08fef0f9239f379c4edc29"
integrity sha512-uJR61dQSnXJgGHDhO+6OKo8uj7haGJNnK7gCEhWwj9KlePCajsRm0UuEwh3tl7Kk9bOeteT/LG5wGkw3I2EZyQ==
dependencies:
"@cozy/minilog" "^1.0.0"
bluebird-retry "^0.11.0"
Expand Down
Loading