cordova-app-loader
Version:
Cordova App Loader - remote update your cordova app
276 lines (245 loc) • 9.95 kB
JavaScript
var CordovaFileCache = require('cordova-file-cache');
var CordovaPromiseFS = require('cordova-promise-fs');
var Promise = null;
var BUNDLE_ROOT = location.href.replace(location.hash,'');
BUNDLE_ROOT = BUNDLE_ROOT.substr(0,BUNDLE_ROOT.lastIndexOf('/')+1);
if(/ip(hone|ad|od)/i.test(navigator.userAgent)){
BUNDLE_ROOT = location.pathname.substr(location.pathname.indexOf('/www/'));
BUNDLE_ROOT = BUNDLE_ROOT.substr(0,BUNDLE_ROOT.lastIndexOf('/')+1);
BUNDLE_ROOT = 'cdvfile://localhost/bundle' + BUNDLE_ROOT;
}
function hash(files){
var keys = Object.keys(files);
keys.sort();
var str = '';
keys.forEach(function(key){
if(files[key] && files[key].version);
str += '@' + files[key].version;
});
return CordovaFileCache.hash(str) + '';
}
function AppLoader(options){
if(!options) throw new Error('CordovaAppLoader has no options!');
if(!options.fs) throw new Error('CordovaAppLoader has no "fs" option (cordova-promise-fs)');
if(!options.serverRoot) throw new Error('CordovaAppLoader has no "serverRoot" option.');
if(!window.pegasus || !window.Manifest) throw new Error('CordovaAppLoader bootstrap.js is missing.');
this.allowServerRootFromManifest = options.allowServerRootFromManifest === true;
Promise = options.fs.Promise;
// initialize variables
this.manifest = window.Manifest;
this.newManifest = null;
this.bundledManifest = null;
this.preventAutoUpdateLoop = options.preventAutoUpdateLoop === true;
this._lastUpdateFiles = localStorage.getItem('last_update_files');
// normalize serverRoot and set remote manifest url
options.serverRoot = options.serverRoot || '';
if(!!options.serverRoot && options.serverRoot[options.serverRoot.length-1] !== '/') options.serverRoot += '/';
this.newManifestUrl = options.manifestUrl || options.serverRoot + (options.manifest || 'manifest.json');
// initialize a file cache
if(options.mode) options.mode = 'mirror';
this.cache = new CordovaFileCache(options);
// private stuff
this.corruptNewManifest = false;
this._toBeCopied = [];
this._toBeDeleted = [];
this._toBeDownloaded = [];
this._updateReady = false;
this._checkTimeout = options.checkTimeout || 10000;
}
AppLoader.prototype._createFilemap = function(files){
var result = {};
var normalize = this.cache._fs.normalize;
Object.keys(files).forEach(function(key){
files[key].filename = normalize(files[key].filename);
result[files[key].filename] = files[key];
});
return result;
};
AppLoader.prototype.copyFromBundle = function(file){
var url = BUNDLE_ROOT + file;
return this.cache._fs.download(url,this.cache.localRoot + file);
};
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.check = function(newManifest){
var self = this, manifest = this.manifest;
if(typeof newManifest === "string") {
self.newManifestUrl = newManifest;
newManifest = undefined;
}
var gotNewManifest = new Promise(function(resolve,reject){
if(typeof newManifest === "object") {
resolve(newManifest);
} else {
pegasus(self.newManifestUrl).then(resolve,reject);
setTimeout(function(){reject(new Error('new manifest timeout'));},self._checkTimeout);
}
});
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];
var newFiles = hash(newManifest.files);
// Prevent end-less update loop, check if new manifest
// has been downloaded before (but failes)
// Check if the newFiles match the previous files (last_update_files)
if(self.preventAutoUpdateLoop === true && newFiles === self._lastUpdateFiles) {
// YES! So we're doing the same update again!
// Check if our current Manifest has indeed the "last_update_files"
var currentFiles = hash(Manifest.files);
if(self._lastUpdateFiles !== currentFiles){
// No! So we've updated, yet they don't appear in our manifest. This means:
console.warn('New manifest available, but an earlier update attempt failed. Will not download.');
self.corruptNewManifest = true;
resolve(null);
}
// Yes, we've updated and we've succeeded.
resolve(false);
return;
}
// Check if new manifest is valid
if(!newManifest.files){
reject(new Error('Downloaded Manifest has no "files" attribute.'));
return;
}
// 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 COPY and DOWNLOAD lists
self._toBeDownloaded = [];
self._toBeCopied = [];
self._toBeDeleted= [];
var isCordova = self.cache._fs.isCordova;
var changes = 0;
Object.keys(newFiles)
// Find files that have changed version or are missing
.filter(function(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);
}
if(!bundledFiles[file] || bundledFiles[file].version !== newFiles[file].version){
changes++;
}
});
// 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;
});
changes += self._toBeDeleted.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.localUrl;
resolve(true);
} else {
resolve(false);
}
}, function(err){
reject(err);
}); // end of .then
}); // end of new Promise
};
AppLoader.prototype.canDownload = function(){
return !!this.newManifest && !this._updateReady;
};
AppLoader.prototype.canUpdate = function(){
return this._updateReady;
};
AppLoader.prototype.download = function(onprogress,includeFileProgressEvents){
var self = this;
if(!self.canDownload()) {
return new Promise(function(resolve){ resolve(null); });
}
// we will delete files, which will invalidate the current manifest...
localStorage.removeItem('manifest');
// only attempt this once - set 'last_update_files'
localStorage.setItem('last_update_files',hash(this.newManifest.files));
this.manifest.files = Manifest.files = {};
return self.cache.remove(self._toBeDeleted,true)
.then(function(){
return Promise.all(self._toBeCopied.map(function(file){
return self.cache._fs.download(BUNDLE_ROOT + file,self.cache.localRoot + file);
}));
})
.then(function(){
if(self.allowServerRootFromManifest && self.newManifest.serverRoot){
self.cache.serverRoot = self.newManifest.serverRoot;
}
self.cache.add(self._toBeDownloaded);
return self.cache.download(onprogress,includeFileProgressEvents);
}).then(function(){
self._toBeDeleted = [];
self._toBeDownloaded = [];
self._updateReady = true;
return self.newManifest;
},function(files){
// on download error, remove files...
if(!!files && files.length){
self.cache.remove(files);
}
return files;
});
};
AppLoader.prototype.update = function(reload){
if(this._updateReady) {
// update manifest
localStorage.setItem('manifest',JSON.stringify(this.newManifest));
if(reload !== false) location.reload();
return true;
}
return false;
};
AppLoader.prototype.clear = function(){
localStorage.removeItem('last_update_files');
localStorage.removeItem('manifest');
return this.cache.clear();
};
AppLoader.prototype.reset = function(){
return this.clear().then(function(){
location.reload();
},function(){
location.reload();
});
};
module.exports = AppLoader;