/* eslint-disable camelcase, handle-callback-err, max-len, one-var, standard/no-callback-literal, */ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * DS103: Rewrite code to no longer use __guard__ * DS201: Simplify complex destructure assignments * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ let ProjectEntityUpdateHandler, self const _ = require('lodash') const async = require('async') const logger = require('logger-sharelatex') const Settings = require('settings-sharelatex') const path = require('path') const { Doc } = require('../../models/Doc') const DocstoreManager = require('../Docstore/DocstoreManager') const DocumentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler') const Errors = require('../Errors/Errors') const { File } = require('../../models/File') const FileStoreHandler = require('../FileStore/FileStoreHandler') const LockManager = require('../../infrastructure/LockManager') const { Project } = require('../../models/Project') const ProjectEntityHandler = require('./ProjectEntityHandler') const ProjectGetter = require('./ProjectGetter') const ProjectLocator = require('./ProjectLocator') const ProjectUpdateHandler = require('./ProjectUpdateHandler') const ProjectEntityMongoUpdateHandler = require('./ProjectEntityMongoUpdateHandler') const SafePath = require('./SafePath') const TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender') const LOCK_NAMESPACE = 'sequentialProjectStructureUpdateLock' const validRootDocExtensions = Settings.validRootDocExtensions const validRootDocRegExp = new RegExp( `^\\.(${validRootDocExtensions.join('|')})$`, 'i' ) const wrapWithLock = function(methodWithoutLock) { // This lock is used to make sure that the project structure updates are made // sequentially. In particular the updates must be made in mongo and sent to // the doc-updater in the same order. let methodWithLock if (typeof methodWithoutLock === 'function') { methodWithLock = function(project_id, ...rest) { const adjustedLength = Math.max(rest.length, 1), args = rest.slice(0, adjustedLength - 1), callback = rest[adjustedLength - 1] return LockManager.runWithLock( LOCK_NAMESPACE, project_id, cb => methodWithoutLock(project_id, ...Array.from(args), cb), callback ) } methodWithLock.withoutLock = methodWithoutLock return methodWithLock } else { // handle case with separate setup and locked stages const wrapWithSetup = methodWithoutLock.beforeLock // a function to set things up before the lock const mainTask = methodWithoutLock.withLock // function to execute inside the lock methodWithLock = wrapWithSetup(function(project_id, ...rest) { const adjustedLength = Math.max(rest.length, 1), args = rest.slice(0, adjustedLength - 1), callback = rest[adjustedLength - 1] return LockManager.runWithLock( LOCK_NAMESPACE, project_id, cb => mainTask(project_id, ...Array.from(args), cb), callback ) }) methodWithLock.withoutLock = wrapWithSetup(mainTask) methodWithLock.beforeLock = methodWithoutLock.beforeLock methodWithLock.mainTask = methodWithoutLock.withLock return methodWithLock } } module.exports = ProjectEntityUpdateHandler = self = { copyFileFromExistingProjectWithProject: wrapWithLock({ beforeLock(next) { return function( project_id, project, folder_id, originalProject_id, origonalFileRef, userId, callback ) { if (callback == null) { callback = function(error, fileRef, folder_id) {} } logger.log( { project_id, folder_id, originalProject_id, origonalFileRef }, 'copying file in s3 with project' ) return ProjectEntityMongoUpdateHandler._confirmFolder( project, folder_id, function(folder_id) { if (origonalFileRef == null) { logger.err( { project_id, folder_id, originalProject_id, origonalFileRef }, 'file trying to copy is null' ) return callback() } // convert any invalid characters in original file to '_' const fileProperties = { name: SafePath.clean(origonalFileRef.name) } if (origonalFileRef.linkedFileData != null) { fileProperties.linkedFileData = origonalFileRef.linkedFileData } if (origonalFileRef.hash != null) { fileProperties.hash = origonalFileRef.hash } const fileRef = new File(fileProperties) return FileStoreHandler.copyFile( originalProject_id, origonalFileRef._id, project._id, fileRef._id, function(err, fileStoreUrl) { if (err != null) { logger.warn( { err, project_id, folder_id, originalProject_id, origonalFileRef }, 'error coping file in s3' ) return callback(err) } return next( project_id, project, folder_id, originalProject_id, origonalFileRef, userId, fileRef, fileStoreUrl, callback ) } ) } ) } }, withLock( project_id, project, folder_id, originalProject_id, origonalFileRef, userId, fileRef, fileStoreUrl, callback ) { if (callback == null) { callback = function(error, fileRef, folder_id) {} } const projectHistoryId = __guard__( project.overleaf != null ? project.overleaf.history : undefined, x => x.id ) return ProjectEntityMongoUpdateHandler._putElement( project, folder_id, fileRef, 'file', function(err, result, newProject) { if (err != null) { logger.warn( { err, project_id, folder_id }, 'error putting element as part of copy' ) return callback(err) } return TpdsUpdateSender.addFile( { project_id, file_id: fileRef._id, path: __guard__( result != null ? result.path : undefined, x1 => x1.fileSystem ), rev: fileRef.rev, project_name: project.name }, function(err) { if (err != null) { logger.err( { err, project_id, folder_id, originalProject_id, origonalFileRef }, 'error sending file to tpds worker' ) } const newFiles = [ { file: fileRef, path: __guard__( result != null ? result.path : undefined, x2 => x2.fileSystem ), url: fileStoreUrl } ] return DocumentUpdaterHandler.updateProjectStructure( project_id, projectHistoryId, userId, { newFiles, newProject }, function(error) { if (error != null) { return callback(error) } return callback(null, fileRef, folder_id) } ) } ) } ) } }), updateDocLines( project_id, doc_id, lines, version, ranges, lastUpdatedAt, lastUpdatedBy, callback ) { if (callback == null) { callback = function(error) {} } return ProjectGetter.getProjectWithoutDocLines(project_id, function( err, project ) { if (err != null) { return callback(err) } if (project == null) { return callback(new Errors.NotFoundError('project not found')) } logger.log({ project_id, doc_id }, 'updating doc lines') return ProjectLocator.findElement( { project, element_id: doc_id, type: 'docs' }, function(err, doc, path) { let isDeletedDoc = false if (err != null) { if (err instanceof Errors.NotFoundError) { // We need to be able to update the doclines of deleted docs. This is // so the doc-updater can flush a doc's content to the doc-store after // the doc is deleted. isDeletedDoc = true doc = _.find( project.deletedDocs, doc => doc._id.toString() === doc_id.toString() ) } else { return callback(err) } } if (doc == null) { // Do not allow an update to a doc which has never exist on this project logger.warn( { doc_id, project_id, lines }, 'doc not found while updating doc lines' ) return callback(new Errors.NotFoundError('doc not found')) } logger.log( { project_id, doc_id }, 'telling docstore manager to update doc' ) return DocstoreManager.updateDoc( project_id, doc_id, lines, version, ranges, function(err, modified, rev) { if (err != null) { logger.warn( { err, doc_id, project_id, lines }, 'error sending doc to docstore' ) return callback(err) } logger.log( { project_id, doc_id, modified }, 'finished updating doc lines' ) // path will only be present if the doc is not deleted if (modified && !isDeletedDoc) { // Don't need to block for marking as updated ProjectUpdateHandler.markAsUpdated( project_id, lastUpdatedAt, lastUpdatedBy ) return TpdsUpdateSender.addDoc( { project_id, path: path.fileSystem, doc_id, project_name: project.name, rev }, callback ) } else { return callback() } } ) } ) }) }, setRootDoc(project_id, newRootDocID, callback) { if (callback == null) { callback = function(error) {} } logger.log({ project_id, rootDocId: newRootDocID }, 'setting root doc') if (project_id == null || newRootDocID == null) { return callback( new Errors.InvalidError('missing arguments (project or doc)') ) } ProjectEntityHandler.getDocPathByProjectIdAndDocId( project_id, newRootDocID, function(err, docPath) { if (err != null) { return callback(err) } if (ProjectEntityUpdateHandler.isPathValidForRootDoc(docPath)) { return Project.update( { _id: project_id }, { rootDoc_id: newRootDocID }, {}, callback ) } else { return callback( new Errors.UnsupportedFileTypeError( 'invalid file extension for root doc' ) ) } } ) }, unsetRootDoc(project_id, callback) { if (callback == null) { callback = function(error) {} } logger.log({ project_id }, 'removing root doc') return Project.update( { _id: project_id }, { $unset: { rootDoc_id: true } }, {}, callback ) }, _addDocAndSendToTpds(project_id, folder_id, doc, callback) { if (callback == null) { callback = function(error, result, project) {} } return ProjectEntityMongoUpdateHandler.addDoc( project_id, folder_id, doc, function(err, result, project) { if (err != null) { logger.warn( { err, project_id, folder_id, doc_name: doc != null ? doc.name : undefined, doc_id: doc != null ? doc._id : undefined }, 'error adding file with project' ) return callback(err) } return TpdsUpdateSender.addDoc( { project_id, doc_id: doc != null ? doc._id : undefined, path: __guard__( result != null ? result.path : undefined, x => x.fileSystem ), project_name: project.name, rev: 0 }, function(err) { if (err != null) { return callback(err) } return callback(null, result, project) } ) } ) }, addDoc(project_id, folder_id, docName, docLines, userId, callback) { return self.addDocWithRanges( project_id, folder_id, docName, docLines, {}, userId, callback ) }, addDocWithRanges: wrapWithLock({ beforeLock(next) { return function( project_id, folder_id, docName, docLines, ranges, userId, callback ) { if (callback == null) { callback = function(error, doc, folder_id) {} } if (!SafePath.isCleanFilename(docName)) { return callback(new Errors.InvalidNameError('invalid element name')) } // Put doc in docstore first, so that if it errors, we don't have a doc_id in the project // which hasn't been created in docstore. const doc = new Doc({ name: docName }) return DocstoreManager.updateDoc( project_id.toString(), doc._id.toString(), docLines, 0, ranges, function(err, modified, rev) { if (err != null) { return callback(err) } return next( project_id, folder_id, doc, docName, docLines, ranges, userId, callback ) } ) } }, withLock( project_id, folder_id, doc, docName, docLines, ranges, userId, callback ) { if (callback == null) { callback = function(error, doc, folder_id) {} } return ProjectEntityUpdateHandler._addDocAndSendToTpds( project_id, folder_id, doc, function(err, result, project) { if (err != null) { return callback(err) } const docPath = __guard__( result != null ? result.path : undefined, x => x.fileSystem ) const projectHistoryId = __guard__( project.overleaf != null ? project.overleaf.history : undefined, x1 => x1.id ) const newDocs = [ { doc, path: docPath, docLines: docLines.join('\n') } ] return DocumentUpdaterHandler.updateProjectStructure( project_id, projectHistoryId, userId, { newDocs, newProject: project }, function(error) { if (error != null) { return callback(error) } return callback(null, doc, folder_id) } ) } ) } }), _uploadFile( project_id, folder_id, fileName, fsPath, linkedFileData, callback ) { if (callback == null) { callback = function(error, fileStoreUrl, fileRef) {} } if (!SafePath.isCleanFilename(fileName)) { return callback(new Errors.InvalidNameError('invalid element name')) } const fileArgs = { name: fileName, linkedFileData } return FileStoreHandler.uploadFileFromDisk( project_id, fileArgs, fsPath, function(err, fileStoreUrl, fileRef) { if (err != null) { logger.warn( { err, project_id, folder_id, file_name: fileName, fileRef }, 'error uploading image to s3' ) return callback(err) } return callback(null, fileStoreUrl, fileRef) } ) }, _addFileAndSendToTpds(project_id, folder_id, fileRef, callback) { if (callback == null) { callback = function(error) {} } return ProjectEntityMongoUpdateHandler.addFile( project_id, folder_id, fileRef, function(err, result, project) { if (err != null) { logger.warn( { err, project_id, folder_id, file_name: fileRef.name, fileRef }, 'error adding file with project' ) return callback(err) } return TpdsUpdateSender.addFile( { project_id, file_id: fileRef._id, path: __guard__( result != null ? result.path : undefined, x => x.fileSystem ), project_name: project.name, rev: fileRef.rev }, function(err) { if (err != null) { return callback(err) } return callback(null, result, project) } ) } ) }, addFile: wrapWithLock({ beforeLock(next) { return function( project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback ) { if (!SafePath.isCleanFilename(fileName)) { return callback(new Errors.InvalidNameError('invalid element name')) } return ProjectEntityUpdateHandler._uploadFile( project_id, folder_id, fileName, fsPath, linkedFileData, function(error, fileStoreUrl, fileRef) { if (error != null) { return callback(error) } return next( project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback ) } ) } }, withLock( project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback ) { if (callback == null) { callback = function(error, fileRef, folder_id) {} } return ProjectEntityUpdateHandler._addFileAndSendToTpds( project_id, folder_id, fileRef, function(err, result, project) { if (err != null) { return callback(err) } const projectHistoryId = __guard__( project.overleaf != null ? project.overleaf.history : undefined, x => x.id ) const newFiles = [ { file: fileRef, path: __guard__( result != null ? result.path : undefined, x1 => x1.fileSystem ), url: fileStoreUrl } ] return DocumentUpdaterHandler.updateProjectStructure( project_id, projectHistoryId, userId, { newFiles, newProject: project }, function(error) { if (error != null) { return callback(error) } return callback(null, fileRef, folder_id) } ) } ) } }), replaceFile: wrapWithLock({ beforeLock(next) { return function( project_id, file_id, fsPath, linkedFileData, userId, callback ) { // create a new file const fileArgs = { name: 'dummy-upload-filename', linkedFileData } return FileStoreHandler.uploadFileFromDisk( project_id, fileArgs, fsPath, function(err, fileStoreUrl, fileRef) { if (err != null) { return callback(err) } return next( project_id, file_id, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback ) } ) } }, withLock( project_id, file_id, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, callback ) { return ProjectEntityMongoUpdateHandler.replaceFileWithNew( project_id, file_id, newFileRef, function(err, oldFileRef, project, path, newProject) { if (err != null) { return callback(err) } const oldFiles = [ { file: oldFileRef, path: path.fileSystem } ] const newFiles = [ { file: newFileRef, path: path.fileSystem, url: fileStoreUrl } ] const projectHistoryId = __guard__( project.overleaf != null ? project.overleaf.history : undefined, x => x.id ) // Increment the rev for an in-place update (with the same path) so the third-party-datastore // knows this is a new file. // Ideally we would get this from ProjectEntityMongoUpdateHandler.replaceFileWithNew // but it returns the original oldFileRef (after incrementing the rev value in mongo), // so we add 1 to the rev from that. This isn't atomic and relies on the lock // but it is acceptable for now. return TpdsUpdateSender.addFile( { project_id: project._id, file_id: newFileRef._id, path: path.fileSystem, rev: oldFileRef.rev + 1, project_name: project.name }, function(err) { if (err != null) { return callback(err) } return DocumentUpdaterHandler.updateProjectStructure( project_id, projectHistoryId, userId, { oldFiles, newFiles, newProject }, callback ) } ) } ) } }), upsertDoc: wrapWithLock(function( project_id, folder_id, docName, docLines, source, userId, callback ) { if (callback == null) { callback = function(err, doc, folder_id, isNewDoc) {} } if (!SafePath.isCleanFilename(docName)) { return callback(new Errors.InvalidNameError('invalid element name')) } return ProjectLocator.findElement( { project_id, element_id: folder_id, type: 'folder' }, function(error, folder) { if (error != null) { return callback(error) } if (folder == null) { return callback(new Error("Couldn't find folder")) } let existingDoc = null for (let doc of Array.from(folder.docs)) { if (doc.name === docName) { existingDoc = doc break } } if (existingDoc != null) { return DocumentUpdaterHandler.setDocument( project_id, existingDoc._id, userId, docLines, source, err => { logger.log( { project_id, doc_id: existingDoc._id }, 'notifying users that the document has been updated' ) return DocumentUpdaterHandler.flushDocToMongo( project_id, existingDoc._id, function(err) { if (err != null) { return callback(err) } return callback(null, existingDoc, existingDoc == null) } ) } ) } else { return self.addDocWithRanges.withoutLock( project_id, folder_id, docName, docLines, {}, userId, function(err, doc) { if (err != null) { return callback(err) } return callback(null, doc, existingDoc == null) } ) } } ) }), upsertFile: wrapWithLock({ beforeLock(next) { return function( project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback ) { if (!SafePath.isCleanFilename(fileName)) { return callback(new Errors.InvalidNameError('invalid element name')) } // create a new file const fileArgs = { name: fileName, linkedFileData } return FileStoreHandler.uploadFileFromDisk( project_id, fileArgs, fsPath, function(err, fileStoreUrl, fileRef) { if (err != null) { return callback(err) } return next( project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback ) } ) } }, withLock( project_id, folder_id, fileName, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, callback ) { if (callback == null) { callback = function(err, file, isNewFile, existingFile) {} } return ProjectLocator.findElement( { project_id, element_id: folder_id, type: 'folder' }, function(error, folder) { if (error != null) { return callback(error) } if (folder == null) { return callback(new Error("Couldn't find folder")) } let existingFile = null for (let fileRef of Array.from(folder.fileRefs)) { if (fileRef.name === fileName) { existingFile = fileRef break } } if (existingFile != null) { // this calls directly into the replaceFile main task (without the beforeLock part) return self.replaceFile.mainTask( project_id, existingFile._id, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, function(err) { if (err != null) { return callback(err) } return callback( null, newFileRef, existingFile == null, existingFile ) } ) } else { // this calls directly into the addFile main task (without the beforeLock part) return self.addFile.mainTask( project_id, folder_id, fileName, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, function(err) { if (err != null) { return callback(err) } return callback( null, newFileRef, existingFile == null, existingFile ) } ) } } ) } }), upsertDocWithPath: wrapWithLock(function( project_id, elementPath, docLines, source, userId, callback ) { if (!SafePath.isCleanPath(elementPath)) { return callback(new Errors.InvalidNameError('invalid element name')) } const docName = path.basename(elementPath) const folderPath = path.dirname(elementPath) return self.mkdirp.withoutLock(project_id, folderPath, function( err, newFolders, folder ) { if (err != null) { return callback(err) } return self.upsertDoc.withoutLock( project_id, folder._id, docName, docLines, source, userId, function(err, doc, isNewDoc) { if (err != null) { return callback(err) } return callback(null, doc, isNewDoc, newFolders, folder) } ) }) }), upsertFileWithPath: wrapWithLock({ beforeLock(next) { return function( project_id, elementPath, fsPath, linkedFileData, userId, callback ) { if (!SafePath.isCleanPath(elementPath)) { return callback(new Errors.InvalidNameError('invalid element name')) } const fileName = path.basename(elementPath) const folderPath = path.dirname(elementPath) // create a new file const fileArgs = { name: fileName, linkedFileData } return FileStoreHandler.uploadFileFromDisk( project_id, fileArgs, fsPath, function(err, fileStoreUrl, fileRef) { if (err != null) { return callback(err) } return next( project_id, folderPath, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback ) } ) } }, withLock( project_id, folderPath, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback ) { return self.mkdirp.withoutLock(project_id, folderPath, function( err, newFolders, folder ) { if (err != null) { return callback(err) } // this calls directly into the upsertFile main task (without the beforeLock part) return self.upsertFile.mainTask( project_id, folder._id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, function(err, newFile, isNewFile, existingFile) { if (err != null) { return callback(err) } return callback( null, newFile, isNewFile, existingFile, newFolders, folder ) } ) }) } }), deleteEntity: wrapWithLock(function( project_id, entity_id, entityType, userId, callback ) { if (callback == null) { callback = function(error) {} } logger.log({ entity_id, entityType, project_id }, 'deleting project entity') if (entityType == null) { logger.warn({ err: 'No entityType set', project_id, entity_id }) return callback(new Error('No entityType set')) } entityType = entityType.toLowerCase() return ProjectEntityMongoUpdateHandler.deleteEntity( project_id, entity_id, entityType, function(error, entity, path, projectBeforeDeletion, newProject) { if (error != null) { return callback(error) } return self._cleanUpEntity( projectBeforeDeletion, newProject, entity, entityType, path.fileSystem, userId, function(error) { if (error != null) { return callback(error) } return TpdsUpdateSender.deleteEntity( { project_id, path: path.fileSystem, project_name: projectBeforeDeletion.name }, function(error) { if (error != null) { return callback(error) } return callback(null, entity_id) } ) } ) } ) }), deleteEntityWithPath: wrapWithLock((project_id, path, userId, callback) => ProjectLocator.findElementByPath({ project_id, path }, function( err, element, type ) { if (err != null) { return callback(err) } if (element == null) { return callback(new Errors.NotFoundError('project not found')) } return self.deleteEntity.withoutLock( project_id, element._id, type, userId, callback ) }) ), mkdirp: wrapWithLock(function(project_id, path, callback) { if (callback == null) { callback = function(err, newlyCreatedFolders, lastFolderInPath) {} } for (let folder of Array.from(path.split('/'))) { if (folder.length > 0 && !SafePath.isCleanFilename(folder)) { return callback(new Errors.InvalidNameError('invalid element name')) } } return ProjectEntityMongoUpdateHandler.mkdirp( project_id, path, { exactCaseMatch: false }, callback ) }), mkdirpWithExactCase: wrapWithLock(function(project_id, path, callback) { if (callback == null) { callback = function(err, newlyCreatedFolders, lastFolderInPath) {} } for (let folder of Array.from(path.split('/'))) { if (folder.length > 0 && !SafePath.isCleanFilename(folder)) { return callback(new Errors.InvalidNameError('invalid element name')) } } return ProjectEntityMongoUpdateHandler.mkdirp( project_id, path, { exactCaseMatch: true }, callback ) }), addFolder: wrapWithLock(function( project_id, parentFolder_id, folderName, callback ) { if (!SafePath.isCleanFilename(folderName)) { return callback(new Errors.InvalidNameError('invalid element name')) } return ProjectEntityMongoUpdateHandler.addFolder( project_id, parentFolder_id, folderName, callback ) }), moveEntity: wrapWithLock(function( project_id, entity_id, destFolderId, entityType, userId, callback ) { if (callback == null) { callback = function(error) {} } logger.log( { entityType, entity_id, project_id, destFolderId }, 'moving entity' ) if (entityType == null) { logger.warn({ err: 'No entityType set', project_id, entity_id }) return callback(new Error('No entityType set')) } entityType = entityType.toLowerCase() return ProjectEntityMongoUpdateHandler.moveEntity( project_id, entity_id, destFolderId, entityType, function(err, project, startPath, endPath, rev, changes) { if (err != null) { return callback(err) } const projectHistoryId = __guard__( project.overleaf != null ? project.overleaf.history : undefined, x => x.id ) TpdsUpdateSender.moveEntity({ project_id, project_name: project.name, startPath, endPath, rev }) return DocumentUpdaterHandler.updateProjectStructure( project_id, projectHistoryId, userId, changes, callback ) } ) }), renameEntity: wrapWithLock(function( project_id, entity_id, entityType, newName, userId, callback ) { if (!SafePath.isCleanFilename(newName)) { return callback(new Errors.InvalidNameError('invalid element name')) } logger.log({ entity_id, project_id }, `renaming ${entityType}`) if (entityType == null) { logger.warn({ err: 'No entityType set', project_id, entity_id }) return callback(new Error('No entityType set')) } entityType = entityType.toLowerCase() return ProjectEntityMongoUpdateHandler.renameEntity( project_id, entity_id, entityType, newName, function(err, project, startPath, endPath, rev, changes) { if (err != null) { return callback(err) } const projectHistoryId = __guard__( project.overleaf != null ? project.overleaf.history : undefined, x => x.id ) TpdsUpdateSender.moveEntity({ project_id, project_name: project.name, startPath, endPath, rev }) return DocumentUpdaterHandler.updateProjectStructure( project_id, projectHistoryId, userId, changes, callback ) } ) }), // This doesn't directly update project structure but we need to take the lock // to prevent anything else being queued before the resync update resyncProjectHistory: wrapWithLock((project_id, callback) => ProjectGetter.getProject( project_id, { rootFolder: true, overleaf: true }, function(error, project) { if (error != null) { return callback(error) } const projectHistoryId = __guard__( __guard__( project != null ? project.overleaf : undefined, x1 => x1.history ), x => x.id ) if (projectHistoryId == null) { error = new Errors.ProjectHistoryDisabledError( `project history not enabled for ${project_id}` ) return callback(error) } return ProjectEntityHandler.getAllEntitiesFromProject(project, function( error, docs, files ) { if (error != null) { return callback(error) } docs = _.map(docs, doc => ({ doc: doc.doc._id, path: doc.path })) files = _.map(files, file => ({ file: file.file._id, path: file.path, url: FileStoreHandler._buildUrl(project_id, file.file._id) })) return DocumentUpdaterHandler.resyncProjectHistory( project_id, projectHistoryId, docs, files, callback ) }) } ) ), isPathValidForRootDoc(docPath) { let docExtension = path.extname(docPath) return validRootDocRegExp.test(docExtension) }, _cleanUpEntity( project, newProject, entity, entityType, path, userId, callback ) { if (callback == null) { callback = function(error) {} } return self._updateProjectStructureWithDeletedEntity( project, newProject, entity, entityType, path, userId, function(error) { if (error != null) { return callback(error) } if (entityType.indexOf('file') !== -1) { return self._cleanUpFile(project, entity, path, userId, callback) } else if (entityType.indexOf('doc') !== -1) { return self._cleanUpDoc(project, entity, path, userId, callback) } else if (entityType.indexOf('folder') !== -1) { return self._cleanUpFolder(project, entity, path, userId, callback) } else { return callback() } } ) }, // Note: the _cleanUpEntity code and _updateProjectStructureWithDeletedEntity // methods both need to recursively iterate over the entities in folder. // These are currently using separate implementations of the recursion. In // future, these could be simplified using a common project entity iterator. _updateProjectStructureWithDeletedEntity( project, newProject, entity, entityType, entityPath, userId, callback ) { // compute the changes to the project structure let changes if (callback == null) { callback = function(error) {} } if (entityType.indexOf('file') !== -1) { changes = { oldFiles: [{ file: entity, path: entityPath }] } } else if (entityType.indexOf('doc') !== -1) { changes = { oldDocs: [{ doc: entity, path: entityPath }] } } else if (entityType.indexOf('folder') !== -1) { changes = { oldDocs: [], oldFiles: [] } var _recurseFolder = function(folder, folderPath) { for (let doc of Array.from(folder.docs)) { changes.oldDocs.push({ doc, path: path.join(folderPath, doc.name) }) } for (let file of Array.from(folder.fileRefs)) { changes.oldFiles.push({ file, path: path.join(folderPath, file.name) }) } return Array.from(folder.folders).map(childFolder => _recurseFolder(childFolder, path.join(folderPath, childFolder.name)) ) } _recurseFolder(entity, entityPath) } // now send the project structure changes to the docupdater changes.newProject = newProject const project_id = project._id.toString() const projectHistoryId = __guard__( project.overleaf != null ? project.overleaf.history : undefined, x => x.id ) return DocumentUpdaterHandler.updateProjectStructure( project_id, projectHistoryId, userId, changes, callback ) }, _cleanUpDoc(project, doc, path, userId, callback) { if (callback == null) { callback = function(error) {} } const project_id = project._id.toString() const doc_id = doc._id.toString() const unsetRootDocIfRequired = callback => { if ( project.rootDoc_id != null && project.rootDoc_id.toString() === doc_id ) { return this.unsetRootDoc(project_id, callback) } else { return callback() } } return unsetRootDocIfRequired(function(error) { if (error != null) { return callback(error) } return ProjectEntityMongoUpdateHandler._insertDeletedDocReference( project._id, doc, function(error) { if (error != null) { return callback(error) } return DocumentUpdaterHandler.deleteDoc(project_id, doc_id, function( error ) { if (error != null) { return callback(error) } return DocstoreManager.deleteDoc(project_id, doc_id, callback) }) } ) }) }, _cleanUpFile(project, file, path, userId, callback) { if (callback == null) { callback = function(error) {} } return ProjectEntityMongoUpdateHandler._insertDeletedFileReference( project._id, file, callback ) }, _cleanUpFolder(project, folder, folderPath, userId, callback) { if (callback == null) { callback = function(error) {} } const jobs = [] for (let doc of Array.from(folder.docs)) { ;(function(doc) { const docPath = path.join(folderPath, doc.name) return jobs.push(callback => self._cleanUpDoc(project, doc, docPath, userId, callback) ) })(doc) } for (let file of Array.from(folder.fileRefs)) { ;(function(file) { const filePath = path.join(folderPath, file.name) return jobs.push(callback => self._cleanUpFile(project, file, filePath, userId, callback) ) })(file) } for (let childFolder of Array.from(folder.folders)) { ;(function(childFolder) { folderPath = path.join(folderPath, childFolder.name) return jobs.push(callback => self._cleanUpFolder( project, childFolder, folderPath, userId, callback ) ) })(childFolder) } return async.series(jobs, callback) } } function __guard__(value, transform) { return typeof value !== 'undefined' && value !== null ? transform(value) : undefined }