p5.js-web-editor/server/controllers/project.controller.js

266 lines
8.3 KiB
JavaScript
Raw Permalink Normal View History

import archiver from 'archiver';
2019-03-02 10:35:40 +01:00
import format from 'date-fns/format';
import isUrl from 'is-url';
import jsdom, { serializeDocument } from 'jsdom';
import isAfter from 'date-fns/is_after';
2019-03-02 10:35:40 +01:00
import request from 'request';
import slugify from 'slugify';
import Project from '../models/project';
import User from '../models/user';
2019-03-02 10:35:40 +01:00
import { resolvePathToFile } from '../utils/filePath';
import generateFileSystemSafeName from '../utils/generateFileSystemSafeName';
import mime from 'mime-types';
2016-06-17 20:11:52 +02:00
Public API: Create new project (fixes #1095) (#1106) * Converts import script to use public API endpoints The endpoints don't exist yet, but this is a good way to see how the implementation of the data structures differ. * Exposes public API endpoint to fetch user's sketches * Implements public API delete endpoint * Adds helper to create custom ApplicationError classes * Adds create project endpoint that understand API's data structure This transforms the nested tree of file data into a mongoose Project model * Returns '201 Created' to match API spec * Removes 'CustomError' variable assignment as it shows up in test output * transformFiles will return file validation errors * Tests API project controller * Tests toModel() * Creates default files if no root-level .html file is provided * Do not auto-generate a slug if it is provided Fixes a bug where the slug was auto-generated using the sketch name, even if a slug property had been provided. * Validates uniqueness of slugs for projects created by the public API * Adds tests for slug uniqueness * Configures node's Promise implementation for mongoose (fixes warnings) * Moves createProject tests to match controller location * Adds support for code to ApplicationErrors * deleteProject controller tests * getProjectsForUser controller tests - implements tests - update apiKey tests to use new User mocks * Ensure error objects have consistent property names `message` is used as a high-level description of the errors `detail` is optional and has an plain language explanation of the individual errors `errors` is an array of each individual problem from `detail` in a machine-readable format * Assert environment variables are provided at script start * Version public API * Expect "files" property to always be provided * Fixes linting error * Converts import script to use public API endpoints The endpoints don't exist yet, but this is a good way to see how the implementation of the data structures differ. * Exposes public API endpoint to fetch user's sketches * Implements public API delete endpoint * Adds helper to create custom ApplicationError classes * Adds create project endpoint that understand API's data structure This transforms the nested tree of file data into a mongoose Project model * Returns '201 Created' to match API spec * Removes 'CustomError' variable assignment as it shows up in test output * transformFiles will return file validation errors * Tests API project controller * Tests toModel() * Creates default files if no root-level .html file is provided * Do not auto-generate a slug if it is provided Fixes a bug where the slug was auto-generated using the sketch name, even if a slug property had been provided. * Validates uniqueness of slugs for projects created by the public API * Adds tests for slug uniqueness * Configures node's Promise implementation for mongoose (fixes warnings) * Moves createProject tests to match controller location * deleteProject controller tests * Adds support for code to ApplicationErrors * getProjectsForUser controller tests - implements tests - update apiKey tests to use new User mocks * Ensure error objects have consistent property names `message` is used as a high-level description of the errors `detail` is optional and has an plain language explanation of the individual errors `errors` is an array of each individual problem from `detail` in a machine-readable format * Assert environment variables are provided at script start * Version public API * Expect "files" property to always be provided * Fixes linting error * Checks that authenticated user has permission to create under this namespace Previously, the project was always created under the authenticated user's namespace, but this not obvious behaviour.
2019-08-30 20:26:57 +02:00
export { default as createProject, apiCreateProject } from './project.controller/createProject';
export { default as deleteProject } from './project.controller/deleteProject';
export { default as getProjectsForUser, apiGetProjectsForUser } from './project.controller/getProjectsForUser';
2016-06-19 00:33:49 +02:00
export function updateProject(req, res) {
Project.findById(req.params.project_id, (findProjectErr, project) => {
if (!project.user.equals(req.user._id)) {
res.status(403).send({ success: false, message: 'Session does not match owner of project.' });
return;
}
if (req.body.updatedAt && isAfter(new Date(project.updatedAt), req.body.updatedAt)) {
res.status(409).send({ success: false, message: 'Attempted to save stale version of project.' });
return;
}
2018-05-05 02:22:39 +02:00
Project.findByIdAndUpdate(
req.params.project_id,
{
$set: req.body
},
{
new: true,
runValidators: true
2018-05-05 02:22:39 +02:00
}
)
.populate('user', 'username')
.exec((updateProjectErr, updatedProject) => {
if (updateProjectErr) {
console.log(updateProjectErr);
res.status(400).json({ success: false });
return;
}
Update sketch list styling (#819) * parent b3c3efcec96b5e5bb4e00be742e8f17a025db409 author Laksh Singla <lakshsingla@gmail.com> 1549106083 +0530 committer Cassie Tarakajian <ctarakajian@gmail.com> 1560540243 -0400 parent b3c3efcec96b5e5bb4e00be742e8f17a025db409 author Laksh Singla <lakshsingla@gmail.com> 1549106083 +0530 committer Cassie Tarakajian <ctarakajian@gmail.com> 1560540198 -0400 parent b3c3efcec96b5e5bb4e00be742e8f17a025db409 author Laksh Singla <lakshsingla@gmail.com> 1549106083 +0530 committer Cassie Tarakajian <ctarakajian@gmail.com> 1560539667 -0400 Created initial html structure and styling for new SketchList design Final styling of ActionDialogueBox commplete Dropdown menu disappearing while clicking anywhere on the table Fixed linting issues and renamed variables Minor tweaks in the SketchList dropdown dialogue UI Themifyed the dropdown Made changes in the dropdown: Arrow positioned slightly updwards, Removed blank space and added box-shadow in dropdown, themifyed dropdowns dashed border color Added Delete and Share functionality to Dialog box Added Duplicate functionality to Dialog box Added download functionality to Dialog box SketchList does not open a sketch if dialogue box is opened SketchList Rename initial UI completed Enter key handled for rename project option [WIP] Updating rename functionality Download option now working for all the sketches Duplicate functionality extended for non opened sketches too Modified overlay behaviour to close only the last overlay Share modal can now display different projects Dropdown closes when Share and Delete are closing for a more natural UX fix broken files from rebasing Created initial html structure and styling for new SketchList design Final styling of ActionDialogueBox commplete Added Delete and Share functionality to Dialog box Added Duplicate functionality to Dialog box [WIP] Updating rename functionality Duplicate functionality extended for non opened sketches too Modified overlay behaviour to close only the last overlay Share modal can now display different projects Final styling of ActionDialogueBox commplete Fixed linting issues and renamed variables Minor tweaks in the SketchList dropdown dialogue UI Themifyed the dropdown Added Delete and Share functionality to Dialog box [WIP] Updating rename functionality Modified overlay behaviour to close only the last overlay Share modal can now display different projects Dropdown closes when Share and Delete are closing for a more natural UX fix broken files from rebasing Final styling of ActionDialogueBox commplete Minor tweaks in the SketchList dropdown dialogue UI Themifyed the dropdown [WIP] Updating rename functionality Duplicate functionality extended for non opened sketches too Modified overlay behaviour to close only the last overlay Share modal can now display different projects Dropdown closes when Share and Delete are closing for a more natural UX * fix bugs in merge commit * move sketch list dialogue to ul/li * update sketch option dropdown to use dropdown placeholder, remove unused css * major refactor of sketchlist component, fix showShareModal action, minor updates ot icon sizing * fix broken links on asset list * remove unused image, fix options for different users in sketch list
2019-06-19 22:21:25 +02:00
if (req.body.files && updatedProject.files.length !== req.body.files.length) {
const oldFileIds = updatedProject.files.map(file => file.id);
const newFileIds = req.body.files.map(file => file.id);
const staleIds = oldFileIds.filter(id => newFileIds.indexOf(id) === -1);
staleIds.forEach((staleId) => {
updatedProject.files.id(staleId).remove();
});
updatedProject.save((innerErr, savedProject) => {
if (innerErr) {
console.log(innerErr);
res.status(400).json({ success: false });
return;
}
res.json(savedProject);
});
} else {
res.json(updatedProject);
}
});
});
2016-06-19 00:33:49 +02:00
}
export function getProject(req, res) {
const { project_id: projectId, username } = req.params;
User.findByUsername(username, (err, user) => { // eslint-disable-line
if (!user) {
return res.status(404).send({ message: 'Project with that username does not exist' });
}
Project.findOne({ user: user._id, $or: [{ _id: projectId }, { slug: projectId }] })
.populate('user', 'username')
.exec((err, project) => { // eslint-disable-line
if (err) {
console.log(err);
return res.status(404).send({ message: 'Project with that id does not exist' });
}
2018-02-07 22:00:09 +01:00
return res.json(project);
});
});
2016-06-24 00:29:55 +02:00
}
2016-07-01 17:30:40 +02:00
export function getProjectsForUserId(userId) {
return new Promise((resolve, reject) => {
Project.find({ user: userId })
2016-07-01 17:30:40 +02:00
.sort('-createdAt')
.select('name files id createdAt updatedAt')
2016-07-01 17:30:40 +02:00
.exec((err, projects) => {
if (err) {
console.log(err);
}
resolve(projects);
});
});
}
export function getProjectAsset(req, res) {
Project.findById(req.params.project_id)
.populate('user', 'username')
2017-10-16 05:27:05 +02:00
.exec((err, project) => { // eslint-disable-line
if (err) {
return res.status(404).send({ message: 'Project with that id does not exist' });
}
2017-11-27 21:14:50 +01:00
if (!project) {
return res.status(404).send({ message: 'Project with that id does not exist' });
}
const filePath = req.params[0];
const resolvedFile = resolvePathToFile(filePath, project.files);
if (!resolvedFile) {
return res.status(404).send({ message: 'Asset does not exist' });
}
if (!resolvedFile.url) {
// set the content type based on the filename
const mimetype = mime.lookup(resolvedFile.name);
if(mimetype) {
res.type(mimetype);
}
return res.send(resolvedFile.content);
}
request({ method: 'GET', url: resolvedFile.url, encoding: null }, (innerErr, response, body) => {
2017-10-16 05:27:05 +02:00
if (innerErr) {
return res.status(404).send({ message: 'Asset does not exist' });
}
return res.send(body);
});
});
}
export function getProjects(req, res) {
if (req.user) {
getProjectsForUserId(req.user._id)
.then((projects) => {
2016-07-01 17:30:40 +02:00
res.json(projects);
});
} else {
// could just move this to client side
res.json([]);
2016-07-01 17:30:40 +02:00
}
}
export function projectExists(projectId, callback) {
Project.findById(projectId, (err, project) => (
project ? callback(true) : callback(false)
));
}
export function projectForUserExists(username, projectId, callback) {
User.findByUsername(username, (err, user) => {
if (!user) {
callback(false);
return;
}
Project.findOne({ user: user._id, $or: [{ _id: projectId }, { slug: projectId }] }, (innerErr, project) => {
2018-02-20 20:16:58 +01:00
if (project) {
callback(true);
}
});
});
}
function bundleExternalLibs(project, 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];
2018-05-05 02:59:43 +02:00
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: 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, req, res) {
const zip = archiver('zip');
const rootFile = project.files.find(file => file.name === 'root');
const numFiles = project.files.filter(file => file.fileType !== 'folder').length;
2018-05-05 02:59:43 +02:00
const { files } = project;
let numCompletedFiles = 0;
2017-01-24 23:20:40 +01:00
zip.on('error', (err) => {
res.status(500).send({ error: err.message });
});
2019-03-02 10:35:40 +01:00
const currentTime = format(new Date(), 'YYYY_MM_DD_HH_mm_ss');
project.slug = slugify(project.name, '_');
res.attachment(`${generateFileSystemSafeName(project.slug)}_${currentTime}.zip`);
zip.pipe(res);
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) {
zip.finalize();
}
});
} else {
zip.append(file.content, { name: `${path}${file.name}` });
numCompletedFiles += 1;
if (numCompletedFiles === numFiles) {
zip.finalize();
}
}
}
bundleExternalLibs(project, zip, () => {
addFileToZip(rootFile, '/');
});
}
export function downloadProjectAsZip(req, res) {
Project.findById(req.params.project_id, (err, project) => {
// save project to some path
buildZip(project, req, res);
});
}