diff --git a/services/clsi/app/coffee/ResourceStateManager.coffee b/services/clsi/app/coffee/ResourceStateManager.coffee new file mode 100644 index 0000000000..2a6d177b2e --- /dev/null +++ b/services/clsi/app/coffee/ResourceStateManager.coffee @@ -0,0 +1,46 @@ +Path = require "path" +fs = require "fs" +mkdirp = require "mkdirp" +logger = require "logger-sharelatex" +settings = require("settings-sharelatex") +Errors = require "./Errors" + +module.exports = ResourceStateManager = + + # 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. + # + # Subsequent incremental compiles must come with the same value - if + # not they will be rejected with a 409 Conflict response. + # + # 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. + + SYNC_STATE_FILE: ".project-sync-state" + + saveProjectStateHash: (state, basePath, callback) -> + stateFile = Path.join(basePath, @SYNC_STATE_FILE) + if not state? # remove the file if no state passed in + logger.log state:state, basePath:basePath, "clearing sync state" + fs.unlink stateFile, (err) -> + if err? and err.code isnt 'ENOENT' + return callback(err) + else + return callback() + else + logger.log state:state, basePath:basePath, "writing sync state" + fs.writeFile stateFile, state, {encoding: 'ascii'}, callback + + checkProjectStateHashMatches: (state, basePath, callback) -> + stateFile = Path.join(basePath, @SYNC_STATE_FILE) + fs.readFile stateFile, {encoding:'ascii'}, (err, oldState) -> + if err? and err.code isnt 'ENOENT' + return callback(err) + else if state isnt oldState + return callback new Errors.FilesOutOfSyncError("invalid state for incremental update") + else if state is oldState + callback(null) diff --git a/services/clsi/app/coffee/ResourceWriter.coffee b/services/clsi/app/coffee/ResourceWriter.coffee index fae570f877..7f6702621c 100644 --- a/services/clsi/app/coffee/ResourceWriter.coffee +++ b/services/clsi/app/coffee/ResourceWriter.coffee @@ -4,9 +4,9 @@ fs = require "fs" async = require "async" mkdirp = require "mkdirp" OutputFileFinder = require "./OutputFileFinder" +ResourceStateManager = require "./ResourceStateManager" ResourceListManager = require "./ResourceListManager" Metrics = require "./Metrics" -Errors = require "./Errors" logger = require "logger-sharelatex" settings = require("settings-sharelatex") @@ -16,9 +16,8 @@ module.exports = ResourceWriter = syncResourcesToDisk: (request, basePath, callback = (error, resourceList) ->) -> if request.syncType is "incremental" - ResourceWriter.checkSyncState request.syncState, basePath, (error, syncStateOk) -> - logger.log syncState: request.syncState, result:syncStateOk, "checked state on incremental request" - return callback new Errors.FilesOutOfSyncError("invalid state for incremental update") if not syncStateOk + ResourceStateManager.checkProjectStateHashMatches request.syncState, basePath, (error) -> + return callback(error) if error? ResourceListManager.loadResourceList basePath, (error, resourceList) -> return callback(error) if error? ResourceWriter._removeExtraneousFiles resourceList, basePath, (error) => @@ -26,53 +25,15 @@ module.exports = ResourceWriter = ResourceWriter.saveIncrementalResourcesToDisk request.project_id, request.resources, basePath, (error) -> return callback(error) if error? callback(null, resourceList) - else @saveAllResourcesToDisk request.project_id, request.resources, basePath, (error) -> return callback(error) if error? - ResourceWriter.storeSyncState request.syncState, basePath, (error) -> + ResourceStateManager.saveProjectStateHash request.syncState, basePath, (error) -> return callback(error) if error? ResourceListManager.saveResourceList request.resources, basePath, (error) => return callback(error) if error? callback(null, request.resources) - # 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. - # - # Subsequent incremental compiles must come with the same value - if - # not they will be rejected with a 409 Conflict response. - # - # 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. - - SYNC_STATE_FILE: ".project-sync-state" - - storeSyncState: (state, basePath, callback) -> - stateFile = Path.join(basePath, @SYNC_STATE_FILE) - if not state? # remove the file if no state passed in - logger.log state:state, basePath:basePath, "clearing sync state" - fs.unlink stateFile, (err) -> - if err? and err.code isnt 'ENOENT' - return callback(err) - else - return callback() - else - logger.log state:state, basePath:basePath, "writing sync state" - fs.writeFile stateFile, state, {encoding: 'ascii'}, callback - - checkSyncState: (state, basePath, callback) -> - stateFile = Path.join(basePath, @SYNC_STATE_FILE) - fs.readFile stateFile, {encoding:'ascii'}, (err, oldState) -> - if err? and err.code isnt 'ENOENT' - return callback(err) - else - # return true if state matches, false otherwise (including file not existing) - callback(null, if state is oldState then true else false) - saveIncrementalResourcesToDisk: (project_id, resources, basePath, callback = (error) ->) -> @_createDirectory basePath, (error) => return callback(error) if error? diff --git a/services/clsi/test/unit/coffee/ResourceWriterTests.coffee b/services/clsi/test/unit/coffee/ResourceWriterTests.coffee index 9a17b05c65..0804438f3b 100644 --- a/services/clsi/test/unit/coffee/ResourceWriterTests.coffee +++ b/services/clsi/test/unit/coffee/ResourceWriterTests.coffee @@ -11,6 +11,7 @@ describe "ResourceWriter", -> mkdir: sinon.stub().callsArg(1) unlink: sinon.stub().callsArg(1) "./ResourceListManager": @ResourceListManager = {} + "./ResourceStateManager": @ResourceStateManager = {} "wrench": @wrench = {} "./UrlCache" : @UrlCache = {} "mkdirp" : @mkdirp = sinon.stub().callsArg(1) @@ -32,8 +33,8 @@ describe "ResourceWriter", -> ] @ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3) @ResourceWriter._removeExtraneousFiles = sinon.stub().callsArg(2) - @ResourceWriter.checkSyncState = sinon.stub().callsArg(2) - @ResourceWriter.storeSyncState = sinon.stub().callsArg(2) + @ResourceStateManager.checkProjectStateHashMatches = sinon.stub().callsArg(2) + @ResourceStateManager.saveProjectStateHash = sinon.stub().callsArg(2) @ResourceListManager.saveResourceList = sinon.stub().callsArg(2) @ResourceListManager.loadResourceList = sinon.stub().callsArg(1) @ResourceWriter.syncResourcesToDisk({ @@ -54,7 +55,7 @@ describe "ResourceWriter", -> .should.equal true it "should store the sync state", -> - @ResourceWriter.storeSyncState + @ResourceStateManager.saveProjectStateHash .calledWith(@syncState, @basePath) .should.equal true @@ -73,8 +74,8 @@ describe "ResourceWriter", -> ] @ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3) @ResourceWriter._removeExtraneousFiles = sinon.stub().callsArg(2) - @ResourceWriter.checkSyncState = sinon.stub().callsArgWith(2, null, true) - @ResourceWriter.storeSyncState = sinon.stub().callsArg(2) + @ResourceStateManager.checkProjectStateHashMatches = sinon.stub().callsArg(2) + @ResourceStateManager.saveProjectStateHash = sinon.stub().callsArg(2) @ResourceListManager.saveResourceList = sinon.stub().callsArg(2) @ResourceListManager.loadResourceList = sinon.stub().callsArgWith(1, null, @resources) @ResourceWriter.syncResourcesToDisk({ @@ -85,7 +86,7 @@ describe "ResourceWriter", -> }, @basePath, @callback) it "should check the sync state matches", -> - @ResourceWriter.checkSyncState + @ResourceStateManager.checkProjectStateHashMatches .calledWith(@syncState, @basePath) .should.equal true @@ -103,6 +104,28 @@ describe "ResourceWriter", -> it "should call the callback", -> @callback.called.should.equal true + describe "syncResourcesToDisk on an incremental update when the state does not match", -> + beforeEach -> + @resources = [ + "resource-1-mock" + ] + @ResourceStateManager.checkProjectStateHashMatches = sinon.stub().callsArgWith(2, @error = new Error()) + @ResourceWriter.syncResourcesToDisk({ + project_id: @project_id, + syncType: "incremental", + syncState: @syncState = "1234567890abcdef", + resources: @resources + }, @basePath, @callback) + + it "should check whether the sync state matches", -> + @ResourceStateManager.checkProjectStateHashMatches + .calledWith(@syncState, @basePath) + .should.equal true + + it "should call the callback with an error", -> + @callback.calledWith(@error).should.equal true + + describe "_removeExtraneousFiles", -> beforeEach -> @output_files = [{