ProjectEntityUpdateHandler.coffee 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  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. if typeof methodWithoutLock is 'function'
  26. methodWithLock = (project_id, args..., callback) ->
  27. LockManager.runWithLock LOCK_NAMESPACE, project_id,
  28. (cb) -> methodWithoutLock project_id, args..., cb
  29. callback
  30. methodWithLock.withoutLock = methodWithoutLock
  31. methodWithLock
  32. else
  33. # handle case with separate setup and locked stages
  34. wrapWithSetup = methodWithoutLock.beforeLock # a function to set things up before the lock
  35. mainTask = methodWithoutLock.withLock # function to execute inside the lock
  36. methodWithLock = wrapWithSetup (project_id, args..., callback) ->
  37. LockManager.runWithLock(LOCK_NAMESPACE, project_id, (cb) ->
  38. mainTask(project_id, args..., cb)
  39. callback)
  40. methodWithLock.withoutLock = wrapWithSetup mainTask
  41. methodWithLock.beforeLock = methodWithoutLock.beforeLock
  42. methodWithLock.mainTask = methodWithoutLock.withLock
  43. methodWithLock
  44. module.exports = ProjectEntityUpdateHandler = self =
  45. copyFileFromExistingProjectWithProject: wrapWithLock
  46. beforeLock: (next) ->
  47. (project_id, project, folder_id, originalProject_id, origonalFileRef, userId, callback = (error, fileRef, folder_id) ->)->
  48. logger.log { project_id, folder_id, originalProject_id, origonalFileRef }, "copying file in s3 with project"
  49. ProjectEntityMongoUpdateHandler._confirmFolder project, folder_id, (folder_id) ->
  50. if !origonalFileRef?
  51. logger.err { project_id, folder_id, originalProject_id, origonalFileRef }, "file trying to copy is null"
  52. return callback()
  53. # convert any invalid characters in original file to '_'
  54. fileProperties = name : SafePath.clean(origonalFileRef.name)
  55. if origonalFileRef.linkedFileData?
  56. fileProperties.linkedFileData = origonalFileRef.linkedFileData
  57. fileRef = new File(fileProperties)
  58. FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err, fileStoreUrl)->
  59. if err?
  60. logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error coping file in s3"
  61. return callback(err)
  62. next(project_id, project, folder_id, originalProject_id, origonalFileRef, userId, fileRef, fileStoreUrl, callback)
  63. withLock: (project_id, project, folder_id, originalProject_id, origonalFileRef, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id) ->)->
  64. projectHistoryId = project.overleaf?.history?.id
  65. ProjectEntityMongoUpdateHandler._putElement project, folder_id, fileRef, "file", (err, result, newProject) ->
  66. if err?
  67. logger.err { err, project_id, folder_id }, "error putting element as part of copy"
  68. return callback(err)
  69. TpdsUpdateSender.addFile { project_id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) ->
  70. if err?
  71. logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error sending file to tpds worker"
  72. newFiles = [
  73. file: fileRef
  74. path: result?.path?.fileSystem
  75. url: fileStoreUrl
  76. ]
  77. DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {newFiles, newProject}, (error) ->
  78. return callback(error) if error?
  79. callback null, fileRef, folder_id
  80. updateDocLines: (project_id, doc_id, lines, version, ranges, lastUpdatedAt, lastUpdatedBy, callback = (error) ->)->
  81. ProjectGetter.getProjectWithoutDocLines project_id, (err, project)->
  82. return callback(err) if err?
  83. return callback(new Errors.NotFoundError("project not found")) if !project?
  84. logger.log project_id: project_id, doc_id: doc_id, "updating doc lines"
  85. ProjectLocator.findElement {project:project, element_id:doc_id, type:"docs"}, (err, doc, path)->
  86. isDeletedDoc = false
  87. if err?
  88. if err instanceof Errors.NotFoundError
  89. # We need to be able to update the doclines of deleted docs. This is
  90. # so the doc-updater can flush a doc's content to the doc-store after
  91. # the doc is deleted.
  92. isDeletedDoc = true
  93. doc = _.find project.deletedDocs, (doc) ->
  94. doc._id.toString() == doc_id.toString()
  95. else
  96. return callback(err)
  97. if !doc?
  98. # Do not allow an update to a doc which has never exist on this project
  99. logger.error {doc_id, project_id, lines}, "doc not found while updating doc lines"
  100. return callback(new Errors.NotFoundError('doc not found'))
  101. logger.log {project_id, doc_id}, "telling docstore manager to update doc"
  102. DocstoreManager.updateDoc project_id, doc_id, lines, version, ranges, (err, modified, rev) ->
  103. if err?
  104. logger.error {err, doc_id, project_id, lines}, "error sending doc to docstore"
  105. return callback(err)
  106. logger.log {project_id, doc_id, modified}, "finished updating doc lines"
  107. # path will only be present if the doc is not deleted
  108. if modified && !isDeletedDoc
  109. # Don't need to block for marking as updated
  110. ProjectUpdateHandler.markAsUpdated project_id, lastUpdatedAt, lastUpdatedBy
  111. TpdsUpdateSender.addDoc {project_id:project_id, path:path.fileSystem, doc_id:doc_id, project_name:project.name, rev:rev}, callback
  112. else
  113. callback()
  114. setRootDoc: (project_id, newRootDocID, callback = (error) ->)->
  115. logger.log project_id: project_id, rootDocId: newRootDocID, "setting root doc"
  116. Project.update {_id:project_id}, {rootDoc_id:newRootDocID}, {}, callback
  117. unsetRootDoc: (project_id, callback = (error) ->) ->
  118. logger.log project_id: project_id, "removing root doc"
  119. Project.update {_id:project_id}, {$unset: {rootDoc_id: true}}, {}, callback
  120. _addDocAndSendToTpds: (project_id, folder_id, doc, callback = (error, result, project) ->)->
  121. ProjectEntityMongoUpdateHandler.addDoc project_id, folder_id, doc, (err, result, project) ->
  122. if err?
  123. logger.err err:err, project_id: project_id, folder_id: folder_id, doc_name: doc?.name, doc_id:doc?._id, "error adding file with project"
  124. return callback(err)
  125. TpdsUpdateSender.addDoc {
  126. project_id: project_id,
  127. doc_id: doc?._id,
  128. path: result?.path?.fileSystem,
  129. project_name: project.name,
  130. rev: 0
  131. }, (err) ->
  132. return callback(err) if err?
  133. callback(null, result, project)
  134. addDoc: (project_id, folder_id, docName, docLines, userId, callback) ->
  135. self.addDocWithRanges(project_id, folder_id, docName, docLines, {}, userId, callback)
  136. addDocWithRanges: wrapWithLock
  137. beforeLock: (next) ->
  138. (project_id, folder_id, docName, docLines, ranges, userId, callback = (error, doc, folder_id) ->) ->
  139. if not SafePath.isCleanFilename docName
  140. return callback new Errors.InvalidNameError("invalid element name")
  141. # Put doc in docstore first, so that if it errors, we don't have a doc_id in the project
  142. # which hasn't been created in docstore.
  143. doc = new Doc name: docName
  144. DocstoreManager.updateDoc project_id.toString(), doc._id.toString(), docLines, 0, ranges, (err, modified, rev) ->
  145. return callback(err) if err?
  146. next(project_id, folder_id, doc, docName, docLines, ranges, userId, callback)
  147. withLock: (project_id, folder_id, doc, docName, docLines, ranges, userId, callback = (error, doc, folder_id) ->) ->
  148. ProjectEntityUpdateHandler._addDocAndSendToTpds project_id, folder_id, doc, (err, result, project) ->
  149. return callback(err) if err?
  150. docPath = result?.path?.fileSystem
  151. projectHistoryId = project.overleaf?.history?.id
  152. newDocs = [
  153. doc: doc
  154. path: docPath
  155. docLines: docLines.join('\n')
  156. ]
  157. DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {newDocs, newProject: project}, (error) ->
  158. return callback(error) if error?
  159. callback null, doc, folder_id
  160. _uploadFile: (project_id, folder_id, fileName, fsPath, linkedFileData, callback = (error, fileRef, fileStoreUrl) ->)->
  161. if not SafePath.isCleanFilename fileName
  162. return callback new Errors.InvalidNameError("invalid element name")
  163. fileRef = new File(
  164. name: fileName
  165. linkedFileData: linkedFileData
  166. )
  167. FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)->
  168. if err?
  169. logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3"
  170. return callback(err)
  171. callback(null, fileRef, fileStoreUrl)
  172. _addFileAndSendToTpds: (project_id, folder_id, fileRef, callback = (error) ->)->
  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: fileRef.name, 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, result, project)
  180. addFile: wrapWithLock
  181. beforeLock: (next) ->
  182. (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback) ->
  183. if not SafePath.isCleanFilename fileName
  184. return callback new Errors.InvalidNameError("invalid element name")
  185. ProjectEntityUpdateHandler._uploadFile project_id, folder_id, fileName, fsPath, linkedFileData, (error, fileRef, fileStoreUrl) ->
  186. return callback(error) if error?
  187. next(project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback)
  188. withLock: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id) ->)->
  189. ProjectEntityUpdateHandler._addFileAndSendToTpds project_id, folder_id, fileRef, (err, result, project) ->
  190. return callback(err) if err?
  191. projectHistoryId = project.overleaf?.history?.id
  192. newFiles = [
  193. file: fileRef
  194. path: result?.path?.fileSystem
  195. url: fileStoreUrl
  196. ]
  197. DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {newFiles, newProject: project}, (error) ->
  198. return callback(error) if error?
  199. callback(null, fileRef, folder_id)
  200. replaceFile: wrapWithLock
  201. beforeLock: (next) ->
  202. (project_id, file_id, fsPath, linkedFileData, userId, callback)->
  203. # create a new file
  204. fileRef = new File(
  205. name: "dummy-upload-filename"
  206. linkedFileData: linkedFileData
  207. )
  208. FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)->
  209. return callback(err) if err?
  210. next project_id, file_id, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback
  211. withLock: (project_id, file_id, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, callback)->
  212. ProjectEntityMongoUpdateHandler.replaceFileWithNew project_id, file_id, newFileRef, (err, oldFileRef, project, path, newProject) ->
  213. return callback(err) if err?
  214. oldFiles = [
  215. file: oldFileRef
  216. path: path.fileSystem
  217. ]
  218. newFiles = [
  219. file: newFileRef
  220. path: path.fileSystem
  221. url: fileStoreUrl
  222. ]
  223. projectHistoryId = project.overleaf?.history?.id
  224. # Increment the rev for an in-place update (with the same path) so the third-party-datastore
  225. # knows this is a new file.
  226. # Ideally we would get this from ProjectEntityMongoUpdateHandler.replaceFileWithNew
  227. # but it returns the original oldFileRef (after incrementing the rev value in mongo),
  228. # so we add 1 to the rev from that. This isn't atomic and relies on the lock
  229. # but it is acceptable for now.
  230. TpdsUpdateSender.addFile {project_id:project._id, file_id:newFileRef._id, path:path.fileSystem, rev:oldFileRef.rev + 1, project_name:project.name}, (err) ->
  231. return callback(err) if err?
  232. DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {oldFiles, newFiles, newProject}, callback
  233. upsertDoc: wrapWithLock (project_id, folder_id, docName, docLines, source, userId, callback = (err, doc, folder_id, isNewDoc)->)->
  234. if not SafePath.isCleanFilename docName
  235. return callback new Errors.InvalidNameError("invalid element name")
  236. ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) ->
  237. return callback(error) if error?
  238. return callback(new Error("Couldn't find folder")) if !folder?
  239. existingDoc = null
  240. for doc in folder.docs
  241. if doc.name == docName
  242. existingDoc = doc
  243. break
  244. if existingDoc?
  245. DocumentUpdaterHandler.setDocument project_id, existingDoc._id, userId, docLines, source, (err)=>
  246. logger.log project_id:project_id, doc_id:existingDoc._id, "notifying users that the document has been updated"
  247. DocumentUpdaterHandler.flushDocToMongo project_id, existingDoc._id, (err) ->
  248. return callback(err) if err?
  249. callback null, existingDoc, !existingDoc?
  250. else
  251. self.addDocWithRanges.withoutLock project_id, folder_id, docName, docLines, {}, userId, (err, doc) ->
  252. return callback(err) if err?
  253. callback null, doc, !existingDoc?
  254. upsertFile: wrapWithLock
  255. beforeLock: (next) ->
  256. (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback)->
  257. if not SafePath.isCleanFilename fileName
  258. return callback new Errors.InvalidNameError("invalid element name")
  259. # create a new file
  260. fileRef = new File(
  261. name: fileName
  262. linkedFileData: linkedFileData
  263. )
  264. FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)->
  265. return callback(err) if err?
  266. next(project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback)
  267. withLock: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, callback = (err, file, isNewFile, existingFile)->)->
  268. ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) ->
  269. return callback(error) if error?
  270. return callback(new Error("Couldn't find folder")) if !folder?
  271. existingFile = null
  272. for fileRef in folder.fileRefs
  273. if fileRef.name == fileName
  274. existingFile = fileRef
  275. break
  276. if existingFile?
  277. # this calls directly into the replaceFile main task (without the beforeLock part)
  278. self.replaceFile.mainTask project_id, existingFile._id, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, (err) ->
  279. return callback(err) if err?
  280. callback null, newFileRef, !existingFile?, existingFile
  281. else
  282. # this calls directly into the addFile main task (without the beforeLock part)
  283. self.addFile.mainTask project_id, folder_id, fileName, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, (err) ->
  284. return callback(err) if err?
  285. callback null, newFileRef, !existingFile?, existingFile
  286. upsertDocWithPath: wrapWithLock (project_id, elementPath, docLines, source, userId, callback) ->
  287. if not SafePath.isCleanPath elementPath
  288. return callback new Errors.InvalidNameError("invalid element name")
  289. docName = path.basename(elementPath)
  290. folderPath = path.dirname(elementPath)
  291. self.mkdirp.withoutLock project_id, folderPath, (err, newFolders, folder) ->
  292. return callback(err) if err?
  293. self.upsertDoc.withoutLock project_id, folder._id, docName, docLines, source, userId, (err, doc, isNewDoc) ->
  294. return callback(err) if err?
  295. callback null, doc, isNewDoc, newFolders, folder
  296. upsertFileWithPath: wrapWithLock
  297. beforeLock: (next) ->
  298. (project_id, elementPath, fsPath, linkedFileData, userId, callback)->
  299. if not SafePath.isCleanPath elementPath
  300. return callback new Errors.InvalidNameError("invalid element name")
  301. fileName = path.basename(elementPath)
  302. folderPath = path.dirname(elementPath)
  303. # create a new file
  304. fileRef = new File(
  305. name: fileName
  306. linkedFileData: linkedFileData
  307. )
  308. FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)->
  309. return callback(err) if err?
  310. next project_id, folderPath, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback
  311. withLock: (project_id, folderPath, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback) ->
  312. self.mkdirp.withoutLock project_id, folderPath, (err, newFolders, folder) ->
  313. return callback(err) if err?
  314. # this calls directly into the upsertFile main task (without the beforeLock part)
  315. self.upsertFile.mainTask project_id, folder._id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, (err, newFile, isNewFile, existingFile) ->
  316. return callback(err) if err?
  317. callback null, newFile, isNewFile, existingFile, newFolders, folder
  318. deleteEntity: wrapWithLock (project_id, entity_id, entityType, userId, callback = (error) ->)->
  319. logger.log entity_id:entity_id, entityType:entityType, project_id:project_id, "deleting project entity"
  320. if !entityType?
  321. logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id
  322. return callback("No entityType set")
  323. entityType = entityType.toLowerCase()
  324. ProjectEntityMongoUpdateHandler.deleteEntity project_id, entity_id, entityType, (error, entity, path, projectBeforeDeletion, newProject) ->
  325. return callback(error) if error?
  326. self._cleanUpEntity projectBeforeDeletion, newProject, entity, entityType, path.fileSystem, userId, (error) ->
  327. return callback(error) if error?
  328. TpdsUpdateSender.deleteEntity project_id:project_id, path:path.fileSystem, project_name:projectBeforeDeletion.name, (error) ->
  329. return callback(error) if error?
  330. callback null, entity_id
  331. deleteEntityWithPath: wrapWithLock (project_id, path, userId, callback) ->
  332. ProjectLocator.findElementByPath project_id: project_id, path: path, (err, element, type)->
  333. return callback(err) if err?
  334. return callback(new Errors.NotFoundError("project not found")) if !element?
  335. self.deleteEntity.withoutLock project_id, element._id, type, userId, callback
  336. mkdirp: wrapWithLock (project_id, path, callback = (err, newlyCreatedFolders, lastFolderInPath)->)->
  337. for folder in path.split('/')
  338. if folder.length > 0 and not SafePath.isCleanFilename folder
  339. return callback new Errors.InvalidNameError("invalid element name")
  340. ProjectEntityMongoUpdateHandler.mkdirp project_id, path, {exactCaseMatch: false}, callback
  341. mkdirpWithExactCase: wrapWithLock (project_id, path, callback = (err, newlyCreatedFolders, lastFolderInPath)->)->
  342. for folder in path.split('/')
  343. if folder.length > 0 and not SafePath.isCleanFilename folder
  344. return callback new Errors.InvalidNameError("invalid element name")
  345. ProjectEntityMongoUpdateHandler.mkdirp project_id, path, {exactCaseMatch: true}, callback
  346. addFolder: wrapWithLock (project_id, parentFolder_id, folderName, callback) ->
  347. if not SafePath.isCleanFilename folderName
  348. return callback new Errors.InvalidNameError("invalid element name")
  349. ProjectEntityMongoUpdateHandler.addFolder project_id, parentFolder_id, folderName, callback
  350. moveEntity: wrapWithLock (project_id, entity_id, destFolderId, entityType, userId, callback = (error) ->)->
  351. logger.log {entityType, entity_id, project_id, destFolderId}, "moving entity"
  352. if !entityType?
  353. logger.err {err: "No entityType set", project_id, entity_id}
  354. return callback("No entityType set")
  355. entityType = entityType.toLowerCase()
  356. ProjectEntityMongoUpdateHandler.moveEntity project_id, entity_id, destFolderId, entityType, (err, project, startPath, endPath, rev, changes) ->
  357. return callback(err) if err?
  358. projectHistoryId = project.overleaf?.history?.id
  359. TpdsUpdateSender.moveEntity { project_id, project_name: project.name, startPath, endPath, rev }
  360. DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, changes, callback
  361. renameEntity: wrapWithLock (project_id, entity_id, entityType, newName, userId, callback)->
  362. if not SafePath.isCleanFilename newName
  363. return callback new Errors.InvalidNameError("invalid element name")
  364. logger.log(entity_id: entity_id, project_id: project_id, ('renaming '+entityType))
  365. if !entityType?
  366. logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id
  367. return callback("No entityType set")
  368. entityType = entityType.toLowerCase()
  369. ProjectEntityMongoUpdateHandler.renameEntity project_id, entity_id, entityType, newName, (err, project, startPath, endPath, rev, changes) ->
  370. return callback(err) if err?
  371. projectHistoryId = project.overleaf?.history?.id
  372. TpdsUpdateSender.moveEntity { project_id, project_name: project.name, startPath, endPath, rev }
  373. DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, changes, callback
  374. # This doesn't directly update project structure but we need to take the lock
  375. # to prevent anything else being queued before the resync update
  376. resyncProjectHistory: wrapWithLock (project_id, callback) ->
  377. ProjectGetter.getProject project_id, rootFolder: true, overleaf: true, (error, project) ->
  378. return callback(error) if error?
  379. projectHistoryId = project?.overleaf?.history?.id
  380. if !projectHistoryId?
  381. error = new Errors.ProjectHistoryDisabledError("project history not enabled for #{project_id}")
  382. return callback(error)
  383. ProjectEntityHandler.getAllEntitiesFromProject project, (error, docs, files) ->
  384. return callback(error) if error?
  385. docs = _.map docs, (doc) ->
  386. doc: doc.doc._id
  387. path: doc.path
  388. files = _.map files, (file) ->
  389. file: file.file._id
  390. path: file.path
  391. url: FileStoreHandler._buildUrl(project_id, file.file._id)
  392. DocumentUpdaterHandler.resyncProjectHistory project_id, projectHistoryId, docs, files, callback
  393. _cleanUpEntity: (project, newProject, entity, entityType, path, userId, callback = (error) ->) ->
  394. self._updateProjectStructureWithDeletedEntity project, newProject, entity, entityType, path, userId, (error) ->
  395. return callback(error) if error?
  396. if(entityType.indexOf("file") != -1)
  397. self._cleanUpFile project, entity, path, userId, callback
  398. else if (entityType.indexOf("doc") != -1)
  399. self._cleanUpDoc project, entity, path, userId, callback
  400. else if (entityType.indexOf("folder") != -1)
  401. self._cleanUpFolder project, entity, path, userId, callback
  402. else
  403. callback()
  404. # Note: the _cleanUpEntity code and _updateProjectStructureWithDeletedEntity
  405. # methods both need to recursively iterate over the entities in folder.
  406. # These are currently using separate implementations of the recursion. In
  407. # future, these could be simplified using a common project entity iterator.
  408. _updateProjectStructureWithDeletedEntity: (project, newProject, entity, entityType, entityPath, userId, callback = (error) ->) ->
  409. # compute the changes to the project structure
  410. if(entityType.indexOf("file") != -1)
  411. changes = oldFiles: [ {file: entity, path: entityPath} ]
  412. else if (entityType.indexOf("doc") != -1)
  413. changes = oldDocs: [ {doc: entity, path: entityPath} ]
  414. else if (entityType.indexOf("folder") != -1)
  415. changes = {oldDocs: [], oldFiles: []}
  416. _recurseFolder = (folder, folderPath) ->
  417. for doc in folder.docs
  418. changes.oldDocs.push {doc, path: path.join(folderPath, doc.name)}
  419. for file in folder.fileRefs
  420. changes.oldFiles.push {file, path: path.join(folderPath, file.name)}
  421. for childFolder in folder.folders
  422. _recurseFolder(childFolder, path.join(folderPath, childFolder.name))
  423. _recurseFolder entity, entityPath
  424. # now send the project structure changes to the docupdater
  425. changes.newProject = newProject
  426. project_id = project._id.toString()
  427. projectHistoryId = project.overleaf?.history?.id
  428. DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, changes, callback
  429. _cleanUpDoc: (project, doc, path, userId, callback = (error) ->) ->
  430. project_id = project._id.toString()
  431. doc_id = doc._id.toString()
  432. unsetRootDocIfRequired = (callback) =>
  433. if project.rootDoc_id? and project.rootDoc_id.toString() == doc_id
  434. @unsetRootDoc project_id, callback
  435. else
  436. callback()
  437. unsetRootDocIfRequired (error) ->
  438. return callback(error) if error?
  439. ProjectEntityMongoUpdateHandler._insertDeletedDocReference project._id, doc, (error) ->
  440. return callback(error) if error?
  441. DocumentUpdaterHandler.deleteDoc project_id, doc_id, (error) ->
  442. return callback(error) if error?
  443. DocstoreManager.deleteDoc project_id, doc_id, callback
  444. _cleanUpFile: (project, file, path, userId, callback = (error) ->) ->
  445. ProjectEntityMongoUpdateHandler._insertDeletedFileReference project._id, file, callback
  446. _cleanUpFolder: (project, folder, folderPath, userId, callback = (error) ->) ->
  447. jobs = []
  448. for doc in folder.docs
  449. do (doc) ->
  450. docPath = path.join(folderPath, doc.name)
  451. jobs.push (callback) -> self._cleanUpDoc project, doc, docPath, userId, callback
  452. for file in folder.fileRefs
  453. do (file) ->
  454. filePath = path.join(folderPath, file.name)
  455. jobs.push (callback) -> self._cleanUpFile project, file, filePath, userId, callback
  456. for childFolder in folder.folders
  457. do (childFolder) ->
  458. folderPath = path.join(folderPath, childFolder.name)
  459. jobs.push (callback) -> self._cleanUpFolder project, childFolder, folderPath, userId, callback
  460. async.series jobs, callback