HttpController.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. import logger from '@overleaf/logger'
  2. import OError from '@overleaf/o-error'
  3. import request from 'request'
  4. import * as UpdatesProcessor from './UpdatesProcessor.js'
  5. import * as SummarizedUpdatesManager from './SummarizedUpdatesManager.js'
  6. import * as DiffManager from './DiffManager.js'
  7. import * as HistoryStoreManager from './HistoryStoreManager.js'
  8. import * as WebApiManager from './WebApiManager.js'
  9. import * as SnapshotManager from './SnapshotManager.js'
  10. import * as HealthChecker from './HealthChecker.js'
  11. import * as SyncManager from './SyncManager.js'
  12. import * as ErrorRecorder from './ErrorRecorder.js'
  13. import * as RedisManager from './RedisManager.js'
  14. import * as LabelsManager from './LabelsManager.js'
  15. import * as HistoryApiManager from './HistoryApiManager.js'
  16. import * as RetryManager from './RetryManager.js'
  17. import * as FlushManager from './FlushManager.js'
  18. import { pipeline } from 'node:stream'
  19. import { RequestFailedError } from '@overleaf/fetch-utils'
  20. const ONE_DAY_IN_SECONDS = 24 * 60 * 60
  21. export function getProjectBlob(req, res, next) {
  22. const historyId = req.params.history_id
  23. const blobHash = req.params.hash
  24. HistoryStoreManager.getProjectBlobStream(
  25. historyId,
  26. blobHash,
  27. (err, stream) => {
  28. if (err != null) {
  29. if (err instanceof RequestFailedError && err.response.status === 404) {
  30. return res.status(404).end()
  31. }
  32. return next(OError.tag(err))
  33. }
  34. res.setHeader('Cache-Control', `private, max-age=${ONE_DAY_IN_SECONDS}`)
  35. pipeline(stream, res, err => {
  36. if (err) next(err)
  37. // res.end() is already called via 'end' event by pipeline.
  38. })
  39. }
  40. )
  41. }
  42. export function initializeProject(req, res, next) {
  43. const { historyId } = req.body
  44. HistoryStoreManager.initializeProject(historyId, (error, id) => {
  45. if (error != null) {
  46. return next(OError.tag(error))
  47. }
  48. res.json({ project: { id } })
  49. })
  50. }
  51. export function flushProject(req, res, next) {
  52. const projectId = req.params.project_id
  53. if (req.query.debug) {
  54. logger.debug(
  55. { projectId },
  56. 'compressing project history in single-step mode'
  57. )
  58. UpdatesProcessor.processSingleUpdateForProject(projectId, error => {
  59. if (error != null) {
  60. return next(OError.tag(error))
  61. }
  62. res.sendStatus(204)
  63. })
  64. } else if (req.query.bisect) {
  65. logger.debug({ projectId }, 'compressing project history in bisect mode')
  66. UpdatesProcessor.processUpdatesForProjectUsingBisect(
  67. projectId,
  68. UpdatesProcessor.REDIS_READ_BATCH_SIZE,
  69. error => {
  70. if (error != null) {
  71. return next(OError.tag(error))
  72. }
  73. res.sendStatus(204)
  74. }
  75. )
  76. } else {
  77. logger.debug({ projectId }, 'compressing project history')
  78. UpdatesProcessor.processUpdatesForProject(projectId, error => {
  79. if (error != null) {
  80. return next(OError.tag(error))
  81. }
  82. res.sendStatus(204)
  83. })
  84. }
  85. }
  86. export function dumpProject(req, res, next) {
  87. const projectId = req.params.project_id
  88. const batchSize = req.query.count || UpdatesProcessor.REDIS_READ_BATCH_SIZE
  89. logger.debug({ projectId }, 'retrieving raw updates')
  90. UpdatesProcessor.getRawUpdates(projectId, batchSize, (error, rawUpdates) => {
  91. if (error != null) {
  92. return next(OError.tag(error))
  93. }
  94. res.json(rawUpdates)
  95. })
  96. }
  97. export function flushOld(req, res, next) {
  98. const { maxAge, queueDelay, limit, timeout, background } = req.query
  99. const options = { maxAge, queueDelay, limit, timeout, background }
  100. FlushManager.flushOldOps(options, (error, results) => {
  101. if (error != null) {
  102. return next(OError.tag(error))
  103. }
  104. res.send(results)
  105. })
  106. }
  107. export function getDiff(req, res, next) {
  108. const projectId = req.params.project_id
  109. const { pathname, from, to } = req.query
  110. if (pathname == null) {
  111. return res.sendStatus(400)
  112. }
  113. logger.debug({ projectId, pathname, from, to }, 'getting diff')
  114. DiffManager.getDiff(projectId, pathname, from, to, (error, diff) => {
  115. if (error != null) {
  116. return next(OError.tag(error))
  117. }
  118. res.json({ diff })
  119. })
  120. }
  121. export function getFileTreeDiff(req, res, next) {
  122. const projectId = req.params.project_id
  123. const { to, from } = req.query
  124. DiffManager.getFileTreeDiff(projectId, from, to, (error, diff) => {
  125. if (error != null) {
  126. return next(OError.tag(error))
  127. }
  128. res.json({ diff })
  129. })
  130. }
  131. export function getUpdates(req, res, next) {
  132. const projectId = req.params.project_id
  133. const { before, min_count: minCount } = req.query
  134. SummarizedUpdatesManager.getSummarizedProjectUpdates(
  135. projectId,
  136. { before, min_count: minCount },
  137. (error, updates, nextBeforeTimestamp) => {
  138. if (error != null) {
  139. return next(OError.tag(error))
  140. }
  141. for (const update of updates) {
  142. // Sets don't JSONify, so convert to arrays
  143. update.pathnames = Array.from(update.pathnames || []).sort()
  144. }
  145. res.json({
  146. updates,
  147. nextBeforeTimestamp,
  148. })
  149. }
  150. )
  151. }
  152. export function latestVersion(req, res, next) {
  153. const projectId = req.params.project_id
  154. logger.debug({ projectId }, 'compressing project history and getting version')
  155. UpdatesProcessor.processUpdatesForProject(projectId, error => {
  156. if (error != null) {
  157. return next(OError.tag(error))
  158. }
  159. WebApiManager.getHistoryId(projectId, (error, historyId) => {
  160. if (error != null) {
  161. return next(OError.tag(error))
  162. }
  163. HistoryStoreManager.getMostRecentVersion(
  164. projectId,
  165. historyId,
  166. (error, version, projectStructureAndDocVersions, lastChange) => {
  167. if (error != null) {
  168. return next(OError.tag(error))
  169. }
  170. res.json({
  171. version,
  172. timestamp: lastChange != null ? lastChange.timestamp : undefined,
  173. v2Authors: lastChange != null ? lastChange.v2Authors : undefined,
  174. })
  175. }
  176. )
  177. })
  178. })
  179. }
  180. export function getFileSnapshot(req, res, next) {
  181. const { project_id: projectId, version, pathname } = req.params
  182. SnapshotManager.getFileSnapshotStream(
  183. projectId,
  184. version,
  185. pathname,
  186. (error, stream) => {
  187. if (error != null) {
  188. return next(OError.tag(error))
  189. }
  190. pipeline(stream, res, err => {
  191. if (err) next(err)
  192. // res.end() is already called via 'end' event by pipeline.
  193. })
  194. }
  195. )
  196. }
  197. export function getRangesSnapshot(req, res, next) {
  198. const { project_id: projectId, version, pathname } = req.params
  199. SnapshotManager.getRangesSnapshot(
  200. projectId,
  201. version,
  202. pathname,
  203. (err, ranges) => {
  204. if (err) {
  205. return next(OError.tag(err))
  206. }
  207. res.json(ranges)
  208. }
  209. )
  210. }
  211. export function getFileMetadataSnapshot(req, res, next) {
  212. const { project_id: projectId, version, pathname } = req.params
  213. SnapshotManager.getFileMetadataSnapshot(
  214. projectId,
  215. version,
  216. pathname,
  217. (err, data) => {
  218. if (err) {
  219. return next(OError.tag(err))
  220. }
  221. res.json(data)
  222. }
  223. )
  224. }
  225. export function getMostRecentChunk(req, res, next) {
  226. const { project_id: projectId } = req.params
  227. WebApiManager.getHistoryId(projectId, (error, historyId) => {
  228. if (error) return next(OError.tag(error))
  229. HistoryStoreManager.getMostRecentChunk(
  230. projectId,
  231. historyId,
  232. (err, data) => {
  233. if (err) return next(OError.tag(err))
  234. res.json(data)
  235. }
  236. )
  237. })
  238. }
  239. export function getLatestSnapshot(req, res, next) {
  240. const { project_id: projectId } = req.params
  241. WebApiManager.getHistoryId(projectId, (error, historyId) => {
  242. if (error) return next(OError.tag(error))
  243. SnapshotManager.getLatestSnapshot(
  244. projectId,
  245. historyId,
  246. (error, details) => {
  247. if (error != null) {
  248. return next(error)
  249. }
  250. const { snapshot, version } = details
  251. res.json({ snapshot: snapshot.toRaw(), version })
  252. }
  253. )
  254. })
  255. }
  256. export function getChangesSince(req, res, next) {
  257. const { project_id: projectId } = req.params
  258. const { since } = req.query
  259. WebApiManager.getHistoryId(projectId, (error, historyId) => {
  260. if (error) return next(OError.tag(error))
  261. SnapshotManager.getChangesSince(
  262. projectId,
  263. historyId,
  264. since,
  265. (error, changes) => {
  266. if (error != null) {
  267. return next(error)
  268. }
  269. res.json(changes.map(c => c.toRaw()))
  270. }
  271. )
  272. })
  273. }
  274. export function getChangesInChunkSince(req, res, next) {
  275. const { project_id: projectId } = req.params
  276. const { since } = req.query
  277. WebApiManager.getHistoryId(projectId, (error, historyId) => {
  278. if (error) return next(OError.tag(error))
  279. SnapshotManager.getChangesInChunkSince(
  280. projectId,
  281. historyId,
  282. since,
  283. (error, details) => {
  284. if (error != null) {
  285. return next(error)
  286. }
  287. const { latestStartVersion, changes } = details
  288. res.json({
  289. latestStartVersion,
  290. changes: changes.map(c => c.toRaw()),
  291. })
  292. }
  293. )
  294. })
  295. }
  296. export function getProjectSnapshot(req, res, next) {
  297. const { project_id: projectId, version } = req.params
  298. SnapshotManager.getProjectSnapshot(
  299. projectId,
  300. version,
  301. (error, snapshotData) => {
  302. if (error != null) {
  303. return next(error)
  304. }
  305. res.json(snapshotData)
  306. }
  307. )
  308. }
  309. export function getPathsAtVersion(req, res, next) {
  310. const { project_id: projectId, version } = req.params
  311. SnapshotManager.getPathsAtVersion(projectId, version, (error, result) => {
  312. if (error != null) {
  313. return next(error)
  314. }
  315. res.json(result)
  316. })
  317. }
  318. export function healthCheck(req, res) {
  319. HealthChecker.check(err => {
  320. if (err != null) {
  321. logger.err({ err }, 'error performing health check')
  322. res.sendStatus(500)
  323. } else {
  324. res.sendStatus(200)
  325. }
  326. })
  327. }
  328. export function checkLock(req, res) {
  329. HealthChecker.checkLock(err => {
  330. if (err != null) {
  331. logger.err({ err }, 'error performing lock check')
  332. res.sendStatus(500)
  333. } else {
  334. res.sendStatus(200)
  335. }
  336. })
  337. }
  338. export function resyncProject(req, res, next) {
  339. const projectId = req.params.project_id
  340. const options = {}
  341. if (req.body.origin) {
  342. options.origin = req.body.origin
  343. }
  344. if (req.body.historyRangesMigration) {
  345. options.historyRangesMigration = req.body.historyRangesMigration
  346. }
  347. if (req.query.force || req.body.force) {
  348. // this will delete the queue and clear the sync state
  349. // use if the project is completely broken
  350. SyncManager.startHardResync(projectId, options, error => {
  351. if (error != null) {
  352. return next(error)
  353. }
  354. // flush the sync operations
  355. UpdatesProcessor.processUpdatesForProject(projectId, error => {
  356. if (error != null) {
  357. return next(error)
  358. }
  359. res.sendStatus(204)
  360. })
  361. })
  362. } else {
  363. SyncManager.startResync(projectId, options, error => {
  364. if (error != null) {
  365. return next(error)
  366. }
  367. // flush the sync operations
  368. UpdatesProcessor.processUpdatesForProject(projectId, error => {
  369. if (error != null) {
  370. return next(error)
  371. }
  372. res.sendStatus(204)
  373. })
  374. })
  375. }
  376. }
  377. export function forceDebugProject(req, res, next) {
  378. const projectId = req.params.project_id
  379. // set the debug flag to true unless we see ?clear=true
  380. const state = !req.query.clear
  381. ErrorRecorder.setForceDebug(projectId, state, error => {
  382. if (error != null) {
  383. return next(error)
  384. }
  385. // display the failure record to help debugging
  386. ErrorRecorder.getFailureRecord(projectId, (error, result) => {
  387. if (error != null) {
  388. return next(error)
  389. }
  390. res.send(result)
  391. })
  392. })
  393. }
  394. export function getFailures(req, res, next) {
  395. ErrorRecorder.getFailures((error, result) => {
  396. if (error != null) {
  397. return next(error)
  398. }
  399. res.send({ failures: result })
  400. })
  401. }
  402. export function getQueueCounts(req, res, next) {
  403. RedisManager.getProjectIdsWithHistoryOpsCount((err, queuedProjectsCount) => {
  404. if (err != null) {
  405. return next(err)
  406. }
  407. res.send({ queuedProjects: queuedProjectsCount })
  408. })
  409. }
  410. export function getLabels(req, res, next) {
  411. const projectId = req.params.project_id
  412. HistoryApiManager.shouldUseProjectHistory(
  413. projectId,
  414. (error, shouldUseProjectHistory) => {
  415. if (error != null) {
  416. return next(error)
  417. }
  418. if (shouldUseProjectHistory) {
  419. LabelsManager.getLabels(projectId, (error, labels) => {
  420. if (error != null) {
  421. return next(error)
  422. }
  423. res.json(labels)
  424. })
  425. } else {
  426. res.sendStatus(409)
  427. }
  428. }
  429. )
  430. }
  431. export function createLabel(req, res, next) {
  432. const { project_id: projectId, user_id: userIdParam } = req.params
  433. const {
  434. version,
  435. comment,
  436. user_id: userIdBody,
  437. created_at: createdAt,
  438. validate_exists: validateExists,
  439. } = req.body
  440. // Temporarily looking up both params and body while rolling out changes
  441. // in the router path - https://github.com/overleaf/internal/pull/20200
  442. const userId = userIdParam || userIdBody
  443. HistoryApiManager.shouldUseProjectHistory(
  444. projectId,
  445. (error, shouldUseProjectHistory) => {
  446. if (error != null) {
  447. return next(error)
  448. }
  449. if (shouldUseProjectHistory) {
  450. LabelsManager.createLabel(
  451. projectId,
  452. userId,
  453. version,
  454. comment,
  455. createdAt,
  456. validateExists,
  457. (error, label) => {
  458. if (error != null) {
  459. return next(error)
  460. }
  461. res.json(label)
  462. }
  463. )
  464. } else {
  465. logger.error(
  466. {
  467. projectId,
  468. userId,
  469. version,
  470. comment,
  471. createdAt,
  472. validateExists,
  473. },
  474. 'not using v2 history'
  475. )
  476. res.sendStatus(409)
  477. }
  478. }
  479. )
  480. }
  481. /**
  482. * This will delete a label if it is owned by the current user. If you wish to
  483. * delete a label regardless of the current user, then use `deleteLabel` instead.
  484. */
  485. export function deleteLabelForUser(req, res, next) {
  486. const {
  487. project_id: projectId,
  488. user_id: userId,
  489. label_id: labelId,
  490. } = req.params
  491. LabelsManager.deleteLabelForUser(projectId, userId, labelId, error => {
  492. if (error != null) {
  493. return next(error)
  494. }
  495. res.sendStatus(204)
  496. })
  497. }
  498. export function deleteLabel(req, res, next) {
  499. const { project_id: projectId, label_id: labelId } = req.params
  500. LabelsManager.deleteLabel(projectId, labelId, error => {
  501. if (error != null) {
  502. return next(error)
  503. }
  504. res.sendStatus(204)
  505. })
  506. }
  507. export function retryFailures(req, res, next) {
  508. const { failureType, timeout, limit, callbackUrl } = req.query
  509. if (callbackUrl) {
  510. // send response but run in background when callbackUrl provided
  511. res.send({ retryStatus: 'running retryFailures in background' })
  512. }
  513. RetryManager.retryFailures(
  514. { failureType, timeout, limit },
  515. (error, result) => {
  516. if (callbackUrl) {
  517. // if present, notify the callbackUrl on success
  518. if (!error) {
  519. // Needs Node 12
  520. // const callbackHeaders = Object.fromEntries(Object.entries(req.headers || {}).filter(([k,v]) => k.match(/^X-CALLBACK-/i)))
  521. const callbackHeaders = {}
  522. for (const key of Object.getOwnPropertyNames(
  523. req.headers || {}
  524. ).filter(key => key.match(/^X-CALLBACK-/i))) {
  525. const found = key.match(/^X-CALLBACK-(.*)/i)
  526. callbackHeaders[found[1]] = req.headers[key]
  527. }
  528. request({ url: callbackUrl, headers: callbackHeaders })
  529. }
  530. } else {
  531. if (error != null) {
  532. return next(error)
  533. }
  534. res.send({ retryStatus: result })
  535. }
  536. }
  537. )
  538. }
  539. export function transferLabels(req, res, next) {
  540. const { from_user: fromUser, to_user: toUser } = req.params
  541. LabelsManager.transferLabels(fromUser, toUser, error => {
  542. if (error != null) {
  543. return next(error)
  544. }
  545. res.sendStatus(204)
  546. })
  547. }
  548. export function deleteProject(req, res, next) {
  549. const { project_id: projectId } = req.params
  550. // clear the timestamp before clearing the queue,
  551. // because the queue location is used in the migration
  552. RedisManager.clearFirstOpTimestamp(projectId, err => {
  553. if (err) {
  554. return next(err)
  555. }
  556. RedisManager.clearCachedHistoryId(projectId, err => {
  557. if (err) {
  558. return next(err)
  559. }
  560. RedisManager.destroyDocUpdatesQueue(projectId, err => {
  561. if (err) {
  562. return next(err)
  563. }
  564. SyncManager.clearResyncState(projectId, err => {
  565. if (err) {
  566. return next(err)
  567. }
  568. ErrorRecorder.clearError(projectId, err => {
  569. if (err) {
  570. return next(err)
  571. }
  572. res.sendStatus(204)
  573. })
  574. })
  575. })
  576. })
  577. })
  578. }