ProjectEntityUpdateHandler.coffee 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. _ = require 'lodash'
  2. async = require 'async'
  3. logger = require('logger-sharelatex')
  4. path = require('path')
  5. Doc = require('../../models/Doc').Doc
  6. DocstoreManager = require('../Docstore/DocstoreManager')
  7. DocumentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler')
  8. Errors = require '../Errors/Errors'
  9. File = require('../../models/File').File
  10. FileStoreHandler = require('../FileStore/FileStoreHandler')
  11. LockManager = require('../../infrastructure/LockManager')
  12. Project = require('../../models/Project').Project
  13. ProjectEntityHandler = require('./ProjectEntityHandler')
  14. ProjectGetter = require('./ProjectGetter')
  15. ProjectLocator = require('./ProjectLocator')
  16. ProjectUpdateHandler = require('./ProjectUpdateHandler')
  17. ProjectEntityMongoUpdateHandler = require('./ProjectEntityMongoUpdateHandler')
  18. SafePath = require './SafePath'
  19. TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender')
  20. LOCK_NAMESPACE = "sequentialProjectStructureUpdateLock"
  21. wrapWithLock = (methodWithoutLock) ->
  22. # This lock is used to make sure that the project structure updates are made
  23. # sequentially. In particular the updates must be made in mongo and sent to
  24. # the doc-updater in the same order.
  25. methodWithLock = (project_id, args..., callback) ->
  26. LockManager.runWithLock LOCK_NAMESPACE, project_id,
  27. (cb) -> methodWithoutLock project_id, args..., cb
  28. callback
  29. methodWithLock.withoutLock = methodWithoutLock
  30. methodWithLock
  31. module.exports = ProjectEntityUpdateHandler = self =
  32. # this doesn't need any locking because it's only called by ProjectDuplicator
  33. copyFileFromExistingProjectWithProject: (project, folder_id, originalProject_id, origonalFileRef, userId, callback = (error, fileRef, folder_id) ->)->
  34. project_id = project._id
  35. logger.log { project_id, folder_id, originalProject_id, origonalFileRef }, "copying file in s3 with project"
  36. return callback(err) if err?
  37. ProjectEntityMongoUpdateHandler._confirmFolder project, folder_id, (folder_id)=>
  38. if !origonalFileRef?
  39. logger.err { project_id, folder_id, originalProject_id, origonalFileRef }, "file trying to copy is null"
  40. return callback()
  41. # convert any invalid characters in original file to '_'
  42. fileRef = new File name : SafePath.clean(origonalFileRef.name)
  43. FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err, fileStoreUrl)->
  44. if err?
  45. logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error coping file in s3"
  46. return callback(err)
  47. ProjectEntityMongoUpdateHandler._putElement project, folder_id, fileRef, "file", (err, result)=>
  48. if err?
  49. logger.err { err, project_id, folder_id }, "error putting element as part of copy"
  50. return callback(err)
  51. TpdsUpdateSender.addFile { project_id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) ->
  52. if err?
  53. logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error sending file to tpds worker"
  54. newFiles = [
  55. file: fileRef
  56. path: result?.path?.fileSystem
  57. url: fileStoreUrl
  58. ]
  59. DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) ->
  60. return callback(error) if error?
  61. callback null, fileRef, folder_id
  62. updateDocLines: (project_id, doc_id, lines, version, ranges, callback = (error) ->)->
  63. ProjectGetter.getProjectWithoutDocLines project_id, (err, project)->
  64. return callback(err) if err?
  65. return callback(new Errors.NotFoundError("project not found")) if !project?
  66. logger.log project_id: project_id, doc_id: doc_id, "updating doc lines"
  67. ProjectLocator.findElement {project:project, element_id:doc_id, type:"docs"}, (err, doc, path)->
  68. isDeletedDoc = false
  69. if err?
  70. if err instanceof Errors.NotFoundError
  71. # We need to be able to update the doclines of deleted docs. This is
  72. # so the doc-updater can flush a doc's content to the doc-store after
  73. # the doc is deleted.
  74. isDeletedDoc = true
  75. doc = _.find project.deletedDocs, (doc) ->
  76. doc._id.toString() == doc_id.toString()
  77. else
  78. return callback(err)
  79. if !doc?
  80. # Do not allow an update to a doc which has never exist on this project
  81. logger.error {doc_id, project_id, lines}, "doc not found while updating doc lines"
  82. return callback(new Errors.NotFoundError('doc not found'))
  83. logger.log {project_id, doc_id}, "telling docstore manager to update doc"
  84. DocstoreManager.updateDoc project_id, doc_id, lines, version, ranges, (err, modified, rev) ->
  85. if err?
  86. logger.error {err, doc_id, project_id, lines}, "error sending doc to docstore"
  87. return callback(err)
  88. logger.log {project_id, doc_id, modified}, "finished updating doc lines"
  89. # path will only be present if the doc is not deleted
  90. if modified && !isDeletedDoc
  91. # Don't need to block for marking as updated
  92. ProjectUpdateHandler.markAsUpdated project_id
  93. TpdsUpdateSender.addDoc {project_id:project_id, path:path.fileSystem, doc_id:doc_id, project_name:project.name, rev:rev}, callback
  94. else
  95. callback()
  96. setRootDoc: (project_id, newRootDocID, callback = (error) ->)->
  97. logger.log project_id: project_id, rootDocId: newRootDocID, "setting root doc"
  98. Project.update {_id:project_id}, {rootDoc_id:newRootDocID}, {}, callback
  99. unsetRootDoc: (project_id, callback = (error) ->) ->
  100. logger.log project_id: project_id, "removing root doc"
  101. Project.update {_id:project_id}, {$unset: {rootDoc_id: true}}, {}, callback
  102. addDoc: wrapWithLock (project_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
  103. self.addDocWithoutUpdatingHistory.withoutLock project_id, folder_id, docName, docLines, userId, (error, doc, folder_id, path) ->
  104. return callback(error) if error?
  105. newDocs = [
  106. doc: doc
  107. path: path
  108. docLines: docLines.join('\n')
  109. ]
  110. DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newDocs}, (error) ->
  111. return callback(error) if error?
  112. callback null, doc, folder_id
  113. addFile: wrapWithLock (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (error, fileRef, folder_id) ->)->
  114. self.addFileWithoutUpdatingHistory.withoutLock project_id, folder_id, fileName, fsPath, linkedFileData, userId, (error, fileRef, folder_id, path, fileStoreUrl) ->
  115. return callback(error) if error?
  116. newFiles = [
  117. file: fileRef
  118. path: path
  119. url: fileStoreUrl
  120. ]
  121. DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) ->
  122. return callback(error) if error?
  123. callback null, fileRef, folder_id
  124. replaceFile: wrapWithLock (project_id, file_id, fsPath, linkedFileData, userId, callback)->
  125. FileStoreHandler.uploadFileFromDisk project_id, file_id, fsPath, (err, fileStoreUrl)->
  126. return callback(err) if err?
  127. ProjectEntityMongoUpdateHandler.replaceFile project_id, file_id, linkedFileData, (err, fileRef, project, path) ->
  128. return callback(err) if err?
  129. newFiles = [
  130. file: fileRef
  131. path: path.fileSystem
  132. url: fileStoreUrl
  133. ]
  134. TpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:path.fileSystem, rev:fileRef.rev+1, project_name:project.name}, (err) ->
  135. return callback(err) if err?
  136. DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, callback
  137. addDocWithoutUpdatingHistory: wrapWithLock (project_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
  138. # This method should never be called directly, except when importing a project
  139. # from Overleaf. It skips sending updates to the project history, which will break
  140. # the history unless you are making sure it is updated in some other way.
  141. if not SafePath.isCleanFilename docName
  142. return callback new Errors.InvalidNameError("invalid element name")
  143. # Put doc in docstore first, so that if it errors, we don't have a doc_id in the project
  144. # which hasn't been created in docstore.
  145. doc = new Doc name: docName
  146. DocstoreManager.updateDoc project_id.toString(), doc._id.toString(), docLines, 0, {}, (err, modified, rev) ->
  147. return callback(err) if err?
  148. ProjectEntityMongoUpdateHandler.addDoc project_id, folder_id, doc, (err, result, project) ->
  149. return callback(err) if err?
  150. TpdsUpdateSender.addDoc {
  151. project_id: project_id,
  152. doc_id: doc?._id
  153. path: result?.path?.fileSystem,
  154. project_name: project.name,
  155. rev: 0
  156. }, (err) ->
  157. return callback(err) if err?
  158. callback(null, doc, folder_id, result?.path?.fileSystem)
  159. addFileWithoutUpdatingHistory: wrapWithLock (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (error, fileRef, folder_id, path, fileStoreUrl) ->)->
  160. # This method should never be called directly, except when importing a project
  161. # from Overleaf. It skips sending updates to the project history, which will break
  162. # the history unless you are making sure it is updated in some other way.
  163. if not SafePath.isCleanFilename fileName
  164. return callback new Errors.InvalidNameError("invalid element name")
  165. fileRef = new File(
  166. name: fileName
  167. linkedFileData: linkedFileData
  168. )
  169. FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)->
  170. if err?
  171. logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3"
  172. return callback(err)
  173. ProjectEntityMongoUpdateHandler.addFile project_id, folder_id, fileRef, (err, result, project) ->
  174. if err?
  175. logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error adding file with project"
  176. return callback(err)
  177. TpdsUpdateSender.addFile {project_id:project_id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err) ->
  178. return callback(err) if err?
  179. callback(null, fileRef, folder_id, result?.path?.fileSystem, fileStoreUrl)
  180. upsertDoc: wrapWithLock (project_id, folder_id, docName, docLines, source, userId, callback = (err, doc, folder_id, isNewDoc)->)->
  181. ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) ->
  182. return callback(error) if error?
  183. return callback(new Error("Couldn't find folder")) if !folder?
  184. existingDoc = null
  185. for doc in folder.docs
  186. if doc.name == docName
  187. existingDoc = doc
  188. break
  189. if existingDoc?
  190. DocumentUpdaterHandler.setDocument project_id, existingDoc._id, userId, docLines, source, (err)=>
  191. logger.log project_id:project_id, doc_id:existingDoc._id, "notifying users that the document has been updated"
  192. DocumentUpdaterHandler.flushDocToMongo project_id, existingDoc._id, (err) ->
  193. return callback(err) if err?
  194. callback null, existingDoc, !existingDoc?
  195. else
  196. self.addDoc.withoutLock project_id, folder_id, docName, docLines, userId, (err, doc) ->
  197. return callback(err) if err?
  198. callback null, doc, !existingDoc?
  199. upsertFile: wrapWithLock (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (err, file, isNewFile)->)->
  200. ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) ->
  201. return callback(error) if error?
  202. return callback(new Error("Couldn't find folder")) if !folder?
  203. existingFile = null
  204. for fileRef in folder.fileRefs
  205. if fileRef.name == fileName
  206. existingFile = fileRef
  207. break
  208. if existingFile?
  209. self.replaceFile.withoutLock project_id, existingFile._id, fsPath, linkedFileData, userId, (err) ->
  210. return callback(err) if err?
  211. callback null, existingFile, !existingFile?
  212. else
  213. self.addFile.withoutLock project_id, folder_id, fileName, fsPath, linkedFileData, userId, (err, file) ->
  214. return callback(err) if err?
  215. callback null, file, !existingFile?
  216. upsertDocWithPath: wrapWithLock (project_id, elementPath, docLines, source, userId, callback) ->
  217. docName = path.basename(elementPath)
  218. folderPath = path.dirname(elementPath)
  219. self.mkdirp.withoutLock project_id, folderPath, (err, newFolders, folder) ->
  220. return callback(err) if err?
  221. self.upsertDoc.withoutLock project_id, folder._id, docName, docLines, source, userId, (err, doc, isNewDoc) ->
  222. return callback(err) if err?
  223. callback null, doc, isNewDoc, newFolders, folder
  224. upsertFileWithPath: wrapWithLock (project_id, elementPath, fsPath, linkedFileData, userId, callback) ->
  225. fileName = path.basename(elementPath)
  226. folderPath = path.dirname(elementPath)
  227. self.mkdirp.withoutLock project_id, folderPath, (err, newFolders, folder) ->
  228. return callback(err) if err?
  229. self.upsertFile.withoutLock project_id, folder._id, fileName, fsPath, linkedFileData, userId, (err, file, isNewFile) ->
  230. return callback(err) if err?
  231. callback null, file, isNewFile, newFolders, folder
  232. deleteEntity: wrapWithLock (project_id, entity_id, entityType, userId, callback = (error) ->)->
  233. logger.log entity_id:entity_id, entityType:entityType, project_id:project_id, "deleting project entity"
  234. if !entityType?
  235. logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id
  236. return callback("No entityType set")
  237. entityType = entityType.toLowerCase()
  238. ProjectEntityMongoUpdateHandler.deleteEntity project_id, entity_id, entityType, (error, entity, path, projectBeforeDeletion) ->
  239. return callback(error) if error?
  240. self._cleanUpEntity projectBeforeDeletion, entity, entityType, path.fileSystem, userId, (error) ->
  241. return callback(error) if error?
  242. TpdsUpdateSender.deleteEntity project_id:project_id, path:path.fileSystem, project_name:projectBeforeDeletion.name, (error) ->
  243. return callback(error) if error?
  244. callback null, entity_id
  245. deleteEntityWithPath: wrapWithLock (project_id, path, userId, callback) ->
  246. ProjectLocator.findElementByPath project_id: project_id, path: path, (err, element, type)->
  247. return callback(err) if err?
  248. return callback(new Errors.NotFoundError("project not found")) if !element?
  249. self.deleteEntity.withoutLock project_id, element._id, type, userId, callback
  250. mkdirp: wrapWithLock (project_id, path, callback = (err, newlyCreatedFolders, lastFolderInPath)->)->
  251. ProjectEntityMongoUpdateHandler.mkdirp project_id, path, callback
  252. addFolder: wrapWithLock (project_id, parentFolder_id, folderName, callback) ->
  253. if not SafePath.isCleanFilename folderName
  254. return callback new Errors.InvalidNameError("invalid element name")
  255. ProjectEntityMongoUpdateHandler.addFolder project_id, parentFolder_id, folderName, callback
  256. moveEntity: wrapWithLock (project_id, entity_id, destFolderId, entityType, userId, callback = (error) ->)->
  257. logger.log {entityType, entity_id, project_id, destFolderId}, "moving entity"
  258. if !entityType?
  259. logger.err {err: "No entityType set", project_id, entity_id}
  260. return callback("No entityType set")
  261. entityType = entityType.toLowerCase()
  262. ProjectEntityMongoUpdateHandler.moveEntity project_id, entity_id, destFolderId, entityType, (err, project_name, startPath, endPath, rev, changes) ->
  263. return callback(err) if err?
  264. TpdsUpdateSender.moveEntity { project_id, project_name, startPath, endPath, rev }
  265. DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback
  266. renameEntity: wrapWithLock (project_id, entity_id, entityType, newName, userId, callback)->
  267. if not SafePath.isCleanFilename newName
  268. return callback new Errors.InvalidNameError("invalid element name")
  269. logger.log(entity_id: entity_id, project_id: project_id, ('renaming '+entityType))
  270. if !entityType?
  271. logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id
  272. return callback("No entityType set")
  273. entityType = entityType.toLowerCase()
  274. ProjectEntityMongoUpdateHandler.renameEntity project_id, entity_id, entityType, newName, (err, project_name, startPath, endPath, rev, changes) ->
  275. return callback(err) if err?
  276. TpdsUpdateSender.moveEntity({project_id, startPath, endPath, project_name, rev})
  277. DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback
  278. # This doesn't directly update project structure but we need to take the lock
  279. # to prevent anything else being queued before the resync update
  280. resyncProjectHistory: wrapWithLock (project_id, callback) ->
  281. ProjectGetter.getProject project_id, rootFolder: true, overleaf: true, (error, project) ->
  282. return callback(error) if error?
  283. if !project?.overleaf?.history?.id?
  284. error = new Errors.ProjectHistoryDisabledError("project history not enabled for #{project_id}")
  285. return callback(error)
  286. ProjectEntityHandler.getAllEntitiesFromProject project, (error, docs, files) ->
  287. return callback(error) if error?
  288. docs = _.map docs, (doc) ->
  289. doc: doc.doc._id
  290. path: doc.path
  291. files = _.map files, (file) ->
  292. file: file.file._id
  293. path: file.path
  294. url: FileStoreHandler._buildUrl(project_id, file.file._id)
  295. DocumentUpdaterHandler.resyncProjectHistory project_id, docs, files, callback
  296. _cleanUpEntity: (project, entity, entityType, path, userId, callback = (error) ->) ->
  297. if(entityType.indexOf("file") != -1)
  298. self._cleanUpFile project, entity, path, userId, callback
  299. else if (entityType.indexOf("doc") != -1)
  300. self._cleanUpDoc project, entity, path, userId, callback
  301. else if (entityType.indexOf("folder") != -1)
  302. self._cleanUpFolder project, entity, path, userId, callback
  303. else
  304. callback()
  305. _cleanUpDoc: (project, doc, path, userId, callback = (error) ->) ->
  306. project_id = project._id.toString()
  307. doc_id = doc._id.toString()
  308. unsetRootDocIfRequired = (callback) =>
  309. if project.rootDoc_id? and project.rootDoc_id.toString() == doc_id
  310. @unsetRootDoc project_id, callback
  311. else
  312. callback()
  313. unsetRootDocIfRequired (error) ->
  314. return callback(error) if error?
  315. self._insertDeletedDocReference project._id, doc, (error) ->
  316. return callback(error) if error?
  317. DocumentUpdaterHandler.deleteDoc project_id, doc_id, (error) ->
  318. return callback(error) if error?
  319. DocstoreManager.deleteDoc project_id, doc_id, (error) ->
  320. return callback(error) if error?
  321. changes = oldDocs: [ {doc, path} ]
  322. DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback
  323. _cleanUpFile: (project, file, path, userId, callback = (error) ->) ->
  324. project_id = project._id.toString()
  325. file_id = file._id.toString()
  326. FileStoreHandler.deleteFile project_id, file_id, (error) ->
  327. return callback(error) if error?
  328. changes = oldFiles: [ {file, path} ]
  329. DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback
  330. _cleanUpFolder: (project, folder, folderPath, userId, callback = (error) ->) ->
  331. jobs = []
  332. for doc in folder.docs
  333. do (doc) ->
  334. docPath = path.join(folderPath, doc.name)
  335. jobs.push (callback) -> self._cleanUpDoc project, doc, docPath, userId, callback
  336. for file in folder.fileRefs
  337. do (file) ->
  338. filePath = path.join(folderPath, file.name)
  339. jobs.push (callback) -> self._cleanUpFile project, file, filePath, userId, callback
  340. for childFolder in folder.folders
  341. do (childFolder) ->
  342. folderPath = path.join(folderPath, childFolder.name)
  343. jobs.push (callback) -> self._cleanUpFolder project, childFolder, folderPath, userId, callback
  344. async.series jobs, callback
  345. _insertDeletedDocReference: (project_id, doc, callback = (error) ->) ->
  346. Project.update {
  347. _id: project_id
  348. }, {
  349. $push: {
  350. deletedDocs: {
  351. _id: doc._id
  352. name: doc.name
  353. }
  354. }
  355. }, {}, callback