cordova-app-loader
Advanced tools
Comparing version 0.10.0 to 0.11.0
@@ -48,5 +48,8 @@ var path = require('path'); | ||
} | ||
if(typeof manifest.version === 'number'){ | ||
manifest.version++; | ||
} else { | ||
manifest.version = crypto.createHash('sha1').update(versionChecksum).digest('hex'); | ||
} | ||
manifest.version = crypto.createHash('sha1').update(versionChecksum).digest('hex'); | ||
try { | ||
@@ -53,0 +56,0 @@ fs.writeFileSync( |
{ | ||
"name": "cordova-app-loadaer", | ||
"main": "dist/CordovaAppLoader.js", | ||
"version": "0.10.0", | ||
"version": "0.11.0", | ||
"homepage": "https://github.com/markmarijnissen/cordova-file-cache", | ||
@@ -6,0 +6,0 @@ "authors": [ |
@@ -48,4 +48,4 @@ /******/ (function(modules) { // webpackBootstrap | ||
window.CordovaAppLoader = __webpack_require__(1); | ||
window.CordovaFileCache = __webpack_require__(2); | ||
window.CordovaPromiseFS = __webpack_require__(3); | ||
window.CordovaFileCache = __webpack_require__(3); | ||
window.CordovaPromiseFS = __webpack_require__(2); | ||
window.Promise = __webpack_require__(4); | ||
@@ -58,5 +58,9 @@ window.setImmediate = window.setTimeout; // for promiscuous to work! | ||
var CordovaFileCache = __webpack_require__(2); | ||
var CordovaFileCache = __webpack_require__(3); | ||
var CordovaPromiseFS = __webpack_require__(2); | ||
var Promise = null; | ||
var BUNDLED_FILE_SYSTEM_TYPE = 8; | ||
var BUNDLE_ROOT = 'www/'; | ||
function AppLoader(options){ | ||
@@ -67,2 +71,3 @@ if(!options) throw new Error('CordovaAppLoader has no options!'); | ||
if(!window.pegasus || !window.Manifest) throw new Error('CordovaAppLoader bootstrap.js is missing.'); | ||
this.allowServerRootFromManifest = options.allowServerRootFromManifest === true; | ||
Promise = options.fs.Promise; | ||
@@ -73,2 +78,4 @@ | ||
this.newManifest = null; | ||
this.bundledManifest = null; | ||
this.bundledFS = null; | ||
this._lastUpdateFiles = localStorage.getItem('last_update_files'); | ||
@@ -79,3 +86,3 @@ | ||
if(!!options.serverRoot && options.serverRoot[options.serverRoot.length-1] !== '/') options.serverRoot += '/'; | ||
this.newManifestUrl = options.serverRoot + (options.manifest || 'manifest.json'); | ||
this.newManifestUrl = options.manifestUrl || options.serverRoot + (options.manifest || 'manifest.json'); | ||
@@ -88,2 +95,3 @@ // initialize a file cache | ||
this.corruptNewManifest = false; | ||
this._toBeCopied = []; | ||
this._toBeDeleted = []; | ||
@@ -105,24 +113,79 @@ this._toBeDownloaded = []; | ||
AppLoader.prototype.getBundledManifest = function(){ | ||
var self = this; | ||
var bootstrapScript = document.querySelector('script[manifest]'); | ||
var bundledManifestUrl = (bootstrapScript? bootstrapScript.getAttribute('manifest'): null) || 'manifest.json'; | ||
return new Promise(function(resolve,reject){ | ||
if(self.bundledManifest) { | ||
resolve(self.bundledManifest); | ||
} else { | ||
pegasus(bundledManifestUrl).then(function(bundledManifest){ | ||
self.bundledManifest = bundledManifest; | ||
resolve(bundledManifest); | ||
},reject); | ||
setTimeout(function(){reject(new Error('bundled manifest timeout'));},self._checkTimeout); | ||
} | ||
}); | ||
}; | ||
AppLoader.prototype.getBundledFS = function(){ | ||
if(!self.bundledFS){ | ||
self.bundledFS = CordovaPromiseFS({ fileSystem: BUNDLED_FILE_SYSTEM_TYPE }); | ||
} | ||
return self.bundledFS; | ||
}; | ||
AppLoader.prototype.copyFromBundle = function(file){ | ||
var self = this; | ||
return new Promise(function(resolve,reject){ | ||
var bundledFS = self.getBundledFS(); | ||
var cacheFS = self.cache._fs; | ||
var destDirPath = cacheFS.dirname(self.cache.localRoot + file); | ||
var srcFilePath = BUNDLE_ROOT + file; | ||
var srcFilename = cacheFS.filename(file); | ||
return Promise.all([bundledFS.file(srcFilePath),cacheFS.ensure(destDirPath)]) | ||
.then(function(val){ | ||
var srcFile = val[0], destDir = val[1]; | ||
srcFile.copyTo(destDir,srcFilename,resolve,reject); | ||
},reject); | ||
}); | ||
}; | ||
AppLoader.prototype.check = function(newManifest){ | ||
var self = this, manifest = this.manifest; | ||
if(typeof newManifest === "string") { | ||
self.newManifestUrl = newManifest; | ||
newManifest = undefined; | ||
} | ||
return new Promise(function(resolve,reject){ | ||
if(typeof newManifest === "string") { | ||
self.newManifestUrl = newManifest; | ||
newManifest = undefined; | ||
var gotNewManifest = new Promise(function(resolve,reject){ | ||
if(typeof newManifest === "object") { | ||
resolve(newManifest); | ||
} else { | ||
console.log('checking new manifest:',self.newManifestUrl); | ||
pegasus(self.newManifestUrl).then(resolve,reject); | ||
setTimeout(function(){reject(new Error('new manifest timeout'));},self._checkTimeout); | ||
} | ||
}); | ||
function checkManifest(newManifest){ | ||
if(JSON.stringify(newManifest.files) === self._lastUpdateFiles) { | ||
if(JSON.stringify(newManifest.files) !== JSON.stringify(Manifest.files)){ | ||
console.warn('New manifest available, but an earlier update attempt failed. Will not download.'); | ||
self.corruptNewManifest = true; | ||
resolve(null); | ||
return new Promise(function(resolve,reject){ | ||
Promise.all([gotNewManifest,self.getBundledManifest(),self.cache.list()]) | ||
.then(function(values){ | ||
var newManifest = values[0]; | ||
var bundledManifest = values[1]; | ||
// Prevent end-less update loop, check if new manifest | ||
// has been downloaded before (but failes) | ||
if(JSON.stringify(newManifest.files) === self._lastUpdateFiles) { | ||
if(JSON.stringify(newManifest.files) !== JSON.stringify(Manifest.files)){ | ||
console.warn('New manifest available, but an earlier update attempt failed. Will not download.'); | ||
self.corruptNewManifest = true; | ||
resolve(null); | ||
} | ||
resolve(false); | ||
return; | ||
} | ||
resolve(false); | ||
return; | ||
} | ||
// make sure cache is ready for the DIFF operations! | ||
self.cache.ready.then(function(list){ | ||
// Check if new manifest is valid | ||
if(!newManifest.files){ | ||
@@ -133,41 +196,62 @@ reject('Downloaded Manifest has no "files" attribute.'); | ||
var newFiles = self._createFilemap(newManifest.files); | ||
var oldFiles = self._createFilemap(manifest.files); | ||
// We're good to go check! Get all the files we need | ||
var cachedFiles = values[2]; // files in cache | ||
var oldFiles = self._createFilemap(manifest.files); // files in current manifest | ||
var newFiles = self._createFilemap(newManifest.files); // files in new manifest | ||
var bundledFiles = self._createFilemap(bundledManifest.files); // files in app bundle | ||
// Create the diff | ||
self._toBeDownloaded = Object.keys(newFiles) | ||
// Create COPY and DOWNLOAD lists | ||
self._toBeDownloaded = []; | ||
self._toBeCopied = []; | ||
self._toBeDeleted= []; | ||
var isCordova = self.cache._fs.isCordova; | ||
Object.keys(newFiles) | ||
// Find files that have changed version or are missing | ||
.filter(function(file){ | ||
return !oldFiles[file] | ||
|| oldFiles[file].version !== newFiles[file].version | ||
|| !self.cache.isCached(file); | ||
// if new file, or... | ||
return !oldFiles[file] || | ||
// version has changed, or... | ||
oldFiles[file].version !== newFiles[file].version || | ||
// not in cache for some reason | ||
!self.cache.isCached(file); | ||
}) | ||
// Add them to the correct list | ||
.forEach(function(file){ | ||
// bundled version matches new version, so we can copy! | ||
if(isCordova && bundledFiles[file] && bundledFiles[file].version === newFiles[file].version){ | ||
self._toBeCopied.push(file); | ||
// othwerwise, we must download | ||
} else { | ||
self._toBeDownloaded.push(file); | ||
} | ||
}); | ||
self.cache.list().then(function(files){ | ||
self._toBeDeleted = files | ||
.map(function(file){ | ||
return file.substr(self.cache.localRoot.length); | ||
}) | ||
.filter(function(file){ | ||
return !newFiles[file]; | ||
}) | ||
.concat(self._toBeDownloaded); | ||
// Delete files | ||
self._toBeDeleted = cachedFiles | ||
.map(function(file){ | ||
return file.substr(self.cache.localRoot.length); | ||
}) | ||
.filter(function(file){ | ||
// Everything that is not in new manifest, or.... | ||
return !newFiles[file] || | ||
// Files that will be downloaded, or... | ||
self._toBeDownloaded.indexOf(file) >= 0 || | ||
// Files that will be copied | ||
self._toBeCopied.indexOf(file) >= 0; | ||
}); | ||
if(self._toBeDeleted.length > 0 || self._toBeDownloaded.length > 0){ | ||
// Save the new Manifest | ||
self.newManifest = newManifest; | ||
self.newManifest.root = self.cache.localInternalURL; | ||
resolve(true); | ||
} else { | ||
resolve(false); | ||
} | ||
},reject); | ||
},reject); | ||
} | ||
if(typeof newManifest === "object") { | ||
checkManifest(newManifest); | ||
} else { | ||
pegasus(self.newManifestUrl).then(checkManifest,reject); | ||
setTimeout(function(){reject(new Error('timeout'));},self._checkTimeout); | ||
} | ||
}); | ||
var changes = self._toBeDeleted.length + self._toBeDownloaded.length; | ||
// Note: if we only need to copy files, we can keep serving from bundle! | ||
// So no update is needed! | ||
if(changes > 0){ | ||
// Save the new Manifest | ||
self.newManifest = newManifest; | ||
self.newManifest.root = self.cache.localInternalURL; | ||
resolve(true); | ||
} else { | ||
resolve(false); | ||
} | ||
}); // end of .then | ||
}); // end of new Promise | ||
}; | ||
@@ -195,2 +279,8 @@ | ||
.then(function(){ | ||
return Promise.all(self._toBeCopied.map(self.copyFromBundle.bind(self))); | ||
}) | ||
.then(function(){ | ||
if(self.allowServerRootFromManifest && self.newManifest.serverRoot){ | ||
self.cache.serverRoot = self.newManifest.serverRoot; | ||
} | ||
self.cache.add(self._toBeDownloaded); | ||
@@ -223,2 +313,3 @@ return self.cache.download(onprogress); | ||
AppLoader.prototype.clear = function(){ | ||
localStorage.removeItem('last_update_files'); | ||
localStorage.removeItem('manifest'); | ||
@@ -242,261 +333,3 @@ return this.cache.clear(); | ||
var hash = __webpack_require__(5); | ||
var Promise = null; | ||
var isCordova = typeof cordova !== 'undefined'; | ||
/* Cordova File Cache x */ | ||
function FileCache(options){ | ||
var self = this; | ||
// cordova-promise-fs | ||
this._fs = options.fs; | ||
if(!this._fs) { | ||
throw new Error('Missing required option "fs". Add an instance of cordova-promise-fs.'); | ||
} | ||
// Use Promises from fs. | ||
Promise = this._fs.Promise; | ||
// 'mirror' mirrors files structure from "serverRoot" to "localRoot" | ||
// 'hash' creates a 1-deep filestructure, where the filenames are hashed server urls (with extension) | ||
this._mirrorMode = options.mode !== 'hash'; | ||
this._retry = options.retry || [500,1500,8000]; | ||
this._cacheBuster = !!options.cacheBuster; | ||
// normalize path | ||
this.localRoot = this._fs.normalize(options.localRoot || 'data'); | ||
this.serverRoot = this._fs.normalize(options.serverRoot || ''); | ||
// set internal variables | ||
this._downloading = []; // download promises | ||
this._added = []; // added files | ||
this._cached = {}; // cached files | ||
// list existing cache contents | ||
this.ready = this._fs.ensure(this.localRoot) | ||
.then(function(entry){ | ||
self.localInternalURL = isCordova? entry.toInternalURL(): entry.toURL(); | ||
self.localUrl = entry.toURL(); | ||
return self.list(); | ||
}); | ||
} | ||
/** | ||
* Helper to cache all 'internalURL' and 'URL' for quick synchronous access | ||
* to the cached files. | ||
*/ | ||
FileCache.prototype.list = function list(){ | ||
var self = this; | ||
return new Promise(function(resolve,reject){ | ||
self._fs.list(self.localRoot,'rfe').then(function(entries){ | ||
self._cached = {}; | ||
entries = entries.map(function(entry){ | ||
var fullPath = self._fs.normalize(entry.fullPath); | ||
self._cached[fullPath] = { | ||
toInternalURL: isCordova? entry.toInternalURL(): entry.toURL(), | ||
toURL: entry.toURL(), | ||
}; | ||
return fullPath; | ||
}); | ||
resolve(entries); | ||
},function(){ | ||
resolve([]); | ||
}); | ||
}); | ||
}; | ||
FileCache.prototype.add = function add(urls){ | ||
if(!urls) urls = []; | ||
if(typeof urls === 'string') urls = [urls]; | ||
var self = this; | ||
urls.forEach(function(url){ | ||
url = self.toServerURL(url); | ||
if(self._added.indexOf(url) === -1) { | ||
self._added.push(url); | ||
} | ||
}); | ||
return self.isDirty(); | ||
}; | ||
FileCache.prototype.remove = function remove(urls,returnPromises){ | ||
if(!urls) urls = []; | ||
var promises = []; | ||
if(typeof urls === 'string') urls = [urls]; | ||
var self = this; | ||
urls.forEach(function(url){ | ||
var index = self._added.indexOf(self.toServerURL(url)); | ||
if(index >= 0) self._added.splice(index,1); | ||
var path = self.toPath(url); | ||
promises.push(self._fs.remove(path)); | ||
delete self._cached[path]; | ||
}); | ||
return returnPromises? Promise.all(promises): self.isDirty(); | ||
}; | ||
FileCache.prototype.getDownloadQueue = function(){ | ||
var self = this; | ||
var queue = self._added.filter(function(url){ | ||
return !self.isCached(url); | ||
}); | ||
return queue; | ||
}; | ||
FileCache.prototype.getAdded = function() { | ||
return this._added; | ||
}; | ||
FileCache.prototype.isDirty = function isDirty(){ | ||
return this.getDownloadQueue().length > 0; | ||
}; | ||
FileCache.prototype.download = function download(onprogress){ | ||
var fs = this._fs; | ||
var self = this; | ||
self.abort(); | ||
return new Promise(function(resolve,reject){ | ||
// make sure cache directory exists and that | ||
// we have retrieved the latest cache contents | ||
// to avoid downloading files we already have! | ||
fs.ensure(self.localRoot).then(function(){ | ||
return self.list(); | ||
}).then(function(){ | ||
// no dowloads needed, resolve | ||
if(!self.isDirty()) { | ||
resolve(self); | ||
return; | ||
} | ||
// keep track of number of downloads! | ||
var queue = self.getDownloadQueue(); | ||
var started = []; | ||
var index = self._downloading.length; | ||
var done = self._downloading.length; | ||
var total = self._downloading.length + queue.length; | ||
// download every file in the queue (which is the diff from _added with _cached) | ||
queue.forEach(function(url){ | ||
var path = self.toPath(url); | ||
// augment progress event with index/total stats | ||
var onSingleDownloadProgress; | ||
if(typeof onprogress === 'function') { | ||
onSingleDownloadProgress = function(ev){ | ||
ev.queueIndex = index; | ||
ev.queueSize = total; | ||
ev.url = url; | ||
ev.path = path; | ||
ev.percentage = index / total; | ||
if(ev.loaded > 0 && ev.total > 0 && index !== total){ | ||
ev.percentage += (ev.loaded / ev.total) / total; | ||
} | ||
if(started.indexOf(url) < 0) { | ||
started.push(url); | ||
index++; | ||
} | ||
onprogress(ev); | ||
}; | ||
} | ||
// callback | ||
var onDone = function(){ | ||
done++; | ||
// when we're done | ||
if(done === total) { | ||
// reset downloads | ||
self._downloading = []; | ||
// check if we got everything | ||
self.list().then(function(){ | ||
// final progress event! | ||
if(onSingleDownloadProgress) onSingleDownloadProgress(new ProgressEvent()); | ||
// Yes, we're not dirty anymore! | ||
if(!self.isDirty()) { | ||
resolve(self); | ||
// Aye, some files got left behind! | ||
} else { | ||
reject(self.getDownloadQueue()); | ||
} | ||
},reject); | ||
} | ||
}; | ||
var downloadUrl = url; | ||
if(self._cacheBuster) downloadUrl += "?"+Date.now(); | ||
var download = fs.download(url,path,{retry:self._retry},onSingleDownloadProgress); | ||
download.then(onDone,onDone); | ||
self._downloading.push(download); | ||
}); | ||
},reject); | ||
}); | ||
}; | ||
FileCache.prototype.abort = function abort(){ | ||
this._downloading.forEach(function(download){ | ||
download.abort(); | ||
}); | ||
this._downloading = []; | ||
}; | ||
FileCache.prototype.isCached = function isCached(url){ | ||
url = this.toPath(url); | ||
return !!this._cached[url]; | ||
}; | ||
FileCache.prototype.clear = function clear(){ | ||
var self = this; | ||
this._cached = {}; | ||
return this._fs.removeDir(this.localRoot).then(function(){ | ||
return self._fs.ensure(self.localRoot); | ||
}); | ||
}; | ||
/** | ||
* Helpers to output to various formats | ||
*/ | ||
FileCache.prototype.toInternalURL = function toInternalURL(url){ | ||
path = this.toPath(url); | ||
if(this._cached[path]) return this._cached[path].toInternalURL; | ||
return url; | ||
}; | ||
FileCache.prototype.get = function get(url){ | ||
path = this.toPath(url); | ||
if(this._cached[path]) return this._cached[path].toInternalURL; | ||
return this.toServerURL(url); | ||
}; | ||
FileCache.prototype.toDataURL = function toDataURL(url){ | ||
return this._fs.toDataURL(this.toPath(url)); | ||
}; | ||
FileCache.prototype.toURL = function toURL(url){ | ||
path = this.toPath(url); | ||
return this._cached[path]? this._cached[path].toURL: url; | ||
}; | ||
FileCache.prototype.toServerURL = function toServerURL(path){ | ||
path = this._fs.normalize(path); | ||
return path.indexOf('://') < 0? this.serverRoot + path: path; | ||
}; | ||
/** | ||
* Helper to transform remote URL to a local path (for cordova-promise-fs) | ||
*/ | ||
FileCache.prototype.toPath = function toPath(url){ | ||
if(this._mirrorMode) { | ||
url = url = this._fs.normalize(url || ''); | ||
len = this.serverRoot.length; | ||
if(url.substr(0,len) !== this.serverRoot) { | ||
return this.localRoot + url; | ||
} else { | ||
return this.localRoot + url.substr(len); | ||
} | ||
} else { | ||
return this.localRoot + hash(url) + url.substr(url.lastIndexOf('.')); | ||
} | ||
}; | ||
module.exports = FileCache; | ||
/***/ }, | ||
/* 3 */ | ||
/***/ function(module, exports, __webpack_require__) { | ||
/** | ||
* Static Private functions | ||
@@ -528,2 +361,3 @@ */ | ||
function normalize(str){ | ||
str = str || ''; | ||
if(str[0] === '/') str = str.substr(1); | ||
@@ -600,3 +434,12 @@ if(!!str && str.indexOf('.') < 0 && str[str.length-1] !== '/') str += '/'; | ||
deviceready.then(function(){ | ||
window.requestFileSystem(options.persistent? 1: 0, options.storageSize, resolve, reject); | ||
var type = options.persistent? 1: 0; | ||
if(typeof options.fileSystem === 'number'){ | ||
type = options.fileSystem; | ||
} | ||
// Chrome only supports persistent and temp storage, not the exotic onces from Cordova | ||
if(!isCordova && type > 1) { | ||
console.warn('Chrome does not support fileSystem "'+type+'". Falling back on "0" (temporary).'); | ||
type = 0; | ||
} | ||
window.requestFileSystem(type, options.storageSize, resolve, reject); | ||
setTimeout(function(){ reject(new Error('Could not retrieve FileSystem after 5 seconds.')); },5100); | ||
@@ -631,5 +474,8 @@ },reject); | ||
function file(path,options){ | ||
path = normalize(path); | ||
options = options || {}; | ||
return new Promise(function(resolve,reject){ | ||
if(path instanceof FileEntry) { | ||
return resolve(path); | ||
} | ||
path = normalize(path); | ||
options = options || {}; | ||
return fs.then(function(fs){ | ||
@@ -973,2 +819,260 @@ fs.root.getFile(path,options,resolve,reject); | ||
/***/ }, | ||
/* 3 */ | ||
/***/ function(module, exports, __webpack_require__) { | ||
var hash = __webpack_require__(5); | ||
var Promise = null; | ||
var isCordova = typeof cordova !== 'undefined'; | ||
/* Cordova File Cache x */ | ||
function FileCache(options){ | ||
var self = this; | ||
// cordova-promise-fs | ||
this._fs = options.fs; | ||
if(!this._fs) { | ||
throw new Error('Missing required option "fs". Add an instance of cordova-promise-fs.'); | ||
} | ||
// Use Promises from fs. | ||
Promise = this._fs.Promise; | ||
// 'mirror' mirrors files structure from "serverRoot" to "localRoot" | ||
// 'hash' creates a 1-deep filestructure, where the filenames are hashed server urls (with extension) | ||
this._mirrorMode = options.mode !== 'hash'; | ||
this._retry = options.retry || [500,1500,8000]; | ||
this._cacheBuster = !!options.cacheBuster; | ||
// normalize path | ||
this.localRoot = this._fs.normalize(options.localRoot || 'data'); | ||
this.serverRoot = this._fs.normalize(options.serverRoot || ''); | ||
// set internal variables | ||
this._downloading = []; // download promises | ||
this._added = []; // added files | ||
this._cached = {}; // cached files | ||
// list existing cache contents | ||
this.ready = this._fs.ensure(this.localRoot) | ||
.then(function(entry){ | ||
self.localInternalURL = isCordova? entry.toInternalURL(): entry.toURL(); | ||
self.localUrl = entry.toURL(); | ||
return self.list(); | ||
}); | ||
} | ||
/** | ||
* Helper to cache all 'internalURL' and 'URL' for quick synchronous access | ||
* to the cached files. | ||
*/ | ||
FileCache.prototype.list = function list(){ | ||
var self = this; | ||
return new Promise(function(resolve,reject){ | ||
self._fs.list(self.localRoot,'rfe').then(function(entries){ | ||
self._cached = {}; | ||
entries = entries.map(function(entry){ | ||
var fullPath = self._fs.normalize(entry.fullPath); | ||
self._cached[fullPath] = { | ||
toInternalURL: isCordova? entry.toInternalURL(): entry.toURL(), | ||
toURL: entry.toURL(), | ||
}; | ||
return fullPath; | ||
}); | ||
resolve(entries); | ||
},function(){ | ||
resolve([]); | ||
}); | ||
}); | ||
}; | ||
FileCache.prototype.add = function add(urls){ | ||
if(!urls) urls = []; | ||
if(typeof urls === 'string') urls = [urls]; | ||
var self = this; | ||
urls.forEach(function(url){ | ||
url = self.toServerURL(url); | ||
if(self._added.indexOf(url) === -1) { | ||
self._added.push(url); | ||
} | ||
}); | ||
return self.isDirty(); | ||
}; | ||
FileCache.prototype.remove = function remove(urls,returnPromises){ | ||
if(!urls) urls = []; | ||
var promises = []; | ||
if(typeof urls === 'string') urls = [urls]; | ||
var self = this; | ||
urls.forEach(function(url){ | ||
var index = self._added.indexOf(self.toServerURL(url)); | ||
if(index >= 0) self._added.splice(index,1); | ||
var path = self.toPath(url); | ||
promises.push(self._fs.remove(path)); | ||
delete self._cached[path]; | ||
}); | ||
return returnPromises? Promise.all(promises): self.isDirty(); | ||
}; | ||
FileCache.prototype.getDownloadQueue = function(){ | ||
var self = this; | ||
var queue = self._added.filter(function(url){ | ||
return !self.isCached(url); | ||
}); | ||
return queue; | ||
}; | ||
FileCache.prototype.getAdded = function() { | ||
return this._added; | ||
}; | ||
FileCache.prototype.isDirty = function isDirty(){ | ||
return this.getDownloadQueue().length > 0; | ||
}; | ||
FileCache.prototype.download = function download(onprogress){ | ||
var fs = this._fs; | ||
var self = this; | ||
self.abort(); | ||
return new Promise(function(resolve,reject){ | ||
// make sure cache directory exists and that | ||
// we have retrieved the latest cache contents | ||
// to avoid downloading files we already have! | ||
fs.ensure(self.localRoot).then(function(){ | ||
return self.list(); | ||
}).then(function(){ | ||
// no dowloads needed, resolve | ||
if(!self.isDirty()) { | ||
resolve(self); | ||
return; | ||
} | ||
// keep track of number of downloads! | ||
var queue = self.getDownloadQueue(); | ||
var started = []; | ||
var index = self._downloading.length; | ||
var done = self._downloading.length; | ||
var total = self._downloading.length + queue.length; | ||
// download every file in the queue (which is the diff from _added with _cached) | ||
queue.forEach(function(url){ | ||
var path = self.toPath(url); | ||
// augment progress event with index/total stats | ||
var onSingleDownloadProgress; | ||
if(typeof onprogress === 'function') { | ||
onSingleDownloadProgress = function(ev){ | ||
ev.queueIndex = index; | ||
ev.queueSize = total; | ||
ev.url = url; | ||
ev.path = path; | ||
ev.percentage = index / total; | ||
if(ev.loaded > 0 && ev.total > 0 && index !== total){ | ||
ev.percentage += (ev.loaded / ev.total) / total; | ||
} | ||
if(started.indexOf(url) < 0) { | ||
started.push(url); | ||
index++; | ||
} | ||
onprogress(ev); | ||
}; | ||
} | ||
// callback | ||
var onDone = function(){ | ||
done++; | ||
// when we're done | ||
if(done === total) { | ||
// reset downloads | ||
self._downloading = []; | ||
// check if we got everything | ||
self.list().then(function(){ | ||
// final progress event! | ||
if(onSingleDownloadProgress) onSingleDownloadProgress(new ProgressEvent()); | ||
// Yes, we're not dirty anymore! | ||
if(!self.isDirty()) { | ||
resolve(self); | ||
// Aye, some files got left behind! | ||
} else { | ||
reject(self.getDownloadQueue()); | ||
} | ||
},reject); | ||
} | ||
}; | ||
var downloadUrl = url; | ||
if(self._cacheBuster) downloadUrl += "?"+Date.now(); | ||
var download = fs.download(downloadUrl,path,{retry:self._retry},onSingleDownloadProgress); | ||
download.then(onDone,onDone); | ||
self._downloading.push(download); | ||
}); | ||
},reject); | ||
}); | ||
}; | ||
FileCache.prototype.abort = function abort(){ | ||
this._downloading.forEach(function(download){ | ||
download.abort(); | ||
}); | ||
this._downloading = []; | ||
}; | ||
FileCache.prototype.isCached = function isCached(url){ | ||
url = this.toPath(url); | ||
return !!this._cached[url]; | ||
}; | ||
FileCache.prototype.clear = function clear(){ | ||
var self = this; | ||
this._cached = {}; | ||
return this._fs.removeDir(this.localRoot).then(function(){ | ||
return self._fs.ensure(self.localRoot); | ||
}); | ||
}; | ||
/** | ||
* Helpers to output to various formats | ||
*/ | ||
FileCache.prototype.toInternalURL = function toInternalURL(url){ | ||
path = this.toPath(url); | ||
if(this._cached[path]) return this._cached[path].toInternalURL; | ||
return url; | ||
}; | ||
FileCache.prototype.get = function get(url){ | ||
path = this.toPath(url); | ||
if(this._cached[path]) return this._cached[path].toInternalURL; | ||
return this.toServerURL(url); | ||
}; | ||
FileCache.prototype.toDataURL = function toDataURL(url){ | ||
return this._fs.toDataURL(this.toPath(url)); | ||
}; | ||
FileCache.prototype.toURL = function toURL(url){ | ||
path = this.toPath(url); | ||
return this._cached[path]? this._cached[path].toURL: url; | ||
}; | ||
FileCache.prototype.toServerURL = function toServerURL(path){ | ||
path = this._fs.normalize(path); | ||
return path.indexOf('://') < 0? this.serverRoot + path: path; | ||
}; | ||
/** | ||
* Helper to transform remote URL to a local path (for cordova-promise-fs) | ||
*/ | ||
FileCache.prototype.toPath = function toPath(url){ | ||
if(this._mirrorMode) { | ||
url = url = this._fs.normalize(url || ''); | ||
len = this.serverRoot.length; | ||
if(url.substr(0,len) !== this.serverRoot) { | ||
return this.localRoot + url; | ||
} else { | ||
return this.localRoot + url.substr(len); | ||
} | ||
} else { | ||
return this.localRoot + hash(url) + url.substr(url.lastIndexOf('.')); | ||
} | ||
}; | ||
module.exports = FileCache; | ||
/***/ }, | ||
/* 4 */ | ||
@@ -975,0 +1079,0 @@ /***/ function(module, exports, __webpack_require__) { |
@@ -1,1 +0,1 @@ | ||
!function(t){function e(o){if(n[o])return n[o].exports;var r=n[o]={exports:{},id:o,loaded:!1};return t[o].call(r.exports,r,r.exports,e),r.loaded=!0,r.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){window.CordovaAppLoader=n(1),window.CordovaFileCache=n(2),window.CordovaPromiseFS=n(3),window.Promise=n(4),window.setImmediate=window.setTimeout},function(t,e,n){function o(t){if(!t)throw new Error("CordovaAppLoader has no options!");if(!t.fs)throw new Error('CordovaAppLoader has no "fs" option (cordova-promise-fs)');if(!t.serverRoot)throw new Error('CordovaAppLoader has no "serverRoot" option.');if(!window.pegasus||!window.Manifest)throw new Error("CordovaAppLoader bootstrap.js is missing.");i=t.fs.Promise,this.manifest=window.Manifest,this.newManifest=null,this._lastUpdateFiles=localStorage.getItem("last_update_files"),t.serverRoot=t.serverRoot||"",t.serverRoot&&"/"!==t.serverRoot[t.serverRoot.length-1]&&(t.serverRoot+="/"),this.newManifestUrl=t.serverRoot+(t.manifest||"manifest.json"),t.mode&&(t.mode="mirror"),this.cache=new r(t),this.corruptNewManifest=!1,this._toBeDeleted=[],this._toBeDownloaded=[],this._updateReady=!1,this._checkTimeout=t.checkTimeout||1e4}var r=n(2),i=null;o.prototype._createFilemap=function(t){var e={},n=this.cache._fs.normalize;return Object.keys(t).forEach(function(o){t[o].filename=n(t[o].filename),e[t[o].filename]=t[o]}),e},o.prototype.check=function(t){var e=this,n=this.manifest;return new i(function(o,r){function i(t){return JSON.stringify(t.files)===e._lastUpdateFiles?(JSON.stringify(t.files)!==JSON.stringify(Manifest.files)&&(console.warn("New manifest available, but an earlier update attempt failed. Will not download."),e.corruptNewManifest=!0,o(null)),void o(!1)):void e.cache.ready.then(function(){if(!t.files)return void r('Downloaded Manifest has no "files" attribute.');var i=e._createFilemap(t.files),a=e._createFilemap(n.files);e._toBeDownloaded=Object.keys(i).filter(function(t){return!a[t]||a[t].version!==i[t].version||!e.cache.isCached(t)}),e.cache.list().then(function(n){e._toBeDeleted=n.map(function(t){return t.substr(e.cache.localRoot.length)}).filter(function(t){return!i[t]}).concat(e._toBeDownloaded),e._toBeDeleted.length>0||e._toBeDownloaded.length>0?(e.newManifest=t,e.newManifest.root=e.cache.localInternalURL,o(!0)):o(!1)},r)},r)}"string"==typeof t&&(e.newManifestUrl=t,t=void 0),"object"==typeof t?i(t):(pegasus(e.newManifestUrl).then(i,r),setTimeout(function(){r(new Error("timeout"))},e._checkTimeout))})},o.prototype.canDownload=function(){return!!this.newManifest&&!this._updateReady},o.prototype.canUpdate=function(){return this._updateReady},o.prototype.download=function(t){var e=this;return e.canDownload()?(localStorage.removeItem("manifest"),localStorage.setItem("last_update_files",JSON.stringify(this.newManifest.files)),this.manifest.files=Manifest.files={},e.cache.remove(e._toBeDeleted,!0).then(function(){return e.cache.add(e._toBeDownloaded),e.cache.download(t)}).then(function(){return e._toBeDeleted=[],e._toBeDownloaded=[],e._updateReady=!0,e.newManifest},function(t){return t&&t.length&&e.cache.remove(t),t})):i.resolve(null)},o.prototype.update=function(t){return this._updateReady?(localStorage.setItem("manifest",JSON.stringify(this.newManifest)),t!==!1&&location.reload(),!0):!1},o.prototype.clear=function(){return localStorage.removeItem("manifest"),this.cache.clear()},o.prototype.reset=function(){return this.clear().then(function(){location.reload()},function(){location.reload()})},t.exports=o},function(t,e,n){function o(t){var e=this;if(this._fs=t.fs,!this._fs)throw new Error('Missing required option "fs". Add an instance of cordova-promise-fs.');i=this._fs.Promise,this._mirrorMode="hash"!==t.mode,this._retry=t.retry||[500,1500,8e3],this._cacheBuster=!!t.cacheBuster,this.localRoot=this._fs.normalize(t.localRoot||"data"),this.serverRoot=this._fs.normalize(t.serverRoot||""),this._downloading=[],this._added=[],this._cached={},this.ready=this._fs.ensure(this.localRoot).then(function(t){return e.localInternalURL=a?t.toInternalURL():t.toURL(),e.localUrl=t.toURL(),e.list()})}var r=n(5),i=null,a="undefined"!=typeof cordova;o.prototype.list=function(){var t=this;return new i(function(e){t._fs.list(t.localRoot,"rfe").then(function(n){t._cached={},n=n.map(function(e){var n=t._fs.normalize(e.fullPath);return t._cached[n]={toInternalURL:a?e.toInternalURL():e.toURL(),toURL:e.toURL()},n}),e(n)},function(){e([])})})},o.prototype.add=function(t){t||(t=[]),"string"==typeof t&&(t=[t]);var e=this;return t.forEach(function(t){t=e.toServerURL(t),-1===e._added.indexOf(t)&&e._added.push(t)}),e.isDirty()},o.prototype.remove=function(t,e){t||(t=[]);var n=[];"string"==typeof t&&(t=[t]);var o=this;return t.forEach(function(t){var e=o._added.indexOf(o.toServerURL(t));e>=0&&o._added.splice(e,1);var r=o.toPath(t);n.push(o._fs.remove(r)),delete o._cached[r]}),e?i.all(n):o.isDirty()},o.prototype.getDownloadQueue=function(){var t=this,e=t._added.filter(function(e){return!t.isCached(e)});return e},o.prototype.getAdded=function(){return this._added},o.prototype.isDirty=function(){return this.getDownloadQueue().length>0},o.prototype.download=function(t){var e=this._fs,n=this;return n.abort(),new i(function(o,r){e.ensure(n.localRoot).then(function(){return n.list()}).then(function(){if(!n.isDirty())return void o(n);var i=n.getDownloadQueue(),a=[],s=n._downloading.length,u=n._downloading.length,c=n._downloading.length+i.length;i.forEach(function(i){var f,l=n.toPath(i);"function"==typeof t&&(f=function(e){e.queueIndex=s,e.queueSize=c,e.url=i,e.path=l,e.percentage=s/c,e.loaded>0&&e.total>0&&s!==c&&(e.percentage+=e.loaded/e.total/c),a.indexOf(i)<0&&(a.push(i),s++),t(e)});var h=function(){u++,u===c&&(n._downloading=[],n.list().then(function(){f&&f(new ProgressEvent),n.isDirty()?r(n.getDownloadQueue()):o(n)},r))},d=i;n._cacheBuster&&(d+="?"+Date.now());var p=e.download(i,l,{retry:n._retry},f);p.then(h,h),n._downloading.push(p)})},r)})},o.prototype.abort=function(){this._downloading.forEach(function(t){t.abort()}),this._downloading=[]},o.prototype.isCached=function(t){return t=this.toPath(t),!!this._cached[t]},o.prototype.clear=function(){var t=this;return this._cached={},this._fs.removeDir(this.localRoot).then(function(){return t._fs.ensure(t.localRoot)})},o.prototype.toInternalURL=function(t){return path=this.toPath(t),this._cached[path]?this._cached[path].toInternalURL:t},o.prototype.get=function(t){return path=this.toPath(t),this._cached[path]?this._cached[path].toInternalURL:this.toServerURL(t)},o.prototype.toDataURL=function(t){return this._fs.toDataURL(this.toPath(t))},o.prototype.toURL=function(t){return path=this.toPath(t),this._cached[path]?this._cached[path].toURL:t},o.prototype.toServerURL=function(t){return t=this._fs.normalize(t),t.indexOf("://")<0?this.serverRoot+t:t},o.prototype.toPath=function(t){return this._mirrorMode?(t=t=this._fs.normalize(t||""),len=this.serverRoot.length,t.substr(0,len)!==this.serverRoot?this.localRoot+t:this.localRoot+t.substr(len)):this.localRoot+r(t)+t.substr(t.lastIndexOf("."))},t.exports=o},function(t){function e(t,n,o,r){t.getDirectory(n[0],{create:!0},function(t){n.length>1?e(t,n.slice(1),o,r):o(t)},r)}function n(t){return t=t.substr(0,t.lastIndexOf("/")+1),"/"===t[0]&&(t=t.substr(1)),t}function o(t){return t.substr(t.lastIndexOf("/")+1)}function r(t){return"/"===t[0]&&(t=t.substr(1)),t&&t.indexOf(".")<0&&"/"!==t[t.length-1]&&(t+="/"),"./"===t&&(t=""),t}var i=[],a=0;t.exports=function(t){function s(t){return new E(function(e){return e(t)})}function u(t){return new E(function(n,o){return C.then(function(r){t?(t=t.split("/").filter(function(t){return t&&t.length>0&&"."!==t[0]}),e(r.root,t,n,o)):n(r.root)},o)})}function c(t,e){return t=r(t),e=e||{},new E(function(n,o){return C.then(function(r){r.root.getFile(t,e,n,o)},o)})}function f(t,e){return t=r(t),e=e||{},new E(function(n,o){return C.then(function(r){t&&"/"!==t?r.root.getDirectory(t,e,n,o):n(r.root)},o)})}function l(t,e){e=e||"";var n=e.indexOf("r")>-1,o=e.indexOf("e")>-1,r=e.indexOf("f")>-1,i=e.indexOf("d")>-1;return r&&i&&(r=!1,i=!1),new E(function(e,a){return f(t).then(function(t){var u=t.createReader();u.readEntries(function(t){var u=[s(t)];n&&t.filter(function(t){return t.isDirectory}).forEach(function(t){u.push(l(t.fullPath,"re"))}),E.all(u).then(function(t){var n=[];n=n.concat.apply(n,t),r&&(n=n.filter(function(t){return t.isFile})),i&&(n=n.filter(function(t){return t.isDirectory})),o||(n=n.map(function(t){return t.fullPath})),e(n)},a)},a)},a)})}function h(t){return new E(function(e,n){c(t).then(function(t){e(t)},function(t){1===t.code?e(!1):n(t)})})}function d(t){return u(n(t)).then(function(){return c(t,{create:!0})})}function p(t){return c(t).then(function(t){return t.toURL()})}function w(t,e){return e=e||"readAsText",c(t).then(function(t){return new E(function(n,o){t.file(function(t){var o=new FileReader;o.onloadend=function(){n(this.result)},o[e](t)},o)})})}function y(t){return w(t,"readAsDataURL")}function v(t){return w(t).then(JSON.parse)}function m(t,e,o){return u(n(t)).then(function(){return c(t,{create:!0})}).then(function(t){return new E(function(n,r){t.createWriter(function(t){t.onwriteend=n,t.onerror=r,"string"==typeof e?e=new Blob([e],{type:o||"text/plain"}):e instanceof Blob!=!0&&(e=new Blob([JSON.stringify(e,null,4)],{type:o||"application/json"})),t.write(e)},r)})})}function _(t,e){return u(n(e)).then(function(n){return c(t).then(function(t){return new E(function(r,i){t.moveTo(n,o(e),r,i)})})})}function g(t,e){return u(n(e)).then(function(n){return c(t).then(function(t){return new E(function(r,i){t.copyTo(n,o(e),r,i)})})})}function R(t,e){var n=e?c:h;return new E(function(e,o){n(t).then(function(t){t!==!1?t.remove(e,o):e(1)},o)}).then(function(t){return 1===t?!1:!0})}function U(t){return f(t).then(function(t){return new E(function(e,n){t.removeRecursively(e,n)})})}function L(){for(;i.length>0&&a<t.concurrency;){a++;var e=i.pop(),n=e.shift(),o=e.shift(),r=e.shift(),s=e.shift(),u=e.shift(),c=e.shift(),f=e.shift(),l=e.shift();n._aborted?a--:o?(n.download.call(n,r,s,u,c,f,l),n.onprogress&&n.onprogress(new ProgressEvent)):n.upload.call(n,s,r,u,c,l,f)}}function D(t){return a--,L(),t}function S(e,n,o,r,s){"function"==typeof r&&(s=r,r={}),n=encodeURI(n),P&&(o=F(o)),r=r||{},r.retry&&r.retry.length||(r.retry=t.retry),r.retry=r.retry.concat();var u=new FileTransfer;s=s||r.onprogress,"function"==typeof s&&(u.onprogress=s);var c=new E(function(t,s){var c=function(a){if(0===r.retry.length)s(a);else{i.unshift([u,e,n,o,t,c,r.trustAllHosts||!1,r]);var f=r.retry.shift();f>0?setTimeout(D,f):D()}};r.retry.unshift(0),a++,c()});return c.then(D,D),c.progress=function(t){return u.onprogress=t,c},c.abort=function(){return u._aborted=!0,u.abort(),c},c}function b(t,e,n,o){return S(!0,t,e,n,o)}function x(t,e,n,o){return S(!1,e,t,n,o)}var E=t.Promise||window.Promise;if(!E)throw new Error("No Promise library given in options.Promise");this.options=t=t||{},t.persistent=void 0!==t.persistent?t.persistent:!0,t.storageSize=t.storageSize||20971520,t.concurrency=t.concurrency||3,t.retry=t.retry||[];var I,P="undefined"!=typeof cordova;P?I=new E(function(t,e){document.addEventListener("deviceready",t,!1),setTimeout(function(){e(new Error("deviceready has not fired after 5 seconds."))},5100)}):(I=s(!0),"undefined"!=typeof webkitRequestFileSystem?(window.requestFileSystem=webkitRequestFileSystem,window.FileTransfer=function(){},FileTransfer.prototype.download=function(t,e,n,o){var r=new XMLHttpRequest;return r.open("GET",t),r.responseType="blob",r.onreadystatechange=function(){4==r.readyState&&(200===r.status?m(e,r.response).then(n,o):o(r.status))},r.send(),r},window.ProgressEvent=function(){}):window.requestFileSystem=function(t,e,n,o){o(new Error("requestFileSystem not supported!"))});var C=new E(function(e,n){I.then(function(){window.requestFileSystem(t.persistent?1:0,t.storageSize,e,n),setTimeout(function(){n(new Error("Could not retrieve FileSystem after 5 seconds."))},5100)},n)});C.then(function(t){window.__fs=t},function(t){console.error("Could not get Cordova FileSystem:",t)});var O,F;return P?(F=function(e){return e=r(e),"cdvfile://localhost/"+(t.persistent?"persistent/":"temporary/")+e},O=function(t){return c(t).then(function(t){return t.toInternalURL()})}):(F=function(e){return e=r(e),"filesystem:"+location.origin+(t.persistent?"/persistent/":"/temporary/")+e},O=function(t){return c(t).then(function(t){return t.toURL()})}),{fs:C,normalize:r,file:c,filename:o,dir:f,dirname:n,create:d,read:w,readJSON:v,write:m,move:_,copy:g,remove:R,removeDir:U,list:l,ensure:u,exists:h,download:b,upload:x,toURL:p,isCordova:P,toInternalURLSync:F,toInternalURL:O,toDataURL:y,deviceready:I,options:t,Promise:E}}},function(t){!function(e,n){function o(t,e){return(typeof e)[0]==t}function r(t,a){return a=function s(u,c,f,l,h,d){function p(t){return function(e){h&&(h=0,s(o,t,e))}}if(l=s.q,u!=o)return r(function(t,e){l.push({p:this,r:t,j:e,1:u,0:c})});if(f&&o(e,f)|o(n,f))try{h=f.then}catch(t){c=0,f=t}if(o(e,h))try{h.call(f,p(1),c=p(0))}catch(t){c(t)}else for(a=function(n,a){return o(e,n=c?n:a)?r(function(t,e){i(this,t,e,f,n)}):t},d=0;d<l.length;)h=l[d++],o(e,u=h[c])?i(h.p,h.r,h.j,f,u):(c?h.r:h.j)(f)},a.q=[],t.call(t={then:function(t,e){return a(t,e)},catch:function(t){return a(0,t)}},function(t){a(o,1,t)},function(t){a(o,0,t)}),t}function i(t,r,i,a,s){setImmediate(function(){try{a=s(a),s=a&&o(n,a)|o(e,a)&&a.then,o(e,s)?a==t?i(TypeError()):s.call(a,r,i):r(a)}catch(t){i(t)}})}function a(t){return r(function(e){e(t)})}t.exports=r,r.resolve=a,r.reject=function(t){return r(function(e,n){n(t)})},r.all=function(t){return r(function(e,n,o,r){r=[],o=t.length||e(r),t.map(function(t,i){a(t).then(function(t){r[i]=t,--o||e(r)},n)})})}}("f","o")},function(t){function e(t,e){var n,o,r,i,a,s,u,c;for(n=3&t.length,o=t.length-n,r=e,a=3432918353,s=461845907,c=0;o>c;)u=255&t.charCodeAt(c)|(255&t.charCodeAt(++c))<<8|(255&t.charCodeAt(++c))<<16|(255&t.charCodeAt(++c))<<24,++c,u=(65535&u)*a+(((u>>>16)*a&65535)<<16)&4294967295,u=u<<15|u>>>17,u=(65535&u)*s+(((u>>>16)*s&65535)<<16)&4294967295,r^=u,r=r<<13|r>>>19,i=5*(65535&r)+((5*(r>>>16)&65535)<<16)&4294967295,r=(65535&i)+27492+(((i>>>16)+58964&65535)<<16);switch(u=0,n){case 3:u^=(255&t.charCodeAt(c+2))<<16;case 2:u^=(255&t.charCodeAt(c+1))<<8;case 1:u^=255&t.charCodeAt(c),u=(65535&u)*a+(((u>>>16)*a&65535)<<16)&4294967295,u=u<<15|u>>>17,u=(65535&u)*s+(((u>>>16)*s&65535)<<16)&4294967295,r^=u}return r^=t.length,r^=r>>>16,r=2246822507*(65535&r)+((2246822507*(r>>>16)&65535)<<16)&4294967295,r^=r>>>13,r=3266489909*(65535&r)+((3266489909*(r>>>16)&65535)<<16)&4294967295,r^=r>>>16,r>>>0}t.exports=e}]); | ||
!function(t){function e(o){if(n[o])return n[o].exports;var r=n[o]={exports:{},id:o,loaded:!1};return t[o].call(r.exports,r,r.exports,e),r.loaded=!0,r.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){window.CordovaAppLoader=n(1),window.CordovaFileCache=n(3),window.CordovaPromiseFS=n(2),window.Promise=n(4),window.setImmediate=window.setTimeout},function(t,e,n){function o(t){if(!t)throw new Error("CordovaAppLoader has no options!");if(!t.fs)throw new Error('CordovaAppLoader has no "fs" option (cordova-promise-fs)');if(!t.serverRoot)throw new Error('CordovaAppLoader has no "serverRoot" option.');if(!window.pegasus||!window.Manifest)throw new Error("CordovaAppLoader bootstrap.js is missing.");this.allowServerRootFromManifest=t.allowServerRootFromManifest===!0,a=t.fs.Promise,this.manifest=window.Manifest,this.newManifest=null,this.bundledManifest=null,this.bundledFS=null,this._lastUpdateFiles=localStorage.getItem("last_update_files"),t.serverRoot=t.serverRoot||"",t.serverRoot&&"/"!==t.serverRoot[t.serverRoot.length-1]&&(t.serverRoot+="/"),this.newManifestUrl=t.manifestUrl||t.serverRoot+(t.manifest||"manifest.json"),t.mode&&(t.mode="mirror"),this.cache=new r(t),this.corruptNewManifest=!1,this._toBeCopied=[],this._toBeDeleted=[],this._toBeDownloaded=[],this._updateReady=!1,this._checkTimeout=t.checkTimeout||1e4}var r=n(3),i=n(2),a=null,s=8,u="www/";o.prototype._createFilemap=function(t){var e={},n=this.cache._fs.normalize;return Object.keys(t).forEach(function(o){t[o].filename=n(t[o].filename),e[t[o].filename]=t[o]}),e},o.prototype.getBundledManifest=function(){var t=this,e=document.querySelector("script[manifest]"),n=(e?e.getAttribute("manifest"):null)||"manifest.json";return new a(function(e,o){t.bundledManifest?e(t.bundledManifest):(pegasus(n).then(function(n){t.bundledManifest=n,e(n)},o),setTimeout(function(){o(new Error("bundled manifest timeout"))},t._checkTimeout))})},o.prototype.getBundledFS=function(){return self.bundledFS||(self.bundledFS=i({fileSystem:s})),self.bundledFS},o.prototype.copyFromBundle=function(t){var e=this;return new a(function(n,o){var r=e.getBundledFS(),i=e.cache._fs,s=i.dirname(e.cache.localRoot+t),c=u+t,f=i.filename(t);return a.all([r.file(c),i.ensure(s)]).then(function(t){var e=t[0],r=t[1];e.copyTo(r,f,n,o)},o)})},o.prototype.check=function(t){var e=this,n=this.manifest;"string"==typeof t&&(e.newManifestUrl=t,t=void 0);var o=new a(function(n,o){"object"==typeof t?n(t):(console.log("checking new manifest:",e.newManifestUrl),pegasus(e.newManifestUrl).then(n,o),setTimeout(function(){o(new Error("new manifest timeout"))},e._checkTimeout))});return new a(function(t,r){a.all([o,e.getBundledManifest(),e.cache.list()]).then(function(o){var i=o[0],a=o[1];if(JSON.stringify(i.files)===e._lastUpdateFiles)return JSON.stringify(i.files)!==JSON.stringify(Manifest.files)&&(console.warn("New manifest available, but an earlier update attempt failed. Will not download."),e.corruptNewManifest=!0,t(null)),void t(!1);if(!i.files)return void r('Downloaded Manifest has no "files" attribute.');var s=o[2],u=e._createFilemap(n.files),c=e._createFilemap(i.files),f=e._createFilemap(a.files);e._toBeDownloaded=[],e._toBeCopied=[],e._toBeDeleted=[];var l=e.cache._fs.isCordova;Object.keys(c).filter(function(t){return!u[t]||u[t].version!==c[t].version||!e.cache.isCached(t)}).forEach(function(t){l&&f[t]&&f[t].version===c[t].version?e._toBeCopied.push(t):e._toBeDownloaded.push(t)}),e._toBeDeleted=s.map(function(t){return t.substr(e.cache.localRoot.length)}).filter(function(t){return!c[t]||e._toBeDownloaded.indexOf(t)>=0||e._toBeCopied.indexOf(t)>=0});var d=e._toBeDeleted.length+e._toBeDownloaded.length;d>0?(e.newManifest=i,e.newManifest.root=e.cache.localInternalURL,t(!0)):t(!1)})})},o.prototype.canDownload=function(){return!!this.newManifest&&!this._updateReady},o.prototype.canUpdate=function(){return this._updateReady},o.prototype.download=function(t){var e=this;return e.canDownload()?(localStorage.removeItem("manifest"),localStorage.setItem("last_update_files",JSON.stringify(this.newManifest.files)),this.manifest.files=Manifest.files={},e.cache.remove(e._toBeDeleted,!0).then(function(){return a.all(e._toBeCopied.map(e.copyFromBundle.bind(e)))}).then(function(){return e.allowServerRootFromManifest&&e.newManifest.serverRoot&&(e.cache.serverRoot=e.newManifest.serverRoot),e.cache.add(e._toBeDownloaded),e.cache.download(t)}).then(function(){return e._toBeDeleted=[],e._toBeDownloaded=[],e._updateReady=!0,e.newManifest},function(t){return t&&t.length&&e.cache.remove(t),t})):a.resolve(null)},o.prototype.update=function(t){return this._updateReady?(localStorage.setItem("manifest",JSON.stringify(this.newManifest)),t!==!1&&location.reload(),!0):!1},o.prototype.clear=function(){return localStorage.removeItem("last_update_files"),localStorage.removeItem("manifest"),this.cache.clear()},o.prototype.reset=function(){return this.clear().then(function(){location.reload()},function(){location.reload()})},t.exports=o},function(t){function e(t,n,o,r){t.getDirectory(n[0],{create:!0},function(t){n.length>1?e(t,n.slice(1),o,r):o(t)},r)}function n(t){return t=t.substr(0,t.lastIndexOf("/")+1),"/"===t[0]&&(t=t.substr(1)),t}function o(t){return t.substr(t.lastIndexOf("/")+1)}function r(t){return t=t||"","/"===t[0]&&(t=t.substr(1)),t&&t.indexOf(".")<0&&"/"!==t[t.length-1]&&(t+="/"),"./"===t&&(t=""),t}var i=[],a=0;t.exports=function(t){function s(t){return new M(function(e){return e(t)})}function u(t){return new M(function(n,o){return x.then(function(r){t?(t=t.split("/").filter(function(t){return t&&t.length>0&&"."!==t[0]}),e(r.root,t,n,o)):n(r.root)},o)})}function c(t,e){return new M(function(n,o){return t instanceof FileEntry?n(t):(t=r(t),e=e||{},x.then(function(r){r.root.getFile(t,e,n,o)},o))})}function f(t,e){return t=r(t),e=e||{},new M(function(n,o){return x.then(function(r){t&&"/"!==t?r.root.getDirectory(t,e,n,o):n(r.root)},o)})}function l(t,e){e=e||"";var n=e.indexOf("r")>-1,o=e.indexOf("e")>-1,r=e.indexOf("f")>-1,i=e.indexOf("d")>-1;return r&&i&&(r=!1,i=!1),new M(function(e,a){return f(t).then(function(t){var u=t.createReader();u.readEntries(function(t){var u=[s(t)];n&&t.filter(function(t){return t.isDirectory}).forEach(function(t){u.push(l(t.fullPath,"re"))}),M.all(u).then(function(t){var n=[];n=n.concat.apply(n,t),r&&(n=n.filter(function(t){return t.isFile})),i&&(n=n.filter(function(t){return t.isDirectory})),o||(n=n.map(function(t){return t.fullPath})),e(n)},a)},a)},a)})}function d(t){return new M(function(e,n){c(t).then(function(t){e(t)},function(t){1===t.code?e(!1):n(t)})})}function h(t){return u(n(t)).then(function(){return c(t,{create:!0})})}function p(t){return c(t).then(function(t){return t.toURL()})}function w(t,e){return e=e||"readAsText",c(t).then(function(t){return new M(function(n,o){t.file(function(t){var o=new FileReader;o.onloadend=function(){n(this.result)},o[e](t)},o)})})}function v(t){return w(t,"readAsDataURL")}function y(t){return w(t).then(JSON.parse)}function m(t,e,o){return u(n(t)).then(function(){return c(t,{create:!0})}).then(function(t){return new M(function(n,r){t.createWriter(function(t){t.onwriteend=n,t.onerror=r,"string"==typeof e?e=new Blob([e],{type:o||"text/plain"}):e instanceof Blob!=!0&&(e=new Blob([JSON.stringify(e,null,4)],{type:o||"application/json"})),t.write(e)},r)})})}function _(t,e){return u(n(e)).then(function(n){return c(t).then(function(t){return new M(function(r,i){t.moveTo(n,o(e),r,i)})})})}function g(t,e){return u(n(e)).then(function(n){return c(t).then(function(t){return new M(function(r,i){t.copyTo(n,o(e),r,i)})})})}function R(t,e){var n=e?c:d;return new M(function(e,o){n(t).then(function(t){t!==!1?t.remove(e,o):e(1)},o)}).then(function(t){return 1===t?!1:!0})}function S(t){return f(t).then(function(t){return new M(function(e,n){t.removeRecursively(e,n)})})}function b(){for(;i.length>0&&a<t.concurrency;){a++;var e=i.pop(),n=e.shift(),o=e.shift(),r=e.shift(),s=e.shift(),u=e.shift(),c=e.shift(),f=e.shift(),l=e.shift();n._aborted?a--:o?(n.download.call(n,r,s,u,c,f,l),n.onprogress&&n.onprogress(new ProgressEvent)):n.upload.call(n,s,r,u,c,l,f)}}function U(t){return a--,b(),t}function D(e,n,o,r,s){"function"==typeof r&&(s=r,r={}),n=encodeURI(n),C&&(o=I(o)),r=r||{},r.retry&&r.retry.length||(r.retry=t.retry),r.retry=r.retry.concat();var u=new FileTransfer;s=s||r.onprogress,"function"==typeof s&&(u.onprogress=s);var c=new M(function(t,s){var c=function(a){if(0===r.retry.length)s(a);else{i.unshift([u,e,n,o,t,c,r.trustAllHosts||!1,r]);var f=r.retry.shift();f>0?setTimeout(U,f):U()}};r.retry.unshift(0),a++,c()});return c.then(U,U),c.progress=function(t){return u.onprogress=t,c},c.abort=function(){return u._aborted=!0,u.abort(),c},c}function L(t,e,n,o){return D(!0,t,e,n,o)}function F(t,e,n,o){return D(!1,e,t,n,o)}var M=t.Promise||window.Promise;if(!M)throw new Error("No Promise library given in options.Promise");this.options=t=t||{},t.persistent=void 0!==t.persistent?t.persistent:!0,t.storageSize=t.storageSize||20971520,t.concurrency=t.concurrency||3,t.retry=t.retry||[];var B,C="undefined"!=typeof cordova;C?B=new M(function(t,e){document.addEventListener("deviceready",t,!1),setTimeout(function(){e(new Error("deviceready has not fired after 5 seconds."))},5100)}):(B=s(!0),"undefined"!=typeof webkitRequestFileSystem?(window.requestFileSystem=webkitRequestFileSystem,window.FileTransfer=function(){},FileTransfer.prototype.download=function(t,e,n,o){var r=new XMLHttpRequest;return r.open("GET",t),r.responseType="blob",r.onreadystatechange=function(){4==r.readyState&&(200===r.status?m(e,r.response).then(n,o):o(r.status))},r.send(),r},window.ProgressEvent=function(){}):window.requestFileSystem=function(t,e,n,o){o(new Error("requestFileSystem not supported!"))});var x=new M(function(e,n){B.then(function(){var o=t.persistent?1:0;"number"==typeof t.fileSystem&&(o=t.fileSystem),!C&&o>1&&(console.warn('Chrome does not support fileSystem "'+o+'". Falling back on "0" (temporary).'),o=0),window.requestFileSystem(o,t.storageSize,e,n),setTimeout(function(){n(new Error("Could not retrieve FileSystem after 5 seconds."))},5100)},n)});x.then(function(t){window.__fs=t},function(t){console.error("Could not get Cordova FileSystem:",t)});var E,I;return C?(I=function(e){return e=r(e),"cdvfile://localhost/"+(t.persistent?"persistent/":"temporary/")+e},E=function(t){return c(t).then(function(t){return t.toInternalURL()})}):(I=function(e){return e=r(e),"filesystem:"+location.origin+(t.persistent?"/persistent/":"/temporary/")+e},E=function(t){return c(t).then(function(t){return t.toURL()})}),{fs:x,normalize:r,file:c,filename:o,dir:f,dirname:n,create:h,read:w,readJSON:y,write:m,move:_,copy:g,remove:R,removeDir:S,list:l,ensure:u,exists:d,download:L,upload:F,toURL:p,isCordova:C,toInternalURLSync:I,toInternalURL:E,toDataURL:v,deviceready:B,options:t,Promise:M}}},function(t,e,n){function o(t){var e=this;if(this._fs=t.fs,!this._fs)throw new Error('Missing required option "fs". Add an instance of cordova-promise-fs.');i=this._fs.Promise,this._mirrorMode="hash"!==t.mode,this._retry=t.retry||[500,1500,8e3],this._cacheBuster=!!t.cacheBuster,this.localRoot=this._fs.normalize(t.localRoot||"data"),this.serverRoot=this._fs.normalize(t.serverRoot||""),this._downloading=[],this._added=[],this._cached={},this.ready=this._fs.ensure(this.localRoot).then(function(t){return e.localInternalURL=a?t.toInternalURL():t.toURL(),e.localUrl=t.toURL(),e.list()})}var r=n(5),i=null,a="undefined"!=typeof cordova;o.prototype.list=function(){var t=this;return new i(function(e){t._fs.list(t.localRoot,"rfe").then(function(n){t._cached={},n=n.map(function(e){var n=t._fs.normalize(e.fullPath);return t._cached[n]={toInternalURL:a?e.toInternalURL():e.toURL(),toURL:e.toURL()},n}),e(n)},function(){e([])})})},o.prototype.add=function(t){t||(t=[]),"string"==typeof t&&(t=[t]);var e=this;return t.forEach(function(t){t=e.toServerURL(t),-1===e._added.indexOf(t)&&e._added.push(t)}),e.isDirty()},o.prototype.remove=function(t,e){t||(t=[]);var n=[];"string"==typeof t&&(t=[t]);var o=this;return t.forEach(function(t){var e=o._added.indexOf(o.toServerURL(t));e>=0&&o._added.splice(e,1);var r=o.toPath(t);n.push(o._fs.remove(r)),delete o._cached[r]}),e?i.all(n):o.isDirty()},o.prototype.getDownloadQueue=function(){var t=this,e=t._added.filter(function(e){return!t.isCached(e)});return e},o.prototype.getAdded=function(){return this._added},o.prototype.isDirty=function(){return this.getDownloadQueue().length>0},o.prototype.download=function(t){var e=this._fs,n=this;return n.abort(),new i(function(o,r){e.ensure(n.localRoot).then(function(){return n.list()}).then(function(){if(!n.isDirty())return void o(n);var i=n.getDownloadQueue(),a=[],s=n._downloading.length,u=n._downloading.length,c=n._downloading.length+i.length;i.forEach(function(i){var f,l=n.toPath(i);"function"==typeof t&&(f=function(e){e.queueIndex=s,e.queueSize=c,e.url=i,e.path=l,e.percentage=s/c,e.loaded>0&&e.total>0&&s!==c&&(e.percentage+=e.loaded/e.total/c),a.indexOf(i)<0&&(a.push(i),s++),t(e)});var d=function(){u++,u===c&&(n._downloading=[],n.list().then(function(){f&&f(new ProgressEvent),n.isDirty()?r(n.getDownloadQueue()):o(n)},r))},h=i;n._cacheBuster&&(h+="?"+Date.now());var p=e.download(h,l,{retry:n._retry},f);p.then(d,d),n._downloading.push(p)})},r)})},o.prototype.abort=function(){this._downloading.forEach(function(t){t.abort()}),this._downloading=[]},o.prototype.isCached=function(t){return t=this.toPath(t),!!this._cached[t]},o.prototype.clear=function(){var t=this;return this._cached={},this._fs.removeDir(this.localRoot).then(function(){return t._fs.ensure(t.localRoot)})},o.prototype.toInternalURL=function(t){return path=this.toPath(t),this._cached[path]?this._cached[path].toInternalURL:t},o.prototype.get=function(t){return path=this.toPath(t),this._cached[path]?this._cached[path].toInternalURL:this.toServerURL(t)},o.prototype.toDataURL=function(t){return this._fs.toDataURL(this.toPath(t))},o.prototype.toURL=function(t){return path=this.toPath(t),this._cached[path]?this._cached[path].toURL:t},o.prototype.toServerURL=function(t){return t=this._fs.normalize(t),t.indexOf("://")<0?this.serverRoot+t:t},o.prototype.toPath=function(t){return this._mirrorMode?(t=t=this._fs.normalize(t||""),len=this.serverRoot.length,t.substr(0,len)!==this.serverRoot?this.localRoot+t:this.localRoot+t.substr(len)):this.localRoot+r(t)+t.substr(t.lastIndexOf("."))},t.exports=o},function(t){!function(e,n){function o(t,e){return(typeof e)[0]==t}function r(t,a){return a=function s(u,c,f,l,d,h){function p(t){return function(e){d&&(d=0,s(o,t,e))}}if(l=s.q,u!=o)return r(function(t,e){l.push({p:this,r:t,j:e,1:u,0:c})});if(f&&o(e,f)|o(n,f))try{d=f.then}catch(t){c=0,f=t}if(o(e,d))try{d.call(f,p(1),c=p(0))}catch(t){c(t)}else for(a=function(n,a){return o(e,n=c?n:a)?r(function(t,e){i(this,t,e,f,n)}):t},h=0;h<l.length;)d=l[h++],o(e,u=d[c])?i(d.p,d.r,d.j,f,u):(c?d.r:d.j)(f)},a.q=[],t.call(t={then:function(t,e){return a(t,e)},catch:function(t){return a(0,t)}},function(t){a(o,1,t)},function(t){a(o,0,t)}),t}function i(t,r,i,a,s){setImmediate(function(){try{a=s(a),s=a&&o(n,a)|o(e,a)&&a.then,o(e,s)?a==t?i(TypeError()):s.call(a,r,i):r(a)}catch(t){i(t)}})}function a(t){return r(function(e){e(t)})}t.exports=r,r.resolve=a,r.reject=function(t){return r(function(e,n){n(t)})},r.all=function(t){return r(function(e,n,o,r){r=[],o=t.length||e(r),t.map(function(t,i){a(t).then(function(t){r[i]=t,--o||e(r)},n)})})}}("f","o")},function(t){function e(t,e){var n,o,r,i,a,s,u,c;for(n=3&t.length,o=t.length-n,r=e,a=3432918353,s=461845907,c=0;o>c;)u=255&t.charCodeAt(c)|(255&t.charCodeAt(++c))<<8|(255&t.charCodeAt(++c))<<16|(255&t.charCodeAt(++c))<<24,++c,u=(65535&u)*a+(((u>>>16)*a&65535)<<16)&4294967295,u=u<<15|u>>>17,u=(65535&u)*s+(((u>>>16)*s&65535)<<16)&4294967295,r^=u,r=r<<13|r>>>19,i=5*(65535&r)+((5*(r>>>16)&65535)<<16)&4294967295,r=(65535&i)+27492+(((i>>>16)+58964&65535)<<16);switch(u=0,n){case 3:u^=(255&t.charCodeAt(c+2))<<16;case 2:u^=(255&t.charCodeAt(c+1))<<8;case 1:u^=255&t.charCodeAt(c),u=(65535&u)*a+(((u>>>16)*a&65535)<<16)&4294967295,u=u<<15|u>>>17,u=(65535&u)*s+(((u>>>16)*s&65535)<<16)&4294967295,r^=u}return r^=t.length,r^=r>>>16,r=2246822507*(65535&r)+((2246822507*(r>>>16)&65535)<<16)&4294967295,r^=r>>>13,r=3266489909*(65535&r)+((3266489909*(r>>>16)&65535)<<16)&4294967295,r^=r>>>16,r>>>0}t.exports=e}]); |
@@ -49,4 +49,8 @@ var CordovaAppLoader = | ||
var CordovaFileCache = __webpack_require__(1); | ||
var CordovaPromiseFS = __webpack_require__(2); | ||
var Promise = null; | ||
var BUNDLED_FILE_SYSTEM_TYPE = 8; | ||
var BUNDLE_ROOT = 'www/'; | ||
function AppLoader(options){ | ||
@@ -57,2 +61,3 @@ if(!options) throw new Error('CordovaAppLoader has no options!'); | ||
if(!window.pegasus || !window.Manifest) throw new Error('CordovaAppLoader bootstrap.js is missing.'); | ||
this.allowServerRootFromManifest = options.allowServerRootFromManifest === true; | ||
Promise = options.fs.Promise; | ||
@@ -63,2 +68,4 @@ | ||
this.newManifest = null; | ||
this.bundledManifest = null; | ||
this.bundledFS = null; | ||
this._lastUpdateFiles = localStorage.getItem('last_update_files'); | ||
@@ -69,3 +76,3 @@ | ||
if(!!options.serverRoot && options.serverRoot[options.serverRoot.length-1] !== '/') options.serverRoot += '/'; | ||
this.newManifestUrl = options.serverRoot + (options.manifest || 'manifest.json'); | ||
this.newManifestUrl = options.manifestUrl || options.serverRoot + (options.manifest || 'manifest.json'); | ||
@@ -78,2 +85,3 @@ // initialize a file cache | ||
this.corruptNewManifest = false; | ||
this._toBeCopied = []; | ||
this._toBeDeleted = []; | ||
@@ -95,24 +103,79 @@ this._toBeDownloaded = []; | ||
AppLoader.prototype.getBundledManifest = function(){ | ||
var self = this; | ||
var bootstrapScript = document.querySelector('script[manifest]'); | ||
var bundledManifestUrl = (bootstrapScript? bootstrapScript.getAttribute('manifest'): null) || 'manifest.json'; | ||
return new Promise(function(resolve,reject){ | ||
if(self.bundledManifest) { | ||
resolve(self.bundledManifest); | ||
} else { | ||
pegasus(bundledManifestUrl).then(function(bundledManifest){ | ||
self.bundledManifest = bundledManifest; | ||
resolve(bundledManifest); | ||
},reject); | ||
setTimeout(function(){reject(new Error('bundled manifest timeout'));},self._checkTimeout); | ||
} | ||
}); | ||
}; | ||
AppLoader.prototype.getBundledFS = function(){ | ||
if(!self.bundledFS){ | ||
self.bundledFS = CordovaPromiseFS({ fileSystem: BUNDLED_FILE_SYSTEM_TYPE }); | ||
} | ||
return self.bundledFS; | ||
}; | ||
AppLoader.prototype.copyFromBundle = function(file){ | ||
var self = this; | ||
return new Promise(function(resolve,reject){ | ||
var bundledFS = self.getBundledFS(); | ||
var cacheFS = self.cache._fs; | ||
var destDirPath = cacheFS.dirname(self.cache.localRoot + file); | ||
var srcFilePath = BUNDLE_ROOT + file; | ||
var srcFilename = cacheFS.filename(file); | ||
return Promise.all([bundledFS.file(srcFilePath),cacheFS.ensure(destDirPath)]) | ||
.then(function(val){ | ||
var srcFile = val[0], destDir = val[1]; | ||
srcFile.copyTo(destDir,srcFilename,resolve,reject); | ||
},reject); | ||
}); | ||
}; | ||
AppLoader.prototype.check = function(newManifest){ | ||
var self = this, manifest = this.manifest; | ||
if(typeof newManifest === "string") { | ||
self.newManifestUrl = newManifest; | ||
newManifest = undefined; | ||
} | ||
return new Promise(function(resolve,reject){ | ||
if(typeof newManifest === "string") { | ||
self.newManifestUrl = newManifest; | ||
newManifest = undefined; | ||
var gotNewManifest = new Promise(function(resolve,reject){ | ||
if(typeof newManifest === "object") { | ||
resolve(newManifest); | ||
} else { | ||
console.log('checking new manifest:',self.newManifestUrl); | ||
pegasus(self.newManifestUrl).then(resolve,reject); | ||
setTimeout(function(){reject(new Error('new manifest timeout'));},self._checkTimeout); | ||
} | ||
}); | ||
function checkManifest(newManifest){ | ||
if(JSON.stringify(newManifest.files) === self._lastUpdateFiles) { | ||
if(JSON.stringify(newManifest.files) !== JSON.stringify(Manifest.files)){ | ||
console.warn('New manifest available, but an earlier update attempt failed. Will not download.'); | ||
self.corruptNewManifest = true; | ||
resolve(null); | ||
return new Promise(function(resolve,reject){ | ||
Promise.all([gotNewManifest,self.getBundledManifest(),self.cache.list()]) | ||
.then(function(values){ | ||
var newManifest = values[0]; | ||
var bundledManifest = values[1]; | ||
// Prevent end-less update loop, check if new manifest | ||
// has been downloaded before (but failes) | ||
if(JSON.stringify(newManifest.files) === self._lastUpdateFiles) { | ||
if(JSON.stringify(newManifest.files) !== JSON.stringify(Manifest.files)){ | ||
console.warn('New manifest available, but an earlier update attempt failed. Will not download.'); | ||
self.corruptNewManifest = true; | ||
resolve(null); | ||
} | ||
resolve(false); | ||
return; | ||
} | ||
resolve(false); | ||
return; | ||
} | ||
// make sure cache is ready for the DIFF operations! | ||
self.cache.ready.then(function(list){ | ||
// Check if new manifest is valid | ||
if(!newManifest.files){ | ||
@@ -123,41 +186,62 @@ reject('Downloaded Manifest has no "files" attribute.'); | ||
var newFiles = self._createFilemap(newManifest.files); | ||
var oldFiles = self._createFilemap(manifest.files); | ||
// We're good to go check! Get all the files we need | ||
var cachedFiles = values[2]; // files in cache | ||
var oldFiles = self._createFilemap(manifest.files); // files in current manifest | ||
var newFiles = self._createFilemap(newManifest.files); // files in new manifest | ||
var bundledFiles = self._createFilemap(bundledManifest.files); // files in app bundle | ||
// Create the diff | ||
self._toBeDownloaded = Object.keys(newFiles) | ||
// Create COPY and DOWNLOAD lists | ||
self._toBeDownloaded = []; | ||
self._toBeCopied = []; | ||
self._toBeDeleted= []; | ||
var isCordova = self.cache._fs.isCordova; | ||
Object.keys(newFiles) | ||
// Find files that have changed version or are missing | ||
.filter(function(file){ | ||
return !oldFiles[file] | ||
|| oldFiles[file].version !== newFiles[file].version | ||
|| !self.cache.isCached(file); | ||
// if new file, or... | ||
return !oldFiles[file] || | ||
// version has changed, or... | ||
oldFiles[file].version !== newFiles[file].version || | ||
// not in cache for some reason | ||
!self.cache.isCached(file); | ||
}) | ||
// Add them to the correct list | ||
.forEach(function(file){ | ||
// bundled version matches new version, so we can copy! | ||
if(isCordova && bundledFiles[file] && bundledFiles[file].version === newFiles[file].version){ | ||
self._toBeCopied.push(file); | ||
// othwerwise, we must download | ||
} else { | ||
self._toBeDownloaded.push(file); | ||
} | ||
}); | ||
self.cache.list().then(function(files){ | ||
self._toBeDeleted = files | ||
.map(function(file){ | ||
return file.substr(self.cache.localRoot.length); | ||
}) | ||
.filter(function(file){ | ||
return !newFiles[file]; | ||
}) | ||
.concat(self._toBeDownloaded); | ||
// Delete files | ||
self._toBeDeleted = cachedFiles | ||
.map(function(file){ | ||
return file.substr(self.cache.localRoot.length); | ||
}) | ||
.filter(function(file){ | ||
// Everything that is not in new manifest, or.... | ||
return !newFiles[file] || | ||
// Files that will be downloaded, or... | ||
self._toBeDownloaded.indexOf(file) >= 0 || | ||
// Files that will be copied | ||
self._toBeCopied.indexOf(file) >= 0; | ||
}); | ||
if(self._toBeDeleted.length > 0 || self._toBeDownloaded.length > 0){ | ||
// Save the new Manifest | ||
self.newManifest = newManifest; | ||
self.newManifest.root = self.cache.localInternalURL; | ||
resolve(true); | ||
} else { | ||
resolve(false); | ||
} | ||
},reject); | ||
},reject); | ||
} | ||
if(typeof newManifest === "object") { | ||
checkManifest(newManifest); | ||
} else { | ||
pegasus(self.newManifestUrl).then(checkManifest,reject); | ||
setTimeout(function(){reject(new Error('timeout'));},self._checkTimeout); | ||
} | ||
}); | ||
var changes = self._toBeDeleted.length + self._toBeDownloaded.length; | ||
// Note: if we only need to copy files, we can keep serving from bundle! | ||
// So no update is needed! | ||
if(changes > 0){ | ||
// Save the new Manifest | ||
self.newManifest = newManifest; | ||
self.newManifest.root = self.cache.localInternalURL; | ||
resolve(true); | ||
} else { | ||
resolve(false); | ||
} | ||
}); // end of .then | ||
}); // end of new Promise | ||
}; | ||
@@ -185,2 +269,8 @@ | ||
.then(function(){ | ||
return Promise.all(self._toBeCopied.map(self.copyFromBundle.bind(self))); | ||
}) | ||
.then(function(){ | ||
if(self.allowServerRootFromManifest && self.newManifest.serverRoot){ | ||
self.cache.serverRoot = self.newManifest.serverRoot; | ||
} | ||
self.cache.add(self._toBeDownloaded); | ||
@@ -213,2 +303,3 @@ return self.cache.download(onprogress); | ||
AppLoader.prototype.clear = function(){ | ||
localStorage.removeItem('last_update_files'); | ||
localStorage.removeItem('manifest'); | ||
@@ -232,3 +323,3 @@ return this.cache.clear(); | ||
var hash = __webpack_require__(2); | ||
var hash = __webpack_require__(3); | ||
var Promise = null; | ||
@@ -411,3 +502,3 @@ var isCordova = typeof cordova !== 'undefined'; | ||
if(self._cacheBuster) downloadUrl += "?"+Date.now(); | ||
var download = fs.download(url,path,{retry:self._retry},onSingleDownloadProgress); | ||
var download = fs.download(downloadUrl,path,{retry:self._retry},onSingleDownloadProgress); | ||
download.then(onDone,onDone); | ||
@@ -493,2 +584,487 @@ self._downloading.push(download); | ||
/** | ||
* Static Private functions | ||
*/ | ||
/* createDir, recursively */ | ||
function __createDir(rootDirEntry, folders, success,error) { | ||
rootDirEntry.getDirectory(folders[0], {create: true}, function(dirEntry) { | ||
// Recursively add the new subfolder (if we still have another to create). | ||
if (folders.length > 1) { | ||
__createDir(dirEntry, folders.slice(1),success,error); | ||
} else { | ||
success(dirEntry); | ||
} | ||
}, error); | ||
} | ||
function dirname(str) { | ||
str = str.substr(0,str.lastIndexOf('/')+1); | ||
if(str[0] === '/') str = str.substr(1); | ||
return str; | ||
} | ||
function filename(str) { | ||
return str.substr(str.lastIndexOf('/')+1); | ||
} | ||
function normalize(str){ | ||
str = str || ''; | ||
if(str[0] === '/') str = str.substr(1); | ||
if(!!str && str.indexOf('.') < 0 && str[str.length-1] !== '/') str += '/'; | ||
if(str === './') str = ''; | ||
return str; | ||
} | ||
var transferQueue = [], // queued fileTransfers | ||
inprogress = 0; // currently active filetransfers | ||
/** | ||
* Factory function: Create a single instance (based on single FileSystem) | ||
*/ | ||
module.exports = function(options){ | ||
/* Promise implementation */ | ||
var Promise = options.Promise || window.Promise; | ||
if(!Promise) { throw new Error("No Promise library given in options.Promise"); } | ||
/* default options */ | ||
this.options = options = options || {}; | ||
options.persistent = options.persistent !== undefined? options.persistent: true; | ||
options.storageSize = options.storageSize || 20*1024*1024; | ||
options.concurrency = options.concurrency || 3; | ||
options.retry = options.retry || []; | ||
/* Cordova deviceready promise */ | ||
var deviceready, isCordova = typeof cordova !== 'undefined'; | ||
if(isCordova){ | ||
deviceready = new Promise(function(resolve,reject){ | ||
document.addEventListener("deviceready", resolve, false); | ||
setTimeout(function(){ reject(new Error('deviceready has not fired after 5 seconds.')); },5100); | ||
}); | ||
} else { | ||
/* FileTransfer implementation for Chrome */ | ||
deviceready = ResolvedPromise(true); | ||
if(typeof webkitRequestFileSystem !== 'undefined'){ | ||
window.requestFileSystem = webkitRequestFileSystem; | ||
window.FileTransfer = function FileTransfer(){}; | ||
FileTransfer.prototype.download = function download(url,file,win,fail) { | ||
var xhr = new XMLHttpRequest(); | ||
xhr.open('GET', url); | ||
xhr.responseType = "blob"; | ||
xhr.onreadystatechange = function(onSuccess, onError, cb) { | ||
if (xhr.readyState == 4) { | ||
if(xhr.status === 200){ | ||
write(file,xhr.response).then(win,fail); | ||
} else { | ||
fail(xhr.status); | ||
} | ||
} | ||
}; | ||
xhr.send(); | ||
return xhr; | ||
}; | ||
window.ProgressEvent = function ProgressEvent(){}; | ||
} else { | ||
window.requestFileSystem = function(x,y,z,fail){ | ||
fail(new Error('requestFileSystem not supported!')); | ||
}; | ||
} | ||
} | ||
/* Promise resolve helper */ | ||
function ResolvedPromise(value){ | ||
return new Promise(function(resolve){ | ||
return resolve(value); | ||
}); | ||
} | ||
/* the filesystem! */ | ||
var fs = new Promise(function(resolve,reject){ | ||
deviceready.then(function(){ | ||
var type = options.persistent? 1: 0; | ||
if(typeof options.fileSystem === 'number'){ | ||
type = options.fileSystem; | ||
} | ||
// Chrome only supports persistent and temp storage, not the exotic onces from Cordova | ||
if(!isCordova && type > 1) { | ||
console.warn('Chrome does not support fileSystem "'+type+'". Falling back on "0" (temporary).'); | ||
type = 0; | ||
} | ||
window.requestFileSystem(type, options.storageSize, resolve, reject); | ||
setTimeout(function(){ reject(new Error('Could not retrieve FileSystem after 5 seconds.')); },5100); | ||
},reject); | ||
}); | ||
/* debug */ | ||
fs.then(function(fs){ | ||
window.__fs = fs; | ||
},function(err){ | ||
console.error('Could not get Cordova FileSystem:',err); | ||
}); | ||
/* ensure directory exists */ | ||
function ensure(folders) { | ||
return new Promise(function(resolve,reject){ | ||
return fs.then(function(fs){ | ||
if(!folders) { | ||
resolve(fs.root); | ||
} else { | ||
folders = folders.split('/').filter(function(folder) { | ||
return folder && folder.length > 0 && folder[0] !== '.'; | ||
}); | ||
__createDir(fs.root,folders,resolve,reject); | ||
} | ||
},reject); | ||
}); | ||
} | ||
/* get file file */ | ||
function file(path,options){ | ||
return new Promise(function(resolve,reject){ | ||
if(path instanceof FileEntry) { | ||
return resolve(path); | ||
} | ||
path = normalize(path); | ||
options = options || {}; | ||
return fs.then(function(fs){ | ||
fs.root.getFile(path,options,resolve,reject); | ||
},reject); | ||
}); | ||
} | ||
/* get directory entry */ | ||
function dir(path,options){ | ||
path = normalize(path); | ||
options = options || {}; | ||
return new Promise(function(resolve,reject){ | ||
return fs.then(function(fs){ | ||
if(!path || path === '/') { | ||
resolve(fs.root); | ||
} else { | ||
fs.root.getDirectory(path,options,resolve,reject); | ||
} | ||
},reject); | ||
}); | ||
} | ||
/* list contents of a directory */ | ||
function list(path,mode) { | ||
mode = mode || ''; | ||
var recursive = mode.indexOf('r') > -1; | ||
var getAsEntries = mode.indexOf('e') > -1; | ||
var onlyFiles = mode.indexOf('f') > -1; | ||
var onlyDirs = mode.indexOf('d') > -1; | ||
if(onlyFiles && onlyDirs) { | ||
onlyFiles = false; | ||
onlyDirs = false; | ||
} | ||
return new Promise(function(resolve,reject){ | ||
return dir(path).then(function(dirEntry){ | ||
var dirReader = dirEntry.createReader(); | ||
dirReader.readEntries(function(entries) { | ||
var promises = [ResolvedPromise(entries)]; | ||
if(recursive) { | ||
entries | ||
.filter(function(entry){return entry.isDirectory; }) | ||
.forEach(function(entry){ | ||
promises.push(list(entry.fullPath,'re')); | ||
}); | ||
} | ||
Promise.all(promises).then(function(values){ | ||
var entries = []; | ||
entries = entries.concat.apply(entries,values); | ||
if(onlyFiles) entries = entries.filter(function(entry) { return entry.isFile; }); | ||
if(onlyDirs) entries = entries.filter(function(entry) { return entry.isDirectory; }); | ||
if(!getAsEntries) entries = entries.map(function(entry) { return entry.fullPath; }); | ||
resolve(entries); | ||
},reject); | ||
}, reject); | ||
},reject); | ||
}); | ||
} | ||
/* does file exist? If so, resolve with fileEntry, if not, resolve with false. */ | ||
function exists(path){ | ||
return new Promise(function(resolve,reject){ | ||
file(path).then( | ||
function(fileEntry){ | ||
resolve(fileEntry); | ||
}, | ||
function(err){ | ||
if(err.code === 1) { | ||
resolve(false); | ||
} else { | ||
reject(err); | ||
} | ||
} | ||
); | ||
}); | ||
} | ||
function create(path){ | ||
return ensure(dirname(path)).then(function(){ | ||
return file(path,{create:true}); | ||
}); | ||
} | ||
/* convert path to URL to be used in JS/CSS/HTML */ | ||
function toURL(path) { | ||
return file(path).then(function(fileEntry) { | ||
return fileEntry.toURL(); | ||
}); | ||
} | ||
/* convert path to URL to be used in JS/CSS/HTML */ | ||
var toInternalURL,toInternalURLSync; | ||
if(isCordova) { | ||
/* synchronous helper to get internal URL. */ | ||
toInternalURLSync = function(path){ | ||
path = normalize(path); | ||
return 'cdvfile://localhost/'+(options.persistent? 'persistent/':'temporary/') + path; | ||
}; | ||
toInternalURL = function(path) { | ||
return file(path).then(function(fileEntry) { | ||
return fileEntry.toInternalURL(); | ||
}); | ||
}; | ||
} else { | ||
/* synchronous helper to get internal URL. */ | ||
toInternalURLSync = function(path){ | ||
path = normalize(path); | ||
return 'filesystem:'+location.origin+(options.persistent? '/persistent/':'/temporary/') + path; | ||
}; | ||
toInternalURL = function(path) { | ||
return file(path).then(function(fileEntry) { | ||
return fileEntry.toURL(); | ||
}); | ||
}; | ||
} | ||
/* return contents of a file */ | ||
function read(path,method) { | ||
method = method || 'readAsText'; | ||
return file(path).then(function(fileEntry) { | ||
return new Promise(function(resolve,reject){ | ||
fileEntry.file(function(file){ | ||
var reader = new FileReader(); | ||
reader.onloadend = function(){ | ||
resolve(this.result); | ||
}; | ||
reader[method](file); | ||
},reject); | ||
}); | ||
}); | ||
} | ||
/* convert path to base64 date URI */ | ||
function toDataURL(path) { | ||
return read(path,'readAsDataURL'); | ||
} | ||
function readJSON(path){ | ||
return read(path).then(JSON.parse); | ||
} | ||
/* write contents to a file */ | ||
function write(path,blob,mimeType) { | ||
return ensure(dirname(path)) | ||
.then(function() { return file(path,{create:true}); }) | ||
.then(function(fileEntry) { | ||
return new Promise(function(resolve,reject){ | ||
fileEntry.createWriter(function(writer){ | ||
writer.onwriteend = resolve; | ||
writer.onerror = reject; | ||
if(typeof blob === 'string') { | ||
blob = new Blob([blob],{type: mimeType || 'text/plain'}); | ||
} else if(blob instanceof Blob !== true){ | ||
blob = new Blob([JSON.stringify(blob,null,4)],{type: mimeType || 'application/json'}); | ||
} | ||
writer.write(blob); | ||
},reject); | ||
}); | ||
}); | ||
} | ||
/* move a file */ | ||
function move(src,dest) { | ||
return ensure(dirname(dest)) | ||
.then(function(dir) { | ||
return file(src).then(function(fileEntry){ | ||
return new Promise(function(resolve,reject){ | ||
fileEntry.moveTo(dir,filename(dest),resolve,reject); | ||
}); | ||
}); | ||
}); | ||
} | ||
/* copy a file */ | ||
function copy(src,dest) { | ||
return ensure(dirname(dest)) | ||
.then(function(dir) { | ||
return file(src).then(function(fileEntry){ | ||
return new Promise(function(resolve,reject){ | ||
fileEntry.copyTo(dir,filename(dest),resolve,reject); | ||
}); | ||
}); | ||
}); | ||
} | ||
/* delete a file */ | ||
function remove(path,mustExist) { | ||
var method = mustExist? file:exists; | ||
return new Promise(function(resolve,reject){ | ||
method(path).then(function(fileEntry){ | ||
if(fileEntry !== false) { | ||
fileEntry.remove(resolve,reject); | ||
} else { | ||
resolve(1); | ||
} | ||
},reject); | ||
}).then(function(val){ | ||
return val === 1? false: true; | ||
}); | ||
} | ||
/* delete a directory */ | ||
function removeDir(path) { | ||
return dir(path).then(function(dirEntry){ | ||
return new Promise(function(resolve,reject) { | ||
dirEntry.removeRecursively(resolve,reject); | ||
}); | ||
}); | ||
} | ||
// Whenever we want to start a transfer, we call popTransferQueue | ||
function popTransferQueue(){ | ||
// while we are not at max concurrency | ||
while(transferQueue.length > 0 && inprogress < options.concurrency){ | ||
// increment activity counter | ||
inprogress++; | ||
// fetch filetranfer, method-type (isDownload) and arguments | ||
var args = transferQueue.pop(); | ||
var ft = args.shift(); | ||
var isDownload = args.shift(); | ||
var serverUrl = args.shift(); | ||
var localPath = args.shift(); | ||
var win = args.shift(); | ||
var fail = args.shift(); | ||
var trustAllHosts = args.shift(); | ||
var transferOptions = args.shift(); | ||
if(ft._aborted) { | ||
inprogress--; | ||
} else if(isDownload){ | ||
ft.download.call(ft,serverUrl,localPath,win,fail,trustAllHosts,transferOptions); | ||
if(ft.onprogress) ft.onprogress(new ProgressEvent()); | ||
} else { | ||
ft.upload.call(ft,localPath,serverUrl,win,fail,transferOptions,trustAllHosts); | ||
} | ||
} | ||
// if we are at max concurrency, popTransferQueue() will be called whenever | ||
// the transfer is ready and there is space avaialable. | ||
} | ||
// Promise callback to check if there are any more queued transfers | ||
function nextTransfer(result){ | ||
inprogress--; // decrement counter to free up one space to start transfers again! | ||
popTransferQueue(); // check if there are any queued transfers | ||
return result; | ||
} | ||
function filetransfer(isDownload,serverUrl,localPath,transferOptions,onprogress){ | ||
if(typeof transferOptions === 'function') { | ||
onprogress = transferOptions; | ||
transferOptions = {}; | ||
} | ||
serverUrl = encodeURI(serverUrl); | ||
if(isCordova) localPath = toInternalURLSync(localPath); | ||
transferOptions = transferOptions || {}; | ||
if(!transferOptions.retry || !transferOptions.retry.length) { | ||
transferOptions.retry = options.retry; | ||
} | ||
transferOptions.retry = transferOptions.retry.concat(); | ||
var ft = new FileTransfer(); | ||
onprogress = onprogress || transferOptions.onprogress; | ||
if(typeof onprogress === 'function') ft.onprogress = onprogress; | ||
var promise = new Promise(function(resolve,reject){ | ||
var attempt = function(err){ | ||
if(transferOptions.retry.length === 0) { | ||
reject(err); | ||
} else { | ||
transferQueue.unshift([ft,isDownload,serverUrl,localPath,resolve,attempt,transferOptions.trustAllHosts || false,transferOptions]); | ||
var timeout = transferOptions.retry.shift(); | ||
if(timeout > 0) { | ||
setTimeout(nextTransfer,timeout); | ||
} else { | ||
nextTransfer(); | ||
} | ||
} | ||
}; | ||
transferOptions.retry.unshift(0); | ||
inprogress++; | ||
attempt(); | ||
}); | ||
promise.then(nextTransfer,nextTransfer); | ||
promise.progress = function(onprogress){ | ||
ft.onprogress = onprogress; | ||
return promise; | ||
}; | ||
promise.abort = function(){ | ||
ft._aborted = true; | ||
ft.abort(); | ||
return promise; | ||
}; | ||
return promise; | ||
} | ||
function download(url,dest,options,onprogress){ | ||
return filetransfer(true,url,dest,options,onprogress); | ||
} | ||
function upload(source,dest,options,onprogress){ | ||
return filetransfer(false,dest,source,options,onprogress); | ||
} | ||
return { | ||
fs: fs, | ||
normalize: normalize, | ||
file: file, | ||
filename: filename, | ||
dir: dir, | ||
dirname: dirname, | ||
create:create, | ||
read: read, | ||
readJSON: readJSON, | ||
write: write, | ||
move: move, | ||
copy: copy, | ||
remove: remove, | ||
removeDir: removeDir, | ||
list: list, | ||
ensure: ensure, | ||
exists: exists, | ||
download: download, | ||
upload: upload, | ||
toURL:toURL, | ||
isCordova:isCordova, | ||
toInternalURLSync: toInternalURLSync, | ||
toInternalURL:toInternalURL, | ||
toDataURL:toDataURL, | ||
deviceready: deviceready, | ||
options: options, | ||
Promise: Promise | ||
}; | ||
}; | ||
/***/ }, | ||
/* 3 */ | ||
/***/ function(module, exports, __webpack_require__) { | ||
/** | ||
* JS Implementation of MurmurHash3 (r136) (as of May 20, 2011) | ||
@@ -495,0 +1071,0 @@ * |
@@ -1,1 +0,1 @@ | ||
var CordovaAppLoader=function(t){function e(n){if(o[n])return o[n].exports;var r=o[n]={exports:{},id:n,loaded:!1};return t[n].call(r.exports,r,r.exports,e),r.loaded=!0,r.exports}var o={};return e.m=t,e.c=o,e.p="",e(0)}([function(t,e,o){function n(t){if(!t)throw new Error("CordovaAppLoader has no options!");if(!t.fs)throw new Error('CordovaAppLoader has no "fs" option (cordova-promise-fs)');if(!t.serverRoot)throw new Error('CordovaAppLoader has no "serverRoot" option.');if(!window.pegasus||!window.Manifest)throw new Error("CordovaAppLoader bootstrap.js is missing.");i=t.fs.Promise,this.manifest=window.Manifest,this.newManifest=null,this._lastUpdateFiles=localStorage.getItem("last_update_files"),t.serverRoot=t.serverRoot||"",t.serverRoot&&"/"!==t.serverRoot[t.serverRoot.length-1]&&(t.serverRoot+="/"),this.newManifestUrl=t.serverRoot+(t.manifest||"manifest.json"),t.mode&&(t.mode="mirror"),this.cache=new r(t),this.corruptNewManifest=!1,this._toBeDeleted=[],this._toBeDownloaded=[],this._updateReady=!1,this._checkTimeout=t.checkTimeout||1e4}var r=o(1),i=null;n.prototype._createFilemap=function(t){var e={},o=this.cache._fs.normalize;return Object.keys(t).forEach(function(n){t[n].filename=o(t[n].filename),e[t[n].filename]=t[n]}),e},n.prototype.check=function(t){var e=this,o=this.manifest;return new i(function(n,r){function i(t){return JSON.stringify(t.files)===e._lastUpdateFiles?(JSON.stringify(t.files)!==JSON.stringify(Manifest.files)&&(console.warn("New manifest available, but an earlier update attempt failed. Will not download."),e.corruptNewManifest=!0,n(null)),void n(!1)):void e.cache.ready.then(function(){if(!t.files)return void r('Downloaded Manifest has no "files" attribute.');var i=e._createFilemap(t.files),a=e._createFilemap(o.files);e._toBeDownloaded=Object.keys(i).filter(function(t){return!a[t]||a[t].version!==i[t].version||!e.cache.isCached(t)}),e.cache.list().then(function(o){e._toBeDeleted=o.map(function(t){return t.substr(e.cache.localRoot.length)}).filter(function(t){return!i[t]}).concat(e._toBeDownloaded),e._toBeDeleted.length>0||e._toBeDownloaded.length>0?(e.newManifest=t,e.newManifest.root=e.cache.localInternalURL,n(!0)):n(!1)},r)},r)}"string"==typeof t&&(e.newManifestUrl=t,t=void 0),"object"==typeof t?i(t):(pegasus(e.newManifestUrl).then(i,r),setTimeout(function(){r(new Error("timeout"))},e._checkTimeout))})},n.prototype.canDownload=function(){return!!this.newManifest&&!this._updateReady},n.prototype.canUpdate=function(){return this._updateReady},n.prototype.download=function(t){var e=this;return e.canDownload()?(localStorage.removeItem("manifest"),localStorage.setItem("last_update_files",JSON.stringify(this.newManifest.files)),this.manifest.files=Manifest.files={},e.cache.remove(e._toBeDeleted,!0).then(function(){return e.cache.add(e._toBeDownloaded),e.cache.download(t)}).then(function(){return e._toBeDeleted=[],e._toBeDownloaded=[],e._updateReady=!0,e.newManifest},function(t){return t&&t.length&&e.cache.remove(t),t})):i.resolve(null)},n.prototype.update=function(t){return this._updateReady?(localStorage.setItem("manifest",JSON.stringify(this.newManifest)),t!==!1&&location.reload(),!0):!1},n.prototype.clear=function(){return localStorage.removeItem("manifest"),this.cache.clear()},n.prototype.reset=function(){return this.clear().then(function(){location.reload()},function(){location.reload()})},t.exports=n},function(t,e,o){function n(t){var e=this;if(this._fs=t.fs,!this._fs)throw new Error('Missing required option "fs". Add an instance of cordova-promise-fs.');i=this._fs.Promise,this._mirrorMode="hash"!==t.mode,this._retry=t.retry||[500,1500,8e3],this._cacheBuster=!!t.cacheBuster,this.localRoot=this._fs.normalize(t.localRoot||"data"),this.serverRoot=this._fs.normalize(t.serverRoot||""),this._downloading=[],this._added=[],this._cached={},this.ready=this._fs.ensure(this.localRoot).then(function(t){return e.localInternalURL=a?t.toInternalURL():t.toURL(),e.localUrl=t.toURL(),e.list()})}var r=o(2),i=null,a="undefined"!=typeof cordova;n.prototype.list=function(){var t=this;return new i(function(e){t._fs.list(t.localRoot,"rfe").then(function(o){t._cached={},o=o.map(function(e){var o=t._fs.normalize(e.fullPath);return t._cached[o]={toInternalURL:a?e.toInternalURL():e.toURL(),toURL:e.toURL()},o}),e(o)},function(){e([])})})},n.prototype.add=function(t){t||(t=[]),"string"==typeof t&&(t=[t]);var e=this;return t.forEach(function(t){t=e.toServerURL(t),-1===e._added.indexOf(t)&&e._added.push(t)}),e.isDirty()},n.prototype.remove=function(t,e){t||(t=[]);var o=[];"string"==typeof t&&(t=[t]);var n=this;return t.forEach(function(t){var e=n._added.indexOf(n.toServerURL(t));e>=0&&n._added.splice(e,1);var r=n.toPath(t);o.push(n._fs.remove(r)),delete n._cached[r]}),e?i.all(o):n.isDirty()},n.prototype.getDownloadQueue=function(){var t=this,e=t._added.filter(function(e){return!t.isCached(e)});return e},n.prototype.getAdded=function(){return this._added},n.prototype.isDirty=function(){return this.getDownloadQueue().length>0},n.prototype.download=function(t){var e=this._fs,o=this;return o.abort(),new i(function(n,r){e.ensure(o.localRoot).then(function(){return o.list()}).then(function(){if(!o.isDirty())return void n(o);var i=o.getDownloadQueue(),a=[],s=o._downloading.length,c=o._downloading.length,h=o._downloading.length+i.length;i.forEach(function(i){var l,d=o.toPath(i);"function"==typeof t&&(l=function(e){e.queueIndex=s,e.queueSize=h,e.url=i,e.path=d,e.percentage=s/h,e.loaded>0&&e.total>0&&s!==h&&(e.percentage+=e.loaded/e.total/h),a.indexOf(i)<0&&(a.push(i),s++),t(e)});var f=function(){c++,c===h&&(o._downloading=[],o.list().then(function(){l&&l(new ProgressEvent),o.isDirty()?r(o.getDownloadQueue()):n(o)},r))},u=i;o._cacheBuster&&(u+="?"+Date.now());var p=e.download(i,d,{retry:o._retry},l);p.then(f,f),o._downloading.push(p)})},r)})},n.prototype.abort=function(){this._downloading.forEach(function(t){t.abort()}),this._downloading=[]},n.prototype.isCached=function(t){return t=this.toPath(t),!!this._cached[t]},n.prototype.clear=function(){var t=this;return this._cached={},this._fs.removeDir(this.localRoot).then(function(){return t._fs.ensure(t.localRoot)})},n.prototype.toInternalURL=function(t){return path=this.toPath(t),this._cached[path]?this._cached[path].toInternalURL:t},n.prototype.get=function(t){return path=this.toPath(t),this._cached[path]?this._cached[path].toInternalURL:this.toServerURL(t)},n.prototype.toDataURL=function(t){return this._fs.toDataURL(this.toPath(t))},n.prototype.toURL=function(t){return path=this.toPath(t),this._cached[path]?this._cached[path].toURL:t},n.prototype.toServerURL=function(t){return t=this._fs.normalize(t),t.indexOf("://")<0?this.serverRoot+t:t},n.prototype.toPath=function(t){return this._mirrorMode?(t=t=this._fs.normalize(t||""),len=this.serverRoot.length,t.substr(0,len)!==this.serverRoot?this.localRoot+t:this.localRoot+t.substr(len)):this.localRoot+r(t)+t.substr(t.lastIndexOf("."))},t.exports=n},function(t){function e(t,e){var o,n,r,i,a,s,c,h;for(o=3&t.length,n=t.length-o,r=e,a=3432918353,s=461845907,h=0;n>h;)c=255&t.charCodeAt(h)|(255&t.charCodeAt(++h))<<8|(255&t.charCodeAt(++h))<<16|(255&t.charCodeAt(++h))<<24,++h,c=(65535&c)*a+(((c>>>16)*a&65535)<<16)&4294967295,c=c<<15|c>>>17,c=(65535&c)*s+(((c>>>16)*s&65535)<<16)&4294967295,r^=c,r=r<<13|r>>>19,i=5*(65535&r)+((5*(r>>>16)&65535)<<16)&4294967295,r=(65535&i)+27492+(((i>>>16)+58964&65535)<<16);switch(c=0,o){case 3:c^=(255&t.charCodeAt(h+2))<<16;case 2:c^=(255&t.charCodeAt(h+1))<<8;case 1:c^=255&t.charCodeAt(h),c=(65535&c)*a+(((c>>>16)*a&65535)<<16)&4294967295,c=c<<15|c>>>17,c=(65535&c)*s+(((c>>>16)*s&65535)<<16)&4294967295,r^=c}return r^=t.length,r^=r>>>16,r=2246822507*(65535&r)+((2246822507*(r>>>16)&65535)<<16)&4294967295,r^=r>>>13,r=3266489909*(65535&r)+((3266489909*(r>>>16)&65535)<<16)&4294967295,r^=r>>>16,r>>>0}t.exports=e}]); | ||
var CordovaAppLoader=function(e){function t(o){if(n[o])return n[o].exports;var r=n[o]={exports:{},id:o,loaded:!1};return e[o].call(r.exports,r,r.exports,t),r.loaded=!0,r.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){function o(e){if(!e)throw new Error("CordovaAppLoader has no options!");if(!e.fs)throw new Error('CordovaAppLoader has no "fs" option (cordova-promise-fs)');if(!e.serverRoot)throw new Error('CordovaAppLoader has no "serverRoot" option.');if(!window.pegasus||!window.Manifest)throw new Error("CordovaAppLoader bootstrap.js is missing.");this.allowServerRootFromManifest=e.allowServerRootFromManifest===!0,s=e.fs.Promise,this.manifest=window.Manifest,this.newManifest=null,this.bundledManifest=null,this.bundledFS=null,this._lastUpdateFiles=localStorage.getItem("last_update_files"),e.serverRoot=e.serverRoot||"",e.serverRoot&&"/"!==e.serverRoot[e.serverRoot.length-1]&&(e.serverRoot+="/"),this.newManifestUrl=e.manifestUrl||e.serverRoot+(e.manifest||"manifest.json"),e.mode&&(e.mode="mirror"),this.cache=new r(e),this.corruptNewManifest=!1,this._toBeCopied=[],this._toBeDeleted=[],this._toBeDownloaded=[],this._updateReady=!1,this._checkTimeout=e.checkTimeout||1e4}var r=n(1),i=n(2),s=null,a=8,u="www/";o.prototype._createFilemap=function(e){var t={},n=this.cache._fs.normalize;return Object.keys(e).forEach(function(o){e[o].filename=n(e[o].filename),t[e[o].filename]=e[o]}),t},o.prototype.getBundledManifest=function(){var e=this,t=document.querySelector("script[manifest]"),n=(t?t.getAttribute("manifest"):null)||"manifest.json";return new s(function(t,o){e.bundledManifest?t(e.bundledManifest):(pegasus(n).then(function(n){e.bundledManifest=n,t(n)},o),setTimeout(function(){o(new Error("bundled manifest timeout"))},e._checkTimeout))})},o.prototype.getBundledFS=function(){return self.bundledFS||(self.bundledFS=i({fileSystem:a})),self.bundledFS},o.prototype.copyFromBundle=function(e){var t=this;return new s(function(n,o){var r=t.getBundledFS(),i=t.cache._fs,a=i.dirname(t.cache.localRoot+e),c=u+e,f=i.filename(e);return s.all([r.file(c),i.ensure(a)]).then(function(e){var t=e[0],r=e[1];t.copyTo(r,f,n,o)},o)})},o.prototype.check=function(e){var t=this,n=this.manifest;"string"==typeof e&&(t.newManifestUrl=e,e=void 0);var o=new s(function(n,o){"object"==typeof e?n(e):(console.log("checking new manifest:",t.newManifestUrl),pegasus(t.newManifestUrl).then(n,o),setTimeout(function(){o(new Error("new manifest timeout"))},t._checkTimeout))});return new s(function(e,r){s.all([o,t.getBundledManifest(),t.cache.list()]).then(function(o){var i=o[0],s=o[1];if(JSON.stringify(i.files)===t._lastUpdateFiles)return JSON.stringify(i.files)!==JSON.stringify(Manifest.files)&&(console.warn("New manifest available, but an earlier update attempt failed. Will not download."),t.corruptNewManifest=!0,e(null)),void e(!1);if(!i.files)return void r('Downloaded Manifest has no "files" attribute.');var a=o[2],u=t._createFilemap(n.files),c=t._createFilemap(i.files),f=t._createFilemap(s.files);t._toBeDownloaded=[],t._toBeCopied=[],t._toBeDeleted=[];var l=t.cache._fs.isCordova;Object.keys(c).filter(function(e){return!u[e]||u[e].version!==c[e].version||!t.cache.isCached(e)}).forEach(function(e){l&&f[e]&&f[e].version===c[e].version?t._toBeCopied.push(e):t._toBeDownloaded.push(e)}),t._toBeDeleted=a.map(function(e){return e.substr(t.cache.localRoot.length)}).filter(function(e){return!c[e]||t._toBeDownloaded.indexOf(e)>=0||t._toBeCopied.indexOf(e)>=0});var d=t._toBeDeleted.length+t._toBeDownloaded.length;d>0?(t.newManifest=i,t.newManifest.root=t.cache.localInternalURL,e(!0)):e(!1)})})},o.prototype.canDownload=function(){return!!this.newManifest&&!this._updateReady},o.prototype.canUpdate=function(){return this._updateReady},o.prototype.download=function(e){var t=this;return t.canDownload()?(localStorage.removeItem("manifest"),localStorage.setItem("last_update_files",JSON.stringify(this.newManifest.files)),this.manifest.files=Manifest.files={},t.cache.remove(t._toBeDeleted,!0).then(function(){return s.all(t._toBeCopied.map(t.copyFromBundle.bind(t)))}).then(function(){return t.allowServerRootFromManifest&&t.newManifest.serverRoot&&(t.cache.serverRoot=t.newManifest.serverRoot),t.cache.add(t._toBeDownloaded),t.cache.download(e)}).then(function(){return t._toBeDeleted=[],t._toBeDownloaded=[],t._updateReady=!0,t.newManifest},function(e){return e&&e.length&&t.cache.remove(e),e})):s.resolve(null)},o.prototype.update=function(e){return this._updateReady?(localStorage.setItem("manifest",JSON.stringify(this.newManifest)),e!==!1&&location.reload(),!0):!1},o.prototype.clear=function(){return localStorage.removeItem("last_update_files"),localStorage.removeItem("manifest"),this.cache.clear()},o.prototype.reset=function(){return this.clear().then(function(){location.reload()},function(){location.reload()})},e.exports=o},function(e,t,n){function o(e){var t=this;if(this._fs=e.fs,!this._fs)throw new Error('Missing required option "fs". Add an instance of cordova-promise-fs.');i=this._fs.Promise,this._mirrorMode="hash"!==e.mode,this._retry=e.retry||[500,1500,8e3],this._cacheBuster=!!e.cacheBuster,this.localRoot=this._fs.normalize(e.localRoot||"data"),this.serverRoot=this._fs.normalize(e.serverRoot||""),this._downloading=[],this._added=[],this._cached={},this.ready=this._fs.ensure(this.localRoot).then(function(e){return t.localInternalURL=s?e.toInternalURL():e.toURL(),t.localUrl=e.toURL(),t.list()})}var r=n(3),i=null,s="undefined"!=typeof cordova;o.prototype.list=function(){var e=this;return new i(function(t){e._fs.list(e.localRoot,"rfe").then(function(n){e._cached={},n=n.map(function(t){var n=e._fs.normalize(t.fullPath);return e._cached[n]={toInternalURL:s?t.toInternalURL():t.toURL(),toURL:t.toURL()},n}),t(n)},function(){t([])})})},o.prototype.add=function(e){e||(e=[]),"string"==typeof e&&(e=[e]);var t=this;return e.forEach(function(e){e=t.toServerURL(e),-1===t._added.indexOf(e)&&t._added.push(e)}),t.isDirty()},o.prototype.remove=function(e,t){e||(e=[]);var n=[];"string"==typeof e&&(e=[e]);var o=this;return e.forEach(function(e){var t=o._added.indexOf(o.toServerURL(e));t>=0&&o._added.splice(t,1);var r=o.toPath(e);n.push(o._fs.remove(r)),delete o._cached[r]}),t?i.all(n):o.isDirty()},o.prototype.getDownloadQueue=function(){var e=this,t=e._added.filter(function(t){return!e.isCached(t)});return t},o.prototype.getAdded=function(){return this._added},o.prototype.isDirty=function(){return this.getDownloadQueue().length>0},o.prototype.download=function(e){var t=this._fs,n=this;return n.abort(),new i(function(o,r){t.ensure(n.localRoot).then(function(){return n.list()}).then(function(){if(!n.isDirty())return void o(n);var i=n.getDownloadQueue(),s=[],a=n._downloading.length,u=n._downloading.length,c=n._downloading.length+i.length;i.forEach(function(i){var f,l=n.toPath(i);"function"==typeof e&&(f=function(t){t.queueIndex=a,t.queueSize=c,t.url=i,t.path=l,t.percentage=a/c,t.loaded>0&&t.total>0&&a!==c&&(t.percentage+=t.loaded/t.total/c),s.indexOf(i)<0&&(s.push(i),a++),e(t)});var d=function(){u++,u===c&&(n._downloading=[],n.list().then(function(){f&&f(new ProgressEvent),n.isDirty()?r(n.getDownloadQueue()):o(n)},r))},h=i;n._cacheBuster&&(h+="?"+Date.now());var p=t.download(h,l,{retry:n._retry},f);p.then(d,d),n._downloading.push(p)})},r)})},o.prototype.abort=function(){this._downloading.forEach(function(e){e.abort()}),this._downloading=[]},o.prototype.isCached=function(e){return e=this.toPath(e),!!this._cached[e]},o.prototype.clear=function(){var e=this;return this._cached={},this._fs.removeDir(this.localRoot).then(function(){return e._fs.ensure(e.localRoot)})},o.prototype.toInternalURL=function(e){return path=this.toPath(e),this._cached[path]?this._cached[path].toInternalURL:e},o.prototype.get=function(e){return path=this.toPath(e),this._cached[path]?this._cached[path].toInternalURL:this.toServerURL(e)},o.prototype.toDataURL=function(e){return this._fs.toDataURL(this.toPath(e))},o.prototype.toURL=function(e){return path=this.toPath(e),this._cached[path]?this._cached[path].toURL:e},o.prototype.toServerURL=function(e){return e=this._fs.normalize(e),e.indexOf("://")<0?this.serverRoot+e:e},o.prototype.toPath=function(e){return this._mirrorMode?(e=e=this._fs.normalize(e||""),len=this.serverRoot.length,e.substr(0,len)!==this.serverRoot?this.localRoot+e:this.localRoot+e.substr(len)):this.localRoot+r(e)+e.substr(e.lastIndexOf("."))},e.exports=o},function(e){function t(e,n,o,r){e.getDirectory(n[0],{create:!0},function(e){n.length>1?t(e,n.slice(1),o,r):o(e)},r)}function n(e){return e=e.substr(0,e.lastIndexOf("/")+1),"/"===e[0]&&(e=e.substr(1)),e}function o(e){return e.substr(e.lastIndexOf("/")+1)}function r(e){return e=e||"","/"===e[0]&&(e=e.substr(1)),e&&e.indexOf(".")<0&&"/"!==e[e.length-1]&&(e+="/"),"./"===e&&(e=""),e}var i=[],s=0;e.exports=function(e){function a(e){return new M(function(t){return t(e)})}function u(e){return new M(function(n,o){return C.then(function(r){e?(e=e.split("/").filter(function(e){return e&&e.length>0&&"."!==e[0]}),t(r.root,e,n,o)):n(r.root)},o)})}function c(e,t){return new M(function(n,o){return e instanceof FileEntry?n(e):(e=r(e),t=t||{},C.then(function(r){r.root.getFile(e,t,n,o)},o))})}function f(e,t){return e=r(e),t=t||{},new M(function(n,o){return C.then(function(r){e&&"/"!==e?r.root.getDirectory(e,t,n,o):n(r.root)},o)})}function l(e,t){t=t||"";var n=t.indexOf("r")>-1,o=t.indexOf("e")>-1,r=t.indexOf("f")>-1,i=t.indexOf("d")>-1;return r&&i&&(r=!1,i=!1),new M(function(t,s){return f(e).then(function(e){var u=e.createReader();u.readEntries(function(e){var u=[a(e)];n&&e.filter(function(e){return e.isDirectory}).forEach(function(e){u.push(l(e.fullPath,"re"))}),M.all(u).then(function(e){var n=[];n=n.concat.apply(n,e),r&&(n=n.filter(function(e){return e.isFile})),i&&(n=n.filter(function(e){return e.isDirectory})),o||(n=n.map(function(e){return e.fullPath})),t(n)},s)},s)},s)})}function d(e){return new M(function(t,n){c(e).then(function(e){t(e)},function(e){1===e.code?t(!1):n(e)})})}function h(e){return u(n(e)).then(function(){return c(e,{create:!0})})}function p(e){return c(e).then(function(e){return e.toURL()})}function w(e,t){return t=t||"readAsText",c(e).then(function(e){return new M(function(n,o){e.file(function(e){var o=new FileReader;o.onloadend=function(){n(this.result)},o[t](e)},o)})})}function v(e){return w(e,"readAsDataURL")}function y(e){return w(e).then(JSON.parse)}function m(e,t,o){return u(n(e)).then(function(){return c(e,{create:!0})}).then(function(e){return new M(function(n,r){e.createWriter(function(e){e.onwriteend=n,e.onerror=r,"string"==typeof t?t=new Blob([t],{type:o||"text/plain"}):t instanceof Blob!=!0&&(t=new Blob([JSON.stringify(t,null,4)],{type:o||"application/json"})),e.write(t)},r)})})}function _(e,t){return u(n(t)).then(function(n){return c(e).then(function(e){return new M(function(r,i){e.moveTo(n,o(t),r,i)})})})}function g(e,t){return u(n(t)).then(function(n){return c(e).then(function(e){return new M(function(r,i){e.copyTo(n,o(t),r,i)})})})}function R(e,t){var n=t?c:d;return new M(function(t,o){n(e).then(function(e){e!==!1?e.remove(t,o):t(1)},o)}).then(function(e){return 1===e?!1:!0})}function S(e){return f(e).then(function(e){return new M(function(t,n){e.removeRecursively(t,n)})})}function b(){for(;i.length>0&&s<e.concurrency;){s++;var t=i.pop(),n=t.shift(),o=t.shift(),r=t.shift(),a=t.shift(),u=t.shift(),c=t.shift(),f=t.shift(),l=t.shift();n._aborted?s--:o?(n.download.call(n,r,a,u,c,f,l),n.onprogress&&n.onprogress(new ProgressEvent)):n.upload.call(n,a,r,u,c,l,f)}}function U(e){return s--,b(),e}function D(t,n,o,r,a){"function"==typeof r&&(a=r,r={}),n=encodeURI(n),x&&(o=O(o)),r=r||{},r.retry&&r.retry.length||(r.retry=e.retry),r.retry=r.retry.concat();var u=new FileTransfer;a=a||r.onprogress,"function"==typeof a&&(u.onprogress=a);var c=new M(function(e,a){var c=function(s){if(0===r.retry.length)a(s);else{i.unshift([u,t,n,o,e,c,r.trustAllHosts||!1,r]);var f=r.retry.shift();f>0?setTimeout(U,f):U()}};r.retry.unshift(0),s++,c()});return c.then(U,U),c.progress=function(e){return u.onprogress=e,c},c.abort=function(){return u._aborted=!0,u.abort(),c},c}function L(e,t,n,o){return D(!0,e,t,n,o)}function F(e,t,n,o){return D(!1,t,e,n,o)}var M=e.Promise||window.Promise;if(!M)throw new Error("No Promise library given in options.Promise");this.options=e=e||{},e.persistent=void 0!==e.persistent?e.persistent:!0,e.storageSize=e.storageSize||20971520,e.concurrency=e.concurrency||3,e.retry=e.retry||[];var B,x="undefined"!=typeof cordova;x?B=new M(function(e,t){document.addEventListener("deviceready",e,!1),setTimeout(function(){t(new Error("deviceready has not fired after 5 seconds."))},5100)}):(B=a(!0),"undefined"!=typeof webkitRequestFileSystem?(window.requestFileSystem=webkitRequestFileSystem,window.FileTransfer=function(){},FileTransfer.prototype.download=function(e,t,n,o){var r=new XMLHttpRequest;return r.open("GET",e),r.responseType="blob",r.onreadystatechange=function(){4==r.readyState&&(200===r.status?m(t,r.response).then(n,o):o(r.status))},r.send(),r},window.ProgressEvent=function(){}):window.requestFileSystem=function(e,t,n,o){o(new Error("requestFileSystem not supported!"))});var C=new M(function(t,n){B.then(function(){var o=e.persistent?1:0;"number"==typeof e.fileSystem&&(o=e.fileSystem),!x&&o>1&&(console.warn('Chrome does not support fileSystem "'+o+'". Falling back on "0" (temporary).'),o=0),window.requestFileSystem(o,e.storageSize,t,n),setTimeout(function(){n(new Error("Could not retrieve FileSystem after 5 seconds."))},5100)},n)});C.then(function(e){window.__fs=e},function(e){console.error("Could not get Cordova FileSystem:",e)});var E,O;return x?(O=function(t){return t=r(t),"cdvfile://localhost/"+(e.persistent?"persistent/":"temporary/")+t},E=function(e){return c(e).then(function(e){return e.toInternalURL()})}):(O=function(t){return t=r(t),"filesystem:"+location.origin+(e.persistent?"/persistent/":"/temporary/")+t},E=function(e){return c(e).then(function(e){return e.toURL()})}),{fs:C,normalize:r,file:c,filename:o,dir:f,dirname:n,create:h,read:w,readJSON:y,write:m,move:_,copy:g,remove:R,removeDir:S,list:l,ensure:u,exists:d,download:L,upload:F,toURL:p,isCordova:x,toInternalURLSync:O,toInternalURL:E,toDataURL:v,deviceready:B,options:e,Promise:M}}},function(e){function t(e,t){var n,o,r,i,s,a,u,c;for(n=3&e.length,o=e.length-n,r=t,s=3432918353,a=461845907,c=0;o>c;)u=255&e.charCodeAt(c)|(255&e.charCodeAt(++c))<<8|(255&e.charCodeAt(++c))<<16|(255&e.charCodeAt(++c))<<24,++c,u=(65535&u)*s+(((u>>>16)*s&65535)<<16)&4294967295,u=u<<15|u>>>17,u=(65535&u)*a+(((u>>>16)*a&65535)<<16)&4294967295,r^=u,r=r<<13|r>>>19,i=5*(65535&r)+((5*(r>>>16)&65535)<<16)&4294967295,r=(65535&i)+27492+(((i>>>16)+58964&65535)<<16);switch(u=0,n){case 3:u^=(255&e.charCodeAt(c+2))<<16;case 2:u^=(255&e.charCodeAt(c+1))<<8;case 1:u^=255&e.charCodeAt(c),u=(65535&u)*s+(((u>>>16)*s&65535)<<16)&4294967295,u=u<<15|u>>>17,u=(65535&u)*a+(((u>>>16)*a&65535)<<16)&4294967295,r^=u}return r^=e.length,r^=r>>>16,r=2246822507*(65535&r)+((2246822507*(r>>>16)&65535)<<16)&4294967295,r^=r>>>13,r=3266489909*(65535&r)+((3266489909*(r>>>16)&65535)<<16)&4294967295,r^=r>>>16,r>>>0}e.exports=t}]); |
@@ -75,2 +75,3 @@ var CordovaPromiseFS = | ||
function normalize(str){ | ||
str = str || ''; | ||
if(str[0] === '/') str = str.substr(1); | ||
@@ -147,3 +148,12 @@ if(!!str && str.indexOf('.') < 0 && str[str.length-1] !== '/') str += '/'; | ||
deviceready.then(function(){ | ||
window.requestFileSystem(options.persistent? 1: 0, options.storageSize, resolve, reject); | ||
var type = options.persistent? 1: 0; | ||
if(typeof options.fileSystem === 'number'){ | ||
type = options.fileSystem; | ||
} | ||
// Chrome only supports persistent and temp storage, not the exotic onces from Cordova | ||
if(!isCordova && type > 1) { | ||
console.warn('Chrome does not support fileSystem "'+type+'". Falling back on "0" (temporary).'); | ||
type = 0; | ||
} | ||
window.requestFileSystem(type, options.storageSize, resolve, reject); | ||
setTimeout(function(){ reject(new Error('Could not retrieve FileSystem after 5 seconds.')); },5100); | ||
@@ -178,5 +188,8 @@ },reject); | ||
function file(path,options){ | ||
path = normalize(path); | ||
options = options || {}; | ||
return new Promise(function(resolve,reject){ | ||
if(path instanceof FileEntry) { | ||
return resolve(path); | ||
} | ||
path = normalize(path); | ||
options = options || {}; | ||
return fs.then(function(fs){ | ||
@@ -183,0 +196,0 @@ fs.root.getFile(path,options,resolve,reject); |
@@ -1,1 +0,1 @@ | ||
var CordovaPromiseFS=function(n){function t(r){if(e[r])return e[r].exports;var o=e[r]={exports:{},id:r,loaded:!1};return n[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var e={};return t.m=n,t.c=e,t.p="",t(0)}([function(n){function t(n,e,r,o){n.getDirectory(e[0],{create:!0},function(n){e.length>1?t(n,e.slice(1),r,o):r(n)},o)}function e(n){return n=n.substr(0,n.lastIndexOf("/")+1),"/"===n[0]&&(n=n.substr(1)),n}function r(n){return n.substr(n.lastIndexOf("/")+1)}function o(n){return"/"===n[0]&&(n=n.substr(1)),n&&n.indexOf(".")<0&&"/"!==n[n.length-1]&&(n+="/"),"./"===n&&(n=""),n}var i=[],u=0;n.exports=function(n){function f(n){return new O(function(t){return t(n)})}function c(n){return new O(function(e,r){return q.then(function(o){n?(n=n.split("/").filter(function(n){return n&&n.length>0&&"."!==n[0]}),t(o.root,n,e,r)):e(o.root)},r)})}function s(n,t){return n=o(n),t=t||{},new O(function(e,r){return q.then(function(o){o.root.getFile(n,t,e,r)},r)})}function a(n,t){return n=o(n),t=t||{},new O(function(e,r){return q.then(function(o){n&&"/"!==n?o.root.getDirectory(n,t,e,r):e(o.root)},r)})}function l(n,t){t=t||"";var e=t.indexOf("r")>-1,r=t.indexOf("e")>-1,o=t.indexOf("f")>-1,i=t.indexOf("d")>-1;return o&&i&&(o=!1,i=!1),new O(function(t,u){return a(n).then(function(n){var c=n.createReader();c.readEntries(function(n){var c=[f(n)];e&&n.filter(function(n){return n.isDirectory}).forEach(function(n){c.push(l(n.fullPath,"re"))}),O.all(c).then(function(n){var e=[];e=e.concat.apply(e,n),o&&(e=e.filter(function(n){return n.isFile})),i&&(e=e.filter(function(n){return n.isDirectory})),r||(e=e.map(function(n){return n.fullPath})),t(e)},u)},u)},u)})}function d(n){return new O(function(t,e){s(n).then(function(n){t(n)},function(n){1===n.code?t(!1):e(n)})})}function p(n){return c(e(n)).then(function(){return s(n,{create:!0})})}function h(n){return s(n).then(function(n){return n.toURL()})}function y(n,t){return t=t||"readAsText",s(n).then(function(n){return new O(function(e,r){n.file(function(n){var r=new FileReader;r.onloadend=function(){e(this.result)},r[t](n)},r)})})}function w(n){return y(n,"readAsDataURL")}function v(n){return y(n).then(JSON.parse)}function m(n,t,r){return c(e(n)).then(function(){return s(n,{create:!0})}).then(function(n){return new O(function(e,o){n.createWriter(function(n){n.onwriteend=e,n.onerror=o,"string"==typeof t?t=new Blob([t],{type:r||"text/plain"}):t instanceof Blob!=!0&&(t=new Blob([JSON.stringify(t,null,4)],{type:r||"application/json"})),n.write(t)},o)})})}function g(n,t){return c(e(t)).then(function(e){return s(n).then(function(n){return new O(function(o,i){n.moveTo(e,r(t),o,i)})})})}function S(n,t){return c(e(t)).then(function(e){return s(n).then(function(n){return new O(function(o,i){n.copyTo(e,r(t),o,i)})})})}function b(n,t){var e=t?s:d;return new O(function(t,r){e(n).then(function(n){n!==!1?n.remove(t,r):t(1)},r)}).then(function(n){return 1===n?!1:!0})}function x(n){return a(n).then(function(n){return new O(function(t,e){n.removeRecursively(t,e)})})}function F(){for(;i.length>0&&u<n.concurrency;){u++;var t=i.pop(),e=t.shift(),r=t.shift(),o=t.shift(),f=t.shift(),c=t.shift(),s=t.shift(),a=t.shift(),l=t.shift();e._aborted?u--:r?(e.download.call(e,o,f,c,s,a,l),e.onprogress&&e.onprogress(new ProgressEvent)):e.upload.call(e,f,o,c,s,l,a)}}function R(n){return u--,F(),n}function T(t,e,r,o,f){"function"==typeof o&&(f=o,o={}),e=encodeURI(e),U&&(r=I(r)),o=o||{},o.retry&&o.retry.length||(o.retry=n.retry),o.retry=o.retry.concat();var c=new FileTransfer;f=f||o.onprogress,"function"==typeof f&&(c.onprogress=f);var s=new O(function(n,f){var s=function(u){if(0===o.retry.length)f(u);else{i.unshift([c,t,e,r,n,s,o.trustAllHosts||!1,o]);var a=o.retry.shift();a>0?setTimeout(R,a):R()}};o.retry.unshift(0),u++,s()});return s.then(R,R),s.progress=function(n){return c.onprogress=n,s},s.abort=function(){return c._aborted=!0,c.abort(),s},s}function E(n,t,e,r){return T(!0,n,t,e,r)}function L(n,t,e,r){return T(!1,t,n,e,r)}var O=n.Promise||window.Promise;if(!O)throw new Error("No Promise library given in options.Promise");this.options=n=n||{},n.persistent=void 0!==n.persistent?n.persistent:!0,n.storageSize=n.storageSize||20971520,n.concurrency=n.concurrency||3,n.retry=n.retry||[];var P,U="undefined"!=typeof cordova;U?P=new O(function(n,t){document.addEventListener("deviceready",n,!1),setTimeout(function(){t(new Error("deviceready has not fired after 5 seconds."))},5100)}):(P=f(!0),"undefined"!=typeof webkitRequestFileSystem?(window.requestFileSystem=webkitRequestFileSystem,window.FileTransfer=function(){},FileTransfer.prototype.download=function(n,t,e,r){var o=new XMLHttpRequest;return o.open("GET",n),o.responseType="blob",o.onreadystatechange=function(){4==o.readyState&&(200===o.status?m(t,o.response).then(e,r):r(o.status))},o.send(),o},window.ProgressEvent=function(){}):window.requestFileSystem=function(n,t,e,r){r(new Error("requestFileSystem not supported!"))});var q=new O(function(t,e){P.then(function(){window.requestFileSystem(n.persistent?1:0,n.storageSize,t,e),setTimeout(function(){e(new Error("Could not retrieve FileSystem after 5 seconds."))},5100)},e)});q.then(function(n){window.__fs=n},function(n){console.error("Could not get Cordova FileSystem:",n)});var D,I;return U?(I=function(t){return t=o(t),"cdvfile://localhost/"+(n.persistent?"persistent/":"temporary/")+t},D=function(n){return s(n).then(function(n){return n.toInternalURL()})}):(I=function(t){return t=o(t),"filesystem:"+location.origin+(n.persistent?"/persistent/":"/temporary/")+t},D=function(n){return s(n).then(function(n){return n.toURL()})}),{fs:q,normalize:o,file:s,filename:r,dir:a,dirname:e,create:p,read:y,readJSON:v,write:m,move:g,copy:S,remove:b,removeDir:x,list:l,ensure:c,exists:d,download:E,upload:L,toURL:h,isCordova:U,toInternalURLSync:I,toInternalURL:D,toDataURL:w,deviceready:P,options:n,Promise:O}}}]); | ||
var CordovaPromiseFS=function(n){function e(r){if(t[r])return t[r].exports;var o=t[r]={exports:{},id:r,loaded:!1};return n[r].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var t={};return e.m=n,e.c=t,e.p="",e(0)}([function(n){function e(n,t,r,o){n.getDirectory(t[0],{create:!0},function(n){t.length>1?e(n,t.slice(1),r,o):r(n)},o)}function t(n){return n=n.substr(0,n.lastIndexOf("/")+1),"/"===n[0]&&(n=n.substr(1)),n}function r(n){return n.substr(n.lastIndexOf("/")+1)}function o(n){return n=n||"","/"===n[0]&&(n=n.substr(1)),n&&n.indexOf(".")<0&&"/"!==n[n.length-1]&&(n+="/"),"./"===n&&(n=""),n}var i=[],u=0;n.exports=function(n){function f(n){return new O(function(e){return e(n)})}function s(n){return new O(function(t,r){return q.then(function(o){n?(n=n.split("/").filter(function(n){return n&&n.length>0&&"."!==n[0]}),e(o.root,n,t,r)):t(o.root)},r)})}function c(n,e){return new O(function(t,r){return n instanceof FileEntry?t(n):(n=o(n),e=e||{},q.then(function(o){o.root.getFile(n,e,t,r)},r))})}function a(n,e){return n=o(n),e=e||{},new O(function(t,r){return q.then(function(o){n&&"/"!==n?o.root.getDirectory(n,e,t,r):t(o.root)},r)})}function l(n,e){e=e||"";var t=e.indexOf("r")>-1,r=e.indexOf("e")>-1,o=e.indexOf("f")>-1,i=e.indexOf("d")>-1;return o&&i&&(o=!1,i=!1),new O(function(e,u){return a(n).then(function(n){var s=n.createReader();s.readEntries(function(n){var s=[f(n)];t&&n.filter(function(n){return n.isDirectory}).forEach(function(n){s.push(l(n.fullPath,"re"))}),O.all(s).then(function(n){var t=[];t=t.concat.apply(t,n),o&&(t=t.filter(function(n){return n.isFile})),i&&(t=t.filter(function(n){return n.isDirectory})),r||(t=t.map(function(n){return n.fullPath})),e(t)},u)},u)},u)})}function d(n){return new O(function(e,t){c(n).then(function(n){e(n)},function(n){1===n.code?e(!1):t(n)})})}function p(n){return s(t(n)).then(function(){return c(n,{create:!0})})}function y(n){return c(n).then(function(n){return n.toURL()})}function h(n,e){return e=e||"readAsText",c(n).then(function(n){return new O(function(t,r){n.file(function(n){var r=new FileReader;r.onloadend=function(){t(this.result)},r[e](n)},r)})})}function w(n){return h(n,"readAsDataURL")}function v(n){return h(n).then(JSON.parse)}function m(n,e,r){return s(t(n)).then(function(){return c(n,{create:!0})}).then(function(n){return new O(function(t,o){n.createWriter(function(n){n.onwriteend=t,n.onerror=o,"string"==typeof e?e=new Blob([e],{type:r||"text/plain"}):e instanceof Blob!=!0&&(e=new Blob([JSON.stringify(e,null,4)],{type:r||"application/json"})),n.write(e)},o)})})}function g(n,e){return s(t(e)).then(function(t){return c(n).then(function(n){return new O(function(o,i){n.moveTo(t,r(e),o,i)})})})}function S(n,e){return s(t(e)).then(function(t){return c(n).then(function(n){return new O(function(o,i){n.copyTo(t,r(e),o,i)})})})}function b(n,e){var t=e?c:d;return new O(function(e,r){t(n).then(function(n){n!==!1?n.remove(e,r):e(1)},r)}).then(function(n){return 1===n?!1:!0})}function F(n){return a(n).then(function(n){return new O(function(e,t){n.removeRecursively(e,t)})})}function x(){for(;i.length>0&&u<n.concurrency;){u++;var e=i.pop(),t=e.shift(),r=e.shift(),o=e.shift(),f=e.shift(),s=e.shift(),c=e.shift(),a=e.shift(),l=e.shift();t._aborted?u--:r?(t.download.call(t,o,f,s,c,a,l),t.onprogress&&t.onprogress(new ProgressEvent)):t.upload.call(t,f,o,s,c,l,a)}}function R(n){return u--,x(),n}function E(e,t,r,o,f){"function"==typeof o&&(f=o,o={}),t=encodeURI(t),U&&(r=C(r)),o=o||{},o.retry&&o.retry.length||(o.retry=n.retry),o.retry=o.retry.concat();var s=new FileTransfer;f=f||o.onprogress,"function"==typeof f&&(s.onprogress=f);var c=new O(function(n,f){var c=function(u){if(0===o.retry.length)f(u);else{i.unshift([s,e,t,r,n,c,o.trustAllHosts||!1,o]);var a=o.retry.shift();a>0?setTimeout(R,a):R()}};o.retry.unshift(0),u++,c()});return c.then(R,R),c.progress=function(n){return s.onprogress=n,c},c.abort=function(){return s._aborted=!0,s.abort(),c},c}function T(n,e,t,r){return E(!0,n,e,t,r)}function L(n,e,t,r){return E(!1,e,n,t,r)}var O=n.Promise||window.Promise;if(!O)throw new Error("No Promise library given in options.Promise");this.options=n=n||{},n.persistent=void 0!==n.persistent?n.persistent:!0,n.storageSize=n.storageSize||20971520,n.concurrency=n.concurrency||3,n.retry=n.retry||[];var P,U="undefined"!=typeof cordova;U?P=new O(function(n,e){document.addEventListener("deviceready",n,!1),setTimeout(function(){e(new Error("deviceready has not fired after 5 seconds."))},5100)}):(P=f(!0),"undefined"!=typeof webkitRequestFileSystem?(window.requestFileSystem=webkitRequestFileSystem,window.FileTransfer=function(){},FileTransfer.prototype.download=function(n,e,t,r){var o=new XMLHttpRequest;return o.open("GET",n),o.responseType="blob",o.onreadystatechange=function(){4==o.readyState&&(200===o.status?m(e,o.response).then(t,r):r(o.status))},o.send(),o},window.ProgressEvent=function(){}):window.requestFileSystem=function(n,e,t,r){r(new Error("requestFileSystem not supported!"))});var q=new O(function(e,t){P.then(function(){var r=n.persistent?1:0;"number"==typeof n.fileSystem&&(r=n.fileSystem),!U&&r>1&&(console.warn('Chrome does not support fileSystem "'+r+'". Falling back on "0" (temporary).'),r=0),window.requestFileSystem(r,n.storageSize,e,t),setTimeout(function(){t(new Error("Could not retrieve FileSystem after 5 seconds."))},5100)},t)});q.then(function(n){window.__fs=n},function(n){console.error("Could not get Cordova FileSystem:",n)});var D,C;return U?(C=function(e){return e=o(e),"cdvfile://localhost/"+(n.persistent?"persistent/":"temporary/")+e},D=function(n){return c(n).then(function(n){return n.toInternalURL()})}):(C=function(e){return e=o(e),"filesystem:"+location.origin+(n.persistent?"/persistent/":"/temporary/")+e},D=function(n){return c(n).then(function(n){return n.toURL()})}),{fs:q,normalize:o,file:c,filename:r,dir:a,dirname:t,create:p,read:h,readJSON:v,write:m,move:g,copy:S,remove:b,removeDir:F,list:l,ensure:s,exists:d,download:T,upload:L,toURL:y,isCordova:U,toInternalURLSync:C,toInternalURL:D,toDataURL:w,deviceready:P,options:n,Promise:O}}}]); |
191
index.js
var CordovaFileCache = require('cordova-file-cache'); | ||
var CordovaPromiseFS = require('cordova-promise-fs'); | ||
var Promise = null; | ||
var BUNDLED_FILE_SYSTEM_TYPE = 8; | ||
var BUNDLE_ROOT = 'www/'; | ||
function AppLoader(options){ | ||
@@ -9,2 +13,3 @@ if(!options) throw new Error('CordovaAppLoader has no options!'); | ||
if(!window.pegasus || !window.Manifest) throw new Error('CordovaAppLoader bootstrap.js is missing.'); | ||
this.allowServerRootFromManifest = options.allowServerRootFromManifest === true; | ||
Promise = options.fs.Promise; | ||
@@ -15,2 +20,4 @@ | ||
this.newManifest = null; | ||
this.bundledManifest = null; | ||
this.bundledFS = null; | ||
this._lastUpdateFiles = localStorage.getItem('last_update_files'); | ||
@@ -21,3 +28,3 @@ | ||
if(!!options.serverRoot && options.serverRoot[options.serverRoot.length-1] !== '/') options.serverRoot += '/'; | ||
this.newManifestUrl = options.serverRoot + (options.manifest || 'manifest.json'); | ||
this.newManifestUrl = options.manifestUrl || options.serverRoot + (options.manifest || 'manifest.json'); | ||
@@ -30,2 +37,3 @@ // initialize a file cache | ||
this.corruptNewManifest = false; | ||
this._toBeCopied = []; | ||
this._toBeDeleted = []; | ||
@@ -47,24 +55,79 @@ this._toBeDownloaded = []; | ||
AppLoader.prototype.getBundledManifest = function(){ | ||
var self = this; | ||
var bootstrapScript = document.querySelector('script[manifest]'); | ||
var bundledManifestUrl = (bootstrapScript? bootstrapScript.getAttribute('manifest'): null) || 'manifest.json'; | ||
return new Promise(function(resolve,reject){ | ||
if(self.bundledManifest) { | ||
resolve(self.bundledManifest); | ||
} else { | ||
pegasus(bundledManifestUrl).then(function(bundledManifest){ | ||
self.bundledManifest = bundledManifest; | ||
resolve(bundledManifest); | ||
},reject); | ||
setTimeout(function(){reject(new Error('bundled manifest timeout'));},self._checkTimeout); | ||
} | ||
}); | ||
}; | ||
AppLoader.prototype.getBundledFS = function(){ | ||
if(!self.bundledFS){ | ||
self.bundledFS = CordovaPromiseFS({ fileSystem: BUNDLED_FILE_SYSTEM_TYPE }); | ||
} | ||
return self.bundledFS; | ||
}; | ||
AppLoader.prototype.copyFromBundle = function(file){ | ||
var self = this; | ||
return new Promise(function(resolve,reject){ | ||
var bundledFS = self.getBundledFS(); | ||
var cacheFS = self.cache._fs; | ||
var destDirPath = cacheFS.dirname(self.cache.localRoot + file); | ||
var srcFilePath = BUNDLE_ROOT + file; | ||
var srcFilename = cacheFS.filename(file); | ||
return Promise.all([bundledFS.file(srcFilePath),cacheFS.ensure(destDirPath)]) | ||
.then(function(val){ | ||
var srcFile = val[0], destDir = val[1]; | ||
srcFile.copyTo(destDir,srcFilename,resolve,reject); | ||
},reject); | ||
}); | ||
}; | ||
AppLoader.prototype.check = function(newManifest){ | ||
var self = this, manifest = this.manifest; | ||
if(typeof newManifest === "string") { | ||
self.newManifestUrl = newManifest; | ||
newManifest = undefined; | ||
} | ||
return new Promise(function(resolve,reject){ | ||
if(typeof newManifest === "string") { | ||
self.newManifestUrl = newManifest; | ||
newManifest = undefined; | ||
var gotNewManifest = new Promise(function(resolve,reject){ | ||
if(typeof newManifest === "object") { | ||
resolve(newManifest); | ||
} else { | ||
console.log('checking new manifest:',self.newManifestUrl); | ||
pegasus(self.newManifestUrl).then(resolve,reject); | ||
setTimeout(function(){reject(new Error('new manifest timeout'));},self._checkTimeout); | ||
} | ||
}); | ||
function checkManifest(newManifest){ | ||
if(JSON.stringify(newManifest.files) === self._lastUpdateFiles) { | ||
if(JSON.stringify(newManifest.files) !== JSON.stringify(Manifest.files)){ | ||
console.warn('New manifest available, but an earlier update attempt failed. Will not download.'); | ||
self.corruptNewManifest = true; | ||
resolve(null); | ||
return new Promise(function(resolve,reject){ | ||
Promise.all([gotNewManifest,self.getBundledManifest(),self.cache.list()]) | ||
.then(function(values){ | ||
var newManifest = values[0]; | ||
var bundledManifest = values[1]; | ||
// Prevent end-less update loop, check if new manifest | ||
// has been downloaded before (but failes) | ||
if(JSON.stringify(newManifest.files) === self._lastUpdateFiles) { | ||
if(JSON.stringify(newManifest.files) !== JSON.stringify(Manifest.files)){ | ||
console.warn('New manifest available, but an earlier update attempt failed. Will not download.'); | ||
self.corruptNewManifest = true; | ||
resolve(null); | ||
} | ||
resolve(false); | ||
return; | ||
} | ||
resolve(false); | ||
return; | ||
} | ||
// make sure cache is ready for the DIFF operations! | ||
self.cache.ready.then(function(list){ | ||
// Check if new manifest is valid | ||
if(!newManifest.files){ | ||
@@ -75,41 +138,62 @@ reject('Downloaded Manifest has no "files" attribute.'); | ||
var newFiles = self._createFilemap(newManifest.files); | ||
var oldFiles = self._createFilemap(manifest.files); | ||
// We're good to go check! Get all the files we need | ||
var cachedFiles = values[2]; // files in cache | ||
var oldFiles = self._createFilemap(manifest.files); // files in current manifest | ||
var newFiles = self._createFilemap(newManifest.files); // files in new manifest | ||
var bundledFiles = self._createFilemap(bundledManifest.files); // files in app bundle | ||
// Create the diff | ||
self._toBeDownloaded = Object.keys(newFiles) | ||
// Create COPY and DOWNLOAD lists | ||
self._toBeDownloaded = []; | ||
self._toBeCopied = []; | ||
self._toBeDeleted= []; | ||
var isCordova = self.cache._fs.isCordova; | ||
Object.keys(newFiles) | ||
// Find files that have changed version or are missing | ||
.filter(function(file){ | ||
return !oldFiles[file] | ||
|| oldFiles[file].version !== newFiles[file].version | ||
|| !self.cache.isCached(file); | ||
// if new file, or... | ||
return !oldFiles[file] || | ||
// version has changed, or... | ||
oldFiles[file].version !== newFiles[file].version || | ||
// not in cache for some reason | ||
!self.cache.isCached(file); | ||
}) | ||
// Add them to the correct list | ||
.forEach(function(file){ | ||
// bundled version matches new version, so we can copy! | ||
if(isCordova && bundledFiles[file] && bundledFiles[file].version === newFiles[file].version){ | ||
self._toBeCopied.push(file); | ||
// othwerwise, we must download | ||
} else { | ||
self._toBeDownloaded.push(file); | ||
} | ||
}); | ||
self.cache.list().then(function(files){ | ||
self._toBeDeleted = files | ||
.map(function(file){ | ||
return file.substr(self.cache.localRoot.length); | ||
}) | ||
.filter(function(file){ | ||
return !newFiles[file]; | ||
}) | ||
.concat(self._toBeDownloaded); | ||
// Delete files | ||
self._toBeDeleted = cachedFiles | ||
.map(function(file){ | ||
return file.substr(self.cache.localRoot.length); | ||
}) | ||
.filter(function(file){ | ||
// Everything that is not in new manifest, or.... | ||
return !newFiles[file] || | ||
// Files that will be downloaded, or... | ||
self._toBeDownloaded.indexOf(file) >= 0 || | ||
// Files that will be copied | ||
self._toBeCopied.indexOf(file) >= 0; | ||
}); | ||
if(self._toBeDeleted.length > 0 || self._toBeDownloaded.length > 0){ | ||
// Save the new Manifest | ||
self.newManifest = newManifest; | ||
self.newManifest.root = self.cache.localInternalURL; | ||
resolve(true); | ||
} else { | ||
resolve(false); | ||
} | ||
},reject); | ||
},reject); | ||
} | ||
if(typeof newManifest === "object") { | ||
checkManifest(newManifest); | ||
} else { | ||
pegasus(self.newManifestUrl).then(checkManifest,reject); | ||
setTimeout(function(){reject(new Error('timeout'));},self._checkTimeout); | ||
} | ||
}); | ||
var changes = self._toBeDeleted.length + self._toBeDownloaded.length; | ||
// Note: if we only need to copy files, we can keep serving from bundle! | ||
// So no update is needed! | ||
if(changes > 0){ | ||
// Save the new Manifest | ||
self.newManifest = newManifest; | ||
self.newManifest.root = self.cache.localInternalURL; | ||
resolve(true); | ||
} else { | ||
resolve(false); | ||
} | ||
}); // end of .then | ||
}); // end of new Promise | ||
}; | ||
@@ -137,2 +221,8 @@ | ||
.then(function(){ | ||
return Promise.all(self._toBeCopied.map(self.copyFromBundle.bind(self))); | ||
}) | ||
.then(function(){ | ||
if(self.allowServerRootFromManifest && self.newManifest.serverRoot){ | ||
self.cache.serverRoot = self.newManifest.serverRoot; | ||
} | ||
self.cache.add(self._toBeDownloaded); | ||
@@ -165,2 +255,3 @@ return self.cache.download(onprogress); | ||
AppLoader.prototype.clear = function(){ | ||
localStorage.removeItem('last_update_files'); | ||
localStorage.removeItem('manifest'); | ||
@@ -167,0 +258,0 @@ return this.cache.clear(); |
{ | ||
"name": "cordova-app-loader", | ||
"version": "0.10.0", | ||
"version": "0.11.0", | ||
"description": "Cordova App Loader - remote update your cordova app", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -306,2 +306,6 @@ cordova-app-loader | ||
### Avoid downloading if you can copy files | ||
When updating, copy files that are already bundled with the app. (Of course, only if the file version has not changed) | ||
### Responsive app: Avoid never-resolving promises | ||
@@ -372,2 +376,3 @@ | ||
### More to be considered? | ||
@@ -377,10 +382,13 @@ | ||
## TODO | ||
## TODO for VERSION 1.0.0 | ||
* Create a demo for **autoupdate.js** | ||
* TODO: Should `check()` reject instead of resolve false when the new manifest is the same as the last updated manifest? | ||
* Write automated tests | ||
## Changelog | ||
### 0.11.0 (21/12/2014) | ||
* You can now set `serverRoot` from Manifest.json (only if you set `allowServerRootFromManifest` to `true`). | ||
* AppLoader will copy files from bundle when possible. | ||
### 0.10.0 (02/12/2014) | ||
@@ -387,0 +395,0 @@ |
var fs = new CordovaPromiseFS({ persistent: typeof cordova !== 'undefined' }); | ||
var SERVER = 'http://data.madebymark.nl/cordova-app-loader/'; | ||
//var SERVER = 'http://localhost:8181/'; | ||
if(location.host === 'localhost:8080'){ | ||
SERVER = 'http://localhost:8080/'; | ||
} | ||
@@ -6,0 +8,0 @@ var loader = window.loader = new CordovaAppLoader({ |
@@ -48,4 +48,4 @@ /******/ (function(modules) { // webpackBootstrap | ||
window.CordovaAppLoader = __webpack_require__(1); | ||
window.CordovaFileCache = __webpack_require__(2); | ||
window.CordovaPromiseFS = __webpack_require__(3); | ||
window.CordovaFileCache = __webpack_require__(3); | ||
window.CordovaPromiseFS = __webpack_require__(2); | ||
window.Promise = __webpack_require__(4); | ||
@@ -58,5 +58,9 @@ window.setImmediate = window.setTimeout; // for promiscuous to work! | ||
var CordovaFileCache = __webpack_require__(2); | ||
var CordovaFileCache = __webpack_require__(3); | ||
var CordovaPromiseFS = __webpack_require__(2); | ||
var Promise = null; | ||
var BUNDLED_FILE_SYSTEM_TYPE = 8; | ||
var BUNDLE_ROOT = 'www/'; | ||
function AppLoader(options){ | ||
@@ -67,2 +71,3 @@ if(!options) throw new Error('CordovaAppLoader has no options!'); | ||
if(!window.pegasus || !window.Manifest) throw new Error('CordovaAppLoader bootstrap.js is missing.'); | ||
this.allowServerRootFromManifest = options.allowServerRootFromManifest === true; | ||
Promise = options.fs.Promise; | ||
@@ -73,2 +78,4 @@ | ||
this.newManifest = null; | ||
this.bundledManifest = null; | ||
this.bundledFS = null; | ||
this._lastUpdateFiles = localStorage.getItem('last_update_files'); | ||
@@ -79,3 +86,3 @@ | ||
if(!!options.serverRoot && options.serverRoot[options.serverRoot.length-1] !== '/') options.serverRoot += '/'; | ||
this.newManifestUrl = options.serverRoot + (options.manifest || 'manifest.json'); | ||
this.newManifestUrl = options.manifestUrl || options.serverRoot + (options.manifest || 'manifest.json'); | ||
@@ -88,2 +95,3 @@ // initialize a file cache | ||
this.corruptNewManifest = false; | ||
this._toBeCopied = []; | ||
this._toBeDeleted = []; | ||
@@ -105,24 +113,79 @@ this._toBeDownloaded = []; | ||
AppLoader.prototype.getBundledManifest = function(){ | ||
var self = this; | ||
var bootstrapScript = document.querySelector('script[manifest]'); | ||
var bundledManifestUrl = (bootstrapScript? bootstrapScript.getAttribute('manifest'): null) || 'manifest.json'; | ||
return new Promise(function(resolve,reject){ | ||
if(self.bundledManifest) { | ||
resolve(self.bundledManifest); | ||
} else { | ||
pegasus(bundledManifestUrl).then(function(bundledManifest){ | ||
self.bundledManifest = bundledManifest; | ||
resolve(bundledManifest); | ||
},reject); | ||
setTimeout(function(){reject(new Error('bundled manifest timeout'));},self._checkTimeout); | ||
} | ||
}); | ||
}; | ||
AppLoader.prototype.getBundledFS = function(){ | ||
if(!self.bundledFS){ | ||
self.bundledFS = CordovaPromiseFS({ fileSystem: BUNDLED_FILE_SYSTEM_TYPE }); | ||
} | ||
return self.bundledFS; | ||
}; | ||
AppLoader.prototype.copyFromBundle = function(file){ | ||
var self = this; | ||
return new Promise(function(resolve,reject){ | ||
var bundledFS = self.getBundledFS(); | ||
var cacheFS = self.cache._fs; | ||
var destDirPath = cacheFS.dirname(self.cache.localRoot + file); | ||
var srcFilePath = BUNDLE_ROOT + file; | ||
var srcFilename = cacheFS.filename(file); | ||
return Promise.all([bundledFS.file(srcFilePath),cacheFS.ensure(destDirPath)]) | ||
.then(function(val){ | ||
var srcFile = val[0], destDir = val[1]; | ||
srcFile.copyTo(destDir,srcFilename,resolve,reject); | ||
},reject); | ||
}); | ||
}; | ||
AppLoader.prototype.check = function(newManifest){ | ||
var self = this, manifest = this.manifest; | ||
if(typeof newManifest === "string") { | ||
self.newManifestUrl = newManifest; | ||
newManifest = undefined; | ||
} | ||
return new Promise(function(resolve,reject){ | ||
if(typeof newManifest === "string") { | ||
self.newManifestUrl = newManifest; | ||
newManifest = undefined; | ||
var gotNewManifest = new Promise(function(resolve,reject){ | ||
if(typeof newManifest === "object") { | ||
resolve(newManifest); | ||
} else { | ||
console.log('checking new manifest:',self.newManifestUrl); | ||
pegasus(self.newManifestUrl).then(resolve,reject); | ||
setTimeout(function(){reject(new Error('new manifest timeout'));},self._checkTimeout); | ||
} | ||
}); | ||
function checkManifest(newManifest){ | ||
if(JSON.stringify(newManifest.files) === self._lastUpdateFiles) { | ||
if(JSON.stringify(newManifest.files) !== JSON.stringify(Manifest.files)){ | ||
console.warn('New manifest available, but an earlier update attempt failed. Will not download.'); | ||
self.corruptNewManifest = true; | ||
resolve(null); | ||
return new Promise(function(resolve,reject){ | ||
Promise.all([gotNewManifest,self.getBundledManifest(),self.cache.list()]) | ||
.then(function(values){ | ||
var newManifest = values[0]; | ||
var bundledManifest = values[1]; | ||
// Prevent end-less update loop, check if new manifest | ||
// has been downloaded before (but failes) | ||
if(JSON.stringify(newManifest.files) === self._lastUpdateFiles) { | ||
if(JSON.stringify(newManifest.files) !== JSON.stringify(Manifest.files)){ | ||
console.warn('New manifest available, but an earlier update attempt failed. Will not download.'); | ||
self.corruptNewManifest = true; | ||
resolve(null); | ||
} | ||
resolve(false); | ||
return; | ||
} | ||
resolve(false); | ||
return; | ||
} | ||
// make sure cache is ready for the DIFF operations! | ||
self.cache.ready.then(function(list){ | ||
// Check if new manifest is valid | ||
if(!newManifest.files){ | ||
@@ -133,41 +196,62 @@ reject('Downloaded Manifest has no "files" attribute.'); | ||
var newFiles = self._createFilemap(newManifest.files); | ||
var oldFiles = self._createFilemap(manifest.files); | ||
// We're good to go check! Get all the files we need | ||
var cachedFiles = values[2]; // files in cache | ||
var oldFiles = self._createFilemap(manifest.files); // files in current manifest | ||
var newFiles = self._createFilemap(newManifest.files); // files in new manifest | ||
var bundledFiles = self._createFilemap(bundledManifest.files); // files in app bundle | ||
// Create the diff | ||
self._toBeDownloaded = Object.keys(newFiles) | ||
// Create COPY and DOWNLOAD lists | ||
self._toBeDownloaded = []; | ||
self._toBeCopied = []; | ||
self._toBeDeleted= []; | ||
var isCordova = self.cache._fs.isCordova; | ||
Object.keys(newFiles) | ||
// Find files that have changed version or are missing | ||
.filter(function(file){ | ||
return !oldFiles[file] | ||
|| oldFiles[file].version !== newFiles[file].version | ||
|| !self.cache.isCached(file); | ||
// if new file, or... | ||
return !oldFiles[file] || | ||
// version has changed, or... | ||
oldFiles[file].version !== newFiles[file].version || | ||
// not in cache for some reason | ||
!self.cache.isCached(file); | ||
}) | ||
// Add them to the correct list | ||
.forEach(function(file){ | ||
// bundled version matches new version, so we can copy! | ||
if(isCordova && bundledFiles[file] && bundledFiles[file].version === newFiles[file].version){ | ||
self._toBeCopied.push(file); | ||
// othwerwise, we must download | ||
} else { | ||
self._toBeDownloaded.push(file); | ||
} | ||
}); | ||
self.cache.list().then(function(files){ | ||
self._toBeDeleted = files | ||
.map(function(file){ | ||
return file.substr(self.cache.localRoot.length); | ||
}) | ||
.filter(function(file){ | ||
return !newFiles[file]; | ||
}) | ||
.concat(self._toBeDownloaded); | ||
// Delete files | ||
self._toBeDeleted = cachedFiles | ||
.map(function(file){ | ||
return file.substr(self.cache.localRoot.length); | ||
}) | ||
.filter(function(file){ | ||
// Everything that is not in new manifest, or.... | ||
return !newFiles[file] || | ||
// Files that will be downloaded, or... | ||
self._toBeDownloaded.indexOf(file) >= 0 || | ||
// Files that will be copied | ||
self._toBeCopied.indexOf(file) >= 0; | ||
}); | ||
if(self._toBeDeleted.length > 0 || self._toBeDownloaded.length > 0){ | ||
// Save the new Manifest | ||
self.newManifest = newManifest; | ||
self.newManifest.root = self.cache.localInternalURL; | ||
resolve(true); | ||
} else { | ||
resolve(false); | ||
} | ||
},reject); | ||
},reject); | ||
} | ||
if(typeof newManifest === "object") { | ||
checkManifest(newManifest); | ||
} else { | ||
pegasus(self.newManifestUrl).then(checkManifest,reject); | ||
setTimeout(function(){reject(new Error('timeout'));},self._checkTimeout); | ||
} | ||
}); | ||
var changes = self._toBeDeleted.length + self._toBeDownloaded.length; | ||
// Note: if we only need to copy files, we can keep serving from bundle! | ||
// So no update is needed! | ||
if(changes > 0){ | ||
// Save the new Manifest | ||
self.newManifest = newManifest; | ||
self.newManifest.root = self.cache.localInternalURL; | ||
resolve(true); | ||
} else { | ||
resolve(false); | ||
} | ||
}); // end of .then | ||
}); // end of new Promise | ||
}; | ||
@@ -195,2 +279,8 @@ | ||
.then(function(){ | ||
return Promise.all(self._toBeCopied.map(self.copyFromBundle.bind(self))); | ||
}) | ||
.then(function(){ | ||
if(self.allowServerRootFromManifest && self.newManifest.serverRoot){ | ||
self.cache.serverRoot = self.newManifest.serverRoot; | ||
} | ||
self.cache.add(self._toBeDownloaded); | ||
@@ -223,2 +313,3 @@ return self.cache.download(onprogress); | ||
AppLoader.prototype.clear = function(){ | ||
localStorage.removeItem('last_update_files'); | ||
localStorage.removeItem('manifest'); | ||
@@ -242,261 +333,3 @@ return this.cache.clear(); | ||
var hash = __webpack_require__(5); | ||
var Promise = null; | ||
var isCordova = typeof cordova !== 'undefined'; | ||
/* Cordova File Cache x */ | ||
function FileCache(options){ | ||
var self = this; | ||
// cordova-promise-fs | ||
this._fs = options.fs; | ||
if(!this._fs) { | ||
throw new Error('Missing required option "fs". Add an instance of cordova-promise-fs.'); | ||
} | ||
// Use Promises from fs. | ||
Promise = this._fs.Promise; | ||
// 'mirror' mirrors files structure from "serverRoot" to "localRoot" | ||
// 'hash' creates a 1-deep filestructure, where the filenames are hashed server urls (with extension) | ||
this._mirrorMode = options.mode !== 'hash'; | ||
this._retry = options.retry || [500,1500,8000]; | ||
this._cacheBuster = !!options.cacheBuster; | ||
// normalize path | ||
this.localRoot = this._fs.normalize(options.localRoot || 'data'); | ||
this.serverRoot = this._fs.normalize(options.serverRoot || ''); | ||
// set internal variables | ||
this._downloading = []; // download promises | ||
this._added = []; // added files | ||
this._cached = {}; // cached files | ||
// list existing cache contents | ||
this.ready = this._fs.ensure(this.localRoot) | ||
.then(function(entry){ | ||
self.localInternalURL = isCordova? entry.toInternalURL(): entry.toURL(); | ||
self.localUrl = entry.toURL(); | ||
return self.list(); | ||
}); | ||
} | ||
/** | ||
* Helper to cache all 'internalURL' and 'URL' for quick synchronous access | ||
* to the cached files. | ||
*/ | ||
FileCache.prototype.list = function list(){ | ||
var self = this; | ||
return new Promise(function(resolve,reject){ | ||
self._fs.list(self.localRoot,'rfe').then(function(entries){ | ||
self._cached = {}; | ||
entries = entries.map(function(entry){ | ||
var fullPath = self._fs.normalize(entry.fullPath); | ||
self._cached[fullPath] = { | ||
toInternalURL: isCordova? entry.toInternalURL(): entry.toURL(), | ||
toURL: entry.toURL(), | ||
}; | ||
return fullPath; | ||
}); | ||
resolve(entries); | ||
},function(){ | ||
resolve([]); | ||
}); | ||
}); | ||
}; | ||
FileCache.prototype.add = function add(urls){ | ||
if(!urls) urls = []; | ||
if(typeof urls === 'string') urls = [urls]; | ||
var self = this; | ||
urls.forEach(function(url){ | ||
url = self.toServerURL(url); | ||
if(self._added.indexOf(url) === -1) { | ||
self._added.push(url); | ||
} | ||
}); | ||
return self.isDirty(); | ||
}; | ||
FileCache.prototype.remove = function remove(urls,returnPromises){ | ||
if(!urls) urls = []; | ||
var promises = []; | ||
if(typeof urls === 'string') urls = [urls]; | ||
var self = this; | ||
urls.forEach(function(url){ | ||
var index = self._added.indexOf(self.toServerURL(url)); | ||
if(index >= 0) self._added.splice(index,1); | ||
var path = self.toPath(url); | ||
promises.push(self._fs.remove(path)); | ||
delete self._cached[path]; | ||
}); | ||
return returnPromises? Promise.all(promises): self.isDirty(); | ||
}; | ||
FileCache.prototype.getDownloadQueue = function(){ | ||
var self = this; | ||
var queue = self._added.filter(function(url){ | ||
return !self.isCached(url); | ||
}); | ||
return queue; | ||
}; | ||
FileCache.prototype.getAdded = function() { | ||
return this._added; | ||
}; | ||
FileCache.prototype.isDirty = function isDirty(){ | ||
return this.getDownloadQueue().length > 0; | ||
}; | ||
FileCache.prototype.download = function download(onprogress){ | ||
var fs = this._fs; | ||
var self = this; | ||
self.abort(); | ||
return new Promise(function(resolve,reject){ | ||
// make sure cache directory exists and that | ||
// we have retrieved the latest cache contents | ||
// to avoid downloading files we already have! | ||
fs.ensure(self.localRoot).then(function(){ | ||
return self.list(); | ||
}).then(function(){ | ||
// no dowloads needed, resolve | ||
if(!self.isDirty()) { | ||
resolve(self); | ||
return; | ||
} | ||
// keep track of number of downloads! | ||
var queue = self.getDownloadQueue(); | ||
var started = []; | ||
var index = self._downloading.length; | ||
var done = self._downloading.length; | ||
var total = self._downloading.length + queue.length; | ||
// download every file in the queue (which is the diff from _added with _cached) | ||
queue.forEach(function(url){ | ||
var path = self.toPath(url); | ||
// augment progress event with index/total stats | ||
var onSingleDownloadProgress; | ||
if(typeof onprogress === 'function') { | ||
onSingleDownloadProgress = function(ev){ | ||
ev.queueIndex = index; | ||
ev.queueSize = total; | ||
ev.url = url; | ||
ev.path = path; | ||
ev.percentage = index / total; | ||
if(ev.loaded > 0 && ev.total > 0 && index !== total){ | ||
ev.percentage += (ev.loaded / ev.total) / total; | ||
} | ||
if(started.indexOf(url) < 0) { | ||
started.push(url); | ||
index++; | ||
} | ||
onprogress(ev); | ||
}; | ||
} | ||
// callback | ||
var onDone = function(){ | ||
done++; | ||
// when we're done | ||
if(done === total) { | ||
// reset downloads | ||
self._downloading = []; | ||
// check if we got everything | ||
self.list().then(function(){ | ||
// final progress event! | ||
if(onSingleDownloadProgress) onSingleDownloadProgress(new ProgressEvent()); | ||
// Yes, we're not dirty anymore! | ||
if(!self.isDirty()) { | ||
resolve(self); | ||
// Aye, some files got left behind! | ||
} else { | ||
reject(self.getDownloadQueue()); | ||
} | ||
},reject); | ||
} | ||
}; | ||
var downloadUrl = url; | ||
if(self._cacheBuster) downloadUrl += "?"+Date.now(); | ||
var download = fs.download(url,path,{retry:self._retry},onSingleDownloadProgress); | ||
download.then(onDone,onDone); | ||
self._downloading.push(download); | ||
}); | ||
},reject); | ||
}); | ||
}; | ||
FileCache.prototype.abort = function abort(){ | ||
this._downloading.forEach(function(download){ | ||
download.abort(); | ||
}); | ||
this._downloading = []; | ||
}; | ||
FileCache.prototype.isCached = function isCached(url){ | ||
url = this.toPath(url); | ||
return !!this._cached[url]; | ||
}; | ||
FileCache.prototype.clear = function clear(){ | ||
var self = this; | ||
this._cached = {}; | ||
return this._fs.removeDir(this.localRoot).then(function(){ | ||
return self._fs.ensure(self.localRoot); | ||
}); | ||
}; | ||
/** | ||
* Helpers to output to various formats | ||
*/ | ||
FileCache.prototype.toInternalURL = function toInternalURL(url){ | ||
path = this.toPath(url); | ||
if(this._cached[path]) return this._cached[path].toInternalURL; | ||
return url; | ||
}; | ||
FileCache.prototype.get = function get(url){ | ||
path = this.toPath(url); | ||
if(this._cached[path]) return this._cached[path].toInternalURL; | ||
return this.toServerURL(url); | ||
}; | ||
FileCache.prototype.toDataURL = function toDataURL(url){ | ||
return this._fs.toDataURL(this.toPath(url)); | ||
}; | ||
FileCache.prototype.toURL = function toURL(url){ | ||
path = this.toPath(url); | ||
return this._cached[path]? this._cached[path].toURL: url; | ||
}; | ||
FileCache.prototype.toServerURL = function toServerURL(path){ | ||
path = this._fs.normalize(path); | ||
return path.indexOf('://') < 0? this.serverRoot + path: path; | ||
}; | ||
/** | ||
* Helper to transform remote URL to a local path (for cordova-promise-fs) | ||
*/ | ||
FileCache.prototype.toPath = function toPath(url){ | ||
if(this._mirrorMode) { | ||
url = url = this._fs.normalize(url || ''); | ||
len = this.serverRoot.length; | ||
if(url.substr(0,len) !== this.serverRoot) { | ||
return this.localRoot + url; | ||
} else { | ||
return this.localRoot + url.substr(len); | ||
} | ||
} else { | ||
return this.localRoot + hash(url) + url.substr(url.lastIndexOf('.')); | ||
} | ||
}; | ||
module.exports = FileCache; | ||
/***/ }, | ||
/* 3 */ | ||
/***/ function(module, exports, __webpack_require__) { | ||
/** | ||
* Static Private functions | ||
@@ -528,2 +361,3 @@ */ | ||
function normalize(str){ | ||
str = str || ''; | ||
if(str[0] === '/') str = str.substr(1); | ||
@@ -600,3 +434,12 @@ if(!!str && str.indexOf('.') < 0 && str[str.length-1] !== '/') str += '/'; | ||
deviceready.then(function(){ | ||
window.requestFileSystem(options.persistent? 1: 0, options.storageSize, resolve, reject); | ||
var type = options.persistent? 1: 0; | ||
if(typeof options.fileSystem === 'number'){ | ||
type = options.fileSystem; | ||
} | ||
// Chrome only supports persistent and temp storage, not the exotic onces from Cordova | ||
if(!isCordova && type > 1) { | ||
console.warn('Chrome does not support fileSystem "'+type+'". Falling back on "0" (temporary).'); | ||
type = 0; | ||
} | ||
window.requestFileSystem(type, options.storageSize, resolve, reject); | ||
setTimeout(function(){ reject(new Error('Could not retrieve FileSystem after 5 seconds.')); },5100); | ||
@@ -631,5 +474,8 @@ },reject); | ||
function file(path,options){ | ||
path = normalize(path); | ||
options = options || {}; | ||
return new Promise(function(resolve,reject){ | ||
if(path instanceof FileEntry) { | ||
return resolve(path); | ||
} | ||
path = normalize(path); | ||
options = options || {}; | ||
return fs.then(function(fs){ | ||
@@ -973,2 +819,260 @@ fs.root.getFile(path,options,resolve,reject); | ||
/***/ }, | ||
/* 3 */ | ||
/***/ function(module, exports, __webpack_require__) { | ||
var hash = __webpack_require__(5); | ||
var Promise = null; | ||
var isCordova = typeof cordova !== 'undefined'; | ||
/* Cordova File Cache x */ | ||
function FileCache(options){ | ||
var self = this; | ||
// cordova-promise-fs | ||
this._fs = options.fs; | ||
if(!this._fs) { | ||
throw new Error('Missing required option "fs". Add an instance of cordova-promise-fs.'); | ||
} | ||
// Use Promises from fs. | ||
Promise = this._fs.Promise; | ||
// 'mirror' mirrors files structure from "serverRoot" to "localRoot" | ||
// 'hash' creates a 1-deep filestructure, where the filenames are hashed server urls (with extension) | ||
this._mirrorMode = options.mode !== 'hash'; | ||
this._retry = options.retry || [500,1500,8000]; | ||
this._cacheBuster = !!options.cacheBuster; | ||
// normalize path | ||
this.localRoot = this._fs.normalize(options.localRoot || 'data'); | ||
this.serverRoot = this._fs.normalize(options.serverRoot || ''); | ||
// set internal variables | ||
this._downloading = []; // download promises | ||
this._added = []; // added files | ||
this._cached = {}; // cached files | ||
// list existing cache contents | ||
this.ready = this._fs.ensure(this.localRoot) | ||
.then(function(entry){ | ||
self.localInternalURL = isCordova? entry.toInternalURL(): entry.toURL(); | ||
self.localUrl = entry.toURL(); | ||
return self.list(); | ||
}); | ||
} | ||
/** | ||
* Helper to cache all 'internalURL' and 'URL' for quick synchronous access | ||
* to the cached files. | ||
*/ | ||
FileCache.prototype.list = function list(){ | ||
var self = this; | ||
return new Promise(function(resolve,reject){ | ||
self._fs.list(self.localRoot,'rfe').then(function(entries){ | ||
self._cached = {}; | ||
entries = entries.map(function(entry){ | ||
var fullPath = self._fs.normalize(entry.fullPath); | ||
self._cached[fullPath] = { | ||
toInternalURL: isCordova? entry.toInternalURL(): entry.toURL(), | ||
toURL: entry.toURL(), | ||
}; | ||
return fullPath; | ||
}); | ||
resolve(entries); | ||
},function(){ | ||
resolve([]); | ||
}); | ||
}); | ||
}; | ||
FileCache.prototype.add = function add(urls){ | ||
if(!urls) urls = []; | ||
if(typeof urls === 'string') urls = [urls]; | ||
var self = this; | ||
urls.forEach(function(url){ | ||
url = self.toServerURL(url); | ||
if(self._added.indexOf(url) === -1) { | ||
self._added.push(url); | ||
} | ||
}); | ||
return self.isDirty(); | ||
}; | ||
FileCache.prototype.remove = function remove(urls,returnPromises){ | ||
if(!urls) urls = []; | ||
var promises = []; | ||
if(typeof urls === 'string') urls = [urls]; | ||
var self = this; | ||
urls.forEach(function(url){ | ||
var index = self._added.indexOf(self.toServerURL(url)); | ||
if(index >= 0) self._added.splice(index,1); | ||
var path = self.toPath(url); | ||
promises.push(self._fs.remove(path)); | ||
delete self._cached[path]; | ||
}); | ||
return returnPromises? Promise.all(promises): self.isDirty(); | ||
}; | ||
FileCache.prototype.getDownloadQueue = function(){ | ||
var self = this; | ||
var queue = self._added.filter(function(url){ | ||
return !self.isCached(url); | ||
}); | ||
return queue; | ||
}; | ||
FileCache.prototype.getAdded = function() { | ||
return this._added; | ||
}; | ||
FileCache.prototype.isDirty = function isDirty(){ | ||
return this.getDownloadQueue().length > 0; | ||
}; | ||
FileCache.prototype.download = function download(onprogress){ | ||
var fs = this._fs; | ||
var self = this; | ||
self.abort(); | ||
return new Promise(function(resolve,reject){ | ||
// make sure cache directory exists and that | ||
// we have retrieved the latest cache contents | ||
// to avoid downloading files we already have! | ||
fs.ensure(self.localRoot).then(function(){ | ||
return self.list(); | ||
}).then(function(){ | ||
// no dowloads needed, resolve | ||
if(!self.isDirty()) { | ||
resolve(self); | ||
return; | ||
} | ||
// keep track of number of downloads! | ||
var queue = self.getDownloadQueue(); | ||
var started = []; | ||
var index = self._downloading.length; | ||
var done = self._downloading.length; | ||
var total = self._downloading.length + queue.length; | ||
// download every file in the queue (which is the diff from _added with _cached) | ||
queue.forEach(function(url){ | ||
var path = self.toPath(url); | ||
// augment progress event with index/total stats | ||
var onSingleDownloadProgress; | ||
if(typeof onprogress === 'function') { | ||
onSingleDownloadProgress = function(ev){ | ||
ev.queueIndex = index; | ||
ev.queueSize = total; | ||
ev.url = url; | ||
ev.path = path; | ||
ev.percentage = index / total; | ||
if(ev.loaded > 0 && ev.total > 0 && index !== total){ | ||
ev.percentage += (ev.loaded / ev.total) / total; | ||
} | ||
if(started.indexOf(url) < 0) { | ||
started.push(url); | ||
index++; | ||
} | ||
onprogress(ev); | ||
}; | ||
} | ||
// callback | ||
var onDone = function(){ | ||
done++; | ||
// when we're done | ||
if(done === total) { | ||
// reset downloads | ||
self._downloading = []; | ||
// check if we got everything | ||
self.list().then(function(){ | ||
// final progress event! | ||
if(onSingleDownloadProgress) onSingleDownloadProgress(new ProgressEvent()); | ||
// Yes, we're not dirty anymore! | ||
if(!self.isDirty()) { | ||
resolve(self); | ||
// Aye, some files got left behind! | ||
} else { | ||
reject(self.getDownloadQueue()); | ||
} | ||
},reject); | ||
} | ||
}; | ||
var downloadUrl = url; | ||
if(self._cacheBuster) downloadUrl += "?"+Date.now(); | ||
var download = fs.download(downloadUrl,path,{retry:self._retry},onSingleDownloadProgress); | ||
download.then(onDone,onDone); | ||
self._downloading.push(download); | ||
}); | ||
},reject); | ||
}); | ||
}; | ||
FileCache.prototype.abort = function abort(){ | ||
this._downloading.forEach(function(download){ | ||
download.abort(); | ||
}); | ||
this._downloading = []; | ||
}; | ||
FileCache.prototype.isCached = function isCached(url){ | ||
url = this.toPath(url); | ||
return !!this._cached[url]; | ||
}; | ||
FileCache.prototype.clear = function clear(){ | ||
var self = this; | ||
this._cached = {}; | ||
return this._fs.removeDir(this.localRoot).then(function(){ | ||
return self._fs.ensure(self.localRoot); | ||
}); | ||
}; | ||
/** | ||
* Helpers to output to various formats | ||
*/ | ||
FileCache.prototype.toInternalURL = function toInternalURL(url){ | ||
path = this.toPath(url); | ||
if(this._cached[path]) return this._cached[path].toInternalURL; | ||
return url; | ||
}; | ||
FileCache.prototype.get = function get(url){ | ||
path = this.toPath(url); | ||
if(this._cached[path]) return this._cached[path].toInternalURL; | ||
return this.toServerURL(url); | ||
}; | ||
FileCache.prototype.toDataURL = function toDataURL(url){ | ||
return this._fs.toDataURL(this.toPath(url)); | ||
}; | ||
FileCache.prototype.toURL = function toURL(url){ | ||
path = this.toPath(url); | ||
return this._cached[path]? this._cached[path].toURL: url; | ||
}; | ||
FileCache.prototype.toServerURL = function toServerURL(path){ | ||
path = this._fs.normalize(path); | ||
return path.indexOf('://') < 0? this.serverRoot + path: path; | ||
}; | ||
/** | ||
* Helper to transform remote URL to a local path (for cordova-promise-fs) | ||
*/ | ||
FileCache.prototype.toPath = function toPath(url){ | ||
if(this._mirrorMode) { | ||
url = url = this._fs.normalize(url || ''); | ||
len = this.serverRoot.length; | ||
if(url.substr(0,len) !== this.serverRoot) { | ||
return this.localRoot + url; | ||
} else { | ||
return this.localRoot + url.substr(len); | ||
} | ||
} else { | ||
return this.localRoot + hash(url) + url.substr(url.lastIndexOf('.')); | ||
} | ||
}; | ||
module.exports = FileCache; | ||
/***/ }, | ||
/* 4 */ | ||
@@ -975,0 +1079,0 @@ /***/ function(module, exports, __webpack_require__) { |
@@ -49,4 +49,8 @@ var CordovaAppLoader = | ||
var CordovaFileCache = __webpack_require__(1); | ||
var CordovaPromiseFS = __webpack_require__(2); | ||
var Promise = null; | ||
var BUNDLED_FILE_SYSTEM_TYPE = 8; | ||
var BUNDLE_ROOT = 'www/'; | ||
function AppLoader(options){ | ||
@@ -57,2 +61,3 @@ if(!options) throw new Error('CordovaAppLoader has no options!'); | ||
if(!window.pegasus || !window.Manifest) throw new Error('CordovaAppLoader bootstrap.js is missing.'); | ||
this.allowServerRootFromManifest = options.allowServerRootFromManifest === true; | ||
Promise = options.fs.Promise; | ||
@@ -63,2 +68,4 @@ | ||
this.newManifest = null; | ||
this.bundledManifest = null; | ||
this.bundledFS = null; | ||
this._lastUpdateFiles = localStorage.getItem('last_update_files'); | ||
@@ -69,3 +76,3 @@ | ||
if(!!options.serverRoot && options.serverRoot[options.serverRoot.length-1] !== '/') options.serverRoot += '/'; | ||
this.newManifestUrl = options.serverRoot + (options.manifest || 'manifest.json'); | ||
this.newManifestUrl = options.manifestUrl || options.serverRoot + (options.manifest || 'manifest.json'); | ||
@@ -78,2 +85,3 @@ // initialize a file cache | ||
this.corruptNewManifest = false; | ||
this._toBeCopied = []; | ||
this._toBeDeleted = []; | ||
@@ -95,24 +103,79 @@ this._toBeDownloaded = []; | ||
AppLoader.prototype.getBundledManifest = function(){ | ||
var self = this; | ||
var bootstrapScript = document.querySelector('script[manifest]'); | ||
var bundledManifestUrl = (bootstrapScript? bootstrapScript.getAttribute('manifest'): null) || 'manifest.json'; | ||
return new Promise(function(resolve,reject){ | ||
if(self.bundledManifest) { | ||
resolve(self.bundledManifest); | ||
} else { | ||
pegasus(bundledManifestUrl).then(function(bundledManifest){ | ||
self.bundledManifest = bundledManifest; | ||
resolve(bundledManifest); | ||
},reject); | ||
setTimeout(function(){reject(new Error('bundled manifest timeout'));},self._checkTimeout); | ||
} | ||
}); | ||
}; | ||
AppLoader.prototype.getBundledFS = function(){ | ||
if(!self.bundledFS){ | ||
self.bundledFS = CordovaPromiseFS({ fileSystem: BUNDLED_FILE_SYSTEM_TYPE }); | ||
} | ||
return self.bundledFS; | ||
}; | ||
AppLoader.prototype.copyFromBundle = function(file){ | ||
var self = this; | ||
return new Promise(function(resolve,reject){ | ||
var bundledFS = self.getBundledFS(); | ||
var cacheFS = self.cache._fs; | ||
var destDirPath = cacheFS.dirname(self.cache.localRoot + file); | ||
var srcFilePath = BUNDLE_ROOT + file; | ||
var srcFilename = cacheFS.filename(file); | ||
return Promise.all([bundledFS.file(srcFilePath),cacheFS.ensure(destDirPath)]) | ||
.then(function(val){ | ||
var srcFile = val[0], destDir = val[1]; | ||
srcFile.copyTo(destDir,srcFilename,resolve,reject); | ||
},reject); | ||
}); | ||
}; | ||
AppLoader.prototype.check = function(newManifest){ | ||
var self = this, manifest = this.manifest; | ||
if(typeof newManifest === "string") { | ||
self.newManifestUrl = newManifest; | ||
newManifest = undefined; | ||
} | ||
return new Promise(function(resolve,reject){ | ||
if(typeof newManifest === "string") { | ||
self.newManifestUrl = newManifest; | ||
newManifest = undefined; | ||
var gotNewManifest = new Promise(function(resolve,reject){ | ||
if(typeof newManifest === "object") { | ||
resolve(newManifest); | ||
} else { | ||
console.log('checking new manifest:',self.newManifestUrl); | ||
pegasus(self.newManifestUrl).then(resolve,reject); | ||
setTimeout(function(){reject(new Error('new manifest timeout'));},self._checkTimeout); | ||
} | ||
}); | ||
function checkManifest(newManifest){ | ||
if(JSON.stringify(newManifest.files) === self._lastUpdateFiles) { | ||
if(JSON.stringify(newManifest.files) !== JSON.stringify(Manifest.files)){ | ||
console.warn('New manifest available, but an earlier update attempt failed. Will not download.'); | ||
self.corruptNewManifest = true; | ||
resolve(null); | ||
return new Promise(function(resolve,reject){ | ||
Promise.all([gotNewManifest,self.getBundledManifest(),self.cache.list()]) | ||
.then(function(values){ | ||
var newManifest = values[0]; | ||
var bundledManifest = values[1]; | ||
// Prevent end-less update loop, check if new manifest | ||
// has been downloaded before (but failes) | ||
if(JSON.stringify(newManifest.files) === self._lastUpdateFiles) { | ||
if(JSON.stringify(newManifest.files) !== JSON.stringify(Manifest.files)){ | ||
console.warn('New manifest available, but an earlier update attempt failed. Will not download.'); | ||
self.corruptNewManifest = true; | ||
resolve(null); | ||
} | ||
resolve(false); | ||
return; | ||
} | ||
resolve(false); | ||
return; | ||
} | ||
// make sure cache is ready for the DIFF operations! | ||
self.cache.ready.then(function(list){ | ||
// Check if new manifest is valid | ||
if(!newManifest.files){ | ||
@@ -123,41 +186,62 @@ reject('Downloaded Manifest has no "files" attribute.'); | ||
var newFiles = self._createFilemap(newManifest.files); | ||
var oldFiles = self._createFilemap(manifest.files); | ||
// We're good to go check! Get all the files we need | ||
var cachedFiles = values[2]; // files in cache | ||
var oldFiles = self._createFilemap(manifest.files); // files in current manifest | ||
var newFiles = self._createFilemap(newManifest.files); // files in new manifest | ||
var bundledFiles = self._createFilemap(bundledManifest.files); // files in app bundle | ||
// Create the diff | ||
self._toBeDownloaded = Object.keys(newFiles) | ||
// Create COPY and DOWNLOAD lists | ||
self._toBeDownloaded = []; | ||
self._toBeCopied = []; | ||
self._toBeDeleted= []; | ||
var isCordova = self.cache._fs.isCordova; | ||
Object.keys(newFiles) | ||
// Find files that have changed version or are missing | ||
.filter(function(file){ | ||
return !oldFiles[file] | ||
|| oldFiles[file].version !== newFiles[file].version | ||
|| !self.cache.isCached(file); | ||
// if new file, or... | ||
return !oldFiles[file] || | ||
// version has changed, or... | ||
oldFiles[file].version !== newFiles[file].version || | ||
// not in cache for some reason | ||
!self.cache.isCached(file); | ||
}) | ||
// Add them to the correct list | ||
.forEach(function(file){ | ||
// bundled version matches new version, so we can copy! | ||
if(isCordova && bundledFiles[file] && bundledFiles[file].version === newFiles[file].version){ | ||
self._toBeCopied.push(file); | ||
// othwerwise, we must download | ||
} else { | ||
self._toBeDownloaded.push(file); | ||
} | ||
}); | ||
self.cache.list().then(function(files){ | ||
self._toBeDeleted = files | ||
.map(function(file){ | ||
return file.substr(self.cache.localRoot.length); | ||
}) | ||
.filter(function(file){ | ||
return !newFiles[file]; | ||
}) | ||
.concat(self._toBeDownloaded); | ||
// Delete files | ||
self._toBeDeleted = cachedFiles | ||
.map(function(file){ | ||
return file.substr(self.cache.localRoot.length); | ||
}) | ||
.filter(function(file){ | ||
// Everything that is not in new manifest, or.... | ||
return !newFiles[file] || | ||
// Files that will be downloaded, or... | ||
self._toBeDownloaded.indexOf(file) >= 0 || | ||
// Files that will be copied | ||
self._toBeCopied.indexOf(file) >= 0; | ||
}); | ||
if(self._toBeDeleted.length > 0 || self._toBeDownloaded.length > 0){ | ||
// Save the new Manifest | ||
self.newManifest = newManifest; | ||
self.newManifest.root = self.cache.localInternalURL; | ||
resolve(true); | ||
} else { | ||
resolve(false); | ||
} | ||
},reject); | ||
},reject); | ||
} | ||
if(typeof newManifest === "object") { | ||
checkManifest(newManifest); | ||
} else { | ||
pegasus(self.newManifestUrl).then(checkManifest,reject); | ||
setTimeout(function(){reject(new Error('timeout'));},self._checkTimeout); | ||
} | ||
}); | ||
var changes = self._toBeDeleted.length + self._toBeDownloaded.length; | ||
// Note: if we only need to copy files, we can keep serving from bundle! | ||
// So no update is needed! | ||
if(changes > 0){ | ||
// Save the new Manifest | ||
self.newManifest = newManifest; | ||
self.newManifest.root = self.cache.localInternalURL; | ||
resolve(true); | ||
} else { | ||
resolve(false); | ||
} | ||
}); // end of .then | ||
}); // end of new Promise | ||
}; | ||
@@ -185,2 +269,8 @@ | ||
.then(function(){ | ||
return Promise.all(self._toBeCopied.map(self.copyFromBundle.bind(self))); | ||
}) | ||
.then(function(){ | ||
if(self.allowServerRootFromManifest && self.newManifest.serverRoot){ | ||
self.cache.serverRoot = self.newManifest.serverRoot; | ||
} | ||
self.cache.add(self._toBeDownloaded); | ||
@@ -213,2 +303,3 @@ return self.cache.download(onprogress); | ||
AppLoader.prototype.clear = function(){ | ||
localStorage.removeItem('last_update_files'); | ||
localStorage.removeItem('manifest'); | ||
@@ -232,3 +323,3 @@ return this.cache.clear(); | ||
var hash = __webpack_require__(2); | ||
var hash = __webpack_require__(3); | ||
var Promise = null; | ||
@@ -411,3 +502,3 @@ var isCordova = typeof cordova !== 'undefined'; | ||
if(self._cacheBuster) downloadUrl += "?"+Date.now(); | ||
var download = fs.download(url,path,{retry:self._retry},onSingleDownloadProgress); | ||
var download = fs.download(downloadUrl,path,{retry:self._retry},onSingleDownloadProgress); | ||
download.then(onDone,onDone); | ||
@@ -493,2 +584,487 @@ self._downloading.push(download); | ||
/** | ||
* Static Private functions | ||
*/ | ||
/* createDir, recursively */ | ||
function __createDir(rootDirEntry, folders, success,error) { | ||
rootDirEntry.getDirectory(folders[0], {create: true}, function(dirEntry) { | ||
// Recursively add the new subfolder (if we still have another to create). | ||
if (folders.length > 1) { | ||
__createDir(dirEntry, folders.slice(1),success,error); | ||
} else { | ||
success(dirEntry); | ||
} | ||
}, error); | ||
} | ||
function dirname(str) { | ||
str = str.substr(0,str.lastIndexOf('/')+1); | ||
if(str[0] === '/') str = str.substr(1); | ||
return str; | ||
} | ||
function filename(str) { | ||
return str.substr(str.lastIndexOf('/')+1); | ||
} | ||
function normalize(str){ | ||
str = str || ''; | ||
if(str[0] === '/') str = str.substr(1); | ||
if(!!str && str.indexOf('.') < 0 && str[str.length-1] !== '/') str += '/'; | ||
if(str === './') str = ''; | ||
return str; | ||
} | ||
var transferQueue = [], // queued fileTransfers | ||
inprogress = 0; // currently active filetransfers | ||
/** | ||
* Factory function: Create a single instance (based on single FileSystem) | ||
*/ | ||
module.exports = function(options){ | ||
/* Promise implementation */ | ||
var Promise = options.Promise || window.Promise; | ||
if(!Promise) { throw new Error("No Promise library given in options.Promise"); } | ||
/* default options */ | ||
this.options = options = options || {}; | ||
options.persistent = options.persistent !== undefined? options.persistent: true; | ||
options.storageSize = options.storageSize || 20*1024*1024; | ||
options.concurrency = options.concurrency || 3; | ||
options.retry = options.retry || []; | ||
/* Cordova deviceready promise */ | ||
var deviceready, isCordova = typeof cordova !== 'undefined'; | ||
if(isCordova){ | ||
deviceready = new Promise(function(resolve,reject){ | ||
document.addEventListener("deviceready", resolve, false); | ||
setTimeout(function(){ reject(new Error('deviceready has not fired after 5 seconds.')); },5100); | ||
}); | ||
} else { | ||
/* FileTransfer implementation for Chrome */ | ||
deviceready = ResolvedPromise(true); | ||
if(typeof webkitRequestFileSystem !== 'undefined'){ | ||
window.requestFileSystem = webkitRequestFileSystem; | ||
window.FileTransfer = function FileTransfer(){}; | ||
FileTransfer.prototype.download = function download(url,file,win,fail) { | ||
var xhr = new XMLHttpRequest(); | ||
xhr.open('GET', url); | ||
xhr.responseType = "blob"; | ||
xhr.onreadystatechange = function(onSuccess, onError, cb) { | ||
if (xhr.readyState == 4) { | ||
if(xhr.status === 200){ | ||
write(file,xhr.response).then(win,fail); | ||
} else { | ||
fail(xhr.status); | ||
} | ||
} | ||
}; | ||
xhr.send(); | ||
return xhr; | ||
}; | ||
window.ProgressEvent = function ProgressEvent(){}; | ||
} else { | ||
window.requestFileSystem = function(x,y,z,fail){ | ||
fail(new Error('requestFileSystem not supported!')); | ||
}; | ||
} | ||
} | ||
/* Promise resolve helper */ | ||
function ResolvedPromise(value){ | ||
return new Promise(function(resolve){ | ||
return resolve(value); | ||
}); | ||
} | ||
/* the filesystem! */ | ||
var fs = new Promise(function(resolve,reject){ | ||
deviceready.then(function(){ | ||
var type = options.persistent? 1: 0; | ||
if(typeof options.fileSystem === 'number'){ | ||
type = options.fileSystem; | ||
} | ||
// Chrome only supports persistent and temp storage, not the exotic onces from Cordova | ||
if(!isCordova && type > 1) { | ||
console.warn('Chrome does not support fileSystem "'+type+'". Falling back on "0" (temporary).'); | ||
type = 0; | ||
} | ||
window.requestFileSystem(type, options.storageSize, resolve, reject); | ||
setTimeout(function(){ reject(new Error('Could not retrieve FileSystem after 5 seconds.')); },5100); | ||
},reject); | ||
}); | ||
/* debug */ | ||
fs.then(function(fs){ | ||
window.__fs = fs; | ||
},function(err){ | ||
console.error('Could not get Cordova FileSystem:',err); | ||
}); | ||
/* ensure directory exists */ | ||
function ensure(folders) { | ||
return new Promise(function(resolve,reject){ | ||
return fs.then(function(fs){ | ||
if(!folders) { | ||
resolve(fs.root); | ||
} else { | ||
folders = folders.split('/').filter(function(folder) { | ||
return folder && folder.length > 0 && folder[0] !== '.'; | ||
}); | ||
__createDir(fs.root,folders,resolve,reject); | ||
} | ||
},reject); | ||
}); | ||
} | ||
/* get file file */ | ||
function file(path,options){ | ||
return new Promise(function(resolve,reject){ | ||
if(path instanceof FileEntry) { | ||
return resolve(path); | ||
} | ||
path = normalize(path); | ||
options = options || {}; | ||
return fs.then(function(fs){ | ||
fs.root.getFile(path,options,resolve,reject); | ||
},reject); | ||
}); | ||
} | ||
/* get directory entry */ | ||
function dir(path,options){ | ||
path = normalize(path); | ||
options = options || {}; | ||
return new Promise(function(resolve,reject){ | ||
return fs.then(function(fs){ | ||
if(!path || path === '/') { | ||
resolve(fs.root); | ||
} else { | ||
fs.root.getDirectory(path,options,resolve,reject); | ||
} | ||
},reject); | ||
}); | ||
} | ||
/* list contents of a directory */ | ||
function list(path,mode) { | ||
mode = mode || ''; | ||
var recursive = mode.indexOf('r') > -1; | ||
var getAsEntries = mode.indexOf('e') > -1; | ||
var onlyFiles = mode.indexOf('f') > -1; | ||
var onlyDirs = mode.indexOf('d') > -1; | ||
if(onlyFiles && onlyDirs) { | ||
onlyFiles = false; | ||
onlyDirs = false; | ||
} | ||
return new Promise(function(resolve,reject){ | ||
return dir(path).then(function(dirEntry){ | ||
var dirReader = dirEntry.createReader(); | ||
dirReader.readEntries(function(entries) { | ||
var promises = [ResolvedPromise(entries)]; | ||
if(recursive) { | ||
entries | ||
.filter(function(entry){return entry.isDirectory; }) | ||
.forEach(function(entry){ | ||
promises.push(list(entry.fullPath,'re')); | ||
}); | ||
} | ||
Promise.all(promises).then(function(values){ | ||
var entries = []; | ||
entries = entries.concat.apply(entries,values); | ||
if(onlyFiles) entries = entries.filter(function(entry) { return entry.isFile; }); | ||
if(onlyDirs) entries = entries.filter(function(entry) { return entry.isDirectory; }); | ||
if(!getAsEntries) entries = entries.map(function(entry) { return entry.fullPath; }); | ||
resolve(entries); | ||
},reject); | ||
}, reject); | ||
},reject); | ||
}); | ||
} | ||
/* does file exist? If so, resolve with fileEntry, if not, resolve with false. */ | ||
function exists(path){ | ||
return new Promise(function(resolve,reject){ | ||
file(path).then( | ||
function(fileEntry){ | ||
resolve(fileEntry); | ||
}, | ||
function(err){ | ||
if(err.code === 1) { | ||
resolve(false); | ||
} else { | ||
reject(err); | ||
} | ||
} | ||
); | ||
}); | ||
} | ||
function create(path){ | ||
return ensure(dirname(path)).then(function(){ | ||
return file(path,{create:true}); | ||
}); | ||
} | ||
/* convert path to URL to be used in JS/CSS/HTML */ | ||
function toURL(path) { | ||
return file(path).then(function(fileEntry) { | ||
return fileEntry.toURL(); | ||
}); | ||
} | ||
/* convert path to URL to be used in JS/CSS/HTML */ | ||
var toInternalURL,toInternalURLSync; | ||
if(isCordova) { | ||
/* synchronous helper to get internal URL. */ | ||
toInternalURLSync = function(path){ | ||
path = normalize(path); | ||
return 'cdvfile://localhost/'+(options.persistent? 'persistent/':'temporary/') + path; | ||
}; | ||
toInternalURL = function(path) { | ||
return file(path).then(function(fileEntry) { | ||
return fileEntry.toInternalURL(); | ||
}); | ||
}; | ||
} else { | ||
/* synchronous helper to get internal URL. */ | ||
toInternalURLSync = function(path){ | ||
path = normalize(path); | ||
return 'filesystem:'+location.origin+(options.persistent? '/persistent/':'/temporary/') + path; | ||
}; | ||
toInternalURL = function(path) { | ||
return file(path).then(function(fileEntry) { | ||
return fileEntry.toURL(); | ||
}); | ||
}; | ||
} | ||
/* return contents of a file */ | ||
function read(path,method) { | ||
method = method || 'readAsText'; | ||
return file(path).then(function(fileEntry) { | ||
return new Promise(function(resolve,reject){ | ||
fileEntry.file(function(file){ | ||
var reader = new FileReader(); | ||
reader.onloadend = function(){ | ||
resolve(this.result); | ||
}; | ||
reader[method](file); | ||
},reject); | ||
}); | ||
}); | ||
} | ||
/* convert path to base64 date URI */ | ||
function toDataURL(path) { | ||
return read(path,'readAsDataURL'); | ||
} | ||
function readJSON(path){ | ||
return read(path).then(JSON.parse); | ||
} | ||
/* write contents to a file */ | ||
function write(path,blob,mimeType) { | ||
return ensure(dirname(path)) | ||
.then(function() { return file(path,{create:true}); }) | ||
.then(function(fileEntry) { | ||
return new Promise(function(resolve,reject){ | ||
fileEntry.createWriter(function(writer){ | ||
writer.onwriteend = resolve; | ||
writer.onerror = reject; | ||
if(typeof blob === 'string') { | ||
blob = new Blob([blob],{type: mimeType || 'text/plain'}); | ||
} else if(blob instanceof Blob !== true){ | ||
blob = new Blob([JSON.stringify(blob,null,4)],{type: mimeType || 'application/json'}); | ||
} | ||
writer.write(blob); | ||
},reject); | ||
}); | ||
}); | ||
} | ||
/* move a file */ | ||
function move(src,dest) { | ||
return ensure(dirname(dest)) | ||
.then(function(dir) { | ||
return file(src).then(function(fileEntry){ | ||
return new Promise(function(resolve,reject){ | ||
fileEntry.moveTo(dir,filename(dest),resolve,reject); | ||
}); | ||
}); | ||
}); | ||
} | ||
/* copy a file */ | ||
function copy(src,dest) { | ||
return ensure(dirname(dest)) | ||
.then(function(dir) { | ||
return file(src).then(function(fileEntry){ | ||
return new Promise(function(resolve,reject){ | ||
fileEntry.copyTo(dir,filename(dest),resolve,reject); | ||
}); | ||
}); | ||
}); | ||
} | ||
/* delete a file */ | ||
function remove(path,mustExist) { | ||
var method = mustExist? file:exists; | ||
return new Promise(function(resolve,reject){ | ||
method(path).then(function(fileEntry){ | ||
if(fileEntry !== false) { | ||
fileEntry.remove(resolve,reject); | ||
} else { | ||
resolve(1); | ||
} | ||
},reject); | ||
}).then(function(val){ | ||
return val === 1? false: true; | ||
}); | ||
} | ||
/* delete a directory */ | ||
function removeDir(path) { | ||
return dir(path).then(function(dirEntry){ | ||
return new Promise(function(resolve,reject) { | ||
dirEntry.removeRecursively(resolve,reject); | ||
}); | ||
}); | ||
} | ||
// Whenever we want to start a transfer, we call popTransferQueue | ||
function popTransferQueue(){ | ||
// while we are not at max concurrency | ||
while(transferQueue.length > 0 && inprogress < options.concurrency){ | ||
// increment activity counter | ||
inprogress++; | ||
// fetch filetranfer, method-type (isDownload) and arguments | ||
var args = transferQueue.pop(); | ||
var ft = args.shift(); | ||
var isDownload = args.shift(); | ||
var serverUrl = args.shift(); | ||
var localPath = args.shift(); | ||
var win = args.shift(); | ||
var fail = args.shift(); | ||
var trustAllHosts = args.shift(); | ||
var transferOptions = args.shift(); | ||
if(ft._aborted) { | ||
inprogress--; | ||
} else if(isDownload){ | ||
ft.download.call(ft,serverUrl,localPath,win,fail,trustAllHosts,transferOptions); | ||
if(ft.onprogress) ft.onprogress(new ProgressEvent()); | ||
} else { | ||
ft.upload.call(ft,localPath,serverUrl,win,fail,transferOptions,trustAllHosts); | ||
} | ||
} | ||
// if we are at max concurrency, popTransferQueue() will be called whenever | ||
// the transfer is ready and there is space avaialable. | ||
} | ||
// Promise callback to check if there are any more queued transfers | ||
function nextTransfer(result){ | ||
inprogress--; // decrement counter to free up one space to start transfers again! | ||
popTransferQueue(); // check if there are any queued transfers | ||
return result; | ||
} | ||
function filetransfer(isDownload,serverUrl,localPath,transferOptions,onprogress){ | ||
if(typeof transferOptions === 'function') { | ||
onprogress = transferOptions; | ||
transferOptions = {}; | ||
} | ||
serverUrl = encodeURI(serverUrl); | ||
if(isCordova) localPath = toInternalURLSync(localPath); | ||
transferOptions = transferOptions || {}; | ||
if(!transferOptions.retry || !transferOptions.retry.length) { | ||
transferOptions.retry = options.retry; | ||
} | ||
transferOptions.retry = transferOptions.retry.concat(); | ||
var ft = new FileTransfer(); | ||
onprogress = onprogress || transferOptions.onprogress; | ||
if(typeof onprogress === 'function') ft.onprogress = onprogress; | ||
var promise = new Promise(function(resolve,reject){ | ||
var attempt = function(err){ | ||
if(transferOptions.retry.length === 0) { | ||
reject(err); | ||
} else { | ||
transferQueue.unshift([ft,isDownload,serverUrl,localPath,resolve,attempt,transferOptions.trustAllHosts || false,transferOptions]); | ||
var timeout = transferOptions.retry.shift(); | ||
if(timeout > 0) { | ||
setTimeout(nextTransfer,timeout); | ||
} else { | ||
nextTransfer(); | ||
} | ||
} | ||
}; | ||
transferOptions.retry.unshift(0); | ||
inprogress++; | ||
attempt(); | ||
}); | ||
promise.then(nextTransfer,nextTransfer); | ||
promise.progress = function(onprogress){ | ||
ft.onprogress = onprogress; | ||
return promise; | ||
}; | ||
promise.abort = function(){ | ||
ft._aborted = true; | ||
ft.abort(); | ||
return promise; | ||
}; | ||
return promise; | ||
} | ||
function download(url,dest,options,onprogress){ | ||
return filetransfer(true,url,dest,options,onprogress); | ||
} | ||
function upload(source,dest,options,onprogress){ | ||
return filetransfer(false,dest,source,options,onprogress); | ||
} | ||
return { | ||
fs: fs, | ||
normalize: normalize, | ||
file: file, | ||
filename: filename, | ||
dir: dir, | ||
dirname: dirname, | ||
create:create, | ||
read: read, | ||
readJSON: readJSON, | ||
write: write, | ||
move: move, | ||
copy: copy, | ||
remove: remove, | ||
removeDir: removeDir, | ||
list: list, | ||
ensure: ensure, | ||
exists: exists, | ||
download: download, | ||
upload: upload, | ||
toURL:toURL, | ||
isCordova:isCordova, | ||
toInternalURLSync: toInternalURLSync, | ||
toInternalURL:toInternalURL, | ||
toDataURL:toDataURL, | ||
deviceready: deviceready, | ||
options: options, | ||
Promise: Promise | ||
}; | ||
}; | ||
/***/ }, | ||
/* 3 */ | ||
/***/ function(module, exports, __webpack_require__) { | ||
/** | ||
* JS Implementation of MurmurHash3 (r136) (as of May 20, 2011) | ||
@@ -495,0 +1071,0 @@ * |
@@ -75,2 +75,3 @@ var CordovaPromiseFS = | ||
function normalize(str){ | ||
str = str || ''; | ||
if(str[0] === '/') str = str.substr(1); | ||
@@ -147,3 +148,12 @@ if(!!str && str.indexOf('.') < 0 && str[str.length-1] !== '/') str += '/'; | ||
deviceready.then(function(){ | ||
window.requestFileSystem(options.persistent? 1: 0, options.storageSize, resolve, reject); | ||
var type = options.persistent? 1: 0; | ||
if(typeof options.fileSystem === 'number'){ | ||
type = options.fileSystem; | ||
} | ||
// Chrome only supports persistent and temp storage, not the exotic onces from Cordova | ||
if(!isCordova && type > 1) { | ||
console.warn('Chrome does not support fileSystem "'+type+'". Falling back on "0" (temporary).'); | ||
type = 0; | ||
} | ||
window.requestFileSystem(type, options.storageSize, resolve, reject); | ||
setTimeout(function(){ reject(new Error('Could not retrieve FileSystem after 5 seconds.')); },5100); | ||
@@ -178,5 +188,8 @@ },reject); | ||
function file(path,options){ | ||
path = normalize(path); | ||
options = options || {}; | ||
return new Promise(function(resolve,reject){ | ||
if(path instanceof FileEntry) { | ||
return resolve(path); | ||
} | ||
path = normalize(path); | ||
options = options || {}; | ||
return fs.then(function(fs){ | ||
@@ -183,0 +196,0 @@ fs.root.getFile(path,options,resolve,reject); |
Sorry, the diff of this file is not supported yet
1688890
37881
460
61