2020-05-13 10:15:23 -04:00
|
|
|
const { callbackify } = require('util')
|
|
|
|
const Path = require('path')
|
2020-05-01 10:00:34 -04:00
|
|
|
const OError = require('@overleaf/o-error')
|
2020-05-13 10:15:23 -04:00
|
|
|
const { promiseMapWithLimit } = require('../../util/promises')
|
|
|
|
const { Doc } = require('../../models/Doc')
|
|
|
|
const { File } = require('../../models/File')
|
|
|
|
const DocstoreManager = require('../Docstore/DocstoreManager')
|
|
|
|
const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
|
|
|
|
const FileStoreHandler = require('../FileStore/FileStoreHandler')
|
2020-05-01 10:00:34 -04:00
|
|
|
const ProjectCreationHandler = require('./ProjectCreationHandler')
|
2020-05-13 10:15:23 -04:00
|
|
|
const ProjectDeleter = require('./ProjectDeleter')
|
|
|
|
const ProjectEntityMongoUpdateHandler = require('./ProjectEntityMongoUpdateHandler')
|
2019-05-29 05:21:06 -04:00
|
|
|
const ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler')
|
2020-05-13 10:15:23 -04:00
|
|
|
const ProjectGetter = require('./ProjectGetter')
|
2020-05-01 10:00:34 -04:00
|
|
|
const ProjectLocator = require('./ProjectLocator')
|
|
|
|
const ProjectOptionsHandler = require('./ProjectOptionsHandler')
|
2020-05-13 10:15:23 -04:00
|
|
|
const SafePath = require('./SafePath')
|
|
|
|
const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher')
|
2021-10-28 09:01:00 -04:00
|
|
|
const _ = require('lodash')
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2020-05-01 10:00:34 -04:00
|
|
|
module.exports = {
|
|
|
|
duplicate: callbackify(duplicate),
|
|
|
|
promises: {
|
2021-04-27 03:52:58 -04:00
|
|
|
duplicate,
|
|
|
|
},
|
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 function duplicate(owner, originalProjectId, newProjectName) {
|
|
|
|
await DocumentUpdaterHandler.promises.flushProjectToMongo(originalProjectId)
|
|
|
|
const originalProject = await ProjectGetter.promises.getProject(
|
|
|
|
originalProjectId,
|
|
|
|
{
|
|
|
|
compiler: true,
|
|
|
|
rootFolder: true,
|
2021-04-27 03:52:58 -04:00
|
|
|
rootDoc_id: true,
|
2021-10-28 09:01:00 -04:00
|
|
|
fromV1TemplateId: true,
|
|
|
|
fromV1TemplateVersionId: true,
|
2020-05-01 10:00:34 -04:00
|
|
|
}
|
|
|
|
)
|
2020-05-13 10:15:23 -04:00
|
|
|
const { path: rootDocPath } = await ProjectLocator.promises.findRootDoc({
|
2021-04-27 03:52:58 -04:00
|
|
|
project_id: originalProjectId,
|
2020-05-01 10:00:34 -04:00
|
|
|
})
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2020-05-13 10:15:23 -04:00
|
|
|
const originalEntries = _getFolderEntries(originalProject.rootFolder[0])
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2021-10-28 09:01:00 -04:00
|
|
|
// Pass template ID as analytics segmentation if duplicating project from a template
|
|
|
|
const segmentation = _.pick(originalProject, [
|
|
|
|
'fromV1TemplateId',
|
|
|
|
'fromV1TemplateVersionId',
|
|
|
|
])
|
|
|
|
segmentation.duplicatedFromProject = originalProjectId
|
|
|
|
|
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,
|
2021-10-28 09:01:00 -04:00
|
|
|
newProjectName,
|
|
|
|
{ segmentation }
|
2020-05-01 10:00:34 -04:00
|
|
|
)
|
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
|
|
|
|
)
|
2020-05-13 10:15:23 -04:00
|
|
|
const [docEntries, fileEntries] = await Promise.all([
|
|
|
|
_copyDocs(originalEntries.docEntries, originalProject, newProject),
|
2021-04-27 03:52:58 -04:00
|
|
|
_copyFiles(originalEntries.fileEntries, originalProject, newProject),
|
2020-05-13 10:15:23 -04:00
|
|
|
])
|
|
|
|
const projectVersion = await ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure(
|
2020-05-01 10:00:34 -04:00
|
|
|
newProject._id,
|
2020-05-13 10:15:23 -04:00
|
|
|
docEntries,
|
|
|
|
fileEntries
|
2020-05-01 10:00:34 -04:00
|
|
|
)
|
2020-11-05 05:07:02 -05:00
|
|
|
// Silently ignore the rootDoc in case it's not valid per the new limits.
|
|
|
|
if (
|
|
|
|
rootDocPath &&
|
|
|
|
ProjectEntityUpdateHandler.isPathValidForRootDoc(rootDocPath.fileSystem)
|
|
|
|
) {
|
2020-05-13 10:15:23 -04:00
|
|
|
await _setRootDoc(newProject._id, rootDocPath.fileSystem)
|
|
|
|
}
|
|
|
|
await _notifyDocumentUpdater(newProject, owner._id, {
|
|
|
|
newFiles: fileEntries,
|
|
|
|
newDocs: docEntries,
|
2021-04-27 03:52:58 -04:00
|
|
|
newProject: { version: projectVersion },
|
2020-05-13 10:15:23 -04:00
|
|
|
})
|
|
|
|
await TpdsProjectFlusher.promises.flushProjectToTpds(newProject._id)
|
2020-05-01 10:00:34 -04:00
|
|
|
} 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)
|
2020-08-11 05:28:29 -04:00
|
|
|
throw OError.tag(err, 'error cloning project, broken clone deleted', {
|
|
|
|
originalProjectId,
|
|
|
|
newProjectName,
|
2021-04-27 03:52:58 -04:00
|
|
|
newProjectId: newProject._id,
|
2020-08-11 05:28:29 -04:00
|
|
|
})
|
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
|
|
|
}
|
2020-05-13 10:15:23 -04:00
|
|
|
|
|
|
|
function _getFolderEntries(folder, folderPath = '/') {
|
|
|
|
const docEntries = []
|
|
|
|
const fileEntries = []
|
|
|
|
const docs = folder.docs || []
|
|
|
|
const files = folder.fileRefs || []
|
|
|
|
const subfolders = folder.folders || []
|
|
|
|
|
|
|
|
for (const doc of docs) {
|
|
|
|
if (doc == null || doc._id == null) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
const path = Path.join(folderPath, doc.name)
|
|
|
|
docEntries.push({ doc, path })
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const file of files) {
|
|
|
|
if (file == null || file._id == null) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
const path = Path.join(folderPath, file.name)
|
|
|
|
fileEntries.push({ file, path })
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const subfolder of subfolders) {
|
|
|
|
if (subfolder == null || subfolder._id == null) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
const subfolderPath = Path.join(folderPath, subfolder.name)
|
|
|
|
const subfolderEntries = _getFolderEntries(subfolder, subfolderPath)
|
|
|
|
for (const docEntry of subfolderEntries.docEntries) {
|
|
|
|
docEntries.push(docEntry)
|
|
|
|
}
|
|
|
|
for (const fileEntry of subfolderEntries.fileEntries) {
|
|
|
|
fileEntries.push(fileEntry)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return { docEntries, fileEntries }
|
|
|
|
}
|
|
|
|
|
|
|
|
async function _copyDocs(sourceEntries, sourceProject, targetProject) {
|
|
|
|
const docLinesById = await _getDocLinesForProject(sourceProject._id)
|
|
|
|
const targetEntries = []
|
|
|
|
for (const sourceEntry of sourceEntries) {
|
|
|
|
const sourceDoc = sourceEntry.doc
|
|
|
|
const path = sourceEntry.path
|
|
|
|
const doc = new Doc({ name: sourceDoc.name })
|
|
|
|
const docLines = docLinesById.get(sourceDoc._id.toString())
|
|
|
|
await DocstoreManager.promises.updateDoc(
|
|
|
|
targetProject._id.toString(),
|
|
|
|
doc._id.toString(),
|
|
|
|
docLines,
|
|
|
|
0,
|
|
|
|
{}
|
|
|
|
)
|
|
|
|
targetEntries.push({ doc, path, docLines: docLines.join('\n') })
|
|
|
|
}
|
|
|
|
return targetEntries
|
|
|
|
}
|
|
|
|
|
|
|
|
async function _getDocLinesForProject(projectId) {
|
|
|
|
const docs = await DocstoreManager.promises.getAllDocs(projectId)
|
|
|
|
const docLinesById = new Map(docs.map(doc => [doc._id, doc.lines]))
|
|
|
|
return docLinesById
|
|
|
|
}
|
|
|
|
|
|
|
|
async function _copyFiles(sourceEntries, sourceProject, targetProject) {
|
|
|
|
const targetEntries = await promiseMapWithLimit(
|
|
|
|
5,
|
|
|
|
sourceEntries,
|
|
|
|
async sourceEntry => {
|
|
|
|
const sourceFile = sourceEntry.file
|
|
|
|
const path = sourceEntry.path
|
|
|
|
const file = new File({ name: SafePath.clean(sourceFile.name) })
|
|
|
|
if (sourceFile.linkedFileData != null) {
|
|
|
|
file.linkedFileData = sourceFile.linkedFileData
|
|
|
|
}
|
|
|
|
if (sourceFile.hash != null) {
|
|
|
|
file.hash = sourceFile.hash
|
|
|
|
}
|
|
|
|
const url = await FileStoreHandler.promises.copyFile(
|
|
|
|
sourceProject._id,
|
|
|
|
sourceFile._id,
|
|
|
|
targetProject._id,
|
|
|
|
file._id
|
|
|
|
)
|
|
|
|
return { file, path, url }
|
|
|
|
}
|
|
|
|
)
|
|
|
|
return targetEntries
|
|
|
|
}
|
|
|
|
|
|
|
|
async function _setRootDoc(projectId, path) {
|
|
|
|
const { element: rootDoc } = await ProjectLocator.promises.findElementByPath({
|
|
|
|
project_id: projectId,
|
|
|
|
path,
|
2021-04-27 03:52:58 -04:00
|
|
|
exactCaseMatch: true,
|
2020-05-13 10:15:23 -04:00
|
|
|
})
|
|
|
|
await ProjectEntityUpdateHandler.promises.setRootDoc(projectId, rootDoc._id)
|
|
|
|
}
|
|
|
|
|
|
|
|
async function _notifyDocumentUpdater(project, userId, changes) {
|
|
|
|
const projectHistoryId =
|
|
|
|
project.overleaf && project.overleaf.history && project.overleaf.history.id
|
|
|
|
await DocumentUpdaterHandler.promises.updateProjectStructure(
|
|
|
|
project._id,
|
|
|
|
projectHistoryId,
|
|
|
|
userId,
|
|
|
|
changes
|
|
|
|
)
|
|
|
|
}
|