ProjectEntityUpdateHandler.js 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676
  1. const _ = require('lodash')
  2. const async = require('async')
  3. const logger = require('logger-sharelatex')
  4. const Settings = require('settings-sharelatex')
  5. const Path = require('path')
  6. const fs = require('fs')
  7. const { Doc } = require('../../models/Doc')
  8. const DocstoreManager = require('../Docstore/DocstoreManager')
  9. const DocumentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler')
  10. const Errors = require('../Errors/Errors')
  11. const FileStoreHandler = require('../FileStore/FileStoreHandler')
  12. const LockManager = require('../../infrastructure/LockManager')
  13. const { Project } = require('../../models/Project')
  14. const ProjectEntityHandler = require('./ProjectEntityHandler')
  15. const ProjectGetter = require('./ProjectGetter')
  16. const ProjectLocator = require('./ProjectLocator')
  17. const ProjectUpdateHandler = require('./ProjectUpdateHandler')
  18. const ProjectEntityMongoUpdateHandler = require('./ProjectEntityMongoUpdateHandler')
  19. const SafePath = require('./SafePath')
  20. const TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender')
  21. const FileWriter = require('../../infrastructure/FileWriter')
  22. const EditorRealTimeController = require('../Editor/EditorRealTimeController')
  23. const { promisifyAll } = require('../../util/promises')
  24. const LOCK_NAMESPACE = 'sequentialProjectStructureUpdateLock'
  25. const VALID_ROOT_DOC_EXTENSIONS = Settings.validRootDocExtensions
  26. const VALID_ROOT_DOC_REGEXP = new RegExp(
  27. `^\\.(${VALID_ROOT_DOC_EXTENSIONS.join('|')})$`,
  28. 'i'
  29. )
  30. function wrapWithLock(methodWithoutLock) {
  31. // This lock is used to make sure that the project structure updates are made
  32. // sequentially. In particular the updates must be made in mongo and sent to
  33. // the doc-updater in the same order.
  34. if (typeof methodWithoutLock === 'function') {
  35. const methodWithLock = (projectId, ...rest) => {
  36. const adjustedLength = Math.max(rest.length, 1)
  37. const args = rest.slice(0, adjustedLength - 1)
  38. const callback = rest[adjustedLength - 1]
  39. LockManager.runWithLock(
  40. LOCK_NAMESPACE,
  41. projectId,
  42. cb => methodWithoutLock(projectId, ...args, cb),
  43. callback
  44. )
  45. }
  46. methodWithLock.withoutLock = methodWithoutLock
  47. return methodWithLock
  48. } else {
  49. // handle case with separate setup and locked stages
  50. const wrapWithSetup = methodWithoutLock.beforeLock // a function to set things up before the lock
  51. const mainTask = methodWithoutLock.withLock // function to execute inside the lock
  52. const methodWithLock = wrapWithSetup((projectId, ...rest) => {
  53. const adjustedLength = Math.max(rest.length, 1)
  54. const args = rest.slice(0, adjustedLength - 1)
  55. const callback = rest[adjustedLength - 1]
  56. LockManager.runWithLock(
  57. LOCK_NAMESPACE,
  58. projectId,
  59. cb => mainTask(projectId, ...args, cb),
  60. callback
  61. )
  62. })
  63. methodWithLock.withoutLock = wrapWithSetup(mainTask)
  64. methodWithLock.beforeLock = methodWithoutLock.beforeLock
  65. methodWithLock.mainTask = methodWithoutLock.withLock
  66. return methodWithLock
  67. }
  68. }
  69. const ProjectEntityUpdateHandler = {
  70. updateDocLines(
  71. projectId,
  72. docId,
  73. lines,
  74. version,
  75. ranges,
  76. lastUpdatedAt,
  77. lastUpdatedBy,
  78. callback
  79. ) {
  80. ProjectGetter.getProjectWithoutDocLines(projectId, (err, project) => {
  81. if (err != null) {
  82. return callback(err)
  83. }
  84. if (project == null) {
  85. return callback(new Errors.NotFoundError('project not found'))
  86. }
  87. logger.log({ projectId, docId }, 'updating doc lines')
  88. ProjectLocator.findElement(
  89. { project, element_id: docId, type: 'docs' },
  90. (err, doc, path) => {
  91. let isDeletedDoc = false
  92. if (err != null) {
  93. if (err instanceof Errors.NotFoundError) {
  94. // We need to be able to update the doclines of deleted docs. This is
  95. // so the doc-updater can flush a doc's content to the doc-store after
  96. // the doc is deleted.
  97. isDeletedDoc = true
  98. doc = _.find(
  99. project.deletedDocs,
  100. doc => doc._id.toString() === docId.toString()
  101. )
  102. } else {
  103. return callback(err)
  104. }
  105. }
  106. if (doc == null) {
  107. // Do not allow an update to a doc which has never exist on this project
  108. logger.warn(
  109. { docId, projectId },
  110. 'doc not found while updating doc lines'
  111. )
  112. return callback(new Errors.NotFoundError('doc not found'))
  113. }
  114. logger.log(
  115. { projectId, docId },
  116. 'telling docstore manager to update doc'
  117. )
  118. DocstoreManager.updateDoc(
  119. projectId,
  120. docId,
  121. lines,
  122. version,
  123. ranges,
  124. (err, modified, rev) => {
  125. if (err != null) {
  126. logger.warn(
  127. { err, docId, projectId },
  128. 'error sending doc to docstore'
  129. )
  130. return callback(err)
  131. }
  132. logger.log(
  133. { projectId, docId, modified },
  134. 'finished updating doc lines'
  135. )
  136. // path will only be present if the doc is not deleted
  137. if (modified && !isDeletedDoc) {
  138. // Don't need to block for marking as updated
  139. ProjectUpdateHandler.markAsUpdated(
  140. projectId,
  141. lastUpdatedAt,
  142. lastUpdatedBy
  143. )
  144. TpdsUpdateSender.addDoc(
  145. {
  146. project_id: projectId,
  147. path: path.fileSystem,
  148. doc_id: docId,
  149. project_name: project.name,
  150. rev
  151. },
  152. callback
  153. )
  154. } else {
  155. callback()
  156. }
  157. }
  158. )
  159. }
  160. )
  161. })
  162. },
  163. setRootDoc(projectId, newRootDocID, callback) {
  164. logger.log({ projectId, rootDocId: newRootDocID }, 'setting root doc')
  165. if (projectId == null || newRootDocID == null) {
  166. return callback(
  167. new Errors.InvalidError('missing arguments (project or doc)')
  168. )
  169. }
  170. ProjectEntityHandler.getDocPathByProjectIdAndDocId(
  171. projectId,
  172. newRootDocID,
  173. (err, docPath) => {
  174. if (err != null) {
  175. return callback(err)
  176. }
  177. if (ProjectEntityUpdateHandler.isPathValidForRootDoc(docPath)) {
  178. Project.update(
  179. { _id: projectId },
  180. { rootDoc_id: newRootDocID },
  181. {},
  182. callback
  183. )
  184. } else {
  185. callback(
  186. new Errors.UnsupportedFileTypeError(
  187. 'invalid file extension for root doc'
  188. )
  189. )
  190. }
  191. }
  192. )
  193. },
  194. unsetRootDoc(projectId, callback) {
  195. logger.log({ projectId }, 'removing root doc')
  196. Project.update(
  197. { _id: projectId },
  198. { $unset: { rootDoc_id: true } },
  199. {},
  200. callback
  201. )
  202. },
  203. _addDocAndSendToTpds(projectId, folderId, doc, callback) {
  204. ProjectEntityMongoUpdateHandler.addDoc(
  205. projectId,
  206. folderId,
  207. doc,
  208. (err, result, project) => {
  209. if (err != null) {
  210. logger.warn(
  211. {
  212. err,
  213. projectId,
  214. folderId,
  215. doc_name: doc != null ? doc.name : undefined,
  216. doc_id: doc != null ? doc._id : undefined
  217. },
  218. 'error adding file with project'
  219. )
  220. return callback(err)
  221. }
  222. TpdsUpdateSender.addDoc(
  223. {
  224. project_id: projectId,
  225. doc_id: doc != null ? doc._id : undefined,
  226. path: result && result.path && result.path.fileSystem,
  227. project_name: project.name,
  228. rev: 0
  229. },
  230. err => {
  231. if (err != null) {
  232. return callback(err)
  233. }
  234. callback(null, result, project)
  235. }
  236. )
  237. }
  238. )
  239. },
  240. addDoc(projectId, folderId, docName, docLines, userId, callback) {
  241. ProjectEntityUpdateHandler.addDocWithRanges(
  242. projectId,
  243. folderId,
  244. docName,
  245. docLines,
  246. {},
  247. userId,
  248. callback
  249. )
  250. },
  251. addDocWithRanges: wrapWithLock({
  252. beforeLock(next) {
  253. return function(
  254. projectId,
  255. folderId,
  256. docName,
  257. docLines,
  258. ranges,
  259. userId,
  260. callback
  261. ) {
  262. if (!SafePath.isCleanFilename(docName)) {
  263. return callback(new Errors.InvalidNameError('invalid element name'))
  264. }
  265. // Put doc in docstore first, so that if it errors, we don't have a doc_id in the project
  266. // which hasn't been created in docstore.
  267. const doc = new Doc({ name: docName })
  268. DocstoreManager.updateDoc(
  269. projectId.toString(),
  270. doc._id.toString(),
  271. docLines,
  272. 0,
  273. ranges,
  274. (err, modified, rev) => {
  275. if (err != null) {
  276. return callback(err)
  277. }
  278. next(
  279. projectId,
  280. folderId,
  281. doc,
  282. docName,
  283. docLines,
  284. ranges,
  285. userId,
  286. callback
  287. )
  288. }
  289. )
  290. }
  291. },
  292. withLock(
  293. projectId,
  294. folderId,
  295. doc,
  296. docName,
  297. docLines,
  298. ranges,
  299. userId,
  300. callback
  301. ) {
  302. ProjectEntityUpdateHandler._addDocAndSendToTpds(
  303. projectId,
  304. folderId,
  305. doc,
  306. (err, result, project) => {
  307. if (err != null) {
  308. return callback(err)
  309. }
  310. const docPath = result && result.path && result.path.fileSystem
  311. const projectHistoryId =
  312. project.overleaf &&
  313. project.overleaf.history &&
  314. project.overleaf.history.id
  315. const newDocs = [
  316. {
  317. doc,
  318. path: docPath,
  319. docLines: docLines.join('\n')
  320. }
  321. ]
  322. DocumentUpdaterHandler.updateProjectStructure(
  323. projectId,
  324. projectHistoryId,
  325. userId,
  326. { newDocs, newProject: project },
  327. error => {
  328. if (error != null) {
  329. return callback(error)
  330. }
  331. callback(null, doc, folderId)
  332. }
  333. )
  334. }
  335. )
  336. }
  337. }),
  338. _uploadFile(projectId, folderId, fileName, fsPath, linkedFileData, callback) {
  339. if (!SafePath.isCleanFilename(fileName)) {
  340. return callback(new Errors.InvalidNameError('invalid element name'))
  341. }
  342. const fileArgs = {
  343. name: fileName,
  344. linkedFileData
  345. }
  346. FileStoreHandler.uploadFileFromDisk(
  347. projectId,
  348. fileArgs,
  349. fsPath,
  350. (err, fileStoreUrl, fileRef) => {
  351. if (err != null) {
  352. logger.warn(
  353. { err, projectId, folderId, file_name: fileName, fileRef },
  354. 'error uploading image to s3'
  355. )
  356. return callback(err)
  357. }
  358. callback(null, fileStoreUrl, fileRef)
  359. }
  360. )
  361. },
  362. _addFileAndSendToTpds(projectId, folderId, fileRef, callback) {
  363. ProjectEntityMongoUpdateHandler.addFile(
  364. projectId,
  365. folderId,
  366. fileRef,
  367. (err, result, project) => {
  368. if (err != null) {
  369. logger.warn(
  370. { err, projectId, folderId, file_name: fileRef.name, fileRef },
  371. 'error adding file with project'
  372. )
  373. return callback(err)
  374. }
  375. TpdsUpdateSender.addFile(
  376. {
  377. project_id: projectId,
  378. file_id: fileRef._id,
  379. path: result && result.path && result.path.fileSystem,
  380. project_name: project.name,
  381. rev: fileRef.rev
  382. },
  383. err => {
  384. if (err != null) {
  385. return callback(err)
  386. }
  387. callback(null, result, project)
  388. }
  389. )
  390. }
  391. )
  392. },
  393. addFile: wrapWithLock({
  394. beforeLock(next) {
  395. return function(
  396. projectId,
  397. folderId,
  398. fileName,
  399. fsPath,
  400. linkedFileData,
  401. userId,
  402. callback
  403. ) {
  404. if (!SafePath.isCleanFilename(fileName)) {
  405. return callback(new Errors.InvalidNameError('invalid element name'))
  406. }
  407. ProjectEntityUpdateHandler._uploadFile(
  408. projectId,
  409. folderId,
  410. fileName,
  411. fsPath,
  412. linkedFileData,
  413. (error, fileStoreUrl, fileRef) => {
  414. if (error != null) {
  415. return callback(error)
  416. }
  417. next(
  418. projectId,
  419. folderId,
  420. fileName,
  421. fsPath,
  422. linkedFileData,
  423. userId,
  424. fileRef,
  425. fileStoreUrl,
  426. callback
  427. )
  428. }
  429. )
  430. }
  431. },
  432. withLock(
  433. projectId,
  434. folderId,
  435. fileName,
  436. fsPath,
  437. linkedFileData,
  438. userId,
  439. fileRef,
  440. fileStoreUrl,
  441. callback
  442. ) {
  443. ProjectEntityUpdateHandler._addFileAndSendToTpds(
  444. projectId,
  445. folderId,
  446. fileRef,
  447. (err, result, project) => {
  448. if (err != null) {
  449. return callback(err)
  450. }
  451. const projectHistoryId =
  452. project.overleaf &&
  453. project.overleaf.history &&
  454. project.overleaf.history.id
  455. const newFiles = [
  456. {
  457. file: fileRef,
  458. path: result && result.path && result.path.fileSystem,
  459. url: fileStoreUrl
  460. }
  461. ]
  462. DocumentUpdaterHandler.updateProjectStructure(
  463. projectId,
  464. projectHistoryId,
  465. userId,
  466. { newFiles, newProject: project },
  467. error => {
  468. if (error != null) {
  469. return callback(error)
  470. }
  471. callback(null, fileRef, folderId)
  472. }
  473. )
  474. }
  475. )
  476. }
  477. }),
  478. replaceFile: wrapWithLock({
  479. beforeLock(next) {
  480. return function(
  481. projectId,
  482. fileId,
  483. fsPath,
  484. linkedFileData,
  485. userId,
  486. callback
  487. ) {
  488. // create a new file
  489. const fileArgs = {
  490. name: 'dummy-upload-filename',
  491. linkedFileData
  492. }
  493. FileStoreHandler.uploadFileFromDisk(
  494. projectId,
  495. fileArgs,
  496. fsPath,
  497. (err, fileStoreUrl, fileRef) => {
  498. if (err != null) {
  499. return callback(err)
  500. }
  501. next(
  502. projectId,
  503. fileId,
  504. fsPath,
  505. linkedFileData,
  506. userId,
  507. fileRef,
  508. fileStoreUrl,
  509. callback
  510. )
  511. }
  512. )
  513. }
  514. },
  515. withLock(
  516. projectId,
  517. fileId,
  518. fsPath,
  519. linkedFileData,
  520. userId,
  521. newFileRef,
  522. fileStoreUrl,
  523. callback
  524. ) {
  525. ProjectEntityMongoUpdateHandler.replaceFileWithNew(
  526. projectId,
  527. fileId,
  528. newFileRef,
  529. (err, oldFileRef, project, path, newProject) => {
  530. if (err != null) {
  531. return callback(err)
  532. }
  533. const oldFiles = [
  534. {
  535. file: oldFileRef,
  536. path: path.fileSystem
  537. }
  538. ]
  539. const newFiles = [
  540. {
  541. file: newFileRef,
  542. path: path.fileSystem,
  543. url: fileStoreUrl
  544. }
  545. ]
  546. const projectHistoryId =
  547. project.overleaf &&
  548. project.overleaf.history &&
  549. project.overleaf.history.id
  550. // Increment the rev for an in-place update (with the same path) so the third-party-datastore
  551. // knows this is a new file.
  552. // Ideally we would get this from ProjectEntityMongoUpdateHandler.replaceFileWithNew
  553. // but it returns the original oldFileRef (after incrementing the rev value in mongo),
  554. // so we add 1 to the rev from that. This isn't atomic and relies on the lock
  555. // but it is acceptable for now.
  556. TpdsUpdateSender.addFile(
  557. {
  558. project_id: project._id,
  559. file_id: newFileRef._id,
  560. path: path.fileSystem,
  561. rev: oldFileRef.rev + 1,
  562. project_name: project.name
  563. },
  564. err => {
  565. if (err != null) {
  566. return callback(err)
  567. }
  568. DocumentUpdaterHandler.updateProjectStructure(
  569. projectId,
  570. projectHistoryId,
  571. userId,
  572. { oldFiles, newFiles, newProject },
  573. callback
  574. )
  575. }
  576. )
  577. }
  578. )
  579. }
  580. }),
  581. upsertDoc: wrapWithLock(function(
  582. projectId,
  583. folderId,
  584. docName,
  585. docLines,
  586. source,
  587. userId,
  588. callback
  589. ) {
  590. if (!SafePath.isCleanFilename(docName)) {
  591. return callback(new Errors.InvalidNameError('invalid element name'))
  592. }
  593. ProjectLocator.findElement(
  594. { project_id: projectId, element_id: folderId, type: 'folder' },
  595. (error, folder, folderPath) => {
  596. if (error != null) {
  597. return callback(error)
  598. }
  599. if (folder == null) {
  600. return callback(new Error("Couldn't find folder"))
  601. }
  602. const existingDoc = folder.docs.find(({ name }) => name === docName)
  603. const existingFile = folder.fileRefs.find(
  604. ({ name }) => name === docName
  605. )
  606. if (existingFile) {
  607. const doc = new Doc({ name: docName })
  608. const filePath = `${folderPath.fileSystem}/${existingFile.name}`
  609. DocstoreManager.updateDoc(
  610. projectId.toString(),
  611. doc._id.toString(),
  612. docLines,
  613. 0,
  614. {},
  615. (err, modified, rev) => {
  616. if (err != null) {
  617. return callback(err)
  618. }
  619. ProjectEntityMongoUpdateHandler.replaceFileWithDoc(
  620. projectId,
  621. existingFile._id,
  622. doc,
  623. (err, project) => {
  624. if (err) {
  625. return callback(err)
  626. }
  627. TpdsUpdateSender.addDoc(
  628. {
  629. project_id: projectId,
  630. doc_id: doc._id,
  631. path: filePath,
  632. project_name: project.name,
  633. rev: existingFile.rev + 1
  634. },
  635. err => {
  636. if (err) {
  637. return callback(err)
  638. }
  639. const projectHistoryId =
  640. project.overleaf &&
  641. project.overleaf.history &&
  642. project.overleaf.history.id
  643. const newDocs = [
  644. {
  645. doc,
  646. path: filePath,
  647. docLines: docLines.join('\n')
  648. }
  649. ]
  650. const oldFiles = [
  651. {
  652. file: existingFile,
  653. path: filePath
  654. }
  655. ]
  656. DocumentUpdaterHandler.updateProjectStructure(
  657. projectId,
  658. projectHistoryId,
  659. userId,
  660. { oldFiles, newDocs, newProject: project },
  661. error => {
  662. if (error != null) {
  663. return callback(error)
  664. }
  665. EditorRealTimeController.emitToRoom(
  666. projectId,
  667. 'removeEntity',
  668. existingFile._id,
  669. 'convertFileToDoc'
  670. )
  671. callback(null, doc, true)
  672. }
  673. )
  674. }
  675. )
  676. }
  677. )
  678. }
  679. )
  680. } else if (existingDoc) {
  681. DocumentUpdaterHandler.setDocument(
  682. projectId,
  683. existingDoc._id,
  684. userId,
  685. docLines,
  686. source,
  687. err => {
  688. if (err != null) {
  689. return callback(err)
  690. }
  691. logger.log(
  692. { projectId, docId: existingDoc._id },
  693. 'notifying users that the document has been updated'
  694. )
  695. DocumentUpdaterHandler.flushDocToMongo(
  696. projectId,
  697. existingDoc._id,
  698. err => {
  699. if (err != null) {
  700. return callback(err)
  701. }
  702. callback(null, existingDoc, existingDoc == null)
  703. }
  704. )
  705. }
  706. )
  707. } else {
  708. ProjectEntityUpdateHandler.addDocWithRanges.withoutLock(
  709. projectId,
  710. folderId,
  711. docName,
  712. docLines,
  713. {},
  714. userId,
  715. (err, doc) => {
  716. if (err != null) {
  717. return callback(err)
  718. }
  719. callback(null, doc, existingDoc == null)
  720. }
  721. )
  722. }
  723. }
  724. )
  725. }),
  726. upsertFile: wrapWithLock({
  727. beforeLock(next) {
  728. return function(
  729. projectId,
  730. folderId,
  731. fileName,
  732. fsPath,
  733. linkedFileData,
  734. userId,
  735. callback
  736. ) {
  737. if (!SafePath.isCleanFilename(fileName)) {
  738. return callback(new Errors.InvalidNameError('invalid element name'))
  739. }
  740. // create a new file
  741. const fileArgs = {
  742. name: fileName,
  743. linkedFileData
  744. }
  745. FileStoreHandler.uploadFileFromDisk(
  746. projectId,
  747. fileArgs,
  748. fsPath,
  749. (err, fileStoreUrl, fileRef) => {
  750. if (err != null) {
  751. return callback(err)
  752. }
  753. next(
  754. projectId,
  755. folderId,
  756. fileName,
  757. fsPath,
  758. linkedFileData,
  759. userId,
  760. fileRef,
  761. fileStoreUrl,
  762. callback
  763. )
  764. }
  765. )
  766. }
  767. },
  768. withLock(
  769. projectId,
  770. folderId,
  771. fileName,
  772. fsPath,
  773. linkedFileData,
  774. userId,
  775. newFileRef,
  776. fileStoreUrl,
  777. callback
  778. ) {
  779. ProjectLocator.findElement(
  780. { project_id: projectId, element_id: folderId, type: 'folder' },
  781. (error, folder) => {
  782. if (error != null) {
  783. return callback(error)
  784. }
  785. if (folder == null) {
  786. return callback(new Error("Couldn't find folder"))
  787. }
  788. const existingFile = folder.fileRefs.find(
  789. ({ name }) => name === fileName
  790. )
  791. const existingDoc = folder.docs.find(({ name }) => name === fileName)
  792. if (existingDoc) {
  793. ProjectLocator.findElement(
  794. {
  795. project_id: projectId,
  796. element_id: existingDoc._id,
  797. type: 'doc'
  798. },
  799. (err, doc, path) => {
  800. if (err) {
  801. return callback(new Error('coudnt find existing file'))
  802. }
  803. ProjectEntityMongoUpdateHandler.replaceDocWithFile(
  804. projectId,
  805. existingDoc._id,
  806. newFileRef,
  807. (err, project) => {
  808. if (err) {
  809. return callback(err)
  810. }
  811. const projectHistoryId =
  812. project.overleaf &&
  813. project.overleaf.history &&
  814. project.overleaf.history.id
  815. TpdsUpdateSender.addFile(
  816. {
  817. project_id: project._id,
  818. file_id: newFileRef._id,
  819. path: path.fileSystem,
  820. rev: newFileRef.rev,
  821. project_name: project.name
  822. },
  823. err => {
  824. if (err) {
  825. return callback(err)
  826. }
  827. DocumentUpdaterHandler.updateProjectStructure(
  828. projectId,
  829. projectHistoryId,
  830. userId,
  831. {
  832. oldDocs: [
  833. { doc: existingDoc, path: path.fileSystem }
  834. ],
  835. newFiles: [
  836. {
  837. file: newFileRef,
  838. path: path.fileSystem,
  839. url: fileStoreUrl
  840. }
  841. ],
  842. newProject: project
  843. },
  844. err => {
  845. if (err) {
  846. return callback(err)
  847. }
  848. EditorRealTimeController.emitToRoom(
  849. projectId,
  850. 'removeEntity',
  851. existingDoc._id,
  852. 'convertDocToFile'
  853. )
  854. callback(null, newFileRef, true, existingFile)
  855. }
  856. )
  857. }
  858. )
  859. }
  860. )
  861. }
  862. )
  863. } else if (existingFile) {
  864. // this calls directly into the replaceFile main task (without the beforeLock part)
  865. return ProjectEntityUpdateHandler.replaceFile.mainTask(
  866. projectId,
  867. existingFile._id,
  868. fsPath,
  869. linkedFileData,
  870. userId,
  871. newFileRef,
  872. fileStoreUrl,
  873. err => {
  874. if (err != null) {
  875. return callback(err)
  876. }
  877. callback(null, newFileRef, existingFile == null, existingFile)
  878. }
  879. )
  880. } else {
  881. // this calls directly into the addFile main task (without the beforeLock part)
  882. ProjectEntityUpdateHandler.addFile.mainTask(
  883. projectId,
  884. folderId,
  885. fileName,
  886. fsPath,
  887. linkedFileData,
  888. userId,
  889. newFileRef,
  890. fileStoreUrl,
  891. err => {
  892. if (err != null) {
  893. return callback(err)
  894. }
  895. callback(null, newFileRef, existingFile == null, existingFile)
  896. }
  897. )
  898. }
  899. }
  900. )
  901. }
  902. }),
  903. upsertDocWithPath: wrapWithLock(function(
  904. projectId,
  905. elementPath,
  906. docLines,
  907. source,
  908. userId,
  909. callback
  910. ) {
  911. if (!SafePath.isCleanPath(elementPath)) {
  912. return callback(new Errors.InvalidNameError('invalid element name'))
  913. }
  914. const docName = Path.basename(elementPath)
  915. const folderPath = Path.dirname(elementPath)
  916. ProjectEntityUpdateHandler.mkdirp.withoutLock(
  917. projectId,
  918. folderPath,
  919. (err, newFolders, folder) => {
  920. if (err != null) {
  921. return callback(err)
  922. }
  923. ProjectEntityUpdateHandler.upsertDoc.withoutLock(
  924. projectId,
  925. folder._id,
  926. docName,
  927. docLines,
  928. source,
  929. userId,
  930. (err, doc, isNewDoc) => {
  931. if (err != null) {
  932. return callback(err)
  933. }
  934. callback(null, doc, isNewDoc, newFolders, folder)
  935. }
  936. )
  937. }
  938. )
  939. }),
  940. upsertFileWithPath: wrapWithLock({
  941. beforeLock(next) {
  942. return function(
  943. projectId,
  944. elementPath,
  945. fsPath,
  946. linkedFileData,
  947. userId,
  948. callback
  949. ) {
  950. if (!SafePath.isCleanPath(elementPath)) {
  951. return callback(new Errors.InvalidNameError('invalid element name'))
  952. }
  953. const fileName = Path.basename(elementPath)
  954. const folderPath = Path.dirname(elementPath)
  955. // create a new file
  956. const fileArgs = {
  957. name: fileName,
  958. linkedFileData
  959. }
  960. FileStoreHandler.uploadFileFromDisk(
  961. projectId,
  962. fileArgs,
  963. fsPath,
  964. (err, fileStoreUrl, fileRef) => {
  965. if (err != null) {
  966. return callback(err)
  967. }
  968. next(
  969. projectId,
  970. folderPath,
  971. fileName,
  972. fsPath,
  973. linkedFileData,
  974. userId,
  975. fileRef,
  976. fileStoreUrl,
  977. callback
  978. )
  979. }
  980. )
  981. }
  982. },
  983. withLock(
  984. projectId,
  985. folderPath,
  986. fileName,
  987. fsPath,
  988. linkedFileData,
  989. userId,
  990. fileRef,
  991. fileStoreUrl,
  992. callback
  993. ) {
  994. ProjectEntityUpdateHandler.mkdirp.withoutLock(
  995. projectId,
  996. folderPath,
  997. (err, newFolders, folder) => {
  998. if (err != null) {
  999. return callback(err)
  1000. }
  1001. // this calls directly into the upsertFile main task (without the beforeLock part)
  1002. ProjectEntityUpdateHandler.upsertFile.mainTask(
  1003. projectId,
  1004. folder._id,
  1005. fileName,
  1006. fsPath,
  1007. linkedFileData,
  1008. userId,
  1009. fileRef,
  1010. fileStoreUrl,
  1011. (err, newFile, isNewFile, existingFile) => {
  1012. if (err != null) {
  1013. return callback(err)
  1014. }
  1015. callback(
  1016. null,
  1017. newFile,
  1018. isNewFile,
  1019. existingFile,
  1020. newFolders,
  1021. folder
  1022. )
  1023. }
  1024. )
  1025. }
  1026. )
  1027. }
  1028. }),
  1029. deleteEntity: wrapWithLock(function(
  1030. projectId,
  1031. entityId,
  1032. entityType,
  1033. userId,
  1034. callback
  1035. ) {
  1036. logger.log({ entityId, entityType, projectId }, 'deleting project entity')
  1037. if (entityType == null) {
  1038. logger.warn({ err: 'No entityType set', projectId, entityId })
  1039. return callback(new Error('No entityType set'))
  1040. }
  1041. entityType = entityType.toLowerCase()
  1042. ProjectEntityMongoUpdateHandler.deleteEntity(
  1043. projectId,
  1044. entityId,
  1045. entityType,
  1046. (error, entity, path, projectBeforeDeletion, newProject) => {
  1047. if (error != null) {
  1048. return callback(error)
  1049. }
  1050. ProjectEntityUpdateHandler._cleanUpEntity(
  1051. projectBeforeDeletion,
  1052. newProject,
  1053. entity,
  1054. entityType,
  1055. path.fileSystem,
  1056. userId,
  1057. error => {
  1058. if (error != null) {
  1059. return callback(error)
  1060. }
  1061. TpdsUpdateSender.deleteEntity(
  1062. {
  1063. project_id: projectId,
  1064. path: path.fileSystem,
  1065. project_name: projectBeforeDeletion.name
  1066. },
  1067. error => {
  1068. if (error != null) {
  1069. return callback(error)
  1070. }
  1071. callback(null, entityId)
  1072. }
  1073. )
  1074. }
  1075. )
  1076. }
  1077. )
  1078. }),
  1079. deleteEntityWithPath: wrapWithLock((projectId, path, userId, callback) =>
  1080. ProjectLocator.findElementByPath(
  1081. { project_id: projectId, path },
  1082. (err, element, type) => {
  1083. if (err != null) {
  1084. return callback(err)
  1085. }
  1086. if (element == null) {
  1087. return callback(new Errors.NotFoundError('project not found'))
  1088. }
  1089. ProjectEntityUpdateHandler.deleteEntity.withoutLock(
  1090. projectId,
  1091. element._id,
  1092. type,
  1093. userId,
  1094. callback
  1095. )
  1096. }
  1097. )
  1098. ),
  1099. mkdirp: wrapWithLock(function(projectId, path, callback) {
  1100. for (let folder of path.split('/')) {
  1101. if (folder.length > 0 && !SafePath.isCleanFilename(folder)) {
  1102. return callback(new Errors.InvalidNameError('invalid element name'))
  1103. }
  1104. }
  1105. ProjectEntityMongoUpdateHandler.mkdirp(
  1106. projectId,
  1107. path,
  1108. { exactCaseMatch: false },
  1109. callback
  1110. )
  1111. }),
  1112. mkdirpWithExactCase: wrapWithLock(function(projectId, path, callback) {
  1113. for (let folder of path.split('/')) {
  1114. if (folder.length > 0 && !SafePath.isCleanFilename(folder)) {
  1115. return callback(new Errors.InvalidNameError('invalid element name'))
  1116. }
  1117. }
  1118. ProjectEntityMongoUpdateHandler.mkdirp(
  1119. projectId,
  1120. path,
  1121. { exactCaseMatch: true },
  1122. callback
  1123. )
  1124. }),
  1125. addFolder: wrapWithLock(function(
  1126. projectId,
  1127. parentFolderId,
  1128. folderName,
  1129. callback
  1130. ) {
  1131. if (!SafePath.isCleanFilename(folderName)) {
  1132. return callback(new Errors.InvalidNameError('invalid element name'))
  1133. }
  1134. ProjectEntityMongoUpdateHandler.addFolder(
  1135. projectId,
  1136. parentFolderId,
  1137. folderName,
  1138. callback
  1139. )
  1140. }),
  1141. moveEntity: wrapWithLock(function(
  1142. projectId,
  1143. entityId,
  1144. destFolderId,
  1145. entityType,
  1146. userId,
  1147. callback
  1148. ) {
  1149. logger.log(
  1150. { entityType, entityId, projectId, destFolderId },
  1151. 'moving entity'
  1152. )
  1153. if (entityType == null) {
  1154. logger.warn({ err: 'No entityType set', projectId, entityId })
  1155. return callback(new Error('No entityType set'))
  1156. }
  1157. entityType = entityType.toLowerCase()
  1158. ProjectEntityMongoUpdateHandler.moveEntity(
  1159. projectId,
  1160. entityId,
  1161. destFolderId,
  1162. entityType,
  1163. (err, project, startPath, endPath, rev, changes) => {
  1164. if (err != null) {
  1165. return callback(err)
  1166. }
  1167. const projectHistoryId =
  1168. project.overleaf &&
  1169. project.overleaf.history &&
  1170. project.overleaf.history.id
  1171. // do not wait
  1172. TpdsUpdateSender.promises
  1173. .moveEntity({
  1174. project_id: projectId,
  1175. project_name: project.name,
  1176. startPath,
  1177. endPath,
  1178. rev
  1179. })
  1180. .catch(err => {
  1181. logger.error({ err }, 'error sending tpds update')
  1182. })
  1183. DocumentUpdaterHandler.updateProjectStructure(
  1184. projectId,
  1185. projectHistoryId,
  1186. userId,
  1187. changes,
  1188. callback
  1189. )
  1190. }
  1191. )
  1192. }),
  1193. renameEntity: wrapWithLock(function(
  1194. projectId,
  1195. entityId,
  1196. entityType,
  1197. newName,
  1198. userId,
  1199. callback
  1200. ) {
  1201. if (!SafePath.isCleanFilename(newName)) {
  1202. return callback(new Errors.InvalidNameError('invalid element name'))
  1203. }
  1204. logger.log({ entityId, projectId }, `renaming ${entityType}`)
  1205. if (entityType == null) {
  1206. logger.warn({ err: 'No entityType set', projectId, entityId })
  1207. return callback(new Error('No entityType set'))
  1208. }
  1209. entityType = entityType.toLowerCase()
  1210. ProjectEntityMongoUpdateHandler.renameEntity(
  1211. projectId,
  1212. entityId,
  1213. entityType,
  1214. newName,
  1215. (err, project, startPath, endPath, rev, changes) => {
  1216. if (err != null) {
  1217. return callback(err)
  1218. }
  1219. const projectHistoryId =
  1220. project.overleaf &&
  1221. project.overleaf.history &&
  1222. project.overleaf.history.id
  1223. // do not wait
  1224. TpdsUpdateSender.promises
  1225. .moveEntity({
  1226. project_id: projectId,
  1227. project_name: project.name,
  1228. startPath,
  1229. endPath,
  1230. rev
  1231. })
  1232. .catch(err => {
  1233. logger.error({ err }, 'error sending tpds update')
  1234. })
  1235. DocumentUpdaterHandler.updateProjectStructure(
  1236. projectId,
  1237. projectHistoryId,
  1238. userId,
  1239. changes,
  1240. callback
  1241. )
  1242. }
  1243. )
  1244. }),
  1245. // This doesn't directly update project structure but we need to take the lock
  1246. // to prevent anything else being queued before the resync update
  1247. resyncProjectHistory: wrapWithLock((projectId, callback) =>
  1248. ProjectGetter.getProject(
  1249. projectId,
  1250. { rootFolder: true, overleaf: true },
  1251. (error, project) => {
  1252. if (error != null) {
  1253. return callback(error)
  1254. }
  1255. const projectHistoryId =
  1256. project &&
  1257. project.overleaf &&
  1258. project.overleaf.history &&
  1259. project.overleaf.history.id
  1260. if (projectHistoryId == null) {
  1261. error = new Errors.ProjectHistoryDisabledError(
  1262. `project history not enabled for ${projectId}`
  1263. )
  1264. return callback(error)
  1265. }
  1266. ProjectEntityHandler.getAllEntitiesFromProject(
  1267. project,
  1268. (error, docs, files) => {
  1269. if (error != null) {
  1270. return callback(error)
  1271. }
  1272. docs = _.map(docs, doc => ({
  1273. doc: doc.doc._id,
  1274. path: doc.path
  1275. }))
  1276. files = _.map(files, file => ({
  1277. file: file.file._id,
  1278. path: file.path,
  1279. url: FileStoreHandler._buildUrl(projectId, file.file._id)
  1280. }))
  1281. DocumentUpdaterHandler.resyncProjectHistory(
  1282. projectId,
  1283. projectHistoryId,
  1284. docs,
  1285. files,
  1286. callback
  1287. )
  1288. }
  1289. )
  1290. }
  1291. )
  1292. ),
  1293. isPathValidForRootDoc(docPath) {
  1294. let docExtension = Path.extname(docPath)
  1295. return VALID_ROOT_DOC_REGEXP.test(docExtension)
  1296. },
  1297. _cleanUpEntity(
  1298. project,
  1299. newProject,
  1300. entity,
  1301. entityType,
  1302. path,
  1303. userId,
  1304. callback
  1305. ) {
  1306. ProjectEntityUpdateHandler._updateProjectStructureWithDeletedEntity(
  1307. project,
  1308. newProject,
  1309. entity,
  1310. entityType,
  1311. path,
  1312. userId,
  1313. error => {
  1314. if (error != null) {
  1315. return callback(error)
  1316. }
  1317. if (entityType.indexOf('file') !== -1) {
  1318. ProjectEntityUpdateHandler._cleanUpFile(
  1319. project,
  1320. entity,
  1321. path,
  1322. userId,
  1323. callback
  1324. )
  1325. } else if (entityType.indexOf('doc') !== -1) {
  1326. ProjectEntityUpdateHandler._cleanUpDoc(
  1327. project,
  1328. entity,
  1329. path,
  1330. userId,
  1331. callback
  1332. )
  1333. } else if (entityType.indexOf('folder') !== -1) {
  1334. ProjectEntityUpdateHandler._cleanUpFolder(
  1335. project,
  1336. entity,
  1337. path,
  1338. userId,
  1339. callback
  1340. )
  1341. } else {
  1342. callback()
  1343. }
  1344. }
  1345. )
  1346. },
  1347. // Note: the _cleanUpEntity code and _updateProjectStructureWithDeletedEntity
  1348. // methods both need to recursively iterate over the entities in folder.
  1349. // These are currently using separate implementations of the recursion. In
  1350. // future, these could be simplified using a common project entity iterator.
  1351. _updateProjectStructureWithDeletedEntity(
  1352. project,
  1353. newProject,
  1354. entity,
  1355. entityType,
  1356. entityPath,
  1357. userId,
  1358. callback
  1359. ) {
  1360. // compute the changes to the project structure
  1361. let changes
  1362. if (entityType.indexOf('file') !== -1) {
  1363. changes = { oldFiles: [{ file: entity, path: entityPath }] }
  1364. } else if (entityType.indexOf('doc') !== -1) {
  1365. changes = { oldDocs: [{ doc: entity, path: entityPath }] }
  1366. } else if (entityType.indexOf('folder') !== -1) {
  1367. changes = { oldDocs: [], oldFiles: [] }
  1368. const _recurseFolder = (folder, folderPath) => {
  1369. for (let doc of folder.docs) {
  1370. changes.oldDocs.push({ doc, path: Path.join(folderPath, doc.name) })
  1371. }
  1372. for (let file of folder.fileRefs) {
  1373. changes.oldFiles.push({
  1374. file,
  1375. path: Path.join(folderPath, file.name)
  1376. })
  1377. }
  1378. for (const childFolder of folder.folders) {
  1379. _recurseFolder(childFolder, Path.join(folderPath, childFolder.name))
  1380. }
  1381. }
  1382. _recurseFolder(entity, entityPath)
  1383. }
  1384. // now send the project structure changes to the docupdater
  1385. changes.newProject = newProject
  1386. const projectId = project._id.toString()
  1387. const projectHistoryId =
  1388. project.overleaf &&
  1389. project.overleaf.history &&
  1390. project.overleaf.history.id
  1391. DocumentUpdaterHandler.updateProjectStructure(
  1392. projectId,
  1393. projectHistoryId,
  1394. userId,
  1395. changes,
  1396. callback
  1397. )
  1398. },
  1399. _cleanUpDoc(project, doc, path, userId, callback) {
  1400. const projectId = project._id.toString()
  1401. const docId = doc._id.toString()
  1402. const unsetRootDocIfRequired = callback => {
  1403. if (
  1404. project.rootDoc_id != null &&
  1405. project.rootDoc_id.toString() === docId
  1406. ) {
  1407. ProjectEntityUpdateHandler.unsetRootDoc(projectId, callback)
  1408. } else {
  1409. callback()
  1410. }
  1411. }
  1412. unsetRootDocIfRequired(error => {
  1413. if (error != null) {
  1414. return callback(error)
  1415. }
  1416. ProjectEntityMongoUpdateHandler._insertDeletedDocReference(
  1417. project._id,
  1418. doc,
  1419. error => {
  1420. if (error != null) {
  1421. return callback(error)
  1422. }
  1423. DocumentUpdaterHandler.deleteDoc(projectId, docId, error => {
  1424. if (error != null) {
  1425. return callback(error)
  1426. }
  1427. DocstoreManager.deleteDoc(projectId, docId, callback)
  1428. })
  1429. }
  1430. )
  1431. })
  1432. },
  1433. _cleanUpFile(project, file, path, userId, callback) {
  1434. ProjectEntityMongoUpdateHandler._insertDeletedFileReference(
  1435. project._id,
  1436. file,
  1437. callback
  1438. )
  1439. },
  1440. _cleanUpFolder(project, folder, folderPath, userId, callback) {
  1441. const jobs = []
  1442. folder.docs.forEach(doc => {
  1443. const docPath = Path.join(folderPath, doc.name)
  1444. jobs.push(callback =>
  1445. ProjectEntityUpdateHandler._cleanUpDoc(
  1446. project,
  1447. doc,
  1448. docPath,
  1449. userId,
  1450. callback
  1451. )
  1452. )
  1453. })
  1454. folder.fileRefs.forEach(file => {
  1455. const filePath = Path.join(folderPath, file.name)
  1456. jobs.push(callback =>
  1457. ProjectEntityUpdateHandler._cleanUpFile(
  1458. project,
  1459. file,
  1460. filePath,
  1461. userId,
  1462. callback
  1463. )
  1464. )
  1465. })
  1466. folder.folders.forEach(childFolder => {
  1467. folderPath = Path.join(folderPath, childFolder.name)
  1468. jobs.push(callback =>
  1469. ProjectEntityUpdateHandler._cleanUpFolder(
  1470. project,
  1471. childFolder,
  1472. folderPath,
  1473. userId,
  1474. callback
  1475. )
  1476. )
  1477. })
  1478. async.series(jobs, callback)
  1479. },
  1480. convertDocToFile: wrapWithLock({
  1481. beforeLock(next) {
  1482. return function(projectId, docId, userId, callback) {
  1483. DocumentUpdaterHandler.flushDocToMongo(projectId, docId, err => {
  1484. if (err) {
  1485. return callback(err)
  1486. }
  1487. ProjectLocator.findElement(
  1488. { project_id: projectId, element_id: docId, type: 'doc' },
  1489. (err, doc, path) => {
  1490. const docPath = path.fileSystem
  1491. if (err) {
  1492. return callback(err)
  1493. }
  1494. DocstoreManager.getDoc(
  1495. projectId,
  1496. docId,
  1497. (err, docLines, rev, version, ranges) => {
  1498. if (err) {
  1499. return callback(err)
  1500. }
  1501. if (!_.isEmpty(ranges)) {
  1502. return callback(new Errors.DocHasRangesError({}))
  1503. }
  1504. DocumentUpdaterHandler.deleteDoc(projectId, docId, err => {
  1505. if (err) {
  1506. return callback(err)
  1507. }
  1508. FileWriter.writeLinesToDisk(
  1509. projectId,
  1510. docLines,
  1511. (err, fsPath) => {
  1512. if (err) {
  1513. return callback(err)
  1514. }
  1515. FileStoreHandler.uploadFileFromDisk(
  1516. projectId,
  1517. { name: doc.name, rev: rev + 1 },
  1518. fsPath,
  1519. (err, fileStoreUrl, fileRef) => {
  1520. if (err) {
  1521. return callback(err)
  1522. }
  1523. fs.unlink(fsPath, err => {
  1524. if (err) {
  1525. logger.warn(
  1526. { err, path: fsPath },
  1527. 'failed to clean up temporary file'
  1528. )
  1529. }
  1530. next(
  1531. projectId,
  1532. doc,
  1533. docPath,
  1534. fileRef,
  1535. fileStoreUrl,
  1536. userId,
  1537. callback
  1538. )
  1539. })
  1540. }
  1541. )
  1542. }
  1543. )
  1544. })
  1545. }
  1546. )
  1547. }
  1548. )
  1549. })
  1550. }
  1551. },
  1552. withLock(projectId, doc, path, fileRef, fileStoreUrl, userId, callback) {
  1553. ProjectEntityMongoUpdateHandler.replaceDocWithFile(
  1554. projectId,
  1555. doc._id,
  1556. fileRef,
  1557. (err, project) => {
  1558. if (err) {
  1559. return callback(err)
  1560. }
  1561. const projectHistoryId =
  1562. project.overleaf &&
  1563. project.overleaf.history &&
  1564. project.overleaf.history.id
  1565. DocumentUpdaterHandler.updateProjectStructure(
  1566. projectId,
  1567. projectHistoryId,
  1568. userId,
  1569. {
  1570. oldDocs: [{ doc, path }],
  1571. newFiles: [{ file: fileRef, path, url: fileStoreUrl }],
  1572. newProject: project
  1573. },
  1574. err => {
  1575. if (err) {
  1576. return callback(err)
  1577. }
  1578. ProjectLocator.findElement(
  1579. {
  1580. project_id: projectId,
  1581. element_id: fileRef._id,
  1582. type: 'file'
  1583. },
  1584. (err, element, path, folder) => {
  1585. if (err) {
  1586. return callback(err)
  1587. }
  1588. EditorRealTimeController.emitToRoom(
  1589. projectId,
  1590. 'removeEntity',
  1591. doc._id,
  1592. 'convertDocToFile'
  1593. )
  1594. EditorRealTimeController.emitToRoom(
  1595. projectId,
  1596. 'reciveNewFile',
  1597. folder._id,
  1598. fileRef,
  1599. 'convertDocToFile',
  1600. null
  1601. )
  1602. callback(null, fileRef)
  1603. }
  1604. )
  1605. }
  1606. )
  1607. }
  1608. )
  1609. }
  1610. })
  1611. }
  1612. module.exports = ProjectEntityUpdateHandler
  1613. module.exports.promises = promisifyAll(ProjectEntityUpdateHandler, {
  1614. without: ['isPathValidForRootDoc'],
  1615. multiResult: {
  1616. _addDocAndSendToTpds: ['result', 'project'],
  1617. addDoc: ['doc', 'folderId'],
  1618. addDocWithRanges: ['doc', 'folderId'],
  1619. _uploadFile: ['fileStoreUrl', 'fileRef'],
  1620. _addFileAndSendToTpds: ['result', 'project'],
  1621. addFile: ['fileRef', 'folderId'],
  1622. upsertDoc: ['doc', 'isNew'],
  1623. upsertFile: ['fileRef', 'isNew', 'oldFileRef'],
  1624. upsertDocWithPath: ['doc', 'isNew', 'newFolders', 'folder'],
  1625. upsertFileWithPath: ['fileRef', 'isNew', 'oldFile', 'newFolders', 'folder'],
  1626. mkdirp: ['newFolders', 'folder'],
  1627. mkdirpWithExactCase: ['newFolders', 'folder'],
  1628. addFolder: ['folder', 'parentFolderId']
  1629. }
  1630. })