ProjectEntityUpdateHandler.js 49 KB

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