2020-02-19 11:14:37 +00:00
|
|
|
const Path = require('path')
|
|
|
|
const fs = require('fs')
|
2022-03-01 15:09:36 +00:00
|
|
|
const logger = require('@overleaf/logger')
|
2020-02-19 11:14:37 +00:00
|
|
|
const Errors = require('./Errors')
|
|
|
|
const SafeReader = require('./SafeReader')
|
2017-08-18 09:22:17 +00:00
|
|
|
|
2021-01-26 11:04:33 +00:00
|
|
|
module.exports = {
|
2020-02-19 11:14:37 +00:00
|
|
|
// The sync state is an identifier which must match for an
|
|
|
|
// incremental update to be allowed.
|
|
|
|
//
|
|
|
|
// The initial value is passed in and stored on a full
|
|
|
|
// compile, along with the list of resources..
|
|
|
|
//
|
|
|
|
// Subsequent incremental compiles must come with the same value - if
|
|
|
|
// not they will be rejected with a 409 Conflict response. The
|
|
|
|
// previous list of resources is returned.
|
|
|
|
//
|
|
|
|
// An incremental compile can only update existing files with new
|
|
|
|
// content. The sync state identifier must change if any docs or
|
|
|
|
// files are moved, added, deleted or renamed.
|
2017-08-18 09:22:17 +00:00
|
|
|
|
2020-02-19 11:14:37 +00:00
|
|
|
SYNC_STATE_FILE: '.project-sync-state',
|
|
|
|
SYNC_STATE_MAX_SIZE: 128 * 1024,
|
2017-08-18 09:22:17 +00:00
|
|
|
|
2020-02-19 11:14:37 +00:00
|
|
|
saveProjectState(state, resources, basePath, callback) {
|
|
|
|
const stateFile = Path.join(basePath, this.SYNC_STATE_FILE)
|
|
|
|
if (state == null) {
|
|
|
|
// remove the file if no state passed in
|
2022-05-16 12:38:18 +00:00
|
|
|
logger.debug({ state, basePath }, 'clearing sync state')
|
2021-01-26 11:04:33 +00:00
|
|
|
fs.unlink(stateFile, function (err) {
|
|
|
|
if (err && err.code !== 'ENOENT') {
|
2020-02-19 11:14:37 +00:00
|
|
|
return callback(err)
|
|
|
|
} else {
|
|
|
|
return callback()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} else {
|
2022-05-16 12:38:18 +00:00
|
|
|
logger.debug({ state, basePath }, 'writing sync state')
|
2021-07-13 11:04:48 +00:00
|
|
|
const resourceList = resources.map(resource => resource.path)
|
2021-01-26 11:04:33 +00:00
|
|
|
fs.writeFile(
|
2020-02-19 11:14:37 +00:00
|
|
|
stateFile,
|
2021-01-26 11:04:33 +00:00
|
|
|
[...resourceList, `stateHash:${state}`].join('\n'),
|
2020-02-19 11:14:37 +00:00
|
|
|
callback
|
|
|
|
)
|
|
|
|
}
|
|
|
|
},
|
2017-08-18 09:22:17 +00:00
|
|
|
|
2020-02-19 11:14:37 +00:00
|
|
|
checkProjectStateMatches(state, basePath, callback) {
|
|
|
|
const stateFile = Path.join(basePath, this.SYNC_STATE_FILE)
|
|
|
|
const size = this.SYNC_STATE_MAX_SIZE
|
2021-07-13 11:04:48 +00:00
|
|
|
SafeReader.readFile(
|
|
|
|
stateFile,
|
|
|
|
size,
|
|
|
|
'utf8',
|
|
|
|
function (err, result, bytesRead) {
|
|
|
|
if (err) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
if (bytesRead === size) {
|
|
|
|
logger.error(
|
|
|
|
{ file: stateFile, size, bytesRead },
|
|
|
|
'project state file truncated'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
const array = result ? result.toString().split('\n') : []
|
|
|
|
const adjustedLength = Math.max(array.length, 1)
|
|
|
|
const resourceList = array.slice(0, adjustedLength - 1)
|
|
|
|
const oldState = array[adjustedLength - 1]
|
|
|
|
const newState = `stateHash:${state}`
|
2022-05-16 12:38:18 +00:00
|
|
|
logger.debug(
|
2021-07-13 11:04:48 +00:00
|
|
|
{ state, oldState, basePath, stateMatches: newState === oldState },
|
|
|
|
'checking sync state'
|
2020-02-19 11:14:37 +00:00
|
|
|
)
|
2021-07-13 11:04:48 +00:00
|
|
|
if (newState !== oldState) {
|
|
|
|
return callback(
|
|
|
|
new Errors.FilesOutOfSyncError(
|
|
|
|
'invalid state for incremental update'
|
|
|
|
)
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
const resources = resourceList.map(path => ({ path }))
|
|
|
|
callback(null, resources)
|
|
|
|
}
|
2020-02-19 11:14:37 +00:00
|
|
|
}
|
2021-07-13 11:04:48 +00:00
|
|
|
)
|
2020-02-19 11:14:37 +00:00
|
|
|
},
|
2017-08-18 09:22:17 +00:00
|
|
|
|
2020-02-19 11:14:37 +00:00
|
|
|
checkResourceFiles(resources, allFiles, basePath, callback) {
|
|
|
|
// check the paths are all relative to current directory
|
2021-07-13 11:04:48 +00:00
|
|
|
const containsRelativePath = resource => {
|
2021-01-26 11:04:33 +00:00
|
|
|
const dirs = resource.path.split('/')
|
|
|
|
return dirs.indexOf('..') !== -1
|
2020-12-18 14:51:46 +00:00
|
|
|
}
|
2021-01-26 11:04:33 +00:00
|
|
|
if (resources.some(containsRelativePath)) {
|
|
|
|
return callback(new Error('relative path in resource file list'))
|
2020-02-19 11:14:37 +00:00
|
|
|
}
|
|
|
|
// check if any of the input files are not present in list of files
|
2021-01-26 11:04:33 +00:00
|
|
|
const seenFiles = new Set(allFiles)
|
|
|
|
const missingFiles = resources
|
2021-07-13 11:04:48 +00:00
|
|
|
.map(resource => resource.path)
|
|
|
|
.filter(path => !seenFiles.has(path))
|
2021-01-26 11:04:33 +00:00
|
|
|
if (missingFiles.length > 0) {
|
2020-02-19 11:14:37 +00:00
|
|
|
logger.err(
|
|
|
|
{ missingFiles, basePath, allFiles, resources },
|
|
|
|
'missing input files for project'
|
|
|
|
)
|
|
|
|
return callback(
|
|
|
|
new Errors.FilesOutOfSyncError(
|
|
|
|
'resource files missing in incremental update'
|
|
|
|
)
|
|
|
|
)
|
|
|
|
} else {
|
2021-01-26 11:04:33 +00:00
|
|
|
callback()
|
2020-02-19 11:14:37 +00:00
|
|
|
}
|
2021-07-13 11:04:48 +00:00
|
|
|
},
|
2020-02-19 11:14:37 +00:00
|
|
|
}
|