diff --git a/gruntfile.js b/gruntfile.js index d2fa564..e08ab82 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -23,8 +23,7 @@ module.exports = function(grunt) { 'src/error.js', 'src/fs.js', 'src/shared.js', - 'src/providers/**/*.js', - 'src/filesystems-manager.js' + 'src/providers/**/*.js' ] }, diff --git a/src/constants.js b/src/constants.js index 847067c..c53387a 100644 --- a/src/constants.js +++ b/src/constants.js @@ -15,6 +15,10 @@ define(function(require) { IDB_RO: 'readonly', IDB_RW: 'readwrite', + WSQL_VERSION: "1", + WSQL_SIZE: 5 * 1024 * 1024, + WSQL_DESC: "FileSystem Storage", + MODE_FILE: 'FILE', MODE_DIRECTORY: 'DIRECTORY', MODE_SYMBOLIC_LINK: 'SYMLINK', diff --git a/src/providers/indexeddb.js b/src/providers/indexeddb.js index 540ec9c..9de9e9a 100644 --- a/src/providers/indexeddb.js +++ b/src/providers/indexeddb.js @@ -7,7 +7,6 @@ define(function(require) { window.webkitIndexedDB || window.msIndexedDB; - var IDB_RW = require('src/constants').IDB_RW; var IDB_RO = require('src/constants').IDB_RO; diff --git a/src/providers/providers.js b/src/providers/providers.js index ba8b58f..edc6c53 100644 --- a/src/providers/providers.js +++ b/src/providers/providers.js @@ -1,7 +1,31 @@ define(function(require) { + + var IndexedDB = require('src/providers/indexeddb'); + var WebSQL = require('src/providers/websql'); + var Memory = require('src/providers/memory'); + return { - IndexedDB: require('src/providers/indexeddb'), - Memory: require('src/providers/memory'), - Default: require('src/providers/indexeddb') + IndexedDB: IndexedDB, + WebSQL: WebSQL, + Memory: Memory, + Default: IndexedDB, + // The Fallback provider does automatic fallback checks + Fallback: (function() { + if(IndexedDB.isSupported()) { + return IndexedDB; + } + + if(WebSQL.isSupported()) { + return WebSQL; + } + + function NotSupported() { + throw "[IDBFS Error] Your browser doesn't support IndexedDB or WebSQL."; + } + NotSupported.isSupported = function() { + return false; + }; + return NotSupported; + }()) }; }); diff --git a/src/providers/websql.js b/src/providers/websql.js new file mode 100644 index 0000000..721b775 --- /dev/null +++ b/src/providers/websql.js @@ -0,0 +1,129 @@ +define(function(require) { + var FILE_SYSTEM_NAME = require('src/constants').FILE_SYSTEM_NAME; + var FILE_STORE_NAME = require('src/constants').FILE_STORE_NAME; + var WSQL_VERSION = require('src/constants').WSQL_VERSION; + var WSQL_SIZE = require('src/constants').WSQL_SIZE; + var WSQL_DESC = require('src/constants').WSQL_DESC; + + function WebSQLContext(db, isReadOnly) { + var that = this; + this.getTransaction = function(callback) { + if(that.transaction) { + callback(that.transaction); + return; + } + // Either do readTransaction() (read-only) or transaction() (read/write) + db[isReadOnly ? 'readTransaction' : 'transaction'](function(transaction) { + that.transaction = transaction; + callback(transaction); + }); + }; + } + WebSQLContext.prototype.clear = function(callback) { + function onError(transaction, error) { + callback(error); + } + function onSuccess(transaction, result) { + callback(null); + } + this.getTransaction(function(transaction) { + transaction.executeSql("DELETE FROM " + FILE_STORE_NAME, + [], onSuccess, onError); + }); + }; + WebSQLContext.prototype.get = function(key, callback) { + function onSuccess(transaction, result) { + // If the key isn't found, return null + var value = result.rows.length === 0 ? null : result.rows.item(0).data; + callback(null, value); + } + function onError(transaction, error) { + callback(error); + } + this.getTransaction(function(transaction) { + transaction.executeSql("SELECT data FROM " + FILE_STORE_NAME + " WHERE id = ?", + [key], onSuccess, onError); + }); + }; + WebSQLContext.prototype.put = function(key, value, callback) { + function onSuccess(transaction, result) { + callback(null); + } + function onError(transaction, error) { + callback(error); + } + this.getTransaction(function(transaction) { + transaction.executeSql("INSERT OR REPLACE INTO " + FILE_STORE_NAME + " (id, data) VALUES (?, ?)", + [key, value], onSuccess, onError); + }); + }; + WebSQLContext.prototype.delete = function(key, callback) { + function onSuccess(transaction, result) { + callback(null); + } + function onError(transaction, error) { + callback(error); + } + this.getTransaction(function(transaction) { + transaction.executeSql("DELETE FROM " + FILE_STORE_NAME + " WHERE id = ?", + [key], onSuccess, onError); + }); + }; + + + function WebSQL(name) { + this.name = name || FILE_SYSTEM_NAME; + this.db = null; + } + WebSQL.isSupported = function() { + return !!window.openDatabase; + }; + + WebSQL.prototype.open = function(callback) { + var that = this; + + // Bail if we already have a db open + if(that.db) { + callback(null, false); + return; + } + + var db = window.openDatabase(that.name, WSQL_VERSION, WSQL_DESC, WSQL_SIZE); + if(!db) { + callback("[WebSQL] Unable to open database."); + return; + } + + function onError(transaction, error) { + callback(error); + } + function onSuccess(transaction, result) { + that.db = db; + + function gotCount(transaction, result) { + var firstAccess = result.rows.item(0).count === 0; + callback(null, firstAccess); + } + function onError(transaction, error) { + callback(error); + } + // Keep track of whether we're accessing this db for the first time + // and therefore needs to get formatted. + transaction.executeSql("SELECT COUNT(id) AS count FROM " + FILE_STORE_NAME + ";", + [], gotCount, onError); + } + + db.transaction(function(transaction) { + transaction.executeSql("CREATE TABLE IF NOT EXISTS " + FILE_STORE_NAME + " (id unique, data)", + [], onSuccess, onError); + }); + }; + WebSQL.prototype.getReadOnlyContext = function() { + return new WebSQLContext(this.db, true); + }; + WebSQL.prototype.getReadWriteContext = function() { + return new WebSQLContext(this.db, false); + }; + + return WebSQL; +}); diff --git a/tests/spec/providers/providers.spec.js b/tests/spec/providers/providers.spec.js index d2dcc50..fbdde86 100644 --- a/tests/spec/providers/providers.spec.js +++ b/tests/spec/providers/providers.spec.js @@ -8,6 +8,10 @@ define(["IDBFS"], function(IDBFS) { expect(typeof IDBFS.FileSystem.providers.IndexedDB).toEqual('function'); }); + it("has WebSQL constructor", function() { + expect(typeof IDBFS.FileSystem.providers.WebSQL).toEqual('function'); + }); + it("has Memory constructor", function() { expect(typeof IDBFS.FileSystem.providers.Memory).toEqual('function'); }); @@ -15,5 +19,9 @@ define(["IDBFS"], function(IDBFS) { it("has a Default constructor", function() { expect(typeof IDBFS.FileSystem.providers.Default).toEqual('function'); }); + + it("has Fallback constructor", function() { + expect(typeof IDBFS.FileSystem.providers.Fallback).toEqual('function'); + }); }); }); diff --git a/tests/spec/providers/providers.websql.spec.js b/tests/spec/providers/providers.websql.spec.js new file mode 100644 index 0000000..d3dcd80 --- /dev/null +++ b/tests/spec/providers/providers.websql.spec.js @@ -0,0 +1,200 @@ +define(["IDBFS"], function(IDBFS) { + + var WEBSQL_NAME = "websql-test-db"; + + function wipeDB(provider) { + var context = provider.getReadWriteContext(); + context.clear(function(err) { + if(err) { + console.error("Problem clearing WebSQL db: [" + err.code + "] - " + err.message); + } + }); + } + + if(!IDBFS.FileSystem.providers.WebSQL.isSupported()) { + console.log("Skipping IDBFS.FileSystem.providers.WebSQL tests, since WebSQL isn't supported."); + return; + } + + describe("IDBFS.FileSystem.providers.WebSQL", function() { + it("is supported -- if it isn't, none of these tests can run.", function() { + expect(IDBFS.FileSystem.providers.WebSQL.isSupported()).toEqual(true); + }); + + it("has open, getReadOnlyContext, and getReadWriteContext instance methods", function() { + var webSQLProvider = new IDBFS.FileSystem.providers.WebSQL(); + expect(typeof webSQLProvider.open).toEqual('function'); + expect(typeof webSQLProvider.getReadOnlyContext).toEqual('function'); + expect(typeof webSQLProvider.getReadWriteContext).toEqual('function'); + }); + + describe("open an WebSQL provider", function() { + afterEach(function() { + wipeDB(this.provider); + }); + + it("should open a new WebSQL database", function() { + var complete = false; + var _error, _result; + + var provider = this.provider = new IDBFS.FileSystem.providers.WebSQL(WEBSQL_NAME); + provider.open(function(err, firstAccess) { + _error = err; + _result = firstAccess; + complete = true; + }); + + waitsFor(function() { + return complete; + }, 'test to complete', DEFAULT_TIMEOUT); + + runs(function() { + expect(_error).toEqual(null); + expect(_result).toEqual(true); + }); + }); + }); + + describe("Read/Write operations on an WebSQL provider", function() { + afterEach(function() { + wipeDB(this.provider); + }); + + it("should allow put() and get()", function() { + var complete = false; + var _error, _result; + + var provider = this.provider = new IDBFS.FileSystem.providers.WebSQL(WEBSQL_NAME); + provider.open(function(err, firstAccess) { + _error = err; + + var context = provider.getReadWriteContext(); + context.put("key", "value", function(err, result) { + _error = _error || err; + context.get("key", function(err, result) { + _error = _error || err; + _result = result; + + complete = true; + }); + }); + }); + + waitsFor(function() { + return complete; + }, 'test to complete', DEFAULT_TIMEOUT); + + runs(function() { + expect(_error).toEqual(null); + expect(_result).toEqual("value"); + }); + }); + + it("should allow delete()", function() { + var complete = false; + var _error, _result; + + var provider = this.provider = new IDBFS.FileSystem.providers.WebSQL(WEBSQL_NAME); + provider.open(function(err, firstAccess) { + _error = err; + + var context = provider.getReadWriteContext(); + context.put("key", "value", function(err, result) { + _error = _error || err; + context.delete("key", function(err, result) { + _error = _error || err; + context.get("key", function(err, result) { + _error = _error || err; + _result = result; + + complete = true; + }); + }); + }); + }); + + waitsFor(function() { + return complete; + }, 'test to complete', DEFAULT_TIMEOUT); + + runs(function() { + expect(_error).toEqual(null); + expect(_result).toEqual(null); + }); + }); + + it("should allow clear()", function() { + var complete = false; + var _error, _result1, _result2; + + var provider = this.provider = new IDBFS.FileSystem.providers.WebSQL(WEBSQL_NAME); + provider.open(function(err, firstAccess) { + _error = err; + + var context = provider.getReadWriteContext(); + context.put("key1", "value1", function(err, result) { + _error = _error || err; + context.put("key2", "value2", function(err, result) { + _error = _error || err; + + context.clear(function(err) { + _error = _error || err; + + context.get("key1", function(err, result) { + _error = _error || err; + _result1 = result; + + context.get("key2", function(err, result) { + _error = _error || err; + _result2 = result; + + complete = true; + }); + }); + }); + }); + }); + }); + + waitsFor(function() { + return complete; + }, 'test to complete', DEFAULT_TIMEOUT); + + runs(function() { + expect(_error).toEqual(null); + expect(_result1).toEqual(null); + expect(_result2).toEqual(null); + }); + }); + + it("should fail when trying to write on ReadOnlyContext", function() { + var complete = false; + var _error, _result; + + var provider = this.provider = new IDBFS.FileSystem.providers.WebSQL(WEBSQL_NAME); + provider.open(function(err, firstAccess) { + _error = err; + + var context = provider.getReadOnlyContext(); + context.put("key1", "value1", function(err, result) { + _error = _error || err; + _result = result; + + complete = true; + }); + }); + + waitsFor(function() { + return complete; + }, 'test to complete', DEFAULT_TIMEOUT); + + runs(function() { + expect(_error).toBeDefined(); + expect(_result).toEqual(null); + }); + }); + }); + + }); + +}); diff --git a/tests/test-manifest.js b/tests/test-manifest.js index 880bf9a..47e9d84 100644 --- a/tests/test-manifest.js +++ b/tests/test-manifest.js @@ -32,6 +32,7 @@ define([ "spec/providers/providers.spec", "spec/providers/providers.memory.spec", "spec/providers/providers.indexeddb.spec", + "spec/providers/providers.websql.spec", // Ported node.js tests (filenames match names in https://github.com/joyent/node/tree/master/test) "spec/node-js/simple/test-fs-mkdir",