Skip to content

Feature :- Download Collection #1691

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
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
33 changes: 28 additions & 5 deletions client/modules/User/components/Collection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,23 @@ import ArrowUpIcon from '../../../images/sort-arrow-up.svg';
import ArrowDownIcon from '../../../images/sort-arrow-down.svg';
import RemoveIcon from '../../../images/close.svg';

const ShareURL = ({ value, t }) => {
const ShareURL = ({
host, username, id, hasItems, t
}) => {
const [showURL, setShowURL] = useState(false);
const node = useRef();
const linkToCollection = `${host}/${username}/collections/${id}`;
const linkToDownload = `${host}/editor/${username}/collections/${id}/zip`;

const downloadCollection = () => {
if (!hasItems) {
alert(t('Collection.DownloadError'));
return;
}
const win = window.open(linkToDownload, '_blank');
win.focus();
setShowURL(false);
};

const handleClickOutside = (e) => {
if (node.current.contains(e.target)) {
Expand Down Expand Up @@ -61,15 +75,24 @@ const ShareURL = ({ value, t }) => {
</Button>
{showURL && (
<div className="collection__share-dropdown">
<CopyableInput value={value} label={t('Collection.URLLink')} />
<CopyableInput value={linkToCollection} label={t('Collection.URLLink')} />
<br />
<CopyableInput value={linkToDownload} label={t('Collection.DownloadLink')} />
<br />
<Button onClick={downloadCollection}>
{t('Collection.Download')}
</Button>
</div>
)}
</div>
);
};

ShareURL.propTypes = {
value: PropTypes.string.isRequired,
host: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
hasItems: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired
};

Expand Down Expand Up @@ -232,7 +255,7 @@ class Collection extends React.Component {
const hostname = window.location.origin;
const { username } = this.props;

const baseURL = `${hostname}/${username}/collections/`;
// const baseURL = `${hostname}/${username}/collections/`;

const handleEditCollectionName = (value) => {
if (value === name) {
Expand Down Expand Up @@ -310,7 +333,7 @@ class Collection extends React.Component {

<div className="collection-metadata__column--right">
<p className="collection-metadata__share">
<ShareURL value={`${baseURL}${id}`} t={this.props.t} />
<ShareURL host={hostname} username={username} id={id} hasItems={this.props.collection.items.length > 0} t={this.props.t} />
</p>
{this.isOwner() && (
<Button onClick={this.showAddSketches}>
Expand Down
173 changes: 173 additions & 0 deletions server/controllers/collection.controller/downloadCollectionAsZip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import archiver from 'archiver';
import format from 'date-fns/format';
import isUrl from 'is-url';
import jsdom, { serializeDocument } from 'jsdom';
import request from 'request';
import slugify from 'slugify';

import generateFileSystemSafeName from '../../utils/generateFileSystemSafeName';
import Collection from '../../models/collection';
import User from '../../models/user';


async function getOwnerUserId(req) {
if (req.params.username) {
const user =
await User.findByUsername(req.params.username);
if (user && user._id) {
return user._id;
}
} else if (req.user._id) {
return req.user._id;
}

return null;
}

function bundleExternalLibs(project, projectFolderName, zip, callback) {
const indexHtml = project.files.find(file => file.name.match(/\.html$/));
let numScriptsResolved = 0;
let numScriptTags = 0;

function resolveScriptTagSrc(scriptTag, document) {
const path = scriptTag.src.split('/');
const filename = path[path.length - 1];
const { src } = scriptTag;

if (!isUrl(src)) {
numScriptsResolved += 1;
if (numScriptsResolved === numScriptTags) {
indexHtml.content = serializeDocument(document);
callback();
}
return;
}

request({ method: 'GET', url: src, encoding: null }, (err, response, body) => {
if (err) {
console.log(err);
} else {
zip.append(body, { name: `/${projectFolderName}/${filename}` });
scriptTag.src = filename;
}

numScriptsResolved += 1;
if (numScriptsResolved === numScriptTags) {
indexHtml.content = serializeDocument(document);
callback();
}
});
}

jsdom.env(indexHtml.content, (innerErr, window) => {
const indexHtmlDoc = window.document;
const scriptTags = indexHtmlDoc.getElementsByTagName('script');
numScriptTags = scriptTags.length;
for (let i = 0; i < numScriptTags; i += 1) {
resolveScriptTagSrc(scriptTags[i], indexHtmlDoc);
}
if (numScriptTags === 0) {
indexHtml.content = serializeDocument(document);
callback();
}
});
}

function buildZip(project, zip, callback) {
const rootFile = project.files.find(file => file.name === 'root');
const numFiles = project.files.filter(file => file.fileType !== 'folder').length;
const { files } = project;
let numCompletedFiles = 0;

const currentTime = format(new Date(), 'yyyy_MM_dd_HH_mm_ss');
project.slug = slugify(project.name, '_');
const projectFolderName = `${generateFileSystemSafeName(project.slug)}_${currentTime}`;


function addFileToZip(file, path) {
if (file.fileType === 'folder') {
const newPath = file.name === 'root' ? path : `${path}${file.name}/`;
file.children.forEach((fileId) => {
const childFile = files.find(f => f.id === fileId);
(() => {
addFileToZip(childFile, newPath);
})();
});
} else if (file.url) {
request({ method: 'GET', url: file.url, encoding: null }, (err, response, body) => {
zip.append(body, { name: `${path}${file.name}` });
numCompletedFiles += 1;
if (numCompletedFiles === numFiles) {
callback();
}
});
} else {
zip.append(file.content, { name: `${path}${file.name}` });
numCompletedFiles += 1;
if (numCompletedFiles === numFiles) {
callback();
}
}
}

bundleExternalLibs(project, projectFolderName, zip, () => {
addFileToZip(rootFile, `/${projectFolderName}/`);
});
}

export default function downloadCollectionAsZip(req, res) {
function sendFailure({ code = 500, message = 'Something went wrong' }) {
res.status(code).json({ success: false, message });
}

function findCollection(owner) {
if (owner == null) {
sendFailure({ code: 404, message: 'User not found' });
}

return Collection
.findById(req.params.id)
.populate({ path: 'items.project' })
.then((collection) => {
if (collection.owner.toString() === owner.toString()) {
return collection;
}
return sendFailure({ code: 403, message: 'Not Authorized' });
});
}

function buildCollectionZip(collection) {
if (collection.items.length === 0) {
sendFailure({ code: 500, message: 'Collection is empty' });
return;
}

const zip = archiver('zip');
zip.on('error', (err) => {
console.log(err);
res.status(500).send({ error: err.message });
});

const currentTime = format(new Date(), 'yyyy_MM_dd_HH_mm_ss');
res.attachment(`${generateFileSystemSafeName(collection.slug)}_${currentTime}.zip`);
zip.pipe(res);

let count = 0;
function checkCount() {
count += 1;
if (count === collection.items.length) {
zip.finalize();
}
}

for (let i = 0; i < collection.items.length; i += 1) {
const { project } = collection.items[i];
buildZip(project, zip, checkCount);
}
}

return getOwnerUserId(req)
.then(findCollection)
.then(buildCollectionZip)
.catch(sendFailure);
}
2 changes: 2 additions & 0 deletions server/controllers/collection.controller/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ 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 downloadCollectionAsZip } from './downloadCollectionAsZip';

3 changes: 3 additions & 0 deletions server/routes/collection.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,7 @@ router.delete(
CollectionController.removeProjectFromCollection
);

// Download collection
router.get('/:username/collections/:id/zip', CollectionController.downloadCollectionAsZip);

export default router;
3 changes: 3 additions & 0 deletions translations/locales/en-US/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,9 @@
"AnothersTitle": "p5.js Web Editor | {{anotheruser}}'s collections",
"Share": "Share",
"URLLink": "Link to Collection",
"DownloadLink": "Link to download Collection",
"Download": "Download",
"DownloadError": "The collection is empty! please add a sketch to download the collection.",
"AddSketch": "Add Sketch",
"DeleteFromCollection": "Are you sure you want to remove {{name_sketch}} from this collection?",
"SketchDeleted": "Sketch deleted",
Expand Down
3 changes: 3 additions & 0 deletions translations/locales/es-419/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,9 @@
"AnothersTitle": "Editor Web p5.js | Colecciones de {{anotheruser}}",
"Share": "Compartir",
"URLLink": "Liga a la Colección",
"DownloadLink": "Enlace para descargar Colección",
"Download": "Descargar",
"DownloadError": "¡La colección está vacía! agregue un boceto para descargar la colección.",
"AddSketch": "Agregar Bosquejo",
"DeleteFromCollection": "¿Estás seguro que quieres remover {{name_sketch}} de esta colección?",
"SketchDeleted": "El bosquejo fue eliminado",
Expand Down