-
Notifications
You must be signed in to change notification settings - Fork 2
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
Pro 4261 export attachments #10
Conversation
PRO-4261 As an editor, I can download a ZIP file of my exported documents
When clicking on the Two new routes This method should take the IDs of the documents the user want to export, as well as the related types that he wants to get. Only the types, not the IDs, since we decided that it would be too heavy for the export modal to get the amount of related docs when we batch a large amount of documents. Generate filesThis method will need to use the Since we need to store documents in a flat EJSON array, it might be a good idea to add an option to this method in order to get a flat array of related documents instead of filling requested documents with their related docs, see how it could potentially be used with query builders. We want one EJSON file for each db collection, for example We want, for each document to export the draft and live versions (only draft if there is no live). The draft version must appear first to avoid situation where a live exists without draft which is forbidden. Same thing for related documents, they must be written first in the EJSON file, otherwise the doc manager will yell when we'll import it. We will create an Generate ZipTo generate the ZIP file we would prefer using the native node library. Progress bar notificationThis progress bar notification is triggered by wrapping your export function with the
The Finally the ZIP file will be sent back to the client and the download triggered from the user's browser. PermissionsFor exporting documents, a user that has at least one per-type permission should be able to export anything. He won’t be able to see For Reference https://github.com/apostrophecms/tech-designs/blob/main/3.x/share-documents-across-sites/design.md Acceptance Criteria
|
e1dc4c2
to
a5f1016
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
neat! 👏
great implementation above the existing, happy to see that it fitted well.
some questions, some comments, few changes requested
i18n/en.json
Outdated
"exportFileGenerationError": "The {{ extension }} file generation failed", | ||
"exportAttachmentError": "Some attachments could not be added to the {{ extension }} file", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
alphabetical order, as you like it ;)
const position = i * max; | ||
return [ | ||
...chunk, | ||
...attachments.slice(position, position + max) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we're sure we don't leave a promise out in the process?
unit-test it could be good to ensure that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes because of this line:
const length = Math.ceil(attachments.length / max);
we create as many arrays as needed chunks. The ceil
allows to be sure we have a last array to put the last promises even if the don't fill it.
Could be tested indeed.
lib/formats/archiver.js
Outdated
for (const chunk of chunkedPromises) { | ||
const results = await Promise.allSettled(chunk); | ||
for (const { status } of results) { | ||
if (status === 'rejected' && !attachmentError) { | ||
attachmentError = true; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can use .some
rather than another for..of
:
for (const chunk of chunkedPromises) { | |
const results = await Promise.allSettled(chunk); | |
for (const { status } of results) { | |
if (status === 'rejected' && !attachmentError) { | |
attachmentError = true; | |
} | |
for (const chunk of chunkedPromises) { | |
const results = await Promise.allSettled(chunk); | |
if (results.some(result => result.status === 'rejected')) { | |
attachmentError = true; | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice one, updated
lib/formats/archiver.js
Outdated
|
||
archive.append(content, { name: filename }); | ||
// If one image download fail should we stop the entire process? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no I don't think so, and your attachmentError
is a good way of telling that one or several attachment failed, and still being able to generate the archive
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment removed.
lib/methods/archive.js
Outdated
}); | ||
} | ||
} catch (err) { | ||
console.log(err); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
console.log(err); | |
self.apos.error('error', error.message); |
(let's rename err
to error
to be consistent in this module 🙏 )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Destructured to get message.
|
||
const docs = await self.apos.doc.db | ||
const docsIdsUniq = [ ...new Set(docsIds) ]; | ||
const docs = await self.apos[collection].db |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
/* ( */ | ||
/* type === 'relationship' && */ | ||
/* field.withType && */ | ||
/* self.apos.modules[field.withType].options.relatedDocument === false */ | ||
/* ) */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add a TODO: handle 'export: false' option
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added (but we should choose another option name)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use another option name since this one is already used by piece-type-exporter
and could occur unclear conflicts.
Also I think it would be more efficient to perform this check before the DB request if we can.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok I'll handle that in #13
lib/methods/export.js
Outdated
}) { | ||
return schema.flatMap(field => { | ||
const fieldValue = doc[field.name]; | ||
const shouldRecurse = recursion < MAX_RECURSION; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const shouldRecurse = recursion < MAX_RECURSION; | |
const shouldRecurse = recursion <= MAX_RECURSION; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hum since the recursion starts at 0, if I put <=
I'll have 11 call of the function if I have a MAX_RECURSION of 10.
But semantically you're right, the first call isn't a recursion 🤔
Hum the universe is so complex, I need to let my body float in its cold matter, nothing makes sense.
let me modify this line of code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah but a 1 recursion means that we are passing a 2nd time in the function.
to me, "recursion" = "re-called".
that's what's been done in apiRoutes
const moduleOptions = window.apos.modules[this.moduleName]; | ||
const label = this.checked.length > 1 ? moduleOptions.pluralLabel : moduleOptions.label; | ||
const moduleOptions = apos.modules[this.moduleName]; | ||
const label = this.checked?.length > 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const label = this.checked?.length > 1 | |
const label = this.count > 1 |
if possible to use the computed below 👇
@@ -141,7 +141,11 @@ export default { | |||
}, | |||
checked: { | |||
type: Array, | |||
default: () => [] | |||
default: null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do you set it to null
?
having an array by default avoids us having to make loads of conditions when checking the length
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because checked is used only in batch operations, otherwise we user this.doc
.
We need a way to now we are in batch operation or in a single piece operation.
I could make a check on this.doc
instead maybe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated to use a data key selectedDocIds
to store ids from this.doc
or from this.checked
@@ -1,5 +1,7 @@ | |||
{ | |||
"export": "Download {{ type }}", | |||
"exportAttachmentError": "Some attachments could not be added to the {{ extension }} file", | |||
"exportFileGenerationError": "The {{ extension }} file generation failed", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
below exported
🔤
Summary
Manage attachments through stream to be generated in the zip files.
A JSON file is generated with attachments documents.
A folder with actual downloaded files, stream from http
GET
request is passed to archive lib for each attachment.Requests are done 5 by 5.
Error notifications, if an error occurs we archive the file isn't generated.
If we have some attachments issues, file is generated but we warn the user.
What are the specific steps to test this change?
Run exports with images or other attachments.
What kind of change does this PR introduce?
Make sure the PR fulfills these requirements: