├── src
├── errors.js
├── header.js
├── impls
│ ├── LocalStorageProvider.js
│ ├── utils.js
│ ├── WebSQLProvider.js
│ ├── IndexedDBProvider.js
│ └── FilesystemAPIProvider.js
├── contrib
│ ├── S3Link.js
│ └── URLCache.js
├── footer.js
├── pipeline.js
└── LargeLocalStorage.js
├── .gitignore
├── test
├── pie.jpg
├── elephant.jpg
├── runner
│ └── mocha.js
├── index.html
├── spec
│ ├── URLCacheTest.js
│ └── LargeLocalStorageTest.js
└── lib
│ ├── mocha
│ └── mocha.css
│ └── expect.js
├── dist
├── contrib
│ ├── S3Link.js
│ └── URLCache.js
├── LargeLocalStorage.min.js
└── LargeLocalStorage.js
├── bower.json
├── examples
└── album
│ ├── main.css
│ ├── index.html
│ └── app.js
├── LICENSE-MIT.txt
├── package.json
├── Gruntfile.js
└── README.md
/src/errors.js:
--------------------------------------------------------------------------------
1 | define({
2 |
3 | });
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bower_components/
2 | node_modules/
3 | doc/
4 | temp.out
5 |
--------------------------------------------------------------------------------
/test/pie.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tantaman/LargeLocalStorage/HEAD/test/pie.jpg
--------------------------------------------------------------------------------
/src/header.js:
--------------------------------------------------------------------------------
1 | (function(glob) {
2 | var undefined = {}.a;
3 |
4 | function definition(Q) {
5 |
--------------------------------------------------------------------------------
/test/elephant.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tantaman/LargeLocalStorage/HEAD/test/elephant.jpg
--------------------------------------------------------------------------------
/src/impls/LocalStorageProvider.js:
--------------------------------------------------------------------------------
1 | var LocalStorageProvider = (function(Q) {
2 | return {
3 | init: function() {
4 | return Q({type: 'LocalStorage'});
5 | }
6 | }
7 | })(Q);
--------------------------------------------------------------------------------
/dist/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/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/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);
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | })();
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | })();
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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);
--------------------------------------------------------------------------------
/examples/album/app.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | var storage = new LargeLocalStorage({
5 | size: 20 * 1024 * 1024,
6 | name: 'lls-album-example'
7 | // forceProvider: 'IndexedDB'
8 | // forceProvider: 'WebSQL'
9 | });
10 |
11 | storage.initialized.then(function() {
12 | console.log(storage.getCapacity());
13 | var $storageNotice = $('.storageNotice');
14 | $storageNotice.css('opacity', 0);
15 | setTimeout(function() {
16 | $storageNotice.css('display', 'none');
17 | }, 1100);
18 |
19 | bind();
20 | }, function() {
21 | console.log('denied');
22 | });
23 |
24 | function bind() {
25 | var dndArea = new Album($('.dndArea'));
26 | }
27 |
28 | function Album($el) {
29 | this.$el = $el;
30 | this._drop = this._drop.bind(this);
31 | this._photoAdded = this._photoAdded.bind(this);
32 | this._appendImage = this._appendImage.bind(this);
33 | this.$el.on('dragover', copyDragover);
34 | this.$el.on('drop', this._drop);
35 | this.$thumbs = this.$el.find('.thumbnails');
36 | this.$usage = this.$el.find('.usage');
37 |
38 | var self = this;
39 | $('#clear').click(function() {
40 | storage.clear().then(function() {
41 | self.$thumbs.empty();
42 | }).done();
43 | });
44 |
45 | this._renderExistingPhotos();
46 | }
47 |
48 | Album.prototype = {
49 | _drop: function(e) {
50 | e.stopPropagation();
51 | e.preventDefault();
52 |
53 | e = e.originalEvent;
54 |
55 | foreach(this._photoAdded, keep(isImage, e.dataTransfer.files));
56 | },
57 |
58 | _photoAdded: function(file) {
59 | // TOOD: see if already exists??
60 | storage.setAttachment('album', file.name, file)
61 | .then(function() {
62 | return storage.getAttachmentURL('album', file.name);
63 | }).then(this._appendImage);
64 | },
65 |
66 | _appendImage: function(url) {
67 | if (this.$usage) {
68 | this.$usage.remove();
69 | this.$usage = null;
70 | }
71 | var container = $('');
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 | })();
--------------------------------------------------------------------------------
/dist/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/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/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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);
--------------------------------------------------------------------------------
/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);
--------------------------------------------------------------------------------
/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);
--------------------------------------------------------------------------------
/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= 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 |
--------------------------------------------------------------------------------
/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);
--------------------------------------------------------------------------------