From 42dc6851a704dcb26dc2e5ca4dd16ab148734c5e Mon Sep 17 00:00:00 2001 From: mhsh312 Date: Sun, 17 Dec 2023 01:35:28 +0530 Subject: [PATCH 1/2] frontend changes --- .../modules/User/components/CollectionShareButton.jsx | 10 +++++++++- client/styles/components/_collection.scss | 1 + translations/locales/en-US/translations.json | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/client/modules/User/components/CollectionShareButton.jsx b/client/modules/User/components/CollectionShareButton.jsx index c4fd06bcb6..fd513bd796 100644 --- a/client/modules/User/components/CollectionShareButton.jsx +++ b/client/modules/User/components/CollectionShareButton.jsx @@ -6,13 +6,18 @@ import Button from '../../../common/Button'; import { DropdownArrowIcon } from '../../../common/icons'; import useModalClose from '../../../common/useModalClose'; import CopyableInput from '../../IDE/components/CopyableInput'; +import { exportCollectionAsZip } from '../../IDE/actions/project'; const ShareURL = ({ value }) => { const [showURL, setShowURL] = useState(false); const { t } = useTranslation(); const close = useCallback(() => setShowURL(false), [setShowURL]); const ref = useModalClose(close); - + function downloadCollection() { + const urlArray = value.split('/'); + const collectionId = urlArray[urlArray.length - 1]; + exportCollectionAsZip(collectionId); + } return (
)} diff --git a/client/styles/components/_collection.scss b/client/styles/components/_collection.scss index a616154ba5..d09b155903 100644 --- a/client/styles/components/_collection.scss +++ b/client/styles/components/_collection.scss @@ -104,6 +104,7 @@ @extend %dropdown-open-right; padding: #{20 / $base-font-size}rem; width: #{350 / $base-font-size}rem; + gap: 1rem; } .collection-content { diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json index a3dde037de..344e576169 100644 --- a/translations/locales/en-US/translations.json +++ b/translations/locales/en-US/translations.json @@ -434,6 +434,7 @@ "AnothersTitle": "p5.js Web Editor | {{anotheruser}}'s collections", "Share": "Share", "URLLink": "Link to Collection", + "Download": "Download Collection", "AddSketch": "Add Sketch", "DeleteFromCollection": "Are you sure you want to remove {{name_sketch}} from this collection?", "SketchDeleted": "Sketch deleted", From 493458c3b4d05d924260ef4b975036ec24bac931 Mon Sep 17 00:00:00 2001 From: mhsh312 Date: Sun, 17 Dec 2023 01:36:36 +0530 Subject: [PATCH 2/2] backend changes --- client/modules/IDE/actions/project.js | 8 ++ .../downloadCollection.js | 104 ++++++++++++++++++ .../collection.controller/index.js | 1 + server/controllers/project.controller.js | 2 +- server/routes/collection.routes.js | 2 +- 5 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 server/controllers/collection.controller/downloadCollection.js diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js index 1d8943336b..2d3164b174 100644 --- a/client/modules/IDE/actions/project.js +++ b/client/modules/IDE/actions/project.js @@ -262,6 +262,14 @@ export function exportProjectAsZip(projectId) { win.focus(); } +export function exportCollectionAsZip(collectionId) { + const win = window.open( + `${ROOT_URL}/downloadCollection/${collectionId}`, + '_blank' + ); + win.focus(); +} + export function resetProject() { return { type: ActionTypes.RESET_PROJECT diff --git a/server/controllers/collection.controller/downloadCollection.js b/server/controllers/collection.controller/downloadCollection.js new file mode 100644 index 0000000000..4b9202aee2 --- /dev/null +++ b/server/controllers/collection.controller/downloadCollection.js @@ -0,0 +1,104 @@ +import JSZip from 'jszip'; +import format from 'date-fns/format'; +import isUrl from 'is-url'; +import { JSDOM } from 'jsdom'; +import slugify from 'slugify'; +import { addFileToZip } from '../project.controller'; +import Collection from '../../models/collection'; +import generateFileSystemSafeName from '../../utils/generateFileSystemSafeName'; + +function formatCollection(collection) { + const { items, name, owner } = collection; + const folder = { + name: 'root', + fileType: 'folder', + children: [] + }; + + const formattedCollection = { + name, + owner: owner.username, + files: [folder] + }; + + items.forEach((item) => { + const { project } = item; + const rootFile = project.files.find((file) => file.name === 'root'); + rootFile.name = project.name; + formattedCollection.files.push(...project.files); + folder.children.push(rootFile.id); + }); + + return formattedCollection; +} + +function bundleExternalLibsForCollection(collection) { + const { files } = collection; + const htmlFiles = []; + const projectFolders = []; + files.forEach((file) => { + if (file.name === 'index.html') htmlFiles.push(file); + else if (file.fileType === 'folder' && file.name !== 'root') + projectFolders.push(file); + }); + htmlFiles.forEach((indexHtml) => { + const { window } = new JSDOM(indexHtml.content); + const scriptTags = window.document.getElementsByTagName('script'); + + const parentFolder = projectFolders.filter((folder) => + folder.children.includes(indexHtml.id) + ); + + Object.values(scriptTags).forEach(async ({ src }, i) => { + if (!isUrl(src)) return; + + const path = src.split('/'); + const filename = path[path.length - 1]; + const libId = `${filename}-${parentFolder[0].name}`; + + collection.files.push({ + name: filename, + url: src, + id: libId + }); + + parentFolder[0].children.push(libId); + }); + }); +} + +async function buildCollectionZip(collection, req, res) { + try { + const zip = new JSZip(); + const currentTime = format(new Date(), 'yyyy_MM_dd_HH_mm_ss'); + collection.slug = slugify(`${collection.name} by ${collection.owner}`, '_'); + const zipFileName = `${generateFileSystemSafeName( + collection.slug + )}_${currentTime}.zip`; + const { files } = collection; + const root = files.find((file) => file.name === 'root'); + + bundleExternalLibsForCollection(collection); + await addFileToZip(root, files, zip); + + const base64 = await zip.generateAsync({ type: 'base64' }); + const buff = Buffer.from(base64, 'base64'); + res.writeHead(200, { + 'Content-Type': 'application/zip', + 'Content-disposition': `attachment; filename=${zipFileName}` + }); + res.end(buff); + } catch (err) { + console.error(err); + res.status(500).send(err); + } +} + +export default async function downloadCollection(req, res) { + const { id } = req.params; + const collection = await Collection.findById(id) + .populate('items.project') + .populate('owner'); + const formattedCollection = formatCollection(collection); + buildCollectionZip(formattedCollection, req, res); +} diff --git a/server/controllers/collection.controller/index.js b/server/controllers/collection.controller/index.js index 8cb9e368d7..90f0538354 100644 --- a/server/controllers/collection.controller/index.js +++ b/server/controllers/collection.controller/index.js @@ -5,3 +5,4 @@ export { default as listCollections } from './listCollections'; export { default as removeCollection } from './removeCollection'; export { default as removeProjectFromCollection } from './removeProjectFromCollection'; export { default as updateCollection } from './updateCollection'; +export { default as downloadCollection } from './downloadCollection'; diff --git a/server/controllers/project.controller.js b/server/controllers/project.controller.js index fd48b7558d..e658bed9fb 100644 --- a/server/controllers/project.controller.js +++ b/server/controllers/project.controller.js @@ -215,7 +215,7 @@ function bundleExternalLibs(project) { }); } -function addFileToZip(file, files, zip, path = '') { +export function addFileToZip(file, files, zip, path = '') { return new Promise((resolve, reject) => { if (file.fileType === 'folder') { const newPath = file.name === 'root' ? path : `${path}${file.name}/`; diff --git a/server/routes/collection.routes.js b/server/routes/collection.routes.js index 31316d6f5b..297557a0fd 100644 --- a/server/routes/collection.routes.js +++ b/server/routes/collection.routes.js @@ -40,5 +40,5 @@ router.delete( isAuthenticated, CollectionController.removeProjectFromCollection ); - +router.get('/downloadCollection/:id', CollectionController.downloadCollection); export default router;