SnapshotManager.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import Core from 'overleaf-editor-core'
  2. import { Readable as StringStream } from 'stream'
  3. import BPromise from 'bluebird'
  4. import OError from '@overleaf/o-error'
  5. import * as HistoryStoreManager from './HistoryStoreManager.js'
  6. import * as WebApiManager from './WebApiManager.js'
  7. import * as Errors from './Errors.js'
  8. StringStream.prototype._read = function () {}
  9. const MAX_REQUESTS = 4 // maximum number of parallel requests to v1 history service
  10. export function getFileSnapshotStream(projectId, version, pathname, callback) {
  11. _getSnapshotAtVersion(projectId, version, (error, snapshot) => {
  12. if (error) {
  13. return callback(OError.tag(error))
  14. }
  15. const file = snapshot.getFile(pathname)
  16. if (file == null) {
  17. error = new Errors.NotFoundError(`${pathname} not found`, {
  18. projectId,
  19. version,
  20. pathname,
  21. })
  22. return callback(error)
  23. }
  24. WebApiManager.getHistoryId(projectId, (err, historyId) => {
  25. if (err) {
  26. return callback(OError.tag(err))
  27. }
  28. if (file.isEditable()) {
  29. file
  30. .load('eager', HistoryStoreManager.getBlobStore(historyId))
  31. .then(() => {
  32. const stream = new StringStream()
  33. stream.push(file.getContent())
  34. stream.push(null)
  35. callback(null, stream)
  36. })
  37. .catch(err => callback(err))
  38. } else {
  39. HistoryStoreManager.getProjectBlobStream(
  40. historyId,
  41. file.getHash(),
  42. callback
  43. )
  44. }
  45. })
  46. })
  47. }
  48. // Returns project snapshot containing the document content for files with
  49. // text operations in the relevant chunk, and hashes for unmodified/binary
  50. // files. Used by git bridge to get the state of the project.
  51. export function getProjectSnapshot(projectId, version, callback) {
  52. _getSnapshotAtVersion(projectId, version, (error, snapshot) => {
  53. if (error) {
  54. return callback(OError.tag(error))
  55. }
  56. WebApiManager.getHistoryId(projectId, (err, historyId) => {
  57. if (err) {
  58. return callback(OError.tag(err))
  59. }
  60. _loadFilesLimit(
  61. snapshot,
  62. 'eager',
  63. HistoryStoreManager.getBlobStore(historyId)
  64. )
  65. .then(() => {
  66. const data = {
  67. projectId,
  68. files: snapshot.getFileMap().files,
  69. }
  70. callback(null, data)
  71. })
  72. .catch(callback)
  73. })
  74. })
  75. }
  76. function _getSnapshotAtVersion(projectId, version, callback) {
  77. WebApiManager.getHistoryId(projectId, (error, historyId) => {
  78. if (error) {
  79. return callback(OError.tag(error))
  80. }
  81. HistoryStoreManager.getChunkAtVersion(
  82. projectId,
  83. historyId,
  84. version,
  85. (error, data) => {
  86. if (error) {
  87. return callback(OError.tag(error))
  88. }
  89. const chunk = Core.Chunk.fromRaw(data.chunk)
  90. const snapshot = chunk.getSnapshot()
  91. const changes = chunk
  92. .getChanges()
  93. .slice(0, version - chunk.getStartVersion())
  94. snapshot.applyAll(changes)
  95. callback(null, snapshot)
  96. }
  97. )
  98. })
  99. }
  100. export function getLatestSnapshot(projectId, historyId, callback) {
  101. HistoryStoreManager.getMostRecentChunk(projectId, historyId, (err, data) => {
  102. if (err) {
  103. return callback(err)
  104. }
  105. if (data == null || data.chunk == null) {
  106. return callback(new OError('undefined chunk'))
  107. }
  108. // apply all the changes in the chunk to get the current snapshot
  109. const chunk = Core.Chunk.fromRaw(data.chunk)
  110. const snapshot = chunk.getSnapshot()
  111. const changes = chunk.getChanges()
  112. snapshot.applyAll(changes)
  113. snapshot
  114. .loadFiles('lazy', HistoryStoreManager.getBlobStore(historyId))
  115. .then(snapshotFiles => callback(null, snapshotFiles))
  116. .catch(err => callback(err))
  117. })
  118. }
  119. function _loadFilesLimit(snapshot, kind, blobStore) {
  120. // bluebird promises only support a limit on concurrency for map()
  121. // so make an array of the files we need to load
  122. const fileList = []
  123. snapshot.fileMap.map(file => fileList.push(file))
  124. // load the files in parallel with a limit on the concurrent requests
  125. return BPromise.map(
  126. fileList,
  127. file => {
  128. // only load changed files, others can be dereferenced from their
  129. // blobs (this method is only used by the git bridge which
  130. // understands how to load blobs).
  131. if (!file.isEditable() || file.getHash()) {
  132. return
  133. }
  134. return file.load(kind, blobStore)
  135. },
  136. { concurrency: MAX_REQUESTS }
  137. )
  138. }