HttpController.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  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 getLatestSnapshot(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. SnapshotManager.getLatestSnapshot(
  230. projectId,
  231. historyId,
  232. (error, details) => {
  233. if (error != null) {
  234. return next(error)
  235. }
  236. const { snapshot, version } = details
  237. res.json({ snapshot: snapshot.toRaw(), version })
  238. }
  239. )
  240. })
  241. }
  242. export function getChangesInChunkSince(req, res, next) {
  243. const { project_id: projectId } = req.params
  244. const { since } = req.query
  245. WebApiManager.getHistoryId(projectId, (error, historyId) => {
  246. if (error) return next(OError.tag(error))
  247. SnapshotManager.getChangesInChunkSince(
  248. projectId,
  249. historyId,
  250. since,
  251. (error, details) => {
  252. if (error != null) {
  253. return next(error)
  254. }
  255. const { latestStartVersion, changes } = details
  256. res.json({
  257. latestStartVersion,
  258. changes: changes.map(c => c.toRaw()),
  259. })
  260. }
  261. )
  262. })
  263. }
  264. export function getProjectSnapshot(req, res, next) {
  265. const { project_id: projectId, version } = req.params
  266. SnapshotManager.getProjectSnapshot(
  267. projectId,
  268. version,
  269. (error, snapshotData) => {
  270. if (error != null) {
  271. return next(error)
  272. }
  273. res.json(snapshotData)
  274. }
  275. )
  276. }
  277. export function getPathsAtVersion(req, res, next) {
  278. const { project_id: projectId, version } = req.params
  279. SnapshotManager.getPathsAtVersion(projectId, version, (error, result) => {
  280. if (error != null) {
  281. return next(error)
  282. }
  283. res.json(result)
  284. })
  285. }
  286. export function healthCheck(req, res) {
  287. HealthChecker.check(err => {
  288. if (err != null) {
  289. logger.err({ err }, 'error performing health check')
  290. res.sendStatus(500)
  291. } else {
  292. res.sendStatus(200)
  293. }
  294. })
  295. }
  296. export function checkLock(req, res) {
  297. HealthChecker.checkLock(err => {
  298. if (err != null) {
  299. logger.err({ err }, 'error performing lock check')
  300. res.sendStatus(500)
  301. } else {
  302. res.sendStatus(200)
  303. }
  304. })
  305. }
  306. export function resyncProject(req, res, next) {
  307. const projectId = req.params.project_id
  308. const options = {}
  309. if (req.body.origin) {
  310. options.origin = req.body.origin
  311. }
  312. if (req.body.historyRangesMigration) {
  313. options.historyRangesMigration = req.body.historyRangesMigration
  314. }
  315. if (req.query.force || req.body.force) {
  316. // this will delete the queue and clear the sync state
  317. // use if the project is completely broken
  318. SyncManager.startHardResync(projectId, options, error => {
  319. if (error != null) {
  320. return next(error)
  321. }
  322. // flush the sync operations
  323. UpdatesProcessor.processUpdatesForProject(projectId, error => {
  324. if (error != null) {
  325. return next(error)
  326. }
  327. res.sendStatus(204)
  328. })
  329. })
  330. } else {
  331. SyncManager.startResync(projectId, options, error => {
  332. if (error != null) {
  333. return next(error)
  334. }
  335. // flush the sync operations
  336. UpdatesProcessor.processUpdatesForProject(projectId, error => {
  337. if (error != null) {
  338. return next(error)
  339. }
  340. res.sendStatus(204)
  341. })
  342. })
  343. }
  344. }
  345. export function forceDebugProject(req, res, next) {
  346. const projectId = req.params.project_id
  347. // set the debug flag to true unless we see ?clear=true
  348. const state = !req.query.clear
  349. ErrorRecorder.setForceDebug(projectId, state, error => {
  350. if (error != null) {
  351. return next(error)
  352. }
  353. // display the failure record to help debugging
  354. ErrorRecorder.getFailureRecord(projectId, (error, result) => {
  355. if (error != null) {
  356. return next(error)
  357. }
  358. res.send(result)
  359. })
  360. })
  361. }
  362. export function getFailures(req, res, next) {
  363. ErrorRecorder.getFailures((error, result) => {
  364. if (error != null) {
  365. return next(error)
  366. }
  367. res.send({ failures: result })
  368. })
  369. }
  370. export function getQueueCounts(req, res, next) {
  371. RedisManager.getProjectIdsWithHistoryOpsCount((err, queuedProjectsCount) => {
  372. if (err != null) {
  373. return next(err)
  374. }
  375. res.send({ queuedProjects: queuedProjectsCount })
  376. })
  377. }
  378. export function getLabels(req, res, next) {
  379. const projectId = req.params.project_id
  380. HistoryApiManager.shouldUseProjectHistory(
  381. projectId,
  382. (error, shouldUseProjectHistory) => {
  383. if (error != null) {
  384. return next(error)
  385. }
  386. if (shouldUseProjectHistory) {
  387. LabelsManager.getLabels(projectId, (error, labels) => {
  388. if (error != null) {
  389. return next(error)
  390. }
  391. res.json(labels)
  392. })
  393. } else {
  394. res.sendStatus(409)
  395. }
  396. }
  397. )
  398. }
  399. export function createLabel(req, res, next) {
  400. const { project_id: projectId, user_id: userIdParam } = req.params
  401. const {
  402. version,
  403. comment,
  404. user_id: userIdBody,
  405. created_at: createdAt,
  406. validate_exists: validateExists,
  407. } = req.body
  408. // Temporarily looking up both params and body while rolling out changes
  409. // in the router path - https://github.com/overleaf/internal/pull/20200
  410. const userId = userIdParam || userIdBody
  411. HistoryApiManager.shouldUseProjectHistory(
  412. projectId,
  413. (error, shouldUseProjectHistory) => {
  414. if (error != null) {
  415. return next(error)
  416. }
  417. if (shouldUseProjectHistory) {
  418. LabelsManager.createLabel(
  419. projectId,
  420. userId,
  421. version,
  422. comment,
  423. createdAt,
  424. validateExists,
  425. (error, label) => {
  426. if (error != null) {
  427. return next(error)
  428. }
  429. res.json(label)
  430. }
  431. )
  432. } else {
  433. logger.error(
  434. {
  435. projectId,
  436. userId,
  437. version,
  438. comment,
  439. createdAt,
  440. validateExists,
  441. },
  442. 'not using v2 history'
  443. )
  444. res.sendStatus(409)
  445. }
  446. }
  447. )
  448. }
  449. /**
  450. * This will delete a label if it is owned by the current user. If you wish to
  451. * delete a label regardless of the current user, then use `deleteLabel` instead.
  452. */
  453. export function deleteLabelForUser(req, res, next) {
  454. const {
  455. project_id: projectId,
  456. user_id: userId,
  457. label_id: labelId,
  458. } = req.params
  459. LabelsManager.deleteLabelForUser(projectId, userId, labelId, error => {
  460. if (error != null) {
  461. return next(error)
  462. }
  463. res.sendStatus(204)
  464. })
  465. }
  466. export function deleteLabel(req, res, next) {
  467. const { project_id: projectId, label_id: labelId } = req.params
  468. LabelsManager.deleteLabel(projectId, labelId, error => {
  469. if (error != null) {
  470. return next(error)
  471. }
  472. res.sendStatus(204)
  473. })
  474. }
  475. export function retryFailures(req, res, next) {
  476. const { failureType, timeout, limit, callbackUrl } = req.query
  477. if (callbackUrl) {
  478. // send response but run in background when callbackUrl provided
  479. res.send({ retryStatus: 'running retryFailures in background' })
  480. }
  481. RetryManager.retryFailures(
  482. { failureType, timeout, limit },
  483. (error, result) => {
  484. if (callbackUrl) {
  485. // if present, notify the callbackUrl on success
  486. if (!error) {
  487. // Needs Node 12
  488. // const callbackHeaders = Object.fromEntries(Object.entries(req.headers || {}).filter(([k,v]) => k.match(/^X-CALLBACK-/i)))
  489. const callbackHeaders = {}
  490. for (const key of Object.getOwnPropertyNames(
  491. req.headers || {}
  492. ).filter(key => key.match(/^X-CALLBACK-/i))) {
  493. const found = key.match(/^X-CALLBACK-(.*)/i)
  494. callbackHeaders[found[1]] = req.headers[key]
  495. }
  496. request({ url: callbackUrl, headers: callbackHeaders })
  497. }
  498. } else {
  499. if (error != null) {
  500. return next(error)
  501. }
  502. res.send({ retryStatus: result })
  503. }
  504. }
  505. )
  506. }
  507. export function transferLabels(req, res, next) {
  508. const { from_user: fromUser, to_user: toUser } = req.params
  509. LabelsManager.transferLabels(fromUser, toUser, error => {
  510. if (error != null) {
  511. return next(error)
  512. }
  513. res.sendStatus(204)
  514. })
  515. }
  516. export function deleteProject(req, res, next) {
  517. const { project_id: projectId } = req.params
  518. // clear the timestamp before clearing the queue,
  519. // because the queue location is used in the migration
  520. RedisManager.clearFirstOpTimestamp(projectId, err => {
  521. if (err) {
  522. return next(err)
  523. }
  524. RedisManager.clearCachedHistoryId(projectId, err => {
  525. if (err) {
  526. return next(err)
  527. }
  528. RedisManager.destroyDocUpdatesQueue(projectId, err => {
  529. if (err) {
  530. return next(err)
  531. }
  532. SyncManager.clearResyncState(projectId, err => {
  533. if (err) {
  534. return next(err)
  535. }
  536. ErrorRecorder.clearError(projectId, err => {
  537. if (err) {
  538. return next(err)
  539. }
  540. res.sendStatus(204)
  541. })
  542. })
  543. })
  544. })
  545. })
  546. }