ProjectEntityUpdateHandler.js 47 KB

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