Last active
June 16, 2016 16:12
-
-
Save andersevenrud/09f198b9f55cbf8e434341f4388844dd to your computer and use it in GitHub Desktop.
localstorage.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /*! | |
| * OS.js - JavaScript Cloud/Web Desktop Platform | |
| * | |
| * Copyright (c) 2011-2016, Anders Evenrud <[email protected]> | |
| * All rights reserved. | |
| * | |
| * Redistribution and use in source and binary forms, with or without | |
| * modification, are permitted provided that the following conditions are met: | |
| * | |
| * 1. Redistributions of source code must retain the above copyright notice, this | |
| * list of conditions and the following disclaimer. | |
| * 2. Redistributions in binary form must reproduce the above copyright notice, | |
| * this list of conditions and the following disclaimer in the documentation | |
| * and/or other materials provided with the distribution. | |
| * | |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |
| * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
| * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | |
| * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |
| * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | |
| * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | |
| * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| * | |
| * @author Anders Evenrud <[email protected]> | |
| * @licence Simplified BSD License | |
| */ | |
| (function(Utils, API) { | |
| 'use strict'; | |
| /* | |
| * This storage works like this: | |
| * | |
| * A map of folders with arrays of metadata | |
| * namespace/tree = {'/': [{id: -1, size: -1, mime: 'str', filename: 'str'}, ...], ...} | |
| * | |
| * A flat map of data | |
| * namespace/data = {'path': %base64%} | |
| * | |
| */ | |
| var OSjs = window.OSjs = window.OSjs || {}; | |
| OSjs.VFS = OSjs.VFS || {}; | |
| OSjs.VFS.Modules = OSjs.VFS.Modules || {}; | |
| ///////////////////////////////////////////////////////////////////////////// | |
| // GLOBALS | |
| ///////////////////////////////////////////////////////////////////////////// | |
| var NAMESPACE = 'OSjs/VFS/LocalStorage'; | |
| var _isMounted = false; | |
| var _cache = {}; | |
| var _fileCache = {}; | |
| ///////////////////////////////////////////////////////////////////////////// | |
| // DEVELOPER MODES | |
| ///////////////////////////////////////////////////////////////////////////// | |
| var DEVMODE = false; | |
| if ( DEVMODE ) { | |
| _cache = { | |
| '/': [{ | |
| size: 19, | |
| mime: 'text/plain', | |
| type: 'file', | |
| filename: 'foo' | |
| }, { | |
| size: 0, | |
| type: 'dir', | |
| filename: 'a' | |
| }, { | |
| size: 0, | |
| type: 'dir', | |
| filename: 'test' | |
| }], | |
| '/a': [{ | |
| size: 0, | |
| type: 'dir', | |
| filename: 'c' | |
| }], | |
| '/a/c': [{ | |
| size: 19, | |
| mime: 'text/plain', | |
| type: 'file', | |
| filename: 'baz' | |
| }], | |
| '/test': [] | |
| }; | |
| _fileCache = { | |
| '/foo': 'VGhpcyBpcyBqdXN0IGEgdGVzdA==', | |
| '/a/c/baz': 'VGhpcyBpcyBqdXN0IGEgdGVzdA==' | |
| }; | |
| } | |
| ///////////////////////////////////////////////////////////////////////////// | |
| // HELPERS | |
| ///////////////////////////////////////////////////////////////////////////// | |
| /** | |
| * Get's the "real" path of a object (which is basically a path without protocol) | |
| */ | |
| function getRealPath(p, par) { | |
| if ( typeof p !== 'string' || !p ) { | |
| throw new TypeError('Expected p as String'); | |
| } | |
| p = OSjs.VFS.getRelativeURL(p).replace(/\/+/g, '/'); | |
| var path = par ? (Utils.dirname(p) || '/') : p; | |
| if ( path !== '/' ) { | |
| path = path.replace(/\/$/, ''); | |
| } | |
| return path; | |
| } | |
| /** | |
| * This methods creates a VFS.File from cache and fills in the gaps | |
| */ | |
| function createMetadata(i, path, p) { | |
| i = Utils.cloneObject(i); | |
| if ( !p.match(/(\/\/)?\/$/) ) { | |
| p += '/'; | |
| } | |
| i.path = p + i.filename; | |
| return new OSjs.VFS.File(i); | |
| } | |
| ///////////////////////////////////////////////////////////////////////////// | |
| // LOCALSTORAGE ABSTRACTION | |
| ///////////////////////////////////////////////////////////////////////////// | |
| /** | |
| * Initialize and restore data from localStorage | |
| */ | |
| function initStorage() { | |
| if ( !_isMounted ) { | |
| if ( !DEVMODE ) { | |
| try { | |
| _cache = JSON.parse(localStorage.getItem(NAMESPACE + '/tree')) || {}; | |
| } catch ( e ) {} | |
| try { | |
| _fileCache = JSON.parse(localStorage.getItem(NAMESPACE + '/data')) || {}; | |
| } catch ( e ) {} | |
| } | |
| if ( typeof _cache['/'] === 'undefined' ) { | |
| _cache['/'] = []; | |
| } | |
| _isMounted = true; | |
| API.message('vfs:mount', 'LocalStorage', {source: null}); | |
| } | |
| } | |
| /** | |
| * Store tree and data to localStorage | |
| */ | |
| function commitStorage() { | |
| try { | |
| if ( !DEVMODE ) { | |
| localStorage.setItem(NAMESPACE + '/tree', JSON.stringify(_cache)); | |
| localStorage.setItem(NAMESPACE + '/data', JSON.stringify(_fileCache)); | |
| } | |
| return true; | |
| } catch ( e ) {} | |
| return false; | |
| } | |
| ///////////////////////////////////////////////////////////////////////////// | |
| // CACHE | |
| ///////////////////////////////////////////////////////////////////////////// | |
| /** | |
| * Adds an entry to the cache | |
| */ | |
| function addToCache(iter, data, dab) { | |
| var path = getRealPath(iter.path); | |
| var dirname = Utils.dirname(path); | |
| var type = typeof data === 'undefined' || data === null ? 'dir' : 'file'; | |
| var mime = iter.mime || 'application/octet-stream'; | |
| var file = { | |
| size: iter.size || (type === 'file' ? (dab.byteLength || dab.length || 0) : 0), | |
| mime: mime, | |
| type: type, | |
| filename: iter.filename | |
| }; | |
| if ( typeof _cache[dirname] === 'undefined' ) { | |
| _cache[dirname] = []; | |
| } | |
| var found = findInCache(iter); | |
| if ( found !== false) { | |
| _cache[dirname][found] = file; | |
| } else { | |
| _cache[dirname].push(file); | |
| } | |
| if ( file.type === 'dir' ) { | |
| if ( _fileCache[path] ) { | |
| delete _fileCache[path]; | |
| } | |
| _cache[path] = []; | |
| } else { | |
| var iof = data.indexOf(','); | |
| _fileCache[path] = data.substr(iof + 1); | |
| } | |
| return true; | |
| } | |
| /** | |
| * Removes an entry from cache (recursively) | |
| */ | |
| function removeFromCache(iter) { | |
| function _removef(i) { | |
| var path = getRealPath(i.path); | |
| //console.warn('-->', '_removef', i, path); | |
| // Remove data | |
| if ( _fileCache[path] ) { | |
| delete _fileCache[path]; | |
| } | |
| // Remove from parent tree | |
| _removefromp(i); | |
| } | |
| function _removed(i) { | |
| var path = getRealPath(i.path); | |
| //console.warn('-->', '_removed', i, path); | |
| if ( path !== '/' ) { | |
| // Remove from parent node | |
| _removefromp(i); | |
| // Remove base node if a root directory | |
| if ( _cache[path] ) { | |
| delete _cache[path]; | |
| } | |
| } | |
| } | |
| function _removefromp(i) { | |
| var path = getRealPath(i.path); | |
| var dirname = Utils.dirname(path); | |
| //console.warn('-->', '_removefromp', i, path, dirname); | |
| if ( _cache[dirname] ) { | |
| var found = -1; | |
| _cache[dirname].forEach(function(ii, idx) { | |
| if ( found === -1 && ii ) { | |
| if ( ii.type === i.type && i.filename === i.filename ) { | |
| found = idx; | |
| } | |
| } | |
| }); | |
| if ( found >= 0 ) { | |
| _cache[dirname].splice(found, 1); | |
| } | |
| } | |
| } | |
| function _op(i) { | |
| //console.warn('-->', '_op', i); | |
| if ( i ) { | |
| if ( i.type === 'dir' ) { | |
| // First go up in the tree | |
| scanStorage(i, false).forEach(function(ii) { | |
| _op(ii); | |
| }); | |
| // Then go down | |
| _removed(i); | |
| } else { | |
| _removef(i); | |
| } | |
| } | |
| } | |
| _op(iter); | |
| return true; | |
| } | |
| /** | |
| * Looks up a file from the cache and returns index | |
| */ | |
| function findInCache(iter) { | |
| var path = getRealPath(iter.path); | |
| var dirname = Utils.dirname(path); | |
| var found = false; | |
| _cache[dirname].forEach(function(chk, idx) { | |
| if ( found === false && chk.filename === iter.filename ) { | |
| found = idx; | |
| } | |
| }); | |
| return found; | |
| } | |
| /** | |
| * Fetches a VFS.File object from cache from path | |
| */ | |
| function getFromCache(pp) { | |
| var path = Utils.dirname(pp); | |
| var fname = Utils.filename(pp); | |
| var result = null; | |
| var tpath = path.replace(/^(.*)\:\/\//, ''); | |
| (_cache[tpath] || []).forEach(function(v) { | |
| if ( !result && v.filename === fname ) { | |
| result = createMetadata(v, null, path); | |
| } | |
| }); | |
| return result; | |
| } | |
| /** | |
| * Scans a directory and returns file list | |
| */ | |
| function scanStorage(item, ui) { | |
| var path = getRealPath(item.path); | |
| var data = _cache[path] || false; | |
| var list = (data === false) ? false : data.filter(function(i) { | |
| return !!i; | |
| }).map(function(i) { | |
| return createMetadata(i, path, item.path); | |
| }); | |
| if ( ui && Utils.dirname(path) !== path ) { | |
| list.unshift({ | |
| size: 0, | |
| mime: null, | |
| type: 'dir', | |
| filename: '..', | |
| path: Utils.dirname(item.path) | |
| }); | |
| } | |
| return list; | |
| } | |
| ///////////////////////////////////////////////////////////////////////////// | |
| // API | |
| ///////////////////////////////////////////////////////////////////////////// | |
| var LocalStorageStorage = { | |
| scandir: function(item, callback, options) { | |
| var list = scanStorage(item, true); | |
| callback(list === false ? 'No such directory' : false, list); | |
| }, | |
| read: function(item, callback, options) { | |
| options = options || {}; | |
| var path = getRealPath(item.path); | |
| function readStorage(cb) { | |
| var metadata = getFromCache(path); | |
| if ( metadata ) { | |
| var data = _fileCache[path]; | |
| if ( data ) { | |
| var ds = 'data:' + metadata.mime + ',' + data; | |
| OSjs.VFS.dataSourceToAb(ds, metadata.mime, function(err, res) { | |
| if ( err ) { | |
| cb(err); | |
| } else { | |
| if ( options.url ) { | |
| OSjs.VFS.abToBlob(res, metadata.mime, function(err, blob) { | |
| cb(err, URL.createObjectURL(blob)); | |
| }); | |
| } else { | |
| cb(err, res); | |
| } | |
| } | |
| }); | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| if ( readStorage(callback) === false ) { | |
| callback('Failed to read', false); | |
| } | |
| }, | |
| write: function(file, data, callback, options) { | |
| options = options || {}; | |
| var mime = file.mime || 'application/octet-stream'; | |
| function writeStorage(cb) { | |
| if ( options.isds ) { | |
| cb(false, data); | |
| } else { | |
| OSjs.VFS.abToDataSource(data, mime, function(err, res) { | |
| if ( err ) { | |
| callback(err, false); | |
| } else { | |
| cb(false, res); | |
| } | |
| }); | |
| } | |
| } | |
| writeStorage(function(err, res) { | |
| try { | |
| if ( addToCache(file, res, data) && commitStorage() ) { | |
| callback(err, true); | |
| } else { | |
| callback('Failed to write', false); | |
| } | |
| } catch ( e ) { | |
| callback(e); | |
| } | |
| }); | |
| }, | |
| unlink: function(src, callback) { | |
| try { | |
| src = getFromCache(src.path) || src; | |
| if ( removeFromCache(src) && commitStorage() ) { | |
| callback(false, true); | |
| } else { | |
| callback('Failed to unlink', false); | |
| } | |
| } catch ( e ) { | |
| callback(e); | |
| } | |
| }, | |
| copy: function(src, dest, callback) { | |
| function _write(s, d, cb) { | |
| LocalStorageStorage.read(s, function(err, data) { | |
| if ( err ) { | |
| cb(err); | |
| } else { | |
| LocalStorageStorage.write(d, data, cb); | |
| } | |
| }); | |
| } | |
| function _op(s, d, cb) { | |
| if ( s.type === 'file' ) { | |
| d.mime = s.mime; | |
| } | |
| d.size = s.size; | |
| d.type = s.type; | |
| if ( d.type === 'dir' ) { | |
| LocalStorageStorage.mkdir(d, function(err, res) { | |
| if ( err ) { | |
| cb(err); | |
| } else { | |
| var list = scanStorage(s, false); | |
| if ( list && list.length ) { | |
| Utils.asyncs(list, function(entry, idx, next) { | |
| var rp = entry.path.substr(src.path.length); | |
| var nd = new OSjs.VFS.File(dest.path + rp); | |
| //console.warn('----->', 'source root', s); | |
| //console.warn('----->', 'dest root', d); | |
| //console.warn('----->', 'files', list.length, idx); | |
| //console.warn('----->', 'relative', rp); | |
| //console.warn('----->', 'new file', nd); | |
| _op(entry, nd, next); | |
| }, function() { | |
| cb(false, true); | |
| }); | |
| } else { | |
| cb(false, true); | |
| } | |
| } | |
| }); | |
| } else { | |
| _write(s, d, cb); | |
| } | |
| } | |
| // Force retrieval of real data so MIME is correctly synced etc | |
| src = getFromCache(src.path) || src; | |
| // Check if destination exists | |
| var droot = getRealPath(Utils.dirname(dest.path)); | |
| if ( droot !== '/' && !getFromCache(droot) ) { | |
| callback('Target folder does not exist'); | |
| return; | |
| } | |
| if ( src.type === 'dir' && src.path === Utils.dirname(dest.path) ) { | |
| callback('You cannot copy a directory into itself'); | |
| return; | |
| } | |
| _op(src, dest, callback); | |
| }, | |
| move: function(src, dest, callback) { | |
| var spath = getRealPath(src.path); | |
| var dpath = getRealPath(dest.path); | |
| var sdirname = Utils.dirname(spath); | |
| var ddirname = Utils.dirname(dpath); | |
| if ( _fileCache[dpath] ) { | |
| callback('File already exists'); | |
| return; | |
| } | |
| // Rename | |
| if ( sdirname === ddirname ) { | |
| if ( _fileCache[spath] ) { | |
| var tmp = _fileCache[spath]; | |
| delete _fileCache[spath]; | |
| _fileCache[dpath] = tmp; | |
| } | |
| if ( _cache[sdirname] ) { | |
| var found = -1; | |
| _cache[sdirname].forEach(function(i, idx) { | |
| if ( i && found === -1 ) { | |
| if ( i.filename === src.filename && i.type === src.type ) { | |
| found = idx; | |
| } | |
| } | |
| }); | |
| if ( found >= 0 ) { | |
| _cache[sdirname][found].filename = dest.filename; | |
| } | |
| } | |
| callback(false, commitStorage()); | |
| } else { | |
| LocalStorageStorage.copy(src, dest, function(err) { | |
| if ( err ) { | |
| callback(err); | |
| } else { | |
| OSjs.VFS.unlink(src, callback); | |
| } | |
| }); | |
| } | |
| }, | |
| exists: function(item, callback) { | |
| var data = getFromCache(getRealPath(item.path)); | |
| callback(false, !!data); | |
| }, | |
| fileinfo: function(item, callback) { | |
| var data = getFromCache(item.path); | |
| callback(data ? false : 'No such file', data); | |
| }, | |
| mkdir: function(dir, callback) { | |
| var dpath = getRealPath(dir.path); | |
| if ( dpath !== '/' && getFromCache(dpath) ) { | |
| callback('Directory already exists'); | |
| return; | |
| } | |
| try { | |
| if ( addToCache(dir) && commitStorage() ) { | |
| callback(false, true); | |
| } else { | |
| callback('Failed to create directory'); | |
| } | |
| } catch ( e ) { | |
| callback(e); | |
| } | |
| }, | |
| upload: function(file, dest, callback) { | |
| var check = new OSjs.VFS.File(Utils.pathJoin(dest, file.name), file.type); | |
| check.size = file.size; | |
| check.type = 'file'; | |
| LocalStorageStorage.exists(check, function(err, exists) { | |
| if ( err || exists ) { | |
| callback(err || 'File already exists'); | |
| } else { | |
| var reader = new FileReader(); | |
| reader.onerror = function(e) { | |
| callback(e); | |
| }; | |
| reader.onloadend = function() { | |
| LocalStorageStorage.write(check, reader.result, callback, {isds: true}); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| }, | |
| url: function(item, callback) { | |
| LocalStorageStorage.exists(item, function(err, exists) { | |
| if ( err || !exists ) { | |
| callback(err || 'File does not exist'); | |
| } else { | |
| LocalStorageStorage.read(item, callback, {url: true}); | |
| } | |
| }); | |
| }, | |
| find: function(file, callback) { | |
| callback(API._('ERR_VFS_UNAVAILABLE')); | |
| }, | |
| trash: function(file, callback) { | |
| callback(API._('ERR_VFS_UNAVAILABLE')); | |
| }, | |
| untrash: function(file, callback) { | |
| callback(API._('ERR_VFS_UNAVAILABLE')); | |
| }, | |
| emptyTrash: function(callback) { | |
| callback(API._('ERR_VFS_UNAVAILABLE')); | |
| }, | |
| freeSpace: function(root, callback) { | |
| var total = 5 * 1024 * 1024; | |
| var used = JSON.stringify(_cache).length + JSON.stringify(_fileCache).length; | |
| callback(false, total - used); | |
| } | |
| }; | |
| ///////////////////////////////////////////////////////////////////////////// | |
| // WRAPPERS | |
| ///////////////////////////////////////////////////////////////////////////// | |
| function makeRequest(name, args, callback, options) { | |
| initStorage(); | |
| var ref = LocalStorageStorage[name]; | |
| var fargs = (args || []).slice(0); | |
| fargs.push(callback || function() {}); | |
| fargs.push(options || {}); | |
| return ref.apply(ref, fargs); | |
| } | |
| ///////////////////////////////////////////////////////////////////////////// | |
| // EXPORTS | |
| ///////////////////////////////////////////////////////////////////////////// | |
| /** | |
| * Experimental localStorage VFS Module | |
| * | |
| * @api OSjs.VFS.Modules.LocalStorage | |
| */ | |
| OSjs.VFS.Modules.LocalStorage = OSjs.VFS.Modules.LocalStorage || OSjs.VFS._createMountpoint({ | |
| readOnly: false, | |
| moduleName: 'localstorage', | |
| description: 'My Documents', | |
| visible: true, | |
| searchable: false, | |
| unmount: function(cb) { | |
| cb = cb || function() {}; | |
| _isMounted = false; | |
| API.message('vfs:unmount', 'LocalStorage', {source: null}); | |
| cb(false, true); | |
| }, | |
| mounted: function() { | |
| return _isMounted; | |
| }, | |
| enabled: function() { | |
| return true; | |
| }, | |
| root: 'documents:///', | |
| icon: 'places/folder-documents.png', | |
| match: /^documents\:\/\//, | |
| request: makeRequest | |
| }); | |
| })(OSjs.Utils, OSjs.API); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment