ResourceStateManager.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. const Path = require('path')
  2. const fs = require('fs')
  3. const logger = require('logger-sharelatex')
  4. const Errors = require('./Errors')
  5. const SafeReader = require('./SafeReader')
  6. module.exports = {
  7. // The sync state is an identifier which must match for an
  8. // incremental update to be allowed.
  9. //
  10. // The initial value is passed in and stored on a full
  11. // compile, along with the list of resources..
  12. //
  13. // Subsequent incremental compiles must come with the same value - if
  14. // not they will be rejected with a 409 Conflict response. The
  15. // previous list of resources is returned.
  16. //
  17. // An incremental compile can only update existing files with new
  18. // content. The sync state identifier must change if any docs or
  19. // files are moved, added, deleted or renamed.
  20. SYNC_STATE_FILE: '.project-sync-state',
  21. SYNC_STATE_MAX_SIZE: 128 * 1024,
  22. saveProjectState(state, resources, basePath, callback) {
  23. const stateFile = Path.join(basePath, this.SYNC_STATE_FILE)
  24. if (state == null) {
  25. // remove the file if no state passed in
  26. logger.log({ state, basePath }, 'clearing sync state')
  27. fs.unlink(stateFile, function (err) {
  28. if (err && err.code !== 'ENOENT') {
  29. return callback(err)
  30. } else {
  31. return callback()
  32. }
  33. })
  34. } else {
  35. logger.log({ state, basePath }, 'writing sync state')
  36. const resourceList = resources.map((resource) => resource.path)
  37. fs.writeFile(
  38. stateFile,
  39. [...resourceList, `stateHash:${state}`].join('\n'),
  40. callback
  41. )
  42. }
  43. },
  44. checkProjectStateMatches(state, basePath, callback) {
  45. const stateFile = Path.join(basePath, this.SYNC_STATE_FILE)
  46. const size = this.SYNC_STATE_MAX_SIZE
  47. SafeReader.readFile(stateFile, size, 'utf8', function (
  48. err,
  49. result,
  50. bytesRead
  51. ) {
  52. if (err) {
  53. return callback(err)
  54. }
  55. if (bytesRead === size) {
  56. logger.error(
  57. { file: stateFile, size, bytesRead },
  58. 'project state file truncated'
  59. )
  60. }
  61. const array = result.toString().split('\n')
  62. const adjustedLength = Math.max(array.length, 1)
  63. const resourceList = array.slice(0, adjustedLength - 1)
  64. const oldState = array[adjustedLength - 1]
  65. const newState = `stateHash:${state}`
  66. logger.log(
  67. { state, oldState, basePath, stateMatches: newState === oldState },
  68. 'checking sync state'
  69. )
  70. if (newState !== oldState) {
  71. return callback(
  72. new Errors.FilesOutOfSyncError('invalid state for incremental update')
  73. )
  74. } else {
  75. const resources = resourceList.map((path) => ({ path }))
  76. callback(null, resources)
  77. }
  78. })
  79. },
  80. checkResourceFiles(resources, allFiles, basePath, callback) {
  81. // check the paths are all relative to current directory
  82. const containsRelativePath = (resource) => {
  83. const dirs = resource.path.split('/')
  84. return dirs.indexOf('..') !== -1
  85. }
  86. if (resources.some(containsRelativePath)) {
  87. return callback(new Error('relative path in resource file list'))
  88. }
  89. // check if any of the input files are not present in list of files
  90. const seenFiles = new Set(allFiles)
  91. const missingFiles = resources
  92. .map((resource) => resource.path)
  93. .filter((path) => !seenFiles.has(path))
  94. if (missingFiles.length > 0) {
  95. logger.err(
  96. { missingFiles, basePath, allFiles, resources },
  97. 'missing input files for project'
  98. )
  99. return callback(
  100. new Errors.FilesOutOfSyncError(
  101. 'resource files missing in incremental update'
  102. )
  103. )
  104. } else {
  105. callback()
  106. }
  107. }
  108. }