diff --git a/.eslintrc b/.eslintrc index edbccd78..54024858 100644 --- a/.eslintrc +++ b/.eslintrc @@ -28,6 +28,7 @@ "curly": ["error", "all"] }, "globals": { + "PERSISTENT": false, "angular": false, "$fh": false, "FileTransfer": false, diff --git a/.gitignore b/.gitignore index 05ade97f..176dfecc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules npm-debug.log .idea +.vscode + diff --git a/README.md b/README.md index 81af4126..bc3aab01 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,59 @@ camera.capture() }); ``` +Example of re-init/restarting fileTransfer queues : + +```javascript +fileClient.processQueue() + .then(function(hasJobs) { + console.info('Found job to process in queues, starting file-Transfer'); + }, function() { + console.info('No jobs to process currently, file-Transfer idle.'); + }) +``` + +Example of saving/persisting fileTransfer queues : + +```javascript +fileClient.persistQueue() + .then(function(success) { + console.info('file-Transfer queues have been saved!'); + }, function(error) { + console.error('Problem saving fileTransfer queues!: ', error); + }); +``` + +Example of saving/persisting fileTransfer queues : + +```javascript +fileClient.persistQueue() + .then(function(success) { + console.info('file-Transfer queues have been saved!'); + }, function(error) { + console.error('Problem saving fileTransfer queues!: ', error); + }); +``` + + +Example of adding item to `uploads` queue : + +```javascript +camera.capture() + .then(function(dataUrl) { + return fileClient.uploads.addItem({ + id: profileData.id, + fileName: 'my-super-img-file.png' + fileURi: dataUrl, + createdTs: Date.now(), + status: 'waiting', + retries: 3, + userId: 'trever', + step: 'rsik-assessment', + result: this.model + }) + }); +``` + For a more complete example around files operations, please check the [demo mobile app](https://github.com/feedhenry-raincatcher/raincatcher-demo-mobile/blob/master/src/app/file/file.js). #### Directives @@ -95,8 +148,8 @@ Base url : `/file/wfm` ## Mediator events the module publishes following topics: -- `wfm:file:detail:close` - on file detail view close. - +- `wfm:file:detail:close` - on file detail view close. + Client app example: ``` mediator.subscribeForScope('wfm:file:detail:close', $scope, function() { diff --git a/lib/config.js b/lib/config.js index 125b41ce..3a13d692 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,8 +1,15 @@ -'use strict'; +// 'use strict'; module.exports = { apiHost: 'http://localhost:8080', apiPath: '/file/wfm', cloudDataTopicPrefix: 'wfm:cloud:data:', - datasetId: 'file' -}; + datasetId: 'file', + queue: { + prefix: 'fh.wfm.file.queue.', + storageType: 'localStorage', + uploadsLabel: 'uploads', + downloadsLabel: 'downloads', + fileContentType: 'application/json' + } +}; \ No newline at end of file diff --git a/lib/file.js b/lib/file.js index 950e0e14..2258231e 100644 --- a/lib/file.js +++ b/lib/file.js @@ -1,6 +1,8 @@ 'use strict'; var config = require('./config'), + QUEUE = require('./queue'), + fsModule = require('./fileSystem.js'), q = require('q'); var client = {}; @@ -25,7 +27,31 @@ client.init = function() { deferredReady.resolve(); } - client.initPromise = q.all([deferredFhinit.promise, deferredReady.promise]); + var deferredInitUploadsQueue = q.defer(); + if (!client.uploads) { + client.uploads = new QUEUE(config.queue.uploadsLabel, config.queue.storageType) || {}; + deferredInitUploadsQueue.resolve(); + } else if (client.uploads.ready) { + deferredInitUploadsQueue.resolve(); + } + + var deferredInitDownloadsQueue = q.defer(); + if (!client.downloads) { + client.downloads = new QUEUE(config.queue.downloadsLabel, config.queue.storageType) || {}; + deferredInitDownloadsQueue.resolve(); + } else if (client.downloads.ready) { + deferredInitUploadsQueue.resolve(); + } + + client.fs = fsModule.fs; + + client.initPromise = q.all([ + deferredFhinit.promise, + deferredReady.promise, + deferredInitUploadsQueue.promise, + deferredInitDownloadsQueue.promise + ]); + return client.initPromise; }; @@ -35,14 +61,12 @@ client.uploadDataUrl = function(userId, dataUrl) { deferred.reject('Both userId and a dataUrl parameters are required.'); } else { $fh.cloud({ - path: config.apiPath + '/owner/'+userId+'/upload/base64/photo.png', + path: config.apiPath + '/owner/' + userId + '/upload/base64/photo.png', method: 'post', data: dataUrl - }, - function(res) { + }, function(res) { deferred.resolve(res); - }, - function(message, props) { + }, function(message, props) { var e = new Error(message); e.props = props; deferred.reject(e); @@ -52,22 +76,19 @@ client.uploadDataUrl = function(userId, dataUrl) { }; client.list = function(userId) { - var url = arguments.length === 0 ? config.apiPath + '/all' - : config.apiPath + '/owner/' + userId; + var url = arguments.length === 0 ? config.apiPath + '/all' : + config.apiPath + '/owner/' + userId; var deferred = q.defer(); $fh.cloud({ path: url, method: 'get' - }, - function(res) { - deferred.resolve(res); - }, - function(message, props) { - var e = new Error(message); - e.props = props; - deferred.reject(e); - } - ); + }, function(res) { + deferred.resolve(res); + }, function(message, props) { + var e = new Error(message); + e.props = props; + deferred.reject(e); + }); return deferred.promise; }; @@ -84,17 +105,16 @@ function fileUpload(fileURI, serverURI, fileUploadOptions) { function fileUploadRetry(fileURI, serverURI, fileUploadOptions, timeout, retries) { return fileUpload(fileURI, serverURI, fileUploadOptions) - .then(function(response) { - return response; - }, function() { - if (retries === 0) { - throw new Error("Can't upload to " + JSON.stringify(serverURI)); - } - return q.delay(timeout) - .then(function() { - return fileUploadRetry(fileURI, serverURI, fileUploadOptions, timeout, retries - 1); - }); - }); + .then(function(response) { + return response; + }, function() { + if (retries === 0) { + throw new Error("Can't upload to " + JSON.stringify(serverURI)); + } + return q.delay(timeout).then(function() { + return fileUploadRetry(fileURI, serverURI, fileUploadOptions, timeout, retries - 1); + }); + }); } client.uploadFile = function(userId, fileURI, options) { @@ -119,6 +139,36 @@ client.uploadFile = function(userId, fileURI, options) { } }; +client.processQueue = function() { + return client.initPromise.then(function() { + return client.uploads.recoverQueue(); + }).then(function(processingQueue) { + if (processingQueue) { + client.uploads.queue.forEach(function(item) { + if (window && window.cordova) { + return client.uploadFile(item.userId, item.filePath, { + fileName: item.fileName + }); + } else { + return client.uploadDataUrl(item.userId, item.filePath); + } + }); + } else { + return; + } + }); +}; + +client.persistQueue = function() { + var d = q.defer(); + client.uploads.save().then(function(success) { + d.resolve(success); + }, function(error) { + d.reject(error); + }); + return d.promise; +}; + client.init(); -module.exports = client; +module.exports = client; \ No newline at end of file diff --git a/lib/fileSystem.js b/lib/fileSystem.js new file mode 100644 index 00000000..1467439e --- /dev/null +++ b/lib/fileSystem.js @@ -0,0 +1,146 @@ +var q = require('q'); + +/** + * @param {object} e error object + */ +function _fsErrorHandler(e) { + console.error(e, e.name); + throw new Error('Error: ' + e); +} + +var _requestedFileSystem = {}; +function _checkEnv() { + var d = q.defer(); + + if (window.requestFileSystem) { + _requestedFileSystem = window.requestFileSystem; + d.resolve(true); + } else if (window.webkitRequestFileSystem) { + _requestedFileSystem = window.webkitRequestFileSystem; + d.resolve(true); + } else { + d.reject(false); + } + + if (window.LocalFileSystem) { + PERSISTENT = window.LocalFileSystem.PERSISTENT; + } else if (window.PERSISTENT) { + PERSISTENT = window.PERSISTENT; + } + + return d.promise; +} + +function _requestFS() { + var d = q.defer(); + _checkEnv().then(function(fileSystemAvailable) { + if (fileSystemAvailable) { + _requestedFileSystem(PERSISTENT, 1024 * 1024, function(fs) { + d.resolve(fs); + }, function(e) { + console.error(e, e.name); + d.reject(e.name); + }); + } else { + d.reject('Unable to retrieve fileSystem.'); + } + }); + return d.promise; +} + +var fileSystem = {}; + +fileSystem.init = function() { + var d = q.defer(); + _requestFS().then(function(fs) { + fileSystem.fs = fs; + d.resolve(true); + }, function(errorMsg) { + d.reject(errorMsg); + }); + return d.promise; +}; + +/** + * @param {string} name directory name + */ +fileSystem.getDir = function(name) { + var d = q.defer(); + if (fileSystem.ready) { + fileSystem.fs.root.getDirectory(name, { + create: true + }, function(dirEntry) { + d.resolve(dirEntry); + }, function(error) { + d.reject(error.toString()); + }); + } else { + d.reject('fileSystem not ready'); + } + return d.promise; +}; + +/** + * @param {string} name fileName/key + * @param {string} contentType file content type + * @param {object} data queue data + */ +fileSystem.writeFile = function(name, contentType, data) { + var d = q.defer(); + var fileData = JSON.stringify(data); + if (fileSystem.ready) { + fileSystem.fs.root.getFile(name, { + create: true + }, function(fileEntry) { + fileEntry.createWriter(function(fileWriter) { + fileWriter.onwriteend = function() { + d.resolve(true); + }; + fileWriter.onerror = function(err) { + d.reject('Write failed: ' + err.toString()); + }; + var blob = new Blob([fileData], { type: contentType }); + fileWriter.write(blob); + }, _fsErrorHandler); + }, _fsErrorHandler); + } else { + d.reject('fileSystem not ready'); + } + + return d.promise; +}; + +/** + * @param {string} name fileName/key + */ +fileSystem.readFile = function(name) { + var d = q.defer(); + if (fileSystem.ready) { + fileSystem.fs.root.getFile(name, { + create: false + }, function(fileEntry) { + fileEntry.file(function(file) { + var reader = new FileReader(); + reader.onloadend = function() { + d.resolve(this.result); + }; + reader.onerror = function(err) { + d.reject('Unable to read '+ name +' file, Error: ' + err.toString()); + }; + reader.readAsText(file); + }); + }); + } else { + d.reject('fileSystem not ready!'); + } + return d.promise; +}; + +// init fieSysten module +fileSystem.init().then(function(ready) { + fileSystem.ready = ready; +}, function(errorMsg) { + console.error(errorMsg); +}); + +module.exports = fileSystem; \ No newline at end of file diff --git a/lib/queue-spec.js b/lib/queue-spec.js new file mode 100644 index 00000000..3ce138dd --- /dev/null +++ b/lib/queue-spec.js @@ -0,0 +1,78 @@ +var assert = require('assert'), + _ = require('lodash'); + +describe('Queue', function() { + var queue = {}, + fileTestQ = {}, + lsTestQ = {}; + + var item = { + 'id': 1, + 'hello': 'world', + 'date': Date.now() + }; + + before(function() { + queue = require('./queue'); + lsTestQ = new queue('lsTestQ', 'localStorage'); + fileTestQ = new queue('fileTestQ', 'file'); + }); + + it('should return new queue with name lsTestQ', function() { + assert.equal(lsTestQ.queueName, 'fh.wfm.file.queue.lsTestQ'); + }); + + it('should return new queue with name fileTestQ', function() { + assert.equal(fileTestQ.queueName, 'fh.wfm.file.queue.fileTestQ'); + }); + + it('lsTestQ should have storageType of localStorage', function() { + assert.equal(lsTestQ.storageType, 'localStorage'); + }); + + it('fileTestQ should have storageType of file', function() { + assert.equal(fileTestQ.storageType, 'file'); + }); + + it('should allow item to be added to lsTestQ queue array', function() { + lsTestQ.addItem(item); + var lsExists = _.includes(lsTestQ.queue, item); + assert.equal(lsExists, true); + + fileTestQ.addItem(item); + var fileExists = _.includes(fileTestQ.queue, item); + assert.equal(fileExists, true); + }); + + it('should get response equal to item object from both queues', function() { + lsTestQ.readItem().then(function(res) { + assert.equal(res, item); + }); + + fileTestQ.readItem().then(function(res) { + assert.equal(res, item); + }); + }); + + it('should update hello prop value to `update test`', function() { + lsTestQ.updateItem(1, 'hello', 'update test'); + lsTestQ.readItem().then(function(item) { + assert.equal(item.hello, 'update test'); + }); + + fileTestQ.updateItem(1, 'hello', 'update test'); + fileTestQ.readItem().then(function(item) { + assert.equal(item.hello, 'update test'); + }); + }); + + it('should remove item from both arrays', function() { + lsTestQ.removeItem(item).then(function() { + assert.equal(lsTestQ.queue.length, 0); + }); + + fileTestQ.removeItem(item).then(function() { + assert.equal(fileTestQ.queue.length, 0); + }); + }); +}); \ No newline at end of file diff --git a/lib/queue.js b/lib/queue.js new file mode 100644 index 00000000..e67f2303 --- /dev/null +++ b/lib/queue.js @@ -0,0 +1,145 @@ +'use strict'; + +var config = require('./config'), + q = require('q'), + fileSystem = require('./fileSystem'), + _ = require('lodash'); + +// example meta data item model schema +// { +// id: , +// fileName: , +// filePath: , +// createdTs: , +// uploadedTs: , +// result: , +// userId: +// step: , +// retries: 3 +// status: +// } + +/** + * @param {object} queue array of q items + * @param {object} item meta-data model + */ +function _existsInQueue(queue, item) { + return _.includes(queue, item); +} + +/** + * @param {string} type + * @param {string} cacheType + */ +var QUEUE = function(type, cacheType) { + this.queueName = config.queue.prefix + type; + this.storageType = cacheType !== undefined ? cacheType : config.storageType; + this.queue = []; + this.ready = true; +}; + +QUEUE.prototype.save = function() { + var d = q.defer(); + + if (this.storageType === 'file') { + return fileSystem.writeFile(this.fileName, config.queue.fileContentType, { + queue: this.queue + }).then(function(success) { + d.resolve(success); + }, function(error) { + d.reject(error); + }); + } else { + localStorage.setItem(this.fileName, JSON.stringify({ queue: this.queue })); + d.resolve(true); + } + + return d.promise; +}; + +QUEUE.prototype.recoverQueue = function() { + var self = this; + var d = q.defer(); + + if (this.storageType === 'file') { + return fileSystem.readFile(this.fileName).then(function(data) { + var fileData = JSON.parse(data); + self.queue = fileData.queue; + d.resolve(true); + }, function() { + d.reject(false); + }); + } else { + var queueData = localStorage.getItem(this.queueName); + if (queueData !== null) { + var fileData = JSON.parse(queueData); + this.queue = fileData.queue; + d.resolve(true); + } else { + d.reject(false); + } + } + + return d.promise; +}; + +/** + * @param {object} item meta data model + */ +QUEUE.prototype.addItem = function(item) { + var d = q.defer(); + if (_existsInQueue(this.queue, item)) { + d.reject('Item already added to queue'); + } else { + this.queue.push(item); + d.resolve(true); + } + return d.promise; +}; + +/** + * @param {object} item meta data model + */ +QUEUE.prototype.removeItem = function(item) { + var d = q.defer(); + if (!_existsInQueue(this.queue, item)) { + d.reject('Cannot find queue item.'); + } else { + _.remove(this.queue, item); + d.resolve(true); + } + return d.promise; +}; + +/** + * @param {object} item meta data model + */ +QUEUE.prototype.updateItem = function(id, key, val) { + var d = q.defer(); + this.queue.filter(function(item) { + if (item.id === id) { + item[key] = val; + d.resolve(true); + } else { + d.reject('Queue item not updated'); + } + }); + return d.promise; +}; + +/** + * @param {object} item meta data model + */ +QUEUE.prototype.readItem = function(id) { + var d = q.defer(); + this.queue.filter(function(item) { + if (item.id === id) { + d.resolve(item); + } else { + d.reject('Unable to read queue item'); + } + }); + return d.promise; +}; + +module.exports = QUEUE; \ No newline at end of file