/* * Copyright 2013, Nexedi SA * Released under the LGPL license. * http://www.gnu.org/licenses/lgpl.html */ /*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */ /*global jIO: true, localStorage: true, setTimeout: true, complex_queries: true */ /** * JIO Local Storage. Type = 'local'. * Local browser "database" storage. */ jIO.addStorageType('local', function (spec, my) { spec = spec || {}; var that, priv, localstorage; that = my.basicStorage(spec, my); priv = {}; /* * Wrapper for the localStorage used to simplify instion of any kind of * values */ localstorage = { getItem: function (item) { var value = localStorage.getItem(item); return value === null ? null : JSON.parse(value); }, setItem: function (item, value) { return localStorage.setItem(item, JSON.stringify(value)); }, removeItem: function (item) { return localStorage.removeItem(item); } }; // attributes priv.username = spec.username || ''; priv.application_name = spec.application_name || 'untitled'; priv.localpath = 'jio/localstorage/' + priv.username + '/' + priv.application_name; // ==================== Tools ==================== /** * Generate a new uuid * @method generateUuid * @return {string} The new uuid */ priv.generateUuid = function () { var S4 = function () { /* 65536 */ var i, string = Math.floor( Math.random() * 0x10000 ).toString(16); for (i = string.length; i < 4; i += 1) { string = '0' + string; } return string; }; return S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4(); }; /** * Update [doc] the document object and remove [doc] keys * which are not in [new_doc]. It only changes [doc] keys not starting * with an underscore. * ex: doc: {key:value1,_key:value2} with * new_doc: {key:value3,_key:value4} updates * doc: {key:value3,_key:value2}. * @param {object} doc The original document object. * @param {object} new_doc The new document object */ priv.documentObjectUpdate = function (doc, new_doc) { var k; for (k in doc) { if (doc.hasOwnProperty(k)) { if (k[0] !== '_') { delete doc[k]; } } } for (k in new_doc) { if (new_doc.hasOwnProperty(k)) { if (k[0] !== '_') { doc[k] = new_doc[k]; } } } }; /** * Checks if an object has no enumerable keys * @method objectIsEmpty * @param {object} obj The object * @return {boolean} true if empty, else false */ priv.objectIsEmpty = function (obj) { var k; for (k in obj) { if (obj.hasOwnProperty(k)) { return false; } } return true; }; // ===================== overrides ====================== that.specToStore = function () { return { "application_name": priv.application_name, "username": priv.username }; }; that.validateState = function () { if (typeof priv.username === "string" && priv.username !== '') { return ''; } return 'Need at least one parameter: "username".'; }; // ==================== commands ==================== /** * Create a document in local storage. * @method post * @param {object} command The JIO command */ that.post = function (command) { setTimeout(function () { var doc, doc_id = command.getDocId(); if (!doc_id) { doc_id = priv.generateUuid(); } doc = localstorage.getItem(priv.localpath + "/" + doc_id); if (doc === null) { doc = command.cloneDoc(); doc._id = doc_id; // the document does not exist localstorage.setItem(priv.localpath + "/" + doc_id, doc); that.success({ "ok": true, "id": doc_id }); } else { // the document already exists that.error({ "status": 409, "statusText": "Conflicts", "error": "conflicts", "message": "Cannot create a new document", "reason": "Document already exists" }); } }); }; /** * Create or update a document in local storage. * @method put * @param {object} command The JIO command */ that.put = function (command) { setTimeout(function () { var doc; doc = localstorage.getItem(priv.localpath + "/" + command.getDocId()); if (doc === null) { // the document does not exist doc = command.cloneDoc(); } else { // the document already exists priv.documentObjectUpdate(doc, command.cloneDoc()); } // write localstorage.setItem(priv.localpath + "/" + command.getDocId(), doc); that.success({ "ok": true, "id": command.getDocId() }); }); }; /** * Add an attachment to a document * @method putAttachment * @param {object} command The JIO command */ that.putAttachment = function (command) { setTimeout(function () { var doc; doc = localstorage.getItem(priv.localpath + "/" + command.getDocId()); if (doc === null) { // the document does not exist that.error({ "status": 404, "statusText": "Not Found", "error": "not_found", "message": "Impossible to add attachment", "reason": "Document not found" }); return; } // the document already exists doc._attachments = doc._attachments || {}; doc._attachments[command.getAttachmentId()] = { "content_type": command.getAttachmentMimeType(), "digest": "md5-" + command.md5SumAttachmentData(), "length": command.getAttachmentLength() }; // upload data localstorage.setItem(priv.localpath + "/" + command.getDocId() + "/" + command.getAttachmentId(), command.getAttachmentData()); // write document localstorage.setItem(priv.localpath + "/" + command.getDocId(), doc); that.success({ "ok": true, "id": command.getDocId(), "attachment": command.getAttachmentId() }); }); }; /** * Get a document * @method get * @param {object} command The JIO command */ that.get = function (command) { setTimeout(function () { var doc = localstorage.getItem(priv.localpath + "/" + command.getDocId()); if (doc !== null) { that.success(doc); } else { that.error({ "status": 404, "statusText": "Not Found", "error": "not_found", "message": "Cannot find the document", "reason": "Document does not exist" }); } }); }; /** * Get a attachment * @method getAttachment * @param {object} command The JIO command */ that.getAttachment = function (command) { setTimeout(function () { var doc = localstorage.getItem(priv.localpath + "/" + command.getDocId() + "/" + command.getAttachmentId()); if (doc !== null) { that.success(doc); } else { that.error({ "status": 404, "statusText": "Not Found", "error": "not_found", "message": "Cannot find the attachment", "reason": "Attachment does not exist" }); } }); }; /** * Remove a document * @method remove * @param {object} command The JIO command */ that.remove = function (command) { setTimeout(function () { var doc, i, attachment_list; doc = localstorage.getItem(priv.localpath + "/" + command.getDocId()); attachment_list = []; if (doc !== null && typeof doc === "object") { if (typeof doc._attachments === "object") { // prepare list of attachments for (i in doc._attachments) { if (doc._attachments.hasOwnProperty(i)) { attachment_list.push(i); } } } } else { return that.error({ "status": 404, "statusText": "Not Found", "error": "not_found", "message": "Document not found", "reason": "missing" }); } localstorage.removeItem(priv.localpath + "/" + command.getDocId()); // delete all attachments for (i = 0; i < attachment_list.length; i += 1) { localstorage.removeItem(priv.localpath + "/" + command.getDocId() + "/" + attachment_list[i]); } that.success({ "ok": true, "id": command.getDocId() }); }); }; /** * Remove an attachment * @method removeAttachment * @param {object} command The JIO command */ that.removeAttachment = function (command) { setTimeout(function () { var doc, error, i, attachment_list; error = function (word) { that.error({ "status": 404, "statusText": "Not Found", "error": "not_found", "message": word + " not found", "reason": "missing" }); }; doc = localstorage.getItem(priv.localpath + "/" + command.getDocId()); // remove attachment from document if (doc !== null && typeof doc === "object" && typeof doc._attachments === "object") { if (typeof doc._attachments[command.getAttachmentId()] === "object") { delete doc._attachments[command.getAttachmentId()]; if (priv.objectIsEmpty(doc._attachments)) { delete doc._attachments; } localstorage.setItem(priv.localpath + "/" + command.getDocId(), doc); localstorage.removeItem(priv.localpath + "/" + command.getDocId() + "/" + command.getAttachmentId()); that.success({ "ok": true, "id": command.getDocId(), "attachment": command.getAttachmentId() }); } else { error("Attachment"); } } else { error("Document"); } }); }; /** * Get all filenames belonging to a user from the document index * @method allDocs * @param {object} command The JIO command */ that.allDocs = function (command) { var i, row, path_re, rows = [], document_list = [], option, document_object; path_re = new RegExp( "^" + complex_queries.stringEscapeRegexpCharacters(priv.localpath) + "/[^/]+$" ); option = command.cloneOption(); if (typeof complex_queries !== "object" || (option.query === undefined && option.sort_on === undefined && option.select_list === undefined && option.include_docs === undefined)) { rows = []; for (i in localStorage) { if (localStorage.hasOwnProperty(i)) { // filter non-documents if (path_re.test(i)) { row = { value: {} }; row.id = i.split('/').slice(-1)[0]; row.key = row.id; if (command.getOption('include_docs')) { row.doc = JSON.parse(localStorage.getItem(i)); } rows.push(row); } } } that.success({"rows": rows, "total_rows": rows.length}); } else { // create complex query object from returned results for (i in localStorage) { if (localStorage.hasOwnProperty(i)) { if (path_re.test(i)) { document_list.push(localstorage.getItem(i)); } } } option.select_list = option.select_list || []; option.select_list.push("_id"); if (option.include_docs === true) { document_object = {}; document_list.forEach(function (meta) { document_object[meta._id] = meta; }); } complex_queries.QueryFactory.create(option.query || ""). exec(document_list, option); document_list = document_list.map(function (value) { var o = { "id": value._id, "key": value._id }; if (option.include_docs === true) { o.doc = document_object[value._id]; delete document_object[value._id]; } delete value._id; o.value = value; return o; }); that.success({"total_rows": document_list.length, "rows": document_list}); } }; return that; });