HistoryManager.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. const { callbackify } = require('util')
  2. const { fetchJson, fetchNothing } = require('@overleaf/fetch-utils')
  3. const settings = require('@overleaf/settings')
  4. const OError = require('@overleaf/o-error')
  5. const UserGetter = require('../User/UserGetter')
  6. const ProjectGetter = require('../Project/ProjectGetter')
  7. async function initializeProject(projectId) {
  8. const body = await fetchJson(`${settings.apis.project_history.url}/project`, {
  9. method: 'POST',
  10. json: { historyId: projectId.toString() },
  11. })
  12. const historyId = body && body.project && body.project.id
  13. if (!historyId) {
  14. throw new OError('project-history did not provide an id', { body })
  15. }
  16. return historyId
  17. }
  18. async function flushProject(projectId) {
  19. try {
  20. await fetchNothing(
  21. `${settings.apis.project_history.url}/project/${projectId}/flush`,
  22. { method: 'POST' }
  23. )
  24. } catch (err) {
  25. throw OError.tag(err, 'failed to flush project to project history', {
  26. projectId,
  27. })
  28. }
  29. }
  30. async function deleteProjectHistory(projectId) {
  31. try {
  32. await fetchNothing(
  33. `${settings.apis.project_history.url}/project/${projectId}`,
  34. { method: 'DELETE' }
  35. )
  36. } catch (err) {
  37. throw OError.tag(err, 'failed to delete project history', {
  38. projectId,
  39. })
  40. }
  41. }
  42. async function resyncProject(projectId, options = {}) {
  43. const body = {}
  44. if (options.force) {
  45. body.force = options.force
  46. }
  47. if (options.origin) {
  48. body.origin = options.origin
  49. }
  50. if (options.historyRangesMigration) {
  51. body.historyRangesMigration = options.historyRangesMigration
  52. }
  53. try {
  54. await fetchNothing(
  55. `${settings.apis.project_history.url}/project/${projectId}/resync`,
  56. {
  57. method: 'POST',
  58. json: body,
  59. signal: AbortSignal.timeout(6 * 60 * 1000),
  60. }
  61. )
  62. } catch (err) {
  63. throw OError.tag(err, 'failed to resync project history', {
  64. projectId,
  65. })
  66. }
  67. }
  68. async function deleteProject(projectId, historyId) {
  69. const tasks = []
  70. tasks.push(_deleteProjectInProjectHistory(projectId))
  71. if (historyId != null) {
  72. tasks.push(_deleteProjectInFullProjectHistory(historyId))
  73. }
  74. await Promise.all(tasks)
  75. }
  76. async function _deleteProjectInProjectHistory(projectId) {
  77. try {
  78. await fetchNothing(
  79. `${settings.apis.project_history.url}/project/${projectId}`,
  80. { method: 'DELETE' }
  81. )
  82. } catch (err) {
  83. throw OError.tag(
  84. err,
  85. 'failed to clear project history in project-history',
  86. { projectId }
  87. )
  88. }
  89. }
  90. async function _deleteProjectInFullProjectHistory(historyId) {
  91. try {
  92. await fetchNothing(
  93. `${settings.apis.v1_history.url}/projects/${historyId}`,
  94. {
  95. method: 'DELETE',
  96. basicAuth: {
  97. user: settings.apis.v1_history.user,
  98. password: settings.apis.v1_history.pass,
  99. },
  100. }
  101. )
  102. } catch (err) {
  103. throw OError.tag(err, 'failed to clear project history', { historyId })
  104. }
  105. }
  106. /**
  107. * Warning: Don't use this method for large projects. It will eagerly load all
  108. * the history data and apply all operations.
  109. * @param {string} projectId
  110. * @returns Promise<object>
  111. */
  112. async function getCurrentContent(projectId) {
  113. const project = await ProjectGetter.promises.getProject(projectId, {
  114. overleaf: true,
  115. })
  116. const historyId = project?.overleaf?.history?.id
  117. if (!historyId) {
  118. throw new OError('project does not have a history id', { projectId })
  119. }
  120. try {
  121. return await fetchJson(
  122. `${settings.apis.v1_history.url}/projects/${historyId}/latest/content`,
  123. {
  124. method: 'GET',
  125. basicAuth: {
  126. user: settings.apis.v1_history.user,
  127. password: settings.apis.v1_history.pass,
  128. },
  129. }
  130. )
  131. } catch (err) {
  132. throw OError.tag(err, 'failed to load project history', { historyId })
  133. }
  134. }
  135. async function injectUserDetails(data) {
  136. // data can be either:
  137. // {
  138. // diff: [{
  139. // i: "foo",
  140. // meta: {
  141. // users: ["user_id", v1_user_id, ...]
  142. // ...
  143. // }
  144. // }, ...]
  145. // }
  146. // or
  147. // {
  148. // updates: [{
  149. // pathnames: ["main.tex"]
  150. // meta: {
  151. // users: ["user_id", v1_user_id, ...]
  152. // ...
  153. // },
  154. // ...
  155. // }, ...]
  156. // }
  157. // Either way, the top level key points to an array of objects with a meta.users property
  158. // that we need to replace user_ids with populated user objects.
  159. // Note that some entries in the users arrays may be v1 ids returned by the v1 history
  160. // service. v1 ids will be `numbers`
  161. let userIds = new Set()
  162. let v1UserIds = new Set()
  163. const entries = Array.isArray(data.diff)
  164. ? data.diff
  165. : Array.isArray(data.updates)
  166. ? data.updates
  167. : []
  168. for (const entry of entries) {
  169. for (const user of (entry.meta && entry.meta.users) || []) {
  170. if (typeof user === 'string') {
  171. userIds.add(user)
  172. } else if (typeof user === 'number') {
  173. v1UserIds.add(user)
  174. }
  175. }
  176. }
  177. userIds = Array.from(userIds)
  178. v1UserIds = Array.from(v1UserIds)
  179. const projection = { first_name: 1, last_name: 1, email: 1 }
  180. const usersArray = await UserGetter.promises.getUsers(userIds, projection)
  181. const users = {}
  182. for (const user of usersArray) {
  183. users[user._id.toString()] = _userView(user)
  184. }
  185. projection.overleaf = 1
  186. const v1IdentifiedUsersArray = await UserGetter.promises.getUsersByV1Ids(
  187. v1UserIds,
  188. projection
  189. )
  190. for (const user of v1IdentifiedUsersArray) {
  191. users[user.overleaf.id] = _userView(user)
  192. }
  193. for (const entry of entries) {
  194. if (entry.meta != null) {
  195. entry.meta.users = ((entry.meta && entry.meta.users) || []).map(user => {
  196. if (typeof user === 'string' || typeof user === 'number') {
  197. return users[user]
  198. } else {
  199. return user
  200. }
  201. })
  202. }
  203. }
  204. return data
  205. }
  206. function _userView(user) {
  207. const { _id, first_name: firstName, last_name: lastName, email } = user
  208. return { first_name: firstName, last_name: lastName, email, id: _id }
  209. }
  210. module.exports = {
  211. initializeProject: callbackify(initializeProject),
  212. flushProject: callbackify(flushProject),
  213. resyncProject: callbackify(resyncProject),
  214. deleteProject: callbackify(deleteProject),
  215. deleteProjectHistory: callbackify(deleteProjectHistory),
  216. injectUserDetails: callbackify(injectUserDetails),
  217. getCurrentContent: callbackify(getCurrentContent),
  218. promises: {
  219. initializeProject,
  220. flushProject,
  221. resyncProject,
  222. deleteProject,
  223. injectUserDetails,
  224. deleteProjectHistory,
  225. getCurrentContent,
  226. },
  227. }