ProjectEntityUpdateHandler.js 57 KB

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