Skip to content
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

Fixes export to only allow one zip on the server per user #1080

Merged
merged 8 commits into from
Mar 23, 2018
Merged
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
2 changes: 1 addition & 1 deletion frontend/src/modules/editor/global/views/editorView.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ define(function(require){
// success
var form = document.createElement('form');
this.$el.append(form);
form.setAttribute('action', '/export/' + tenantId + '/' + courseId + '/' + data.zipName + '/download.zip');
form.setAttribute('action', '/export/' + tenantId + '/' + courseId + '/download.zip');
form.submit();
}, this)).fail(_.bind(function(jqXHR, textStatus, errorThrown) {
// failure
Expand Down
21 changes: 20 additions & 1 deletion lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,24 @@ function replaceAll(str, search, replacement) {
return str.split(search).join(replacement);
}

/**
* Returns a slugified string, e.g. for use in a published filename
* Removes non-word/whitespace chars, converts to lowercase and replaces spaces with hyphens
* Multiple arguments are joined with spaces (and therefore hyphenated)
* @return {string}
**/
function slugify() {
var str = Array.apply(null,arguments).join(' ');
var strip_re = /[^\w\s-]/g;
var hyphenate_re = /[-\s]+/g;

str = str.replace(strip_re, '').trim()
str = str.toLowerCase();
str = str.replace(hyphenate_re, '-');

return str;
}

function cloneObject(obj) {
var clone = {};
if(!obj) return clone;
Expand Down Expand Up @@ -227,5 +245,6 @@ exports = module.exports = {
isMasterPreviewAccessible: isMasterPreviewAccessible,
isValidEmail: isValidEmail,
replaceAll: replaceAll,
cloneObject: cloneObject
cloneObject: cloneObject,
slugify: slugify
};
15 changes: 0 additions & 15 deletions lib/outputmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -900,21 +900,6 @@ OutputPlugin.prototype.export = function (courseId, req, res, next) {
throw new Error('OutputPlugin#export must be implemented by extending objects!');
};

/**
* Returns a slugified string, e.g. for use in a published filename
*
* @return {string}
*/
OutputPlugin.prototype.slugify = function(s) {
var _slugify_strip_re = /[^\w\s-]/g;
var _slugify_hyphenate_re = /[-\s]+/g;

s = s.replace(_slugify_strip_re, '').trim().toLowerCase();
s = s.replace(_slugify_hyphenate_re, '-');

return s;
};

/**
* Returns a string with double and single quote characters escaped
*
Expand Down
51 changes: 15 additions & 36 deletions plugins/output/adapt/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var origin = require('../../../'),
assetmanager = require('../../../lib/assetmanager'),
exec = require('child_process').exec,
semver = require('semver'),
helpers = require('../../../lib/helpers'),
installHelpers = require('../../../lib/installHelpers'),
logger = require('../../../lib/logger');

Expand Down Expand Up @@ -223,8 +224,8 @@ AdaptOutput.prototype.publish = function(courseId, mode, request, response, next
function(callback) {
if (mode === Constants.Modes.publish) {
// Now zip the build package
var filename = path.join(COURSE_FOLDER, Constants.Filenames.Download);
var zipName = self.slugify(outputJson['course'].title);
var filename = path.join(FRAMEWORK_ROOT_FOLDER, Constants.Folders.AllCourses, tenantId, courseId, Constants.Filenames.Download);
var zipName = helpers.slugify(outputJson['course'].title);
var output = fs.createWriteStream(filename),
archive = archiver('zip');

Expand Down Expand Up @@ -267,42 +268,19 @@ AdaptOutput.prototype.publish = function(courseId, mode, request, response, next
AdaptOutput.prototype.export = function (courseId, request, response, next) {
var self = this;
var tenantId = usermanager.getCurrentUser().tenant._id;
var timestamp = new Date().toISOString().replace('T', '-').replace(/:/g, '').substr(0,17);
var userId = usermanager.getCurrentUser()._id;

var FRAMEWORK_ROOT_FOLDER = path.join(configuration.tempDir, configuration.getConfig('masterTenantID'), Constants.Folders.Framework);
var COURSE_ROOT_FOLDER = path.join(FRAMEWORK_ROOT_FOLDER, Constants.Folders.AllCourses, tenantId, courseId);

// set in getCourseName
var exportName;
var exportDir;
var exportDir = path.join(FRAMEWORK_ROOT_FOLDER, Constants.Folders.Exports, userId);

var mode = Constants.Modes.export;

async.waterfall([
function publishCourse(callback) {
self.publish(courseId, mode, request, response, callback);
},
function getCourseName(results, callback) {
database.getDatabase(function (error, db) {
if (error) {
return callback(err);
}

db.retrieve('course', { _id: courseId }, { jsonOnly: true }, function (error, results) {
if (error) {
return callback(error);
}
if(!results || results.length > 1) {
return callback(new Error('Unexpected results returned for course ' + courseId + ' (' + results.length + ')', self));
}

exportName = self.slugify(results[0].title) + '-export-' + timestamp;
exportDir = path.join(FRAMEWORK_ROOT_FOLDER, Constants.Folders.Exports, exportName);
callback();
});
});
},
function copyFiles(callback) {
function copyFiles(results, callback) {
self.generateIncludesForCourse(courseId, function(error, includes) {
if(error) {
return callback(error);
Expand Down Expand Up @@ -348,20 +326,21 @@ AdaptOutput.prototype.export = function (courseId, request, response, next) {
},
function zipFiles(callback) {
var archive = archiver('zip');
var output = fs.createWriteStream(exportDir + '.zip');

var zipPath = exportDir + '.zip';
var output = fs.createWriteStream(zipPath);
archive.on('error', callback);
output.on('close', callback);
archive.pipe(output);
archive.bulk([{ expand: true, cwd: exportDir, src: ['**/*'] }]).finalize();
},
function cleanUp(callback) {
fse.remove(exportDir, function (error) {
callback(error, { zipName: exportName + '.zip' });
});
}
],
next);
function onDone(asyncError) {
// remove the exportDir, if there is one
fse.remove(exportDir, function(removeError) {
// async error more important
next(asyncError || removeError);
});
});
};

/**
Expand Down
59 changes: 32 additions & 27 deletions routes/export/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,58 +41,63 @@ server.get('/export/:tenant/:course', function (req, res, next) {
plugin.export(course, req, res, function (error, result) {
if (error) {
logger.log('error', 'Unable to export:', error);
res.statusCode = 500;
return res.json({
return res.status(500).json({
success: false,
message: error.message
});
}

result.success = true;
result.message = app.polyglot.t('app.exportcoursesuccess');
res.statusCode = 200;
return res.json(result);
return res.status(200).json({
success: true,
message: app.polyglot.t('app.exportcoursesuccess')
});
});
}
});
} else {
res.statusCode = 401;
return res.json({
return res.status(401).json({
success: false,
message: app.polyglot.t('app.errorusernoaccess')
});
}
});
});
// TODO probably needs to be moved to download route
Copy link
Contributor

@canstudios-louisem canstudios-louisem Feb 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good time to address this TODO. Were calling it download it should probably be part of the download lib and route.

server.get('/export/:tenant/:course/:title/download.zip', function (req, res, next) {
server.get('/export/:tenant/:course/download.zip', function (req, res, next) {
var tenantId = req.params.tenant;
var courseId = req.params.course;
var userId = usermanager.getCurrentUser()._id;
var zipName = req.params.title;
// TODO don't like having to specify this here AND in plugins/output/adapt.export->getCourseName()-exportDir
var zipDir = path.join(
configuration.tempDir,
configuration.getConfig('masterTenantID'),
Constants.Folders.Framework,
Constants.Folders.Exports,
zipName
userId + '.zip'
);

fs.stat(zipDir, function(err, stat) {
if (err) {
next(err);
} else {
res.writeHead(200, {
'Content-Type': 'application/zip',
'Content-Length': stat.size,
'Content-disposition' : 'attachment; filename=' + zipName,
'Pragma' : 'no-cache',
'Expires' : '0'
// get the course name
app.contentmanager.getContentPlugin('course', function (error, plugin) {
if (error) return callback(error);
plugin.retrieve({ _id:courseId }, {}, function(error, results) {
if (error) {
return callback(error);
}
if (results.length !== 1) {
return callback(new Error('Export: cannot find course (' + courseId + ')'));
}
fs.stat(zipDir, function(error, stat) {
if (error) {
return next(error);
}
var zipName = helpers.slugify(results[0].title,'export') + '.zip';
res.writeHead(200, {
'Content-Type': 'application/zip',
'Content-Length': stat.size,
'Content-disposition' : 'attachment; filename=' + zipName,
'Pragma' : 'no-cache',
'Expires' : '0'
});
fs.createReadStream(zipDir).pipe(res);
});

var readStream = fs.createReadStream(zipDir);
readStream.pipe(res);
}
});
});
});