Skip to content

Download collection as a zip #2735

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

Closed
wants to merge 3 commits into from
Closed
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
8 changes: 8 additions & 0 deletions client/modules/IDE/actions/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion client/modules/User/components/CollectionShareButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="collection-share" ref={ref}>
<Button
Expand All @@ -24,6 +29,9 @@ const ShareURL = ({ value }) => {
{showURL && (
<div className="collection__share-dropdown">
<CopyableInput value={value} label={t('Collection.URLLink')} />
<Button onClick={downloadCollection}>
{t('Collection.Download')}
</Button>
</div>
)}
</div>
Expand Down
1 change: 1 addition & 0 deletions client/styles/components/_collection.scss
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
@extend %dropdown-open-right;
padding: #{20 / $base-font-size}rem;
width: #{350 / $base-font-size}rem;
gap: 1rem;
}

.collection-content {
Expand Down
104 changes: 104 additions & 0 deletions server/controllers/collection.controller/downloadCollection.js
Original file line number Diff line number Diff line change
@@ -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);
}
1 change: 1 addition & 0 deletions server/controllers/collection.controller/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
2 changes: 1 addition & 1 deletion server/controllers/project.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}/`;
Expand Down
2 changes: 1 addition & 1 deletion server/routes/collection.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,5 @@ router.delete(
isAuthenticated,
CollectionController.removeProjectFromCollection
);

router.get('/downloadCollection/:id', CollectionController.downloadCollection);
export default router;
1 change: 1 addition & 0 deletions translations/locales/en-US/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down