snapshot.js 6.6 KB

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