HttpController.js 15 KB

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