├── .gitignore ├── Gruntfile.js ├── LICENSE-MIT.txt ├── README.md ├── bower.json ├── dist ├── LargeLocalStorage.js ├── LargeLocalStorage.min.js └── contrib │ ├── S3Link.js │ └── URLCache.js ├── examples └── album │ ├── app.js │ ├── index.html │ └── main.css ├── package.json ├── src ├── LargeLocalStorage.js ├── contrib │ ├── S3Link.js │ └── URLCache.js ├── errors.js ├── footer.js ├── header.js ├── impls │ ├── FilesystemAPIProvider.js │ ├── IndexedDBProvider.js │ ├── LocalStorageProvider.js │ ├── WebSQLProvider.js │ └── utils.js └── pipeline.js └── test ├── elephant.jpg ├── index.html ├── lib ├── chai.js ├── expect.js ├── mocha │ ├── mocha.css │ └── mocha.js └── sinon.js ├── pie.jpg ├── runner └── mocha.js └── spec ├── LargeLocalStorageTest.js └── URLCacheTest.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | node_modules/ 3 | doc/ 4 | temp.out 5 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (grunt) { 4 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 5 | 6 | grunt.initConfig({ 7 | pkg: grunt.file.readJSON('package.json'), 8 | concat: { 9 | options: { 10 | seperator: ';' 11 | }, 12 | scripts: { 13 | src: ['src/header.js', 14 | 'src/pipeline.js', 15 | 'src/impls/utils.js', 16 | 'src/impls/FilesystemAPIProvider.js', 17 | 'src/impls/IndexedDBProvider.js', 18 | 'src/impls/LocalStorageProvider.js', 19 | 'src/impls/WebSQLProvider.js', 20 | 'src/LargeLocalStorage.js', 21 | 'src/footer.js'], 22 | dest: 'dist/LargeLocalStorage.js' 23 | } 24 | }, 25 | 26 | uglify: { 27 | options: { 28 | mangle: { 29 | except: ['Q'] 30 | } 31 | }, 32 | scripts: { 33 | files: { 34 | 'dist/LargeLocalStorage.min.js': ['dist/LargeLocalStorage.js'] 35 | } 36 | } 37 | }, 38 | 39 | watch: { 40 | scripts: { 41 | files: ["src/**/*.js"], 42 | tasks: ["concat"] 43 | }, 44 | contrib: { 45 | files: ["src/contrib/**/*.js"], 46 | tasks: ["copy:contrib"] 47 | } 48 | }, 49 | 50 | copy: { 51 | contrib: { 52 | files: [{expand: true, cwd: "src/contrib/", src: "**", dest: "dist/contrib/"}] 53 | } 54 | }, 55 | 56 | connect: { 57 | server: { 58 | options: { 59 | port: 9001, 60 | base: '.' 61 | } 62 | } 63 | }, 64 | 65 | // docview: { 66 | // compile: { 67 | // files: { 68 | // "doc/LargeLocalStorage.html": "doc/library.handlebars" 69 | // } 70 | // } 71 | // }, 72 | 73 | yuidoc: { 74 | compile: { 75 | name: '<%= pkg.name %>', 76 | description: '<%= pkg.description %>', 77 | version: '<%= pkg.version %>', 78 | url: '<%= pkg.homepage %>', 79 | options: { 80 | paths: 'src', 81 | themedir: 'node_modules/yuidoc-library-theme', 82 | helpers: ['node_modules/yuidoc-library-theme/helpers/helpers.js'], 83 | outdir: 'doc', 84 | // parseOnly: true 85 | } 86 | } 87 | } 88 | }); 89 | 90 | grunt.registerTask('default', ['concat', 'copy', 'connect', 'watch']); 91 | grunt.registerTask('docs', ['yuidoc']); 92 | grunt.registerTask('build', ['concat', 'copy', 'uglify']); 93 | }; 94 | -------------------------------------------------------------------------------- /LICENSE-MIT.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Matt Crinklaw-Vogt 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | LargeLocalStorage 2 | ================= 3 | 4 | 5 | **Problem:** You need a large key-value store in the browser. 6 | 7 | To make things worse: 8 | * DOMStorage only gives you 5mb 9 | * Chrome doesn't let you store blobs in IndexedDB 10 | * Safari doesn't support IndexedDB, 11 | * IE and Firefox both support IndexedDB but not the FilesystemAPI. 12 | 13 | `LargeLocalStorage` bridges all of that to give you a large capacity (up to several GB when authorized by the user) key-value store in the browser 14 | (IE 10, Chrome, Safari 6+, Firefox, Opera). 15 | 16 | * [docs](http://tantaman.github.io/LargeLocalStorage/doc/classes/LargeLocalStorage.html) 17 | * [tests](http://tantaman.github.io/LargeLocalStorage/test/) 18 | * [demo app](http://tantaman.github.io/LargeLocalStorage/examples/album/) 19 | 20 | ## Basic Rundown / Examples 21 | 22 | ### Creating a database 23 | 24 | ```javascript 25 | // Specify desired capacity in bytes 26 | var desiredCapacity = 125 * 1024 * 1024; 27 | 28 | // Create a 125MB key-value store 29 | var storage = new LargeLocalStorage({size: desiredCapacity, name: 'myDb'}); 30 | 31 | // Await initialization of the storage area 32 | storage.initialized.then(function(grantedCapacity) { 33 | // Check to see how much space the user authorized us to actually use. 34 | // Some browsers don't indicate how much space was granted in which case 35 | // grantedCapacity will be 1. 36 | if (grantedCapacity != -1 && grantedCapacity != desiredCapacity) { 37 | } 38 | }); 39 | ``` 40 | 41 | ### Setting data 42 | 43 | ```javascript 44 | // You can set the contents of "documents" which are identified by a key. 45 | // Documents can only contains strings for their values but binary 46 | // data can be added as attachments. 47 | // All operations are asynchronous and return Q promises 48 | storage.setContents('docKey', "the contents...").then(function() { 49 | alert('doc created/updated'); 50 | }); 51 | 52 | // Attachments can be added to documents. 53 | // Attachments are Blobs or any subclass of Blob (e.g, File). 54 | // Attachments can be added whether or not a corresponding document exists. 55 | // setAttachment returns a promise so you know when the set has completed. 56 | storage.setAttachment('myDoc', 'titleImage', blob).then(function() { 57 | alert('finished setting the titleImage attachment'); 58 | }); 59 | ``` 60 | 61 | ### Retrieving Data 62 | 63 | ```javascript 64 | // get the contents of a document 65 | storage.getContents('myDoc').then(function(content) { 66 | }); 67 | 68 | // Call getAttachment with the docKey and attachmentKey 69 | storage.getAttachment('myDoc', 'titleImage').then(function(titleImage) { 70 | // Create an image element with the retrieved attachment 71 | // (or video or sound or whatever you decide to attach and use) 72 | var img = new Image(); 73 | img.src = URL.createObjectURL(titleImage); 74 | document.body.appendChild(img); 75 | URL.revokeObjectURL(titleImage); 76 | }); 77 | 78 | 79 | // If you just need a URL to your attachment you can get 80 | // the attachment URL instead of the attachment itself 81 | storge.getAttachmentURL('somePreviouslySavedDoc', 'someAttachment').then(function(url) { 82 | // do something with the attachment URL 83 | // ... 84 | 85 | // revoke the URL 86 | storage.revokeAttachmentURL(url); 87 | }); 88 | ``` 89 | 90 | ### Listing 91 | ```javascript 92 | // You can do an ls to get all of the keys in your data store 93 | storage.ls().then(function(listing) { 94 | // listing is a list of all of the document keys 95 | alert(listing); 96 | }); 97 | 98 | // Or get a listing of a document's attachments 99 | storage.ls('somePreviouslySavedDoc').then(function(listing) { 100 | // listing is a list of all attachments belonging to `somePreviouslySavedDoc` 101 | alert(listing); 102 | }); 103 | ``` 104 | 105 | ### Removing 106 | ```javascript 107 | // you can remove a document with rm 108 | // removing a document also removes all of that document's 109 | // attachments. 110 | storage.rm('somePreviouslySavedDoc'); 111 | 112 | // you can also rm an attachment 113 | storage.rmAttachment('someOtherDocKey', 'attachmentKey'); 114 | 115 | // removals return promises as well so you know when the removal completes (or fails). 116 | storage.rm('docKey').then(function() { 117 | alert('Removed!'); 118 | }, function(err) { 119 | console.error('Failed removal'); 120 | console.error(err); 121 | }); 122 | 123 | // clear the entire database 124 | storage.clear(); 125 | ``` 126 | 127 | More: 128 | * Read the [docs](http://tantaman.github.io/LargeLocalStorage/doc/classes/LargeLocalStorage.html) 129 | * Run the [tests](http://tantaman.github.io/LargeLocalStorage/test/) 130 | * View the [demo app](http://tantaman.github.io/LargeLocalStorage/examples/album/) 131 | 132 | ##Including 133 | 134 | Include it on your page with a script tag: 135 | 136 | ``` 137 | 138 | ``` 139 | 140 | Or load it as an amd module: 141 | 142 | ``` 143 | define(['components/lls/dist/LargeLocalStorage'], function(lls) { 144 | var storage = new lls({size: 100 * 1024 * 1024}); 145 | }); 146 | ``` 147 | 148 | LLS depends on [Q](https://github.com/kriskowal/q) so you'll have to make sure you have that dependency. 149 | 150 | ##Getting 151 | downlad it directly 152 | 153 | * (dev) https://raw.github.com/tantaman/LargeLocalStorage/master/dist/LargeLocalStorage.js 154 | * (min) https://raw.github.com/tantaman/LargeLocalStorage/master/dist/LargeLocalStorage.min.js 155 | 156 | Or `bower install lls` 157 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lls", 3 | "version": "0.1.3", 4 | "main": "dist/LargeLocalStorage.js", 5 | "ignore": [ 6 | "examples", 7 | "src", 8 | "test", 9 | "Gruntfile.js", 10 | "todo.txt", 11 | "package.json" 12 | ], 13 | 14 | "dependencies": { 15 | "q": "~0.9.7" 16 | }, 17 | 18 | "devDependencies": { 19 | "jquery": "~2.0.3", 20 | "bootstrap": "~3.0.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /dist/LargeLocalStorage.js: -------------------------------------------------------------------------------- 1 | (function(glob) { 2 | var undefined = {}.a; 3 | 4 | function definition(Q) { 5 | 6 | 7 | /** 8 | @author Matt Crinklaw-Vogt 9 | */ 10 | function PipeContext(handlers, nextMehod, end) { 11 | this._handlers = handlers; 12 | this._next = nextMehod; 13 | this._end = end; 14 | 15 | this._i = 0; 16 | } 17 | 18 | PipeContext.prototype = { 19 | next: function() { 20 | // var args = Array.prototype.slice.call(arguments, 0); 21 | // args.unshift(this); 22 | this.__pipectx = this; 23 | return this._next.apply(this, arguments); 24 | }, 25 | 26 | _nextHandler: function() { 27 | if (this._i >= this._handlers.length) return this._end; 28 | 29 | var handler = this._handlers[this._i].handler; 30 | this._i += 1; 31 | return handler; 32 | }, 33 | 34 | length: function() { 35 | return this._handlers.length; 36 | } 37 | }; 38 | 39 | function indexOfHandler(handlers, len, target) { 40 | for (var i = 0; i < len; ++i) { 41 | var handler = handlers[i]; 42 | if (handler.name === target || handler.handler === target) { 43 | return i; 44 | } 45 | } 46 | 47 | return -1; 48 | } 49 | 50 | function forward(ctx) { 51 | return ctx.next.apply(ctx, Array.prototype.slice.call(arguments, 1)); 52 | } 53 | 54 | function coerce(methodNames, handler) { 55 | methodNames.forEach(function(meth) { 56 | if (!handler[meth]) 57 | handler[meth] = forward; 58 | }); 59 | } 60 | 61 | var abstractPipeline = { 62 | addFirst: function(name, handler) { 63 | coerce(this._pipedMethodNames, handler); 64 | this._handlers.unshift({name: name, handler: handler}); 65 | }, 66 | 67 | addLast: function(name, handler) { 68 | coerce(this._pipedMethodNames, handler); 69 | this._handlers.push({name: name, handler: handler}); 70 | }, 71 | 72 | /** 73 | Add the handler with the given name after the 74 | handler specified by target. Target can be a handler 75 | name or a handler instance. 76 | */ 77 | addAfter: function(target, name, handler) { 78 | coerce(this._pipedMethodNames, handler); 79 | var handlers = this._handlers; 80 | var len = handlers.length; 81 | var i = indexOfHandler(handlers, len, target); 82 | 83 | if (i >= 0) { 84 | handlers.splice(i+1, 0, {name: name, handler: handler}); 85 | } 86 | }, 87 | 88 | /** 89 | Add the handler with the given name after the handler 90 | specified by target. Target can be a handler name or 91 | a handler instance. 92 | */ 93 | addBefore: function(target, name, handler) { 94 | coerce(this._pipedMethodNames, handler); 95 | var handlers = this._handlers; 96 | var len = handlers.length; 97 | var i = indexOfHandler(handlers, len, target); 98 | 99 | if (i >= 0) { 100 | handlers.splice(i, 0, {name: name, handler: handler}); 101 | } 102 | }, 103 | 104 | /** 105 | Replace the handler specified by target. 106 | */ 107 | replace: function(target, newName, handler) { 108 | coerce(this._pipedMethodNames, handler); 109 | var handlers = this._handlers; 110 | var len = handlers.length; 111 | var i = indexOfHandler(handlers, len, target); 112 | 113 | if (i >= 0) { 114 | handlers.splice(i, 1, {name: newName, handler: handler}); 115 | } 116 | }, 117 | 118 | removeFirst: function() { 119 | return this._handlers.shift(); 120 | }, 121 | 122 | removeLast: function() { 123 | return this._handlers.pop(); 124 | }, 125 | 126 | remove: function(target) { 127 | var handlers = this._handlers; 128 | var len = handlers.length; 129 | var i = indexOfHandler(handlers, len, target); 130 | 131 | if (i >= 0) 132 | handlers.splice(i, 1); 133 | }, 134 | 135 | getHandler: function(name) { 136 | var i = indexOfHandler(this._handlers, this._handlers.length, name); 137 | if (i >= 0) 138 | return this._handlers[i].handler; 139 | return null; 140 | } 141 | }; 142 | 143 | function createPipeline(pipedMethodNames) { 144 | var end = {}; 145 | var endStubFunc = function() { return end; }; 146 | var nextMethods = {}; 147 | 148 | function Pipeline(pipedMethodNames) { 149 | this.pipe = { 150 | _handlers: [], 151 | _contextCtor: PipeContext, 152 | _nextMethods: nextMethods, 153 | end: end, 154 | _pipedMethodNames: pipedMethodNames 155 | }; 156 | } 157 | 158 | var pipeline = new Pipeline(pipedMethodNames); 159 | for (var k in abstractPipeline) { 160 | pipeline.pipe[k] = abstractPipeline[k]; 161 | } 162 | 163 | pipedMethodNames.forEach(function(name) { 164 | end[name] = endStubFunc; 165 | 166 | nextMethods[name] = new Function( 167 | "var handler = this._nextHandler();" + 168 | "handler.__pipectx = this.__pipectx;" + 169 | "return handler." + name + ".apply(handler, arguments);"); 170 | 171 | pipeline[name] = new Function( 172 | "var ctx = new this.pipe._contextCtor(this.pipe._handlers, this.pipe._nextMethods." + name + ", this.pipe.end);" 173 | + "return ctx.next.apply(ctx, arguments);"); 174 | }); 175 | 176 | return pipeline; 177 | } 178 | 179 | createPipeline.isPipeline = function(obj) { 180 | return obj instanceof Pipeline; 181 | } 182 | var utils = (function() { 183 | return { 184 | convertToBase64: function(blob, cb) { 185 | var fr = new FileReader(); 186 | fr.onload = function(e) { 187 | cb(e.target.result); 188 | }; 189 | fr.onerror = function(e) { 190 | }; 191 | fr.onabort = function(e) { 192 | }; 193 | fr.readAsDataURL(blob); 194 | }, 195 | 196 | dataURLToBlob: function(dataURL) { 197 | var BASE64_MARKER = ';base64,'; 198 | if (dataURL.indexOf(BASE64_MARKER) == -1) { 199 | var parts = dataURL.split(','); 200 | var contentType = parts[0].split(':')[1]; 201 | var raw = parts[1]; 202 | 203 | return new Blob([raw], {type: contentType}); 204 | } 205 | 206 | var parts = dataURL.split(BASE64_MARKER); 207 | var contentType = parts[0].split(':')[1]; 208 | var raw = window.atob(parts[1]); 209 | var rawLength = raw.length; 210 | 211 | var uInt8Array = new Uint8Array(rawLength); 212 | 213 | for (var i = 0; i < rawLength; ++i) { 214 | uInt8Array[i] = raw.charCodeAt(i); 215 | } 216 | 217 | return new Blob([uInt8Array.buffer], {type: contentType}); 218 | }, 219 | 220 | splitAttachmentPath: function(path) { 221 | var parts = path.split('/'); 222 | if (parts.length == 1) 223 | parts.unshift('__nodoc__'); 224 | return parts; 225 | }, 226 | 227 | mapAsync: function(fn, promise) { 228 | var deferred = Q.defer(); 229 | promise.then(function(data) { 230 | _mapAsync(fn, data, [], deferred); 231 | }, function(e) { 232 | deferred.reject(e); 233 | }); 234 | 235 | return deferred.promise; 236 | }, 237 | 238 | countdown: function(n, cb) { 239 | var args = []; 240 | return function() { 241 | for (var i = 0; i < arguments.length; ++i) 242 | args.push(arguments[i]); 243 | n -= 1; 244 | if (n == 0) 245 | cb.apply(this, args); 246 | } 247 | } 248 | }; 249 | 250 | function _mapAsync(fn, data, result, deferred) { 251 | fn(data[result.length], function(v) { 252 | result.push(v); 253 | if (result.length == data.length) 254 | deferred.resolve(result); 255 | else 256 | _mapAsync(fn, data, result, deferred); 257 | }, function(err) { 258 | deferred.reject(err); 259 | }) 260 | } 261 | })(); 262 | var requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem; 263 | var persistentStorage = navigator.persistentStorage || navigator.webkitPersistentStorage; 264 | var FilesystemAPIProvider = (function(Q) { 265 | function makeErrorHandler(deferred, finalDeferred) { 266 | // TODO: normalize the error so 267 | // we can handle it upstream 268 | return function(e) { 269 | if (e.code == 1) { 270 | deferred.resolve(undefined); 271 | } else { 272 | if (finalDeferred) 273 | finalDeferred.reject(e); 274 | else 275 | deferred.reject(e); 276 | } 277 | } 278 | } 279 | 280 | function getAttachmentPath(docKey, attachKey) { 281 | docKey = docKey.replace(/\//g, '--'); 282 | var attachmentsDir = docKey + "-attachments"; 283 | return { 284 | dir: attachmentsDir, 285 | path: attachmentsDir + "/" + attachKey 286 | }; 287 | } 288 | 289 | function readDirEntries(reader, result) { 290 | var deferred = Q.defer(); 291 | 292 | _readDirEntries(reader, result, deferred); 293 | 294 | return deferred.promise; 295 | } 296 | 297 | function _readDirEntries(reader, result, deferred) { 298 | reader.readEntries(function(entries) { 299 | if (entries.length == 0) { 300 | deferred.resolve(result); 301 | } else { 302 | result = result.concat(entries); 303 | _readDirEntries(reader, result, deferred); 304 | } 305 | }, function(err) { 306 | deferred.reject(err); 307 | }); 308 | } 309 | 310 | function entryToFile(entry, cb, eb) { 311 | entry.file(cb, eb); 312 | } 313 | 314 | function entryToURL(entry) { 315 | return entry.toURL(); 316 | } 317 | 318 | function FSAPI(fs, numBytes, prefix) { 319 | this._fs = fs; 320 | this._capacity = numBytes; 321 | this._prefix = prefix; 322 | this.type = "FileSystemAPI"; 323 | } 324 | 325 | FSAPI.prototype = { 326 | getContents: function(path, options) { 327 | var deferred = Q.defer(); 328 | path = this._prefix + path; 329 | this._fs.root.getFile(path, {}, function(fileEntry) { 330 | fileEntry.file(function(file) { 331 | var reader = new FileReader(); 332 | 333 | reader.onloadend = function(e) { 334 | var data = e.target.result; 335 | var err; 336 | if (options && options.json) { 337 | try { 338 | data = JSON.parse(data); 339 | } catch(e) { 340 | err = new Error('unable to parse JSON for ' + path); 341 | } 342 | } 343 | 344 | if (err) { 345 | deferred.reject(err); 346 | } else { 347 | deferred.resolve(data); 348 | } 349 | }; 350 | 351 | reader.readAsText(file); 352 | }, makeErrorHandler(deferred)); 353 | }, makeErrorHandler(deferred)); 354 | 355 | return deferred.promise; 356 | }, 357 | 358 | // create a file at path 359 | // and write `data` to it 360 | setContents: function(path, data, options) { 361 | var deferred = Q.defer(); 362 | 363 | if (options && options.json) 364 | data = JSON.stringify(data); 365 | 366 | path = this._prefix + path; 367 | this._fs.root.getFile(path, {create:true}, function(fileEntry) { 368 | fileEntry.createWriter(function(fileWriter) { 369 | var blob; 370 | fileWriter.onwriteend = function(e) { 371 | fileWriter.onwriteend = function() { 372 | deferred.resolve(); 373 | }; 374 | fileWriter.truncate(blob.size); 375 | } 376 | 377 | fileWriter.onerror = makeErrorHandler(deferred); 378 | 379 | if (data instanceof Blob) { 380 | blob = data; 381 | } else { 382 | blob = new Blob([data], {type: 'text/plain'}); 383 | } 384 | 385 | fileWriter.write(blob); 386 | }, makeErrorHandler(deferred)); 387 | }, makeErrorHandler(deferred)); 388 | 389 | return deferred.promise; 390 | }, 391 | 392 | ls: function(docKey) { 393 | var isRoot = false; 394 | if (!docKey) {docKey = this._prefix; isRoot = true;} 395 | else docKey = this._prefix + docKey + "-attachments"; 396 | 397 | var deferred = Q.defer(); 398 | 399 | this._fs.root.getDirectory(docKey, {create:false}, 400 | function(entry) { 401 | var reader = entry.createReader(); 402 | readDirEntries(reader, []).then(function(entries) { 403 | var listing = []; 404 | entries.forEach(function(entry) { 405 | if (!entry.isDirectory) { 406 | listing.push(entry.name); 407 | } 408 | }); 409 | deferred.resolve(listing); 410 | }); 411 | }, function(error) { 412 | deferred.reject(error); 413 | }); 414 | 415 | return deferred.promise; 416 | }, 417 | 418 | clear: function() { 419 | var deferred = Q.defer(); 420 | var failed = false; 421 | var ecb = function(err) { 422 | failed = true; 423 | deferred.reject(err); 424 | } 425 | 426 | this._fs.root.getDirectory(this._prefix, {}, 427 | function(entry) { 428 | var reader = entry.createReader(); 429 | reader.readEntries(function(entries) { 430 | var latch = 431 | utils.countdown(entries.length, function() { 432 | if (!failed) 433 | deferred.resolve(); 434 | }); 435 | 436 | entries.forEach(function(entry) { 437 | if (entry.isDirectory) { 438 | entry.removeRecursively(latch, ecb); 439 | } else { 440 | entry.remove(latch, ecb); 441 | } 442 | }); 443 | 444 | if (entries.length == 0) 445 | deferred.resolve(); 446 | }, ecb); 447 | }, ecb); 448 | 449 | return deferred.promise; 450 | }, 451 | 452 | rm: function(path) { 453 | var deferred = Q.defer(); 454 | var finalDeferred = Q.defer(); 455 | 456 | // remove attachments that go along with the path 457 | path = this._prefix + path; 458 | var attachmentsDir = path + "-attachments"; 459 | 460 | this._fs.root.getFile(path, {create:false}, 461 | function(entry) { 462 | entry.remove(function() { 463 | deferred.promise.then(finalDeferred.resolve); 464 | }, function(err) { 465 | finalDeferred.reject(err); 466 | }); 467 | }, 468 | makeErrorHandler(finalDeferred)); 469 | 470 | this._fs.root.getDirectory(attachmentsDir, {}, 471 | function(entry) { 472 | entry.removeRecursively(function() { 473 | deferred.resolve(); 474 | }, function(err) { 475 | finalDeferred.reject(err); 476 | }); 477 | }, 478 | makeErrorHandler(deferred, finalDeferred)); 479 | 480 | return finalDeferred.promise; 481 | }, 482 | 483 | getAttachment: function(docKey, attachKey) { 484 | var attachmentPath = this._prefix + getAttachmentPath(docKey, attachKey).path; 485 | 486 | var deferred = Q.defer(); 487 | this._fs.root.getFile(attachmentPath, {}, function(fileEntry) { 488 | fileEntry.file(function(file) { 489 | if (file.size == 0) 490 | deferred.resolve(undefined); 491 | else 492 | deferred.resolve(file); 493 | }, makeErrorHandler(deferred)); 494 | }, function(err) { 495 | if (err.code == 1) { 496 | deferred.resolve(undefined); 497 | } else { 498 | deferred.reject(err); 499 | } 500 | }); 501 | 502 | return deferred.promise; 503 | }, 504 | 505 | getAttachmentURL: function(docKey, attachKey) { 506 | var attachmentPath = this._prefix + getAttachmentPath(docKey, attachKey).path; 507 | 508 | var deferred = Q.defer(); 509 | var url = 'filesystem:' + window.location.protocol + '//' + window.location.host + '/persistent/' + attachmentPath; 510 | deferred.resolve(url); 511 | // this._fs.root.getFile(attachmentPath, {}, function(fileEntry) { 512 | // deferred.resolve(fileEntry.toURL()); 513 | // }, makeErrorHandler(deferred, "getting attachment file entry")); 514 | 515 | return deferred.promise; 516 | }, 517 | 518 | getAllAttachments: function(docKey) { 519 | var deferred = Q.defer(); 520 | var attachmentsDir = this._prefix + docKey + "-attachments"; 521 | 522 | this._fs.root.getDirectory(attachmentsDir, {}, 523 | function(entry) { 524 | var reader = entry.createReader(); 525 | deferred.resolve( 526 | utils.mapAsync(function(entry, cb, eb) { 527 | entry.file(function(file) { 528 | cb({ 529 | data: file, 530 | docKey: docKey, 531 | attachKey: entry.name 532 | }); 533 | }, eb); 534 | }, readDirEntries(reader, []))); 535 | }, function(err) { 536 | deferred.resolve([]); 537 | }); 538 | 539 | return deferred.promise; 540 | }, 541 | 542 | getAllAttachmentURLs: function(docKey) { 543 | var deferred = Q.defer(); 544 | var attachmentsDir = this._prefix + docKey + "-attachments"; 545 | 546 | this._fs.root.getDirectory(attachmentsDir, {}, 547 | function(entry) { 548 | var reader = entry.createReader(); 549 | readDirEntries(reader, []).then(function(entries) { 550 | deferred.resolve(entries.map( 551 | function(entry) { 552 | return { 553 | url: entry.toURL(), 554 | docKey: docKey, 555 | attachKey: entry.name 556 | }; 557 | })); 558 | }); 559 | }, function(err) { 560 | deferred.reject(err); 561 | }); 562 | 563 | return deferred.promise; 564 | }, 565 | 566 | revokeAttachmentURL: function(url) { 567 | // we return FS urls so this is a no-op 568 | // unless someone is being silly and doing 569 | // createObjectURL(getAttachment()) ...... 570 | }, 571 | 572 | // Create a folder at dirname(path)+"-attachments" 573 | // add attachment under that folder as basename(path) 574 | setAttachment: function(docKey, attachKey, data) { 575 | var attachInfo = getAttachmentPath(docKey, attachKey); 576 | 577 | var deferred = Q.defer(); 578 | 579 | var self = this; 580 | this._fs.root.getDirectory(this._prefix + attachInfo.dir, 581 | {create:true}, function(dirEntry) { 582 | deferred.resolve(self.setContents(attachInfo.path, data)); 583 | }, makeErrorHandler(deferred)); 584 | 585 | return deferred.promise; 586 | }, 587 | 588 | // rm the thing at dirname(path)+"-attachments/"+basename(path) 589 | rmAttachment: function(docKey, attachKey) { 590 | var attachmentPath = getAttachmentPath(docKey, attachKey).path; 591 | 592 | var deferred = Q.defer(); 593 | this._fs.root.getFile(this._prefix + attachmentPath, {create:false}, 594 | function(entry) { 595 | entry.remove(function() { 596 | deferred.resolve(); 597 | }, makeErrorHandler(deferred)); 598 | }, makeErrorHandler(deferred)); 599 | 600 | return deferred.promise; 601 | }, 602 | 603 | getCapacity: function() { 604 | return this._capacity; 605 | } 606 | }; 607 | 608 | return { 609 | init: function(config) { 610 | var deferred = Q.defer(); 611 | 612 | if (!requestFileSystem) { 613 | deferred.reject("No FS API"); 614 | return deferred.promise; 615 | } 616 | 617 | var prefix = config.name + '/'; 618 | 619 | persistentStorage.requestQuota(config.size, 620 | function(numBytes) { 621 | requestFileSystem(window.PERSISTENT, numBytes, 622 | function(fs) { 623 | fs.root.getDirectory(config.name, {create: true}, 624 | function() { 625 | deferred.resolve(new FSAPI(fs, numBytes, prefix)); 626 | }, function(err) { 627 | console.error(err); 628 | deferred.reject(err); 629 | }); 630 | }, function(err) { 631 | // TODO: implement various error messages. 632 | console.error(err); 633 | deferred.reject(err); 634 | }); 635 | }, function(err) { 636 | // TODO: implement various error messages. 637 | console.error(err); 638 | deferred.reject(err); 639 | }); 640 | 641 | return deferred.promise; 642 | }, 643 | 644 | isAvailable: function() { 645 | return requestFileSystem != null; 646 | } 647 | } 648 | })(Q); 649 | var indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.OIndexedDB || window.msIndexedDB; 650 | var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.OIDBTransaction || window.msIDBTransaction; 651 | var IndexedDBProvider = (function(Q) { 652 | var URL = window.URL || window.webkitURL; 653 | 654 | var convertToBase64 = utils.convertToBase64; 655 | var dataURLToBlob = utils.dataURLToBlob; 656 | 657 | function IDB(db) { 658 | this._db = db; 659 | this.type = 'IndexedDB'; 660 | 661 | var transaction = this._db.transaction(['attachments'], 'readwrite'); 662 | this._supportsBlobs = true; 663 | try { 664 | transaction.objectStore('attachments') 665 | .put(Blob(["sdf"], {type: "text/plain"}), "featurecheck"); 666 | } catch (e) { 667 | this._supportsBlobs = false; 668 | } 669 | } 670 | 671 | // TODO: normalize returns and errors. 672 | IDB.prototype = { 673 | getContents: function(docKey) { 674 | var deferred = Q.defer(); 675 | var transaction = this._db.transaction(['files'], 'readonly'); 676 | 677 | var get = transaction.objectStore('files').get(docKey); 678 | get.onsuccess = function(e) { 679 | deferred.resolve(e.target.result); 680 | }; 681 | 682 | get.onerror = function(e) { 683 | deferred.reject(e); 684 | }; 685 | 686 | return deferred.promise; 687 | }, 688 | 689 | setContents: function(docKey, data) { 690 | var deferred = Q.defer(); 691 | var transaction = this._db.transaction(['files'], 'readwrite'); 692 | 693 | var put = transaction.objectStore('files').put(data, docKey); 694 | put.onsuccess = function(e) { 695 | deferred.resolve(e); 696 | }; 697 | 698 | put.onerror = function(e) { 699 | deferred.reject(e); 700 | }; 701 | 702 | return deferred.promise; 703 | }, 704 | 705 | rm: function(docKey) { 706 | var deferred = Q.defer(); 707 | var finalDeferred = Q.defer(); 708 | 709 | var transaction = this._db.transaction(['files', 'attachments'], 'readwrite'); 710 | 711 | var del = transaction.objectStore('files').delete(docKey); 712 | 713 | del.onsuccess = function(e) { 714 | deferred.promise.then(function() { 715 | finalDeferred.resolve(); 716 | }); 717 | }; 718 | 719 | del.onerror = function(e) { 720 | deferred.promise.catch(function() { 721 | finalDeferred.reject(e); 722 | }); 723 | }; 724 | 725 | var attachmentsStore = transaction.objectStore('attachments'); 726 | var index = attachmentsStore.index('fname'); 727 | var cursor = index.openCursor(IDBKeyRange.only(docKey)); 728 | cursor.onsuccess = function(e) { 729 | var cursor = e.target.result; 730 | if (cursor) { 731 | cursor.delete(); 732 | cursor.continue(); 733 | } else { 734 | deferred.resolve(); 735 | } 736 | }; 737 | 738 | cursor.onerror = function(e) { 739 | deferred.reject(e); 740 | } 741 | 742 | return finalDeferred.promise; 743 | }, 744 | 745 | getAttachment: function(docKey, attachKey) { 746 | var deferred = Q.defer(); 747 | 748 | var transaction = this._db.transaction(['attachments'], 'readonly'); 749 | var get = transaction.objectStore('attachments').get(docKey + '/' + attachKey); 750 | 751 | var self = this; 752 | get.onsuccess = function(e) { 753 | if (!e.target.result) { 754 | deferred.resolve(undefined); 755 | return; 756 | } 757 | 758 | var data = e.target.result.data; 759 | if (!self._supportsBlobs) { 760 | data = dataURLToBlob(data); 761 | } 762 | deferred.resolve(data); 763 | }; 764 | 765 | get.onerror = function(e) { 766 | deferred.reject(e); 767 | }; 768 | 769 | return deferred.promise; 770 | }, 771 | 772 | ls: function(docKey) { 773 | var deferred = Q.defer(); 774 | 775 | if (!docKey) { 776 | // list docs 777 | var store = 'files'; 778 | } else { 779 | // list attachments 780 | var store = 'attachments'; 781 | } 782 | 783 | var transaction = this._db.transaction([store], 'readonly'); 784 | var cursor = transaction.objectStore(store).openCursor(); 785 | var listing = []; 786 | 787 | cursor.onsuccess = function(e) { 788 | var cursor = e.target.result; 789 | if (cursor) { 790 | listing.push(!docKey ? cursor.key : cursor.key.split('/')[1]); 791 | cursor.continue(); 792 | } else { 793 | deferred.resolve(listing); 794 | } 795 | }; 796 | 797 | cursor.onerror = function(e) { 798 | deferred.reject(e); 799 | }; 800 | 801 | return deferred.promise; 802 | }, 803 | 804 | clear: function() { 805 | var deferred = Q.defer(); 806 | var finalDeferred = Q.defer(); 807 | 808 | var t = this._db.transaction(['attachments', 'files'], 'readwrite'); 809 | 810 | 811 | var req1 = t.objectStore('attachments').clear(); 812 | var req2 = t.objectStore('files').clear(); 813 | 814 | req1.onsuccess = function() { 815 | deferred.promise.then(finalDeferred.resolve); 816 | }; 817 | 818 | req2.onsuccess = function() { 819 | deferred.resolve(); 820 | }; 821 | 822 | req1.onerror = function(err) { 823 | finalDeferred.reject(err); 824 | }; 825 | 826 | req2.onerror = function(err) { 827 | finalDeferred.reject(err); 828 | }; 829 | 830 | return finalDeferred.promise; 831 | }, 832 | 833 | getAllAttachments: function(docKey) { 834 | var deferred = Q.defer(); 835 | var self = this; 836 | 837 | var transaction = this._db.transaction(['attachments'], 'readonly'); 838 | var index = transaction.objectStore('attachments').index('fname'); 839 | 840 | var cursor = index.openCursor(IDBKeyRange.only(docKey)); 841 | var values = []; 842 | cursor.onsuccess = function(e) { 843 | var cursor = e.target.result; 844 | if (cursor) { 845 | var data; 846 | if (!self._supportsBlobs) { 847 | data = dataURLToBlob(cursor.value.data) 848 | } else { 849 | data = cursor.value.data; 850 | } 851 | values.push({ 852 | data: data, 853 | docKey: docKey, 854 | attachKey: cursor.primaryKey.split('/')[1] // TODO 855 | }); 856 | cursor.continue(); 857 | } else { 858 | deferred.resolve(values); 859 | } 860 | }; 861 | 862 | cursor.onerror = function(e) { 863 | deferred.reject(e); 864 | }; 865 | 866 | return deferred.promise; 867 | }, 868 | 869 | getAllAttachmentURLs: function(docKey) { 870 | var deferred = Q.defer(); 871 | this.getAllAttachments(docKey).then(function(attachments) { 872 | var urls = attachments.map(function(a) { 873 | a.url = URL.createObjectURL(a.data); 874 | delete a.data; 875 | return a; 876 | }); 877 | 878 | deferred.resolve(urls); 879 | }, function(e) { 880 | deferred.reject(e); 881 | }); 882 | 883 | return deferred.promise; 884 | }, 885 | 886 | getAttachmentURL: function(docKey, attachKey) { 887 | var deferred = Q.defer(); 888 | this.getAttachment(docKey, attachKey).then(function(attachment) { 889 | deferred.resolve(URL.createObjectURL(attachment)); 890 | }, function(e) { 891 | deferred.reject(e); 892 | }); 893 | 894 | return deferred.promise; 895 | }, 896 | 897 | revokeAttachmentURL: function(url) { 898 | URL.revokeObjectURL(url); 899 | }, 900 | 901 | setAttachment: function(docKey, attachKey, data) { 902 | var deferred = Q.defer(); 903 | 904 | if (data instanceof Blob && !this._supportsBlobs) { 905 | var self = this; 906 | convertToBase64(data, function(data) { 907 | continuation.call(self, data); 908 | }); 909 | } else { 910 | continuation.call(this, data); 911 | } 912 | 913 | function continuation(data) { 914 | var obj = { 915 | path: docKey + '/' + attachKey, 916 | fname: docKey, 917 | data: data 918 | }; 919 | var transaction = this._db.transaction(['attachments'], 'readwrite'); 920 | var put = transaction.objectStore('attachments').put(obj); 921 | 922 | put.onsuccess = function(e) { 923 | deferred.resolve(e); 924 | }; 925 | 926 | put.onerror = function(e) { 927 | deferred.reject(e); 928 | }; 929 | } 930 | 931 | return deferred.promise; 932 | }, 933 | 934 | rmAttachment: function(docKey, attachKey) { 935 | var deferred = Q.defer(); 936 | var transaction = this._db.transaction(['attachments'], 'readwrite'); 937 | var del = transaction.objectStore('attachments').delete(docKey + '/' + attachKey); 938 | 939 | del.onsuccess = function(e) { 940 | deferred.resolve(e); 941 | }; 942 | 943 | del.onerror = function(e) { 944 | deferred.reject(e); 945 | }; 946 | 947 | return deferred.promise; 948 | } 949 | }; 950 | 951 | return { 952 | init: function(config) { 953 | var deferred = Q.defer(); 954 | var dbVersion = 2; 955 | 956 | if (!indexedDB || !IDBTransaction) { 957 | deferred.reject("No IndexedDB"); 958 | return deferred.promise; 959 | } 960 | 961 | var request = indexedDB.open(config.name, dbVersion); 962 | 963 | function createObjectStore(db) { 964 | db.createObjectStore("files"); 965 | var attachStore = db.createObjectStore("attachments", {keyPath: 'path'}); 966 | attachStore.createIndex('fname', 'fname', {unique: false}) 967 | } 968 | 969 | // TODO: normalize errors 970 | request.onerror = function (event) { 971 | deferred.reject(event); 972 | }; 973 | 974 | request.onsuccess = function (event) { 975 | var db = request.result; 976 | 977 | db.onerror = function (event) { 978 | console.log(event); 979 | }; 980 | 981 | // Chrome workaround 982 | if (db.setVersion) { 983 | if (db.version != dbVersion) { 984 | var setVersion = db.setVersion(dbVersion); 985 | setVersion.onsuccess = function () { 986 | createObjectStore(db); 987 | deferred.resolve(); 988 | }; 989 | } 990 | else { 991 | deferred.resolve(new IDB(db)); 992 | } 993 | } else { 994 | deferred.resolve(new IDB(db)); 995 | } 996 | } 997 | 998 | request.onupgradeneeded = function (event) { 999 | createObjectStore(event.target.result); 1000 | }; 1001 | 1002 | return deferred.promise; 1003 | }, 1004 | 1005 | isAvailable: function() { 1006 | return indexedDB != null && IDBTransaction != null; 1007 | } 1008 | } 1009 | })(Q); 1010 | var LocalStorageProvider = (function(Q) { 1011 | return { 1012 | init: function() { 1013 | return Q({type: 'LocalStorage'}); 1014 | } 1015 | } 1016 | })(Q); 1017 | var openDb = window.openDatabase; 1018 | var WebSQLProvider = (function(Q) { 1019 | var URL = window.URL || window.webkitURL; 1020 | var convertToBase64 = utils.convertToBase64; 1021 | var dataURLToBlob = utils.dataURLToBlob; 1022 | 1023 | function WSQL(db) { 1024 | this._db = db; 1025 | this.type = 'WebSQL'; 1026 | } 1027 | 1028 | WSQL.prototype = { 1029 | getContents: function(docKey, options) { 1030 | var deferred = Q.defer(); 1031 | this._db.transaction(function(tx) { 1032 | tx.executeSql('SELECT value FROM files WHERE fname = ?', [docKey], 1033 | function(tx, res) { 1034 | if (res.rows.length == 0) { 1035 | deferred.resolve(undefined); 1036 | } else { 1037 | var data = res.rows.item(0).value; 1038 | if (options && options.json) 1039 | data = JSON.parse(data); 1040 | deferred.resolve(data); 1041 | } 1042 | }); 1043 | }, function(err) { 1044 | consol.log(err); 1045 | deferred.reject(err); 1046 | }); 1047 | 1048 | return deferred.promise; 1049 | }, 1050 | 1051 | setContents: function(docKey, data, options) { 1052 | var deferred = Q.defer(); 1053 | if (options && options.json) 1054 | data = JSON.stringify(data); 1055 | 1056 | this._db.transaction(function(tx) { 1057 | tx.executeSql( 1058 | 'INSERT OR REPLACE INTO files (fname, value) VALUES(?, ?)', [docKey, data]); 1059 | }, function(err) { 1060 | console.log(err); 1061 | deferred.reject(err); 1062 | }, function() { 1063 | deferred.resolve(); 1064 | }); 1065 | 1066 | return deferred.promise; 1067 | }, 1068 | 1069 | rm: function(docKey) { 1070 | var deferred = Q.defer(); 1071 | 1072 | this._db.transaction(function(tx) { 1073 | tx.executeSql('DELETE FROM files WHERE fname = ?', [docKey]); 1074 | tx.executeSql('DELETE FROM attachments WHERE fname = ?', [docKey]); 1075 | }, function(err) { 1076 | console.log(err); 1077 | deferred.reject(err); 1078 | }, function() { 1079 | deferred.resolve(); 1080 | }); 1081 | 1082 | return deferred.promise; 1083 | }, 1084 | 1085 | getAttachment: function(fname, akey) { 1086 | var deferred = Q.defer(); 1087 | 1088 | this._db.transaction(function(tx){ 1089 | tx.executeSql('SELECT value FROM attachments WHERE fname = ? AND akey = ?', 1090 | [fname, akey], 1091 | function(tx, res) { 1092 | if (res.rows.length == 0) { 1093 | deferred.resolve(undefined); 1094 | } else { 1095 | deferred.resolve(dataURLToBlob(res.rows.item(0).value)); 1096 | } 1097 | }); 1098 | }, function(err) { 1099 | deferred.reject(err); 1100 | }); 1101 | 1102 | return deferred.promise; 1103 | }, 1104 | 1105 | getAttachmentURL: function(docKey, attachKey) { 1106 | var deferred = Q.defer(); 1107 | this.getAttachment(docKey, attachKey).then(function(blob) { 1108 | deferred.resolve(URL.createObjectURL(blob)); 1109 | }, function() { 1110 | deferred.reject(); 1111 | }); 1112 | 1113 | return deferred.promise; 1114 | }, 1115 | 1116 | ls: function(docKey) { 1117 | var deferred = Q.defer(); 1118 | 1119 | var select; 1120 | var field; 1121 | if (!docKey) { 1122 | select = 'SELECT fname FROM files'; 1123 | field = 'fname'; 1124 | } else { 1125 | select = 'SELECT akey FROM attachments WHERE fname = ?'; 1126 | field = 'akey'; 1127 | } 1128 | 1129 | this._db.transaction(function(tx) { 1130 | tx.executeSql(select, docKey ? [docKey] : [], 1131 | function(tx, res) { 1132 | var listing = []; 1133 | for (var i = 0; i < res.rows.length; ++i) { 1134 | listing.push(res.rows.item(i)[field]); 1135 | } 1136 | 1137 | deferred.resolve(listing); 1138 | }, function(err) { 1139 | deferred.reject(err); 1140 | }); 1141 | }); 1142 | 1143 | return deferred.promise; 1144 | }, 1145 | 1146 | clear: function() { 1147 | var deffered1 = Q.defer(); 1148 | var deffered2 = Q.defer(); 1149 | 1150 | this._db.transaction(function(tx) { 1151 | tx.executeSql('DELETE FROM files', function() { 1152 | deffered1.resolve(); 1153 | }); 1154 | tx.executeSql('DELETE FROM attachments', function() { 1155 | deffered2.resolve(); 1156 | }); 1157 | }, function(err) { 1158 | deffered1.reject(err); 1159 | deffered2.reject(err); 1160 | }); 1161 | 1162 | return Q.all([deffered1, deffered2]); 1163 | }, 1164 | 1165 | getAllAttachments: function(fname) { 1166 | var deferred = Q.defer(); 1167 | 1168 | this._db.transaction(function(tx) { 1169 | tx.executeSql('SELECT value, akey FROM attachments WHERE fname = ?', 1170 | [fname], 1171 | function(tx, res) { 1172 | // TODO: ship this work off to a webworker 1173 | // since there could be many of these conversions? 1174 | var result = []; 1175 | for (var i = 0; i < res.rows.length; ++i) { 1176 | var item = res.rows.item(i); 1177 | result.push({ 1178 | docKey: fname, 1179 | attachKey: item.akey, 1180 | data: dataURLToBlob(item.value) 1181 | }); 1182 | } 1183 | 1184 | deferred.resolve(result); 1185 | }); 1186 | }, function(err) { 1187 | deferred.reject(err); 1188 | }); 1189 | 1190 | return deferred.promise; 1191 | }, 1192 | 1193 | getAllAttachmentURLs: function(fname) { 1194 | var deferred = Q.defer(); 1195 | this.getAllAttachments(fname).then(function(attachments) { 1196 | var urls = attachments.map(function(a) { 1197 | a.url = URL.createObjectURL(a.data); 1198 | delete a.data; 1199 | return a; 1200 | }); 1201 | 1202 | deferred.resolve(urls); 1203 | }, function(e) { 1204 | deferred.reject(e); 1205 | }); 1206 | 1207 | return deferred.promise; 1208 | }, 1209 | 1210 | revokeAttachmentURL: function(url) { 1211 | URL.revokeObjectURL(url); 1212 | }, 1213 | 1214 | setAttachment: function(fname, akey, data) { 1215 | var deferred = Q.defer(); 1216 | 1217 | var self = this; 1218 | convertToBase64(data, function(data) { 1219 | self._db.transaction(function(tx) { 1220 | tx.executeSql( 1221 | 'INSERT OR REPLACE INTO attachments (fname, akey, value) VALUES(?, ?, ?)', 1222 | [fname, akey, data]); 1223 | }, function(err) { 1224 | deferred.reject(err); 1225 | }, function() { 1226 | deferred.resolve(); 1227 | }); 1228 | }); 1229 | 1230 | return deferred.promise; 1231 | }, 1232 | 1233 | rmAttachment: function(fname, akey) { 1234 | var deferred = Q.defer(); 1235 | this._db.transaction(function(tx) { 1236 | tx.executeSql('DELETE FROM attachments WHERE fname = ? AND akey = ?', 1237 | [fname, akey]); 1238 | }, function(err) { 1239 | deferred.reject(err); 1240 | }, function() { 1241 | deferred.resolve(); 1242 | }); 1243 | 1244 | return deferred.promise; 1245 | } 1246 | }; 1247 | 1248 | return { 1249 | init: function(config) { 1250 | var deferred = Q.defer(); 1251 | if (!openDb) { 1252 | deferred.reject("No WebSQL"); 1253 | return deferred.promise; 1254 | } 1255 | 1256 | var db = openDb(config.name, '1.0', 'large local storage', config.size); 1257 | 1258 | db.transaction(function(tx) { 1259 | tx.executeSql('CREATE TABLE IF NOT EXISTS files (fname unique, value)'); 1260 | tx.executeSql('CREATE TABLE IF NOT EXISTS attachments (fname, akey, value)'); 1261 | tx.executeSql('CREATE INDEX IF NOT EXISTS fname_index ON attachments (fname)'); 1262 | tx.executeSql('CREATE INDEX IF NOT EXISTS akey_index ON attachments (akey)'); 1263 | tx.executeSql('CREATE UNIQUE INDEX IF NOT EXISTS uniq_attach ON attachments (fname, akey)') 1264 | }, function(err) { 1265 | deferred.reject(err); 1266 | }, function() { 1267 | deferred.resolve(new WSQL(db)); 1268 | }); 1269 | 1270 | return deferred.promise; 1271 | }, 1272 | 1273 | isAvailable: function() { 1274 | return openDb != null; 1275 | } 1276 | } 1277 | })(Q); 1278 | var LargeLocalStorage = (function(Q) { 1279 | var sessionMeta = localStorage.getItem('LargeLocalStorage-meta'); 1280 | if (sessionMeta) 1281 | sessionMeta = JSON.parse(sessionMeta); 1282 | else 1283 | sessionMeta = {}; 1284 | 1285 | window.addEventListener('beforeunload', function() { 1286 | localStorage.setItem('LargeLocalStorage-meta', JSON.stringify(sessionMeta)); 1287 | }); 1288 | 1289 | function defaults(options, defaultOptions) { 1290 | for (var k in defaultOptions) { 1291 | if (options[k] === undefined) 1292 | options[k] = defaultOptions[k]; 1293 | } 1294 | 1295 | return options; 1296 | } 1297 | 1298 | var providers = { 1299 | FileSystemAPI: FilesystemAPIProvider, 1300 | IndexedDB: IndexedDBProvider, 1301 | WebSQL: WebSQLProvider 1302 | // LocalStorage: LocalStorageProvider 1303 | } 1304 | 1305 | var defaultConfig = { 1306 | size: 10 * 1024 * 1024, 1307 | name: 'lls' 1308 | }; 1309 | 1310 | function selectImplementation(config) { 1311 | if (!config) config = {}; 1312 | config = defaults(config, defaultConfig); 1313 | 1314 | if (config.forceProvider) { 1315 | return providers[config.forceProvider].init(config); 1316 | } 1317 | 1318 | return FilesystemAPIProvider.init(config).then(function(impl) { 1319 | return Q(impl); 1320 | }, function() { 1321 | return IndexedDBProvider.init(config); 1322 | }).then(function(impl) { 1323 | return Q(impl); 1324 | }, function() { 1325 | return WebSQLProvider.init(config); 1326 | }).then(function(impl) { 1327 | return Q(impl); 1328 | }, function() { 1329 | console.error('Unable to create any storage implementations. Using LocalStorage'); 1330 | return LocalStorageProvider.init(config); 1331 | }); 1332 | } 1333 | 1334 | function copy(obj) { 1335 | var result = {}; 1336 | Object.keys(obj).forEach(function(key) { 1337 | result[key] = obj[key]; 1338 | }); 1339 | 1340 | return result; 1341 | } 1342 | 1343 | function handleDataMigration(storageInstance, config, previousProviderType, currentProivderType) { 1344 | var previousProviderType = 1345 | sessionMeta[config.name] && sessionMeta[config.name].lastStorageImpl; 1346 | if (config.migrate) { 1347 | if (previousProviderType != currentProivderType 1348 | && previousProviderType in providers) { 1349 | config = copy(config); 1350 | config.forceProvider = previousProviderType; 1351 | selectImplementation(config).then(function(prevImpl) { 1352 | config.migrate(null, prevImpl, storageInstance, config); 1353 | }, function(e) { 1354 | config.migrate(e); 1355 | }); 1356 | } else { 1357 | if (config.migrationComplete) 1358 | config.migrationComplete(); 1359 | } 1360 | } 1361 | } 1362 | 1363 | /** 1364 | * 1365 | * LargeLocalStorage (or LLS) gives you a large capacity 1366 | * (up to several gig with permission from the user) 1367 | * key-value store in the browser. 1368 | * 1369 | * For storage, LLS uses the [FilesystemAPI](https://developer.mozilla.org/en-US/docs/WebGuide/API/File_System) 1370 | * when running in Chrome and Opera, 1371 | * [IndexedDB](https://developer.mozilla.org/en-US/docs/IndexedDB) in Firefox and IE 1372 | * and [WebSQL](http://www.w3.org/TR/webdatabase/) in Safari. 1373 | * 1374 | * When IndexedDB becomes available in Safari, LLS will 1375 | * update to take advantage of that storage implementation. 1376 | * 1377 | * 1378 | * Upon construction a LargeLocalStorage (LLS) object will be 1379 | * immediately returned but not necessarily immediately ready for use. 1380 | * 1381 | * A LLS object has an `initialized` property which is a promise 1382 | * that is resolved when the LLS object is ready for us. 1383 | * 1384 | * Usage of LLS would typically be: 1385 | * ``` 1386 | * var storage = new LargeLocalStorage({size: 75*1024*1024}); 1387 | * storage.initialized.then(function(grantedCapacity) { 1388 | * // storage ready to be used. 1389 | * }); 1390 | * ``` 1391 | * 1392 | * The reason that LLS may not be immediately ready for 1393 | * use is that some browsers require confirmation from the 1394 | * user before a storage area may be created. Also, 1395 | * the browser's native storage APIs are asynchronous. 1396 | * 1397 | * If an LLS instance is used before the storage 1398 | * area is ready then any 1399 | * calls to it will throw an exception with code: "NO_IMPLEMENTATION" 1400 | * 1401 | * This behavior is useful when you want the application 1402 | * to continue to function--regardless of whether or 1403 | * not the user has allowed it to store data--and would 1404 | * like to know when your storage calls fail at the point 1405 | * of those calls. 1406 | * 1407 | * LLS-contrib has utilities to queue storage calls until 1408 | * the implementation is ready. If an implementation 1409 | * is never ready this could obviously lead to memory issues 1410 | * which is why it is not the default behavior. 1411 | * 1412 | * @example 1413 | * var desiredCapacity = 50 * 1024 * 1024; // 50MB 1414 | * var storage = new LargeLocalStorage({ 1415 | * // desired capacity, in bytes. 1416 | * size: desiredCapacity, 1417 | * 1418 | * // optional name for your LLS database. Defaults to lls. 1419 | * // This is the name given to the underlying 1420 | * // IndexedDB or WebSQL DB or FSAPI Folder. 1421 | * // LLS's with different names are independent. 1422 | * name: 'myStorage' 1423 | * 1424 | * // the following is an optional param 1425 | * // that is useful for debugging. 1426 | * // force LLS to use a specific storage implementation 1427 | * // forceProvider: 'IndexedDB' or 'WebSQL' or 'FilesystemAPI' 1428 | * 1429 | * // These parameters can be used to migrate data from one 1430 | * // storage implementation to another 1431 | * // migrate: LargeLocalStorage.copyOldData, 1432 | * // migrationComplete: function(err) { 1433 | * // db is initialized and old data has been copied. 1434 | * // } 1435 | * }); 1436 | * storage.initialized.then(function(capacity) { 1437 | * if (capacity != -1 && capacity != desiredCapacity) { 1438 | * // the user didn't authorize your storage request 1439 | * // so instead you have some limitation on your storage 1440 | * } 1441 | * }) 1442 | * 1443 | * @class LargeLocalStorage 1444 | * @constructor 1445 | * @param {object} config {size: sizeInByes, [forceProvider: force a specific implementation]} 1446 | * @return {LargeLocalStorage} 1447 | */ 1448 | function LargeLocalStorage(config) { 1449 | var deferred = Q.defer(); 1450 | /** 1451 | * @property {promise} initialized 1452 | */ 1453 | this.initialized = deferred.promise; 1454 | 1455 | var piped = createPipeline([ 1456 | 'ready', 1457 | 'ls', 1458 | 'rm', 1459 | 'clear', 1460 | 'getContents', 1461 | 'setContents', 1462 | 'getAttachment', 1463 | 'setAttachment', 1464 | 'getAttachmentURL', 1465 | 'getAllAttachments', 1466 | 'getAllAttachmentURLs', 1467 | 'revokeAttachmentURL', 1468 | 'rmAttachment', 1469 | 'getCapacity', 1470 | 'initialized']); 1471 | 1472 | piped.pipe.addLast('lls', this); 1473 | piped.initialized = this.initialized; 1474 | 1475 | var self = this; 1476 | selectImplementation(config).then(function(impl) { 1477 | self._impl = impl; 1478 | handleDataMigration(piped, config, self._impl.type); 1479 | sessionMeta[config.name] = sessionMeta[config.name] || {}; 1480 | sessionMeta[config.name].lastStorageImpl = impl.type; 1481 | deferred.resolve(piped); 1482 | }).catch(function(e) { 1483 | // This should be impossible 1484 | console.log(e); 1485 | deferred.reject('No storage provider found'); 1486 | }); 1487 | 1488 | return piped; 1489 | } 1490 | 1491 | LargeLocalStorage.prototype = { 1492 | /** 1493 | * Whether or not LLS is ready to store data. 1494 | * The `initialized` property can be used to 1495 | * await initialization. 1496 | * @example 1497 | * // may or may not be true 1498 | * storage.ready(); 1499 | * 1500 | * storage.initialized.then(function() { 1501 | * // always true 1502 | * storage.ready(); 1503 | * }) 1504 | * @method ready 1505 | */ 1506 | ready: function() { 1507 | return this._impl != null; 1508 | }, 1509 | 1510 | /** 1511 | * List all attachments under a given key. 1512 | * 1513 | * List all documents if no key is provided. 1514 | * 1515 | * Returns a promise that is fulfilled with 1516 | * the listing. 1517 | * 1518 | * @example 1519 | * storage.ls().then(function(docKeys) { 1520 | * console.log(docKeys); 1521 | * }) 1522 | * 1523 | * @method ls 1524 | * @param {string} [docKey] 1525 | * @returns {promise} resolved with the listing, rejected if the listing fails. 1526 | */ 1527 | ls: function(docKey) { 1528 | this._checkAvailability(); 1529 | return this._impl.ls(docKey); 1530 | }, 1531 | 1532 | /** 1533 | * Remove the specified document and all 1534 | * of its attachments. 1535 | * 1536 | * Returns a promise that is fulfilled when the 1537 | * removal completes. 1538 | * 1539 | * If no docKey is specified, this throws an error. 1540 | * 1541 | * To remove all files in LargeLocalStorage call 1542 | * `lls.clear();` 1543 | * 1544 | * To remove all attachments that were written without 1545 | * a docKey, call `lls.rm('__emptydoc__');` 1546 | * 1547 | * rm works this way to ensure you don't lose 1548 | * data due to an accidently undefined variable. 1549 | * 1550 | * @example 1551 | * stoarge.rm('exampleDoc').then(function() { 1552 | * alert('doc and all attachments were removed'); 1553 | * }) 1554 | * 1555 | * @method rm 1556 | * @param {string} docKey 1557 | * @returns {promise} resolved when removal completes, rejected if the removal fails. 1558 | */ 1559 | rm: function(docKey) { 1560 | this._checkAvailability(); 1561 | return this._impl.rm(docKey); 1562 | }, 1563 | 1564 | /** 1565 | * An explicit way to remove all documents and 1566 | * attachments from LargeLocalStorage. 1567 | * 1568 | * @example 1569 | * storage.clear().then(function() { 1570 | * alert('all data has been removed'); 1571 | * }); 1572 | * 1573 | * @returns {promise} resolve when clear completes, rejected if clear fails. 1574 | */ 1575 | clear: function() { 1576 | this._checkAvailability(); 1577 | return this._impl.clear(); 1578 | }, 1579 | 1580 | /** 1581 | * Get the contents of a document identified by `docKey` 1582 | * TODO: normalize all implementations to allow storage 1583 | * and retrieval of JS objects? 1584 | * 1585 | * @example 1586 | * storage.getContents('exampleDoc').then(function(contents) { 1587 | * alert(contents); 1588 | * }); 1589 | * 1590 | * @method getContents 1591 | * @param {string} docKey 1592 | * @returns {promise} resolved with the contents when the get completes 1593 | */ 1594 | getContents: function(docKey, options) { 1595 | this._checkAvailability(); 1596 | return this._impl.getContents(docKey, options); 1597 | }, 1598 | 1599 | /** 1600 | * Set the contents identified by `docKey` to `data`. 1601 | * The document will be created if it does not exist. 1602 | * 1603 | * @example 1604 | * storage.setContents('exampleDoc', 'some data...').then(function() { 1605 | * alert('doc written'); 1606 | * }); 1607 | * 1608 | * @method setContents 1609 | * @param {string} docKey 1610 | * @param {any} data 1611 | * @returns {promise} fulfilled when set completes 1612 | */ 1613 | setContents: function(docKey, data, options) { 1614 | this._checkAvailability(); 1615 | return this._impl.setContents(docKey, data, options); 1616 | }, 1617 | 1618 | /** 1619 | * Get the attachment identified by `docKey` and `attachKey` 1620 | * 1621 | * @example 1622 | * storage.getAttachment('exampleDoc', 'examplePic').then(function(attachment) { 1623 | * var url = URL.createObjectURL(attachment); 1624 | * var image = new Image(url); 1625 | * document.body.appendChild(image); 1626 | * URL.revokeObjectURL(url); 1627 | * }) 1628 | * 1629 | * @method getAttachment 1630 | * @param {string} [docKey] Defaults to `__emptydoc__` 1631 | * @param {string} attachKey key of the attachment 1632 | * @returns {promise} fulfilled with the attachment or 1633 | * rejected if it could not be found. code: 1 1634 | */ 1635 | getAttachment: function(docKey, attachKey) { 1636 | if (!docKey) docKey = '__emptydoc__'; 1637 | this._checkAvailability(); 1638 | return this._impl.getAttachment(docKey, attachKey); 1639 | }, 1640 | 1641 | /** 1642 | * Set an attachment for a given document. Identified 1643 | * by `docKey` and `attachKey`. 1644 | * 1645 | * @example 1646 | * storage.setAttachment('myDoc', 'myPic', blob).then(function() { 1647 | * alert('Attachment written'); 1648 | * }) 1649 | * 1650 | * @method setAttachment 1651 | * @param {string} [docKey] Defaults to `__emptydoc__` 1652 | * @param {string} attachKey key for the attachment 1653 | * @param {any} attachment data 1654 | * @returns {promise} resolved when the write completes. Rejected 1655 | * if an error occurs. 1656 | */ 1657 | setAttachment: function(docKey, attachKey, data) { 1658 | if (!docKey) docKey = '__emptydoc__'; 1659 | this._checkAvailability(); 1660 | return this._impl.setAttachment(docKey, attachKey, data); 1661 | }, 1662 | 1663 | /** 1664 | * Get the URL for a given attachment. 1665 | * 1666 | * @example 1667 | * storage.getAttachmentURL('myDoc', 'myPic').then(function(url) { 1668 | * var image = new Image(); 1669 | * image.src = url; 1670 | * document.body.appendChild(image); 1671 | * storage.revokeAttachmentURL(url); 1672 | * }) 1673 | * 1674 | * This is preferrable to getting the attachment and then getting the 1675 | * URL via `createObjectURL` (on some systems) as LLS can take advantage of 1676 | * lower level details to improve performance. 1677 | * 1678 | * @method getAttachmentURL 1679 | * @param {string} [docKey] Identifies the document. Defaults to `__emptydoc__` 1680 | * @param {string} attachKey Identifies the attachment. 1681 | * @returns {promose} promise that is resolved with the attachment url. 1682 | */ 1683 | getAttachmentURL: function(docKey, attachKey) { 1684 | if (!docKey) docKey = '__emptydoc__'; 1685 | this._checkAvailability(); 1686 | return this._impl.getAttachmentURL(docKey, attachKey); 1687 | }, 1688 | 1689 | /** 1690 | * Gets all of the attachments for a document. 1691 | * 1692 | * @example 1693 | * storage.getAllAttachments('exampleDoc').then(function(attachEntries) { 1694 | * attachEntries.map(function(entry) { 1695 | * var a = entry.data; 1696 | * // do something with it... 1697 | * if (a.type.indexOf('image') == 0) { 1698 | * // show image... 1699 | * } else if (a.type.indexOf('audio') == 0) { 1700 | * // play audio... 1701 | * } else ... 1702 | * }) 1703 | * }) 1704 | * 1705 | * @method getAllAttachments 1706 | * @param {string} [docKey] Identifies the document. Defaults to `__emptydoc__` 1707 | * @returns {promise} Promise that is resolved with all of the attachments for 1708 | * the given document. 1709 | */ 1710 | getAllAttachments: function(docKey) { 1711 | if (!docKey) docKey = '__emptydoc__'; 1712 | this._checkAvailability(); 1713 | return this._impl.getAllAttachments(docKey); 1714 | }, 1715 | 1716 | /** 1717 | * Gets all attachments URLs for a document. 1718 | * 1719 | * @example 1720 | * storage.getAllAttachmentURLs('exampleDoc').then(function(urlEntries) { 1721 | * urlEntries.map(function(entry) { 1722 | * var url = entry.url; 1723 | * // do something with the url... 1724 | * }) 1725 | * }) 1726 | * 1727 | * @method getAllAttachmentURLs 1728 | * @param {string} [docKey] Identifies the document. Defaults to the `__emptydoc__` document. 1729 | * @returns {promise} Promise that is resolved with all of the attachment 1730 | * urls for the given doc. 1731 | */ 1732 | getAllAttachmentURLs: function(docKey) { 1733 | if (!docKey) docKey = '__emptydoc__'; 1734 | this._checkAvailability(); 1735 | return this._impl.getAllAttachmentURLs(docKey); 1736 | }, 1737 | 1738 | /** 1739 | * Revoke the attachment URL as required by the underlying 1740 | * storage system. 1741 | * 1742 | * This is akin to `URL.revokeObjectURL(url)` 1743 | * URLs that come from `getAttachmentURL` or `getAllAttachmentURLs` 1744 | * should be revoked by LLS and not `URL.revokeObjectURL` 1745 | * 1746 | * @example 1747 | * storage.getAttachmentURL('doc', 'attach').then(function(url) { 1748 | * // do something with the URL 1749 | * storage.revokeAttachmentURL(url); 1750 | * }) 1751 | * 1752 | * @method revokeAttachmentURL 1753 | * @param {string} url The URL as returned by `getAttachmentURL` or `getAttachmentURLs` 1754 | * @returns {void} 1755 | */ 1756 | revokeAttachmentURL: function(url) { 1757 | this._checkAvailability(); 1758 | return this._impl.revokeAttachmentURL(url); 1759 | }, 1760 | 1761 | /** 1762 | * Remove an attachment from a document. 1763 | * 1764 | * @example 1765 | * storage.rmAttachment('exampleDoc', 'someAttachment').then(function() { 1766 | * alert('exampleDoc/someAttachment removed'); 1767 | * }).catch(function(e) { 1768 | * alert('Attachment removal failed: ' + e); 1769 | * }); 1770 | * 1771 | * @method rmAttachment 1772 | * @param {string} docKey 1773 | * @param {string} attachKey 1774 | * @returns {promise} Promise that is resolved once the remove completes 1775 | */ 1776 | rmAttachment: function(docKey, attachKey) { 1777 | if (!docKey) docKey = '__emptydoc__'; 1778 | this._checkAvailability(); 1779 | return this._impl.rmAttachment(docKey, attachKey); 1780 | }, 1781 | 1782 | /** 1783 | * Returns the actual capacity of the storage or -1 1784 | * if it is unknown. If the user denies your request for 1785 | * storage you'll get back some smaller amount of storage than what you 1786 | * actually requested. 1787 | * 1788 | * TODO: return an estimated capacity if actual capacity is unknown? 1789 | * -Firefox is 50MB until authorized to go above, 1790 | * -Chrome is some % of available disk space, 1791 | * -Safari unlimited as long as the user keeps authorizing size increases 1792 | * -Opera same as safari? 1793 | * 1794 | * @example 1795 | * // the initialized property will call you back with the capacity 1796 | * storage.initialized.then(function(capacity) { 1797 | * console.log('Authorized to store: ' + capacity + ' bytes'); 1798 | * }); 1799 | * // or if you know your storage is already available 1800 | * // you can call getCapacity directly 1801 | * storage.getCapacity() 1802 | * 1803 | * @method getCapacity 1804 | * @returns {number} Capacity, in bytes, of the storage. -1 if unknown. 1805 | */ 1806 | getCapacity: function() { 1807 | this._checkAvailability(); 1808 | if (this._impl.getCapacity) 1809 | return this._impl.getCapacity(); 1810 | else 1811 | return -1; 1812 | }, 1813 | 1814 | _checkAvailability: function() { 1815 | if (!this._impl) { 1816 | throw { 1817 | msg: "No storage implementation is available yet. The user most likely has not granted you app access to FileSystemAPI or IndexedDB", 1818 | code: "NO_IMPLEMENTATION" 1819 | }; 1820 | } 1821 | } 1822 | }; 1823 | 1824 | LargeLocalStorage.contrib = {}; 1825 | 1826 | function writeAttachments(docKey, attachments, storage) { 1827 | var promises = []; 1828 | attachments.forEach(function(attachment) { 1829 | promises.push(storage.setAttachment(docKey, attachment.attachKey, attachment.data)); 1830 | }); 1831 | 1832 | return Q.all(promises); 1833 | } 1834 | 1835 | function copyDocs(docKeys, oldStorage, newStorage) { 1836 | var promises = []; 1837 | docKeys.forEach(function(key) { 1838 | promises.push(oldStorage.getContents(key).then(function(contents) { 1839 | return newStorage.setContents(key, contents); 1840 | })); 1841 | }); 1842 | 1843 | docKeys.forEach(function(key) { 1844 | promises.push(oldStorage.getAllAttachments(key).then(function(attachments) { 1845 | return writeAttachments(key, attachments, newStorage); 1846 | })); 1847 | }); 1848 | 1849 | return Q.all(promises); 1850 | } 1851 | 1852 | LargeLocalStorage.copyOldData = function(err, oldStorage, newStorage, config) { 1853 | if (err) { 1854 | throw err; 1855 | } 1856 | 1857 | oldStorage.ls().then(function(docKeys) { 1858 | return copyDocs(docKeys, oldStorage, newStorage) 1859 | }).then(function() { 1860 | if (config.migrationComplete) 1861 | config.migrationComplete(); 1862 | }, function(e) { 1863 | config.migrationComplete(e); 1864 | }); 1865 | }; 1866 | 1867 | LargeLocalStorage._sessionMeta = sessionMeta; 1868 | 1869 | var availableProviders = []; 1870 | Object.keys(providers).forEach(function(potentialProvider) { 1871 | if (providers[potentialProvider].isAvailable()) 1872 | availableProviders.push(potentialProvider); 1873 | }); 1874 | 1875 | LargeLocalStorage.availableProviders = availableProviders; 1876 | 1877 | return LargeLocalStorage; 1878 | })(Q); 1879 | 1880 | return LargeLocalStorage; 1881 | } 1882 | 1883 | if (typeof define === 'function' && define.amd) { 1884 | define(['Q'], definition); 1885 | } else { 1886 | glob.LargeLocalStorage = definition.call(glob, Q); 1887 | } 1888 | 1889 | }).call(this, this); -------------------------------------------------------------------------------- /dist/LargeLocalStorage.min.js: -------------------------------------------------------------------------------- 1 | (function(a){function b(Q){function a(a,b,c){this._handlers=a,this._next=b,this._end=c,this._i=0}function b(a,b,c){for(var d=0;b>d;++d){var e=a[d];if(e.name===c||e.handler===c)return d}return-1}function d(a){return a.next.apply(a,Array.prototype.slice.call(arguments,1))}function e(a,b){a.forEach(function(a){b[a]||(b[a]=d)})}function f(b){function c(b){this.pipe={_handlers:[],_contextCtor:a,_nextMethods:f,end:d,_pipedMethodNames:b}}var d={},e=function(){return d},f={},h=new c(b);for(var i in g)h.pipe[i]=g[i];return b.forEach(function(a){d[a]=e,f[a]=new Function("var handler = this._nextHandler();handler.__pipectx = this.__pipectx;return handler."+a+".apply(handler, arguments);"),h[a]=new Function("var ctx = new this.pipe._contextCtor(this.pipe._handlers, this.pipe._nextMethods."+a+", this.pipe.end);return ctx.next.apply(ctx, arguments);")}),h}a.prototype={next:function(){return this.__pipectx=this,this._next.apply(this,arguments)},_nextHandler:function(){if(this._i>=this._handlers.length)return this._end;var a=this._handlers[this._i].handler;return this._i+=1,a},length:function(){return this._handlers.length}};var g={addFirst:function(a,b){e(this._pipedMethodNames,b),this._handlers.unshift({name:a,handler:b})},addLast:function(a,b){e(this._pipedMethodNames,b),this._handlers.push({name:a,handler:b})},addAfter:function(a,c,d){e(this._pipedMethodNames,d);var f=this._handlers,g=f.length,h=b(f,g,a);h>=0&&f.splice(h+1,0,{name:c,handler:d})},addBefore:function(a,c,d){e(this._pipedMethodNames,d);var f=this._handlers,g=f.length,h=b(f,g,a);h>=0&&f.splice(h,0,{name:c,handler:d})},replace:function(a,c,d){e(this._pipedMethodNames,d);var f=this._handlers,g=f.length,h=b(f,g,a);h>=0&&f.splice(h,1,{name:c,handler:d})},removeFirst:function(){return this._handlers.shift()},removeLast:function(){return this._handlers.pop()},remove:function(a){var c=this._handlers,d=c.length,e=b(c,d,a);e>=0&&c.splice(e,1)},getHandler:function(a){var c=b(this._handlers,this._handlers.length,a);return c>=0?this._handlers[c].handler:null}};f.isPipeline=function(a){return a instanceof Pipeline};var h=function(){function a(b,c,d,e){b(c[d.length],function(f){d.push(f),d.length==c.length?e.resolve(d):a(b,c,d,e)},function(a){e.reject(a)})}return{convertToBase64:function(a,b){var c=new FileReader;c.onload=function(a){b(a.target.result)},c.onerror=function(){},c.onabort=function(){},c.readAsDataURL(a)},dataURLToBlob:function(a){var b=";base64,";if(-1==a.indexOf(b)){var c=a.split(","),d=c[0].split(":")[1],e=c[1];return new Blob([e],{type:d})}for(var c=a.split(b),d=c[0].split(":")[1],e=window.atob(c[1]),f=e.length,g=new Uint8Array(f),h=0;f>h;++h)g[h]=e.charCodeAt(h);return new Blob([g.buffer],{type:d})},splitAttachmentPath:function(a){var b=a.split("/");return 1==b.length&&b.unshift("__nodoc__"),b},mapAsync:function(b,c){var d=Q.defer();return c.then(function(c){a(b,c,[],d)},function(a){d.reject(a)}),d.promise},countdown:function(a,b){var c=[];return function(){for(var d=0;d'); 72 | var image = new Image(); 73 | image.src = url; 74 | var self = this; 75 | image.onload = function() { 76 | var scale = 171 / image.naturalWidth; 77 | 78 | var newHeight = scale * image.naturalHeight; 79 | if (newHeight > 180) { 80 | scale = 180 / image.naturalHeight; 81 | newHeight = 180; 82 | } 83 | 84 | var newWidth = scale * image.naturalWidth; 85 | 86 | image.width = newWidth; 87 | image.height = newHeight; 88 | 89 | container.append(image); 90 | self.$thumbs.append(container); 91 | }; 92 | 93 | storage.revokeAttachmentURL(url); 94 | }, 95 | 96 | _renderExistingPhotos: function() { 97 | var self = this; 98 | storage.getAllAttachmentURLs('album') 99 | .then(function(urls) { 100 | urls = urls.map(function(u) { 101 | return u.url; 102 | }); 103 | foreach(self._appendImage, urls); 104 | }); 105 | } 106 | }; 107 | 108 | 109 | function copyDragover(e) { 110 | e.stopPropagation(); 111 | e.preventDefault(); 112 | e = e.originalEvent; 113 | e.dataTransfer.dropEffect = 'copy'; 114 | } 115 | 116 | function foreach(cb, arr) { 117 | for (var i = 0; i < arr.length; ++i) { 118 | cb(arr[i]); 119 | } 120 | } 121 | 122 | function isImage(file) { 123 | return file.type.indexOf('image') == 0; 124 | } 125 | 126 | function keep(pred, arr) { 127 | return filter(not(pred), arr); 128 | } 129 | 130 | function not(pred) { 131 | return function(e) { 132 | return !pred(e); 133 | } 134 | } 135 | 136 | function filter(pred, arr) { 137 | var result = []; 138 | for (var i = 0; i < arr.length; ++i) { 139 | var e = arr[i]; 140 | if (!pred(e)) 141 | result.push(e); 142 | } 143 | 144 | return result; 145 | } 146 | })(); -------------------------------------------------------------------------------- /examples/album/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
Drag and drop images to add to your album.
10 |
11 |
12 |
13 | 14 |
15 |
16 | In order to keep your photos you need to grant this app the ability 17 | to save your photos! Please click accept on the browser's prompt. 18 |
19 |
20 | Clear Album 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/album/main.css: -------------------------------------------------------------------------------- 1 | .dndArea { 2 | margin-top: 20px; 3 | border: 2px dashed #bbb; 4 | border-radius: 5px; 5 | min-height: 240px; 6 | padding: 20px; 7 | } 8 | 9 | .storageNotice { 10 | position: absolute; 11 | width: 100%; 12 | height: 100%; 13 | top: 20px; 14 | text-align: center; 15 | -webkit-transition: opacity 1s; 16 | transition: all 1s; 17 | } 18 | 19 | .usage { 20 | width: 100%; 21 | height: 100%; 22 | text-align: center; 23 | font-size: 32px; 24 | color: #CCC; 25 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lls", 3 | "version": "0.1.3", 4 | "description": "Storage large files and blob in a cross platform way, in the browser", 5 | "main": "Gruntfile.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/tantaman/LargeLocalStorage.git" 15 | }, 16 | "keywords": [ 17 | "LocalStorage", 18 | "key", 19 | "value", 20 | "key-value", 21 | "storage", 22 | "browser", 23 | "indexeddb", 24 | "websql", 25 | "filesystemapi" 26 | ], 27 | "author": "Matt Crinklaw-Vogt", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/tantaman/LargeLocalStorage/issues" 31 | }, 32 | "devDependencies": { 33 | "grunt": "~0.4.1", 34 | "grunt-contrib-requirejs": "~0.4.1", 35 | "matchdep": "~0.3.0", 36 | "grunt-contrib-concat": "~0.3.0", 37 | "grunt-contrib-watch": "~0.5.3", 38 | "grunt-contrib-connect": "~0.5.0", 39 | "grunt-contrib-yuidoc": "~0.5.0", 40 | "yuidoc-library-theme": "git://github.com/tantaman/yuidoc-library-theme.git", 41 | "grunt-contrib-copy": "~0.4.1", 42 | "grunt-contrib-uglify": "~0.2.4" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/LargeLocalStorage.js: -------------------------------------------------------------------------------- 1 | var LargeLocalStorage = (function(Q) { 2 | var sessionMeta = localStorage.getItem('LargeLocalStorage-meta'); 3 | if (sessionMeta) 4 | sessionMeta = JSON.parse(sessionMeta); 5 | else 6 | sessionMeta = {}; 7 | 8 | window.addEventListener('beforeunload', function() { 9 | localStorage.setItem('LargeLocalStorage-meta', JSON.stringify(sessionMeta)); 10 | }); 11 | 12 | function defaults(options, defaultOptions) { 13 | for (var k in defaultOptions) { 14 | if (options[k] === undefined) 15 | options[k] = defaultOptions[k]; 16 | } 17 | 18 | return options; 19 | } 20 | 21 | var providers = { 22 | FileSystemAPI: FilesystemAPIProvider, 23 | IndexedDB: IndexedDBProvider, 24 | WebSQL: WebSQLProvider 25 | // LocalStorage: LocalStorageProvider 26 | } 27 | 28 | var defaultConfig = { 29 | size: 10 * 1024 * 1024, 30 | name: 'lls' 31 | }; 32 | 33 | function selectImplementation(config) { 34 | if (!config) config = {}; 35 | config = defaults(config, defaultConfig); 36 | 37 | if (config.forceProvider) { 38 | return providers[config.forceProvider].init(config); 39 | } 40 | 41 | return FilesystemAPIProvider.init(config).then(function(impl) { 42 | return Q(impl); 43 | }, function() { 44 | return IndexedDBProvider.init(config); 45 | }).then(function(impl) { 46 | return Q(impl); 47 | }, function() { 48 | return WebSQLProvider.init(config); 49 | }).then(function(impl) { 50 | return Q(impl); 51 | }, function() { 52 | console.error('Unable to create any storage implementations. Using LocalStorage'); 53 | return LocalStorageProvider.init(config); 54 | }); 55 | } 56 | 57 | function copy(obj) { 58 | var result = {}; 59 | Object.keys(obj).forEach(function(key) { 60 | result[key] = obj[key]; 61 | }); 62 | 63 | return result; 64 | } 65 | 66 | function handleDataMigration(storageInstance, config, previousProviderType, currentProivderType) { 67 | var previousProviderType = 68 | sessionMeta[config.name] && sessionMeta[config.name].lastStorageImpl; 69 | if (config.migrate) { 70 | if (previousProviderType != currentProivderType 71 | && previousProviderType in providers) { 72 | config = copy(config); 73 | config.forceProvider = previousProviderType; 74 | selectImplementation(config).then(function(prevImpl) { 75 | config.migrate(null, prevImpl, storageInstance, config); 76 | }, function(e) { 77 | config.migrate(e); 78 | }); 79 | } else { 80 | if (config.migrationComplete) 81 | config.migrationComplete(); 82 | } 83 | } 84 | } 85 | 86 | /** 87 | * 88 | * LargeLocalStorage (or LLS) gives you a large capacity 89 | * (up to several gig with permission from the user) 90 | * key-value store in the browser. 91 | * 92 | * For storage, LLS uses the [FilesystemAPI](https://developer.mozilla.org/en-US/docs/WebGuide/API/File_System) 93 | * when running in Chrome and Opera, 94 | * [IndexedDB](https://developer.mozilla.org/en-US/docs/IndexedDB) in Firefox and IE 95 | * and [WebSQL](http://www.w3.org/TR/webdatabase/) in Safari. 96 | * 97 | * When IndexedDB becomes available in Safari, LLS will 98 | * update to take advantage of that storage implementation. 99 | * 100 | * 101 | * Upon construction a LargeLocalStorage (LLS) object will be 102 | * immediately returned but not necessarily immediately ready for use. 103 | * 104 | * A LLS object has an `initialized` property which is a promise 105 | * that is resolved when the LLS object is ready for us. 106 | * 107 | * Usage of LLS would typically be: 108 | * ``` 109 | * var storage = new LargeLocalStorage({size: 75*1024*1024}); 110 | * storage.initialized.then(function(grantedCapacity) { 111 | * // storage ready to be used. 112 | * }); 113 | * ``` 114 | * 115 | * The reason that LLS may not be immediately ready for 116 | * use is that some browsers require confirmation from the 117 | * user before a storage area may be created. Also, 118 | * the browser's native storage APIs are asynchronous. 119 | * 120 | * If an LLS instance is used before the storage 121 | * area is ready then any 122 | * calls to it will throw an exception with code: "NO_IMPLEMENTATION" 123 | * 124 | * This behavior is useful when you want the application 125 | * to continue to function--regardless of whether or 126 | * not the user has allowed it to store data--and would 127 | * like to know when your storage calls fail at the point 128 | * of those calls. 129 | * 130 | * LLS-contrib has utilities to queue storage calls until 131 | * the implementation is ready. If an implementation 132 | * is never ready this could obviously lead to memory issues 133 | * which is why it is not the default behavior. 134 | * 135 | * @example 136 | * var desiredCapacity = 50 * 1024 * 1024; // 50MB 137 | * var storage = new LargeLocalStorage({ 138 | * // desired capacity, in bytes. 139 | * size: desiredCapacity, 140 | * 141 | * // optional name for your LLS database. Defaults to lls. 142 | * // This is the name given to the underlying 143 | * // IndexedDB or WebSQL DB or FSAPI Folder. 144 | * // LLS's with different names are independent. 145 | * name: 'myStorage' 146 | * 147 | * // the following is an optional param 148 | * // that is useful for debugging. 149 | * // force LLS to use a specific storage implementation 150 | * // forceProvider: 'IndexedDB' or 'WebSQL' or 'FilesystemAPI' 151 | * 152 | * // These parameters can be used to migrate data from one 153 | * // storage implementation to another 154 | * // migrate: LargeLocalStorage.copyOldData, 155 | * // migrationComplete: function(err) { 156 | * // db is initialized and old data has been copied. 157 | * // } 158 | * }); 159 | * storage.initialized.then(function(capacity) { 160 | * if (capacity != -1 && capacity != desiredCapacity) { 161 | * // the user didn't authorize your storage request 162 | * // so instead you have some limitation on your storage 163 | * } 164 | * }) 165 | * 166 | * @class LargeLocalStorage 167 | * @constructor 168 | * @param {object} config {size: sizeInByes, [forceProvider: force a specific implementation]} 169 | * @return {LargeLocalStorage} 170 | */ 171 | function LargeLocalStorage(config) { 172 | var deferred = Q.defer(); 173 | /** 174 | * @property {promise} initialized 175 | */ 176 | this.initialized = deferred.promise; 177 | 178 | var piped = createPipeline([ 179 | 'ready', 180 | 'ls', 181 | 'rm', 182 | 'clear', 183 | 'getContents', 184 | 'setContents', 185 | 'getAttachment', 186 | 'setAttachment', 187 | 'getAttachmentURL', 188 | 'getAllAttachments', 189 | 'getAllAttachmentURLs', 190 | 'revokeAttachmentURL', 191 | 'rmAttachment', 192 | 'getCapacity', 193 | 'initialized']); 194 | 195 | piped.pipe.addLast('lls', this); 196 | piped.initialized = this.initialized; 197 | 198 | var self = this; 199 | selectImplementation(config).then(function(impl) { 200 | self._impl = impl; 201 | handleDataMigration(piped, config, self._impl.type); 202 | sessionMeta[config.name] = sessionMeta[config.name] || {}; 203 | sessionMeta[config.name].lastStorageImpl = impl.type; 204 | deferred.resolve(piped); 205 | }).catch(function(e) { 206 | // This should be impossible 207 | console.log(e); 208 | deferred.reject('No storage provider found'); 209 | }); 210 | 211 | return piped; 212 | } 213 | 214 | LargeLocalStorage.prototype = { 215 | /** 216 | * Whether or not LLS is ready to store data. 217 | * The `initialized` property can be used to 218 | * await initialization. 219 | * @example 220 | * // may or may not be true 221 | * storage.ready(); 222 | * 223 | * storage.initialized.then(function() { 224 | * // always true 225 | * storage.ready(); 226 | * }) 227 | * @method ready 228 | */ 229 | ready: function() { 230 | return this._impl != null; 231 | }, 232 | 233 | /** 234 | * List all attachments under a given key. 235 | * 236 | * List all documents if no key is provided. 237 | * 238 | * Returns a promise that is fulfilled with 239 | * the listing. 240 | * 241 | * @example 242 | * storage.ls().then(function(docKeys) { 243 | * console.log(docKeys); 244 | * }) 245 | * 246 | * @method ls 247 | * @param {string} [docKey] 248 | * @returns {promise} resolved with the listing, rejected if the listing fails. 249 | */ 250 | ls: function(docKey) { 251 | this._checkAvailability(); 252 | return this._impl.ls(docKey); 253 | }, 254 | 255 | /** 256 | * Remove the specified document and all 257 | * of its attachments. 258 | * 259 | * Returns a promise that is fulfilled when the 260 | * removal completes. 261 | * 262 | * If no docKey is specified, this throws an error. 263 | * 264 | * To remove all files in LargeLocalStorage call 265 | * `lls.clear();` 266 | * 267 | * To remove all attachments that were written without 268 | * a docKey, call `lls.rm('__emptydoc__');` 269 | * 270 | * rm works this way to ensure you don't lose 271 | * data due to an accidently undefined variable. 272 | * 273 | * @example 274 | * stoarge.rm('exampleDoc').then(function() { 275 | * alert('doc and all attachments were removed'); 276 | * }) 277 | * 278 | * @method rm 279 | * @param {string} docKey 280 | * @returns {promise} resolved when removal completes, rejected if the removal fails. 281 | */ 282 | rm: function(docKey) { 283 | this._checkAvailability(); 284 | return this._impl.rm(docKey); 285 | }, 286 | 287 | /** 288 | * An explicit way to remove all documents and 289 | * attachments from LargeLocalStorage. 290 | * 291 | * @example 292 | * storage.clear().then(function() { 293 | * alert('all data has been removed'); 294 | * }); 295 | * 296 | * @returns {promise} resolve when clear completes, rejected if clear fails. 297 | */ 298 | clear: function() { 299 | this._checkAvailability(); 300 | return this._impl.clear(); 301 | }, 302 | 303 | /** 304 | * Get the contents of a document identified by `docKey` 305 | * TODO: normalize all implementations to allow storage 306 | * and retrieval of JS objects? 307 | * 308 | * @example 309 | * storage.getContents('exampleDoc').then(function(contents) { 310 | * alert(contents); 311 | * }); 312 | * 313 | * @method getContents 314 | * @param {string} docKey 315 | * @returns {promise} resolved with the contents when the get completes 316 | */ 317 | getContents: function(docKey, options) { 318 | this._checkAvailability(); 319 | return this._impl.getContents(docKey, options); 320 | }, 321 | 322 | /** 323 | * Set the contents identified by `docKey` to `data`. 324 | * The document will be created if it does not exist. 325 | * 326 | * @example 327 | * storage.setContents('exampleDoc', 'some data...').then(function() { 328 | * alert('doc written'); 329 | * }); 330 | * 331 | * @method setContents 332 | * @param {string} docKey 333 | * @param {any} data 334 | * @returns {promise} fulfilled when set completes 335 | */ 336 | setContents: function(docKey, data, options) { 337 | this._checkAvailability(); 338 | return this._impl.setContents(docKey, data, options); 339 | }, 340 | 341 | /** 342 | * Get the attachment identified by `docKey` and `attachKey` 343 | * 344 | * @example 345 | * storage.getAttachment('exampleDoc', 'examplePic').then(function(attachment) { 346 | * var url = URL.createObjectURL(attachment); 347 | * var image = new Image(url); 348 | * document.body.appendChild(image); 349 | * URL.revokeObjectURL(url); 350 | * }) 351 | * 352 | * @method getAttachment 353 | * @param {string} [docKey] Defaults to `__emptydoc__` 354 | * @param {string} attachKey key of the attachment 355 | * @returns {promise} fulfilled with the attachment or 356 | * rejected if it could not be found. code: 1 357 | */ 358 | getAttachment: function(docKey, attachKey) { 359 | if (!docKey) docKey = '__emptydoc__'; 360 | this._checkAvailability(); 361 | return this._impl.getAttachment(docKey, attachKey); 362 | }, 363 | 364 | /** 365 | * Set an attachment for a given document. Identified 366 | * by `docKey` and `attachKey`. 367 | * 368 | * @example 369 | * storage.setAttachment('myDoc', 'myPic', blob).then(function() { 370 | * alert('Attachment written'); 371 | * }) 372 | * 373 | * @method setAttachment 374 | * @param {string} [docKey] Defaults to `__emptydoc__` 375 | * @param {string} attachKey key for the attachment 376 | * @param {any} attachment data 377 | * @returns {promise} resolved when the write completes. Rejected 378 | * if an error occurs. 379 | */ 380 | setAttachment: function(docKey, attachKey, data) { 381 | if (!docKey) docKey = '__emptydoc__'; 382 | this._checkAvailability(); 383 | return this._impl.setAttachment(docKey, attachKey, data); 384 | }, 385 | 386 | /** 387 | * Get the URL for a given attachment. 388 | * 389 | * @example 390 | * storage.getAttachmentURL('myDoc', 'myPic').then(function(url) { 391 | * var image = new Image(); 392 | * image.src = url; 393 | * document.body.appendChild(image); 394 | * storage.revokeAttachmentURL(url); 395 | * }) 396 | * 397 | * This is preferrable to getting the attachment and then getting the 398 | * URL via `createObjectURL` (on some systems) as LLS can take advantage of 399 | * lower level details to improve performance. 400 | * 401 | * @method getAttachmentURL 402 | * @param {string} [docKey] Identifies the document. Defaults to `__emptydoc__` 403 | * @param {string} attachKey Identifies the attachment. 404 | * @returns {promose} promise that is resolved with the attachment url. 405 | */ 406 | getAttachmentURL: function(docKey, attachKey) { 407 | if (!docKey) docKey = '__emptydoc__'; 408 | this._checkAvailability(); 409 | return this._impl.getAttachmentURL(docKey, attachKey); 410 | }, 411 | 412 | /** 413 | * Gets all of the attachments for a document. 414 | * 415 | * @example 416 | * storage.getAllAttachments('exampleDoc').then(function(attachEntries) { 417 | * attachEntries.map(function(entry) { 418 | * var a = entry.data; 419 | * // do something with it... 420 | * if (a.type.indexOf('image') == 0) { 421 | * // show image... 422 | * } else if (a.type.indexOf('audio') == 0) { 423 | * // play audio... 424 | * } else ... 425 | * }) 426 | * }) 427 | * 428 | * @method getAllAttachments 429 | * @param {string} [docKey] Identifies the document. Defaults to `__emptydoc__` 430 | * @returns {promise} Promise that is resolved with all of the attachments for 431 | * the given document. 432 | */ 433 | getAllAttachments: function(docKey) { 434 | if (!docKey) docKey = '__emptydoc__'; 435 | this._checkAvailability(); 436 | return this._impl.getAllAttachments(docKey); 437 | }, 438 | 439 | /** 440 | * Gets all attachments URLs for a document. 441 | * 442 | * @example 443 | * storage.getAllAttachmentURLs('exampleDoc').then(function(urlEntries) { 444 | * urlEntries.map(function(entry) { 445 | * var url = entry.url; 446 | * // do something with the url... 447 | * }) 448 | * }) 449 | * 450 | * @method getAllAttachmentURLs 451 | * @param {string} [docKey] Identifies the document. Defaults to the `__emptydoc__` document. 452 | * @returns {promise} Promise that is resolved with all of the attachment 453 | * urls for the given doc. 454 | */ 455 | getAllAttachmentURLs: function(docKey) { 456 | if (!docKey) docKey = '__emptydoc__'; 457 | this._checkAvailability(); 458 | return this._impl.getAllAttachmentURLs(docKey); 459 | }, 460 | 461 | /** 462 | * Revoke the attachment URL as required by the underlying 463 | * storage system. 464 | * 465 | * This is akin to `URL.revokeObjectURL(url)` 466 | * URLs that come from `getAttachmentURL` or `getAllAttachmentURLs` 467 | * should be revoked by LLS and not `URL.revokeObjectURL` 468 | * 469 | * @example 470 | * storage.getAttachmentURL('doc', 'attach').then(function(url) { 471 | * // do something with the URL 472 | * storage.revokeAttachmentURL(url); 473 | * }) 474 | * 475 | * @method revokeAttachmentURL 476 | * @param {string} url The URL as returned by `getAttachmentURL` or `getAttachmentURLs` 477 | * @returns {void} 478 | */ 479 | revokeAttachmentURL: function(url) { 480 | this._checkAvailability(); 481 | return this._impl.revokeAttachmentURL(url); 482 | }, 483 | 484 | /** 485 | * Remove an attachment from a document. 486 | * 487 | * @example 488 | * storage.rmAttachment('exampleDoc', 'someAttachment').then(function() { 489 | * alert('exampleDoc/someAttachment removed'); 490 | * }).catch(function(e) { 491 | * alert('Attachment removal failed: ' + e); 492 | * }); 493 | * 494 | * @method rmAttachment 495 | * @param {string} docKey 496 | * @param {string} attachKey 497 | * @returns {promise} Promise that is resolved once the remove completes 498 | */ 499 | rmAttachment: function(docKey, attachKey) { 500 | if (!docKey) docKey = '__emptydoc__'; 501 | this._checkAvailability(); 502 | return this._impl.rmAttachment(docKey, attachKey); 503 | }, 504 | 505 | /** 506 | * Returns the actual capacity of the storage or -1 507 | * if it is unknown. If the user denies your request for 508 | * storage you'll get back some smaller amount of storage than what you 509 | * actually requested. 510 | * 511 | * TODO: return an estimated capacity if actual capacity is unknown? 512 | * -Firefox is 50MB until authorized to go above, 513 | * -Chrome is some % of available disk space, 514 | * -Safari unlimited as long as the user keeps authorizing size increases 515 | * -Opera same as safari? 516 | * 517 | * @example 518 | * // the initialized property will call you back with the capacity 519 | * storage.initialized.then(function(capacity) { 520 | * console.log('Authorized to store: ' + capacity + ' bytes'); 521 | * }); 522 | * // or if you know your storage is already available 523 | * // you can call getCapacity directly 524 | * storage.getCapacity() 525 | * 526 | * @method getCapacity 527 | * @returns {number} Capacity, in bytes, of the storage. -1 if unknown. 528 | */ 529 | getCapacity: function() { 530 | this._checkAvailability(); 531 | if (this._impl.getCapacity) 532 | return this._impl.getCapacity(); 533 | else 534 | return -1; 535 | }, 536 | 537 | _checkAvailability: function() { 538 | if (!this._impl) { 539 | throw { 540 | msg: "No storage implementation is available yet. The user most likely has not granted you app access to FileSystemAPI or IndexedDB", 541 | code: "NO_IMPLEMENTATION" 542 | }; 543 | } 544 | } 545 | }; 546 | 547 | LargeLocalStorage.contrib = {}; 548 | 549 | function writeAttachments(docKey, attachments, storage) { 550 | var promises = []; 551 | attachments.forEach(function(attachment) { 552 | promises.push(storage.setAttachment(docKey, attachment.attachKey, attachment.data)); 553 | }); 554 | 555 | return Q.all(promises); 556 | } 557 | 558 | function copyDocs(docKeys, oldStorage, newStorage) { 559 | var promises = []; 560 | docKeys.forEach(function(key) { 561 | promises.push(oldStorage.getContents(key).then(function(contents) { 562 | return newStorage.setContents(key, contents); 563 | })); 564 | }); 565 | 566 | docKeys.forEach(function(key) { 567 | promises.push(oldStorage.getAllAttachments(key).then(function(attachments) { 568 | return writeAttachments(key, attachments, newStorage); 569 | })); 570 | }); 571 | 572 | return Q.all(promises); 573 | } 574 | 575 | LargeLocalStorage.copyOldData = function(err, oldStorage, newStorage, config) { 576 | if (err) { 577 | throw err; 578 | } 579 | 580 | oldStorage.ls().then(function(docKeys) { 581 | return copyDocs(docKeys, oldStorage, newStorage) 582 | }).then(function() { 583 | if (config.migrationComplete) 584 | config.migrationComplete(); 585 | }, function(e) { 586 | config.migrationComplete(e); 587 | }); 588 | }; 589 | 590 | LargeLocalStorage._sessionMeta = sessionMeta; 591 | 592 | var availableProviders = []; 593 | Object.keys(providers).forEach(function(potentialProvider) { 594 | if (providers[potentialProvider].isAvailable()) 595 | availableProviders.push(potentialProvider); 596 | }); 597 | 598 | LargeLocalStorage.availableProviders = availableProviders; 599 | 600 | return LargeLocalStorage; 601 | })(Q); -------------------------------------------------------------------------------- /src/contrib/S3Link.js: -------------------------------------------------------------------------------- 1 | LargeLocalStorage.contrib.S3Link = (function() { 2 | function S3Link(config) { 3 | 4 | } 5 | 6 | S3Link.prototype = { 7 | push: function(docKey, options) { 8 | 9 | } 10 | }; 11 | 12 | return S3Link; 13 | })(); -------------------------------------------------------------------------------- /src/contrib/URLCache.js: -------------------------------------------------------------------------------- 1 | LargeLocalStorage.contrib.URLCache = (function() { 2 | var defaultOptions = { 3 | manageRevocation: true 4 | }; 5 | 6 | function defaults(options, defaultOptions) { 7 | for (var k in defaultOptions) { 8 | if (options[k] === undefined) 9 | options[k] = defaultOptions[k]; 10 | } 11 | 12 | return options; 13 | } 14 | 15 | function add(docKey, attachKey, url) { 16 | if (this.options.manageRevocation) 17 | expunge.call(this, docKey, attachKey, true); 18 | 19 | var mainCache = this.cache.main; 20 | var docCache = mainCache[docKey]; 21 | if (!docCache) { 22 | docCache = {}; 23 | mainCache[docKey] = docCache; 24 | } 25 | 26 | docCache[attachKey] = url; 27 | this.cache.reverse[url] = {docKey: docKey, attachKey: attachKey}; 28 | } 29 | 30 | function addAll(urlEntries) { 31 | urlEntries.forEach(function(entry) { 32 | add.call(this, entry.docKey, entry.attachKey, entry.url); 33 | }, this); 34 | } 35 | 36 | function expunge(docKey, attachKey, needsRevoke) { 37 | function delAndRevoke(attachKey) { 38 | var url = docCache[attachKey]; 39 | delete docCache[attachKey]; 40 | delete this.cache.reverse[url]; 41 | if (this.options.manageRevocation && needsRevoke) 42 | this.llshandler.revokeAttachmentURL(url, {bypassUrlCache: true}); 43 | } 44 | 45 | var docCache = this.cache.main[docKey]; 46 | if (docCache) { 47 | if (attachKey) { 48 | delAndRevoke.call(this, attachKey); 49 | } else { 50 | for (var attachKey in docCache) { 51 | delAndRevoke.call(this, attachKey); 52 | } 53 | delete this.cache.main[docKey]; 54 | } 55 | } 56 | } 57 | 58 | function expungeByUrl(url) { 59 | var keys = this.cache.reverse[url]; 60 | if (keys) { 61 | expunge.call(this, keys.docKey, keys.attachKey, false); 62 | } 63 | } 64 | 65 | 66 | function URLCache(llspipe, options) { 67 | options = options || {}; 68 | this.options = defaults(options, defaultOptions); 69 | this.llshandler = llspipe.pipe.getHandler('lls'); 70 | this.pending = {}; 71 | this.cache = { 72 | main: {}, 73 | reverse: {} 74 | }; 75 | } 76 | 77 | URLCache.prototype = { 78 | setAttachment: function(docKey, attachKey, blob) { 79 | expunge.call(this, docKey, attachKey); 80 | return this.__pipectx.next(docKey, attachKey, blob); 81 | }, 82 | 83 | rmAttachment: function(docKey, attachKey) { 84 | expunge.call(this, docKey, attachKey); 85 | return this.__pipectx.next(docKey, attachKey); 86 | }, 87 | 88 | rm: function(docKey) { 89 | expunge.call(this, docKey); 90 | return this.__pipectx.next(docKey); 91 | }, 92 | 93 | revokeAttachmentURL: function(url, options) { 94 | if (!options || !options.bypassUrlCache) 95 | expungeByUrl.call(this, url); 96 | 97 | return this.__pipectx.next(url, options); 98 | }, 99 | 100 | getAttachmentURL: function(docKey, attachKey) { 101 | var pendingKey = docKey + attachKey; 102 | var pending = this.pending[pendingKey]; 103 | if (pending) 104 | return pending; 105 | 106 | var promise = this.__pipectx.next(docKey, attachKey); 107 | var self = this; 108 | promise.then(function(url) { 109 | add.call(self, docKey, attachKey, url); 110 | delete self.pending[pendingKey]; 111 | }); 112 | 113 | this.pending[pendingKey] = promise; 114 | 115 | return promise; 116 | }, 117 | 118 | // TODO: pending between this and getAttachmentURL... 119 | // Execute this as an ls and then 120 | // a loop on getAttachmentURL instead??? 121 | // doing it the way mentiond above 122 | // will prevent us from leaking blobs. 123 | getAllAttachmentURLs: function(docKey) { 124 | var promise = this.__pipectx.next(docKey); 125 | var self = this; 126 | promise.then(function(urlEntries) { 127 | addAll.call(self, urlEntries); 128 | }); 129 | 130 | return promise; 131 | }, 132 | 133 | clear: function() { 134 | this.revokeAllCachedURLs(); 135 | return this.__pipectx.next(); 136 | }, 137 | 138 | revokeAllCachedURLs: function() { 139 | for (var url in this.cache.reverse) { 140 | this.llshandler.revokeAttachmentURL(url, {bypassUrlCache: true}); 141 | } 142 | 143 | this.cache.reverse = {}; 144 | this.cache.main = {}; 145 | } 146 | }; 147 | 148 | return { 149 | addTo: function(lls, options) { 150 | var cache = new URLCache(lls, options); 151 | lls.pipe.addFirst('URLCache', cache); 152 | return lls; 153 | } 154 | } 155 | })(); -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | define({ 2 | 3 | }); -------------------------------------------------------------------------------- /src/footer.js: -------------------------------------------------------------------------------- 1 | 2 | return LargeLocalStorage; 3 | } 4 | 5 | if (typeof define === 'function' && define.amd) { 6 | define(['Q'], definition); 7 | } else { 8 | glob.LargeLocalStorage = definition.call(glob, Q); 9 | } 10 | 11 | }).call(this, this); -------------------------------------------------------------------------------- /src/header.js: -------------------------------------------------------------------------------- 1 | (function(glob) { 2 | var undefined = {}.a; 3 | 4 | function definition(Q) { 5 | -------------------------------------------------------------------------------- /src/impls/FilesystemAPIProvider.js: -------------------------------------------------------------------------------- 1 | var requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem; 2 | var persistentStorage = navigator.persistentStorage || navigator.webkitPersistentStorage; 3 | var FilesystemAPIProvider = (function(Q) { 4 | function makeErrorHandler(deferred, finalDeferred) { 5 | // TODO: normalize the error so 6 | // we can handle it upstream 7 | return function(e) { 8 | if (e.code == 1) { 9 | deferred.resolve(undefined); 10 | } else { 11 | if (finalDeferred) 12 | finalDeferred.reject(e); 13 | else 14 | deferred.reject(e); 15 | } 16 | } 17 | } 18 | 19 | function getAttachmentPath(docKey, attachKey) { 20 | docKey = docKey.replace(/\//g, '--'); 21 | var attachmentsDir = docKey + "-attachments"; 22 | return { 23 | dir: attachmentsDir, 24 | path: attachmentsDir + "/" + attachKey 25 | }; 26 | } 27 | 28 | function readDirEntries(reader, result) { 29 | var deferred = Q.defer(); 30 | 31 | _readDirEntries(reader, result, deferred); 32 | 33 | return deferred.promise; 34 | } 35 | 36 | function _readDirEntries(reader, result, deferred) { 37 | reader.readEntries(function(entries) { 38 | if (entries.length == 0) { 39 | deferred.resolve(result); 40 | } else { 41 | result = result.concat(entries); 42 | _readDirEntries(reader, result, deferred); 43 | } 44 | }, function(err) { 45 | deferred.reject(err); 46 | }); 47 | } 48 | 49 | function entryToFile(entry, cb, eb) { 50 | entry.file(cb, eb); 51 | } 52 | 53 | function entryToURL(entry) { 54 | return entry.toURL(); 55 | } 56 | 57 | function FSAPI(fs, numBytes, prefix) { 58 | this._fs = fs; 59 | this._capacity = numBytes; 60 | this._prefix = prefix; 61 | this.type = "FileSystemAPI"; 62 | } 63 | 64 | FSAPI.prototype = { 65 | getContents: function(path, options) { 66 | var deferred = Q.defer(); 67 | path = this._prefix + path; 68 | this._fs.root.getFile(path, {}, function(fileEntry) { 69 | fileEntry.file(function(file) { 70 | var reader = new FileReader(); 71 | 72 | reader.onloadend = function(e) { 73 | var data = e.target.result; 74 | var err; 75 | if (options && options.json) { 76 | try { 77 | data = JSON.parse(data); 78 | } catch(e) { 79 | err = new Error('unable to parse JSON for ' + path); 80 | } 81 | } 82 | 83 | if (err) { 84 | deferred.reject(err); 85 | } else { 86 | deferred.resolve(data); 87 | } 88 | }; 89 | 90 | reader.readAsText(file); 91 | }, makeErrorHandler(deferred)); 92 | }, makeErrorHandler(deferred)); 93 | 94 | return deferred.promise; 95 | }, 96 | 97 | // create a file at path 98 | // and write `data` to it 99 | setContents: function(path, data, options) { 100 | var deferred = Q.defer(); 101 | 102 | if (options && options.json) 103 | data = JSON.stringify(data); 104 | 105 | path = this._prefix + path; 106 | this._fs.root.getFile(path, {create:true}, function(fileEntry) { 107 | fileEntry.createWriter(function(fileWriter) { 108 | var blob; 109 | fileWriter.onwriteend = function(e) { 110 | fileWriter.onwriteend = function() { 111 | deferred.resolve(); 112 | }; 113 | fileWriter.truncate(blob.size); 114 | } 115 | 116 | fileWriter.onerror = makeErrorHandler(deferred); 117 | 118 | if (data instanceof Blob) { 119 | blob = data; 120 | } else { 121 | blob = new Blob([data], {type: 'text/plain'}); 122 | } 123 | 124 | fileWriter.write(blob); 125 | }, makeErrorHandler(deferred)); 126 | }, makeErrorHandler(deferred)); 127 | 128 | return deferred.promise; 129 | }, 130 | 131 | ls: function(docKey) { 132 | var isRoot = false; 133 | if (!docKey) {docKey = this._prefix; isRoot = true;} 134 | else docKey = this._prefix + docKey + "-attachments"; 135 | 136 | var deferred = Q.defer(); 137 | 138 | this._fs.root.getDirectory(docKey, {create:false}, 139 | function(entry) { 140 | var reader = entry.createReader(); 141 | readDirEntries(reader, []).then(function(entries) { 142 | var listing = []; 143 | entries.forEach(function(entry) { 144 | if (!entry.isDirectory) { 145 | listing.push(entry.name); 146 | } 147 | }); 148 | deferred.resolve(listing); 149 | }); 150 | }, function(error) { 151 | deferred.reject(error); 152 | }); 153 | 154 | return deferred.promise; 155 | }, 156 | 157 | clear: function() { 158 | var deferred = Q.defer(); 159 | var failed = false; 160 | var ecb = function(err) { 161 | failed = true; 162 | deferred.reject(err); 163 | } 164 | 165 | this._fs.root.getDirectory(this._prefix, {}, 166 | function(entry) { 167 | var reader = entry.createReader(); 168 | reader.readEntries(function(entries) { 169 | var latch = 170 | utils.countdown(entries.length, function() { 171 | if (!failed) 172 | deferred.resolve(); 173 | }); 174 | 175 | entries.forEach(function(entry) { 176 | if (entry.isDirectory) { 177 | entry.removeRecursively(latch, ecb); 178 | } else { 179 | entry.remove(latch, ecb); 180 | } 181 | }); 182 | 183 | if (entries.length == 0) 184 | deferred.resolve(); 185 | }, ecb); 186 | }, ecb); 187 | 188 | return deferred.promise; 189 | }, 190 | 191 | rm: function(path) { 192 | var deferred = Q.defer(); 193 | var finalDeferred = Q.defer(); 194 | 195 | // remove attachments that go along with the path 196 | path = this._prefix + path; 197 | var attachmentsDir = path + "-attachments"; 198 | 199 | this._fs.root.getFile(path, {create:false}, 200 | function(entry) { 201 | entry.remove(function() { 202 | deferred.promise.then(finalDeferred.resolve); 203 | }, function(err) { 204 | finalDeferred.reject(err); 205 | }); 206 | }, 207 | makeErrorHandler(finalDeferred)); 208 | 209 | this._fs.root.getDirectory(attachmentsDir, {}, 210 | function(entry) { 211 | entry.removeRecursively(function() { 212 | deferred.resolve(); 213 | }, function(err) { 214 | finalDeferred.reject(err); 215 | }); 216 | }, 217 | makeErrorHandler(deferred, finalDeferred)); 218 | 219 | return finalDeferred.promise; 220 | }, 221 | 222 | getAttachment: function(docKey, attachKey) { 223 | var attachmentPath = this._prefix + getAttachmentPath(docKey, attachKey).path; 224 | 225 | var deferred = Q.defer(); 226 | this._fs.root.getFile(attachmentPath, {}, function(fileEntry) { 227 | fileEntry.file(function(file) { 228 | if (file.size == 0) 229 | deferred.resolve(undefined); 230 | else 231 | deferred.resolve(file); 232 | }, makeErrorHandler(deferred)); 233 | }, function(err) { 234 | if (err.code == 1) { 235 | deferred.resolve(undefined); 236 | } else { 237 | deferred.reject(err); 238 | } 239 | }); 240 | 241 | return deferred.promise; 242 | }, 243 | 244 | getAttachmentURL: function(docKey, attachKey) { 245 | var attachmentPath = this._prefix + getAttachmentPath(docKey, attachKey).path; 246 | 247 | var deferred = Q.defer(); 248 | var url = 'filesystem:' + window.location.protocol + '//' + window.location.host + '/persistent/' + attachmentPath; 249 | deferred.resolve(url); 250 | // this._fs.root.getFile(attachmentPath, {}, function(fileEntry) { 251 | // deferred.resolve(fileEntry.toURL()); 252 | // }, makeErrorHandler(deferred, "getting attachment file entry")); 253 | 254 | return deferred.promise; 255 | }, 256 | 257 | getAllAttachments: function(docKey) { 258 | var deferred = Q.defer(); 259 | var attachmentsDir = this._prefix + docKey + "-attachments"; 260 | 261 | this._fs.root.getDirectory(attachmentsDir, {}, 262 | function(entry) { 263 | var reader = entry.createReader(); 264 | deferred.resolve( 265 | utils.mapAsync(function(entry, cb, eb) { 266 | entry.file(function(file) { 267 | cb({ 268 | data: file, 269 | docKey: docKey, 270 | attachKey: entry.name 271 | }); 272 | }, eb); 273 | }, readDirEntries(reader, []))); 274 | }, function(err) { 275 | deferred.resolve([]); 276 | }); 277 | 278 | return deferred.promise; 279 | }, 280 | 281 | getAllAttachmentURLs: function(docKey) { 282 | var deferred = Q.defer(); 283 | var attachmentsDir = this._prefix + docKey + "-attachments"; 284 | 285 | this._fs.root.getDirectory(attachmentsDir, {}, 286 | function(entry) { 287 | var reader = entry.createReader(); 288 | readDirEntries(reader, []).then(function(entries) { 289 | deferred.resolve(entries.map( 290 | function(entry) { 291 | return { 292 | url: entry.toURL(), 293 | docKey: docKey, 294 | attachKey: entry.name 295 | }; 296 | })); 297 | }); 298 | }, function(err) { 299 | deferred.reject(err); 300 | }); 301 | 302 | return deferred.promise; 303 | }, 304 | 305 | revokeAttachmentURL: function(url) { 306 | // we return FS urls so this is a no-op 307 | // unless someone is being silly and doing 308 | // createObjectURL(getAttachment()) ...... 309 | }, 310 | 311 | // Create a folder at dirname(path)+"-attachments" 312 | // add attachment under that folder as basename(path) 313 | setAttachment: function(docKey, attachKey, data) { 314 | var attachInfo = getAttachmentPath(docKey, attachKey); 315 | 316 | var deferred = Q.defer(); 317 | 318 | var self = this; 319 | this._fs.root.getDirectory(this._prefix + attachInfo.dir, 320 | {create:true}, function(dirEntry) { 321 | deferred.resolve(self.setContents(attachInfo.path, data)); 322 | }, makeErrorHandler(deferred)); 323 | 324 | return deferred.promise; 325 | }, 326 | 327 | // rm the thing at dirname(path)+"-attachments/"+basename(path) 328 | rmAttachment: function(docKey, attachKey) { 329 | var attachmentPath = getAttachmentPath(docKey, attachKey).path; 330 | 331 | var deferred = Q.defer(); 332 | this._fs.root.getFile(this._prefix + attachmentPath, {create:false}, 333 | function(entry) { 334 | entry.remove(function() { 335 | deferred.resolve(); 336 | }, makeErrorHandler(deferred)); 337 | }, makeErrorHandler(deferred)); 338 | 339 | return deferred.promise; 340 | }, 341 | 342 | getCapacity: function() { 343 | return this._capacity; 344 | } 345 | }; 346 | 347 | return { 348 | init: function(config) { 349 | var deferred = Q.defer(); 350 | 351 | if (!requestFileSystem) { 352 | deferred.reject("No FS API"); 353 | return deferred.promise; 354 | } 355 | 356 | var prefix = config.name + '/'; 357 | 358 | persistentStorage.requestQuota(config.size, 359 | function(numBytes) { 360 | requestFileSystem(window.PERSISTENT, numBytes, 361 | function(fs) { 362 | fs.root.getDirectory(config.name, {create: true}, 363 | function() { 364 | deferred.resolve(new FSAPI(fs, numBytes, prefix)); 365 | }, function(err) { 366 | console.error(err); 367 | deferred.reject(err); 368 | }); 369 | }, function(err) { 370 | // TODO: implement various error messages. 371 | console.error(err); 372 | deferred.reject(err); 373 | }); 374 | }, function(err) { 375 | // TODO: implement various error messages. 376 | console.error(err); 377 | deferred.reject(err); 378 | }); 379 | 380 | return deferred.promise; 381 | }, 382 | 383 | isAvailable: function() { 384 | return requestFileSystem != null; 385 | } 386 | } 387 | })(Q); -------------------------------------------------------------------------------- /src/impls/IndexedDBProvider.js: -------------------------------------------------------------------------------- 1 | var indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.OIndexedDB || window.msIndexedDB; 2 | var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.OIDBTransaction || window.msIDBTransaction; 3 | var IndexedDBProvider = (function(Q) { 4 | var URL = window.URL || window.webkitURL; 5 | 6 | var convertToBase64 = utils.convertToBase64; 7 | var dataURLToBlob = utils.dataURLToBlob; 8 | 9 | function IDB(db) { 10 | this._db = db; 11 | this.type = 'IndexedDB'; 12 | 13 | var transaction = this._db.transaction(['attachments'], 'readwrite'); 14 | this._supportsBlobs = true; 15 | try { 16 | transaction.objectStore('attachments') 17 | .put(Blob(["sdf"], {type: "text/plain"}), "featurecheck"); 18 | } catch (e) { 19 | this._supportsBlobs = false; 20 | } 21 | } 22 | 23 | // TODO: normalize returns and errors. 24 | IDB.prototype = { 25 | getContents: function(docKey) { 26 | var deferred = Q.defer(); 27 | var transaction = this._db.transaction(['files'], 'readonly'); 28 | 29 | var get = transaction.objectStore('files').get(docKey); 30 | get.onsuccess = function(e) { 31 | deferred.resolve(e.target.result); 32 | }; 33 | 34 | get.onerror = function(e) { 35 | deferred.reject(e); 36 | }; 37 | 38 | return deferred.promise; 39 | }, 40 | 41 | setContents: function(docKey, data) { 42 | var deferred = Q.defer(); 43 | var transaction = this._db.transaction(['files'], 'readwrite'); 44 | 45 | var put = transaction.objectStore('files').put(data, docKey); 46 | put.onsuccess = function(e) { 47 | deferred.resolve(e); 48 | }; 49 | 50 | put.onerror = function(e) { 51 | deferred.reject(e); 52 | }; 53 | 54 | return deferred.promise; 55 | }, 56 | 57 | rm: function(docKey) { 58 | var deferred = Q.defer(); 59 | var finalDeferred = Q.defer(); 60 | 61 | var transaction = this._db.transaction(['files', 'attachments'], 'readwrite'); 62 | 63 | var del = transaction.objectStore('files').delete(docKey); 64 | 65 | del.onsuccess = function(e) { 66 | deferred.promise.then(function() { 67 | finalDeferred.resolve(); 68 | }); 69 | }; 70 | 71 | del.onerror = function(e) { 72 | deferred.promise.catch(function() { 73 | finalDeferred.reject(e); 74 | }); 75 | }; 76 | 77 | var attachmentsStore = transaction.objectStore('attachments'); 78 | var index = attachmentsStore.index('fname'); 79 | var cursor = index.openCursor(IDBKeyRange.only(docKey)); 80 | cursor.onsuccess = function(e) { 81 | var cursor = e.target.result; 82 | if (cursor) { 83 | cursor.delete(); 84 | cursor.continue(); 85 | } else { 86 | deferred.resolve(); 87 | } 88 | }; 89 | 90 | cursor.onerror = function(e) { 91 | deferred.reject(e); 92 | } 93 | 94 | return finalDeferred.promise; 95 | }, 96 | 97 | getAttachment: function(docKey, attachKey) { 98 | var deferred = Q.defer(); 99 | 100 | var transaction = this._db.transaction(['attachments'], 'readonly'); 101 | var get = transaction.objectStore('attachments').get(docKey + '/' + attachKey); 102 | 103 | var self = this; 104 | get.onsuccess = function(e) { 105 | if (!e.target.result) { 106 | deferred.resolve(undefined); 107 | return; 108 | } 109 | 110 | var data = e.target.result.data; 111 | if (!self._supportsBlobs) { 112 | data = dataURLToBlob(data); 113 | } 114 | deferred.resolve(data); 115 | }; 116 | 117 | get.onerror = function(e) { 118 | deferred.reject(e); 119 | }; 120 | 121 | return deferred.promise; 122 | }, 123 | 124 | ls: function(docKey) { 125 | var deferred = Q.defer(); 126 | 127 | if (!docKey) { 128 | // list docs 129 | var store = 'files'; 130 | } else { 131 | // list attachments 132 | var store = 'attachments'; 133 | } 134 | 135 | var transaction = this._db.transaction([store], 'readonly'); 136 | var cursor = transaction.objectStore(store).openCursor(); 137 | var listing = []; 138 | 139 | cursor.onsuccess = function(e) { 140 | var cursor = e.target.result; 141 | if (cursor) { 142 | listing.push(!docKey ? cursor.key : cursor.key.split('/')[1]); 143 | cursor.continue(); 144 | } else { 145 | deferred.resolve(listing); 146 | } 147 | }; 148 | 149 | cursor.onerror = function(e) { 150 | deferred.reject(e); 151 | }; 152 | 153 | return deferred.promise; 154 | }, 155 | 156 | clear: function() { 157 | var deferred = Q.defer(); 158 | var finalDeferred = Q.defer(); 159 | 160 | var t = this._db.transaction(['attachments', 'files'], 'readwrite'); 161 | 162 | 163 | var req1 = t.objectStore('attachments').clear(); 164 | var req2 = t.objectStore('files').clear(); 165 | 166 | req1.onsuccess = function() { 167 | deferred.promise.then(finalDeferred.resolve); 168 | }; 169 | 170 | req2.onsuccess = function() { 171 | deferred.resolve(); 172 | }; 173 | 174 | req1.onerror = function(err) { 175 | finalDeferred.reject(err); 176 | }; 177 | 178 | req2.onerror = function(err) { 179 | finalDeferred.reject(err); 180 | }; 181 | 182 | return finalDeferred.promise; 183 | }, 184 | 185 | getAllAttachments: function(docKey) { 186 | var deferred = Q.defer(); 187 | var self = this; 188 | 189 | var transaction = this._db.transaction(['attachments'], 'readonly'); 190 | var index = transaction.objectStore('attachments').index('fname'); 191 | 192 | var cursor = index.openCursor(IDBKeyRange.only(docKey)); 193 | var values = []; 194 | cursor.onsuccess = function(e) { 195 | var cursor = e.target.result; 196 | if (cursor) { 197 | var data; 198 | if (!self._supportsBlobs) { 199 | data = dataURLToBlob(cursor.value.data) 200 | } else { 201 | data = cursor.value.data; 202 | } 203 | values.push({ 204 | data: data, 205 | docKey: docKey, 206 | attachKey: cursor.primaryKey.split('/')[1] // TODO 207 | }); 208 | cursor.continue(); 209 | } else { 210 | deferred.resolve(values); 211 | } 212 | }; 213 | 214 | cursor.onerror = function(e) { 215 | deferred.reject(e); 216 | }; 217 | 218 | return deferred.promise; 219 | }, 220 | 221 | getAllAttachmentURLs: function(docKey) { 222 | var deferred = Q.defer(); 223 | this.getAllAttachments(docKey).then(function(attachments) { 224 | var urls = attachments.map(function(a) { 225 | a.url = URL.createObjectURL(a.data); 226 | delete a.data; 227 | return a; 228 | }); 229 | 230 | deferred.resolve(urls); 231 | }, function(e) { 232 | deferred.reject(e); 233 | }); 234 | 235 | return deferred.promise; 236 | }, 237 | 238 | getAttachmentURL: function(docKey, attachKey) { 239 | var deferred = Q.defer(); 240 | this.getAttachment(docKey, attachKey).then(function(attachment) { 241 | deferred.resolve(URL.createObjectURL(attachment)); 242 | }, function(e) { 243 | deferred.reject(e); 244 | }); 245 | 246 | return deferred.promise; 247 | }, 248 | 249 | revokeAttachmentURL: function(url) { 250 | URL.revokeObjectURL(url); 251 | }, 252 | 253 | setAttachment: function(docKey, attachKey, data) { 254 | var deferred = Q.defer(); 255 | 256 | if (data instanceof Blob && !this._supportsBlobs) { 257 | var self = this; 258 | convertToBase64(data, function(data) { 259 | continuation.call(self, data); 260 | }); 261 | } else { 262 | continuation.call(this, data); 263 | } 264 | 265 | function continuation(data) { 266 | var obj = { 267 | path: docKey + '/' + attachKey, 268 | fname: docKey, 269 | data: data 270 | }; 271 | var transaction = this._db.transaction(['attachments'], 'readwrite'); 272 | var put = transaction.objectStore('attachments').put(obj); 273 | 274 | put.onsuccess = function(e) { 275 | deferred.resolve(e); 276 | }; 277 | 278 | put.onerror = function(e) { 279 | deferred.reject(e); 280 | }; 281 | } 282 | 283 | return deferred.promise; 284 | }, 285 | 286 | rmAttachment: function(docKey, attachKey) { 287 | var deferred = Q.defer(); 288 | var transaction = this._db.transaction(['attachments'], 'readwrite'); 289 | var del = transaction.objectStore('attachments').delete(docKey + '/' + attachKey); 290 | 291 | del.onsuccess = function(e) { 292 | deferred.resolve(e); 293 | }; 294 | 295 | del.onerror = function(e) { 296 | deferred.reject(e); 297 | }; 298 | 299 | return deferred.promise; 300 | } 301 | }; 302 | 303 | return { 304 | init: function(config) { 305 | var deferred = Q.defer(); 306 | var dbVersion = 2; 307 | 308 | if (!indexedDB || !IDBTransaction) { 309 | deferred.reject("No IndexedDB"); 310 | return deferred.promise; 311 | } 312 | 313 | var request = indexedDB.open(config.name, dbVersion); 314 | 315 | function createObjectStore(db) { 316 | db.createObjectStore("files"); 317 | var attachStore = db.createObjectStore("attachments", {keyPath: 'path'}); 318 | attachStore.createIndex('fname', 'fname', {unique: false}) 319 | } 320 | 321 | // TODO: normalize errors 322 | request.onerror = function (event) { 323 | deferred.reject(event); 324 | }; 325 | 326 | request.onsuccess = function (event) { 327 | var db = request.result; 328 | 329 | db.onerror = function (event) { 330 | console.log(event); 331 | }; 332 | 333 | // Chrome workaround 334 | if (db.setVersion) { 335 | if (db.version != dbVersion) { 336 | var setVersion = db.setVersion(dbVersion); 337 | setVersion.onsuccess = function () { 338 | createObjectStore(db); 339 | deferred.resolve(); 340 | }; 341 | } 342 | else { 343 | deferred.resolve(new IDB(db)); 344 | } 345 | } else { 346 | deferred.resolve(new IDB(db)); 347 | } 348 | } 349 | 350 | request.onupgradeneeded = function (event) { 351 | createObjectStore(event.target.result); 352 | }; 353 | 354 | return deferred.promise; 355 | }, 356 | 357 | isAvailable: function() { 358 | return indexedDB != null && IDBTransaction != null; 359 | } 360 | } 361 | })(Q); -------------------------------------------------------------------------------- /src/impls/LocalStorageProvider.js: -------------------------------------------------------------------------------- 1 | var LocalStorageProvider = (function(Q) { 2 | return { 3 | init: function() { 4 | return Q({type: 'LocalStorage'}); 5 | } 6 | } 7 | })(Q); -------------------------------------------------------------------------------- /src/impls/WebSQLProvider.js: -------------------------------------------------------------------------------- 1 | var openDb = window.openDatabase; 2 | var WebSQLProvider = (function(Q) { 3 | var URL = window.URL || window.webkitURL; 4 | var convertToBase64 = utils.convertToBase64; 5 | var dataURLToBlob = utils.dataURLToBlob; 6 | 7 | function WSQL(db) { 8 | this._db = db; 9 | this.type = 'WebSQL'; 10 | } 11 | 12 | WSQL.prototype = { 13 | getContents: function(docKey, options) { 14 | var deferred = Q.defer(); 15 | this._db.transaction(function(tx) { 16 | tx.executeSql('SELECT value FROM files WHERE fname = ?', [docKey], 17 | function(tx, res) { 18 | if (res.rows.length == 0) { 19 | deferred.resolve(undefined); 20 | } else { 21 | var data = res.rows.item(0).value; 22 | if (options && options.json) 23 | data = JSON.parse(data); 24 | deferred.resolve(data); 25 | } 26 | }); 27 | }, function(err) { 28 | consol.log(err); 29 | deferred.reject(err); 30 | }); 31 | 32 | return deferred.promise; 33 | }, 34 | 35 | setContents: function(docKey, data, options) { 36 | var deferred = Q.defer(); 37 | if (options && options.json) 38 | data = JSON.stringify(data); 39 | 40 | this._db.transaction(function(tx) { 41 | tx.executeSql( 42 | 'INSERT OR REPLACE INTO files (fname, value) VALUES(?, ?)', [docKey, data]); 43 | }, function(err) { 44 | console.log(err); 45 | deferred.reject(err); 46 | }, function() { 47 | deferred.resolve(); 48 | }); 49 | 50 | return deferred.promise; 51 | }, 52 | 53 | rm: function(docKey) { 54 | var deferred = Q.defer(); 55 | 56 | this._db.transaction(function(tx) { 57 | tx.executeSql('DELETE FROM files WHERE fname = ?', [docKey]); 58 | tx.executeSql('DELETE FROM attachments WHERE fname = ?', [docKey]); 59 | }, function(err) { 60 | console.log(err); 61 | deferred.reject(err); 62 | }, function() { 63 | deferred.resolve(); 64 | }); 65 | 66 | return deferred.promise; 67 | }, 68 | 69 | getAttachment: function(fname, akey) { 70 | var deferred = Q.defer(); 71 | 72 | this._db.transaction(function(tx){ 73 | tx.executeSql('SELECT value FROM attachments WHERE fname = ? AND akey = ?', 74 | [fname, akey], 75 | function(tx, res) { 76 | if (res.rows.length == 0) { 77 | deferred.resolve(undefined); 78 | } else { 79 | deferred.resolve(dataURLToBlob(res.rows.item(0).value)); 80 | } 81 | }); 82 | }, function(err) { 83 | deferred.reject(err); 84 | }); 85 | 86 | return deferred.promise; 87 | }, 88 | 89 | getAttachmentURL: function(docKey, attachKey) { 90 | var deferred = Q.defer(); 91 | this.getAttachment(docKey, attachKey).then(function(blob) { 92 | deferred.resolve(URL.createObjectURL(blob)); 93 | }, function() { 94 | deferred.reject(); 95 | }); 96 | 97 | return deferred.promise; 98 | }, 99 | 100 | ls: function(docKey) { 101 | var deferred = Q.defer(); 102 | 103 | var select; 104 | var field; 105 | if (!docKey) { 106 | select = 'SELECT fname FROM files'; 107 | field = 'fname'; 108 | } else { 109 | select = 'SELECT akey FROM attachments WHERE fname = ?'; 110 | field = 'akey'; 111 | } 112 | 113 | this._db.transaction(function(tx) { 114 | tx.executeSql(select, docKey ? [docKey] : [], 115 | function(tx, res) { 116 | var listing = []; 117 | for (var i = 0; i < res.rows.length; ++i) { 118 | listing.push(res.rows.item(i)[field]); 119 | } 120 | 121 | deferred.resolve(listing); 122 | }, function(err) { 123 | deferred.reject(err); 124 | }); 125 | }); 126 | 127 | return deferred.promise; 128 | }, 129 | 130 | clear: function() { 131 | var deffered1 = Q.defer(); 132 | var deffered2 = Q.defer(); 133 | 134 | this._db.transaction(function(tx) { 135 | tx.executeSql('DELETE FROM files', function() { 136 | deffered1.resolve(); 137 | }); 138 | tx.executeSql('DELETE FROM attachments', function() { 139 | deffered2.resolve(); 140 | }); 141 | }, function(err) { 142 | deffered1.reject(err); 143 | deffered2.reject(err); 144 | }); 145 | 146 | return Q.all([deffered1, deffered2]); 147 | }, 148 | 149 | getAllAttachments: function(fname) { 150 | var deferred = Q.defer(); 151 | 152 | this._db.transaction(function(tx) { 153 | tx.executeSql('SELECT value, akey FROM attachments WHERE fname = ?', 154 | [fname], 155 | function(tx, res) { 156 | // TODO: ship this work off to a webworker 157 | // since there could be many of these conversions? 158 | var result = []; 159 | for (var i = 0; i < res.rows.length; ++i) { 160 | var item = res.rows.item(i); 161 | result.push({ 162 | docKey: fname, 163 | attachKey: item.akey, 164 | data: dataURLToBlob(item.value) 165 | }); 166 | } 167 | 168 | deferred.resolve(result); 169 | }); 170 | }, function(err) { 171 | deferred.reject(err); 172 | }); 173 | 174 | return deferred.promise; 175 | }, 176 | 177 | getAllAttachmentURLs: function(fname) { 178 | var deferred = Q.defer(); 179 | this.getAllAttachments(fname).then(function(attachments) { 180 | var urls = attachments.map(function(a) { 181 | a.url = URL.createObjectURL(a.data); 182 | delete a.data; 183 | return a; 184 | }); 185 | 186 | deferred.resolve(urls); 187 | }, function(e) { 188 | deferred.reject(e); 189 | }); 190 | 191 | return deferred.promise; 192 | }, 193 | 194 | revokeAttachmentURL: function(url) { 195 | URL.revokeObjectURL(url); 196 | }, 197 | 198 | setAttachment: function(fname, akey, data) { 199 | var deferred = Q.defer(); 200 | 201 | var self = this; 202 | convertToBase64(data, function(data) { 203 | self._db.transaction(function(tx) { 204 | tx.executeSql( 205 | 'INSERT OR REPLACE INTO attachments (fname, akey, value) VALUES(?, ?, ?)', 206 | [fname, akey, data]); 207 | }, function(err) { 208 | deferred.reject(err); 209 | }, function() { 210 | deferred.resolve(); 211 | }); 212 | }); 213 | 214 | return deferred.promise; 215 | }, 216 | 217 | rmAttachment: function(fname, akey) { 218 | var deferred = Q.defer(); 219 | this._db.transaction(function(tx) { 220 | tx.executeSql('DELETE FROM attachments WHERE fname = ? AND akey = ?', 221 | [fname, akey]); 222 | }, function(err) { 223 | deferred.reject(err); 224 | }, function() { 225 | deferred.resolve(); 226 | }); 227 | 228 | return deferred.promise; 229 | } 230 | }; 231 | 232 | return { 233 | init: function(config) { 234 | var deferred = Q.defer(); 235 | if (!openDb) { 236 | deferred.reject("No WebSQL"); 237 | return deferred.promise; 238 | } 239 | 240 | var db = openDb(config.name, '1.0', 'large local storage', config.size); 241 | 242 | db.transaction(function(tx) { 243 | tx.executeSql('CREATE TABLE IF NOT EXISTS files (fname unique, value)'); 244 | tx.executeSql('CREATE TABLE IF NOT EXISTS attachments (fname, akey, value)'); 245 | tx.executeSql('CREATE INDEX IF NOT EXISTS fname_index ON attachments (fname)'); 246 | tx.executeSql('CREATE INDEX IF NOT EXISTS akey_index ON attachments (akey)'); 247 | tx.executeSql('CREATE UNIQUE INDEX IF NOT EXISTS uniq_attach ON attachments (fname, akey)') 248 | }, function(err) { 249 | deferred.reject(err); 250 | }, function() { 251 | deferred.resolve(new WSQL(db)); 252 | }); 253 | 254 | return deferred.promise; 255 | }, 256 | 257 | isAvailable: function() { 258 | return openDb != null; 259 | } 260 | } 261 | })(Q); -------------------------------------------------------------------------------- /src/impls/utils.js: -------------------------------------------------------------------------------- 1 | var utils = (function() { 2 | return { 3 | convertToBase64: function(blob, cb) { 4 | var fr = new FileReader(); 5 | fr.onload = function(e) { 6 | cb(e.target.result); 7 | }; 8 | fr.onerror = function(e) { 9 | }; 10 | fr.onabort = function(e) { 11 | }; 12 | fr.readAsDataURL(blob); 13 | }, 14 | 15 | dataURLToBlob: function(dataURL) { 16 | var BASE64_MARKER = ';base64,'; 17 | if (dataURL.indexOf(BASE64_MARKER) == -1) { 18 | var parts = dataURL.split(','); 19 | var contentType = parts[0].split(':')[1]; 20 | var raw = parts[1]; 21 | 22 | return new Blob([raw], {type: contentType}); 23 | } 24 | 25 | var parts = dataURL.split(BASE64_MARKER); 26 | var contentType = parts[0].split(':')[1]; 27 | var raw = window.atob(parts[1]); 28 | var rawLength = raw.length; 29 | 30 | var uInt8Array = new Uint8Array(rawLength); 31 | 32 | for (var i = 0; i < rawLength; ++i) { 33 | uInt8Array[i] = raw.charCodeAt(i); 34 | } 35 | 36 | return new Blob([uInt8Array.buffer], {type: contentType}); 37 | }, 38 | 39 | splitAttachmentPath: function(path) { 40 | var parts = path.split('/'); 41 | if (parts.length == 1) 42 | parts.unshift('__nodoc__'); 43 | return parts; 44 | }, 45 | 46 | mapAsync: function(fn, promise) { 47 | var deferred = Q.defer(); 48 | promise.then(function(data) { 49 | _mapAsync(fn, data, [], deferred); 50 | }, function(e) { 51 | deferred.reject(e); 52 | }); 53 | 54 | return deferred.promise; 55 | }, 56 | 57 | countdown: function(n, cb) { 58 | var args = []; 59 | return function() { 60 | for (var i = 0; i < arguments.length; ++i) 61 | args.push(arguments[i]); 62 | n -= 1; 63 | if (n == 0) 64 | cb.apply(this, args); 65 | } 66 | } 67 | }; 68 | 69 | function _mapAsync(fn, data, result, deferred) { 70 | fn(data[result.length], function(v) { 71 | result.push(v); 72 | if (result.length == data.length) 73 | deferred.resolve(result); 74 | else 75 | _mapAsync(fn, data, result, deferred); 76 | }, function(err) { 77 | deferred.reject(err); 78 | }) 79 | } 80 | })(); -------------------------------------------------------------------------------- /src/pipeline.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | @author Matt Crinklaw-Vogt 4 | */ 5 | function PipeContext(handlers, nextMehod, end) { 6 | this._handlers = handlers; 7 | this._next = nextMehod; 8 | this._end = end; 9 | 10 | this._i = 0; 11 | } 12 | 13 | PipeContext.prototype = { 14 | next: function() { 15 | // var args = Array.prototype.slice.call(arguments, 0); 16 | // args.unshift(this); 17 | this.__pipectx = this; 18 | return this._next.apply(this, arguments); 19 | }, 20 | 21 | _nextHandler: function() { 22 | if (this._i >= this._handlers.length) return this._end; 23 | 24 | var handler = this._handlers[this._i].handler; 25 | this._i += 1; 26 | return handler; 27 | }, 28 | 29 | length: function() { 30 | return this._handlers.length; 31 | } 32 | }; 33 | 34 | function indexOfHandler(handlers, len, target) { 35 | for (var i = 0; i < len; ++i) { 36 | var handler = handlers[i]; 37 | if (handler.name === target || handler.handler === target) { 38 | return i; 39 | } 40 | } 41 | 42 | return -1; 43 | } 44 | 45 | function forward(ctx) { 46 | return ctx.next.apply(ctx, Array.prototype.slice.call(arguments, 1)); 47 | } 48 | 49 | function coerce(methodNames, handler) { 50 | methodNames.forEach(function(meth) { 51 | if (!handler[meth]) 52 | handler[meth] = forward; 53 | }); 54 | } 55 | 56 | var abstractPipeline = { 57 | addFirst: function(name, handler) { 58 | coerce(this._pipedMethodNames, handler); 59 | this._handlers.unshift({name: name, handler: handler}); 60 | }, 61 | 62 | addLast: function(name, handler) { 63 | coerce(this._pipedMethodNames, handler); 64 | this._handlers.push({name: name, handler: handler}); 65 | }, 66 | 67 | /** 68 | Add the handler with the given name after the 69 | handler specified by target. Target can be a handler 70 | name or a handler instance. 71 | */ 72 | addAfter: function(target, name, handler) { 73 | coerce(this._pipedMethodNames, handler); 74 | var handlers = this._handlers; 75 | var len = handlers.length; 76 | var i = indexOfHandler(handlers, len, target); 77 | 78 | if (i >= 0) { 79 | handlers.splice(i+1, 0, {name: name, handler: handler}); 80 | } 81 | }, 82 | 83 | /** 84 | Add the handler with the given name after the handler 85 | specified by target. Target can be a handler name or 86 | a handler instance. 87 | */ 88 | addBefore: function(target, name, handler) { 89 | coerce(this._pipedMethodNames, handler); 90 | var handlers = this._handlers; 91 | var len = handlers.length; 92 | var i = indexOfHandler(handlers, len, target); 93 | 94 | if (i >= 0) { 95 | handlers.splice(i, 0, {name: name, handler: handler}); 96 | } 97 | }, 98 | 99 | /** 100 | Replace the handler specified by target. 101 | */ 102 | replace: function(target, newName, handler) { 103 | coerce(this._pipedMethodNames, handler); 104 | var handlers = this._handlers; 105 | var len = handlers.length; 106 | var i = indexOfHandler(handlers, len, target); 107 | 108 | if (i >= 0) { 109 | handlers.splice(i, 1, {name: newName, handler: handler}); 110 | } 111 | }, 112 | 113 | removeFirst: function() { 114 | return this._handlers.shift(); 115 | }, 116 | 117 | removeLast: function() { 118 | return this._handlers.pop(); 119 | }, 120 | 121 | remove: function(target) { 122 | var handlers = this._handlers; 123 | var len = handlers.length; 124 | var i = indexOfHandler(handlers, len, target); 125 | 126 | if (i >= 0) 127 | handlers.splice(i, 1); 128 | }, 129 | 130 | getHandler: function(name) { 131 | var i = indexOfHandler(this._handlers, this._handlers.length, name); 132 | if (i >= 0) 133 | return this._handlers[i].handler; 134 | return null; 135 | } 136 | }; 137 | 138 | function createPipeline(pipedMethodNames) { 139 | var end = {}; 140 | var endStubFunc = function() { return end; }; 141 | var nextMethods = {}; 142 | 143 | function Pipeline(pipedMethodNames) { 144 | this.pipe = { 145 | _handlers: [], 146 | _contextCtor: PipeContext, 147 | _nextMethods: nextMethods, 148 | end: end, 149 | _pipedMethodNames: pipedMethodNames 150 | }; 151 | } 152 | 153 | var pipeline = new Pipeline(pipedMethodNames); 154 | for (var k in abstractPipeline) { 155 | pipeline.pipe[k] = abstractPipeline[k]; 156 | } 157 | 158 | pipedMethodNames.forEach(function(name) { 159 | end[name] = endStubFunc; 160 | 161 | nextMethods[name] = new Function( 162 | "var handler = this._nextHandler();" + 163 | "handler.__pipectx = this.__pipectx;" + 164 | "return handler." + name + ".apply(handler, arguments);"); 165 | 166 | pipeline[name] = new Function( 167 | "var ctx = new this.pipe._contextCtor(this.pipe._handlers, this.pipe._nextMethods." + name + ", this.pipe.end);" 168 | + "return ctx.next.apply(ctx, arguments);"); 169 | }); 170 | 171 | return pipeline; 172 | } 173 | 174 | createPipeline.isPipeline = function(obj) { 175 | return obj instanceof Pipeline; 176 | } -------------------------------------------------------------------------------- /test/elephant.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tantaman/LargeLocalStorage/e4fc5d03be1dfd497f29b0dadde25e3c5388c0ea/test/elephant.jpg -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Mocha Spec Runner 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/lib/expect.js: -------------------------------------------------------------------------------- 1 | 2 | (function (global, module) { 3 | 4 | if ('undefined' == typeof module) { 5 | var module = { exports: {} } 6 | , exports = module.exports 7 | } 8 | 9 | /** 10 | * Exports. 11 | */ 12 | 13 | module.exports = expect; 14 | expect.Assertion = Assertion; 15 | 16 | /** 17 | * Exports version. 18 | */ 19 | 20 | expect.version = '0.1.2'; 21 | 22 | /** 23 | * Possible assertion flags. 24 | */ 25 | 26 | var flags = { 27 | not: ['to', 'be', 'have', 'include', 'only'] 28 | , to: ['be', 'have', 'include', 'only', 'not'] 29 | , only: ['have'] 30 | , have: ['own'] 31 | , be: ['an'] 32 | }; 33 | 34 | function expect (obj) { 35 | return new Assertion(obj); 36 | } 37 | 38 | /** 39 | * Constructor 40 | * 41 | * @api private 42 | */ 43 | 44 | function Assertion (obj, flag, parent) { 45 | this.obj = obj; 46 | this.flags = {}; 47 | 48 | if (undefined != parent) { 49 | this.flags[flag] = true; 50 | 51 | for (var i in parent.flags) { 52 | if (parent.flags.hasOwnProperty(i)) { 53 | this.flags[i] = true; 54 | } 55 | } 56 | } 57 | 58 | var $flags = flag ? flags[flag] : keys(flags) 59 | , self = this 60 | 61 | if ($flags) { 62 | for (var i = 0, l = $flags.length; i < l; i++) { 63 | // avoid recursion 64 | if (this.flags[$flags[i]]) continue; 65 | 66 | var name = $flags[i] 67 | , assertion = new Assertion(this.obj, name, this) 68 | 69 | if ('function' == typeof Assertion.prototype[name]) { 70 | // clone the function, make sure we dont touch the prot reference 71 | var old = this[name]; 72 | this[name] = function () { 73 | return old.apply(self, arguments); 74 | } 75 | 76 | for (var fn in Assertion.prototype) { 77 | if (Assertion.prototype.hasOwnProperty(fn) && fn != name) { 78 | this[name][fn] = bind(assertion[fn], assertion); 79 | } 80 | } 81 | } else { 82 | this[name] = assertion; 83 | } 84 | } 85 | } 86 | }; 87 | 88 | /** 89 | * Performs an assertion 90 | * 91 | * @api private 92 | */ 93 | 94 | Assertion.prototype.assert = function (truth, msg, error) { 95 | var msg = this.flags.not ? error : msg 96 | , ok = this.flags.not ? !truth : truth; 97 | 98 | if (!ok) { 99 | throw new Error(msg); 100 | } 101 | 102 | this.and = new Assertion(this.obj); 103 | }; 104 | 105 | /** 106 | * Check if the value is truthy 107 | * 108 | * @api public 109 | */ 110 | 111 | Assertion.prototype.ok = function () { 112 | this.assert( 113 | !!this.obj 114 | , 'expected ' + i(this.obj) + ' to be truthy' 115 | , 'expected ' + i(this.obj) + ' to be falsy'); 116 | }; 117 | 118 | /** 119 | * Assert that the function throws. 120 | * 121 | * @param {Function|RegExp} callback, or regexp to match error string against 122 | * @api public 123 | */ 124 | 125 | Assertion.prototype.throwError = 126 | Assertion.prototype.throwException = function (fn) { 127 | expect(this.obj).to.be.a('function'); 128 | 129 | var thrown = false 130 | , not = this.flags.not 131 | 132 | try { 133 | this.obj(); 134 | } catch (e) { 135 | if ('function' == typeof fn) { 136 | fn(e); 137 | } else if ('object' == typeof fn) { 138 | var subject = 'string' == typeof e ? e : e.message; 139 | if (not) { 140 | expect(subject).to.not.match(fn); 141 | } else { 142 | expect(subject).to.match(fn); 143 | } 144 | } 145 | thrown = true; 146 | } 147 | 148 | if ('object' == typeof fn && not) { 149 | // in the presence of a matcher, ensure the `not` only applies to 150 | // the matching. 151 | this.flags.not = false; 152 | } 153 | 154 | var name = this.obj.name || 'fn'; 155 | this.assert( 156 | thrown 157 | , 'expected ' + name + ' to throw an exception' 158 | , 'expected ' + name + ' not to throw an exception'); 159 | }; 160 | 161 | /** 162 | * Checks if the array is empty. 163 | * 164 | * @api public 165 | */ 166 | 167 | Assertion.prototype.empty = function () { 168 | var expectation; 169 | 170 | if ('object' == typeof this.obj && null !== this.obj && !isArray(this.obj)) { 171 | if ('number' == typeof this.obj.length) { 172 | expectation = !this.obj.length; 173 | } else { 174 | expectation = !keys(this.obj).length; 175 | } 176 | } else { 177 | if ('string' != typeof this.obj) { 178 | expect(this.obj).to.be.an('object'); 179 | } 180 | 181 | expect(this.obj).to.have.property('length'); 182 | expectation = !this.obj.length; 183 | } 184 | 185 | this.assert( 186 | expectation 187 | , 'expected ' + i(this.obj) + ' to be empty' 188 | , 'expected ' + i(this.obj) + ' to not be empty'); 189 | return this; 190 | }; 191 | 192 | /** 193 | * Checks if the obj exactly equals another. 194 | * 195 | * @api public 196 | */ 197 | 198 | Assertion.prototype.be = 199 | Assertion.prototype.equal = function (obj) { 200 | this.assert( 201 | obj === this.obj 202 | , 'expected ' + i(this.obj) + ' to equal ' + i(obj) 203 | , 'expected ' + i(this.obj) + ' to not equal ' + i(obj)); 204 | return this; 205 | }; 206 | 207 | /** 208 | * Checks if the obj sortof equals another. 209 | * 210 | * @api public 211 | */ 212 | 213 | Assertion.prototype.eql = function (obj) { 214 | this.assert( 215 | expect.eql(obj, this.obj) 216 | , 'expected ' + i(this.obj) + ' to sort of equal ' + i(obj) 217 | , 'expected ' + i(this.obj) + ' to sort of not equal ' + i(obj)); 218 | return this; 219 | }; 220 | 221 | /** 222 | * Assert within start to finish (inclusive). 223 | * 224 | * @param {Number} start 225 | * @param {Number} finish 226 | * @api public 227 | */ 228 | 229 | Assertion.prototype.within = function (start, finish) { 230 | var range = start + '..' + finish; 231 | this.assert( 232 | this.obj >= start && this.obj <= finish 233 | , 'expected ' + i(this.obj) + ' to be within ' + range 234 | , 'expected ' + i(this.obj) + ' to not be within ' + range); 235 | return this; 236 | }; 237 | 238 | /** 239 | * Assert typeof / instance of 240 | * 241 | * @api public 242 | */ 243 | 244 | Assertion.prototype.a = 245 | Assertion.prototype.an = function (type) { 246 | if ('string' == typeof type) { 247 | // proper english in error msg 248 | var n = /^[aeiou]/.test(type) ? 'n' : ''; 249 | 250 | // typeof with support for 'array' 251 | this.assert( 252 | 'array' == type ? isArray(this.obj) : 253 | 'object' == type 254 | ? 'object' == typeof this.obj && null !== this.obj 255 | : type == typeof this.obj 256 | , 'expected ' + i(this.obj) + ' to be a' + n + ' ' + type 257 | , 'expected ' + i(this.obj) + ' not to be a' + n + ' ' + type); 258 | } else { 259 | // instanceof 260 | var name = type.name || 'supplied constructor'; 261 | this.assert( 262 | this.obj instanceof type 263 | , 'expected ' + i(this.obj) + ' to be an instance of ' + name 264 | , 'expected ' + i(this.obj) + ' not to be an instance of ' + name); 265 | } 266 | 267 | return this; 268 | }; 269 | 270 | /** 271 | * Assert numeric value above _n_. 272 | * 273 | * @param {Number} n 274 | * @api public 275 | */ 276 | 277 | Assertion.prototype.greaterThan = 278 | Assertion.prototype.above = function (n) { 279 | this.assert( 280 | this.obj > n 281 | , 'expected ' + i(this.obj) + ' to be above ' + n 282 | , 'expected ' + i(this.obj) + ' to be below ' + n); 283 | return this; 284 | }; 285 | 286 | /** 287 | * Assert numeric value below _n_. 288 | * 289 | * @param {Number} n 290 | * @api public 291 | */ 292 | 293 | Assertion.prototype.lessThan = 294 | Assertion.prototype.below = function (n) { 295 | this.assert( 296 | this.obj < n 297 | , 'expected ' + i(this.obj) + ' to be below ' + n 298 | , 'expected ' + i(this.obj) + ' to be above ' + n); 299 | return this; 300 | }; 301 | 302 | /** 303 | * Assert string value matches _regexp_. 304 | * 305 | * @param {RegExp} regexp 306 | * @api public 307 | */ 308 | 309 | Assertion.prototype.match = function (regexp) { 310 | this.assert( 311 | regexp.exec(this.obj) 312 | , 'expected ' + i(this.obj) + ' to match ' + regexp 313 | , 'expected ' + i(this.obj) + ' not to match ' + regexp); 314 | return this; 315 | }; 316 | 317 | /** 318 | * Assert property "length" exists and has value of _n_. 319 | * 320 | * @param {Number} n 321 | * @api public 322 | */ 323 | 324 | Assertion.prototype.length = function (n) { 325 | expect(this.obj).to.have.property('length'); 326 | var len = this.obj.length; 327 | this.assert( 328 | n == len 329 | , 'expected ' + i(this.obj) + ' to have a length of ' + n + ' but got ' + len 330 | , 'expected ' + i(this.obj) + ' to not have a length of ' + len); 331 | return this; 332 | }; 333 | 334 | /** 335 | * Assert property _name_ exists, with optional _val_. 336 | * 337 | * @param {String} name 338 | * @param {Mixed} val 339 | * @api public 340 | */ 341 | 342 | Assertion.prototype.property = function (name, val) { 343 | if (this.flags.own) { 344 | this.assert( 345 | Object.prototype.hasOwnProperty.call(this.obj, name) 346 | , 'expected ' + i(this.obj) + ' to have own property ' + i(name) 347 | , 'expected ' + i(this.obj) + ' to not have own property ' + i(name)); 348 | return this; 349 | } 350 | 351 | if (this.flags.not && undefined !== val) { 352 | if (undefined === this.obj[name]) { 353 | throw new Error(i(this.obj) + ' has no property ' + i(name)); 354 | } 355 | } else { 356 | var hasProp; 357 | try { 358 | hasProp = name in this.obj 359 | } catch (e) { 360 | hasProp = undefined !== this.obj[name] 361 | } 362 | 363 | this.assert( 364 | hasProp 365 | , 'expected ' + i(this.obj) + ' to have a property ' + i(name) 366 | , 'expected ' + i(this.obj) + ' to not have a property ' + i(name)); 367 | } 368 | 369 | if (undefined !== val) { 370 | this.assert( 371 | val === this.obj[name] 372 | , 'expected ' + i(this.obj) + ' to have a property ' + i(name) 373 | + ' of ' + i(val) + ', but got ' + i(this.obj[name]) 374 | , 'expected ' + i(this.obj) + ' to not have a property ' + i(name) 375 | + ' of ' + i(val)); 376 | } 377 | 378 | this.obj = this.obj[name]; 379 | return this; 380 | }; 381 | 382 | /** 383 | * Assert that the array contains _obj_ or string contains _obj_. 384 | * 385 | * @param {Mixed} obj|string 386 | * @api public 387 | */ 388 | 389 | Assertion.prototype.string = 390 | Assertion.prototype.contain = function (obj) { 391 | if ('string' == typeof this.obj) { 392 | this.assert( 393 | ~this.obj.indexOf(obj) 394 | , 'expected ' + i(this.obj) + ' to contain ' + i(obj) 395 | , 'expected ' + i(this.obj) + ' to not contain ' + i(obj)); 396 | } else { 397 | this.assert( 398 | ~indexOf(this.obj, obj) 399 | , 'expected ' + i(this.obj) + ' to contain ' + i(obj) 400 | , 'expected ' + i(this.obj) + ' to not contain ' + i(obj)); 401 | } 402 | return this; 403 | }; 404 | 405 | /** 406 | * Assert exact keys or inclusion of keys by using 407 | * the `.own` modifier. 408 | * 409 | * @param {Array|String ...} keys 410 | * @api public 411 | */ 412 | 413 | Assertion.prototype.key = 414 | Assertion.prototype.keys = function ($keys) { 415 | var str 416 | , ok = true; 417 | 418 | $keys = isArray($keys) 419 | ? $keys 420 | : Array.prototype.slice.call(arguments); 421 | 422 | if (!$keys.length) throw new Error('keys required'); 423 | 424 | var actual = keys(this.obj) 425 | , len = $keys.length; 426 | 427 | // Inclusion 428 | ok = every($keys, function (key) { 429 | return ~indexOf(actual, key); 430 | }); 431 | 432 | // Strict 433 | if (!this.flags.not && this.flags.only) { 434 | ok = ok && $keys.length == actual.length; 435 | } 436 | 437 | // Key string 438 | if (len > 1) { 439 | $keys = map($keys, function (key) { 440 | return i(key); 441 | }); 442 | var last = $keys.pop(); 443 | str = $keys.join(', ') + ', and ' + last; 444 | } else { 445 | str = i($keys[0]); 446 | } 447 | 448 | // Form 449 | str = (len > 1 ? 'keys ' : 'key ') + str; 450 | 451 | // Have / include 452 | str = (!this.flags.only ? 'include ' : 'only have ') + str; 453 | 454 | // Assertion 455 | this.assert( 456 | ok 457 | , 'expected ' + i(this.obj) + ' to ' + str 458 | , 'expected ' + i(this.obj) + ' to not ' + str); 459 | 460 | return this; 461 | }; 462 | 463 | /** 464 | * Function bind implementation. 465 | */ 466 | 467 | function bind (fn, scope) { 468 | return function () { 469 | return fn.apply(scope, arguments); 470 | } 471 | } 472 | 473 | /** 474 | * Array every compatibility 475 | * 476 | * @see bit.ly/5Fq1N2 477 | * @api public 478 | */ 479 | 480 | function every (arr, fn, thisObj) { 481 | var scope = thisObj || global; 482 | for (var i = 0, j = arr.length; i < j; ++i) { 483 | if (!fn.call(scope, arr[i], i, arr)) { 484 | return false; 485 | } 486 | } 487 | return true; 488 | }; 489 | 490 | /** 491 | * Array indexOf compatibility. 492 | * 493 | * @see bit.ly/a5Dxa2 494 | * @api public 495 | */ 496 | 497 | function indexOf (arr, o, i) { 498 | if (Array.prototype.indexOf) { 499 | return Array.prototype.indexOf.call(arr, o, i); 500 | } 501 | 502 | if (arr.length === undefined) { 503 | return -1; 504 | } 505 | 506 | for (var j = arr.length, i = i < 0 ? i + j < 0 ? 0 : i + j : i || 0 507 | ; i < j && arr[i] !== o; i++); 508 | 509 | return j <= i ? -1 : i; 510 | }; 511 | 512 | /** 513 | * Inspects an object. 514 | * 515 | * @see taken from node.js `util` module (copyright Joyent, MIT license) 516 | * @api private 517 | */ 518 | 519 | function i (obj, showHidden, depth) { 520 | var seen = []; 521 | 522 | function stylize (str) { 523 | return str; 524 | }; 525 | 526 | function format (value, recurseTimes) { 527 | // Provide a hook for user-specified inspect functions. 528 | // Check that value is an object with an inspect function on it 529 | if (value && typeof value.inspect === 'function' && 530 | // Filter out the util module, it's inspect function is special 531 | value !== exports && 532 | // Also filter out any prototype objects using the circular check. 533 | !(value.constructor && value.constructor.prototype === value)) { 534 | return value.inspect(recurseTimes); 535 | } 536 | 537 | // Primitive types cannot have properties 538 | switch (typeof value) { 539 | case 'undefined': 540 | return stylize('undefined', 'undefined'); 541 | 542 | case 'string': 543 | var simple = '\'' + json.stringify(value).replace(/^"|"$/g, '') 544 | .replace(/'/g, "\\'") 545 | .replace(/\\"/g, '"') + '\''; 546 | return stylize(simple, 'string'); 547 | 548 | case 'number': 549 | return stylize('' + value, 'number'); 550 | 551 | case 'boolean': 552 | return stylize('' + value, 'boolean'); 553 | } 554 | // For some reason typeof null is "object", so special case here. 555 | if (value === null) { 556 | return stylize('null', 'null'); 557 | } 558 | 559 | // Look up the keys of the object. 560 | var visible_keys = keys(value); 561 | var $keys = showHidden ? Object.getOwnPropertyNames(value) : visible_keys; 562 | 563 | // Functions without properties can be shortcutted. 564 | if (typeof value === 'function' && $keys.length === 0) { 565 | if (isRegExp(value)) { 566 | return stylize('' + value, 'regexp'); 567 | } else { 568 | var name = value.name ? ': ' + value.name : ''; 569 | return stylize('[Function' + name + ']', 'special'); 570 | } 571 | } 572 | 573 | // Dates without properties can be shortcutted 574 | if (isDate(value) && $keys.length === 0) { 575 | return stylize(value.toUTCString(), 'date'); 576 | } 577 | 578 | var base, type, braces; 579 | // Determine the object type 580 | if (isArray(value)) { 581 | type = 'Array'; 582 | braces = ['[', ']']; 583 | } else { 584 | type = 'Object'; 585 | braces = ['{', '}']; 586 | } 587 | 588 | // Make functions say that they are functions 589 | if (typeof value === 'function') { 590 | var n = value.name ? ': ' + value.name : ''; 591 | base = (isRegExp(value)) ? ' ' + value : ' [Function' + n + ']'; 592 | } else { 593 | base = ''; 594 | } 595 | 596 | // Make dates with properties first say the date 597 | if (isDate(value)) { 598 | base = ' ' + value.toUTCString(); 599 | } 600 | 601 | if ($keys.length === 0) { 602 | return braces[0] + base + braces[1]; 603 | } 604 | 605 | if (recurseTimes < 0) { 606 | if (isRegExp(value)) { 607 | return stylize('' + value, 'regexp'); 608 | } else { 609 | return stylize('[Object]', 'special'); 610 | } 611 | } 612 | 613 | seen.push(value); 614 | 615 | var output = map($keys, function (key) { 616 | var name, str; 617 | if (value.__lookupGetter__) { 618 | if (value.__lookupGetter__(key)) { 619 | if (value.__lookupSetter__(key)) { 620 | str = stylize('[Getter/Setter]', 'special'); 621 | } else { 622 | str = stylize('[Getter]', 'special'); 623 | } 624 | } else { 625 | if (value.__lookupSetter__(key)) { 626 | str = stylize('[Setter]', 'special'); 627 | } 628 | } 629 | } 630 | if (indexOf(visible_keys, key) < 0) { 631 | name = '[' + key + ']'; 632 | } 633 | if (!str) { 634 | if (indexOf(seen, value[key]) < 0) { 635 | if (recurseTimes === null) { 636 | str = format(value[key]); 637 | } else { 638 | str = format(value[key], recurseTimes - 1); 639 | } 640 | if (str.indexOf('\n') > -1) { 641 | if (isArray(value)) { 642 | str = map(str.split('\n'), function (line) { 643 | return ' ' + line; 644 | }).join('\n').substr(2); 645 | } else { 646 | str = '\n' + map(str.split('\n'), function (line) { 647 | return ' ' + line; 648 | }).join('\n'); 649 | } 650 | } 651 | } else { 652 | str = stylize('[Circular]', 'special'); 653 | } 654 | } 655 | if (typeof name === 'undefined') { 656 | if (type === 'Array' && key.match(/^\d+$/)) { 657 | return str; 658 | } 659 | name = json.stringify('' + key); 660 | if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { 661 | name = name.substr(1, name.length - 2); 662 | name = stylize(name, 'name'); 663 | } else { 664 | name = name.replace(/'/g, "\\'") 665 | .replace(/\\"/g, '"') 666 | .replace(/(^"|"$)/g, "'"); 667 | name = stylize(name, 'string'); 668 | } 669 | } 670 | 671 | return name + ': ' + str; 672 | }); 673 | 674 | seen.pop(); 675 | 676 | var numLinesEst = 0; 677 | var length = reduce(output, function (prev, cur) { 678 | numLinesEst++; 679 | if (indexOf(cur, '\n') >= 0) numLinesEst++; 680 | return prev + cur.length + 1; 681 | }, 0); 682 | 683 | if (length > 50) { 684 | output = braces[0] + 685 | (base === '' ? '' : base + '\n ') + 686 | ' ' + 687 | output.join(',\n ') + 688 | ' ' + 689 | braces[1]; 690 | 691 | } else { 692 | output = braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; 693 | } 694 | 695 | return output; 696 | } 697 | return format(obj, (typeof depth === 'undefined' ? 2 : depth)); 698 | }; 699 | 700 | function isArray (ar) { 701 | return Object.prototype.toString.call(ar) == '[object Array]'; 702 | }; 703 | 704 | function isRegExp(re) { 705 | var s = '' + re; 706 | return re instanceof RegExp || // easy case 707 | // duck-type for context-switching evalcx case 708 | typeof(re) === 'function' && 709 | re.constructor.name === 'RegExp' && 710 | re.compile && 711 | re.test && 712 | re.exec && 713 | s.match(/^\/.*\/[gim]{0,3}$/); 714 | }; 715 | 716 | function isDate(d) { 717 | if (d instanceof Date) return true; 718 | return false; 719 | }; 720 | 721 | function keys (obj) { 722 | if (Object.keys) { 723 | return Object.keys(obj); 724 | } 725 | 726 | var keys = []; 727 | 728 | for (var i in obj) { 729 | if (Object.prototype.hasOwnProperty.call(obj, i)) { 730 | keys.push(i); 731 | } 732 | } 733 | 734 | return keys; 735 | } 736 | 737 | function map (arr, mapper, that) { 738 | if (Array.prototype.map) { 739 | return Array.prototype.map.call(arr, mapper, that); 740 | } 741 | 742 | var other= new Array(arr.length); 743 | 744 | for (var i= 0, n = arr.length; i= 2) { 770 | var rv = arguments[1]; 771 | } else { 772 | do { 773 | if (i in this) { 774 | rv = this[i++]; 775 | break; 776 | } 777 | 778 | // if array contains no values, no initial value to return 779 | if (++i >= len) 780 | throw new TypeError(); 781 | } while (true); 782 | } 783 | 784 | for (; i < len; i++) { 785 | if (i in this) 786 | rv = fun.call(null, rv, this[i], i, this); 787 | } 788 | 789 | return rv; 790 | }; 791 | 792 | /** 793 | * Asserts deep equality 794 | * 795 | * @see taken from node.js `assert` module (copyright Joyent, MIT license) 796 | * @api private 797 | */ 798 | 799 | expect.eql = function eql (actual, expected) { 800 | // 7.1. All identical values are equivalent, as determined by ===. 801 | if (actual === expected) { 802 | return true; 803 | } else if ('undefined' != typeof Buffer 804 | && Buffer.isBuffer(actual) && Buffer.isBuffer(expected)) { 805 | if (actual.length != expected.length) return false; 806 | 807 | for (var i = 0; i < actual.length; i++) { 808 | if (actual[i] !== expected[i]) return false; 809 | } 810 | 811 | return true; 812 | 813 | // 7.2. If the expected value is a Date object, the actual value is 814 | // equivalent if it is also a Date object that refers to the same time. 815 | } else if (actual instanceof Date && expected instanceof Date) { 816 | return actual.getTime() === expected.getTime(); 817 | 818 | // 7.3. Other pairs that do not both pass typeof value == "object", 819 | // equivalence is determined by ==. 820 | } else if (typeof actual != 'object' && typeof expected != 'object') { 821 | return actual == expected; 822 | 823 | // 7.4. For all other Object pairs, including Array objects, equivalence is 824 | // determined by having the same number of owned properties (as verified 825 | // with Object.prototype.hasOwnProperty.call), the same set of keys 826 | // (although not necessarily the same order), equivalent values for every 827 | // corresponding key, and an identical "prototype" property. Note: this 828 | // accounts for both named and indexed properties on Arrays. 829 | } else { 830 | return objEquiv(actual, expected); 831 | } 832 | } 833 | 834 | function isUndefinedOrNull (value) { 835 | return value === null || value === undefined; 836 | } 837 | 838 | function isArguments (object) { 839 | return Object.prototype.toString.call(object) == '[object Arguments]'; 840 | } 841 | 842 | function objEquiv (a, b) { 843 | if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) 844 | return false; 845 | // an identical "prototype" property. 846 | if (a.prototype !== b.prototype) return false; 847 | //~~~I've managed to break Object.keys through screwy arguments passing. 848 | // Converting to array solves the problem. 849 | if (isArguments(a)) { 850 | if (!isArguments(b)) { 851 | return false; 852 | } 853 | a = pSlice.call(a); 854 | b = pSlice.call(b); 855 | return expect.eql(a, b); 856 | } 857 | try{ 858 | var ka = keys(a), 859 | kb = keys(b), 860 | key, i; 861 | } catch (e) {//happens when one is a string literal and the other isn't 862 | return false; 863 | } 864 | // having the same number of owned properties (keys incorporates hasOwnProperty) 865 | if (ka.length != kb.length) 866 | return false; 867 | //the same set of keys (although not necessarily the same order), 868 | ka.sort(); 869 | kb.sort(); 870 | //~~~cheap key test 871 | for (i = ka.length - 1; i >= 0; i--) { 872 | if (ka[i] != kb[i]) 873 | return false; 874 | } 875 | //equivalent values for every corresponding key, and 876 | //~~~possibly expensive deep test 877 | for (i = ka.length - 1; i >= 0; i--) { 878 | key = ka[i]; 879 | if (!expect.eql(a[key], b[key])) 880 | return false; 881 | } 882 | return true; 883 | } 884 | 885 | var json = (function () { 886 | "use strict"; 887 | 888 | if ('object' == typeof JSON && JSON.parse && JSON.stringify) { 889 | return { 890 | parse: nativeJSON.parse 891 | , stringify: nativeJSON.stringify 892 | } 893 | } 894 | 895 | var JSON = {}; 896 | 897 | function f(n) { 898 | // Format integers to have at least two digits. 899 | return n < 10 ? '0' + n : n; 900 | } 901 | 902 | function date(d, key) { 903 | return isFinite(d.valueOf()) ? 904 | d.getUTCFullYear() + '-' + 905 | f(d.getUTCMonth() + 1) + '-' + 906 | f(d.getUTCDate()) + 'T' + 907 | f(d.getUTCHours()) + ':' + 908 | f(d.getUTCMinutes()) + ':' + 909 | f(d.getUTCSeconds()) + 'Z' : null; 910 | }; 911 | 912 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 913 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 914 | gap, 915 | indent, 916 | meta = { // table of character substitutions 917 | '\b': '\\b', 918 | '\t': '\\t', 919 | '\n': '\\n', 920 | '\f': '\\f', 921 | '\r': '\\r', 922 | '"' : '\\"', 923 | '\\': '\\\\' 924 | }, 925 | rep; 926 | 927 | 928 | function quote(string) { 929 | 930 | // If the string contains no control characters, no quote characters, and no 931 | // backslash characters, then we can safely slap some quotes around it. 932 | // Otherwise we must also replace the offending characters with safe escape 933 | // sequences. 934 | 935 | escapable.lastIndex = 0; 936 | return escapable.test(string) ? '"' + string.replace(escapable, function (a) { 937 | var c = meta[a]; 938 | return typeof c === 'string' ? c : 939 | '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 940 | }) + '"' : '"' + string + '"'; 941 | } 942 | 943 | 944 | function str(key, holder) { 945 | 946 | // Produce a string from holder[key]. 947 | 948 | var i, // The loop counter. 949 | k, // The member key. 950 | v, // The member value. 951 | length, 952 | mind = gap, 953 | partial, 954 | value = holder[key]; 955 | 956 | // If the value has a toJSON method, call it to obtain a replacement value. 957 | 958 | if (value instanceof Date) { 959 | value = date(key); 960 | } 961 | 962 | // If we were called with a replacer function, then call the replacer to 963 | // obtain a replacement value. 964 | 965 | if (typeof rep === 'function') { 966 | value = rep.call(holder, key, value); 967 | } 968 | 969 | // What happens next depends on the value's type. 970 | 971 | switch (typeof value) { 972 | case 'string': 973 | return quote(value); 974 | 975 | case 'number': 976 | 977 | // JSON numbers must be finite. Encode non-finite numbers as null. 978 | 979 | return isFinite(value) ? String(value) : 'null'; 980 | 981 | case 'boolean': 982 | case 'null': 983 | 984 | // If the value is a boolean or null, convert it to a string. Note: 985 | // typeof null does not produce 'null'. The case is included here in 986 | // the remote chance that this gets fixed someday. 987 | 988 | return String(value); 989 | 990 | // If the type is 'object', we might be dealing with an object or an array or 991 | // null. 992 | 993 | case 'object': 994 | 995 | // Due to a specification blunder in ECMAScript, typeof null is 'object', 996 | // so watch out for that case. 997 | 998 | if (!value) { 999 | return 'null'; 1000 | } 1001 | 1002 | // Make an array to hold the partial results of stringifying this object value. 1003 | 1004 | gap += indent; 1005 | partial = []; 1006 | 1007 | // Is the value an array? 1008 | 1009 | if (Object.prototype.toString.apply(value) === '[object Array]') { 1010 | 1011 | // The value is an array. Stringify every element. Use null as a placeholder 1012 | // for non-JSON values. 1013 | 1014 | length = value.length; 1015 | for (i = 0; i < length; i += 1) { 1016 | partial[i] = str(i, value) || 'null'; 1017 | } 1018 | 1019 | // Join all of the elements together, separated with commas, and wrap them in 1020 | // brackets. 1021 | 1022 | v = partial.length === 0 ? '[]' : gap ? 1023 | '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : 1024 | '[' + partial.join(',') + ']'; 1025 | gap = mind; 1026 | return v; 1027 | } 1028 | 1029 | // If the replacer is an array, use it to select the members to be stringified. 1030 | 1031 | if (rep && typeof rep === 'object') { 1032 | length = rep.length; 1033 | for (i = 0; i < length; i += 1) { 1034 | if (typeof rep[i] === 'string') { 1035 | k = rep[i]; 1036 | v = str(k, value); 1037 | if (v) { 1038 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 1039 | } 1040 | } 1041 | } 1042 | } else { 1043 | 1044 | // Otherwise, iterate through all of the keys in the object. 1045 | 1046 | for (k in value) { 1047 | if (Object.prototype.hasOwnProperty.call(value, k)) { 1048 | v = str(k, value); 1049 | if (v) { 1050 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 1051 | } 1052 | } 1053 | } 1054 | } 1055 | 1056 | // Join all of the member texts together, separated with commas, 1057 | // and wrap them in braces. 1058 | 1059 | v = partial.length === 0 ? '{}' : gap ? 1060 | '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : 1061 | '{' + partial.join(',') + '}'; 1062 | gap = mind; 1063 | return v; 1064 | } 1065 | } 1066 | 1067 | // If the JSON object does not yet have a stringify method, give it one. 1068 | 1069 | JSON.stringify = function (value, replacer, space) { 1070 | 1071 | // The stringify method takes a value and an optional replacer, and an optional 1072 | // space parameter, and returns a JSON text. The replacer can be a function 1073 | // that can replace values, or an array of strings that will select the keys. 1074 | // A default replacer method can be provided. Use of the space parameter can 1075 | // produce text that is more easily readable. 1076 | 1077 | var i; 1078 | gap = ''; 1079 | indent = ''; 1080 | 1081 | // If the space parameter is a number, make an indent string containing that 1082 | // many spaces. 1083 | 1084 | if (typeof space === 'number') { 1085 | for (i = 0; i < space; i += 1) { 1086 | indent += ' '; 1087 | } 1088 | 1089 | // If the space parameter is a string, it will be used as the indent string. 1090 | 1091 | } else if (typeof space === 'string') { 1092 | indent = space; 1093 | } 1094 | 1095 | // If there is a replacer, it must be a function or an array. 1096 | // Otherwise, throw an error. 1097 | 1098 | rep = replacer; 1099 | if (replacer && typeof replacer !== 'function' && 1100 | (typeof replacer !== 'object' || 1101 | typeof replacer.length !== 'number')) { 1102 | throw new Error('JSON.stringify'); 1103 | } 1104 | 1105 | // Make a fake root object containing our value under the key of ''. 1106 | // Return the result of stringifying the value. 1107 | 1108 | return str('', {'': value}); 1109 | }; 1110 | 1111 | // If the JSON object does not yet have a parse method, give it one. 1112 | 1113 | JSON.parse = function (text, reviver) { 1114 | // The parse method takes a text and an optional reviver function, and returns 1115 | // a JavaScript value if the text is a valid JSON text. 1116 | 1117 | var j; 1118 | 1119 | function walk(holder, key) { 1120 | 1121 | // The walk method is used to recursively walk the resulting structure so 1122 | // that modifications can be made. 1123 | 1124 | var k, v, value = holder[key]; 1125 | if (value && typeof value === 'object') { 1126 | for (k in value) { 1127 | if (Object.prototype.hasOwnProperty.call(value, k)) { 1128 | v = walk(value, k); 1129 | if (v !== undefined) { 1130 | value[k] = v; 1131 | } else { 1132 | delete value[k]; 1133 | } 1134 | } 1135 | } 1136 | } 1137 | return reviver.call(holder, key, value); 1138 | } 1139 | 1140 | 1141 | // Parsing happens in four stages. In the first stage, we replace certain 1142 | // Unicode characters with escape sequences. JavaScript handles many characters 1143 | // incorrectly, either silently deleting them, or treating them as line endings. 1144 | 1145 | text = String(text); 1146 | cx.lastIndex = 0; 1147 | if (cx.test(text)) { 1148 | text = text.replace(cx, function (a) { 1149 | return '\\u' + 1150 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 1151 | }); 1152 | } 1153 | 1154 | // In the second stage, we run the text against regular expressions that look 1155 | // for non-JSON patterns. We are especially concerned with '()' and 'new' 1156 | // because they can cause invocation, and '=' because it can cause mutation. 1157 | // But just to be safe, we want to reject all unexpected forms. 1158 | 1159 | // We split the second stage into 4 regexp operations in order to work around 1160 | // crippling inefficiencies in IE's and Safari's regexp engines. First we 1161 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we 1162 | // replace all simple value tokens with ']' characters. Third, we delete all 1163 | // open brackets that follow a colon or comma or that begin the text. Finally, 1164 | // we look to see that the remaining characters are only whitespace or ']' or 1165 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. 1166 | 1167 | if (/^[\],:{}\s]*$/ 1168 | .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') 1169 | .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') 1170 | .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { 1171 | 1172 | // In the third stage we use the eval function to compile the text into a 1173 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity 1174 | // in JavaScript: it can begin a block or an object literal. We wrap the text 1175 | // in parens to eliminate the ambiguity. 1176 | 1177 | j = eval('(' + text + ')'); 1178 | 1179 | // In the optional fourth stage, we recursively walk the new structure, passing 1180 | // each name/value pair to a reviver function for possible transformation. 1181 | 1182 | return typeof reviver === 'function' ? 1183 | walk({'': j}, '') : j; 1184 | } 1185 | 1186 | // If the text is not JSON parseable, then a SyntaxError is thrown. 1187 | 1188 | throw new SyntaxError('JSON.parse'); 1189 | }; 1190 | 1191 | return JSON; 1192 | })(); 1193 | 1194 | if ('undefined' != typeof window) { 1195 | window.expect = module.exports; 1196 | } 1197 | 1198 | })( 1199 | this 1200 | , 'undefined' != typeof module ? module : {} 1201 | , 'undefined' != typeof exports ? exports : {} 1202 | ); 1203 | -------------------------------------------------------------------------------- /test/lib/mocha/mocha.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body { 4 | margin:0; 5 | } 6 | 7 | #mocha { 8 | font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | margin: 60px 50px; 10 | } 11 | 12 | #mocha ul, #mocha li { 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | #mocha ul { 18 | list-style: none; 19 | } 20 | 21 | #mocha h1, #mocha h2 { 22 | margin: 0; 23 | } 24 | 25 | #mocha h1 { 26 | margin-top: 15px; 27 | font-size: 1em; 28 | font-weight: 200; 29 | } 30 | 31 | #mocha h1 a { 32 | text-decoration: none; 33 | color: inherit; 34 | } 35 | 36 | #mocha h1 a:hover { 37 | text-decoration: underline; 38 | } 39 | 40 | #mocha .suite .suite h1 { 41 | margin-top: 0; 42 | font-size: .8em; 43 | } 44 | 45 | #mocha .hidden { 46 | display: none; 47 | } 48 | 49 | #mocha h2 { 50 | font-size: 12px; 51 | font-weight: normal; 52 | cursor: pointer; 53 | } 54 | 55 | #mocha .suite { 56 | margin-left: 15px; 57 | } 58 | 59 | #mocha .test { 60 | margin-left: 15px; 61 | overflow: hidden; 62 | } 63 | 64 | #mocha .test.pending:hover h2::after { 65 | content: '(pending)'; 66 | font-family: arial, sans-serif; 67 | } 68 | 69 | #mocha .test.pass.medium .duration { 70 | background: #C09853; 71 | } 72 | 73 | #mocha .test.pass.slow .duration { 74 | background: #B94A48; 75 | } 76 | 77 | #mocha .test.pass::before { 78 | content: '✓'; 79 | font-size: 12px; 80 | display: block; 81 | float: left; 82 | margin-right: 5px; 83 | color: #00d6b2; 84 | } 85 | 86 | #mocha .test.pass .duration { 87 | font-size: 9px; 88 | margin-left: 5px; 89 | padding: 2px 5px; 90 | color: white; 91 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 92 | -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 93 | box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 94 | -webkit-border-radius: 5px; 95 | -moz-border-radius: 5px; 96 | -ms-border-radius: 5px; 97 | -o-border-radius: 5px; 98 | border-radius: 5px; 99 | } 100 | 101 | #mocha .test.pass.fast .duration { 102 | display: none; 103 | } 104 | 105 | #mocha .test.pending { 106 | color: #0b97c4; 107 | } 108 | 109 | #mocha .test.pending::before { 110 | content: '◦'; 111 | color: #0b97c4; 112 | } 113 | 114 | #mocha .test.fail { 115 | color: #c00; 116 | } 117 | 118 | #mocha .test.fail pre { 119 | color: black; 120 | } 121 | 122 | #mocha .test.fail::before { 123 | content: '✖'; 124 | font-size: 12px; 125 | display: block; 126 | float: left; 127 | margin-right: 5px; 128 | color: #c00; 129 | } 130 | 131 | #mocha .test pre.error { 132 | color: #c00; 133 | max-height: 300px; 134 | overflow: auto; 135 | } 136 | 137 | #mocha .test pre { 138 | display: block; 139 | float: left; 140 | clear: left; 141 | font: 12px/1.5 monaco, monospace; 142 | margin: 5px; 143 | padding: 15px; 144 | border: 1px solid #eee; 145 | border-bottom-color: #ddd; 146 | -webkit-border-radius: 3px; 147 | -webkit-box-shadow: 0 1px 3px #eee; 148 | -moz-border-radius: 3px; 149 | -moz-box-shadow: 0 1px 3px #eee; 150 | border-radius: 3px; 151 | } 152 | 153 | #mocha .test h2 { 154 | position: relative; 155 | } 156 | 157 | #mocha .test a.replay { 158 | position: absolute; 159 | top: 3px; 160 | right: 0; 161 | text-decoration: none; 162 | vertical-align: middle; 163 | display: block; 164 | width: 15px; 165 | height: 15px; 166 | line-height: 15px; 167 | text-align: center; 168 | background: #eee; 169 | font-size: 15px; 170 | -moz-border-radius: 15px; 171 | border-radius: 15px; 172 | -webkit-transition: opacity 200ms; 173 | -moz-transition: opacity 200ms; 174 | transition: opacity 200ms; 175 | opacity: 0.3; 176 | color: #888; 177 | } 178 | 179 | #mocha .test:hover a.replay { 180 | opacity: 1; 181 | } 182 | 183 | #mocha-report.pass .test.fail { 184 | display: none; 185 | } 186 | 187 | #mocha-report.fail .test.pass { 188 | display: none; 189 | } 190 | 191 | #mocha-report.pending .test.pass, 192 | #mocha-report.pending .test.fail { 193 | display: none; 194 | } 195 | #mocha-report.pending .test.pass.pending { 196 | display: block; 197 | } 198 | 199 | #mocha-error { 200 | color: #c00; 201 | font-size: 1.5em; 202 | font-weight: 100; 203 | letter-spacing: 1px; 204 | } 205 | 206 | #mocha-stats { 207 | position: fixed; 208 | top: 15px; 209 | right: 10px; 210 | font-size: 12px; 211 | margin: 0; 212 | color: #888; 213 | z-index: 1; 214 | } 215 | 216 | #mocha-stats .progress { 217 | float: right; 218 | padding-top: 0; 219 | } 220 | 221 | #mocha-stats em { 222 | color: black; 223 | } 224 | 225 | #mocha-stats a { 226 | text-decoration: none; 227 | color: inherit; 228 | } 229 | 230 | #mocha-stats a:hover { 231 | border-bottom: 1px solid #eee; 232 | } 233 | 234 | #mocha-stats li { 235 | display: inline-block; 236 | margin: 0 5px; 237 | list-style: none; 238 | padding-top: 11px; 239 | } 240 | 241 | #mocha-stats canvas { 242 | width: 40px; 243 | height: 40px; 244 | } 245 | 246 | #mocha code .comment { color: #ddd } 247 | #mocha code .init { color: #2F6FAD } 248 | #mocha code .string { color: #5890AD } 249 | #mocha code .keyword { color: #8A6343 } 250 | #mocha code .number { color: #2F6FAD } 251 | 252 | @media screen and (max-device-width: 480px) { 253 | #mocha { 254 | margin: 60px 0px; 255 | } 256 | 257 | #mocha #stats { 258 | position: absolute; 259 | } 260 | } -------------------------------------------------------------------------------- /test/pie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tantaman/LargeLocalStorage/e4fc5d03be1dfd497f29b0dadde25e3c5388c0ea/test/pie.jpg -------------------------------------------------------------------------------- /test/runner/mocha.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var runner = mocha.run(); 3 | if(window.PHANTOMJS) { 4 | runner.on('test', function(test) { 5 | sendMessage('testStart', test.title); 6 | }); 7 | 8 | runner.on('test end', function(test) { 9 | sendMessage('testDone', test.title, test.state); 10 | }); 11 | 12 | runner.on('suite', function(suite) { 13 | sendMessage('suiteStart', suite.title); 14 | }); 15 | 16 | runner.on('suite end', function(suite) { 17 | if (suite.root) return; 18 | sendMessage('suiteDone', suite.title); 19 | }); 20 | 21 | runner.on('fail', function(test, err) { 22 | sendMessage('testFail', test.title, err); 23 | }); 24 | 25 | runner.on('end', function() { 26 | var output = { 27 | failed : this.failures, 28 | passed : this.total - this.failures, 29 | total : this.total 30 | }; 31 | 32 | sendMessage('done', output.failed,output.passed, output.total); 33 | }); 34 | 35 | function sendMessage() { 36 | var args = [].slice.call(arguments); 37 | alert(JSON.stringify(args)); 38 | } 39 | } 40 | })(); -------------------------------------------------------------------------------- /test/spec/LargeLocalStorageTest.js: -------------------------------------------------------------------------------- 1 | (function(lls) { 2 | Q.longStackSupport = true; 3 | Q.onerror = function(err) { 4 | console.log(err); 5 | throw err; 6 | }; 7 | 8 | var storage = new lls({ 9 | size: 10 * 1024 * 1024, 10 | name: 'lls-test' 11 | // forceProvider: 'WebSQL' // force a desired provider. 12 | }); 13 | 14 | // for debug 15 | // window.storage = storage; 16 | 17 | function getAttachment(a, cb) { 18 | var xhr = new XMLHttpRequest(), 19 | blob; 20 | 21 | xhr.open("GET", a, true); 22 | var is_safari = navigator.userAgent.indexOf("Safari") > -1; 23 | if (is_safari) { 24 | xhr.responseType = "arraybuffer"; 25 | } else { 26 | xhr.responseType = "blob"; 27 | } 28 | 29 | xhr.addEventListener("load", function () { 30 | if (xhr.status === 200) { 31 | if (is_safari) { 32 | blob = new Blob([xhr.response], {type: 'image/jpeg'}); 33 | } else { 34 | blob = xhr.response; 35 | } 36 | cb(blob); 37 | } 38 | }, false); 39 | xhr.send(); 40 | } 41 | 42 | describe('LargeLocalStorage', function() { 43 | it('Allows string contents to be set and read', function(done) { 44 | storage.setContents("testFile", "contents").then(function() { 45 | return storage.getContents("testFile"); 46 | }).then(function(contents) { 47 | expect(contents).to.equal("contents"); 48 | }).done(done); 49 | }); 50 | 51 | it('Allows js objects to be set and read', function(done) { 52 | var jsondoc = { 53 | a: 1, 54 | b: 2, 55 | c: {a: true} 56 | }; 57 | storage.setContents("testfile2", jsondoc, {json:true}).then(function() { 58 | return storage.getContents("testfile2", {json:true}); 59 | }).then(function(contents) { 60 | expect(jsondoc).to.eql(contents); 61 | }).done(done); 62 | }); 63 | 64 | it('Allows items to be deleted', function(done) { 65 | storage.setContents("testfile3", "contents").then(function() { 66 | return storage.rm("testfile3"); 67 | }).then(function() { 68 | return storage.getContents("testfile3"); 69 | }).then(function(contents) { 70 | expect(contents).to.equal(undefined); 71 | }).done(done); 72 | }); 73 | 74 | 75 | it('Allows attachments to be written, read', function(done) { 76 | getAttachment("elephant.jpg", function(blob) { 77 | storage.setContents("testfile4", "file...").then(function() { 78 | return storage.setAttachment("testfile4", "ele", blob); 79 | }).then(function() { 80 | return storage.getAttachment("testfile4", "ele"); 81 | }).then(function(attach) { 82 | expect(attach instanceof Blob).to.equal(true); 83 | }).done(done); 84 | }); 85 | }); 86 | 87 | 88 | // Apparently these tests are being run sequentially... 89 | // so taking advantage of that. 90 | it('Allows us to get attachments as urls', function(done) { 91 | storage.getAttachmentURL("testfile4", "ele").then(function(url) { 92 | // urls are pretty opaque since they could be from 93 | // filesystem api, indexeddb, or websql 94 | // meaning there isn't much we can do to verify them 95 | // besides ensure that they are strings. 96 | expect(typeof url === 'string').to.equal(true); 97 | $(document.body).append(''); 98 | }).done(done); 99 | }); 100 | 101 | it('Allows attachments to be deleted', function(done) { 102 | storage.rmAttachment("testfile4", "ele").then(function() { 103 | // .done will throw any errors and fail the test for us if 104 | // something went wrong. 105 | }).done(done); 106 | }); 107 | 108 | it('Removes all attachments when removing a file', function(done) { 109 | getAttachment("pie.jpg", function(blob) { 110 | storage.setContents("testfile5", "fileo").then(function() { 111 | return storage.setAttachment("testfile5", "pie", blob); 112 | }).then(function() { 113 | return storage.setAttachment("testfile5", "pie2", blob); 114 | }).then(function() { 115 | return storage.rm("testfile5"); 116 | }).then(function() { 117 | return storage.getAttachment("testfile5", "pie"); 118 | }).then(function(val) { 119 | expect(val).to.equal(undefined); 120 | 121 | storage.getAttachment("testfile5", "pie2") 122 | .then(function(a) { 123 | expect(a).to.equal(undefined); 124 | }).done(done); 125 | }).done(); 126 | }); 127 | }); 128 | 129 | it('Allows one to revoke attachment urls', function() { 130 | storage.revokeAttachmentURL(''); 131 | }); 132 | 133 | it('Allows all attachments to be gotten in one shot', function(done) { 134 | var c = countdown(2, continuation); 135 | getAttachment("pie.jpg", function(pie) { 136 | c(pie); 137 | }); 138 | 139 | getAttachment("elephant.jpg", function(ele) { 140 | c(ele); 141 | }); 142 | 143 | function continuation(blob1, blob2) { 144 | Q.all([ 145 | storage.setAttachment("testfile6", "blob1", blob1), 146 | storage.setAttachment("testfile6", "blob2", blob2) 147 | ]).then(function() { 148 | return storage.getAllAttachments("testfile6"); 149 | }).then(function(attachments) { 150 | expect(attachments.length).to.equal(2); 151 | expect(attachments[0].docKey).to.equal('testfile6'); 152 | expect(attachments[1].docKey).to.equal('testfile6'); 153 | expect(attachments[0].attachKey.indexOf('blob')).to.equal(0); 154 | expect(attachments[1].attachKey.indexOf('blob')).to.equal(0); 155 | }).done(done); 156 | } 157 | }); 158 | 159 | it('Allows all attachment urls to be gotten in one shot', function(done) { 160 | storage.getAllAttachmentURLs('testfile6').then(function(urls) { 161 | expect(urls.length).to.equal(2); 162 | urls.forEach(function(url) { 163 | $(document.body).append(''); 164 | }); 165 | }).done(done); 166 | }); 167 | 168 | 169 | it('Allows us to ls the attachments on a document', function(done) { 170 | storage.ls('testfile6').then(function(listing) { 171 | expect(listing.length).to.equal(2); 172 | expect(listing[0] == 'blob1' || listing[0] == 'blob2').to.equal(true); 173 | expect(listing[1] == 'blob1' || listing[1] == 'blob2').to.equal(true); 174 | }).done(done); 175 | }); 176 | 177 | // TODO: create a new db to test on so this isn't 178 | // broken when updating other tests 179 | it('Allows us to ls for all docs', function(done) { 180 | storage.ls().then(function(listing) { 181 | expect(listing.indexOf('testfile4')).to.not.equal(-1); 182 | expect(listing.indexOf('testFile')).to.not.equal(-1); 183 | expect(listing.indexOf('testfile2')).to.not.equal(-1); 184 | expect(listing.length).to.equal(3); 185 | }).done(done); 186 | }); 187 | 188 | it('Allows us to clear out the entire storage', function(done) { 189 | storage.clear().then(function() { 190 | var scb = countdown(2, function(value) { 191 | if (value != undefined) 192 | throw new Error('Files were not removed.'); 193 | done(); 194 | }); 195 | 196 | var ecb = function(err) { 197 | throw new Error('getting missing documents should not return an error'); 198 | }; 199 | 200 | storage.getContents('testfile4').then(scb, ecb); 201 | storage.getContents('testfile2').then(scb, ecb); 202 | }).done(); 203 | }); 204 | 205 | describe('Data Migration', function() { 206 | it('Allows us to copy data when the implementation changes', function(done) { 207 | var available = lls.availableProviders; 208 | if (available.length >= 2) 209 | testDataMigration(done, available); 210 | else 211 | done(); 212 | }); 213 | }); 214 | }); 215 | 216 | function testDataMigration(done, availableProviders) { 217 | var fromStorage = new lls({ 218 | name: 'lls-migration-test', 219 | forceProvider: availableProviders[0] 220 | }); 221 | 222 | var toStorage; 223 | 224 | var test1doc = 'Allo Allo'; 225 | var test2doc = 'Ello Ello'; 226 | var test1a1txt = '123asd'; 227 | var test1a2txt = 'sdfsdfsdf'; 228 | var test1a1 = new Blob([test1a1txt], {type: 'text/plain'}); 229 | var test1a2 = new Blob([test1a2txt], {type: 'text/plain'}); 230 | 231 | fromStorage.initialized.then(function() { 232 | return fromStorage.setContents('test1', test1doc); 233 | }).then(function() { 234 | return fromStorage.setContents('test2', test2doc); 235 | }).then(function() { 236 | return fromStorage.setAttachment('test1', 'a1', test1a1); 237 | }).then(function() { 238 | return fromStorage.setAttachment('test1', 'a2', test1a2); 239 | }).then(function() { 240 | var deferred = Q.defer(); 241 | toStorage = new lls({ 242 | name: 'lls-migration-test', 243 | forceProvider: availableProviders[1], 244 | migrate: lls.copyOldData, 245 | migrationComplete: function(err) { 246 | deferred.resolve(); 247 | } 248 | }); 249 | console.log('Migrating to: ' + availableProviders[1] 250 | + ' From: ' + availableProviders[0]); 251 | 252 | return deferred.promise; 253 | }).then(function() { 254 | return toStorage.getContents('test1'); 255 | }).then(function(content) { 256 | expect(content).to.eql(test1doc); 257 | return toStorage.getContents('test2'); 258 | }).then(function(content) { 259 | expect(content).to.eql(test2doc); 260 | return toStorage.getAttachment('test1', 'a1'); 261 | }).then(function(attachment) { 262 | var deferred = Q.defer(); 263 | var r = new FileReader(); 264 | r.addEventListener("loadend", function() { 265 | expect(r.result).to.eql(test1a1txt); 266 | toStorage.getAttachment('test1', 'a2').then(deferred.resolve, deferred.reject) 267 | }); 268 | r.readAsText(attachment); 269 | return deferred.promise; 270 | }).then(function(attachment) { 271 | var r = new FileReader(); 272 | r.addEventListener("loadend", function() { 273 | console.log(r.result); 274 | expect(r.result).to.eql(test1a2txt); 275 | Q.all([fromStorage.clear(), toStorage.clear()]).done(function() {done();}); 276 | }); 277 | console.log('Attach: ' + attachment); 278 | r.readAsText(attachment); 279 | }).done(); 280 | } 281 | 282 | function getAvailableImplementations() { 283 | var deferred = Q.defer(); 284 | var available = []; 285 | 286 | var potentialProviders = Object.keys(lls._providers); 287 | 288 | var latch = countdown(potentialProviders.length, function() { 289 | deferred.resolve(available); 290 | }); 291 | 292 | potentialProviders.forEach(function(potentialProvider) { 293 | lls._providers[potentialProvider].init({name: 'lls-test-avail'}).then(function() { 294 | available.push(potentialProvider); 295 | latch(); 296 | }, function() { 297 | latch(); 298 | }) 299 | }); 300 | 301 | return deferred.promise; 302 | } 303 | 304 | 305 | storage.initialized.then(function() { 306 | storage.clear().then(function() { 307 | window.runMocha(); 308 | }).catch(function(err) { 309 | console.log(err); 310 | }); 311 | }, function(err) { 312 | console.log(err); 313 | alert('Could not initialize storage. Did you not authorize it? ' + err); 314 | }); 315 | })(LargeLocalStorage); -------------------------------------------------------------------------------- /test/spec/URLCacheTest.js: -------------------------------------------------------------------------------- 1 | (function(lls) { 2 | function fail(err) { 3 | console.log(err); 4 | expect(true).to.equal(false); 5 | } 6 | 7 | var blob = new Blob(['

worthless

'], {type: 'text/html'}); 8 | 9 | var storage = new lls({name: 'lls-urlcache-test', size: 10 * 1024 * 1024}); 10 | LargeLocalStorage.contrib.URLCache.addTo(storage); 11 | var cacheObj = storage.pipe.getHandler('URLCache').cache; 12 | 13 | // for debug 14 | // window.cacheObj = cacheObj; 15 | // window.storage = storage; 16 | 17 | // TODO: spy on LargeLocalStorage to ensure that 18 | // revokeAttachmentURL is being called. 19 | // And also spy to make sure piped methods are receiving their calls. 20 | 21 | function loadTests() { 22 | describe('URLCache', function() { 23 | it('Caches getAttachmentURL operations', 24 | function(done) { 25 | storage.setAttachment('doc', 'attach', blob) 26 | .then(function() { 27 | console.log('Getting attach url'); 28 | return storage.getAttachmentURL('doc', 'attach'); 29 | }) 30 | .then(function(url) { 31 | console.log('Comparison'); 32 | expect(url).to.equal(cacheObj.main.doc.attach); 33 | expect(cacheObj.reverse[url]).to.eql({ 34 | docKey: 'doc', 35 | attachKey: 'attach' 36 | }); 37 | }).done(done); 38 | }); 39 | 40 | it('Removes the URL from the cache when updating the attachment', 41 | function(done) { 42 | storage.setAttachment('doc', 'attach', blob) 43 | .then(function() { 44 | expect(cacheObj.main.doc.attach).to.equal(undefined); 45 | expect(cacheObj.reverse).to.eql({}); 46 | }).done(done); 47 | }); 48 | 49 | it('Removes the URL from the cache when removing the attachment', 50 | function(done) { 51 | var theUrl; 52 | storage.getAttachmentURL('doc', 'attach').then(function(url) { 53 | expect(url).to.equal(cacheObj.main.doc.attach); 54 | theUrl = url; 55 | return storage.rmAttachment('doc', 'attach'); 56 | }).then(function() { 57 | expect(cacheObj.main.doc.attach).to.equal(undefined); 58 | expect(cacheObj.reverse[theUrl]).to.equal(undefined); 59 | }).done(done); 60 | }); 61 | 62 | it('Removes the URL from the cache when removing the attachment via removing the host document', 63 | function(done) { 64 | storage.setAttachment('doc2', 'attach', blob) 65 | .then(function() { 66 | return storage.rm('doc2'); 67 | }).then(function() { 68 | expect(cacheObj.main.doc2).to.equal(undefined); 69 | expect(cacheObj.reverse).to.eql({}); 70 | }).done(done); 71 | }); 72 | 73 | it('Removes the URL from the cache when revoking the URL', 74 | function(done) { 75 | storage.setAttachment('doc3', 'attach', blob) 76 | .then(function() { 77 | return storage.getAttachmentURL('doc3', 'attach'); 78 | }).then(function(url) { 79 | expect(url).to.equal(cacheObj.main.doc3.attach); 80 | expect(cacheObj.reverse[url]).to.eql({ 81 | docKey: 'doc3', 82 | attachKey: 'attach' 83 | }); 84 | storage.revokeAttachmentURL(url); 85 | expect(cacheObj.main.doc3.attach).to.equal(undefined); 86 | expect(cacheObj.reverse).to.eql({}); 87 | }).done(done); 88 | }); 89 | 90 | it('Removes all URLs when emptying the database', 91 | function(done) { 92 | Q.all([storage.setAttachment('doc4', 'attach', blob), 93 | storage.setAttachment('doc5', 'attach', blob)]) 94 | .then(function() { 95 | return storage.clear(); 96 | }).then(function() { 97 | expect(cacheObj.reverse).to.eql({}); 98 | expect(cacheObj.main).to.eql({}); 99 | }).done(done); 100 | }); 101 | }); 102 | } 103 | 104 | loadTests(); 105 | storage.initialized.then(function() { 106 | window.runMocha(); 107 | }); 108 | })(LargeLocalStorage); --------------------------------------------------------------------------------