snapshot.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. 'use strict'
  2. const assert = require('check-types').assert
  3. const OError = require('@overleaf/o-error')
  4. const FileMap = require('./file_map')
  5. const V2DocVersions = require('./v2_doc_versions')
  6. const FILE_LOAD_CONCURRENCY = 50
  7. /**
  8. * @typedef {import("./types").BlobStore} BlobStore
  9. * @typedef {import("./types").ReadonlyBlobStore} ReadonlyBlobStore
  10. * @typedef {import("./change")} Change
  11. * @typedef {import("./operation/text_operation")} TextOperation
  12. */
  13. class EditMissingFileError extends OError {}
  14. /**
  15. * A Snapshot represents the state of a {@link Project} at a
  16. * particular version.
  17. */
  18. class Snapshot {
  19. static PROJECT_VERSION_RX_STRING = '^[0-9]+\\.[0-9]+$'
  20. static PROJECT_VERSION_RX = new RegExp(Snapshot.PROJECT_VERSION_RX_STRING)
  21. static EditMissingFileError = EditMissingFileError
  22. static fromRaw(raw) {
  23. assert.object(raw.files, 'bad raw.files')
  24. return new Snapshot(
  25. FileMap.fromRaw(raw.files),
  26. raw.projectVersion,
  27. V2DocVersions.fromRaw(raw.v2DocVersions)
  28. )
  29. }
  30. toRaw() {
  31. const raw = {
  32. files: this.fileMap.toRaw(),
  33. }
  34. if (this.projectVersion) raw.projectVersion = this.projectVersion
  35. if (this.v2DocVersions) raw.v2DocVersions = this.v2DocVersions.toRaw()
  36. return raw
  37. }
  38. /**
  39. * @param {FileMap} [fileMap]
  40. * @param {string} [projectVersion]
  41. * @param {V2DocVersions} [v2DocVersions]
  42. */
  43. constructor(fileMap, projectVersion, v2DocVersions) {
  44. assert.maybe.instance(fileMap, FileMap, 'bad fileMap')
  45. this.fileMap = fileMap || new FileMap({})
  46. this.projectVersion = projectVersion
  47. this.v2DocVersions = v2DocVersions
  48. }
  49. /**
  50. * @return {string | null | undefined}
  51. */
  52. getProjectVersion() {
  53. return this.projectVersion
  54. }
  55. setProjectVersion(projectVersion) {
  56. assert.maybe.match(
  57. projectVersion,
  58. Snapshot.PROJECT_VERSION_RX,
  59. 'Snapshot: bad projectVersion'
  60. )
  61. this.projectVersion = projectVersion
  62. }
  63. /**
  64. * @return {V2DocVersions | null | undefined}
  65. */
  66. getV2DocVersions() {
  67. return this.v2DocVersions
  68. }
  69. setV2DocVersions(v2DocVersions) {
  70. assert.maybe.instance(
  71. v2DocVersions,
  72. V2DocVersions,
  73. 'Snapshot: bad v2DocVersions'
  74. )
  75. this.v2DocVersions = v2DocVersions
  76. }
  77. updateV2DocVersions(v2DocVersions) {
  78. // merge new v2DocVersions into this.v2DocVersions
  79. v2DocVersions.applyTo(this)
  80. }
  81. /**
  82. * The underlying file map.
  83. * @return {FileMap}
  84. */
  85. getFileMap() {
  86. return this.fileMap
  87. }
  88. /**
  89. * The pathnames of all of the files.
  90. *
  91. * @return {Array.<string>} in no particular order
  92. */
  93. getFilePathnames() {
  94. return this.fileMap.getPathnames()
  95. }
  96. /**
  97. * Get a File by its pathname.
  98. * @see FileMap#getFile
  99. */
  100. getFile(pathname) {
  101. return this.fileMap.getFile(pathname)
  102. }
  103. /**
  104. * Add the given file to the snapshot.
  105. * @see FileMap#addFile
  106. */
  107. addFile(pathname, file) {
  108. this.fileMap.addFile(pathname, file)
  109. }
  110. /**
  111. * Move or remove a file.
  112. * @see FileMap#moveFile
  113. */
  114. moveFile(pathname, newPathname) {
  115. this.fileMap.moveFile(pathname, newPathname)
  116. }
  117. /**
  118. * The number of files in the snapshot.
  119. *
  120. * @return {number}
  121. */
  122. countFiles() {
  123. return this.fileMap.countFiles()
  124. }
  125. /**
  126. * Edit the content of an editable file.
  127. *
  128. * Throws an error if no file with the given name exists.
  129. *
  130. * @param {string} pathname
  131. * @param {TextOperation} textOperation
  132. */
  133. editFile(pathname, textOperation) {
  134. const file = this.fileMap.getFile(pathname)
  135. if (!file) {
  136. throw new Snapshot.EditMissingFileError(
  137. `can't find file for editing: ${pathname}`
  138. )
  139. }
  140. file.edit(textOperation)
  141. }
  142. /**
  143. * Apply all changes in sequence. Modifies the snapshot in place.
  144. *
  145. * Ignore recoverable errors (caused by historical bad data) unless opts.strict is true
  146. *
  147. * @param {Change[]} changes
  148. * @param {object} [opts]
  149. * @param {boolean} opts.strict - do not ignore recoverable errors
  150. */
  151. applyAll(changes, opts) {
  152. for (const change of changes) {
  153. change.applyTo(this, opts)
  154. }
  155. }
  156. /**
  157. * If the Files in this Snapshot reference blob hashes, add them to the given
  158. * set.
  159. *
  160. * @param {Set.<String>} blobHashes
  161. */
  162. findBlobHashes(blobHashes) {
  163. // eslint-disable-next-line array-callback-return
  164. this.fileMap.map(file => {
  165. const hash = file.getHash()
  166. const rangeHash = file.getRangesHash()
  167. if (hash) blobHashes.add(hash)
  168. if (rangeHash) blobHashes.add(rangeHash)
  169. })
  170. }
  171. /**
  172. * Load all of the files in this snapshot.
  173. *
  174. * @param {string} kind see {File#load}
  175. * @param {ReadonlyBlobStore} blobStore
  176. * @return {Promise<Object>} an object where keys are the pathnames and
  177. * values are the files in the snapshot
  178. */
  179. async loadFiles(kind, blobStore) {
  180. return await this.fileMap.mapAsync(
  181. file => file.load(kind, blobStore),
  182. FILE_LOAD_CONCURRENCY
  183. )
  184. }
  185. /**
  186. * Store each of the files in this snapshot and return the raw snapshot for
  187. * long term storage.
  188. *
  189. * @param {BlobStore} blobStore
  190. * @param {number} [concurrency]
  191. * @return {Promise.<Object>}
  192. */
  193. async store(blobStore, concurrency) {
  194. assert.maybe.number(concurrency, 'bad concurrency')
  195. const projectVersion = this.projectVersion
  196. const rawV2DocVersions = this.v2DocVersions
  197. ? this.v2DocVersions.toRaw()
  198. : undefined
  199. const rawFiles = await this.fileMap.mapAsync(
  200. file => file.store(blobStore),
  201. concurrency
  202. )
  203. return {
  204. files: rawFiles,
  205. projectVersion,
  206. v2DocVersions: rawV2DocVersions,
  207. }
  208. }
  209. /**
  210. * Create a deep clone of this snapshot.
  211. *
  212. * @return {Snapshot}
  213. */
  214. clone() {
  215. return Snapshot.fromRaw(this.toRaw())
  216. }
  217. }
  218. module.exports = Snapshot