From 7d83552013a92149016dbe8881257d75575a75e0 Mon Sep 17 00:00:00 2001 From: Andrew Miner Date: Wed, 27 Apr 2016 22:47:44 -0700 Subject: [PATCH] Load all data files from a single archive --- src/client/models/base_model.coffee | 30 +++++-- src/client/models/event_recorder.coffee | 6 +- src/client/models/game/mod.coffee | 1 + src/client/models/site/file_cache.coffee | 86 +++++++++++++++++++ src/client/models/site/file_cache.test.coffee | 63 ++++++++++++++ src/client/site/site_controller.coffee | 6 +- src/common/constants.coffee | 1 + 7 files changed, 179 insertions(+), 14 deletions(-) create mode 100644 src/client/models/site/file_cache.coffee create mode 100644 src/client/models/site/file_cache.test.coffee diff --git a/src/client/models/base_model.coffee b/src/client/models/base_model.coffee index 8f88c9fff..3f8c279ab 100644 --- a/src/client/models/base_model.coffee +++ b/src/client/models/base_model.coffee @@ -19,10 +19,10 @@ module.exports = class BaseModel extends Backbone.Model continue if name is 'id' Object.defineProperty this, name, get:makeGetter(name), set:makeSetter(name) + @fileCache = options.fileCache or null + @loading = null @logEvents = options.logEvents or false - @state = c.modelState.unloaded - - @loading = null + @state = c.modelState.unloaded Object.defineProperties this, isUnloaded: { get:-> @state is c.modelState.unloaded } @@ -60,12 +60,24 @@ module.exports = class BaseModel extends Backbone.Model @state = c.modelState.loading @trigger c.event.request, this - @loading = w.promise (resolve, reject)=> - $.ajax - url: url - dataType: 'text' - success: (text, status, xhr)=> resolve @onLoadSucceeded text, status, xhr - error: (xhr, status, error)=> reject @onLoadFailed error, status, xhr + + loadFromServer = => + w.promise (resolve, reject)=> + $.ajax + url: url + dataType: 'text' + success: (text, status, xhr)=> resolve @onLoadSucceeded text, status, xhr + error: (xhr, status, error)=> reject @onLoadFailed error, status, xhr + + if @fileCache? + @loading = @fileCache.loading.then => + if @fileCache.hasFile url + @onLoadSucceeded @fileCache.getFile(url), 'success', {url:url} + return w.resolve(true) + else + @loading = loadFromServer() + else + @loading = loadFromServer() @loading.catch (e)-> # do nothing return @loading diff --git a/src/client/models/event_recorder.coffee b/src/client/models/event_recorder.coffee index 27d07947a..507ed32cd 100644 --- a/src/client/models/event_recorder.coffee +++ b/src/client/models/event_recorder.coffee @@ -1,9 +1,9 @@ -### +# # Crafting Guide - event_recorder.coffee # -# Copyright (c) 2014-2015 by Redwood Labs +# Copyright © 2014-2016 by Redwood Labs # All rights reserved. -### +# util = require 'util' diff --git a/src/client/models/game/mod.coffee b/src/client/models/game/mod.coffee index 8e613d821..d26e87907 100644 --- a/src/client/models/game/mod.coffee +++ b/src/client/models/game/mod.coffee @@ -129,6 +129,7 @@ module.exports = class Mod extends BaseModel @_modVersions.push modVersion @listenTo modVersion, c.event.change, => @trigger c.event.change, this + modVersion.fileCache = this.fileCache modVersion.mod = this @trigger c.event.add + ':modVersion', modVersion, this diff --git a/src/client/models/site/file_cache.coffee b/src/client/models/site/file_cache.coffee new file mode 100644 index 000000000..3a6b3eaf1 --- /dev/null +++ b/src/client/models/site/file_cache.coffee @@ -0,0 +1,86 @@ +# +# Crafting Guide - file_cache.coffee +# +# Copyright © 2014-2016 by Redwood Labs +# All rights reserved. +# + +######################################################################################################################## + +module.exports = class FileCache extends Backbone.Events + + @::FILE_TOKEN = '#file=' + + constructor: (url)-> + @_ajax = $.ajax + @_files = {} + @_loading = w.resolve(true) + + if url? then @loadArchive(url).catch(->) + + # Public Methods ############################################################################### + + getFile: (file)-> + return null unless @hasFile file + return @_files[file] + + hasFile: (file)-> + return @_files[file]? + + loadArchive: (url)-> + if not _.isFunction(@_ajax) then throw new Error '$.ajax is required' + + newLoading = w.promise (resolve, reject)=> + @_ajax + url: url + dataType: 'text' + success: (text, status, xhr)=> resolve @_onLoadSuccess text, status, xhr + error: (xhr, status, error)=> resolve @_onLoadError error, status, xhr + + @_loading = w.join @_loading, newLoading + + # Property Methods ############################################################################# + + Object.defineProperties @prototype, + + loading: + get: -> return @_loading + + # Private Methods ############################################################################## + + _onLoadError: (error, status, xhr)-> + console.error "Failed to load archive: #{xhr.responseText}" + return false + + _onLoadSuccess: (text, status, xhr)-> + return @_parseArchive text + + _parseArchive: (text)-> + file = null + tokenIndex = nameStartIndex = nameEndIndex = fileStartIndex = fileEndIndex = 0 + fileCount = 0 + + while true + tokenIndex = text.indexOf @FILE_TOKEN, fileStartIndex + if tokenIndex is -1 + if file? + @_files[file] = text.substring fileStartIndex, text.length + fileCount += 1 + break + + if file? + fileEndIndex = tokenIndex - 1 + @_files[file] = text.substring fileStartIndex, fileEndIndex + fileCount += 1 + + nameStartIndex = tokenIndex + @FILE_TOKEN.length + nameEndIndex = text.indexOf '\n', nameStartIndex + if nameEndIndex is -1 + file = text.substring nameStartIndex, text.length + @_files[file] = '' + break + + file = text.substring nameStartIndex, nameEndIndex + fileStartIndex = nameEndIndex + '\n'.length + + return fileCount > 0 diff --git a/src/client/models/site/file_cache.test.coffee b/src/client/models/site/file_cache.test.coffee new file mode 100644 index 000000000..ac0cea48f --- /dev/null +++ b/src/client/models/site/file_cache.test.coffee @@ -0,0 +1,63 @@ + +FileCache = require './file_cache' + +######################################################################################################################## + +describe 'file_cache.coffee', -> + + beforeEach -> + @cache = new FileCache + + makeTestAjax = (text)-> + return (options)-> + options.success text, 'success', {url:options.url} + + it 'can load an archive with a single file', -> + @cache._ajax = makeTestAjax '#file=alpha\nbravo charlie delta' + @cache.loadArchive 'http://test' + .then => + @cache.hasFile('alpha').should.equal true + @cache.getFile('alpha').should.equal 'bravo charlie delta' + + it 'can load an archive with multiple files', -> + @cache._ajax = makeTestAjax '#file=alpha\nbravo charlie\n#file=delta\necho foxtrot' + @cache.loadArchive 'http://test' + .then => + @cache.hasFile('alpha').should.equal true + @cache.getFile('alpha').should.equal 'bravo charlie' + @cache.hasFile('delta').should.equal true + @cache.getFile('delta').should.equal 'echo foxtrot' + + it 'can cope with an empty file in the middle of an archive', -> + @cache._ajax = makeTestAjax '#file=alpha\nbravo charlie\n#file=delta\n#file=echo\nfoxtrot golf' + @cache.loadArchive 'http://test' + .then => + @cache.hasFile('alpha').should.equal true + @cache.getFile('alpha').should.equal 'bravo charlie' + @cache.hasFile('delta').should.equal true + @cache.getFile('delta').should.equal '\n' + @cache.hasFile('echo').should.equal true + @cache.getFile('echo').should.equal 'foxtrot golf' + + it 'can cope with an empty file at the end of an archive', -> + @cache._ajax = makeTestAjax '#file=alpha\nbravo charlie\n#file=delta' + @cache.loadArchive 'http://test' + .then => + @cache.hasFile('alpha').should.equal true + @cache.getFile('alpha').should.equal 'bravo charlie' + @cache.hasFile('delta').should.equal true + @cache.getFile('delta').should.equal '' + + it 'can load multiple, non-overlapping archives', -> + @cache._ajax = makeTestAjax '#file=alpha\nbravo charlie\n#file=delta\necho foxtrot' + @cache.loadArchive 'http://test' + + @cache._ajax = makeTestAjax '#file=golf\nhotel india' + @cache.loadArchive 'http://test2' + .then => + @cache.hasFile('alpha').should.equal true + @cache.getFile('alpha').should.equal 'bravo charlie' + @cache.hasFile('delta').should.equal true + @cache.getFile('delta').should.equal 'echo foxtrot' + @cache.hasFile('golf').should.equal true + @cache.getFile('golf').should.equal 'hotel india' diff --git a/src/client/site/site_controller.coffee b/src/client/site/site_controller.coffee index 9d8c0ca4b..0cf7b832f 100644 --- a/src/client/site/site_controller.coffee +++ b/src/client/site/site_controller.coffee @@ -7,6 +7,7 @@ BaseController = require './base_controller' FeedbackController = require './feedback/feedback_controller' +FileCache = require '../models/site/file_cache' FooterController = require './footer/footer_controller' GitHubUser = require '../models/site/github_user' HeaderController = require './header/header_controller' @@ -26,8 +27,9 @@ module.exports = class SiteController extends BaseController super options @client = options.client + @fileCache = new FileCache c.url.modpackArchive() @imageLoader = new ImageLoader defaultUrl:'/images/unknown.png' - @modPack = new ModPack + @modPack = new ModPack {}, fileCache:@fileCache @router = new Router this @storage = options.storage @@ -42,7 +44,7 @@ module.exports = class SiteController extends BaseController m.activeModVersion.fetch() if m.activeModVersion? for modSlug, modData of c.defaultMods - mod = new Mod slug:modSlug + mod = new Mod {slug:modSlug}, {fileCache:@fileCache} mod.on c.event.change + ':activeModVersion', makeResponder mod @storage.register "mod:#{mod.slug}", mod, 'activeVersion', modData.defaultVersion mod.fetch() diff --git a/src/common/constants.coffee b/src/common/constants.coffee index e4134b7db..908445d0f 100644 --- a/src/common/constants.coffee +++ b/src/common/constants.coffee @@ -138,6 +138,7 @@ url.login = _.template "/login" url.mod = _.template "/browse/<%= modSlug %>/" url.modData = _.template "/data/<%= modSlug %>/mod.cg" url.modIcon = _.template "/data/<%= modSlug %>/icon.png" +url.modpackArchive = _.template "/data/modpack.cg" url.modVersionData = _.template "/data/<%= modSlug %>/versions/<%= modVersion %>/mod-version.cg" url.root = _.template "/" url.tutorial = _.template "/browse/<%= modSlug %>/tutorials/<%= tutorialSlug %>/"