ProjectEntityUpdateHandler.js 54 KB

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