Add in backend multiple project downloading

This commit is contained in:
James Allen 2014-06-18 16:37:18 +01:00
parent b837a4e9f3
commit 2b349039c3
7 changed files with 176 additions and 6 deletions

View file

@ -13,7 +13,7 @@ rclient.auth(settings.redis.web.password)
Project = require("../../models/Project").Project Project = require("../../models/Project").Project
ProjectLocator = require('../../Features/Project/ProjectLocator') ProjectLocator = require('../../Features/Project/ProjectLocator')
module.exports = module.exports = DocumentUpdaterHandler =
queueChange : (project_id, doc_id, change, sl_req_id, callback = ()->)-> queueChange : (project_id, doc_id, change, sl_req_id, callback = ()->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
@ -42,6 +42,14 @@ module.exports =
logger.error err: error, project_id: project_id, sl_req_id: sl_req_id, "document updater returned failure status code: #{res.statusCode}" logger.error err: error, project_id: project_id, sl_req_id: sl_req_id, "document updater returned failure status code: #{res.statusCode}"
return callback(error) return callback(error)
flushMultipleProjectsToMongo: (project_ids, callback = (error) ->) ->
jobs = []
for project_id in project_ids
do (project_id) ->
jobs.push (callback) ->
DocumentUpdaterHandler.flushProjectToMongo project_id, callback
async.series jobs, callback
flushProjectToMongoAndDelete: (project_id, sl_req_id, callback = ()->) -> flushProjectToMongoAndDelete: (project_id, sl_req_id, callback = ()->) ->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id) {callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
logger.log project_id:project_id, sl_req_id:sl_req_id, "deleting project from document updater" logger.log project_id:project_id, sl_req_id:sl_req_id, "deleting project from document updater"

View file

@ -22,4 +22,19 @@ module.exports = ProjectDownloadsController =
res.contentType('application/zip') res.contentType('application/zip')
stream.pipe(res) stream.pipe(res)
downloadMultipleProjects: (req, res, next) ->
project_ids = req.query.project_ids.split(",")
Metrics.inc "zip-downloads-multiple"
logger.log project_ids: project_ids, "downloading multiple projects"
DocumentUpdaterHandler.flushMultipleProjectsToMongo project_ids, (error) ->
return next(error) if error?
ProjectZipStreamManager.createZipStreamForMultipleProjects project_ids, (error, stream) ->
return next(error) if error?
res.header(
"Content-Disposition",
"attachment; filename=ShareLaTeX Projects (#{project_ids.length} items).zip"
)
res.contentType('application/zip')
stream.pipe(res)

View file

@ -3,8 +3,37 @@ async = require "async"
logger = require "logger-sharelatex" logger = require "logger-sharelatex"
ProjectEntityHandler = require "../Project/ProjectEntityHandler" ProjectEntityHandler = require "../Project/ProjectEntityHandler"
FileStoreHandler = require("../FileStore/FileStoreHandler") FileStoreHandler = require("../FileStore/FileStoreHandler")
Project = require("../../models/Project").Project
module.exports = ProjectZipStreamManager = module.exports = ProjectZipStreamManager =
createZipStreamForMultipleProjects: (project_ids, callback = (error, stream) ->) ->
# We'll build up a zip file that contains multiple zip files
archive = archiver("zip")
archive.on "error", (err)->
logger.err err:err, project_ids:project_ids, "something went wrong building archive of project"
callback null, archive
logger.log project_ids: project_ids, "creating zip stream of multiple projects"
jobs = []
for project_id in project_ids or []
do (project_id) ->
jobs.push (callback) ->
Project.findById project_id, "name", (error, project) ->
return callback(error) if error?
logger.log project_id: project_id, name: project.name, "appending project to zip stream"
ProjectZipStreamManager.createZipStreamForProject project_id, (error, stream) ->
return callback(error) if error?
archive.append stream, name: "#{project.name}.zip"
stream.on "end", () ->
logger.log project_id: project_id, name: project.name, "zip stream ended"
callback()
async.series jobs, () ->
logger.log project_ids: project_ids, "finished creating zip stream of multiple projects"
archive.finalize()
createZipStreamForProject: (project_id, callback = (error, stream) ->) -> createZipStreamForProject: (project_id, callback = (error, stream) ->) ->
archive = archiver("zip") archive = archiver("zip")
# return stream immediately before we start adding things to it # return stream immediately before we start adding things to it

View file

@ -9,8 +9,9 @@ AuthenticationController = require("../Features/Authentication/AuthenticationCon
_ = require('underscore') _ = require('underscore')
metrics = require('../infrastructure/Metrics') metrics = require('../infrastructure/Metrics')
querystring = require('querystring') querystring = require('querystring')
async = require "async"
module.exports = module.exports = SecurityManager =
restricted : (req, res, next)-> restricted : (req, res, next)->
if req.session.user? if req.session.user?
res.render 'user/restricted', res.render 'user/restricted',
@ -25,6 +26,21 @@ module.exports =
else else
callback null, null callback null, null
requestCanAccessMultipleProjects: (req, res, next) ->
project_ids = req.query.project_ids?.split(",")
jobs = []
for project_id in project_ids or []
do (project_id) ->
jobs.push (callback) ->
# This is a bit hacky - better to have an abstracted method
# that we can pass project_id to, but this whole file needs
# a serious refactor ATM.
req.params.Project_id = project_id
SecurityManager.requestCanAccessProject req, res, (error) ->
delete req.params.Project_id
callback(error)
async.series jobs, next
requestCanAccessProject : (req, res, next)-> requestCanAccessProject : (req, res, next)->
doRequest = (req, res, next) -> doRequest = (req, res, next) ->
getRequestUserAndProject req, res, {allow_auth_token: options?.allow_auth_token}, (err, user, project)-> getRequestUserAndProject req, res, {allow_auth_token: options?.allow_auth_token}, (err, user, project)->

View file

@ -133,7 +133,7 @@ module.exports = class Router
app.get '/project/:Project_id/collaborators', SecurityManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators app.get '/project/:Project_id/collaborators', SecurityManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators
app.get '/Project/:Project_id/download/zip', SecurityManager.requestCanAccessProject, ProjectDownloadsController.downloadProject app.get '/Project/:Project_id/download/zip', SecurityManager.requestCanAccessProject, ProjectDownloadsController.downloadProject
app.get '/project/download/zip', SecurityManager.requestCanAccessMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
app.get '/tag', AuthenticationController.requireLogin(), TagsController.getAllTags app.get '/tag', AuthenticationController.requireLogin(), TagsController.getAllTags
app.post '/project/:project_id/tag', AuthenticationController.requireLogin(), TagsController.processTagsUpdate app.post '/project/:project_id/tag', AuthenticationController.requireLogin(), TagsController.processTagsUpdate

View file

@ -73,3 +73,50 @@ describe "ProjectDownloadsController", ->
.calledWith(sinon.match.any, "downloading project") .calledWith(sinon.match.any, "downloading project")
.should.equal true .should.equal true
describe "downloadMultipleProjects", ->
beforeEach ->
@stream =
pipe: sinon.stub()
@ProjectZipStreamManager.createZipStreamForMultipleProjects =
sinon.stub().callsArgWith(1, null, @stream)
@project_ids = ["project-1", "project-2"]
@req.query = project_ids: @project_ids.join(",")
@res.contentType = sinon.stub()
@res.header = sinon.stub()
@DocumentUpdaterHandler.flushMultipleProjectsToMongo = sinon.stub().callsArgWith(1)
@metrics.inc = sinon.stub()
@ProjectDownloadsController.downloadMultipleProjects @req, @res, @next
it "should create a zip from the project", ->
@ProjectZipStreamManager.createZipStreamForMultipleProjects
.calledWith(@project_ids)
.should.equal true
it "should stream the zip to the request", ->
@stream.pipe.calledWith(@res)
.should.equal true
it "should set the correct content type on the request", ->
@res.contentType
.calledWith("application/zip")
.should.equal true
it "should flush the projects to mongo", ->
@DocumentUpdaterHandler.flushMultipleProjectsToMongo
.calledWith(@project_ids)
.should.equal true
it "should name the downloaded file after the project", ->
@res.header
.calledWith(
"Content-Disposition",
"attachment; filename=ShareLaTeX Projects (2 items).zip")
.should.equal true
it "should record the action via Metrics", ->
@metrics.inc.calledWith("zip-downloads-multiple").should.equal true
it "should log the action", ->
@logger.log
.calledWith(sinon.match.any, "downloading multiple projects")
.should.equal true

View file

@ -12,11 +12,68 @@ describe "ProjectZipStreamManager", ->
@callback = sinon.stub() @callback = sinon.stub()
@archive = @archive =
on:-> on:->
append: sinon.stub()
@ProjectZipStreamManager = SandboxedModule.require modulePath, requires: @ProjectZipStreamManager = SandboxedModule.require modulePath, requires:
"archiver": @archiver = sinon.stub().returns @archive "archiver": @archiver = sinon.stub().returns @archive
"logger-sharelatex": @logger = {error: sinon.stub(), log: sinon.stub()} "logger-sharelatex": @logger = {error: sinon.stub(), log: sinon.stub()}
"../Project/ProjectEntityHandler" : @ProjectEntityHandler = {} "../Project/ProjectEntityHandler" : @ProjectEntityHandler = {}
"../FileStore/FileStoreHandler": @FileStoreHandler = {} "../FileStore/FileStoreHandler": @FileStoreHandler = {}
"../../models/Project": Project: @Project = {}
describe "createZipStreamForMultipleProjects", ->
describe "successfully", ->
beforeEach (done) ->
@project_ids = ["project-1", "project-2"]
@zip_streams =
"project-1": new EventEmitter()
"project-2": new EventEmitter()
@project_names =
"project-1": "Project One Name"
"project-2": "Project Two Name"
@ProjectZipStreamManager.createZipStreamForProject = (project_id, callback) =>
callback null, @zip_streams[project_id]
setTimeout () =>
@zip_streams[project_id].emit "end",
0
sinon.spy @ProjectZipStreamManager, "createZipStreamForProject"
@Project.findById = (project_id, fields, callback) =>
callback null, name: @project_names[project_id]
sinon.spy @Project, "findById"
@ProjectZipStreamManager.createZipStreamForMultipleProjects @project_ids, (args...) =>
@callback args...
@archive.finalize = () ->
done()
it "should create a zip archive", ->
@archiver.calledWith("zip").should.equal true
it "should return a stream before any processing is done", ->
@callback.calledWith(sinon.match.falsy, @archive).should.equal true
@callback.calledBefore(@ProjectZipStreamManager.createZipStreamForProject).should.equal true
it "should get a zip stream for all of the projects", ->
for project_id in @project_ids
@ProjectZipStreamManager.createZipStreamForProject
.calledWith(project_id)
.should.equal true
it "should get the names of each project", ->
for project_id in @project_ids
@Project.findById
.calledWith(project_id, "name")
.should.equal true
it "should add all of the projects to the zip", ->
for project_id in @project_ids
@archive.append
.calledWith(@zip_streams[project_id], name: @project_names[project_id] + ".zip")
.should.equal true
describe "createZipStreamForProject", -> describe "createZipStreamForProject", ->
describe "successfully", -> describe "successfully", ->
@ -89,7 +146,6 @@ describe "ProjectZipStreamManager", ->
"/chapters/chapter1.tex": "/chapters/chapter1.tex":
lines: ["chapter1", "content"] lines: ["chapter1", "content"]
@ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs) @ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs)
@archive.append = sinon.stub()
@ProjectZipStreamManager.addAllDocsToArchive @project_id, @archive, (error) => @ProjectZipStreamManager.addAllDocsToArchive @project_id, @archive, (error) =>
@callback(error) @callback(error)
done() done()
@ -116,7 +172,6 @@ describe "ProjectZipStreamManager", ->
"file-id-1" : new EventEmitter() "file-id-1" : new EventEmitter()
"file-id-2" : new EventEmitter() "file-id-2" : new EventEmitter()
@ProjectEntityHandler.getAllFiles = sinon.stub().callsArgWith(1, null, @files) @ProjectEntityHandler.getAllFiles = sinon.stub().callsArgWith(1, null, @files)
@archive.append = sinon.stub()
@FileStoreHandler.getFileStream = (project_id, file_id, {}, callback) => @FileStoreHandler.getFileStream = (project_id, file_id, {}, callback) =>
callback null, @streams[file_id] callback null, @streams[file_id]
sinon.spy @FileStoreHandler, "getFileStream" sinon.spy @FileStoreHandler, "getFileStream"