snapshot.js 6.7 KB

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