Commit 02b14c97 authored by Romain Courteaud's avatar Romain Courteaud 🐸

Change localstorage model.

Allow to read all entries in localStorage.
localStorage becomes just one jio document with many attachments.
parent a2086c76
......@@ -4,9 +4,8 @@
* http://www.gnu.org/licenses/lgpl.html
*/
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true, regexp: true */
/*global jIO, localStorage, setTimeout, window, define, Blob, Uint8Array,
exports, require, console, RSVP */
/*jslint nomen: true */
/*global jIO, localStorage, window, Blob, Uint8Array, RSVP */
/**
* JIO Local Storage. Type = 'local'.
......@@ -15,201 +14,25 @@
* Storage Description:
*
* {
* "type": "local",
* "mode": <string>,
* // - "localStorage" // default
* // - "memory"
* "username": <non empty string>, // to define user space
* "application_name": <string> // default 'untitled'
* "type": "local"
* }
*
* Document are stored in path
* 'jio/localstorage/username/application_name/document_id' like this:
*
* {
* "_id": "document_id",
* "_attachments": {
* "attachment_name": {
* "length": data_length,
* "digest": "md5-XXX",
* "content_type": "mime/type"
* },
* "attachment_name2": {..}, ...
* },
* "metadata_name": "metadata_value"
* "metadata_name2": ...
* ...
* }
*
* Only "_id" and "_attachments" are specific metadata keys, other one can be
* added without loss.
*
* @class LocalStorage
*/
(function (exports, jIO) {
(function (jIO) {
"use strict";
/**
* Checks if an object has no enumerable keys
*
* @param {Object} obj The object
* @return {Boolean} true if empty, else false
*/
function objectIsEmpty(obj) {
var k;
for (k in obj) {
if (obj.hasOwnProperty(k)) {
return false;
}
}
return true;
}
var ram = {}, memorystorage, localstorage;
/*
* 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);
}
};
/*
* Wrapper for the localStorage used to simplify instion of any kind of
* values
*/
memorystorage = {
getItem: function (item) {
var value = ram[item];
return value === undefined ? null : JSON.parse(value);
},
setItem: function (item, value) {
ram[item] = JSON.stringify(value);
},
removeItem: function (item) {
delete ram[item];
}
};
/**
* The JIO LocalStorage extension
*
* @class LocalStorage
* @constructor
*/
function LocalStorage(spec) {
switch (spec.mode) {
case "memory":
this._database = ram;
this._storage = memorystorage;
this._mode = "memory";
break;
default:
this._database = localStorage;
this._storage = localstorage;
this._mode = "localStorage";
this._key_schema = spec.key_schema;
break;
}
function LocalStorage() {
return;
}
/**
* Create a document in local storage.
*
* @method post
* @param {Object} metadata The metadata to store
* @param {Object} options The command options
*/
LocalStorage.prototype.post = function (metadata) {
var doc, doc_id = metadata._id;
if (doc_id === undefined) {
doc_id = jIO.util.generateUuid();
function restrictDocumentId(id) {
if (id !== "/") {
throw new jIO.util.jIOError("id " + id + " is forbidden (!== /)",
400);
}
if (this._storage.getItem(doc_id) === null) {
// the document does not exist
doc = jIO.util.deepClone(metadata);
doc._id = doc_id;
// XXX
delete doc._attachments;
this._storage.setItem(doc_id, doc);
return doc_id;
}
// the document already exists
throw new jIO.util.jIOError("Cannot create a new document", 409);
};
/**
* Create or update a document in local storage.
*
* @method put
* @param {Object} command The JIO command
* @param {Object} metadata The metadata to store
* @param {Object} options The command options
*/
LocalStorage.prototype.put = function (metadata) {
var doc, tmp;
doc = this._storage.getItem(metadata._id);
if (doc === null) {
// the document does not exist
doc = jIO.util.deepClone(metadata);
delete doc._attachments;
} else {
// the document already exists
tmp = jIO.util.deepClone(metadata);
tmp._attachments = doc._attachments;
doc = tmp;
}
// write
this._storage.setItem(metadata._id, doc);
return metadata._id;
};
/**
* Add an attachment to a document
*
* @method putAttachment
* @param {Object} param The given parameters
* @param {Object} options The command options
*/
LocalStorage.prototype.putAttachment = function (param) {
var that = this, doc;
doc = this.get({"_id": param._id});
// the document already exists
// download data
return new RSVP.Queue()
.push(function () {
return jIO.util.readBlobAsBinaryString(param._blob);
})
.push(function (e) {
doc._attachments = doc._attachments || {};
// if (doc._attachments[param._attachment]) {
// status = "no_content";
// }
doc._attachments[param._attachment] = {
"content_type": param._blob.type,
"digest": jIO.util.makeBinaryStringDigest(e.target.result),
"length": param._blob.size
};
that._storage.setItem(param._id + "/" +
param._attachment, e.target.result);
that._storage.setItem(param._id, doc);
return {"digest": doc._attachments[param._attachment].digest};
});
};
/**
* Get a document
......@@ -219,9 +42,19 @@
* @param {Object} options The command options
*/
LocalStorage.prototype.get = function (param) {
var doc = this._storage.getItem(param._id);
if (doc === null) {
throw new jIO.util.jIOError("Cannot find document", 404);
restrictDocumentId(param._id);
var doc = {},
attachments = {},
key;
for (key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
attachments[key] = {};
}
}
if (attachments.length !== 0) {
doc._attachments = attachments;
}
return doc;
};
......@@ -234,426 +67,55 @@
* @param {Object} options The command options
*/
LocalStorage.prototype.getAttachment = function (param) {
var doc, i, uint8array, binarystring;
doc = this.get({"_id": param._id});
restrictDocumentId(param._id);
var binarystring = localStorage.getItem(param._attachment),
i,
uint8array;
if (typeof doc._attachments !== 'object' ||
typeof doc._attachments[param._attachment] !== 'object') {
if (binarystring === null) {
throw new jIO.util.jIOError("Cannot find attachment", 404);
}
// Storing data twice in binarystring and in uint8array (in memory)
// is not a problem here because localStorage <= 5MB
binarystring = this._storage.getItem(
param._id + "/" + param._attachment
) || "";
uint8array = new Uint8Array(binarystring.length);
for (i = 0; i < binarystring.length; i += 1) {
uint8array[i] = binarystring.charCodeAt(i); // mask `& 0xFF` not necessary
}
uint8array = new Blob([uint8array.buffer], {
"type": doc._attachments[param._attachment].content_type || ""
});
return {
"data": uint8array,
"digest": doc._attachments[param._attachment].digest
};
return new Blob([uint8array.buffer]);
};
/**
* Remove a document
*
* @method remove
* @param {Object} command The JIO command
* @param {Object} param The given parameters
* @param {Object} options The command options
*/
LocalStorage.prototype.remove = function (param) {
// var doc, i, attachment_list;
// doc = this._storage.getItem(param._id);
// 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 command.error(
// "not_found",
// "missing",
// "Document not found"
// );
// }
this._storage.removeItem(param._id);
// // delete all attachments
// for (i = 0; i < attachment_list.length; i += 1) {
// this._storage.removeItem(this._localpath + "/" + param._id +
// "/" + attachment_list[i]);
// }
// command.success();
return param._id;
};
LocalStorage.prototype.putAttachment = function (param) {
restrictDocumentId(param._id);
/**
* Remove an attachment
*
* @method removeAttachment
* @param {Object} command The JIO command
* @param {Object} param The given parameters
* @param {Object} options The command options
*/
LocalStorage.prototype.removeAttachment = function (command, param) {
var doc = this._storage.getItem(this._localpath + "/" + param._id);
if (typeof doc !== 'object' || doc === null) {
return command.error(
"not_found",
"missing document",
"Document not found"
);
}
if (typeof doc._attachments !== "object" ||
typeof doc._attachments[param._attachment] !== "object") {
return command.error(
"not_found",
"missing attachment",
"Attachment not found"
);
}
// the document already exists
// download data
return new RSVP.Queue()
.push(function () {
return jIO.util.readBlobAsBinaryString(param._blob);
})
.push(function (e) {
localStorage.setItem(param._attachment, e.target.result);
});
};
delete doc._attachments[param._attachment];
if (objectIsEmpty(doc._attachments)) {
delete doc._attachments;
}
this._storage.setItem(this._localpath + "/" + param._id, doc);
this._storage.removeItem(this._localpath + "/" + param._id +
"/" + param._attachment);
command.success();
LocalStorage.prototype.removeAttachment = function (param) {
restrictDocumentId(param._id);
return localStorage.removeItem(param._attachment);
};
LocalStorage.prototype.hasCapacity = function (name) {
return (name === "list");
};
LocalStorage.prototype.buildQuery = function () {
var rows = [],
i;
for (i in this._database) {
if (this._database.hasOwnProperty(i)) {
rows.push({
id: i,
return [{
id: "/",
value: {}
});
}
}
return rows;
};
// /**
// * Get all filenames belonging to a user from the document index
// *
// * @method allDocs
// * @param {Object} command The JIO command
// * @param {Object} param The given parameters
// * @param {Object} options The command options
// */
// LocalStorage.prototype.allDocs = function (command, param, options) {
// console.log("allDocs begin");
// var i, row, path_re, rows, document_list, document_object, delete_id;
// param.unused = true;
// rows = [];
// document_list = [];
// path_re = new RegExp(
// "^" + jIO.Query.stringEscapeRegexpCharacters(this._localpath) +
// "/[^/]+$"
// );
// if (options.query === undefined && options.sort_on === undefined &&
// options.select_list === undefined &&
// options.include_docs === undefined) {
// rows = [];
// for (i in this._database) {
// if (this._database.hasOwnProperty(i)) {
// // filter non-documents
// if (path_re.test(i)) {
// row = { value: {} };
// row.id = i.split('/').slice(-1)[0];
// row.key = row.id;
// if (options.include_docs) {
// row.doc = JSON.parse(this._storage.getItem(i));
// }
// rows.push(row);
// }
// }
// }
// command.success({"data": {"rows": rows, "total_rows": rows.length}});
// } else {
// // create jio query object from returned results
// for (i in this._database) {
// if (this._database.hasOwnProperty(i)) {
// if (path_re.test(i)) {
// document_list.push(this._storage.getItem(i));
// }
// }
// }
// options.select_list = options.select_list || [];
// if (options.select_list.indexOf("_id") === -1) {
// options.select_list.push("_id");
// delete_id = true;
// }
// if (options.include_docs === true) {
// document_object = {};
// document_list.forEach(function (meta) {
// document_object[meta._id] = meta;
// });
// }
// jIO.QueryFactory.create(options.query || "",
// this._key_schema).
// exec(document_list, options).then(function () {
// document_list = document_list.map(function (value) {
// var o = {
// "id": value._id,
// "key": value._id
// };
// if (options.include_docs === true) {
// o.doc = document_object[value._id];
// delete document_object[value._id];
// }
// if (delete_id) {
// delete value._id;
// }
// o.value = value;
// return o;
// });
// command.success({"data": {
// "total_rows": document_list.length,
// "rows": document_list
// }});
// });
// }
// };
/**
* Check the storage or a specific document
*
* @method check
* @param {Object} command The JIO command
* @param {Object} param The command parameters
* @param {Object} options The command options
*/
LocalStorage.prototype.check = function (command, param) {
this.genericRepair(command, param, false);
};
/**
* Repair the storage or a specific document
*
* @method repair
* @param {Object} command The JIO command
* @param {Object} param The command parameters
* @param {Object} options The command options
*/
LocalStorage.prototype.repair = function (command, param) {
this.genericRepair(command, param, true);
};
/**
* A generic method that manage check or repair command
*
* @method genericRepair
* @param {Object} command The JIO command
* @param {Object} param The command parameters
* @param {Boolean} repair If true then repair else just check
*/
LocalStorage.prototype.genericRepair = function (command, param, repair) {
var that = this, final_result;
function referenceAttachment(param, attachment) {
if (param.referenced_attachments.indexOf(attachment) !== -1) {
return;
}
var i = param.unreferenced_attachments.indexOf(attachment);
if (i !== -1) {
param.unreferenced_attachments.splice(i, 1);
}
param.referenced_attachments[param.referenced_attachments.length] =
attachment;
}
function attachmentFound(param, attachment) {
if (param.referenced_attachments.indexOf(attachment) !== -1) {
return;
}
if (param.unreferenced_attachments.indexOf(attachment) !== -1) {
return;
}
param.unreferenced_attachments[param.unreferenced_attachments.length] =
attachment;
}
function repairOne(param, repair) {
var i, doc, modified;
doc = that._storage.getItem(that._localpath + "/" + param._id);
if (doc === null) {
return; // OK
}
// check document type
if (typeof doc !== 'object' || doc === null) {
// wrong document
if (!repair) {
return {"error": true, "answers": [
"conflict",
"corrupted",
"Document is unrecoverable"
]};
}
// delete the document
that._storage.removeItem(that._localpath + "/" + param._id);
return; // OK
}
// good document type
// repair json document
if (!repair) {
if (!(new jIO.Metadata(doc).check())) {
return {"error": true, "answers": [
"conflict",
"corrupted",
"Some metadata might be lost"
]};
}
} else {
modified = jIO.util.uniqueJSONStringify(doc) !==
jIO.util.uniqueJSONStringify(new jIO.Metadata(doc).format()._dict);
}
if (doc._attachments !== undefined) {
if (typeof doc._attachments !== 'object') {
if (!repair) {
return {"error": true, "answers": [
"conflict",
"corrupted",
"Attachments are unrecoverable"
]};
}
delete doc._attachments;
that._storage.setItem(that._localpath + "/" + param._id, doc);
return; // OK
}
for (i in doc._attachments) {
if (doc._attachments.hasOwnProperty(i)) {
// check attachment existence
if (that._storage.getItem(that._localpath + "/" + param._id + "/" +
i) !== 'string') {
if (!repair) {
return {"error": true, "answers": [
"conflict",
"missing attachment",
"Attachment \"" + i + "\" of \"" + param._id + "\" is missing"
]};
}
delete doc._attachments[i];
if (objectIsEmpty(doc._attachments)) {
delete doc._attachments;
}
modified = true;
} else {
// attachment exists
// check attachment metadata
// check length
referenceAttachment(param, param._id + "/" + doc._attachments[i]);
if (doc._attachments[i].length !== undefined &&
typeof doc._attachments[i].length !== 'number') {
if (!repair) {
return {"error": true, "answers": [
"conflict",
"corrupted",
"Attachment metadata length corrupted"
]};
}
// It could take a long time to get the length, no repair.
// length can be omited
delete doc._attachments[i].length;
}
// It could take a long time to regenerate the hash, no check.
// Impossible to discover the attachment content type.
}
}
}
}
if (modified) {
that._storage.setItem(that._localpath + "/" + param._id, doc);
}
// OK
}
function repairAll(param, repair) {
var i, result;
for (i in that._database) {
if (that._database.hasOwnProperty(i)) {
// browsing every entry
if (i.slice(0, that._localpath.length) === that._localpath) {
// is part of the user space
if (/^[^\/]+\/[^\/]+$/.test(i.slice(that._localpath.length + 1))) {
// this is an attachment
attachmentFound(param, i.slice(that._localpath.length + 1));
} else if (/^[^\/]+$/.test(i.slice(that._localpath.length + 1))) {
// this is a document
param._id = i.slice(that._localpath.length + 1);
result = repairOne(param, repair);
if (result) {
return result;
}
} else {
// this is pollution
that._storage.removeItem(i);
}
}
}
}
// remove unreferenced attachments
for (i = 0; i < param.unreferenced_attachments.length; i += 1) {
that._storage.removeItem(that._localpath + "/" +
param.unreferenced_attachments[i]);
}
}
param.referenced_attachments = [];
param.unreferenced_attachments = [];
if (typeof param._id === 'string') {
final_result = repairOne(param, repair) || {};
} else {
final_result = repairAll(param, repair) || {};
}
if (final_result.error) {
return command.error.apply(command, final_result.answers || []);
}
command.success.apply(command, final_result.answers || []);
}];
};
jIO.addStorage('local', LocalStorage);
function clearLocalStorage() {
var k;
for (k in localStorage) {
if (localStorage.hasOwnProperty(k)) {
if (/^jio\/localstorage\//.test(k)) {
localStorage.removeItem(k);
}
}
}
}
function clearMemoryStorage() {
jIO.util.dictClear(ram);
}
exports.clear = clearLocalStorage;
exports.clearLocalStorage = clearLocalStorage;
exports.clearMemoryStorage = clearMemoryStorage;
}(window, jIO));
}(jIO));
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment