2020-05-01 10:00:34 -04:00
|
|
|
const { callbackify, promisify } = require('util')
|
|
|
|
const OError = require('@overleaf/o-error')
|
|
|
|
const ProjectCreationHandler = require('./ProjectCreationHandler')
|
2019-05-29 05:21:06 -04:00
|
|
|
const ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler')
|
2020-05-01 10:00:34 -04:00
|
|
|
const ProjectLocator = require('./ProjectLocator')
|
|
|
|
const ProjectOptionsHandler = require('./ProjectOptionsHandler')
|
|
|
|
const ProjectDeleter = require('./ProjectDeleter')
|
2019-05-29 05:21:06 -04:00
|
|
|
const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
|
|
|
|
const DocstoreManager = require('../Docstore/DocstoreManager')
|
|
|
|
const ProjectGetter = require('./ProjectGetter')
|
|
|
|
const _ = require('underscore')
|
|
|
|
const async = require('async')
|
|
|
|
const logger = require('logger-sharelatex')
|
|
|
|
|
2020-05-01 10:00:34 -04:00
|
|
|
module.exports = {
|
|
|
|
duplicate: callbackify(duplicate),
|
|
|
|
promises: {
|
|
|
|
duplicate
|
|
|
|
}
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2020-05-01 10:00:34 -04:00
|
|
|
function _copyDocs(
|
|
|
|
ownerId,
|
|
|
|
newProject,
|
|
|
|
originalRootDoc,
|
|
|
|
originalFolder,
|
|
|
|
desFolder,
|
|
|
|
docContents,
|
|
|
|
callback
|
|
|
|
) {
|
|
|
|
const setRootDoc = _.once(docId => {
|
|
|
|
ProjectEntityUpdateHandler.setRootDoc(newProject._id, docId, () => {})
|
|
|
|
})
|
|
|
|
const docs = originalFolder.docs || []
|
|
|
|
const jobs = docs.map(
|
|
|
|
doc =>
|
|
|
|
function(cb) {
|
|
|
|
if (doc == null || doc._id == null) {
|
|
|
|
return callback()
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2020-05-01 10:00:34 -04:00
|
|
|
const content = docContents[doc._id.toString()]
|
|
|
|
ProjectEntityUpdateHandler.addDoc(
|
|
|
|
newProject._id,
|
|
|
|
desFolder._id,
|
|
|
|
doc.name,
|
|
|
|
content.lines,
|
|
|
|
ownerId,
|
|
|
|
function(err, newDoc) {
|
|
|
|
if (err != null) {
|
|
|
|
logger.warn({ err }, 'error copying doc')
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
originalRootDoc != null &&
|
|
|
|
newDoc.name === originalRootDoc.name
|
|
|
|
) {
|
|
|
|
setRootDoc(newDoc._id)
|
|
|
|
}
|
|
|
|
cb()
|
|
|
|
}
|
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2020-05-01 10:00:34 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2020-05-01 10:00:34 -04:00
|
|
|
async.series(jobs, callback)
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2020-05-01 10:00:34 -04:00
|
|
|
function _copyFiles(
|
|
|
|
ownerId,
|
|
|
|
newProject,
|
|
|
|
originalProjectId,
|
|
|
|
originalFolder,
|
|
|
|
desFolder,
|
|
|
|
callback
|
|
|
|
) {
|
|
|
|
const fileRefs = originalFolder.fileRefs || []
|
|
|
|
let firstError = null // track first error to exit gracefully from parallel copy
|
|
|
|
const jobs = fileRefs.map(
|
|
|
|
file =>
|
|
|
|
function(cb) {
|
|
|
|
if (firstError != null) {
|
|
|
|
return async.setImmediate(cb)
|
|
|
|
} // skip further copies if an error has occurred
|
|
|
|
ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject(
|
|
|
|
newProject._id,
|
|
|
|
newProject,
|
|
|
|
desFolder._id,
|
|
|
|
originalProjectId,
|
|
|
|
file,
|
|
|
|
ownerId,
|
|
|
|
function(err) {
|
|
|
|
if (err != null) {
|
|
|
|
if (!firstError) {
|
|
|
|
firstError = err
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2020-05-01 10:00:34 -04:00
|
|
|
} // set the error flag if this copy failed
|
|
|
|
cb()
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
2020-05-01 10:00:34 -04:00
|
|
|
}
|
|
|
|
)
|
|
|
|
// If one of these jobs fails then we wait until all running jobs have
|
|
|
|
// finished, skipping those which have not started yet. We need to wait
|
|
|
|
// for all the copy jobs to finish to avoid them writing to the project
|
|
|
|
// entry in the background while we are deleting it.
|
|
|
|
async.parallelLimit(jobs, 5, function(err) {
|
|
|
|
if (firstError != null) {
|
|
|
|
return callback(firstError)
|
|
|
|
}
|
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
|
|
|
} // shouldn't happen
|
|
|
|
callback()
|
|
|
|
})
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2020-05-01 10:00:34 -04:00
|
|
|
function _copyFolderRecursively(
|
|
|
|
ownerId,
|
|
|
|
newProjectId,
|
|
|
|
originalProjectId,
|
|
|
|
originalRootDoc,
|
|
|
|
originalFolder,
|
|
|
|
desFolder,
|
|
|
|
docContents,
|
|
|
|
callback
|
|
|
|
) {
|
|
|
|
ProjectGetter.getProject(
|
|
|
|
newProjectId,
|
|
|
|
{ rootFolder: true, name: true },
|
|
|
|
function(err, newProject) {
|
|
|
|
if (err != null) {
|
|
|
|
logger.warn({ projectId: newProjectId }, 'could not get project')
|
|
|
|
return callback(err)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
|
2020-05-01 10:00:34 -04:00
|
|
|
const folders = originalFolder.folders || []
|
|
|
|
|
|
|
|
const jobs = folders.map(
|
|
|
|
childFolder =>
|
|
|
|
function(cb) {
|
|
|
|
if (childFolder == null || childFolder._id == null) {
|
|
|
|
return cb()
|
|
|
|
}
|
|
|
|
ProjectEntityUpdateHandler.addFolder(
|
|
|
|
newProject._id,
|
|
|
|
desFolder != null ? desFolder._id : undefined,
|
|
|
|
childFolder.name,
|
|
|
|
function(err, newFolder) {
|
|
|
|
if (err != null) {
|
|
|
|
return cb(err)
|
|
|
|
}
|
|
|
|
_copyFolderRecursively(
|
|
|
|
ownerId,
|
|
|
|
newProjectId,
|
|
|
|
originalProjectId,
|
|
|
|
originalRootDoc,
|
|
|
|
childFolder,
|
|
|
|
newFolder,
|
|
|
|
docContents,
|
|
|
|
cb
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
jobs.push(cb =>
|
|
|
|
_copyFiles(
|
|
|
|
ownerId,
|
|
|
|
newProject,
|
|
|
|
originalProjectId,
|
|
|
|
originalFolder,
|
|
|
|
desFolder,
|
2019-05-29 05:21:06 -04:00
|
|
|
cb
|
|
|
|
)
|
2020-05-01 10:00:34 -04:00
|
|
|
)
|
|
|
|
jobs.push(cb =>
|
|
|
|
_copyDocs(
|
|
|
|
ownerId,
|
|
|
|
newProject,
|
|
|
|
originalRootDoc,
|
|
|
|
originalFolder,
|
|
|
|
desFolder,
|
|
|
|
docContents,
|
2019-05-29 05:21:06 -04:00
|
|
|
cb
|
|
|
|
)
|
2020-05-01 10:00:34 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
async.series(jobs, callback)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2020-05-01 10:00:34 -04:00
|
|
|
)
|
|
|
|
}
|
|
|
|
const _copyFolderRecursivelyAsync = promisify(_copyFolderRecursively)
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2020-05-01 10:00:34 -04:00
|
|
|
async function duplicate(owner, originalProjectId, newProjectName) {
|
|
|
|
await DocumentUpdaterHandler.promises.flushProjectToMongo(originalProjectId)
|
|
|
|
const originalProject = await ProjectGetter.promises.getProject(
|
|
|
|
originalProjectId,
|
|
|
|
{
|
|
|
|
compiler: true,
|
|
|
|
rootFolder: true,
|
|
|
|
rootDoc_id: true
|
|
|
|
}
|
|
|
|
)
|
|
|
|
const {
|
|
|
|
element: originalRootDoc
|
|
|
|
} = await ProjectLocator.promises.findRootDoc({
|
|
|
|
project_id: originalProjectId
|
|
|
|
})
|
|
|
|
const docContentsArray = await DocstoreManager.promises.getAllDocs(
|
|
|
|
originalProjectId
|
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2020-05-01 10:00:34 -04:00
|
|
|
const docContents = {}
|
|
|
|
for (const docContent of docContentsArray) {
|
|
|
|
docContents[docContent._id] = docContent
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2020-05-01 10:00:34 -04:00
|
|
|
// Now create the new project, cleaning it up on failure if necessary
|
|
|
|
const newProject = await ProjectCreationHandler.promises.createBlankProject(
|
|
|
|
owner._id,
|
|
|
|
newProjectName
|
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2020-05-01 10:00:34 -04:00
|
|
|
try {
|
|
|
|
await ProjectOptionsHandler.promises.setCompiler(
|
|
|
|
newProject._id,
|
|
|
|
originalProject.compiler
|
|
|
|
)
|
|
|
|
await _copyFolderRecursivelyAsync(
|
|
|
|
owner._id,
|
|
|
|
newProject._id,
|
|
|
|
originalProjectId,
|
|
|
|
originalRootDoc,
|
|
|
|
originalProject.rootFolder[0],
|
|
|
|
newProject.rootFolder[0],
|
|
|
|
docContents
|
|
|
|
)
|
|
|
|
} catch (err) {
|
|
|
|
// Clean up broken clone on error.
|
|
|
|
// Make sure we delete the new failed project, not the original one!
|
|
|
|
await ProjectDeleter.promises.deleteProject(newProject._id)
|
|
|
|
throw new OError({
|
|
|
|
message: 'error cloning project, broken clone deleted',
|
|
|
|
info: {
|
|
|
|
originalProjectId,
|
2019-05-29 05:21:06 -04:00
|
|
|
newProjectName,
|
2020-05-01 10:00:34 -04:00
|
|
|
newProjectId: newProject._id
|
|
|
|
}
|
|
|
|
}).withCause(err)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2020-05-01 10:00:34 -04:00
|
|
|
return newProject
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|