HistoryUpgradeHelper.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. const { ReadPreference, ObjectId } = require('mongodb')
  2. const { db } = require('../../app/src/infrastructure/mongodb')
  3. const ProjectHistoryHandler = require('../../app/src/Features/Project/ProjectHistoryHandler')
  4. const HistoryManager = require('../../app/src/Features/History/HistoryManager')
  5. const ProjectHistoryController = require('../../modules/admin-panel/app/src/ProjectHistoryController')
  6. // Timestamp of when 'Enable history for SL in background' release
  7. const ID_WHEN_FULL_PROJECT_HISTORY_ENABLED = '5a8d8a370000000000000000'
  8. const OBJECT_ID_WHEN_FULL_PROJECT_HISTORY_ENABLED = new ObjectId(
  9. ID_WHEN_FULL_PROJECT_HISTORY_ENABLED
  10. )
  11. const DATETIME_WHEN_FULL_PROJECT_HISTORY_ENABLED =
  12. OBJECT_ID_WHEN_FULL_PROJECT_HISTORY_ENABLED.getTimestamp()
  13. async function determineProjectHistoryType(project) {
  14. if (project.overleaf && project.overleaf.history) {
  15. if (project.overleaf.history.upgradeFailed) {
  16. return 'UpgradeFailed'
  17. }
  18. if (project.overleaf.history.conversionFailed) {
  19. return 'ConversionFailed'
  20. }
  21. }
  22. if (
  23. project.overleaf &&
  24. project.overleaf.history &&
  25. project.overleaf.history.id
  26. ) {
  27. if (project.overleaf.history.display) {
  28. // v2: full project history, do nothing
  29. return 'V2'
  30. } else {
  31. if (projectCreatedAfterFullProjectHistoryEnabled(project)) {
  32. // IF project initialised after full project history enabled for all projects
  33. // THEN project history should contain all information we need, without intervention
  34. return 'V1WithoutConversion'
  35. } else {
  36. // ELSE SL history may predate full project history
  37. // THEN delete full project history and convert their SL history to full project history
  38. // --
  39. // TODO: how to verify this, can get rough start date of SL history, but not full project history
  40. const preserveHistory = await shouldPreserveHistory(project)
  41. const anyDocHistory = await anyDocHistoryExists(project)
  42. const anyDocHistoryIndex = await anyDocHistoryIndexExists(project)
  43. if (preserveHistory) {
  44. if (anyDocHistory || anyDocHistoryIndex) {
  45. // if SL history exists that we need to preserve, then we must convert
  46. return 'V1WithConversion'
  47. } else {
  48. // otherwise just upgrade without conversion
  49. return 'V1WithoutConversion'
  50. }
  51. } else {
  52. // if preserveHistory false, then max 7 days of SL history
  53. // but v1 already record to both histories, so safe to upgrade
  54. return 'V1WithoutConversion'
  55. }
  56. }
  57. }
  58. } else {
  59. const preserveHistory = await shouldPreserveHistory(project)
  60. const anyDocHistory = await anyDocHistoryExists(project)
  61. const anyDocHistoryIndex = await anyDocHistoryIndexExists(project)
  62. if (anyDocHistory || anyDocHistoryIndex) {
  63. // IF there is SL history ->
  64. if (preserveHistory) {
  65. // that needs to be preserved:
  66. // THEN initialise full project history and convert SL history to full project history
  67. return 'NoneWithConversion'
  68. } else {
  69. return 'NoneWithTemporaryHistory'
  70. }
  71. } else {
  72. // ELSE there is not any SL history ->
  73. // THEN initialise full project history and sync with current content
  74. return 'NoneWithoutConversion'
  75. }
  76. }
  77. }
  78. async function upgradeProject(project) {
  79. const historyType = await determineProjectHistoryType(project)
  80. if (historyType === 'V2') {
  81. return { historyType, upgraded: true }
  82. }
  83. const upgradeFn = getUpgradeFunctionForType(historyType)
  84. if (!upgradeFn) {
  85. return { error: 'unsupported history type' }
  86. }
  87. const result = await upgradeFn(project)
  88. result.historyType = historyType
  89. return result
  90. }
  91. // Do upgrades/conversion:
  92. function getUpgradeFunctionForType(historyType) {
  93. return UpgradeFunctionMapping[historyType]
  94. }
  95. const UpgradeFunctionMapping = {
  96. NoneWithoutConversion: doUpgradeForNoneWithoutConversion,
  97. UpgradeFailed: doUpgradeForNoneWithoutConversion,
  98. ConversionFailed: doUpgradeForNoneWithConversion,
  99. V1WithoutConversion: doUpgradeForV1WithoutConversion,
  100. V1WithConversion: doUpgradeForV1WithConversion,
  101. NoneWithConversion: doUpgradeForNoneWithConversion,
  102. NoneWithTemporaryHistory: doUpgradeForNoneWithConversion,
  103. }
  104. async function doUpgradeForV1WithoutConversion(project) {
  105. await db.projects.updateOne(
  106. { _id: project._id },
  107. {
  108. $set: {
  109. 'overleaf.history.display': true,
  110. 'overleaf.history.upgradedAt': new Date(),
  111. 'overleaf.history.upgradeReason': `v1-without-sl-history`,
  112. },
  113. }
  114. )
  115. return { upgraded: true }
  116. }
  117. async function doUpgradeForV1WithConversion(project) {
  118. const result = {}
  119. const projectId = project._id
  120. // migrateProjectHistory expects project id as a string
  121. const projectIdString = project._id.toString()
  122. try {
  123. // We treat these essentially as None projects, the V1 history is irrelevant,
  124. // so we will delete it, and do a conversion as if we're a None project
  125. await ProjectHistoryController.deleteProjectHistory(projectIdString)
  126. await ProjectHistoryController.migrateProjectHistory(projectIdString)
  127. } catch (err) {
  128. // if migrateProjectHistory fails, it cleans up by deleting
  129. // the history and unsetting the history id
  130. // therefore a failed project will still look like a 'None with conversion' project
  131. result.error = err
  132. await db.projects.updateOne(
  133. { _id: projectId },
  134. {
  135. $set: {
  136. 'overleaf.history.conversionFailed': true,
  137. },
  138. }
  139. )
  140. return result
  141. }
  142. await db.projects.updateOne(
  143. { _id: projectId },
  144. {
  145. $set: {
  146. 'overleaf.history.upgradeReason': `v1-with-conversion`,
  147. },
  148. $unset: {
  149. 'overleaf.history.upgradeFailed': true,
  150. 'overleaf.history.conversionFailed': true,
  151. },
  152. }
  153. )
  154. result.upgraded = true
  155. return result
  156. }
  157. async function doUpgradeForNoneWithoutConversion(project) {
  158. const result = {}
  159. const projectId = project._id
  160. try {
  161. // Logic originally from ProjectHistoryHandler.ensureHistoryExistsForProject
  162. // However sends a force resync project to project history instead
  163. // of a resync request to doc-updater
  164. const historyId = await ProjectHistoryHandler.promises.getHistoryId(
  165. projectId
  166. )
  167. if (!historyId) {
  168. const history = await HistoryManager.promises.initializeProject()
  169. if (history && history.overleaf_id) {
  170. await ProjectHistoryHandler.promises.setHistoryId(
  171. projectId,
  172. history.overleaf_id
  173. )
  174. }
  175. }
  176. await HistoryManager.promises.resyncProject(projectId, {
  177. force: true,
  178. origin: { kind: 'history-migration' },
  179. })
  180. await HistoryManager.promises.flushProject(projectId)
  181. } catch (err) {
  182. result.error = err
  183. await db.projects.updateOne(
  184. { _id: project._id },
  185. {
  186. $set: {
  187. 'overleaf.history.upgradeFailed': true,
  188. },
  189. }
  190. )
  191. return result
  192. }
  193. await db.projects.updateOne(
  194. { _id: project._id },
  195. {
  196. $set: {
  197. 'overleaf.history.display': true,
  198. 'overleaf.history.upgradedAt': new Date(),
  199. 'overleaf.history.upgradeReason': `none-without-conversion`,
  200. },
  201. }
  202. )
  203. result.upgraded = true
  204. return result
  205. }
  206. async function doUpgradeForNoneWithConversion(project) {
  207. const result = {}
  208. const projectId = project._id
  209. // migrateProjectHistory expects project id as a string
  210. const projectIdString = project._id.toString()
  211. try {
  212. await ProjectHistoryController.migrateProjectHistory(projectIdString)
  213. } catch (err) {
  214. // if migrateProjectHistory fails, it cleans up by deleting
  215. // the history and unsetting the history id
  216. // therefore a failed project will still look like a 'None with conversion' project
  217. result.error = err
  218. await db.projects.updateOne(
  219. { _id: projectId },
  220. {
  221. $set: {
  222. 'overleaf.history.conversionFailed': true,
  223. },
  224. }
  225. )
  226. return result
  227. }
  228. await db.projects.updateOne(
  229. { _id: projectId },
  230. {
  231. $set: {
  232. 'overleaf.history.upgradeReason': `none-with-conversion`,
  233. },
  234. $unset: {
  235. 'overleaf.history.upgradeFailed': true,
  236. 'overleaf.history.conversionFailed': true,
  237. },
  238. }
  239. )
  240. result.upgraded = true
  241. return result
  242. }
  243. // Util
  244. function projectCreatedAfterFullProjectHistoryEnabled(project) {
  245. return (
  246. project._id.getTimestamp() >= DATETIME_WHEN_FULL_PROJECT_HISTORY_ENABLED
  247. )
  248. }
  249. async function shouldPreserveHistory(project) {
  250. return await db.projectHistoryMetaData.findOne(
  251. {
  252. $and: [
  253. { project_id: { $eq: project._id } },
  254. { preserveHistory: { $eq: true } },
  255. ],
  256. },
  257. { readPreference: ReadPreference.SECONDARY }
  258. )
  259. }
  260. async function anyDocHistoryExists(project) {
  261. return await db.docHistory.findOne(
  262. { project_id: { $eq: project._id } },
  263. {
  264. projection: { _id: 1 },
  265. readPreference: ReadPreference.SECONDARY,
  266. }
  267. )
  268. }
  269. async function anyDocHistoryIndexExists(project) {
  270. return await db.docHistoryIndex.findOne(
  271. { project_id: { $eq: project._id } },
  272. {
  273. projection: { _id: 1 },
  274. readPreference: ReadPreference.SECONDARY,
  275. }
  276. )
  277. }
  278. module.exports = {
  279. determineProjectHistoryType,
  280. getUpgradeFunctionForType,
  281. upgradeProject,
  282. }