ProjectEntityUpdateHandler.coffee 18 KB

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