pax_global_header 0000666 0000000 0000000 00000000064 13037644422 0014517 g ustar 00root root 0000000 0000000 52 comment=8a170cabe382a651ac2b8394e47f04fbc8fc01fe
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/ 0000775 0000000 0000000 00000000000 13037644422 0022520 5 ustar 00root root 0000000 0000000 jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/ 0000775 0000000 0000000 00000000000 13037644422 0023307 5 ustar 00root root 0000000 0000000 jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/ 0000775 0000000 0000000 00000000000 13037644422 0025533 5 ustar 00root root 0000000 0000000 jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/cryptstorage.js 0000664 0000000 0000000 00000013266 13037644422 0030627 0 ustar 00root root 0000000 0000000 /*
* Copyright 2015, Nexedi SA
* Released under the LGPL license.
* http://www.gnu.org/licenses/lgpl.html
*/
/*jslint nomen: true*/
/*global jIO, RSVP, DOMException, Blob, crypto, Uint8Array, ArrayBuffer*/
(function (jIO, RSVP, DOMException, Blob, crypto, Uint8Array, ArrayBuffer) {
"use strict";
// you the cryptography system used by this storage is AES-GCM.
// here is an example of how to generate a key to the json format.
// var key,
// jsonKey;
// crypto.subtle.generateKey({name: "AES-GCM",length: 256},
// (true), ["encrypt", "decrypt"])
// .then(function(res){key = res;});
//
// window.crypto.subtle.exportKey("jwk", key)
// .then(function(res){jsonKey = val})
//
//var storage = jIO.createJIO({type: "crypt", key: jsonKey,
// sub_storage: {...}});
// find more informations about this cryptography system on
// https://github.com/diafygi/webcrypto-examples#aes-gcm
/**
* The JIO Cryptography Storage extension
*
* @class CryptStorage
* @constructor
*/
var MIME_TYPE = "application/x-jio-aes-gcm-encryption";
function CryptStorage(spec) {
this._key = spec.key;
this._jsonKey = true;
this._sub_storage = jIO.createJIO(spec.sub_storage);
}
function convertKey(that) {
return new RSVP.Queue()
.push(function () {
return crypto.subtle.importKey("jwk", that._key,
"AES-GCM", false,
["encrypt", "decrypt"]);
})
.push(function (res) {
that._key = res;
that._jsonKey = false;
return;
});
}
CryptStorage.prototype.get = function () {
return this._sub_storage.get.apply(this._sub_storage,
arguments);
};
CryptStorage.prototype.post = function () {
return this._sub_storage.post.apply(this._sub_storage,
arguments);
};
CryptStorage.prototype.put = function () {
return this._sub_storage.put.apply(this._sub_storage,
arguments);
};
CryptStorage.prototype.remove = function () {
return this._sub_storage.remove.apply(this._sub_storage,
arguments);
};
CryptStorage.prototype.hasCapacity = function () {
return this._sub_storage.hasCapacity.apply(this._sub_storage,
arguments);
};
CryptStorage.prototype.buildQuery = function () {
return this._sub_storage.buildQuery.apply(this._sub_storage,
arguments);
};
CryptStorage.prototype.putAttachment = function (id, name, blob) {
var initializaton_vector = crypto.getRandomValues(new Uint8Array(12)),
that = this;
return new RSVP.Queue()
.push(function () {
if (that._jsonKey === true) {
return convertKey(that);
}
return;
})
.push(function () {
return jIO.util.readBlobAsDataURL(blob);
})
.push(function (dataURL) {
//string->arraybuffer
var strLen = dataURL.currentTarget.result.length,
buf = new ArrayBuffer(strLen),
bufView = new Uint8Array(buf),
i;
dataURL = dataURL.currentTarget.result;
for (i = 0; i < strLen; i += 1) {
bufView[i] = dataURL.charCodeAt(i);
}
return crypto.subtle.encrypt({
name : "AES-GCM",
iv : initializaton_vector
},
that._key, buf);
})
.push(function (coded) {
var blob = new Blob([initializaton_vector, coded], {type: MIME_TYPE});
return that._sub_storage.putAttachment(id, name, blob);
});
};
CryptStorage.prototype.getAttachment = function (id, name) {
var that = this;
return that._sub_storage.getAttachment(id, name)
.push(function (blob) {
if (blob.type !== MIME_TYPE) {
return blob;
}
return new RSVP.Queue()
.push(function () {
if (that._jsonKey === true) {
return convertKey(that);
}
return;
})
.push(function () {
return jIO.util.readBlobAsArrayBuffer(blob);
})
.push(function (coded) {
var initializaton_vector;
coded = coded.currentTarget.result;
initializaton_vector = new Uint8Array(coded.slice(0, 12));
return new RSVP.Queue()
.push(function () {
return crypto.subtle.decrypt({
name : "AES-GCM",
iv : initializaton_vector
},
that._key, coded.slice(12));
})
.push(function (arr) {
//arraybuffer->string
arr = String.fromCharCode.apply(null, new Uint8Array(arr));
return jIO.util.dataURItoBlob(arr);
})
.push(undefined, function (error) {
if (error instanceof DOMException) {
return blob;
}
throw error;
});
});
});
};
CryptStorage.prototype.removeAttachment = function () {
return this._sub_storage.removeAttachment.apply(this._sub_storage,
arguments);
};
CryptStorage.prototype.allAttachments = function () {
return this._sub_storage.allAttachments.apply(this._sub_storage,
arguments);
};
jIO.addStorage('crypt', CryptStorage);
}(jIO, RSVP, DOMException, Blob, crypto, Uint8Array, ArrayBuffer));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/davstorage.js 0000664 0000000 0000000 00000021425 13037644422 0030234 0 ustar 00root root 0000000 0000000 /*
* Copyright 2013, Nexedi SA
* Released under the LGPL license.
* http://www.gnu.org/licenses/lgpl.html
*/
/*jslint nomen: true*/
/*global jIO, RSVP, DOMParser, Blob */
// JIO Dav Storage Description :
// {
// type: "dav",
// url: {string},
// basic_login: {string} // Basic authentication
// }
// NOTE: to get the authentication type ->
// curl --verbose -X OPTION http://domain/
// In the headers: "WWW-Authenticate: Basic realm="DAV-upload"
(function (jIO, RSVP, DOMParser, Blob) {
"use strict";
function ajax(storage, options) {
if (options === undefined) {
options = {};
}
if (storage._authorization !== undefined) {
if (options.headers === undefined) {
options.headers = {};
}
options.headers.Authorization = storage._authorization;
}
if (storage._with_credentials !== undefined) {
if (options.xhrFields === undefined) {
options.xhrFields = {};
}
options.xhrFields.withCredentials = storage._with_credentials;
}
// if (start !== undefined) {
// if (end !== undefined) {
// headers.Range = "bytes=" + start + "-" + end;
// } else {
// headers.Range = "bytes=" + start + "-";
// }
// }
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax(options);
});
}
function restrictDocumentId(id) {
if (id.indexOf("/") !== 0) {
throw new jIO.util.jIOError("id " + id + " is forbidden (no begin /)",
400);
}
if (id.lastIndexOf("/") !== (id.length - 1)) {
throw new jIO.util.jIOError("id " + id + " is forbidden (no end /)",
400);
}
return id;
}
function restrictAttachmentId(id) {
if (id.indexOf("/") !== -1) {
throw new jIO.util.jIOError("attachment " + id + " is forbidden",
400);
}
}
/**
* The JIO WebDAV Storage extension
*
* @class DavStorage
* @constructor
*/
function DavStorage(spec) {
if (typeof spec.url !== 'string') {
throw new TypeError("DavStorage 'url' is not of type string");
}
this._url = spec.url;
// XXX digest login
if (typeof spec.basic_login === 'string') {
this._authorization = "Basic " + spec.basic_login;
}
this._with_credentials = spec.with_credentials;
}
DavStorage.prototype.put = function (id, param) {
var that = this;
id = restrictDocumentId(id);
if (Object.getOwnPropertyNames(param).length > 0) {
// Reject if param has some properties
throw new jIO.util.jIOError("Can not store properties: " +
Object.getOwnPropertyNames(param), 400);
}
return new RSVP.Queue()
.push(function () {
return ajax(that, {
type: "MKCOL",
url: that._url + id
});
})
.push(undefined, function (err) {
if ((err.target !== undefined) &&
(err.target.status === 405)) {
return;
}
throw err;
});
};
DavStorage.prototype.remove = function (id) {
id = restrictDocumentId(id);
return ajax(this, {
type: "DELETE",
url: this._url + id
});
};
DavStorage.prototype.get = function (id) {
var context = this;
id = restrictDocumentId(id);
return new RSVP.Queue()
.push(function () {
return ajax(context, {
type: "PROPFIND",
url: context._url + id,
dataType: "text",
headers: {
// Increasing this value is a performance killer
Depth: "1"
}
});
})
.push(function () {
return {};
}, function (error) {
if ((error.target !== undefined) &&
(error.target.status === 404)) {
throw new jIO.util.jIOError("Cannot find document", 404);
}
throw error;
});
};
DavStorage.prototype.allAttachments = function (id) {
var context = this;
id = restrictDocumentId(id);
return new RSVP.Queue()
.push(function () {
return ajax(context, {
type: "PROPFIND",
url: context._url + id,
dataType: "text",
headers: {
// Increasing this value is a performance killer
Depth: "1"
}
});
})
.push(function (response) {
// Extract all meta informations and return them to JSON
var i,
attachment = {},
id,
attachment_list = new DOMParser().parseFromString(
response.target.responseText,
"text/xml"
).querySelectorAll(
"D\\:response, response"
);
// exclude parent folder and browse
for (i = 1; i < attachment_list.length; i += 1) {
// XXX Only get files for now
id = attachment_list[i].querySelector("D\\:href, href").
textContent.split('/').slice(-1)[0];
// XXX Ugly
if ((id !== undefined) && (id !== "")) {
attachment[id] = {};
}
}
return attachment;
}, function (error) {
if ((error.target !== undefined) &&
(error.target.status === 404)) {
throw new jIO.util.jIOError("Cannot find document", 404);
}
throw error;
});
};
DavStorage.prototype.putAttachment = function (id, name, blob) {
var that = this;
id = restrictDocumentId(id);
restrictAttachmentId(name);
return new RSVP.Queue()
.push(function () {
return ajax(that, {
type: "PUT",
url: that._url + id + name,
data: blob
});
})
.push(undefined, function (error) {
if (error.target.status === 403 || error.target.status === 424) {
throw new jIO.util.jIOError("Cannot access subdocument", 404);
}
throw error;
});
};
DavStorage.prototype.getAttachment = function (id, name) {
var context = this;
id = restrictDocumentId(id);
restrictAttachmentId(name);
return new RSVP.Queue()
.push(function () {
return ajax(context, {
type: "GET",
url: context._url + id + name,
dataType: "blob"
});
})
.push(function (response) {
return new Blob(
[response.target.response || response.target.responseText],
{"type": response.target.getResponseHeader('Content-Type') ||
"application/octet-stream"}
);
}, function (error) {
if ((error.target !== undefined) &&
(error.target.status === 404)) {
throw new jIO.util.jIOError("Cannot find attachment: "
+ id + " , " + name,
404);
}
throw error;
});
};
DavStorage.prototype.removeAttachment = function (id, name) {
var context = this;
id = restrictDocumentId(id);
restrictAttachmentId(name);
return new RSVP.Queue()
.push(function () {
return ajax(context, {
type: "DELETE",
url: context._url + id + name
});
})
.push(undefined, function (error) {
if ((error.target !== undefined) &&
(error.target.status === 404)) {
throw new jIO.util.jIOError("Cannot find attachment: "
+ id + " , " + name,
404);
}
throw error;
});
};
// JIO COMMANDS //
// wedDav methods rfc4918 (short summary)
// COPY Reproduces single resources (files) and collections (directory
// trees). Will overwrite files (if specified by request) but will
// respond 209 (Conflict) if it would overwrite a tree
// DELETE deletes files and directory trees
// GET just the vanilla HTTP/1.1 behaviour
// HEAD ditto
// LOCK locks a resources
// MKCOL creates a directory
// MOVE Moves (rename or copy) a file or a directory tree. Will
// 'overwrite' files (if specified by the request) but will respond
// 209 (Conflict) if it would overwrite a tree.
// OPTIONS If WebDAV is enabled and available for the path this reports the
// WebDAV extension methods
// PROPFIND Retrieves the requested file characteristics, DAV lock status
// and 'dead' properties for individual files, a directory and its
// child files, or a directory tree
// PROPPATCHset and remove 'dead' meta-data properties
// PUT Update or create resource or collections
// UNLOCK unlocks a resource
// Notes: all Ajax requests should be CORS (cross-domain)
// adding custom headers triggers preflight OPTIONS request!
// http://remysharp.com/2011/04/21/getting-cors-working/
jIO.addStorage('dav', DavStorage);
}(jIO, RSVP, DOMParser, Blob));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/documentstorage.js 0000664 0000000 0000000 00000015754 13037644422 0031310 0 ustar 00root root 0000000 0000000 /*jslint nomen: true*/
/*global Blob, atob, btoa, RSVP*/
(function (jIO, Blob, atob, btoa, RSVP) {
"use strict";
/**
* The jIO DocumentStorage extension
*
* @class DocumentStorage
* @constructor
*/
function DocumentStorage(spec) {
this._sub_storage = jIO.createJIO(spec.sub_storage);
this._document_id = spec.document_id;
this._repair_attachment = spec.repair_attachment || false;
}
var DOCUMENT_EXTENSION = ".json",
DOCUMENT_REGEXP = new RegExp("^jio_document/([\\w=]+)" +
DOCUMENT_EXTENSION + "$"),
ATTACHMENT_REGEXP = new RegExp("^jio_attachment/([\\w=]+)/([\\w=]+)$");
function getSubAttachmentIdFromParam(id, name) {
if (name === undefined) {
return 'jio_document/' + btoa(id) + DOCUMENT_EXTENSION;
}
return 'jio_attachment/' + btoa(id) + "/" + btoa(name);
}
DocumentStorage.prototype.get = function (id) {
return this._sub_storage.getAttachment(
this._document_id,
getSubAttachmentIdFromParam(id),
{format: "json"}
);
};
DocumentStorage.prototype.allAttachments = function (id) {
return this._sub_storage.allAttachments(this._document_id)
.push(function (result) {
var attachments = {},
exec,
key;
for (key in result) {
if (result.hasOwnProperty(key)) {
if (ATTACHMENT_REGEXP.test(key)) {
exec = ATTACHMENT_REGEXP.exec(key);
try {
if (atob(exec[1]) === id) {
attachments[atob(exec[2])] = {};
}
} catch (error) {
// Check if unable to decode base64 data
if (!error instanceof ReferenceError) {
throw error;
}
}
}
}
}
return attachments;
});
};
DocumentStorage.prototype.put = function (doc_id, param) {
return this._sub_storage.putAttachment(
this._document_id,
getSubAttachmentIdFromParam(doc_id),
new Blob([JSON.stringify(param)], {type: "application/json"})
)
.push(function () {
return doc_id;
});
};
DocumentStorage.prototype.remove = function (id) {
var context = this;
return this.allAttachments(id)
.push(function (result) {
var key,
promise_list = [];
for (key in result) {
if (result.hasOwnProperty(key)) {
promise_list.push(context.removeAttachment(id, key));
}
}
return RSVP.all(promise_list);
})
.push(function () {
return context._sub_storage.removeAttachment(
context._document_id,
getSubAttachmentIdFromParam(id)
);
})
.push(function () {
return id;
});
};
DocumentStorage.prototype.repair = function () {
var context = this;
return this._sub_storage.repair.apply(this._sub_storage, arguments)
.push(function (result) {
if (context._repair_attachment) {
return context._sub_storage.allAttachments(context._document_id)
.push(function (result_dict) {
var promise_list = [],
id_dict = {},
attachment_dict = {},
id,
attachment,
exec,
key;
for (key in result_dict) {
if (result_dict.hasOwnProperty(key)) {
id = undefined;
attachment = undefined;
if (DOCUMENT_REGEXP.test(key)) {
try {
id = atob(DOCUMENT_REGEXP.exec(key)[1]);
} catch (error) {
// Check if unable to decode base64 data
if (!error instanceof ReferenceError) {
throw error;
}
}
if (id !== undefined) {
id_dict[id] = null;
}
} else if (ATTACHMENT_REGEXP.test(key)) {
exec = ATTACHMENT_REGEXP.exec(key);
try {
id = atob(exec[1]);
attachment = atob(exec[2]);
} catch (error) {
// Check if unable to decode base64 data
if (!error instanceof ReferenceError) {
throw error;
}
}
if (attachment !== undefined) {
if (!id_dict.hasOwnProperty(id)) {
if (!attachment_dict.hasOwnProperty(id)) {
attachment_dict[id] = {};
}
attachment_dict[id][attachment] = null;
}
}
}
}
}
for (id in attachment_dict) {
if (attachment_dict.hasOwnProperty(id)) {
if (!id_dict.hasOwnProperty(id)) {
for (attachment in attachment_dict[id]) {
if (attachment_dict[id].hasOwnProperty(attachment)) {
promise_list.push(context.removeAttachment(
id,
attachment
));
}
}
}
}
}
return RSVP.all(promise_list);
});
}
return result;
});
};
DocumentStorage.prototype.hasCapacity = function (capacity) {
return (capacity === "list");
};
DocumentStorage.prototype.buildQuery = function () {
return this._sub_storage.allAttachments(this._document_id)
.push(function (attachment_dict) {
var result = [],
key;
for (key in attachment_dict) {
if (attachment_dict.hasOwnProperty(key)) {
if (DOCUMENT_REGEXP.test(key)) {
try {
result.push({
id: atob(DOCUMENT_REGEXP.exec(key)[1]),
value: {}
});
} catch (error) {
// Check if unable to decode base64 data
if (!error instanceof ReferenceError) {
throw error;
}
}
}
}
}
return result;
});
};
DocumentStorage.prototype.getAttachment = function (id, name) {
return this._sub_storage.getAttachment(
this._document_id,
getSubAttachmentIdFromParam(id, name)
);
};
DocumentStorage.prototype.putAttachment = function (id, name, blob) {
return this._sub_storage.putAttachment(
this._document_id,
getSubAttachmentIdFromParam(id, name),
blob
);
};
DocumentStorage.prototype.removeAttachment = function (id, name) {
return this._sub_storage.removeAttachment(
this._document_id,
getSubAttachmentIdFromParam(id, name)
);
};
jIO.addStorage('document', DocumentStorage);
}(jIO, Blob, atob, btoa, RSVP));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/drivetojiomapping.js 0000664 0000000 0000000 00000015452 13037644422 0031632 0 ustar 00root root 0000000 0000000 /*jslint nomen: true*/
/*global RSVP, Blob*/
(function (jIO, RSVP, Blob) {
"use strict";
/**
* The jIO FileSystemBridgeStorage extension
*
* @class FileSystemBridgeStorage
* @constructor
*/
function FileSystemBridgeStorage(spec) {
this._sub_storage = jIO.createJIO(spec.sub_storage);
}
var DOCUMENT_EXTENSION = ".json",
DOCUMENT_KEY = "/.jio_documents/",
ROOT = "/";
function endsWith(str, suffix) {
return str.indexOf(suffix, str.length - suffix.length) !== -1;
}
FileSystemBridgeStorage.prototype.get = function (id) {
var context = this;
return new RSVP.Queue()
// First, try to get explicit reference to the document
.push(function () {
// First get the document itself if it exists
return context._sub_storage.getAttachment(
DOCUMENT_KEY,
id + DOCUMENT_EXTENSION,
{format: "json"}
);
})
.push(undefined, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
// Second, try to get default attachment
return context._sub_storage.allAttachments(ROOT)
.push(function (attachment_dict) {
if (attachment_dict.hasOwnProperty(id)) {
return {};
}
throw new jIO.util.jIOError("Cannot find document " + id,
404);
});
}
throw error;
});
};
FileSystemBridgeStorage.prototype.allAttachments = function (id) {
var context = this;
return context._sub_storage.allAttachments(ROOT)
.push(function (attachment_dict) {
if (attachment_dict.hasOwnProperty(id)) {
return {
enclosure: {}
};
}
// Second get the document itself if it exists
return context._sub_storage.getAttachment(
DOCUMENT_KEY,
id + DOCUMENT_EXTENSION
)
.push(function () {
return {};
}, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
throw new jIO.util.jIOError("Cannot find document " + id,
404);
}
throw error;
});
});
};
FileSystemBridgeStorage.prototype.put = function (doc_id, param) {
var context = this;
// XXX Handle conflict!
return context._sub_storage.putAttachment(
DOCUMENT_KEY,
doc_id + DOCUMENT_EXTENSION,
new Blob([JSON.stringify(param)], {type: "application/json"})
)
.push(undefined, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return context._sub_storage.put(DOCUMENT_KEY, {})
.push(function () {
return context._sub_storage.putAttachment(
DOCUMENT_KEY,
doc_id + DOCUMENT_EXTENSION,
new Blob([JSON.stringify(param)],
{type: "application/json"})
);
});
}
throw error;
})
.push(function () {
return doc_id;
});
};
FileSystemBridgeStorage.prototype.remove = function (doc_id) {
var context = this,
got_error = false;
return new RSVP.Queue()
// First, try to remove enclosure
.push(function () {
return context._sub_storage.removeAttachment(
ROOT,
doc_id
);
})
.push(undefined, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
got_error = true;
return;
}
throw error;
})
// Second, try to remove explicit doc
.push(function () {
return context._sub_storage.removeAttachment(
DOCUMENT_KEY,
doc_id + DOCUMENT_EXTENSION
);
})
.push(undefined, function (error) {
if ((!got_error) && (error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return doc_id;
}
throw error;
});
};
FileSystemBridgeStorage.prototype.hasCapacity = function (capacity) {
return (capacity === "list");
};
FileSystemBridgeStorage.prototype.buildQuery = function () {
var result_dict = {},
context = this;
return new RSVP.Queue()
// First, get list of explicit documents
.push(function () {
return context._sub_storage.allAttachments(DOCUMENT_KEY);
})
.push(function (result) {
var key;
for (key in result) {
if (result.hasOwnProperty(key)) {
if (endsWith(key, DOCUMENT_EXTENSION)) {
result_dict[key.substring(
0,
key.length - DOCUMENT_EXTENSION.length
)] = null;
}
}
}
}, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return;
}
throw error;
})
// Second, get list of enclosure
.push(function () {
return context._sub_storage.allAttachments(ROOT);
})
.push(function (result) {
var key;
for (key in result) {
if (result.hasOwnProperty(key)) {
result_dict[key] = null;
}
}
})
// Finally, build the result
.push(function () {
var result = [],
key;
for (key in result_dict) {
if (result_dict.hasOwnProperty(key)) {
result.push({
id: key,
value: {}
});
}
}
return result;
});
};
FileSystemBridgeStorage.prototype.getAttachment = function (id, name) {
if (name !== "enclosure") {
throw new jIO.util.jIOError("Only support 'enclosure' attachment",
400);
}
return this._sub_storage.getAttachment(ROOT, id);
};
FileSystemBridgeStorage.prototype.putAttachment = function (id, name, blob) {
if (name !== "enclosure") {
throw new jIO.util.jIOError("Only support 'enclosure' attachment",
400);
}
return this._sub_storage.putAttachment(
ROOT,
id,
blob
);
};
FileSystemBridgeStorage.prototype.removeAttachment = function (id, name) {
if (name !== "enclosure") {
throw new jIO.util.jIOError("Only support 'enclosure' attachment",
400);
}
return this._sub_storage.removeAttachment(ROOT, id);
};
FileSystemBridgeStorage.prototype.repair = function () {
return this._sub_storage.repair.apply(this._sub_storage, arguments);
};
jIO.addStorage('drivetojiomapping', FileSystemBridgeStorage);
}(jIO, RSVP, Blob));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/dropboxstorage.js 0000664 0000000 0000000 00000017637 13037644422 0031151 0 ustar 00root root 0000000 0000000 /*
* Copyright 2013, Nexedi SA
* Released under the LGPL license.
* http://www.gnu.org/licenses/lgpl.html
*/
/**
* JIO Dropbox Storage. Type = "dropbox".
* Dropbox "database" storage.
*/
/*global Blob, jIO, RSVP, UriTemplate*/
/*jslint nomen: true*/
(function (jIO, RSVP, Blob, UriTemplate) {
"use strict";
var UPLOAD_URL = "https://content.dropboxapi.com/1/files_put/" +
"{+root}{+id}{+name}{?access_token}",
upload_template = UriTemplate.parse(UPLOAD_URL),
CREATE_DIR_URL = "https://api.dropboxapi.com/1/fileops/create_folder" +
"{?access_token,root,path}",
create_dir_template = UriTemplate.parse(CREATE_DIR_URL),
REMOVE_URL = "https://api.dropboxapi.com/1/fileops/delete/" +
"{?access_token,root,path}",
remote_template = UriTemplate.parse(REMOVE_URL),
GET_URL = "https://content.dropboxapi.com/1/files" +
"{/root,id}{+name}{?access_token}",
get_template = UriTemplate.parse(GET_URL),
//LIST_URL = 'https://api.dropboxapi.com/1/metadata/sandbox/';
METADATA_URL = "https://api.dropboxapi.com/1/metadata" +
"{/root}{+id}{?access_token}",
metadata_template = UriTemplate.parse(METADATA_URL);
function restrictDocumentId(id) {
if (id.indexOf("/") !== 0) {
throw new jIO.util.jIOError("id " + id + " is forbidden (no begin /)",
400);
}
if (id.lastIndexOf("/") !== (id.length - 1)) {
throw new jIO.util.jIOError("id " + id + " is forbidden (no end /)",
400);
}
return id;
}
function restrictAttachmentId(id) {
if (id.indexOf("/") !== -1) {
throw new jIO.util.jIOError("attachment " + id + " is forbidden",
400);
}
}
/**
* The JIO Dropbox Storage extension
*
* @class DropboxStorage
* @constructor
*/
function DropboxStorage(spec) {
if (typeof spec.access_token !== 'string' || !spec.access_token) {
throw new TypeError("Access Token' must be a string " +
"which contains more than one character.");
}
if (typeof spec.root !== 'string' || !spec.root ||
(spec.root !== "dropbox" && spec.root !== "sandbox")) {
throw new TypeError("root must be 'dropbox' or 'sandbox'");
}
this._access_token = spec.access_token;
this._root = spec.root;
}
DropboxStorage.prototype.put = function (id, param) {
var that = this;
id = restrictDocumentId(id);
if (Object.getOwnPropertyNames(param).length > 0) {
// Reject if param has some properties
throw new jIO.util.jIOError("Can not store properties: " +
Object.getOwnPropertyNames(param), 400);
}
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
type: "POST",
url: create_dir_template.expand({
access_token: that._access_token,
root: that._root,
path: id
})
});
})
.push(undefined, function (err) {
if ((err.target !== undefined) &&
(err.target.status === 405)) {
// Directory already exists, no need to fail
return;
}
throw err;
});
};
DropboxStorage.prototype.remove = function (id) {
id = restrictDocumentId(id);
return jIO.util.ajax({
type: "POST",
url: remote_template.expand({
access_token: this._access_token,
root: this._root,
path: id
})
});
};
DropboxStorage.prototype.get = function (id) {
var that = this;
if (id === "/") {
return {};
}
id = restrictDocumentId(id);
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
type: "GET",
url: metadata_template.expand({
access_token: that._access_token,
root: that._root,
id: id
})
});
})
.push(function (evt) {
var obj = JSON.parse(evt.target.response ||
evt.target.responseText);
if (obj.is_dir) {
return {};
}
throw new jIO.util.jIOError("Not a directory: " + id, 404);
}, function (error) {
if (error.target !== undefined && error.target.status === 404) {
throw new jIO.util.jIOError("Cannot find document: " + id, 404);
}
throw error;
});
};
DropboxStorage.prototype.allAttachments = function (id) {
var that = this;
id = restrictDocumentId(id);
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
type: "GET",
url: metadata_template.expand({
access_token: that._access_token,
root: that._root,
id: id
})
});
})
.push(function (evt) {
var obj = JSON.parse(evt.target.response || evt.target.responseText),
i,
result = {};
if (!obj.is_dir) {
throw new jIO.util.jIOError("Not a directory: " + id, 404);
}
for (i = 0; i < obj.contents.length; i += 1) {
if (!obj.contents[i].is_dir) {
result[obj.contents[i].path.split("/").pop()] = {};
}
}
return result;
}, function (error) {
if (error.target !== undefined && error.target.status === 404) {
throw new jIO.util.jIOError("Cannot find document: " + id, 404);
}
throw error;
});
};
//currently, putAttachment will fail with files larger than 150MB,
//due to the Dropbox API. the API provides the "chunked_upload" method
//to pass this limit, but upload process becomes more complex to implement.
//
//putAttachment will also create a folder if you try to put an attachment
//to an inexisting foler.
DropboxStorage.prototype.putAttachment = function (id, name, blob) {
id = restrictDocumentId(id);
restrictAttachmentId(name);
return jIO.util.ajax({
type: "PUT",
url: upload_template.expand({
root: this._root,
id: id,
name: name,
access_token: this._access_token
}),
dataType: blob.type,
data: blob
});
};
DropboxStorage.prototype.getAttachment = function (id, name) {
var that = this;
id = restrictDocumentId(id);
restrictAttachmentId(name);
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
type: "GET",
dataType: "blob",
url: get_template.expand({
root: that._root,
id: id,
name: name,
access_token: that._access_token
})
});
})
.push(function (evt) {
return new Blob(
[evt.target.response || evt.target.responseText],
{"type": evt.target.getResponseHeader('Content-Type') ||
"application/octet-stream"}
);
}, function (error) {
if (error.target !== undefined && error.target.status === 404) {
throw new jIO.util.jIOError("Cannot find attachment: " +
id + ", " + name, 404);
}
throw error;
});
};
//removeAttachment removes also directories.(due to Dropbox API)
DropboxStorage.prototype.removeAttachment = function (id, name) {
var that = this;
id = restrictDocumentId(id);
restrictAttachmentId(name);
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
type: "POST",
url: remote_template.expand({
access_token: that._access_token,
root: that._root,
path: id + name
})
});
}).push(undefined, function (error) {
if (error.target !== undefined && error.target.status === 404) {
throw new jIO.util.jIOError("Cannot find attachment: " +
id + ", " + name, 404);
}
throw error;
});
};
jIO.addStorage('dropbox', DropboxStorage);
}(jIO, RSVP, Blob, UriTemplate));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/erp5storage.js 0000664 0000000 0000000 00000042515 13037644422 0030340 0 ustar 00root root 0000000 0000000 /*
* Copyright 2013, Nexedi SA
* Released under the LGPL license.
* http://www.gnu.org/licenses/lgpl.html
*/
// JIO ERP5 Storage Description :
// {
// type: "erp5"
// url: {string}
// }
/*jslint nomen: true, unparam: true */
/*global jIO, UriTemplate, FormData, RSVP, URI, Blob,
SimpleQuery, ComplexQuery*/
(function (jIO, UriTemplate, FormData, RSVP, URI, Blob,
SimpleQuery, ComplexQuery) {
"use strict";
function getSiteDocument(storage) {
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
"type": "GET",
"url": storage._url,
"xhrFields": {
withCredentials: true
}
});
})
.push(function (event) {
return JSON.parse(event.target.responseText);
});
}
function getDocumentAndHateoas(storage, id, options) {
if (options === undefined) {
options = {};
}
return getSiteDocument(storage)
.push(function (site_hal) {
// XXX need to get modified metadata
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
"type": "GET",
"url": UriTemplate.parse(site_hal._links.traverse.href)
.expand({
relative_url: id,
view: options._view
}),
"xhrFields": {
withCredentials: true
}
});
})
.push(undefined, function (error) {
if ((error.target !== undefined) &&
(error.target.status === 404)) {
throw new jIO.util.jIOError("Cannot find document: " + id, 404);
}
throw error;
});
});
}
var allowed_field_dict = {
"StringField": null,
"EmailField": null,
"IntegerField": null,
"FloatField": null,
"TextAreaField": null
};
function extractPropertyFromFormJSON(json) {
return new RSVP.Queue()
.push(function () {
var form = json._embedded._view,
converted_json = {
portal_type: json._links.type.name
},
form_data_json = {},
field,
key,
prefix_length,
result;
if (json._links.hasOwnProperty('parent')) {
converted_json.parent_relative_url =
new URI(json._links.parent.href).segment(2);
}
form_data_json.form_id = {
"key": [form.form_id.key],
"default": form.form_id["default"]
};
// XXX How to store datetime
for (key in form) {
if (form.hasOwnProperty(key)) {
field = form[key];
prefix_length = 0;
if (key.indexOf('my_') === 0 && field.editable) {
prefix_length = 3;
}
if (key.indexOf('your_') === 0) {
prefix_length = 5;
}
if ((prefix_length !== 0) &&
(allowed_field_dict.hasOwnProperty(field.type))) {
form_data_json[key.substring(prefix_length)] = {
"default": field["default"],
"key": field.key
};
converted_json[key.substring(prefix_length)] = field["default"];
}
}
}
result = {
data: converted_json,
form_data: form_data_json
};
if (form.hasOwnProperty('_actions') &&
form._actions.hasOwnProperty('put')) {
result.action_href = form._actions.put.href;
}
return result;
});
}
function extractPropertyFromForm(context, id) {
return context.getAttachment(id, "view")
.push(function (blob) {
return jIO.util.readBlobAsText(blob);
})
.push(function (evt) {
return JSON.parse(evt.target.result);
})
.push(function (json) {
return extractPropertyFromFormJSON(json);
});
}
// XXX docstring
function ERP5Storage(spec) {
if (typeof spec.url !== "string" || !spec.url) {
throw new TypeError("ERP5 'url' must be a string " +
"which contains more than one character.");
}
this._url = spec.url;
this._default_view_reference = spec.default_view_reference;
}
function convertJSONToGet(json) {
var key,
result = json.data;
// Remove all ERP5 hateoas links / convert them into jIO ID
for (key in result) {
if (result.hasOwnProperty(key)) {
if (!result[key]) {
delete result[key];
}
}
}
return result;
}
ERP5Storage.prototype.get = function (id) {
return extractPropertyFromForm(this, id)
.push(function (result) {
return convertJSONToGet(result);
});
};
ERP5Storage.prototype.bulk = function (request_list) {
var i,
storage = this,
bulk_list = [];
for (i = 0; i < request_list.length; i += 1) {
if (request_list[i].method !== "get") {
throw new Error("ERP5Storage: not supported " +
request_list[i].method + " in bulk");
}
bulk_list.push({
relative_url: request_list[i].parameter_list[0],
view: storage._default_view_reference
});
}
return getSiteDocument(storage)
.push(function (site_hal) {
var form_data = new FormData();
form_data.append("bulk_list", JSON.stringify(bulk_list));
return jIO.util.ajax({
"type": "POST",
"url": site_hal._actions.bulk.href,
"data": form_data,
// "headers": {
// "Content-Type": "application/json"
// },
"xhrFields": {
withCredentials: true
}
});
})
.push(function (response) {
var result_list = [],
hateoas = JSON.parse(response.target.responseText);
function pushResult(json) {
return extractPropertyFromFormJSON(json)
.push(function (json2) {
return convertJSONToGet(json2);
});
}
for (i = 0; i < hateoas.result_list.length; i += 1) {
result_list.push(pushResult(hateoas.result_list[i]));
}
return RSVP.all(result_list);
});
};
ERP5Storage.prototype.post = function (data) {
var context = this,
new_id;
return getSiteDocument(this)
.push(function (site_hal) {
var form_data = new FormData();
form_data.append("portal_type", data.portal_type);
form_data.append("parent_relative_url", data.parent_relative_url);
return jIO.util.ajax({
type: "POST",
url: site_hal._actions.add.href,
data: form_data,
xhrFields: {
withCredentials: true
}
});
})
.push(function (evt) {
var location = evt.target.getResponseHeader("X-Location"),
uri = new URI(location);
new_id = uri.segment(2);
return context.put(new_id, data);
})
.push(function () {
return new_id;
});
};
ERP5Storage.prototype.put = function (id, data) {
var context = this;
return extractPropertyFromForm(context, id)
.push(function (result) {
var key,
json = result.form_data,
form_data = {};
form_data[json.form_id.key] = json.form_id["default"];
// XXX How to store datetime:!!!!!
for (key in data) {
if (data.hasOwnProperty(key)) {
if (key === "form_id") {
throw new jIO.util.jIOError(
"ERP5: forbidden property: " + key,
400
);
}
if ((key !== "portal_type") && (key !== "parent_relative_url")) {
if (!json.hasOwnProperty(key)) {
throw new jIO.util.jIOError(
"ERP5: can not store property: " + key,
400
);
}
form_data[json[key].key] = data[key];
}
}
}
if (!result.hasOwnProperty('action_href')) {
throw new jIO.util.jIOError(
"ERP5: can not modify document: " + id,
403
);
}
return context.putAttachment(
id,
result.action_href,
new Blob([JSON.stringify(form_data)], {type: "application/json"})
);
});
};
ERP5Storage.prototype.allAttachments = function (id) {
var context = this;
return getDocumentAndHateoas(this, id)
.push(function () {
if (context._default_view_reference === undefined) {
return {
links: {}
};
}
return {
view: {},
links: {}
};
});
};
ERP5Storage.prototype.getAttachment = function (id, action, options) {
if (options === undefined) {
options = {};
}
if (action === "view") {
if (this._default_view_reference === undefined) {
throw new jIO.util.jIOError(
"Cannot find attachment view for: " + id,
404
);
}
return getDocumentAndHateoas(this, id,
{"_view": this._default_view_reference})
.push(function (response) {
var result = JSON.parse(response.target.responseText);
// Remove all ERP5 hateoas links / convert them into jIO ID
// XXX Change default action to an jio urn with attachment name inside
// if Base_edit, do put URN
// if others, do post URN (ie, unique new attachment name)
// XXX Except this attachment name should be generated when
return new Blob(
[JSON.stringify(result)],
{"type": 'application/hal+json'}
);
});
}
if (action === "links") {
return getDocumentAndHateoas(this, id)
.push(function (response) {
return new Blob(
[JSON.stringify(JSON.parse(response.target.responseText))],
{"type": 'application/hal+json'}
);
});
}
if (action.indexOf(this._url) === 0) {
return new RSVP.Queue()
.push(function () {
var start,
end,
range,
request_options = {
"type": "GET",
"dataType": "blob",
"url": action,
"xhrFields": {
withCredentials: true
}
};
if (options.start !== undefined || options.end !== undefined) {
start = options.start || 0;
end = options.end;
if (end !== undefined && end < 0) {
throw new jIO.util.jIOError("end must be positive",
400);
}
if (start < 0) {
range = "bytes=" + start;
} else if (end === undefined) {
range = "bytes=" + start + "-";
} else {
if (start > end) {
throw new jIO.util.jIOError("start is greater than end",
400);
}
range = "bytes=" + start + "-" + end;
}
request_options.headers = {Range: range};
}
return jIO.util.ajax(request_options);
})
.push(function (evt) {
if (evt.target.response === undefined) {
return new Blob(
[evt.target.responseText],
{"type": evt.target.getResponseHeader("Content-Type")}
);
}
return evt.target.response;
});
}
throw new jIO.util.jIOError("ERP5: not support get attachment: " + action,
400);
};
ERP5Storage.prototype.putAttachment = function (id, name, blob) {
// Assert we use a callable on a document from the ERP5 site
if (name.indexOf(this._url) !== 0) {
throw new jIO.util.jIOError("Can not store outside ERP5: " +
name, 400);
}
return new RSVP.Queue()
.push(function () {
return jIO.util.readBlobAsText(blob);
})
.push(function (evt) {
var form_data = JSON.parse(evt.target.result),
data = new FormData(),
array,
i,
key,
value;
for (key in form_data) {
if (form_data.hasOwnProperty(key)) {
if (Array.isArray(form_data[key])) {
array = form_data[key];
} else {
array = [form_data[key]];
}
for (i = 0; i < array.length; i += 1) {
value = array[i];
if (typeof value === "object") {
data.append(key, jIO.util.dataURItoBlob(value.url),
value.file_name);
} else {
data.append(key, value);
}
}
}
}
return jIO.util.ajax({
"type": "POST",
"url": name,
"data": data,
"xhrFields": {
withCredentials: true
}
});
});
};
ERP5Storage.prototype.hasCapacity = function (name) {
return ((name === "list") || (name === "query") ||
(name === "select") || (name === "limit") ||
(name === "sort"));
};
function isSingleLocalRoles(parsed_query) {
if ((parsed_query instanceof SimpleQuery) &&
(parsed_query.key === 'local_roles')) {
// local_roles:"Assignee"
return parsed_query.value;
}
}
function isMultipleLocalRoles(parsed_query) {
var i,
sub_query,
is_multiple = true,
local_role_list = [];
if ((parsed_query instanceof ComplexQuery) &&
(parsed_query.operator === 'OR')) {
for (i = 0; i < parsed_query.query_list.length; i += 1) {
sub_query = parsed_query.query_list[i];
if ((sub_query instanceof SimpleQuery) &&
(sub_query.key === 'local_roles')) {
local_role_list.push(sub_query.value);
} else {
is_multiple = false;
}
}
if (is_multiple) {
// local_roles:"Assignee" OR local_roles:"Assignor"
return local_role_list;
}
}
}
ERP5Storage.prototype.buildQuery = function (options) {
// if (typeof options.query !== "string") {
// options.query = (options.query ?
// jIO.Query.objectToSearchText(options.query) :
// undefined);
// }
return getSiteDocument(this)
.push(function (site_hal) {
var query = options.query,
i,
parsed_query,
sub_query,
result_list,
local_roles,
sort_list = [];
if (options.query) {
parsed_query = jIO.QueryFactory.create(options.query);
result_list = isSingleLocalRoles(parsed_query);
if (result_list) {
query = undefined;
local_roles = result_list;
} else {
result_list = isMultipleLocalRoles(parsed_query);
if (result_list) {
query = undefined;
local_roles = result_list;
} else if ((parsed_query instanceof ComplexQuery) &&
(parsed_query.operator === 'AND')) {
// portal_type:"Person" AND local_roles:"Assignee"
for (i = 0; i < parsed_query.query_list.length; i += 1) {
sub_query = parsed_query.query_list[i];
result_list = isSingleLocalRoles(sub_query);
if (result_list) {
local_roles = result_list;
parsed_query.query_list.splice(i, 1);
query = jIO.Query.objectToSearchText(parsed_query);
i = parsed_query.query_list.length;
} else {
result_list = isMultipleLocalRoles(sub_query);
if (result_list) {
local_roles = result_list;
parsed_query.query_list.splice(i, 1);
query = jIO.Query.objectToSearchText(parsed_query);
i = parsed_query.query_list.length;
}
}
}
}
}
}
if (options.sort_on) {
for (i = 0; i < options.sort_on.length; i += 1) {
sort_list.push(JSON.stringify(options.sort_on[i]));
}
}
return jIO.util.ajax({
"type": "GET",
"url": UriTemplate.parse(site_hal._links.raw_search.href)
.expand({
query: query,
// XXX Force erp5 to return embedded document
select_list: options.select_list || ["title", "reference"],
limit: options.limit,
sort_on: sort_list,
local_roles: local_roles
}),
"xhrFields": {
withCredentials: true
}
});
})
.push(function (response) {
return JSON.parse(response.target.responseText);
})
.push(function (catalog_json) {
var data = catalog_json._embedded.contents,
count = data.length,
i,
uri,
item,
result = [];
for (i = 0; i < count; i += 1) {
item = data[i];
uri = new URI(item._links.self.href);
delete item._links;
result.push({
id: uri.segment(2),
value: item
});
}
return result;
});
};
jIO.addStorage("erp5", ERP5Storage);
}(jIO, UriTemplate, FormData, RSVP, URI, Blob,
SimpleQuery, ComplexQuery));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/gdrivestorage.js 0000664 0000000 0000000 00000016062 13037644422 0030743 0 ustar 00root root 0000000 0000000 /*
* Copyright 2015, Nexedi SA
* Released under the LGPL license.
* http://www.gnu.org/licenses/lgpl.html
*/
/**
* JIO Google Drive Storage. Type = "gdrive".
* Google Drive "database" storage.
*/
/*global jIO, Blob, RSVP, UriTemplate, JSON*/
/*jslint nomen: true*/
(function (jIO, Blob, RSVP, UriTemplate, JSON) {
"use strict";
var UPLOAD_URL = "https://www.googleapis.com{/upload}/drive/v2/files{/id}" +
"{?uploadType,access_token}",
upload_template = UriTemplate.parse(UPLOAD_URL),
REMOVE_URL = "https://www.googleapis.com/drive/v2/" +
"files{/id,trash}{?access_token}",
remove_template = UriTemplate.parse(REMOVE_URL),
LIST_URL = "https://www.googleapis.com/drive/v2/files" +
"?prettyPrint=false{&pageToken}&q=trashed=false" +
"&fields=nextPageToken,items(id){&access_token}",
list_template = UriTemplate.parse(LIST_URL),
GET_URL = "https://www.googleapis.com/drive/v2/files{/id}{?alt}",
get_template = UriTemplate.parse(GET_URL);
function handleError(error, id) {
if (error.target.status === 404) {
throw new jIO.util.jIOError(
"Cannot find document: " + id,
404
);
}
throw error;
}
function listPage(result, token) {
var i,
obj;
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
"type": "GET",
"url": list_template.expand({
pageToken : (result.nextPageToken || ""),
access_token: token
})
});
})
.push(function (data) {
obj = JSON.parse(data.target.response || data.target.responseText);
for (i = 0; i < obj.items.length; i += 1) {
obj.items[i].value = {};
result.push(obj.items[i]);
}
result.nextPageToken = obj.nextPageToken;
return result;
}, handleError);
}
function checkName(name) {
if (name !== "enclosure") {
throw new jIO.util.jIOError("Only support 'enclosure' attachment", 400);
}
}
/**
* The JIO Google Drive Storage extension
*
* @class GdriveStorage
* @constructor
*/
function GdriveStorage(spec) {
if (spec === undefined || spec.access_token === undefined ||
typeof spec.access_token !== 'string') {
throw new TypeError("Access Token must be a string " +
"which contains more than one character.");
}
if (spec.trashing !== undefined &&
(spec.trashing !== true && spec.trashing !== false)) {
throw new TypeError("trashing parameter" +
" must be a boolean (true or false)");
}
this._trashing = spec.trashing || true;
this._access_token = spec.access_token;
return;
}
function recursiveAllDocs(result, accessToken) {
return new RSVP.Queue()
.push(function () {
return listPage(result, accessToken);
})
.push(function () {
if (result.nextPageToken) {
return recursiveAllDocs(result, accessToken);
}
return result;
});
}
GdriveStorage.prototype.hasCapacity = function (name) {
return (name === "list");
};
GdriveStorage.prototype.buildQuery = function () {
return recursiveAllDocs([], this._access_token);
};
function sendMetaData(id, param, token) {
var boundary = "-------314159265358979323846";
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
"type": id ? "PUT" : "POST",
"url": upload_template.expand({
access_token: token,
id: id || [],
upload: id ? [] : "upload",
uploadType: "multipart"
}),
headers: {
"Content-Type" : 'multipart/related; boundary="' + boundary + '"'
},
data: '--' + boundary + '\n' +
'Content-Type: application/json; charset=UTF-8\n\n' +
JSON.stringify(param) + '\n\n--' + boundary + "--"
});
})
.push(function (result) {
var obj = JSON.parse(result.target.responseText);
return obj.id;
},
function (error) {handleError(error, id); });
}
GdriveStorage.prototype.put = function (id, param) {
return sendMetaData(id, param, this._access_token);
};
GdriveStorage.prototype.post = function (param) {
return sendMetaData(undefined, param, this._access_token);
};
function sendData(id, blob, token) {
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
"type": "PUT",
"url": upload_template.expand({
access_token: token,
upload: "upload",
id: id,
uploadType: "media"
}),
data: blob
});
})
.push(function (data) {
data = JSON.parse(data.target.responseText);
if (data.mimeType === "application/vnd.google-apps.folder") {
throw new jIO.util.jIOError("cannot put attachments to folder", 400);
}
return data;
}, function (error) {handleError(error, id); });
}
GdriveStorage.prototype.putAttachment = function (id, name, blob) {
checkName(name);
return sendData(id, blob, this._access_token);
};
GdriveStorage.prototype.remove = function (id) {
var that = this;
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
type: that._trashing ? "POST" : "DELETE",
url: remove_template.expand({
id : id,
access_token : that._access_token,
trash : that._trashing ? "trash" : []
})
});
})
.push(undefined, function (error) {handleError(error, id); });
};
function getData(id, attach, token) {
return new RSVP.Queue()
.push(function () {
return jIO.util.ajax({
type: "GET",
dataType: attach ? "blob" : "json",
url: get_template.expand({
id: id,
alt: attach ? "media" : [],
access_token: token
}),
headers: {
"Authorization" : "Bearer " + token
}
});
})
.push(function (evt) {
return evt.target.response ||
(attach ? new Blob([evt.target.responseText],
{"type" :
evt.target.responseHeaders["Content-Type"]}) :
JSON.parse(evt.target.responseText));
}, function (error) {handleError(error, id); });
}
GdriveStorage.prototype.get = function (id) {
return getData(id, false, this._access_token);
};
GdriveStorage.prototype.getAttachment = function (id, name) {
checkName(name);
return getData(id, true, this._access_token);
};
GdriveStorage.prototype.allAttachments = function (id) {
var token = this._access_token;
return new RSVP.Queue()
.push(function () {
return getData(id, false, token);
})
.push(function (data) {
if (data.mimeType === "application/vnd.google-apps.folder") {
return {};
}
return {"enclosure": {}};
});
};
jIO.addStorage('gdrive', GdriveStorage);
}(jIO, Blob, RSVP, UriTemplate, JSON));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/gidstorage.js 0000664 0000000 0000000 00000044333 13037644422 0030230 0 ustar 00root root 0000000 0000000 /*
* JIO extension for resource global identifier management.
* Copyright (C) 2013 Nexedi SA
*
* This library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*/
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global define, jIO */
/**
* JIO GID Storage. Type = 'gid'.
* Identifies document with their global identifier representation
*
* Sub storages must support queries and include_docs options.
*
* Storage Description:
*
* {
* "type": "gid",
* "sub_storage": {},
* "constraints": {
* "default": {
* "identifier": "list", // ['a', 1]
* "type": "DCMIType", // 'Text'
* "title": "string" // 'something blue'
* },
* "Text": {
* "format": "contentType" // contains 'text/plain;charset=utf-8'
* },
* "Image": {
* "version": "json" // value as is
* }
* }
* }
*/
// define([module_name], [dependencies], module);
(function (dependencies, module) {
"use strict";
if (typeof define === 'function' && define.amd) {
return define(dependencies, module);
}
module(jIO);
}(['jio'], function (jIO) {
"use strict";
var dcmi_types, metadata_actions, content_type_re, tool;
dcmi_types = {
'Collection': 'Collection',
'Dataset': 'Dataset',
'Event': 'Event',
'Image': 'Image',
'InteractiveResource': 'InteractiveResource',
'MovingImage': 'MovingImage',
'PhysicalObject': 'PhysicalObject',
'Service': 'Service',
'Software': 'Software',
'Sound': 'Sound',
'StillImage': 'StillImage',
'Text': 'Text'
};
metadata_actions = {
/**
* Returns the metadata value
*/
json: function (value) {
return value;
},
/**
* Returns the metadata if there is a string
*/
string: function (value) {
if (!Array.isArray(value)) {
if (typeof value === 'object') {
return value.content;
}
return value;
}
},
/**
* Returns the metadata in a array format
*/
list: function (value) {
var i, new_value = [];
if (Array.isArray(value)) {
for (i = 0; i < value.length; i += 1) {
if (typeof value[i] === 'object') {
new_value[new_value.length] = value[i].content;
} else {
new_value[new_value.length] = value[i];
}
}
} else if (value !== undefined) {
value = [value];
}
return value;
},
/**
* Returns the metadata if there is a string equal to a DCMIType
*/
DCMIType: function (value) {
var i;
if (!Array.isArray(value)) {
value = [value];
}
for (i = 0; i < value.length; i += 1) {
if (typeof value[i] === 'object' && dcmi_types[value[i].content]) {
return value[i].content;
}
if (dcmi_types[value[i]]) {
return value[i];
}
}
},
/**
* Returns the metadata content type if exist
*/
contentType: function (value) {
var i;
if (!Array.isArray(value)) {
value = [value];
}
for (i = 0; i < value.length; i += 1) {
if (value[i] === 'object') {
if (content_type_re.test(value[i].content)) {
return value[i].content;
}
} else {
if (content_type_re.test(value[i])) {
return value[i];
}
}
}
},
/**
* Returns the metadata if it is a date
*/
date: function (value) {
var d;
if (!Array.isArray(value)) {
if (typeof value === 'object') {
d = new Date(value.content);
value = value.content;
} else {
d = new Date(value);
}
}
if (Object.prototype.toString.call(d) === "[object Date]") {
if (!isNaN(d.getTime())) {
return value;
}
}
}
};
content_type_re = new RegExp(
'^([a-z]+\\/[a-zA-Z0-9\\+\\-\\.]+)' +
'((?:\\s*;\\s*[a-zA-Z\\+\\-\\.]+\\s*=' +
'\\s*[a-zA-Z0-9\\-\\+\\.,]+)*)$'
);
tool = {
"deepClone": jIO.util.deepClone
};
/**
* Creates a gid from metadata and constraints.
*
* @param {Object} metadata The metadata to use
* @param {Object} constraints The constraints
* @return {String} The gid or undefined if metadata doesn't respect the
* constraints
*/
function gidFormat(metadata, constraints) {
var types, i, j, meta_key, result = [], tmp, constraint, actions;
types = (metadata_actions.list(metadata.type) || []).slice();
types.unshift('default');
for (i = 0; i < types.length; i += 1) {
constraint = constraints[types[i]];
for (meta_key in constraint) {
if (constraint.hasOwnProperty(meta_key)) {
actions = constraint[meta_key];
if (!Array.isArray(actions)) {
actions = [actions];
}
for (j = 0; j < actions.length; j += 1) {
tmp = metadata_actions[
actions[j]
](metadata[meta_key]);
if (tmp === undefined) {
return;
}
}
result[result.length] = [meta_key, tmp];
}
}
}
// sort dict keys to make gid universal
result.sort(function (a, b) {
return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
});
tmp = {};
for (i = 0; i < result.length; i += 1) {
tmp[result[i][0]] = result[i][1];
}
return JSON.stringify(tmp);
}
/**
* Convert a gid to a jio query.
*
* @param {Object,String} gid The gid
* @return {Object} A jio serialized query
*/
function gidToJIOQuery(gid) {
var k, i, result = [], meta, content;
if (typeof gid === 'string') {
gid = JSON.parse(gid);
}
for (k in gid) {
if (gid.hasOwnProperty(k)) {
meta = gid[k];
if (!Array.isArray(meta)) {
meta = [meta];
}
for (i = 0; i < meta.length; i += 1) {
content = meta[i];
if (typeof content === 'object') {
content = content.content;
}
result[result.length] = {
"type": "simple",
"operator": "=",
"key": k,
"value": content
};
}
}
}
return {
"type": "complex",
"operator": "AND",
"query_list": result
};
}
/**
* Parse the gid and returns a metadata object containing gid keys and values.
*
* @param {String} gid The gid to convert
* @param {Object} constraints The constraints
* @return {Object} The gid metadata
*/
function gidParse(gid, constraints) {
var object;
try {
object = JSON.parse(gid);
} catch (e) {
return;
}
if (gid !== gidFormat(object, constraints)) {
return;
}
return object;
}
/**
* The gid storage used by JIO.
*
* This storage change the id of a document with its global id. A global id
* is representation of a document metadata used to define it as uniq. The way
* to generate global ids can be define in the storage description. It allows
* us use duplicating storage with different sub storage kind.
*
* @class GidStorage
*/
function GidStorage(spec) {
var that = this, priv = {};
priv.sub_storage = spec.sub_storage;
priv.constraints = spec.constraints || {
"default": {
"type": "DCMIType",
"title": "string"
}
};
// JIO Commands
/**
* Generic command for post or put one.
*
* This command will check if the document already exist with an allDocs
* and a jio query. If exist, then post will fail. Put will update the
* retrieved document thanks to its real id. If no documents are found, post
* and put will create a new document with the sub storage id generator.
*
* @method putOrPost
* @private
* @param {Command} command The JIO command
* @param {String} method The command method
*/
priv.putOrPost = function (command, metadata, method) {
var gid, jio_query, doc = tool.deepClone(metadata);
gid = gidFormat(doc, priv.constraints);
if (gid === undefined || (doc._id && gid !== doc._id)) {
return command.error(
"bad_request",
"metadata should respect constraints",
"Cannot " + method + " document"
);
}
jio_query = gidToJIOQuery(gid);
command.storage(priv.sub_storage).allDocs({
"query": jio_query
}).then(function (response) {
var update_method = method;
response = response.data;
if (response.total_rows !== 0) {
if (method === 'post') {
return command.error(
"conflict",
"Document already exists",
"Cannot " + method + " document"
);
}
doc = tool.deepClone(metadata);
doc._id = response.rows[0].id;
} else {
doc = tool.deepClone(metadata);
delete doc._id;
update_method = 'post';
}
command.storage(priv.sub_storage)[update_method](
doc
).then(function (response) {
response.id = gid;
command.success(response);
}, function (err) {
err.message = "Cannot " + method + " document";
command.error(err);
});
}, function (err) {
err.message = "Cannot " + method + " document";
command.error(err);
});
};
/**
* Generic command for putAttachment, getAttachment or removeAttachment.
*
* This command will check if the document exist with an allDocs and a
* jio query. If not exist, then it returns 404. Otherwise the
* action will be done on the attachment of the found document.
*
* @method putGetOrRemoveAttachment
* @private
* @param {Object} command The JIO command
* @param {Object} doc The command parameters
* @param {String} method The command method
*/
priv.putGetOrRemoveAttachment = function (command, doc, method) {
var gid_object, jio_query;
gid_object = gidParse(doc._id, priv.constraints);
if (gid_object === undefined) {
return command.error(
"bad_request",
"metadata should respect constraints",
"Cannot " + method + " attachment"
);
}
jio_query = gidToJIOQuery(gid_object);
command.storage(priv.sub_storage).allDocs({
"query": jio_query
}).then(function (response) {
response = response.data;
if (response.total_rows === 0) {
return command.error(
"not_found",
"Document already exists",
"Cannot " + method + " attachment"
);
}
gid_object = doc._id;
doc._id = response.rows[0].id;
command.storage(priv.sub_storage)[method + "Attachment"](
doc
).then(function (response) {
response.id = gid_object;
command.success(response);
}, function (err) {
err.message = "Cannot " + method + " attachment";
command.error(err);
});
}, function (err) {
err.message = "Cannot " + method + " attachment";
command.error(err);
});
};
/**
* See {{#crossLink "gidStorage/putOrPost:method"}}{{/#crossLink}}.
*
* @method post
* @param {Command} command The JIO command
*/
that.post = function (command, metadata) {
priv.putOrPost(command, metadata, 'post');
};
/**
* See {{#crossLink "gidStorage/putOrPost:method"}}{{/#crossLink}}.
*
* @method put
* @param {Command} command The JIO command
*/
that.put = function (command, metadata) {
priv.putOrPost(command, metadata, 'put');
};
/**
* Puts an attachment to a document thank to its gid, a sub allDocs and a
* jio query.
*
* @method putAttachment
* @param {Command} command The JIO command
*/
that.putAttachment = function (command, param) {
priv.putGetOrRemoveAttachment(command, param, 'put');
};
/**
* Gets a document thank to its gid, a sub allDocs and a jio query.
*
* @method get
* @param {Object} command The JIO command
*/
that.get = function (command, param) {
var gid_object, jio_query;
gid_object = gidParse(param._id, priv.constraints);
if (gid_object === undefined) {
return command.error(
"bad_request",
"metadata should respect constraints",
"Cannot get document"
);
}
jio_query = gidToJIOQuery(gid_object);
command.storage(priv.sub_storage).allDocs({
"query": jio_query,
"include_docs": true
}).then(function (response) {
response = response.data;
if (response.total_rows === 0) {
return command.error(
"not_found",
"missing",
"Cannot get document"
);
}
response.rows[0].doc._id = param._id;
return command.success({"data": response.rows[0].doc});
}, function (err) {
err.message = "Cannot get document";
return command.error(err);
});
};
/**
* Gets an attachment from a document thank to its gid, a sub allDocs and a
* jio query.
*
* @method getAttachment
* @param {Command} command The JIO command
*/
that.getAttachment = function (command, param) {
priv.putGetOrRemoveAttachment(command, param, 'get');
};
/**
* Remove a document thank to its gid, sub allDocs and a jio query.
*
* @method remove
* @param {Command} command The JIO command.
*/
that.remove = function (command, doc) {
var gid_object, jio_query;
gid_object = gidParse(doc._id, priv.constraints);
if (gid_object === undefined) {
return command.error(
"bad_request",
"metadata should respect constraints",
"Cannot remove document"
);
}
jio_query = gidToJIOQuery(gid_object);
command.storage(priv.sub_storage).allDocs({
"query": jio_query
}).then(function (response) {
response = response.data;
if (response.total_rows === 0) {
return command.error(
"not_found",
"missing",
"Cannot remove document"
);
}
gid_object = doc._id;
doc = {"_id": response.rows[0].id};
command.storage(priv.sub_storage).remove(
doc
).then(function (response) {
response.id = gid_object;
command.success(response);
}, function (err) {
err.message = "Cannot remove document";
command.error(err);
});
}, function (err) {
err.message = "Cannot remove document";
command.error(err);
});
};
/**
* Removes an attachment to a document thank to its gid, a sub allDocs and a
* jio query.
*
* @method removeAttachment
* @param {Command} command The JIO command
*/
that.removeAttachment = function (command, param) {
priv.putGetOrRemoveAttachment(command, param, 'remove');
};
/**
* Retrieve a list of document which respect gid constraints.
*
* @method allDocs
* @param {Command} command The JIO command
*/
that.allDocs = function (command, param, options) {
/*jslint unparam: true */
var include_docs;
include_docs = options.include_docs;
options.include_docs = true;
command.storage(priv.sub_storage).allDocs(
options
).then(function (response) {
/*jslint ass: true */
var result = [], doc_gids = {}, row, gid;
response = response.data;
while ((row = response.rows.shift()) !== undefined) {
gid = gidFormat(row.doc, priv.constraints);
if (gid !== undefined) {
if (!doc_gids[gid]) {
doc_gids[gid] = true;
row.id = gid;
delete row.key;
result[result.length] = row;
if (include_docs === true) {
row.doc._id = gid;
} else {
delete row.doc;
}
}
}
}
doc_gids = undefined; // free memory
row = undefined;
command.success({"data": {
"total_rows": result.length,
"rows": result
}});
}, function (err) {
err.message = "Cannot get all documents";
return command.error(err);
});
};
that.check = function (command, param, options) {
return that.repair(command, param, options, "check");
};
that.repair = function (command, param, options, action) {
var gid_object, jio_query, sub_storage;
if (typeof param._id !== "string" || !param._id) {
return command.error("bad_request", "document id must be provided");
}
if (action === undefined) {
action = "repair";
}
gid_object = gidParse(param._id, priv.constraints);
if (gid_object === undefined) {
return command.error(
"bad_request",
"metadata should respect constraints",
"Cannot " + action + " document"
);
}
jio_query = gidToJIOQuery(gid_object);
sub_storage = command.storage(priv.sub_storage);
sub_storage.allDocs({
"query": jio_query
}).then(function (response) {
response = response.data;
if (response.total_rows === 0) {
// document not found, nothing to repair or check
return command.success();
}
return sub_storage[action]({"_id": response.rows[0].id}, options);
}).then(command.success, command.error, command.notify);
};
}
jIO.addStorage('gid', GidStorage);
}));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/indexeddbstorage.js 0000664 0000000 0000000 00000032466 13037644422 0031417 0 ustar 00root root 0000000 0000000 /*
* Copyright 2014, Nexedi SA
* Released under the LGPL license.
* http://www.gnu.org/licenses/lgpl.html
*/
/**
* JIO Indexed Database Storage.
*
* A local browser "database" storage greatly more powerful than localStorage.
*
* Description:
*
* {
* "type": "indexeddb",
* "database":
* }
*
* The database name will be prefixed by "jio:", so if the database property is
* "hello", then you can manually reach this database with
* `indexedDB.open("jio:hello");`. (Or
* `indexedDB.deleteDatabase("jio:hello");`.)
*
* For more informations:
*
* - http://www.w3.org/TR/IndexedDB/
* - https://developer.mozilla.org/en-US/docs/IndexedDB/Using_IndexedDB
*/
/*jslint nomen: true */
/*global indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange, IDBOpenDBRequest,
DOMError, Event*/
(function (indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange, IDBOpenDBRequest,
DOMError) {
"use strict";
// Read only as changing it can lead to data corruption
var UNITE = 2000000;
function IndexedDBStorage(description) {
if (typeof description.database !== "string" ||
description.database === "") {
throw new TypeError("IndexedDBStorage 'database' description property " +
"must be a non-empty string");
}
this._database_name = "jio:" + description.database;
}
IndexedDBStorage.prototype.hasCapacity = function (name) {
return ((name === "list") || (name === "include"));
};
function buildKeyPath(key_list) {
return key_list.join("_");
}
function handleUpgradeNeeded(evt) {
var db = evt.target.result,
store;
store = db.createObjectStore("metadata", {
keyPath: "_id",
autoIncrement: false
});
// It is not possible to use openKeyCursor on keypath directly
// https://www.w3.org/Bugs/Public/show_bug.cgi?id=19955
store.createIndex("_id", "_id", {unique: true});
store = db.createObjectStore("attachment", {
keyPath: "_key_path",
autoIncrement: false
});
store.createIndex("_id", "_id", {unique: false});
store = db.createObjectStore("blob", {
keyPath: "_key_path",
autoIncrement: false
});
store.createIndex("_id_attachment",
["_id", "_attachment"], {unique: false});
store.createIndex("_id", "_id", {unique: false});
}
function openIndexedDB(jio_storage) {
var db_name = jio_storage._database_name;
function resolver(resolve, reject) {
// Open DB //
var request = indexedDB.open(db_name);
request.onerror = function (error) {
if (request.result) {
request.result.close();
}
if ((error !== undefined) &&
(error.target instanceof IDBOpenDBRequest) &&
(error.target.error instanceof DOMError)) {
reject("Connection to: " + db_name + " failed: " +
error.target.error.message);
} else {
reject(error);
}
};
request.onabort = function () {
request.result.close();
reject("Aborting connection to: " + db_name);
};
request.ontimeout = function () {
request.result.close();
reject("Connection to: " + db_name + " timeout");
};
request.onblocked = function () {
request.result.close();
reject("Connection to: " + db_name + " was blocked");
};
// Create DB if necessary //
request.onupgradeneeded = handleUpgradeNeeded;
request.onversionchange = function () {
request.result.close();
reject(db_name + " was upgraded");
};
request.onsuccess = function () {
resolve(request.result);
};
}
// XXX Canceller???
return new RSVP.Queue()
.push(function () {
return new RSVP.Promise(resolver);
});
}
function openTransaction(db, stores, flag, autoclosedb) {
var tx = db.transaction(stores, flag);
if (autoclosedb !== false) {
tx.oncomplete = function () {
db.close();
};
}
tx.onabort = function () {
db.close();
};
return tx;
}
function handleCursor(request, callback) {
function resolver(resolve, reject) {
// Open DB //
request.onerror = function (error) {
if (request.transaction) {
request.transaction.abort();
}
reject(error);
};
request.onsuccess = function (evt) {
var cursor = evt.target.result;
if (cursor) {
// XXX Wait for result
try {
callback(cursor);
} catch (error) {
reject(error);
}
// continue to next iteration
cursor["continue"]();
} else {
resolve();
}
};
}
// XXX Canceller???
return new RSVP.Promise(resolver);
}
IndexedDBStorage.prototype.buildQuery = function (options) {
var result_list = [];
function pushIncludedMetadata(cursor) {
result_list.push({
"id": cursor.key,
"value": {},
"doc": cursor.value.doc
});
}
function pushMetadata(cursor) {
result_list.push({
"id": cursor.key,
"value": {}
});
}
return openIndexedDB(this)
.push(function (db) {
var tx = openTransaction(db, ["metadata"], "readonly");
if (options.include_docs === true) {
return handleCursor(tx.objectStore("metadata").index("_id")
.openCursor(), pushIncludedMetadata);
}
return handleCursor(tx.objectStore("metadata").index("_id")
.openKeyCursor(), pushMetadata);
})
.push(function () {
return result_list;
});
};
function handleGet(request) {
function resolver(resolve, reject) {
request.onerror = reject;
request.onsuccess = function () {
if (request.result) {
resolve(request.result);
}
// XXX How to get ID
reject(new jIO.util.jIOError("Cannot find document", 404));
};
}
return new RSVP.Promise(resolver);
}
IndexedDBStorage.prototype.get = function (id) {
return openIndexedDB(this)
.push(function (db) {
var transaction = openTransaction(db, ["metadata"],
"readonly");
return handleGet(transaction.objectStore("metadata").get(id));
})
.push(function (result) {
return result.doc;
});
};
IndexedDBStorage.prototype.allAttachments = function (id) {
var attachment_dict = {};
function addEntry(cursor) {
attachment_dict[cursor.value._attachment] = {};
}
return openIndexedDB(this)
.push(function (db) {
var transaction = openTransaction(db, ["metadata", "attachment"],
"readonly");
return RSVP.all([
handleGet(transaction.objectStore("metadata").get(id)),
handleCursor(transaction.objectStore("attachment").index("_id")
.openCursor(IDBKeyRange.only(id)), addEntry)
]);
})
.push(function () {
return attachment_dict;
});
};
function handleRequest(request) {
function resolver(resolve, reject) {
request.onerror = reject;
request.onsuccess = function () {
resolve(request.result);
};
}
return new RSVP.Promise(resolver);
}
IndexedDBStorage.prototype.put = function (id, metadata) {
return openIndexedDB(this)
.push(function (db) {
var transaction = openTransaction(db, ["metadata"], "readwrite");
return handleRequest(transaction.objectStore("metadata").put({
"_id": id,
"doc": metadata
}));
});
};
function deleteEntry(cursor) {
cursor["delete"]();
}
IndexedDBStorage.prototype.remove = function (id) {
return openIndexedDB(this)
.push(function (db) {
var transaction = openTransaction(db, ["metadata", "attachment",
"blob"], "readwrite");
return RSVP.all([
handleRequest(transaction
.objectStore("metadata")["delete"](id)),
// XXX Why not possible to delete with KeyCursor?
handleCursor(transaction.objectStore("attachment").index("_id")
.openCursor(IDBKeyRange.only(id)), deleteEntry),
handleCursor(transaction.objectStore("blob").index("_id")
.openCursor(IDBKeyRange.only(id)), deleteEntry)
]);
});
};
IndexedDBStorage.prototype.getAttachment = function (id, name, options) {
var transaction,
type,
start,
end;
if (options === undefined) {
options = {};
}
return openIndexedDB(this)
.push(function (db) {
transaction = openTransaction(db, ["attachment", "blob"], "readonly");
// XXX Should raise if key is not good
return handleGet(transaction.objectStore("attachment")
.get(buildKeyPath([id, name])));
})
.push(function (attachment) {
var total_length = attachment.info.length,
i,
promise_list = [],
store = transaction.objectStore("blob"),
start_index,
end_index;
type = attachment.info.content_type;
start = options.start || 0;
end = options.end || total_length;
if (end > total_length) {
end = total_length;
}
if (start < 0 || end < 0) {
throw new jIO.util.jIOError("_start and _end must be positive",
400);
}
if (start > end) {
throw new jIO.util.jIOError("_start is greater than _end",
400);
}
start_index = Math.floor(start / UNITE);
end_index = Math.floor(end / UNITE);
if (end % UNITE === 0) {
end_index -= 1;
}
for (i = start_index; i <= end_index; i += 1) {
promise_list.push(
handleGet(store.get(buildKeyPath([id,
name, i])))
);
}
return RSVP.all(promise_list);
})
.push(function (result_list) {
var array_buffer_list = [],
blob,
i,
index,
len = result_list.length;
for (i = 0; i < len; i += 1) {
array_buffer_list.push(result_list[i].blob);
}
if ((options.start === undefined) && (options.end === undefined)) {
return new Blob(array_buffer_list, {type: type});
}
index = Math.floor(start / UNITE) * UNITE;
blob = new Blob(array_buffer_list, {type: "application/octet-stream"});
return blob.slice(start - index, end - index,
"application/octet-stream");
});
};
function removeAttachment(transaction, id, name) {
return RSVP.all([
// XXX How to get the right attachment
handleRequest(transaction.objectStore("attachment")["delete"](
buildKeyPath([id, name])
)),
handleCursor(transaction.objectStore("blob").index("_id_attachment")
.openCursor(IDBKeyRange.only(
[id, name]
)),
deleteEntry
)
]);
}
IndexedDBStorage.prototype.putAttachment = function (id, name, blob) {
var blob_part = [],
transaction,
db;
return openIndexedDB(this)
.push(function (database) {
db = database;
// Split the blob first
return jIO.util.readBlobAsArrayBuffer(blob);
})
.push(function (event) {
var array_buffer = event.target.result,
total_size = blob.size,
handled_size = 0;
while (handled_size < total_size) {
blob_part.push(array_buffer.slice(handled_size,
handled_size + UNITE));
handled_size += UNITE;
}
// Remove previous attachment
transaction = openTransaction(db, ["attachment", "blob"], "readwrite");
return removeAttachment(transaction, id, name);
})
.push(function () {
var promise_list = [
handleRequest(transaction.objectStore("attachment").put({
"_key_path": buildKeyPath([id, name]),
"_id": id,
"_attachment": name,
"info": {
"content_type": blob.type,
"length": blob.size
}
}))
],
len = blob_part.length,
blob_store = transaction.objectStore("blob"),
i;
for (i = 0; i < len; i += 1) {
promise_list.push(
handleRequest(blob_store.put({
"_key_path": buildKeyPath([id, name,
i]),
"_id" : id,
"_attachment" : name,
"_part" : i,
"blob": blob_part[i]
}))
);
}
// Store all new data
return RSVP.all(promise_list);
});
};
IndexedDBStorage.prototype.removeAttachment = function (id, name) {
return openIndexedDB(this)
.push(function (db) {
var transaction = openTransaction(db, ["attachment", "blob"],
"readwrite");
return removeAttachment(transaction, id, name);
});
};
jIO.addStorage("indexeddb", IndexedDBStorage);
}(indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange, IDBOpenDBRequest, DOMError));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/indexstorage.js 0000664 0000000 0000000 00000064115 13037644422 0030574 0 ustar 00root root 0000000 0000000 /*
* JIO extension for resource indexing.
* Copyright (C) 2013 Nexedi SA
*
* This library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*/
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true, regexp: true */
/*global window, exports, require, define, jIO, RSVP */
/**
* JIO Index Storage.
* Manages indexes for specified storages.
* Description:
* {
* "type": "index",
* "indices": [{
* "id": "index_title_subject.json", // doc id where to store indices
* "index": ["title", "subject"], // metadata to index
* "attachment": "youhou", // default "body"
* "metadata": { // default {}
* "type": "Dataset",
* "format": "application/json",
* "date": "yyyy-mm-ddTHH:MM:SS+HH:MM",
* "title": "My index database",
* "creator": "Me"
* },
* "sub_storage":
* (default equal to parent sub_storage field)
* }, {
* "id": "index_year.json",
* "index": "year"
* ...
* }],
* "sub_storage":
* }
*
* Sent document metadata will be:
* index_titre_subject.json
* {
* "_id": "index_title_subject.json",
* "type": "Dataset",
* "format": "application/json",
* "date": "yyyy-mm-ddTHH:MM:SS+HH:MM",
* "title": "My index database",
* "creator": "Me",
* "_attachments": {
* "youhou": {
* "length": Num,
* "digest": "XXX",
* "content_type": "application/json"
* }
* }
* }
* Attachment "youhou"
* {
* "indexing": ["title", "subject"],
* "free": [0],
* "location": {
* "foo": 1,
* "bar": 2,
* ...
* },
* "database": [
* {},
* {"_id": "foo", "title": "...", "subject": ...},
* {"_id": "bar", "title": "...", "subject": ...},
* ...
* ]
* }
*
* index_year.json
* {
* "_id": "index_year.json",
* "_attachments": {
* "body": {..}
* }
* }
* Attachment "body"
* {
* "indexing": ["year"],
* "free": [1],
* "location": {
* "foo": 0,
* "bar": 2,
* ...
* },
* "database": [
* {"_id": "foo", "year": "..."},
* {},
* {"_id": "bar", "year": "..."},
* ...
* ]
* }
*
* A put document will be indexed to the free location if exist, else it will be
* indexed at the end of the database. The document id will be indexed, also, in
* 'location' to quickly replace metadata.
*
* Only one or two loops are executed:
* - one to filter retrieved document list (no query -> no loop)
* - one to format the result to a JIO response
*/
// define([module_name], [dependencies], module);
(function (dependencies, module) {
"use strict";
if (typeof define === 'function' && define.amd) {
return define(dependencies, module);
}
if (typeof exports === 'object') {
return module(
exports,
require('jio'),
require('rsvp')
);
}
window.index_storage = {};
module(window.index_storage, jIO, RSVP);
}([
'exports',
'jio',
'rsvp'
], function (exports, jIO, RSVP) {
"use strict";
/**
* A JSON Index manipulator
*
* @class JSONIndex
* @constructor
*/
function JSONIndex(spec) {
var that = this;
spec = spec || {};
/**
* The document id
*
* @property _id
* @type String
*/
that._id = spec._id;
/**
* The attachment id
*
* @property _attachment
* @type String
*/
that._attachment = spec._attachment;
/**
* The array with metadata key to index
*
* @property _indexing
* @type Array
*/
that._indexing = spec.indexing || [];
/**
* The array of free location index
*
* @property _free
* @type Array
* @default []
*/
that._free = spec.free || [];
/**
* The dictionnary document id -> database index
*
* @property _location
* @type Object
* @default {}
*/
that._location = spec.location || {};
/**
* The database array containing document metadata
*
* @property _database
* @type Array
* @default []
*/
that._database = spec.database || [];
/**
* True if it has been modified
*
* @property modified
* @type Boolean
* @default false
*/
that.modified = false;
/**
* Updates the modified date
*
* @method touch
*/
that.touch = function () {
that.modified = true;
};
/**
* Adds a metadata object in the database, replace if already exist
*
* @method put
* @param {Object} meta The metadata to add
* @return {Boolean} true if added, false otherwise
*/
that.put = function (meta) {
var k, needed_meta = {}, ok = false;
if (typeof meta._id !== "string" || meta._id === "") {
throw new TypeError("Corrupted Metadata");
}
for (k in meta) {
if (meta.hasOwnProperty(k)) {
if (k[0] === "_") {
if (k === "_id") {
needed_meta[k] = meta[k];
}
} else if (that._indexing_object[k]) {
needed_meta[k] = meta[k];
ok = true;
}
}
}
if (ok) {
if (typeof that._location[meta._id] === "number") {
that._database[that._location[meta._id]] = needed_meta;
} else if (that._free.length > 0) {
k = that._free.shift();
that._database[k] = needed_meta;
that._location[meta._id] = k;
} else {
that._database.push(needed_meta);
that._location[meta._id] = that._database.length - 1;
}
that.modified = true;
return true;
}
if (typeof that._location[meta._id] === "number") {
return that.remove(meta);
}
return false;
};
/**
* Removes a metadata object from the database if exist
*
* @method remove
* @param {Object} meta The metadata to remove
* @return {Boolean} true if removed else false
*/
that.remove = function (meta) {
if (typeof meta._id !== "string") {
throw new TypeError("Corrupted Metadata");
}
if (typeof that._location[meta._id] !== "number") {
// throw new ReferenceError("Not Found");
return false;
}
that._database[that._location[meta._id]] = null;
that._free.push(that._location[meta._id]);
delete that._location[meta._id];
that.modified = true;
return true;
};
/**
* Checks if the index database document is correct
*
* @method check
*/
that.check = function () {
var id, database_meta;
if (typeof that._id !== "string" ||
that._id === "" ||
typeof that._attachment !== "string" ||
that._attachment === "" ||
!Array.isArray(that._free) ||
!Array.isArray(that._indexing) ||
typeof that._location !== 'object' ||
Array.isArray(that._location) ||
!Array.isArray(that._database) ||
that._indexing.length === 0) {
throw new TypeError("Corrupted Index");
}
for (id in that._location) {
if (that._location.hasOwnProperty(id)) {
database_meta = that._database[that._location[id]];
if (typeof database_meta !== 'object' ||
Object.getPrototypeOf(database_meta || []) !== Object.prototype ||
database_meta._id !== id) {
throw new TypeError("Corrupted Index");
}
}
}
};
that.equals = function (json_index) {
function equalsDirection(a, b) {
var k;
for (k in a._location) {
if (a._location.hasOwnProperty(k)) {
if (b._location[k] === undefined ||
JSON.stringify(b._database[b._location[k]]) !==
JSON.stringify(a._database[a._location[k]])) {
return false;
}
}
}
return true;
}
if (!equalsDirection(that, json_index)) {
return false;
}
if (!equalsDirection(json_index, that)) {
return false;
}
return true;
};
that.checkDocument = function (doc) {
var i, key, db_doc;
if (typeof that._location[doc._id] !== "number") {
throw new TypeError("Different Index");
}
db_doc = that._database(that._location[doc._id])._id;
if (db_doc !== doc._id) {
throw new TypeError("Different Index");
}
for (i = 0; i < that._indexing.length; i += 1) {
key = that._indexing[i];
if (doc[key] !== db_doc[key]) {
throw new TypeError("Different Index");
}
}
};
/**
* Recreates database indices and remove free space
*
* @method repair
*/
that.repair = function () {
var i = 0, meta;
that._free = [];
that._location = {};
if (!Array.isArray(that._database)) {
that._database = [];
}
while (i < that._database.length) {
meta = that._database[i];
if (typeof meta === 'object' &&
Object.getPrototypeOf(meta || []) === Object.prototype &&
typeof meta._id === "string" && meta._id !== "" &&
!that._location[meta._id]) {
that._location[meta._id] = i;
i += 1;
} else {
that._database.splice(i, 1);
}
}
that.modified = true;
return true;
};
/**
* Returns the serialized version of this object (not cloned)
*
* @method toJSON
* @return {Object} The serialized version
*/
that.toJSON = function () {
return {
"indexing": that._indexing,
"free": that._free,
"location": that._location,
"database": that._database
};
};
that.check();
that._indexing_object = {};
that._indexing.forEach(function (meta_key) {
that._indexing_object[meta_key] = true;
});
}
/**
* Return the similarity percentage (1 >= p >= 0) between two index lists.
*
* @param {Array} list_a An index list
* @param {Array} list_b Another index list
* @return {Number} The similarity percentage
*/
function similarityPercentage(list_a, list_b) {
var ai, bi, count = 0;
for (ai = 0; ai < list_a.length; ai += 1) {
for (bi = 0; bi < list_b.length; bi += 1) {
if (list_a[ai] === list_b[bi]) {
count += 1;
break;
}
}
}
return count / (list_a.length > list_b.length ?
list_a.length : list_b.length);
}
/**
* The JIO index storage constructor
*
* @class IndexStorage
* @constructor
*/
function IndexStorage(spec) {
var i;
if (!Array.isArray(spec.indices)) {
throw new TypeError("IndexStorage 'indices' must be an array of " +
"objects.");
}
this._indices = spec.indices;
if (typeof spec.sub_storage !== 'object' ||
Object.getPrototypeOf(spec.sub_storage || []) !== Object.prototype) {
throw new TypeError("IndexStorage 'sub_storage' must be a storage " +
"description.");
}
// check indices IDs
for (i = 0; i < this._indices.length; i += 1) {
if (typeof this._indices[i].id !== "string" ||
this._indices[i].id === "") {
throw new TypeError("IndexStorage " +
"'indices[x].id' must be a non empty string");
}
if (!Array.isArray(this._indices[i].index)) {
throw new TypeError("IndexStorage " +
"'indices[x].index' must be a string array");
}
}
this._sub_storage = spec.sub_storage;
}
/**
* Select the good index to use according to a select list.
*
* @method selectIndex
* @param {Array} select_list An array of strings
* @return {Number} The index index
*/
IndexStorage.prototype.selectIndex = function (select_list) {
var i, tmp, selector = {"index": 0, "similarity": 0};
for (i = 0; i < this._indices.length; i += 1) {
tmp = similarityPercentage(select_list, this._indices[i].index);
if (tmp > selector.similarity) {
selector.index = i;
selector.similarity = tmp;
}
}
return selector.index;
};
IndexStorage.prototype.getIndexDatabase = function (command, index) {
index = this._indices[index];
function makeNewIndex() {
var json_index = new JSONIndex({
"_id": index.id,
"_attachment": index.attachment || "body",
"indexing": index.index
});
json_index.touch();
return json_index;
}
return command.storage(
index.sub_storage || this._sub_storage
).getAttachment({
"_id": index.id,
"_attachment": index.attachment || "body"
}).then(function (response) {
return jIO.util.readBlobAsText(response.data);
}).then(function (e) {
try {
e = JSON.parse(e.target.result);
e._id = index.id;
e._attachment = index.attachment || "body";
} catch (e1) {
return makeNewIndex();
}
return new JSONIndex(e);
}, function (err) {
if (err.status === 404) {
return makeNewIndex();
// go back to fulfillment channel
}
throw err;
// propagate err
});
};
IndexStorage.prototype.getIndexDatabases = function (command) {
var i, promises = [];
for (i = 0; i < this._indices.length; i += 1) {
promises[promises.length] = this.getIndexDatabase(command, i);
}
return RSVP.all(promises);
};
IndexStorage.prototype.storeIndexDatabase = function (command, database,
index) {
var that = this;
if (!database.modified) {
return RSVP.resolve({"result": "success"});
}
index = this._indices[index];
function putAttachment() {
return command.storage(
index.sub_storage || that._sub_storage
).putAttachment({
"_id": index.id,
"_attachment": index.attachment || "body",
"_data": JSON.stringify(database),
"_content_type": "application/json"
});
}
function createDatabaseAndPutAttachmentIfPossible(err) {
var metadata;
if (err.status === 404) {
metadata = {"_id": index.id};
if (typeof index.metadata === 'object' &&
// adding metadata
index.metadata !== null &&
!Array.isArray(index.metadata)) {
metadata = jIO.util.dictUpdate(metadata, index.metadata);
}
return command.storage(
index.sub_storage || that._sub_storage
).post(metadata).then(putAttachment, null, function () {
throw null; // stop post progress propagation
});
}
throw err;
}
return putAttachment().
then(null, createDatabaseAndPutAttachmentIfPossible);
};
IndexStorage.prototype.storeIndexDatabases = function (command, databases) {
var i, promises = [];
for (i = 0; i < this._indices.length; i += 1) {
if (databases[i] !== undefined) {
promises[promises.length] =
this.storeIndexDatabase(command, databases[i], i);
}
}
return RSVP.all(promises);
};
/**
* Generic method for 'post', 'put', 'get' and 'remove'. It delegates the
* command to the sub storage and update the databases.
*
* @method genericCommand
* @param {String} method The method to use
* @param {Object} command The JIO command
* @param {Object} metadata The metadata to post
* @param {Object} option The command option
*/
IndexStorage.prototype.genericCommand = function (method, command,
metadata, option) {
var that = this, generic_response;
function updateAndStoreIndexDatabases(responses) {
var i, database_list = responses[0];
generic_response = responses[1];
if (method === 'get') {
jIO.util.dictUpdate(metadata, generic_response.data);
}
metadata._id = generic_response.id;
if (method === 'remove') {
for (i = 0; i < database_list.length; i += 1) {
database_list[i].remove(metadata);
}
} else {
for (i = 0; i < database_list.length; i += 1) {
database_list[i].put(metadata);
}
}
return that.storeIndexDatabases(command, database_list);
}
function allProgress(progress) {
if (progress.index === 1) {
progress.value.percentage *= 0.7; // 0 to 70%
command.notify(progress.value);
}
throw null; // stop propagation
}
function success() {
command.success(generic_response);
}
function storeProgress(progress) {
progress.percentage = (0.3 * progress.percentage) + 70; // 70 to 100%
command.notify(progress);
}
RSVP.all([
this.getIndexDatabases(command),
command.storage(this._sub_storage)[method](metadata, option)
]).then(updateAndStoreIndexDatabases, null, allProgress).
then(success, command.error, storeProgress);
};
/**
* Post the document metadata and update the index
*
* @method post
* @param {Object} command The JIO command
* @param {Object} metadata The metadata to post
* @param {Object} option The command option
*/
IndexStorage.prototype.post = function (command, metadata, option) {
this.genericCommand('post', command, metadata, option);
};
/**
* Update the document metadata and update the index
*
* @method put
* @param {Object} command The JIO command
* @param {Object} metadata The metadata to put
* @param {Object} option The command option
*/
IndexStorage.prototype.put = function (command, metadata, option) {
this.genericCommand('put', command, metadata, option);
};
/**
* Add an attachment to a document (no index modification)
*
* @method putAttachment
* @param {Object} command The JIO command
* @param {Object} param The command parameters
* @param {Object} option The command option
*/
IndexStorage.prototype.putAttachment = function (command, param, option) {
command.storage(this._sub_storage).putAttachment(param, option).
then(command.success, command.error, command.notify);
};
/**
* Get the document metadata
*
* @method get
* @param {Object} command The JIO command
* @param {Object} param The command parameters
* @param {Object} option The command option
*/
IndexStorage.prototype.get = function (command, param, option) {
this.genericCommand('get', command, param, option);
};
/**
* Get the attachment.
*
* @method getAttachment
* @param {Object} command The JIO command
* @param {Object} param The command parameters
* @param {Object} option The command option
*/
IndexStorage.prototype.getAttachment = function (command, param, option) {
command.storage(this._sub_storage).getAttachment(param, option).
then(command.success, command.error, command.notify);
};
/**
* Remove document - removing documents updates index!.
*
* @method remove
* @param {Object} command The JIO command
* @param {Object} param The command parameters
* @param {Object} option The command option
*/
IndexStorage.prototype.remove = function (command, param, option) {
this.genericCommand('remove', command, param, option);
};
/**
* Remove attachment
*
* @method removeAttachment
* @param {Object} command The JIO command
* @param {Object} param The command parameters
* @param {Object} option The command option
*/
IndexStorage.prototype.removeAttachment = function (command, param, option) {
command.storage(this._sub_storage).removeAttachment(param, option).
then(command.success, command.error, command.notify);
};
/**
* Gets a document list from the substorage
*
* @method allDocs
* @param {Object} command The JIO command
* @param {Object} param The command parameters
* @param {Object} option The command option
* @param {Boolean} [option.include_docs=false] Also retrieve the actual
* document content.
*/
IndexStorage.prototype.allDocs = function (command, param, option) { // XXX
/*jslint unparam: true */
var index = this.selectIndex(option.select_list || []), delete_id, now;
option.select_list = (
Array.isArray(option.select_list) ? option.select_list : []
);
if (option.select_list.indexOf("_id") === -1) {
option.select_list.push("_id");
delete_id = true;
}
if (option.include_docs) {
now = Date.now();
option.select_list.push("_" + now);
}
this.getIndexDatabase(command, index).then(function (db) {
var i, id;
db = db._database;
if (option.include_docs) {
// XXX find another way to manage include_docs option!!
for (i = 0; i < db.length; i += 1) {
db[i]["_" + now] = db[i];
}
}
jIO.QueryFactory.create(option.query || '').
exec(db, option).then(function () {
for (i = 0; i < db.length; i += 1) {
id = db[i]._id;
if (delete_id) {
delete db[i]._id;
}
if (option.include_docs) {
db[i] = {
"id": id,
"value": db[i],
"doc": db[i]["_" + now]
};
delete db[i].doc["_" + now];
delete db[i].value["_" + now];
} else {
db[i] = {
"id": id,
"value": db[i]
};
}
}
command.success(200, {"data": {"total_rows": db.length, "rows": db}});
});
}, function (err) {
if (err.status === 404) {
return command.success(200, {"data": {"total_rows": 0, "rows": []}});
}
command.error(err);
});
};
// IndexStorage.prototype.check = function (command, param, option) { // XXX
// this.repair(command, true, param, option);
// };
// IndexStorage.prototype.repairIndexDatabase = function (
// command,
// index,
// just_check,
// param,
// option
// ) { // XXX
// var i, that = this;
// command.storage(this._sub_storage).allDocs({'include_docs': true}).then(
// function (response) {
// var db_list = [], db = new JSONIndex({
// "_id": param._id,
// "_attachment": that._indices[index].attachment || "body",
// "indexing": that._indices[index].index
// });
// for (i = 0; i < response.rows.length; i += 1) {
// db.put(response.rows[i].doc);
// }
// db_list[index] = db;
// if (just_check) {
// this.getIndexDatabase(command, option, index, function (current_db) {
// if (db.equals(current_db)) {
// return command.success({"ok": true, "id": param._id});
// }
// return command.error(
// "conflict",
// "corrupted",
// "Database is not up to date"
// );
// });
// } else {
// that.storeIndexDatabaseList(command, db_list, {}, function () {
// command.success({"ok": true, "id": param._id});
// });
// }
// },
// function (err) {
// err.message = "Unable to repair the index database";
// command.error(err);
// }
// );
// };
// IndexStorage.prototype.repairDocument = function (
// command,
// just_check,
// param,
// option
// ) { // XXX
// var i, that = this;
// command.storage(this._sub_storage).get(param, {}).then(
// function (response) {
// response._id = param._id;
// that.getIndexDatabaseList(command, option, function (database_list) {
// if (just_check) {
// for (i = 0; i < database_list.length; i += 1) {
// try {
// database_list[i].checkDocument(response);
// } catch (e) {
// return command.error(
// "conflict",
// e.message,
// "Corrupt index database"
// );
// }
// }
// command.success({"_id": param._id, "ok": true});
// } else {
// for (i = 0; i < database_list.length; i += 1) {
// database_list[i].put(response);
// }
// that.storeIndexDatabaseList(
// command,
// database_list,
// option,
// function () {
// command.success({"ok": true, "id": param._id});
// }
// );
// }
// });
// },
// function (err) {
// err.message = "Unable to repair document";
// return command.error(err);
// }
// );
// };
// IndexStorage.prototype.repair = function (command, just_check, param,
// option) { // XXX
// var database_index = -1, i, that = this;
// for (i = 0; i < this._indices.length; i += 1) {
// if (this._indices[i].id === param._id) {
// database_index = i;
// break;
// }
// }
// command.storage(this._sub_storage).repair(param, option).then(
// function () {
// if (database_index !== -1) {
// that.repairIndexDatabase(
// command,
// database_index,
// just_check,
// param,
// option
// );
// } else {
// that.repairDocument(command, just_check, param, option);
// }
// },
// function (err) {
// err.message = "Could not repair sub storage";
// command.error(err);
// }
// );
// };
jIO.addStorage("index", IndexStorage);
exports.createDescription = function () {
// XXX
return;
};
}));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/localstorage.js 0000664 0000000 0000000 00000004463 13037644422 0030557 0 ustar 00root root 0000000 0000000 /*
* Copyright 2013, Nexedi SA
* Released under the LGPL license.
* http://www.gnu.org/licenses/lgpl.html
*/
/*jslint nomen: true*/
/*global jIO, sessionStorage, localStorage, RSVP */
/**
* JIO Local Storage. Type = 'local'.
* Local browser "database" storage.
*
* Storage Description:
*
* {
* "type": "local",
* "sessiononly": false
* }
*
* @class LocalStorage
*/
(function (jIO, sessionStorage, localStorage, RSVP) {
"use strict";
function LocalStorage(spec) {
if (spec.sessiononly === true) {
this._storage = sessionStorage;
} else {
this._storage = localStorage;
}
}
function restrictDocumentId(id) {
if (id !== "/") {
throw new jIO.util.jIOError("id " + id + " is forbidden (!== /)",
400);
}
}
LocalStorage.prototype.get = function (id) {
restrictDocumentId(id);
return {};
};
LocalStorage.prototype.allAttachments = function (id) {
restrictDocumentId(id);
var attachments = {},
key;
for (key in this._storage) {
if (this._storage.hasOwnProperty(key)) {
attachments[key] = {};
}
}
return attachments;
};
LocalStorage.prototype.getAttachment = function (id, name) {
restrictDocumentId(id);
var textstring = this._storage.getItem(name);
if (textstring === null) {
throw new jIO.util.jIOError(
"Cannot find attachment " + name,
404
);
}
return jIO.util.dataURItoBlob(textstring);
};
LocalStorage.prototype.putAttachment = function (id, name, blob) {
var context = this;
restrictDocumentId(id);
// the document already exists
// download data
return new RSVP.Queue()
.push(function () {
return jIO.util.readBlobAsDataURL(blob);
})
.push(function (e) {
context._storage.setItem(name, e.target.result);
});
};
LocalStorage.prototype.removeAttachment = function (id, name) {
restrictDocumentId(id);
return this._storage.removeItem(name);
};
LocalStorage.prototype.hasCapacity = function (name) {
return (name === "list");
};
LocalStorage.prototype.buildQuery = function () {
return [{
id: "/",
value: {}
}];
};
jIO.addStorage('local', LocalStorage);
}(jIO, sessionStorage, localStorage, RSVP));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/memorystorage.js 0000664 0000000 0000000 00000007344 13037644422 0030776 0 ustar 00root root 0000000 0000000 /*
* Copyright 2013, Nexedi SA
* Released under the LGPL license.
* http://www.gnu.org/licenses/lgpl.html
*/
/*jslint nomen: true*/
/*global jIO, RSVP*/
/**
* JIO Memory Storage. Type = 'memory'.
* Memory browser "database" storage.
*
* Storage Description:
*
* {
* "type": "memory"
* }
*
* @class MemoryStorage
*/
(function (jIO, JSON, RSVP) {
"use strict";
/**
* The JIO MemoryStorage extension
*
* @class MemoryStorage
* @constructor
*/
function MemoryStorage() {
this._database = {};
}
MemoryStorage.prototype.put = function (id, metadata) {
if (!this._database.hasOwnProperty(id)) {
this._database[id] = {
attachments: {}
};
}
this._database[id].doc = JSON.stringify(metadata);
return id;
};
MemoryStorage.prototype.get = function (id) {
try {
return JSON.parse(this._database[id].doc);
} catch (error) {
if (error instanceof TypeError) {
throw new jIO.util.jIOError(
"Cannot find document: " + id,
404
);
}
throw error;
}
};
MemoryStorage.prototype.allAttachments = function (id) {
var key,
attachments = {};
try {
for (key in this._database[id].attachments) {
if (this._database[id].attachments.hasOwnProperty(key)) {
attachments[key] = {};
}
}
} catch (error) {
if (error instanceof TypeError) {
throw new jIO.util.jIOError(
"Cannot find document: " + id,
404
);
}
throw error;
}
return attachments;
};
MemoryStorage.prototype.remove = function (id) {
delete this._database[id];
return id;
};
MemoryStorage.prototype.getAttachment = function (id, name) {
try {
var result = this._database[id].attachments[name];
if (result === undefined) {
throw new jIO.util.jIOError(
"Cannot find attachment: " + id + " , " + name,
404
);
}
return jIO.util.dataURItoBlob(result);
} catch (error) {
if (error instanceof TypeError) {
throw new jIO.util.jIOError(
"Cannot find attachment: " + id + " , " + name,
404
);
}
throw error;
}
};
MemoryStorage.prototype.putAttachment = function (id, name, blob) {
var attachment_dict;
try {
attachment_dict = this._database[id].attachments;
} catch (error) {
if (error instanceof TypeError) {
throw new jIO.util.jIOError("Cannot find document: " + id, 404);
}
throw error;
}
return new RSVP.Queue()
.push(function () {
return jIO.util.readBlobAsDataURL(blob);
})
.push(function (evt) {
attachment_dict[name] = evt.target.result;
});
};
MemoryStorage.prototype.removeAttachment = function (id, name) {
try {
delete this._database[id].attachments[name];
} catch (error) {
if (error instanceof TypeError) {
throw new jIO.util.jIOError(
"Cannot find document: " + id,
404
);
}
throw error;
}
};
MemoryStorage.prototype.hasCapacity = function (name) {
return ((name === "list") || (name === "include"));
};
MemoryStorage.prototype.buildQuery = function (options) {
var rows = [],
i;
for (i in this._database) {
if (this._database.hasOwnProperty(i)) {
if (options.include_docs === true) {
rows.push({
id: i,
value: {},
doc: JSON.parse(this._database[i].doc)
});
} else {
rows.push({
id: i,
value: {}
});
}
}
}
return rows;
};
jIO.addStorage('memory', MemoryStorage);
}(jIO, JSON, RSVP));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/multisplitstorage.js 0000664 0000000 0000000 00000041454 13037644422 0031674 0 ustar 00root root 0000000 0000000 /*
* Copyright 2013, Nexedi SA
* Released under the LGPL license.
* http://www.gnu.org/licenses/lgpl.html
*/
/*jslint indent:2, maxlen: 80, nomen: true */
/*global jIO, define, Blob */
/**
* Provides a split storage for JIO. This storage splits data
* and store them in the sub storages defined on the description.
*
* {
* "type": "split",
* "storage_list": [, ...]
* }
*/
// define([dependencies], module);
(function (dependencies, module) {
"use strict";
if (typeof define === 'function' && define.amd) {
return define(dependencies, module);
}
module(jIO);
}(['jio'], function (jIO) {
"use strict";
/**
* Select a storage to put the document part
*
* @method selectStorage
* @private
* @return {Object} The selected storage
*/
function selectStorage(arg, iteration) {
var step = iteration % arg.length;
return arg[step];
}
/**
* Class to merge allDocs responses from several sub storages.
*
* @class AllDocsResponseMerger
* @constructor
*/
function AllDocsResponseMerger() {
/**
* A list of allDocs response.
*
* @attribute response_list
* @type {Array} Contains allDocs responses
* @default []
*/
this.response_list = [];
}
AllDocsResponseMerger.prototype.constructor = AllDocsResponseMerger;
/**
* Add an allDocs response to the response list.
*
* @method addResponse
* @param {Object} response The allDocs response.
* @return {AllDocsResponseMerger} This
*/
AllDocsResponseMerger.prototype.addResponse = function (response) {
this.response_list.push(response);
return this;
};
/**
* Add several allDocs responses to the response list.
*
* @method addResponseList
* @param {Array} response_list An array of allDocs responses.
* @return {AllDocsResponseMerger} This
*/
AllDocsResponseMerger.prototype.addResponseList = function (response_list) {
var i;
for (i = 0; i < response_list.length; i += 1) {
this.response_list.push(response_list[i]);
}
return this;
};
/**
* Merge the response_list to one allDocs response.
*
* The merger will find rows with the same id in order to merge them, thanks
* to the onRowToMerge method. If no row correspond to an id, rows with the
* same id will be ignored.
*
* @method merge
* @param {Object} [option={}] The merge options
* @param {Boolean} [option.include_docs=false] Tell the merger to also
* merge metadata if true.
* @return {Object} The merged allDocs response.
*/
AllDocsResponseMerger.prototype.merge = function (option) {
var result = [], row, to_merge = [], tmp, i;
if (this.response_list.length === 0) {
return [];
}
/*jslint ass: true */
while ((row = this.response_list[0].data.rows.shift()) !== undefined) {
to_merge[0] = row;
for (i = 1; i < this.response_list.length; i += 1) {
to_merge[i] = AllDocsResponseMerger.listPopFromRowId(
this.response_list[i].data.rows,
row.id
);
if (to_merge[i] === undefined) {
break;
}
}
tmp = this.onRowToMerge(to_merge, option || {});
if (tmp !== undefined) {
result[result.length] = tmp;
}
}
this.response_list = [];
return {"total_rows": result.length, "rows": result};
};
/**
* This method is called when the merger want to merge several rows with the
* same id.
*
* @method onRowToMerge
* @param {Array} row_list An array of rows.
* @param {Object} [option={}] The merge option.
* @param {Boolean} [option.include_docs=false] Also merge the metadata if
* true
* @return {Object} The merged row
*/
AllDocsResponseMerger.prototype.onRowToMerge = function (row_list, option) {
var i, k, new_row = {"value": {}}, data = "";
option = option || {};
for (i = 0; i < row_list.length; i += 1) {
new_row.id = row_list[i].id;
if (row_list[i].key) {
new_row.key = row_list[i].key;
}
if (option.include_docs) {
new_row.doc = new_row.doc || {};
for (k in row_list[i].doc) {
if (row_list[i].doc.hasOwnProperty(k)) {
if (k[0] === "_") {
new_row.doc[k] = row_list[i].doc[k];
}
}
}
data += row_list[i].doc.data;
}
}
if (option.include_docs) {
try {
data = JSON.parse(data);
} catch (e) { return undefined; }
for (k in data) {
if (data.hasOwnProperty(k)) {
new_row.doc[k] = data[k];
}
}
}
return new_row;
};
/**
* Search for a specific row and pop it. During the search operation, all
* parsed rows are stored on a dictionnary in order to be found instantly
* later.
*
* @method listPopFromRowId
* @param {Array} rows The row list
* @param {String} doc_id The document/row id
* @return {Object/undefined} The poped row
*/
AllDocsResponseMerger.listPopFromRowId = function (rows, doc_id) {
var row;
if (!rows.dict) {
rows.dict = {};
}
if (rows.dict[doc_id]) {
row = rows.dict[doc_id];
delete rows.dict[doc_id];
return row;
}
/*jslint ass: true*/
while ((row = rows.shift()) !== undefined) {
if (row.id === doc_id) {
return row;
}
rows.dict[row.id] = row;
}
};
/**
* The split storage class used by JIO.
*
* A split storage instance is able to i/o on several sub storages with
* split documents.
*
* @class MultiSplitStorage
*/
function MultiSplitStorage(spec) {
var that = this, priv = {};
/**
* The list of sub storages we want to use to store part of documents.
*
* @attribute storage_list
* @private
* @type {Array} Array of storage descriptions
*/
priv.storage_list = spec.storage_list;
//////////////////////////////////////////////////////////////////////
// Tools
/**
* Send a command to all sub storages. All the response are returned
* in a list. The index of the response correspond to the storage_list
* index. If an error occurs during operation, the callback is called with
* `callback(err, undefined)`. The response is given with
* `callback(undefined, response_list)`.
*
* `doc` is the document informations but can also be a list of dedicated
* document informations. In this case, each document is associated to one
* sub storage.
*
* @method send
* @private
* @param {String} method The command method
* @param {Object,Array} doc The document information to send to each sub
* storages or a list of dedicated document
* @param {Object} option The command option
* @param {Function} callback Called at the end
*/
function send(command, method, doc, option, callback) {
var i, answer_list = [], failed = false, currentServer;
function onEnd() {
i += 1;
if (i === priv.storage_list.length) {
callback(undefined, answer_list);
}
}
function onSuccess(i) {
return function (response) {
if (!failed) {
answer_list[i] = response;
}
onEnd();
};
}
function onError(i) {
return function (err) {
if (!failed) {
failed = true;
err.index = i;
callback(err, undefined);
}
};
}
if (!Array.isArray(doc)) {
for (i = 0; i < doc.length; i += 1) {
currentServer = selectStorage(priv.storage_list, i, doc.length);
if (method === 'allDocs') {
command.storage(currentServer)[method](option).
then(onSuccess(i), onError(i));
} else {
command.storage(currentServer)[method](doc, option).
then(onSuccess(i), onError(i));
}
}
} else {
for (i = 0; i < doc.length; i += 1) {
currentServer = selectStorage(priv.storage_list, i, doc.length);
// we assume that alldocs is not called if the there is several docs
command.storage(priv.storage_list[i])[method](doc[i], option).
then(onSuccess(i), onError(i));
}
}
//default splitstorage method
/*
if (!Array.isArray(doc)) {
for (i = 0; i < priv.storage_list.length; i += 1) {
if (method === 'allDocs') {
command.storage(priv.storage_list[i])[method](option).
then(onSuccess(i), onError(i));
} else {
command.storage(priv.storage_list[i])[method](doc, option).
then(onSuccess(i), onError(i));
}
}
} else {
for (i = 0; i < priv.storage_list.length; i += 1) {
// we assume that alldocs is not called if the there is several docs
command.storage(priv.storage_list[i])[method](doc[i], option).
then(onSuccess(i), onError(i));
}
}
*/
//re-init
i = 0;
}
/**
* Split document metadata then store them to the sub storages.
*
* @method postOrPut
* @private
* @param {Object} doc A serialized document object
* @param {Object} option Command option properties
* @param {String} method The command method ('post' or 'put')
*/
priv.postOrPut = function (command, doc, option, method) {
var i, data, doc_list = [], doc_underscores = {};
for (i in doc) {
if (doc.hasOwnProperty(i)) {
if (i[0] === "_") {
doc_underscores[i] = doc[i];
delete doc[i];
}
}
}
data = JSON.stringify(doc);
//for (i = 0; i < priv.storage_list.length; i += 1) {
//doc_list[i] = JSON.parse(JSON.stringify(doc_underscores));
//doc_list[i].data = data.slice(
//(data.length / priv.storage_list.length) * i,
//(data.length / priv.storage_list.length) * (i + 1)
//);
//console.info('doc_list[i].data');
//console.log(doc_list[i].data);
//}
for (i = 0; i < 100; i += 1) {
doc_list[i] = JSON.parse(JSON.stringify(doc_underscores));
doc_list[i]._id = doc_list[i]._id + '_' + i;
doc_list[i].data = data.slice(
(data.length / 100) * i,
(data.length / 100) * (i + 1)
);
}
send(command, method, doc_list, option, function (err) {
if (err) {
err.message = "Unable to " + method + " document";
delete err.index;
return command.error(err);
}
command.success({"id": doc_underscores._id});
});
};
//////////////////////////////////////////////////////////////////////
// JIO commands
/**
* Split document metadata then store them to the sub storages.
*
* @method post
* @param {Object} command The JIO command
*/
that.post = function (command, metadata, option) {
priv.postOrPut(command, metadata, option, 'post');
};
/**
* Split document metadata then store them to the sub storages.
*
* @method put
* @param {Object} command The JIO command
*/
that.put = function (command, metadata, option) {
priv.postOrPut(command, metadata, option, 'put');
};
/**
* Puts an attachment to the sub storages.
*
* @method putAttachment
* @param {Object} command The JIO command
*/
that.putAttachment = function (command, param, option) {
var i, attachment_list = [], data = param._blob;
for (i = 0; i < priv.storage_list.length; i += 1) {
attachment_list[i] = jIO.util.deepClone(param);
attachment_list[i]._blob = data.slice(
data.size * i / priv.storage_list.length,
data.size * (i + 1) / priv.storage_list.length,
data.type
);
}
send(
command,
'putAttachment',
attachment_list,
option,
function (err) {
if (err) {
err.message = "Unable to put attachment";
delete err.index;
return command.error(err);
}
command.success();
}
);
};
/**
* Gets splited document metadata then returns real document.
*
* @method get
* @param {Object} command The JIO command
*/
that.get = function (command, param, option) {
var doc = param;
send(command, 'get', doc, option, function (err, response) {
var i, k;
if (err) {
err.message = "Unable to get document";
delete err.index;
return command.error(err);
}
doc = '';
for (i = 0; i < response.length; i += 1) {
response[i] = response[i].data;
doc += response[i].data;
}
doc = JSON.parse(doc);
for (i = 0; i < response.length; i += 1) {
for (k in response[i]) {
if (response[i].hasOwnProperty(k)) {
if (k[0] === "_") {
doc[k] = response[i][k];
}
}
}
}
delete doc._attachments;
for (i = 0; i < response.length; i += 1) {
if (response[i]._attachments) {
for (k in response[i]._attachments) {
if (response[i]._attachments.hasOwnProperty(k)) {
doc._attachments = doc._attachments || {};
doc._attachments[k] = doc._attachments[k] || {
"length": 0,
"content_type": ""
};
doc._attachments[k].length += response[i]._attachments[k].
length;
// if (response[i]._attachments[k].digest) {
// if (doc._attachments[k].digest) {
// doc._attachments[k].digest += " " + response[i].
// _attachments[k].digest;
// } else {
// doc._attachments[k].digest = response[i].
// _attachments[k].digest;
// }
// }
doc._attachments[k].content_type = response[i]._attachments[k].
content_type;
}
}
}
}
command.success({"data": doc});
});
};
/**
* Gets splited document attachment then returns real attachment data.
*
* @method getAttachment
* @param {Object} command The JIO command
*/
that.getAttachment = function (command, param, option) {
send(command, 'getAttachment', param, option, function (
err,
response
) {
if (err) {
err.message = "Unable to get attachment";
delete err.index;
return command.error(err);
}
command.success({"data": new Blob(response.map(function (answer) {
return answer.data;
}), {"type": response[0].data.type})});
});
};
/**
* Removes a document from the sub storages.
*
* @method remove
* @param {Object} command The JIO command
*/
that.remove = function (command, param, option) {
send(
command,
'remove',
param,
option,
function (err) {
if (err) {
err.message = "Unable to remove document";
delete err.index;
return command.error(err);
}
command.success();
}
);
};
/**
* Removes an attachment from the sub storages.
*
* @method removeAttachment
* @param {Object} command The JIO command
*/
that.removeAttachment = function (command, param, option) {
send(
command,
'removeAttachment',
param,
option,
function (err) {
if (err) {
err.message = "Unable to remove attachment";
delete err.index;
return command.error(err);
}
command.success();
}
);
};
/**
* Retreive a list of all document in the sub storages.
*
* If include_docs option is false, then it returns the document list from
* the first sub storage. Else, it will merge results and return.
*
* @method allDocs
* @param {Object} command The JIO command
*/
that.allDocs = function (command, param, option) {
option = {"include_docs": option.include_docs};
send(
command,
'allDocs',
param,
option,
function (err, response_list) {
var all_docs_merger;
if (err) {
err.message = "Unable to retrieve document list";
delete err.index;
return command.error(err);
}
all_docs_merger = new AllDocsResponseMerger();
all_docs_merger.addResponseList(response_list);
return command.success({"data": all_docs_merger.merge(option)});
}
);
};
} // end of MultiplitStorage
jIO.addStorage('multisplit', MultiSplitStorage);
}));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/qiniustorage.js 0000664 0000000 0000000 00000016500 13037644422 0030605 0 ustar 00root root 0000000 0000000 /*
* Copyright 2015, Nexedi SA
* Released under the LGPL license.
* http://www.gnu.org/licenses/lgpl.html
*/
/**
* JIO Qiniu Storage. Type = "qiniu".
* Qiniu "database" storage.
*
* REMAINING WORK:
* - removeAttachment should support CORS
* - allAttachments should support CORS
* - disable getAttachment HTTP cache
*/
/*global JSON, FormData, btoa, Blob, jIO, RSVP, UriTemplate, crypto,
Uint8Array, TextEncoder*/
/*jslint nomen: true*/
(function (JSON, FormData, btoa, Blob, jIO, RSVP, UriTemplate, Crypto,
Uint8Array, TextEncoder) {
"use strict";
var METADATA_URL = "http://{+bucket}/{+key}{?e,token}",
metadata_template = UriTemplate.parse(METADATA_URL),
UPLOAD_URL = "http://up.qiniu.com/",
DEADLINE = 2451491200;
function urlsafe_base64_encode(string) {
return string
.replace(/\+/g, '-') // Convert '+' to '-'
.replace(/\//g, '_'); // Convert '/' to '_'
// .replace(/=+$/, ''); // Remove ending '='
}
function bytesToASCIIString(bytes) {
return String.fromCharCode.apply(null, new Uint8Array(bytes));
}
// http://blog.engelke.com/tag/webcrypto/
function stringToArrayBuffer(string) {
var encoder = new TextEncoder("utf-8");
return encoder.encode(string);
}
function b64_hmac_sha1(secret_key, message) {
return new RSVP.Queue()
.push(function () {
return Crypto.subtle.importKey(
"raw",
stringToArrayBuffer(secret_key),
{name: "HMAC", hash: "SHA-1"},
false,
["sign"]
);
})
.push(function (key) {
return Crypto.subtle.sign({
name: "HMAC",
hash: "SHA-1"
}, key, stringToArrayBuffer(message));
})
.push(function (signature) {
return urlsafe_base64_encode(btoa(bytesToASCIIString(signature)));
});
}
/**
* The JIO QiniuStorage extension
*
* @class QiniuStorage
* @constructor
*/
function QiniuStorage(spec) {
if (typeof spec.bucket !== 'string' && !spec.bucket) {
throw new TypeError("Qiniu 'bucket' must be a string " +
"which contains more than one character.");
}
if (typeof spec.access_key !== 'string' && !spec.access_key) {
throw new TypeError("Qiniu 'access_key' must be a string " +
"which contains more than one character.");
}
if (typeof spec.secret_key !== 'string' && !spec.secret_key) {
throw new TypeError("Qiniu 'secret_key' must be a string " +
"which contains more than one character.");
}
this._bucket = spec.bucket;
this._access_key = spec.access_key;
this._secret_key = spec.secret_key;
}
function restrictDocumentId(id) {
if (id !== "/") {
throw new jIO.util.jIOError("id " + id + " is forbidden (!== /)",
400);
}
}
QiniuStorage.prototype.get = function (id) {
restrictDocumentId(id);
return {};
};
QiniuStorage.prototype.getAttachment = function (id, key) {
restrictDocumentId(id);
var context = this,
download_url = metadata_template.expand({
bucket: context._bucket,
key: key,
e: DEADLINE
});
return new RSVP.Queue()
.push(function () {
return b64_hmac_sha1(context._secret_key, download_url);
})
.push(function (token) {
return jIO.util.ajax({
type: "GET",
url: metadata_template.expand({
bucket: context._bucket,
key: key,
e: DEADLINE,
token: context._access_key + ':' + token
})
});
})
.push(function (result) {
return new Blob([result.target.response ||
result.target.responseText]);
}, function (error) {
if ((error.target !== undefined) &&
(error.target.status === 404)) {
throw new jIO.util.jIOError("Cannot find attachment: "
+ id + " , " + key,
404);
}
throw error;
});
};
QiniuStorage.prototype.putAttachment = function (id, key, blob) {
restrictDocumentId(id);
var data,
context = this,
put_policy,
encoded,
upload_token;
data = new FormData();
put_policy = JSON.stringify({
"scope": "bucket" + ':' + key,
"deadline": DEADLINE
});
encoded = btoa(put_policy);
return new RSVP.Queue()
.push(function () {
return b64_hmac_sha1(context._secret_key, encoded);
})
.push(function (encode_signed) {
upload_token = context._access_key + ":" + encode_signed + ":" +
encoded;
data.append("key", key);
data.append("token", upload_token);
data.append(
"file",
blob,
key
);
return jIO.util.ajax({
type: "POST",
url: UPLOAD_URL,
data: data
});
});
};
QiniuStorage.prototype.hasCapacity = function (name) {
return (name === "list");
};
QiniuStorage.prototype.buildQuery = function () {
return [{
id: "/",
value: {}
}];
};
// QiniuStorage.prototype.remove = function (command, param) {
//
// var DELETE_HOST = "http://rs.qiniu.com",
// DELETE_PREFIX = "/delete/",
// encoded_entry_uri = urlsafe_base64_encode(btoa(
// this._bucket + ':' + param._id
// )),
// delete_url = DELETE_HOST + DELETE_PREFIX + encoded_entry_uri,
// data = DELETE_PREFIX + encoded_entry_uri + '\n',
// token = b64_hmac_sha1(this._secret_key, data);
//
// jIO.util.ajax({
// "type": "POST",
// "url": delete_url,
// "headers": {
// Authorization: "QBox " + this._access_key + ':' + token,
// "Content-Type": 'application/x-www-form-urlencoded'
// }
// }).then(
// command.success
// ).fail(function (error) {
// command.error(
// "not_found",
// "missing",
// "Unable to delete doc"
// );
// });
// };
//
// QiniuStorage.prototype.allDocs = function (command, param, options) {
// var LIST_HOST = "http://rsf.qiniu.com",
// LIST_PREFIX = "/list?bucket=" + this._bucket,
// list_url = LIST_HOST + LIST_PREFIX,
// token = b64_hmac_sha1(this._secret_key, LIST_PREFIX + '\n');
//
// jIO.util.ajax({
// "type": "POST",
// "url": list_url,
// "headers": {
// Authorization: "QBox " + this._access_key + ':' + token,
// "Content-Type": 'application/x-www-form-urlencoded'
// }
// }).then(function (response) {
// var data = JSON.parse(response.target.responseText),
// count = data.items.length,
// result = [],
// item,
// i;
// for (i = 0; i < count; i += 1) {
// item = data.items[i];
// result.push({
// id: item.key,
// key: item.key,
// doc: {},
// value: {}
// });
// }
// command.success({"data": {"rows": result, "total_rows": count}});
// }).fail(function (error) {
// command.error(
// "error",
// "did not work as expected",
// "Unable to call allDocs"
// );
// });
// };
jIO.addStorage('qiniu', QiniuStorage);
}(JSON, FormData, btoa, Blob, jIO, RSVP, UriTemplate, crypto, Uint8Array,
TextEncoder));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/querystorage.js 0000664 0000000 0000000 00000015144 13037644422 0030630 0 ustar 00root root 0000000 0000000 /*jslint nomen: true*/
/*global RSVP*/
(function (jIO, RSVP) {
"use strict";
/**
* The jIO QueryStorage extension
*
* @class QueryStorage
* @constructor
*/
function QueryStorage(spec) {
this._sub_storage = jIO.createJIO(spec.sub_storage);
this._key_schema = spec.key_schema;
}
QueryStorage.prototype.get = function () {
return this._sub_storage.get.apply(this._sub_storage, arguments);
};
QueryStorage.prototype.allAttachments = function () {
return this._sub_storage.allAttachments.apply(this._sub_storage, arguments);
};
QueryStorage.prototype.post = function () {
return this._sub_storage.post.apply(this._sub_storage, arguments);
};
QueryStorage.prototype.put = function () {
return this._sub_storage.put.apply(this._sub_storage, arguments);
};
QueryStorage.prototype.remove = function () {
return this._sub_storage.remove.apply(this._sub_storage, arguments);
};
QueryStorage.prototype.getAttachment = function () {
return this._sub_storage.getAttachment.apply(this._sub_storage, arguments);
};
QueryStorage.prototype.putAttachment = function () {
return this._sub_storage.putAttachment.apply(this._sub_storage, arguments);
};
QueryStorage.prototype.removeAttachment = function () {
return this._sub_storage.removeAttachment.apply(this._sub_storage,
arguments);
};
QueryStorage.prototype.repair = function () {
return this._sub_storage.repair.apply(this._sub_storage, arguments);
};
QueryStorage.prototype.hasCapacity = function (name) {
var this_storage_capacity_list = ["limit",
"sort",
"select",
"query"];
if (this_storage_capacity_list.indexOf(name) !== -1) {
return true;
}
if (name === "list") {
return this._sub_storage.hasCapacity(name);
}
return false;
};
QueryStorage.prototype.buildQuery = function (options) {
var substorage = this._sub_storage,
context = this,
sub_options = {},
is_manual_query_needed = false,
is_manual_include_needed = false;
if (substorage.hasCapacity("list")) {
// Can substorage handle the queries if needed?
try {
if (((options.query === undefined) ||
(substorage.hasCapacity("query"))) &&
((options.sort_on === undefined) ||
(substorage.hasCapacity("sort"))) &&
((options.select_list === undefined) ||
(substorage.hasCapacity("select"))) &&
((options.limit === undefined) ||
(substorage.hasCapacity("limit")))) {
sub_options.query = options.query;
sub_options.sort_on = options.sort_on;
sub_options.select_list = options.select_list;
sub_options.limit = options.limit;
}
} catch (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 501)) {
is_manual_query_needed = true;
} else {
throw error;
}
}
// Can substorage include the docs if needed?
try {
if ((is_manual_query_needed ||
(options.include_docs === true)) &&
(substorage.hasCapacity("include"))) {
sub_options.include_docs = true;
}
} catch (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 501)) {
is_manual_include_needed = true;
} else {
throw error;
}
}
return substorage.buildQuery(sub_options)
// Include docs if needed
.push(function (result) {
var include_query_list = [result],
len,
i;
function safeGet(j) {
var id = result[j].id;
return substorage.get(id)
.push(function (doc) {
// XXX Can delete user data!
doc._id = id;
return doc;
}, function (error) {
// Document may have been dropped after listing
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return;
}
throw error;
});
}
if (is_manual_include_needed) {
len = result.length;
for (i = 0; i < len; i += 1) {
include_query_list.push(safeGet(i));
}
result = RSVP.all(include_query_list);
}
return result;
})
.push(function (result) {
var original_result,
len,
i;
if (is_manual_include_needed) {
original_result = result[0];
len = original_result.length;
for (i = 0; i < len; i += 1) {
original_result[i].doc = result[i + 1];
}
result = original_result;
}
return result;
})
// Manual query if needed
.push(function (result) {
var data_rows = [],
len,
i;
if (is_manual_query_needed) {
len = result.length;
for (i = 0; i < len; i += 1) {
result[i].doc.__id = result[i].id;
data_rows.push(result[i].doc);
}
if (options.select_list) {
options.select_list.push("__id");
}
result = jIO.QueryFactory.create(options.query || "",
context._key_schema).
exec(data_rows, options);
}
return result;
})
// reconstruct filtered rows, preserving the order from docs
.push(function (result) {
var new_result = [],
element,
len,
i;
if (is_manual_query_needed) {
len = result.length;
for (i = 0; i < len; i += 1) {
element = {
id: result[i].__id,
value: options.select_list ? result[i] : {},
doc: {}
};
if (options.select_list) {
// Does not work if user manually request __id
delete element.value.__id;
}
if (options.include_docs) {
// XXX To implement
throw new Error("QueryStorage does not support include docs");
}
new_result.push(element);
}
result = new_result;
}
return result;
});
}
};
jIO.addStorage('query', QueryStorage);
}(jIO, RSVP));
replicaterevisionstorage.js 0000664 0000000 0000000 00000054310 13037644422 0033131 0 ustar 00root root 0000000 0000000 jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage /*jslint indent: 2, maxlen: 80, nomen: true */
/*global jIO, define */
/**
* JIO Replicate Revision Storage.
* It manages storages that manage revisions and conflicts.
* Description:
* {
* "type": "replicaterevision",
* "storage_list": [
* ,
* ...
* ]
* }
*/
// define([module_name], [dependencies], module);
(function (dependencies, module) {
"use strict";
if (typeof define === 'function' && define.amd) {
return define(dependencies, module);
}
module(jIO);
}(['jio'], function (jIO) {
"use strict";
jIO.addStorage('replicaterevision', function (spec) {
var that = this, priv = {};
spec = spec || {};
priv.storage_list_key = "storage_list";
priv.storage_list = spec[priv.storage_list_key];
priv.emptyFunction = function () {
return;
};
/**
* Generate a new uuid
*
* @return {string} The new uuid
*/
priv.generateUuid = function () {
var S4 = function () {
var i, string = Math.floor(
Math.random() * 0x10000 /* 65536 */
).toString(16);
for (i = string.length; i < 4; i += 1) {
string = "0" + string;
}
return string;
};
return S4() + S4() + "-" +
S4() + "-" +
S4() + "-" +
S4() + "-" +
S4() + S4() + S4();
};
/**
* Create an array containing dictionnary keys
*
* @param {object} dict The object to convert
* @return {array} The array of keys
*/
priv.dictKeys2Array = function (dict) {
var k, newlist = [];
for (k in dict) {
if (dict.hasOwnProperty(k)) {
newlist.push(k);
}
}
return newlist;
};
/**
* Checks a revision format
*
* @param {string} revision The revision string
* @return {boolean} True if ok, else false
*/
priv.checkRevisionFormat = function (revision) {
return (/^[0-9]+-[0-9a-zA-Z_]+$/.test(revision));
};
/**
* Clones an object in deep (without functions)
*
* @param {any} object The object to clone
* @return {any} The cloned object
*/
priv.clone = function (object) {
var tmp = JSON.stringify(object);
if (tmp === undefined) {
return undefined;
}
return JSON.parse(tmp);
};
/**
* Like addJob but also return the method and the index of the storage
*
* @param {string} method The request method
* @param {number} index The storage index
* @param {object} doc The document object
* @param {object} option The request object
* @param {function} callback The callback. Parameters:
* - {string} The request method
* - {number} The storage index
* - {object} The error object
* - {object} The response object
*/
priv.send = function (command, method, index, doc, option, callback) {
var wrapped_callback_success, wrapped_callback_error;
callback = callback || priv.emptyFunction;
wrapped_callback_success = function (response) {
callback(method, index, undefined, response);
};
wrapped_callback_error = function (err) {
callback(method, index, err, undefined);
};
if (method === 'allDocs') {
command.storage(priv.storage_list[index]).allDocs(option).
then(wrapped_callback_success, wrapped_callback_error);
} else {
command.storage(priv.storage_list[index])[method](doc, option).
then(wrapped_callback_success, wrapped_callback_error);
}
};
/**
* Use "send" method to all sub storages.
* Calling "callback" for each storage response.
*
* @param {string} method The request method
* @param {object} doc The document object
* @param {object} option The request option
* @param {function} callback The callback. Parameters:
* - {string} The request method
* - {number} The storage index
* - {object} The error object
* - {object} The response object
*/
priv.sendToAll = function (command, method, doc, option, callback) {
var i;
for (i = 0; i < priv.storage_list.length; i += 1) {
priv.send(command, method, i, doc, option, callback);
}
};
/**
* Use "send" method to all sub storages.
* Calling "callback" only with the first response
*
* @param {string} method The request method
* @param {object} doc The document object
* @param {object} option The request option
* @param {function} callback The callback. Parameters:
* - {string} The request method
* - {object} The error object
* - {object} The response object
*/
priv.sendToAllFastestResponseOnly = function (command, method,
doc, option, callback) {
var i, callbackWrapper, error_count;
error_count = 0;
callbackWrapper = function (method, index, err, response) {
/*jslint unparam: true */
if (err) {
error_count += 1;
if (error_count === priv.storage_list.length) {
return callback(method, err, response);
}
}
callback(method, err, response);
};
for (i = 0; i < priv.storage_list.length; i += 1) {
priv.send(command, method, i, doc, option, callbackWrapper);
}
};
/**
* Use "sendToAll" method, calling "callback" at the last response with
* the response list
*
* @param {string} method The request method
* @param {object} doc The document object
* @param {object} option The request option
* @return {function} callback The callback. Parameters:
* - {string} The request method
* - {object} The error object
* - {object} The response object
*/
priv.sendToAllGetResponseList = function (command, method,
doc, option, callback) {
var wrapper, callback_count = 0, response_list = [], error_list = [];
response_list.length = priv.storage_list.length;
wrapper = function (method, index, err, response) {
/*jslint unparam: true */
error_list[index] = err;
response_list[index] = response;
callback_count += 1;
if (callback_count === priv.storage_list.length) {
callback(error_list, response_list);
}
};
priv.sendToAll(command, method, doc, option, wrapper);
};
/**
* Checks if the sub storage are identical
* @method check
* @param {object} command The JIO command
*/
that.check = function (command, param, option) {
function callback(err) {
if (err) {
return command.error(err);
}
command.success();
}
if (!param._id) {
return callback({"status": 501});
}
priv.check(command, param, option, callback);
};
/**
* Repair the sub storages to make them identical
* @method repair
* @param {object} command The JIO command
*/
that.repair = function (command, param, option) {
function callback(err) {
if (err) {
return command.error(err);
}
command.success();
}
if (!param._id) {
return callback({
"status": 501
});
}
priv.repair(
command,
param,
option,
true,
callback
);
};
priv.check = function (command, doc, option, success, error) {
priv.repair(command, doc, option, false, success, error);
};
priv.repair = function (command, doc, option, repair, callback) {
var functions = {};
callback = callback || priv.emptyFunction;
option = option || {};
functions.begin = function () {
// // XXX make revision storage check and repair
// // to enable check/repair sub storage from this storage
// // by calling this function just below
// //functions.repairAllSubStorages();
// // else we assume that sub storages are good
// functions.getAllDocuments(functions.newParam(
// doc,
// option,
// repair
// ));
// };
// functions.repairAllSubStorages = function () {
var i;
for (i = 0; i < priv.storage_list.length; i += 1) {
priv.send(
command,
repair ? "repair" : "check",
i,
doc,
option,
functions.repairAllSubStoragesCallback
);
}
};
functions.repair_sub_storages_count = 0;
functions.repairAllSubStoragesCallback = function (method,
index, err, response) {
/*jslint unparam: true */
if (err) {
return command.error(err);
}
functions.repair_sub_storages_count += 1;
if (functions.repair_sub_storages_count === priv.storage_list.length) {
functions.getAllDocuments(functions.newParam(
doc,
option,
repair
));
}
};
functions.newParam = function (doc, option, repair) {
var param = {
"doc": doc, // the document to repair
"option": option,
"repair": repair,
"responses": {
"count": 0,
"list": [
// 0: response0
// 1: response1
// 2: response2
],
"stats": {
// responseA: [0, 1]
// responseB: [2]
},
"stats_items": [
// 0: [responseA, [0, 1]]
// 1: [responseB, [2]]
],
"attachments": {
// attachmentA : {_id: attachmentA, _revs_info, _content_type: ..}
// attachmentB : {_id: attachmentB, _revs_info, _content_type: ..}
}
},
"conflicts": {
// revC: true
// revD: true
},
"deal_result_state": "ok",
"my_rev": undefined
};
param.responses.list.length = priv.storage_list.length;
return param;
};
functions.getAllDocuments = function (param) {
var i, metadata, cloned_option;
metadata = priv.clone(param.doc);
cloned_option = priv.clone(param.option);
cloned_option.conflicts = true;
cloned_option.revs = true;
cloned_option.revs_info = true;
for (i = 0; i < priv.storage_list.length; i += 1) {
// if the document is not loaded
priv.send(command, "get", i,
metadata, cloned_option, functions.dealResults(param));
}
functions.finished_count += 1;
};
functions.dealResults = function (param) {
return function (method, index, err, response) {
/*jslint unparam: true */
var response_object = {};
if (param.deal_result_state !== "ok") {
// deal result is in a wrong state, exit
return;
}
if (err) {
if (err.status !== 404) {
// get document failed, exit
param.deal_result_state = "error";
callback({
"status": 409,
"message": "An error occured on the sub storage",
"reason": err.reason
}, undefined);
return;
}
}
response = response.data;
// success to get the document
// add the response in memory
param.responses.count += 1;
param.responses.list[index] = response;
// add the conflicting revision for other synchronizations
functions.addConflicts(param, (response || {})._conflicts);
if (param.responses.count !== param.responses.list.length) {
// this is not the last response, wait for the next response
return;
}
// this is now the last response
functions.makeResponsesStats(param.responses);
//console.log(JSON.parse(JSON.stringify(param.responses)));
if (param.responses.stats_items.length === 1) {
// the responses are equals!
response_object.ok = true;
response_object.id = param.doc._id;
if (doc._rev) {
response_object.rev = doc._rev;
// "rev": (typeof param.responses.list[0] === "object" ?
// param.responses.list[0]._rev : undefined)
}
callback(undefined, response_object);
return;
}
// the responses are different
if (param.repair === false) {
// do not repair
callback({
"status": 409,
"message": "Some documents are different in the sub storages",
"reason": "Storage contents differ"
}, undefined);
return;
}
// repair
functions.getAttachments(param);
};
};
functions.addConflicts = function (param, list) {
var i;
list = list || [];
for (i = 0; i < list.length; i += 1) {
param.conflicts[list[i]] = true;
}
};
functions.makeResponsesStats = function (responses) {
var i, str_response;
for (i = 0; i < responses.count; i += 1) {
str_response = JSON.stringify(responses.list[i]);
if (responses.stats[str_response] === undefined) {
responses.stats[str_response] = [];
responses.stats_items.push([
str_response,
responses.stats[str_response]
]);
}
responses.stats[str_response].push(i);
}
};
functions.getAttachments = function (param) {
var response, parsed_response, attachment;
for (response in param.responses.stats) {
if (param.responses.stats.hasOwnProperty(response)) {
parsed_response = JSON.parse(response);
for (attachment in parsed_response._attachments) {
if ((parsed_response._attachments).hasOwnProperty(attachment)) {
functions.get_attachment_count += 1;
priv.send(
command,
"getAttachment",
param.responses.stats[response][0],
{
"_id": param.doc._id,
"_attachment": attachment,
"_rev": JSON.parse(response)._rev
},
param.option,
functions.getAttachmentsCallback(
param,
attachment,
param.responses.stats[response]
)
);
}
}
}
}
};
functions.get_attachment_count = 0;
functions.getAttachmentsCallback = function (param, attachment_id) {
return function (method, index, err, response) {
/*jslint unparam: true */
if (err) {
callback({
"status": 409,
"message": "Unable to retreive attachments",
"reason": err.reason
}, undefined);
return;
}
response = response.data;
functions.get_attachment_count -= 1;
param.responses.attachments[attachment_id] = response;
if (functions.get_attachment_count === 0) {
functions.synchronizeAllSubStorage(param);
if (param.option.synchronize_conflicts !== false) {
functions.synchronizeConflicts(param);
}
}
};
};
functions.synchronizeAllSubStorage = function (param) {
var i, j, len = param.responses.stats_items.length;
for (i = 0; i < len; i += 1) {
// browsing responses
for (j = 0; j < len; j += 1) {
// browsing storage list
if (i !== j) {
functions.synchronizeResponseToSubStorage(
param,
param.responses.stats_items[i][0],
param.responses.stats_items[j][1]
);
}
}
}
functions.finished_count -= 1;
};
functions.synchronizeResponseToSubStorage = function (
param,
response,
storage_list
) {
var i, new_doc, attachment_to_put = [];
if (response === undefined) {
// no response to sync
return;
}
new_doc = JSON.parse(response);
new_doc._revs = new_doc._revisions;
delete new_doc._rev;
delete new_doc._revisions;
delete new_doc._conflicts;
for (i in new_doc._attachments) {
if (new_doc._attachments.hasOwnProperty(i)) {
attachment_to_put.push({
"_id": i,
"_content_type": new_doc._attachments[i].content_type,
"_revs_info": new_doc._revs_info
});
}
}
for (i = 0; i < storage_list.length; i += 1) {
functions.finished_count += attachment_to_put.length || 1;
priv.send(
command,
"put",
storage_list[i],
new_doc,
param.option,
functions.putAttachments(param, attachment_to_put)
);
}
functions.finished_count += 1;
functions.finished();
};
functions.synchronizeConflicts = function (param) {
var rev, new_doc, new_option;
new_option = priv.clone(param.option);
new_option.synchronize_conflict = false;
for (rev in param.conflicts) {
if (param.conflicts.hasOwnProperty(rev)) {
new_doc = priv.clone(param.doc);
new_doc._rev = rev;
// no need to synchronize all the conflicts again, do it once
functions.getAllDocuments(functions.newParam(
new_doc,
new_option,
param.repair
));
}
}
};
functions.putAttachments = function (param, attachment_to_put) {
return function (method, index, err, response) {
/*jslint unparam: true */
var i, attachment;
if (err) {
return callback({
"status": 409,
"message": "Unable to copy attachments",
"reason": err.reason
}, undefined);
}
for (i = 0; i < attachment_to_put.length; i += 1) {
attachment = {
"_id": param.doc._id,
"_attachment": attachment_to_put[i]._id,
"_content_type": attachment_to_put[i]._content_type,
"_revs_info": attachment_to_put[i]._revs_info,
// "_revs_info": param.responses.list[index]._revs_info,
"_data": param.responses.attachments[attachment_to_put[i]._id]
};
priv.send(
command,
"putAttachment",
index,
attachment,
option,
functions.putAttachmentCallback(param)
);
}
if (attachment_to_put.length === 0) {
functions.finished();
}
};
};
functions.putAttachmentCallback = function (param) {
/*jslint unparam: true */
return function (method, index, err, response) {
if (err) {
return callback(err, undefined);
}
functions.finished();
};
};
functions.finished_count = 0;
functions.finished = function () {
var response_object = {};
functions.finished_count -= 1;
if (functions.finished_count === 0) {
response_object.ok = true;
response_object.id = doc._id;
if (doc._rev) {
response_object.rev = doc._rev;
}
callback(undefined, response_object);
}
};
functions.begin();
};
/**
* The generic method to use
* @method genericRequest
* @param {object} command The JIO command
* @param {string} method The method to use
*/
that.genericRequest = function (command, method, param, option) {
var doc = param;
doc._id = doc._id || priv.generateUuid();
priv.sendToAllFastestResponseOnly(
command,
method,
doc,
option,
function (method, err, response) {
/*jslint unparam: true */
if (err) {
return command.error(err);
}
command.success(response);
}
);
};
/**
* Post the document metadata to all sub storages
* @method post
* @param {object} command The JIO command
*/
that.post = function (command, metadata, option) {
that.genericRequest(command, "post", metadata, option);
};
/**
* Put the document metadata to all sub storages
* @method put
* @param {object} command The JIO command
*/
that.put = function (command, metadata, option) {
that.genericRequest(command, "put", metadata, option);
};
/**
* Put an attachment to a document to all sub storages
* @method putAttachment
* @param {object} command The JIO command
*/
that.putAttachment = function (command, param, option) {
that.genericRequest(command, "putAttachment", param, option);
};
/**
* Get the document from all sub storages, get the fastest.
* @method get
* @param {object} command The JIO command
*/
that.get = function (command, param, option) {
that.genericRequest(command, "get", param, option);
};
/**
* Get the attachment from all sub storages, get the fastest.
* @method getAttachment
* @param {object} command The JIO command
*/
that.getAttachment = function (command, param, option) {
that.genericRequest(command, "getAttachment", param, option);
};
/**
* Remove the document from all sub storages.
* @method remove
* @param {object} command The JIO command
*/
that.remove = function (command, param, option) {
that.genericRequest(command, "remove", param, option);
};
/**
* Remove the attachment from all sub storages.
* @method remove
* @param {object} command The JIO command
*/
that.removeAttachment = function (command, param, option) {
that.genericRequest(command, "removeAttachment", param, option);
};
});
}));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/replicatestorage.js 0000664 0000000 0000000 00000051231 13037644422 0031430 0 ustar 00root root 0000000 0000000 /*
* JIO extension for resource replication.
* Copyright (C) 2013, 2015 Nexedi SA
*
* This library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*/
/*jslint nomen: true*/
/*global jIO, RSVP, Rusha*/
(function (jIO, RSVP, Rusha, stringify) {
"use strict";
var rusha = new Rusha(),
CONFLICT_THROW = 0,
CONFLICT_KEEP_LOCAL = 1,
CONFLICT_KEEP_REMOTE = 2,
CONFLICT_CONTINUE = 3;
/****************************************************
Use a local jIO to read/write/search documents
Synchronize in background those document with a remote jIO.
Synchronization status is stored for each document as an local attachment.
****************************************************/
function generateHash(content) {
// XXX Improve performance by moving calculation to WebWorker
return rusha.digestFromString(content);
}
function ReplicateStorage(spec) {
this._query_options = spec.query || {};
this._local_sub_storage = jIO.createJIO(spec.local_sub_storage);
this._remote_sub_storage = jIO.createJIO(spec.remote_sub_storage);
this._signature_hash = "_replicate_" + generateHash(
stringify(spec.local_sub_storage) +
stringify(spec.remote_sub_storage) +
stringify(this._query_options)
);
this._signature_sub_storage = jIO.createJIO({
type: "document",
document_id: this._signature_hash,
sub_storage: spec.signature_storage || spec.local_sub_storage
});
this._use_remote_post = spec.use_remote_post || false;
this._conflict_handling = spec.conflict_handling || 0;
// 0: no resolution (ie, throw an Error)
// 1: keep the local state
// (overwrites the remote document with local content)
// (delete remote document if local is deleted)
// 2: keep the remote state
// (overwrites the local document with remote content)
// (delete local document if remote is deleted)
// 3: keep both copies (leave documents untouched, no signature update)
if ((this._conflict_handling !== CONFLICT_THROW) &&
(this._conflict_handling !== CONFLICT_KEEP_LOCAL) &&
(this._conflict_handling !== CONFLICT_KEEP_REMOTE) &&
(this._conflict_handling !== CONFLICT_CONTINUE)) {
throw new jIO.util.jIOError("Unsupported conflict handling: " +
this._conflict_handling, 400);
}
this._check_local_modification = spec.check_local_modification;
if (this._check_local_modification === undefined) {
this._check_local_modification = true;
}
this._check_local_creation = spec.check_local_creation;
if (this._check_local_creation === undefined) {
this._check_local_creation = true;
}
this._check_local_deletion = spec.check_local_deletion;
if (this._check_local_deletion === undefined) {
this._check_local_deletion = true;
}
this._check_remote_modification = spec.check_remote_modification;
if (this._check_remote_modification === undefined) {
this._check_remote_modification = true;
}
this._check_remote_creation = spec.check_remote_creation;
if (this._check_remote_creation === undefined) {
this._check_remote_creation = true;
}
this._check_remote_deletion = spec.check_remote_deletion;
if (this._check_remote_deletion === undefined) {
this._check_remote_deletion = true;
}
}
ReplicateStorage.prototype.remove = function (id) {
if (id === this._signature_hash) {
throw new jIO.util.jIOError(this._signature_hash + " is frozen",
403);
}
return this._local_sub_storage.remove.apply(this._local_sub_storage,
arguments);
};
ReplicateStorage.prototype.post = function () {
return this._local_sub_storage.post.apply(this._local_sub_storage,
arguments);
};
ReplicateStorage.prototype.put = function (id) {
if (id === this._signature_hash) {
throw new jIO.util.jIOError(this._signature_hash + " is frozen",
403);
}
return this._local_sub_storage.put.apply(this._local_sub_storage,
arguments);
};
ReplicateStorage.prototype.get = function () {
return this._local_sub_storage.get.apply(this._local_sub_storage,
arguments);
};
ReplicateStorage.prototype.hasCapacity = function () {
return this._local_sub_storage.hasCapacity.apply(this._local_sub_storage,
arguments);
};
ReplicateStorage.prototype.buildQuery = function () {
// XXX Remove signature document?
return this._local_sub_storage.buildQuery.apply(this._local_sub_storage,
arguments);
};
ReplicateStorage.prototype.repair = function () {
var context = this,
argument_list = arguments,
skip_document_dict = {};
// Do not sync the signature document
skip_document_dict[context._signature_hash] = null;
function propagateModification(source, destination, doc, hash, id,
options) {
var result,
post_id,
to_skip = true;
if (options === undefined) {
options = {};
}
if (options.use_post) {
result = destination.post(doc)
.push(function (new_id) {
to_skip = false;
post_id = new_id;
return source.put(post_id, doc);
})
.push(function () {
return source.remove(id);
})
.push(function () {
return context._signature_sub_storage.remove(id);
})
.push(function () {
to_skip = true;
return context._signature_sub_storage.put(post_id, {
"hash": hash
});
})
.push(function () {
skip_document_dict[post_id] = null;
});
} else {
result = destination.put(id, doc)
.push(function () {
return context._signature_sub_storage.put(id, {
"hash": hash
});
});
}
return result
.push(function () {
if (to_skip) {
skip_document_dict[id] = null;
}
});
}
function propagateDeletion(destination, id) {
return destination.remove(id)
.push(function () {
return context._signature_sub_storage.remove(id);
})
.push(function () {
skip_document_dict[id] = null;
});
}
function checkAndPropagate(status_hash, local_hash, doc,
source, destination, id,
conflict_force, conflict_revert,
conflict_ignore,
options) {
return destination.get(id)
.push(function (remote_doc) {
return [remote_doc, generateHash(stringify(remote_doc))];
}, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return [null, null];
}
throw error;
})
.push(function (remote_list) {
var remote_doc = remote_list[0],
remote_hash = remote_list[1];
if (local_hash === remote_hash) {
// Same modifications on both side
if (local_hash === null) {
// Deleted on both side, drop signature
return context._signature_sub_storage.remove(id)
.push(function () {
skip_document_dict[id] = null;
});
}
return context._signature_sub_storage.put(id, {
"hash": local_hash
})
.push(function () {
skip_document_dict[id] = null;
});
}
if ((remote_hash === status_hash) || (conflict_force === true)) {
// Modified only locally. No conflict or force
if (local_hash === null) {
// Deleted locally
return propagateDeletion(destination, id);
}
return propagateModification(source, destination, doc,
local_hash, id,
{use_post: ((options.use_post) &&
(remote_hash === null))});
}
// Conflict cases
if (conflict_ignore === true) {
return;
}
if ((conflict_revert === true) || (local_hash === null)) {
// Automatically resolve conflict or force revert
if (remote_hash === null) {
// Deleted remotely
return propagateDeletion(source, id);
}
return propagateModification(
destination,
source,
remote_doc,
remote_hash,
id,
{use_post: ((options.use_revert_post) &&
(local_hash === null))}
);
}
// Minimize conflict if it can be resolved
if (remote_hash === null) {
// Copy remote modification remotely
return propagateModification(source, destination, doc,
local_hash, id,
{use_post: options.use_post});
}
throw new jIO.util.jIOError("Conflict on '" + id + "': " +
stringify(doc || '') + " !== " +
stringify(remote_doc || ''),
409);
});
}
function checkLocalDeletion(queue, destination, id, source,
conflict_force, conflict_revert,
conflict_ignore, options) {
var status_hash;
queue
.push(function () {
return context._signature_sub_storage.get(id);
})
.push(function (result) {
status_hash = result.hash;
return checkAndPropagate(status_hash, null, null,
source, destination, id,
conflict_force, conflict_revert,
conflict_ignore,
options);
});
}
function checkSignatureDifference(queue, source, destination, id,
conflict_force, conflict_revert,
conflict_ignore,
is_creation, is_modification,
getMethod, options) {
queue
.push(function () {
// Optimisation to save a get call to signature storage
if (is_creation === true) {
return RSVP.all([
getMethod(id),
{hash: null}
]);
}
if (is_modification === true) {
return RSVP.all([
getMethod(id),
context._signature_sub_storage.get(id)
]);
}
throw new jIO.util.jIOError("Unexpected call of"
+ " checkSignatureDifference",
409);
})
.push(function (result_list) {
var doc = result_list[0],
local_hash = generateHash(stringify(doc)),
status_hash = result_list[1].hash;
if (local_hash !== status_hash) {
return checkAndPropagate(status_hash, local_hash, doc,
source, destination, id,
conflict_force, conflict_revert,
conflict_ignore,
options);
}
});
}
function checkBulkSignatureDifference(queue, source, destination, id_list,
document_status_list, options,
conflict_force, conflict_revert,
conflict_ignore) {
queue
.push(function () {
return source.bulk(id_list);
})
.push(function (result_list) {
var i,
sub_queue = new RSVP.Queue();
function getResult(j) {
return function (id) {
if (id !== id_list[j].parameter_list[0]) {
throw new Error("Does not access expected ID " + id);
}
return result_list[j];
};
}
for (i = 0; i < result_list.length; i += 1) {
checkSignatureDifference(sub_queue, source, destination,
id_list[i].parameter_list[0],
conflict_force, conflict_revert,
conflict_ignore,
document_status_list[i].is_creation,
document_status_list[i].is_modification,
getResult(i), options);
}
return sub_queue;
});
}
function pushStorage(source, destination, options) {
var queue = new RSVP.Queue();
if (!options.hasOwnProperty("use_post")) {
options.use_post = false;
}
if (!options.hasOwnProperty("use_revert_post")) {
options.use_revert_post = false;
}
return queue
.push(function () {
return RSVP.all([
source.allDocs(context._query_options),
context._signature_sub_storage.allDocs()
]);
})
.push(function (result_list) {
var i,
local_dict = {},
document_list = [],
document_status_list = [],
signature_dict = {},
is_modification,
is_creation,
key;
for (i = 0; i < result_list[0].data.total_rows; i += 1) {
if (!skip_document_dict.hasOwnProperty(
result_list[0].data.rows[i].id
)) {
local_dict[result_list[0].data.rows[i].id] = i;
}
}
for (i = 0; i < result_list[1].data.total_rows; i += 1) {
if (!skip_document_dict.hasOwnProperty(
result_list[1].data.rows[i].id
)) {
signature_dict[result_list[1].data.rows[i].id] = i;
}
}
for (key in local_dict) {
if (local_dict.hasOwnProperty(key)) {
is_modification = signature_dict.hasOwnProperty(key)
&& options.check_modification;
is_creation = !signature_dict.hasOwnProperty(key)
&& options.check_creation;
if (is_modification === true || is_creation === true) {
if (options.use_bulk_get === true) {
document_list.push({
method: "get",
parameter_list: [key]
});
document_status_list.push({
is_creation: is_creation,
is_modification: is_modification
});
} else {
checkSignatureDifference(queue, source, destination, key,
options.conflict_force,
options.conflict_revert,
options.conflict_ignore,
is_creation, is_modification,
source.get.bind(source),
options);
}
}
}
}
if (options.check_deletion === true) {
for (key in signature_dict) {
if (signature_dict.hasOwnProperty(key)) {
if (!local_dict.hasOwnProperty(key)) {
checkLocalDeletion(queue, destination, key, source,
options.conflict_force,
options.conflict_revert,
options.conflict_ignore,
options);
}
}
}
}
if ((options.use_bulk_get === true) && (document_list.length !== 0)) {
checkBulkSignatureDifference(queue, source, destination,
document_list, document_status_list,
options,
options.conflict_force,
options.conflict_revert,
options.conflict_ignore);
}
});
}
return new RSVP.Queue()
.push(function () {
// Ensure that the document storage is usable
return context._signature_sub_storage.__storage._sub_storage.get(
context._signature_hash
);
})
.push(undefined, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return context._signature_sub_storage.__storage._sub_storage.put(
context._signature_hash,
{}
);
}
throw error;
})
.push(function () {
return RSVP.all([
// Don't repair local_sub_storage twice
// context._signature_sub_storage.repair.apply(
// context._signature_sub_storage,
// argument_list
// ),
context._local_sub_storage.repair.apply(
context._local_sub_storage,
argument_list
),
context._remote_sub_storage.repair.apply(
context._remote_sub_storage,
argument_list
)
]);
})
.push(function () {
if (context._check_local_modification ||
context._check_local_creation ||
context._check_local_deletion) {
return pushStorage(context._local_sub_storage,
context._remote_sub_storage,
{
use_post: context._use_remote_post,
conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_LOCAL),
conflict_revert: (context._conflict_handling ===
CONFLICT_KEEP_REMOTE),
conflict_ignore: (context._conflict_handling ===
CONFLICT_CONTINUE),
check_modification: context._check_local_modification,
check_creation: context._check_local_creation,
check_deletion: context._check_local_deletion
});
}
})
.push(function () {
// Autoactivate bulk if substorage implements it
// Keep it like this until the bulk API is stabilized
var use_bulk_get = false;
try {
use_bulk_get = context._remote_sub_storage.hasCapacity("bulk");
} catch (error) {
if (!((error instanceof jIO.util.jIOError) &&
(error.status_code === 501))) {
throw error;
}
}
if (context._check_remote_modification ||
context._check_remote_creation ||
context._check_remote_deletion) {
return pushStorage(context._remote_sub_storage,
context._local_sub_storage, {
use_bulk_get: use_bulk_get,
use_revert_post: context._use_remote_post,
conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_REMOTE),
conflict_revert: (context._conflict_handling ===
CONFLICT_KEEP_LOCAL),
conflict_ignore: (context._conflict_handling ===
CONFLICT_CONTINUE),
check_modification: context._check_remote_modification,
check_creation: context._check_remote_creation,
check_deletion: context._check_remote_deletion
});
}
});
};
jIO.addStorage('replicate', ReplicateStorage);
}(jIO, RSVP, Rusha, jIO.util.stringify));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/revisionstorage.js 0000664 0000000 0000000 00000103420 13037644422 0031314 0 ustar 00root root 0000000 0000000 /*jslint indent: 2, maxlen: 80, nomen: true */
/*global jIO, hex_sha256, define */
/**
* JIO Revision Storage.
* It manages document version and can generate conflicts.
* Description:
* {
* "type": "revision",
* "sub_storage":
* }
*/
// define([module_name], [dependencies], module);
(function (dependencies, module) {
"use strict";
if (typeof define === 'function' && define.amd) {
return define(dependencies, module);
}
module(jIO, {hex_sha256: hex_sha256});
}(['jio', 'sha256'], function (jIO, sha256) {
"use strict";
var tool = {
"readBlobAsBinaryString": jIO.util.readBlobAsBinaryString,
"uniqueJSONStringify": jIO.util.uniqueJSONStringify
};
jIO.addStorage("revision", function (spec) {
var that = this, priv = {};
spec = spec || {};
// ATTRIBUTES //
priv.doc_tree_suffix = ".revision_tree.json";
priv.sub_storage = spec.sub_storage;
// METHODS //
/**
* Clones an object in deep (without functions)
* @method clone
* @param {any} object The object to clone
* @return {any} The cloned object
*/
priv.clone = function (object) {
var tmp = JSON.stringify(object);
if (tmp === undefined) {
return undefined;
}
return JSON.parse(tmp);
};
/**
* 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();
};
/**
* Generates a hash code of a string
* @method hashCode
* @param {string} string The string to hash
* @return {string} The string hash code
*/
priv.hashCode = function (string) {
return sha256.hex_sha256(string);
};
/**
* Checks a revision format
* @method checkDocumentRevisionFormat
* @param {object} doc The document object
* @return {object} null if ok, else error object
*/
priv.checkDocumentRevisionFormat = function (doc) {
var send_error = function (message) {
return {
"status": 409,
"message": message,
"reason": "Wrong revision"
};
};
if (typeof doc._rev === "string") {
if (/^[0-9]+-[0-9a-zA-Z]+$/.test(doc._rev) === false) {
return send_error("The document revision does not match " +
"^[0-9]+-[0-9a-zA-Z]+$");
}
}
if (typeof doc._revs === "object") {
if (typeof doc._revs.start !== "number" ||
typeof doc._revs.ids !== "object" ||
typeof doc._revs.ids.length !== "number") {
return send_error(
"The document revision history is not well formated"
);
}
}
if (typeof doc._revs_info === "object") {
if (typeof doc._revs_info.length !== "number") {
return send_error("The document revision information " +
"is not well formated");
}
}
};
/**
* Creates a new document tree
* @method newDocTree
* @return {object} The new document tree
*/
priv.newDocTree = function () {
return {"children": []};
};
/**
* Convert revs_info to a simple revisions history
* @method revsInfoToHistory
* @param {array} revs_info The revs info
* @return {object} The revisions history
*/
priv.revsInfoToHistory = function (revs_info) {
var i, revisions = {
"start": 0,
"ids": []
};
revs_info = revs_info || [];
if (revs_info.length > 0) {
revisions.start = parseInt(revs_info[0].rev.split('-')[0], 10);
}
for (i = 0; i < revs_info.length; i += 1) {
revisions.ids.push(revs_info[i].rev.split('-')[1]);
}
return revisions;
};
/**
* Convert the revision history object to an array of revisions.
* @method revisionHistoryToList
* @param {object} revs The revision history
* @return {array} The revision array
*/
priv.revisionHistoryToList = function (revs) {
var i, start = revs.start, new_list = [];
for (i = 0; i < revs.ids.length; i += 1, start -= 1) {
new_list.push(start + "-" + revs.ids[i]);
}
return new_list;
};
/**
* Convert revision list to revs info.
* @method revisionListToRevsInfo
* @param {array} revision_list The revision list
* @param {object} doc_tree The document tree
* @return {array} The document revs info
*/
priv.revisionListToRevsInfo = function (revision_list, doc_tree) {
var revisionListToRevsInfoRec, revs_info = [], j;
for (j = 0; j < revision_list.length; j += 1) {
revs_info.push({"rev": revision_list[j], "status": "missing"});
}
revisionListToRevsInfoRec = function (index, doc_tree) {
var child, i;
if (index < 0) {
return;
}
for (i = 0; i < doc_tree.children.length; i += 1) {
child = doc_tree.children[i];
if (child.rev === revision_list[index]) {
revs_info[index].status = child.status;
revisionListToRevsInfoRec(index - 1, child);
}
}
};
revisionListToRevsInfoRec(revision_list.length - 1, doc_tree);
return revs_info;
};
/**
* Update a document metadata revision properties
* @method fillDocumentRevisionProperties
* @param {object} doc The document object
* @param {object} doc_tree The document tree
*/
priv.fillDocumentRevisionProperties = function (doc, doc_tree) {
if (doc._revs_info) {
doc._revs = priv.revsInfoToHistory(doc._revs_info);
} else if (doc._revs) {
doc._revs_info = priv.revisionListToRevsInfo(
priv.revisionHistoryToList(doc._revs),
doc_tree
);
} else if (doc._rev) {
doc._revs_info = priv.getRevisionInfo(doc._rev, doc_tree);
doc._revs = priv.revsInfoToHistory(doc._revs_info);
} else {
doc._revs_info = [];
doc._revs = {"start": 0, "ids": []};
}
if (doc._revs.start > 0) {
doc._rev = doc._revs.start + "-" + doc._revs.ids[0];
} else {
delete doc._rev;
}
};
/**
* Generates the next revision of a document.
* @methode generateNextRevision
* @param {object} doc The document metadata
* @param {boolean} deleted_flag The deleted flag
* @return {array} 0:The next revision number and 1:the hash code
*/
priv.generateNextRevision = function (doc, deleted_flag) {
var string, revision_history, revs_info;
doc = priv.clone(doc) || {};
revision_history = doc._revs;
revs_info = doc._revs_info;
delete doc._rev;
delete doc._revs;
delete doc._revs_info;
string = tool.uniqueJSONStringify(doc) +
tool.uniqueJSONStringify(revision_history) +
JSON.stringify(deleted_flag ? true : false);
revision_history.start += 1;
revision_history.ids.unshift(priv.hashCode(string));
doc._revs = revision_history;
doc._rev = revision_history.start + "-" + revision_history.ids[0];
revs_info.unshift({
"rev": doc._rev,
"status": deleted_flag ? "deleted" : "available"
});
doc._revs_info = revs_info;
return doc;
};
/**
* Gets the revs info from the document tree
* @method getRevisionInfo
* @param {string} revision The revision to search for
* @param {object} doc_tree The document tree
* @return {array} The revs info
*/
priv.getRevisionInfo = function (revision, doc_tree) {
var getRevisionInfoRec;
getRevisionInfoRec = function (doc_tree) {
var i, child, revs_info;
for (i = 0; i < doc_tree.children.length; i += 1) {
child = doc_tree.children[i];
if (child.rev === revision) {
return [{"rev": child.rev, "status": child.status}];
}
revs_info = getRevisionInfoRec(child);
if (revs_info.length > 0 || revision === undefined) {
revs_info.push({"rev": child.rev, "status": child.status});
return revs_info;
}
}
return [];
};
return getRevisionInfoRec(doc_tree);
};
priv.updateDocumentTree = function (doc, doc_tree) {
var revs_info, updateDocumentTreeRec;
doc = priv.clone(doc);
revs_info = doc._revs_info;
updateDocumentTreeRec = function (doc_tree, revs_info) {
var i, child, info;
if (revs_info.length === 0) {
return;
}
info = revs_info.pop();
for (i = 0; i < doc_tree.children.length; i += 1) {
child = doc_tree.children[i];
if (child.rev === info.rev) {
return updateDocumentTreeRec(child, revs_info);
}
}
doc_tree.children.unshift({
"rev": info.rev,
"status": info.status,
"children": []
});
updateDocumentTreeRec(doc_tree.children[0], revs_info);
};
updateDocumentTreeRec(doc_tree, priv.clone(revs_info));
};
priv.send = function (command, method, doc, option, callback) {
var storage = command.storage(priv.sub_storage);
function onSuccess(success) {
callback(undefined, success);
}
function onError(err) {
callback(err, undefined);
}
if (method === 'allDocs') {
storage.allDocs(option).then(onSuccess, onError);
} else {
storage[method](doc, option).then(onSuccess, onError);
}
};
priv.getWinnerRevsInfo = function (doc_tree) {
var revs_info = [], getWinnerRevsInfoRec;
getWinnerRevsInfoRec = function (doc_tree, tmp_revs_info) {
var i;
if (doc_tree.rev) {
tmp_revs_info.unshift({
"rev": doc_tree.rev,
"status": doc_tree.status
});
}
if (doc_tree.children.length === 0) {
if (revs_info.length === 0 ||
(revs_info[0].status !== "available" &&
tmp_revs_info[0].status === "available") ||
(tmp_revs_info[0].status === "available" &&
revs_info.length < tmp_revs_info.length)) {
revs_info = priv.clone(tmp_revs_info);
}
}
for (i = 0; i < doc_tree.children.length; i += 1) {
getWinnerRevsInfoRec(doc_tree.children[i], tmp_revs_info);
}
tmp_revs_info.shift();
};
getWinnerRevsInfoRec(doc_tree, []);
return revs_info;
};
priv.getConflicts = function (revision, doc_tree) {
var conflicts = [], getConflictsRec;
getConflictsRec = function (doc_tree) {
var i;
if (doc_tree.rev === revision) {
return;
}
if (doc_tree.children.length === 0) {
if (doc_tree.status !== "deleted") {
conflicts.push(doc_tree.rev);
}
}
for (i = 0; i < doc_tree.children.length; i += 1) {
getConflictsRec(doc_tree.children[i]);
}
};
getConflictsRec(doc_tree);
return conflicts.length === 0 ? undefined : conflicts;
};
priv.get = function (command, doc, option, callback) {
priv.send(command, "get", doc, option, callback);
};
priv.put = function (command, doc, option, callback) {
priv.send(command, "put", doc, option, callback);
};
priv.remove = function (command, doc, option, callback) {
priv.send(command, "remove", doc, option, callback);
};
priv.getAttachment = function (command, attachment, option, callback) {
priv.send(command, "getAttachment", attachment, option, callback);
};
priv.putAttachment = function (command, attachment, option, callback) {
priv.send(command, "putAttachment", attachment, option, callback);
};
priv.removeAttachment = function (command, attachment, option, callback) {
priv.send(command, "removeAttachment", attachment, option, callback);
};
priv.getDocument = function (command, doc, option, callback) {
doc = priv.clone(doc);
doc._id = doc._id + "." + doc._rev;
delete doc._attachment;
delete doc._rev;
delete doc._revs;
delete doc._revs_info;
priv.get(command, doc, option, callback);
};
priv.putDocument = function (command, doc, option, callback) {
doc = priv.clone(doc);
doc._id = doc._id + "." + doc._rev;
delete doc._attachment;
delete doc._data;
delete doc._mimetype;
delete doc._content_type;
delete doc._rev;
delete doc._revs;
delete doc._revs_info;
priv.put(command, doc, option, callback);
};
priv.getRevisionTree = function (command, doc, option, callback) {
doc = priv.clone(doc);
doc._id = doc._id + priv.doc_tree_suffix;
priv.get(command, doc, option, function (err, response) {
if (err) {
return callback(err, response);
}
if (response.data && response.data.children) {
response.data.children = JSON.parse(response.data.children);
}
return callback(err, response);
});
};
priv.getAttachmentList = function (command, doc, option, callback) {
var attachment_id, dealResults, state = "ok", result_list = [], count = 0;
dealResults = function (attachment_id, attachment_meta) {
return function (err, response) {
if (state !== "ok") {
return;
}
count -= 1;
if (err) {
if (err.status === 404) {
result_list.push(undefined);
} else {
state = "error";
return callback(err, undefined);
}
}
result_list.push({
"_attachment": attachment_id,
"_data": response.data,
"_content_type": attachment_meta.content_type
});
if (count === 0) {
state = "finished";
callback(undefined, {"data": result_list});
}
};
};
for (attachment_id in doc._attachments) {
if (doc._attachments.hasOwnProperty(attachment_id)) {
count += 1;
priv.getAttachment(
command,
{"_id": doc._id, "_attachment": attachment_id},
option,
dealResults(attachment_id, doc._attachments[attachment_id])
);
}
}
if (count === 0) {
callback(undefined, {"data": []});
}
};
priv.putAttachmentList = function (command, doc, option,
attachment_list, callback) {
var i, dealResults, state = "ok", count = 0, attachment;
attachment_list = attachment_list || [];
dealResults = function () {
return function (err) {
if (state !== "ok") {
return;
}
count -= 1;
if (err) {
state = "error";
return callback(err, undefined);
}
if (count === 0) {
state = "finished";
callback(undefined, {});
}
};
};
for (i = 0; i < attachment_list.length; i += 1) {
attachment = attachment_list[i];
if (attachment !== undefined) {
count += 1;
attachment._id = doc._id + "." + doc._rev;
priv.putAttachment(command, attachment, option, dealResults(i));
}
}
if (count === 0) {
return callback(undefined, {});
}
};
priv.putDocumentTree = function (command, doc, option, doc_tree, callback) {
doc_tree = priv.clone(doc_tree);
doc_tree._id = doc._id + priv.doc_tree_suffix;
if (doc_tree.children) {
doc_tree.children = JSON.stringify(doc_tree.children);
}
priv.put(command, doc_tree, option, callback);
};
priv.notFoundError = function (message, reason) {
return {
"status": 404,
"message": message,
"reason": reason
};
};
priv.conflictError = function (message, reason) {
return {
"status": 409,
"message": message,
"reason": reason
};
};
priv.revisionGenericRequest = function (command, doc, option,
specific_parameter, onEnd) {
var prev_doc, doc_tree, attachment_list, callback = {};
if (specific_parameter.doc_id) {
doc._id = specific_parameter.doc_id;
}
if (specific_parameter.attachment_id) {
doc._attachment = specific_parameter.attachment_id;
}
callback.begin = function () {
var check_error;
doc._id = doc._id || priv.generateUuid(); // XXX should not generate id
if (specific_parameter.revision_needed && !doc._rev) {
return onEnd(priv.conflictError(
"Document update conflict",
"No document revision was provided"
), undefined);
}
// check revision format
check_error = priv.checkDocumentRevisionFormat(doc);
if (check_error !== undefined) {
return onEnd(check_error, undefined);
}
priv.getRevisionTree(command, doc, option, callback.getRevisionTree);
};
callback.getRevisionTree = function (err, response) {
var winner_info, previous_revision, generate_new_revision;
previous_revision = doc._rev;
generate_new_revision = doc._revs || doc._revs_info ? false : true;
if (err) {
if (err.status !== 404) {
err.message = "Cannot get document revision tree";
return onEnd(err, undefined);
}
}
doc_tree = (response && response.data) || priv.newDocTree();
if (specific_parameter.get || specific_parameter.getAttachment) {
if (!doc._rev) {
winner_info = priv.getWinnerRevsInfo(doc_tree);
if (winner_info.length === 0) {
return onEnd(priv.notFoundError(
"Document not found",
"missing"
), undefined);
}
if (winner_info[0].status === "deleted") {
return onEnd(priv.notFoundError(
"Document not found",
"deleted"
), undefined);
}
doc._rev = winner_info[0].rev;
}
priv.fillDocumentRevisionProperties(doc, doc_tree);
return priv.getDocument(command, doc, option, callback.getDocument);
}
priv.fillDocumentRevisionProperties(doc, doc_tree);
if (generate_new_revision) {
if (previous_revision && doc._revs_info.length === 0) {
// the document history has changed, it means that the document
// revision was wrong. Add a pseudo history to the document
doc._rev = previous_revision;
doc._revs = {
"start": parseInt(previous_revision.split("-")[0], 10),
"ids": [previous_revision.split("-")[1]]
};
doc._revs_info = [{"rev": previous_revision, "status": "missing"}];
}
doc = priv.generateNextRevision(
doc,
specific_parameter.remove
);
}
if (doc._revs_info.length > 1) {
prev_doc = {
"_id": doc._id,
"_rev": doc._revs_info[1].rev
};
if (!generate_new_revision && specific_parameter.putAttachment) {
prev_doc._rev = doc._revs_info[0].rev;
}
}
// force revs_info status
doc._revs_info[0].status = (specific_parameter.remove ?
"deleted" : "available");
priv.updateDocumentTree(doc, doc_tree);
if (prev_doc) {
return priv.getDocument(command, prev_doc,
option, callback.getDocument);
}
if (specific_parameter.remove || specific_parameter.removeAttachment) {
return onEnd(priv.notFoundError(
"Unable to remove an inexistent document",
"missing"
), undefined);
}
priv.putDocument(command, doc, option, callback.putDocument);
};
callback.getDocument = function (err, res_doc) {
var k, conflicts;
if (err) {
if (err.status === 404) {
if (specific_parameter.remove ||
specific_parameter.removeAttachment) {
return onEnd(priv.conflictError(
"Document update conflict",
"Document is missing"
), undefined);
}
if (specific_parameter.get) {
return onEnd(priv.notFoundError(
"Unable to find the document",
"missing"
), undefined);
}
res_doc = {"data": {}};
} else {
err.message = "Cannot get document";
return onEnd(err, undefined);
}
}
res_doc = res_doc.data;
if (specific_parameter.get) {
res_doc._id = doc._id;
res_doc._rev = doc._rev;
if (option.conflicts === true) {
conflicts = priv.getConflicts(doc._rev, doc_tree);
if (conflicts) {
res_doc._conflicts = conflicts;
}
}
if (option.revs === true) {
res_doc._revisions = doc._revs;
}
if (option.revs_info === true) {
res_doc._revs_info = doc._revs_info;
}
return onEnd(undefined, {"data": res_doc});
}
if (specific_parameter.putAttachment ||
specific_parameter.removeAttachment) {
// copy metadata (not beginning by "_" to document
for (k in res_doc) {
if (res_doc.hasOwnProperty(k) && !k.match("^_")) {
doc[k] = res_doc[k];
}
}
}
if (specific_parameter.remove) {
priv.putDocumentTree(command, doc, option,
doc_tree, callback.putDocumentTree);
} else {
priv.getAttachmentList(command, res_doc, option,
callback.getAttachmentList);
}
};
callback.getAttachmentList = function (err, res_list) {
var i, attachment_found = false;
if (err) {
err.message = "Cannot get attachment";
return onEnd(err, undefined);
}
res_list = res_list.data;
attachment_list = res_list || [];
if (specific_parameter.getAttachment) {
// getting specific attachment
for (i = 0; i < attachment_list.length; i += 1) {
if (attachment_list[i] &&
doc._attachment ===
attachment_list[i]._attachment) {
return onEnd(undefined, {"data": attachment_list[i]._data});
}
}
return onEnd(priv.notFoundError(
"Unable to get an inexistent attachment",
"missing"
), undefined);
}
if (specific_parameter.remove_from_attachment_list) {
// removing specific attachment
for (i = 0; i < attachment_list.length; i += 1) {
if (attachment_list[i] &&
specific_parameter.remove_from_attachment_list._attachment ===
attachment_list[i]._attachment) {
attachment_found = true;
attachment_list[i] = undefined;
break;
}
}
if (!attachment_found) {
return onEnd(priv.notFoundError(
"Unable to remove an inexistent attachment",
"missing"
), undefined);
}
}
priv.putDocument(command, doc, option, callback.putDocument);
};
callback.putDocument = function (err) {
var i, attachment_found = false;
if (err) {
err.message = "Cannot post the document";
return onEnd(err, undefined);
}
if (specific_parameter.add_to_attachment_list) {
// adding specific attachment
attachment_list = attachment_list || [];
for (i = 0; i < attachment_list.length; i += 1) {
if (attachment_list[i] &&
specific_parameter.add_to_attachment_list._attachment ===
attachment_list[i]._attachment) {
attachment_found = true;
attachment_list[i] = specific_parameter.add_to_attachment_list;
break;
}
}
if (!attachment_found) {
attachment_list.unshift(specific_parameter.add_to_attachment_list);
}
}
priv.putAttachmentList(
command,
doc,
option,
attachment_list,
callback.putAttachmentList
);
};
callback.putAttachmentList = function (err) {
if (err) {
err.message = "Cannot copy attacments to the document";
return onEnd(err, undefined);
}
priv.putDocumentTree(command, doc, option,
doc_tree, callback.putDocumentTree);
};
callback.putDocumentTree = function (err) {
var response_object;
if (err) {
err.message = "Cannot update the document history";
return onEnd(err, undefined);
}
response_object = {
"id": doc._id,
"rev": doc._rev
};
if (specific_parameter.putAttachment ||
specific_parameter.removeAttachment ||
specific_parameter.getAttachment) {
response_object.attachment = doc._attachment;
}
onEnd(undefined, response_object);
// if (option.keep_revision_history !== true) {
// // priv.remove(command, prev_doc, option, function () {
// // - change "available" status to "deleted"
// // - remove attachments
// // - done, no callback
// // });
// }
};
callback.begin();
};
/**
* Post the document metadata and create or update a document tree.
* Options:
* - {boolean} keep_revision_history To keep the previous revisions
* (false by default) (NYI).
* @method post
* @param {object} command The JIO command
*/
that.post = function (command, metadata, option) {
priv.revisionGenericRequest(
command,
metadata,
option,
{},
function (err, response) {
if (err) {
return command.error(err);
}
command.success({"id": response.id, "rev": response.rev});
}
);
};
/**
* Put the document metadata and create or update a document tree.
* Options:
* - {boolean} keep_revision_history To keep the previous revisions
* (false by default) (NYI).
* @method put
* @param {object} command The JIO command
*/
that.put = function (command, metadata, option) {
priv.revisionGenericRequest(
command,
metadata,
option,
{},
function (err, response) {
if (err) {
return command.error(err);
}
command.success({"rev": response.rev});
}
);
};
that.putAttachment = function (command, param, option) {
tool.readBlobAsBinaryString(param._blob).then(function (event) {
param._content_type = param._blob.type;
param._data = event.target.result;
delete param._blob;
priv.revisionGenericRequest(
command,
param,
option,
{
"doc_id": param._id,
"attachment_id": param._attachment,
"add_to_attachment_list": {
"_attachment": param._attachment,
"_content_type": param._content_type,
"_data": param._data
},
"putAttachment": true
},
function (err, response) {
if (err) {
return command.error(err);
}
command.success({"rev": response.rev});
}
);
}, function () {
command.error("conflict", "broken blob", "Cannot read data to put");
});
};
that.remove = function (command, param, option) {
priv.revisionGenericRequest(
command,
param,
option,
{
"revision_needed": true,
"remove": true
},
function (err, response) {
if (err) {
return command.error(err);
}
command.success({"rev": response.rev});
}
);
};
that.removeAttachment = function (command, param, option) {
priv.revisionGenericRequest(
command,
param,
option,
{
"doc_id": param._id,
"attachment_id": param._attachment,
"revision_needed": true,
"removeAttachment": true,
"remove_from_attachment_list": {
"_attachment": param._attachment
}
},
function (err, response) {
if (err) {
return command.error(err);
}
command.success({"rev": response.rev});
}
);
};
that.get = function (command, param, option) {
priv.revisionGenericRequest(
command,
param,
option,
{
"get": true
},
function (err, response) {
if (err) {
return command.error(err);
}
command.success({"data": response.data});
}
);
};
that.getAttachment = function (command, param, option) {
priv.revisionGenericRequest(
command,
param,
option,
{
"doc_id": param._id,
"attachment_id": param._attachment,
"getAttachment": true
},
function (err, response) {
if (err) {
return command.error(err);
}
command.success({"data": response.data});
}
);
};
that.allDocs = function (command, param, option) {
/*jslint unparam: true */
var rows, result = {"total_rows": 0, "rows": []}, functions = {};
functions.finished = 0;
functions.falseResponseGenerator = function (response, callback) {
callback(undefined, response);
};
functions.fillResultGenerator = function (doc_id) {
return function (err, doc_tree) {
var document_revision, row, revs_info;
if (err) {
return command.error(err);
}
doc_tree = doc_tree.data;
if (typeof doc_tree.children === 'string') {
doc_tree.children = JSON.parse(doc_tree.children);
}
revs_info = priv.getWinnerRevsInfo(doc_tree);
document_revision =
rows.document_revisions[doc_id + "." + revs_info[0].rev];
if (document_revision) {
row = {
"id": doc_id,
"key": doc_id,
"value": {
"rev": revs_info[0].rev
}
};
if (document_revision.doc && option.include_docs) {
document_revision.doc._id = doc_id;
document_revision.doc._rev = revs_info[0].rev;
row.doc = document_revision.doc;
}
result.rows.push(row);
result.total_rows += 1;
}
functions.success();
};
};
functions.success = function () {
functions.finished -= 1;
if (functions.finished === 0) {
command.success({"data": result});
}
};
priv.send(command, "allDocs", null, option, function (err, response) {
var i, row, selector, selected;
if (err) {
return command.error(err);
}
response = response.data;
selector = /\.revision_tree\.json$/;
rows = {
"revision_trees": {
// id.revision_tree.json: {
// id: blabla
// doc: {...}
// }
},
"document_revisions": {
// id.rev: {
// id: blabla
// rev: 1-1
// doc: {...}
// }
}
};
while (response.rows.length > 0) {
// filling rows
row = response.rows.shift();
selected = selector.exec(row.id);
if (selected) {
selected = selected.input.substring(0, selected.index);
// this is a revision tree
rows.revision_trees[row.id] = {
"id": selected
};
if (row.doc) {
rows.revision_trees[row.id].doc = row.doc;
}
} else {
// this is a simple revision
rows.document_revisions[row.id] = {
"id": row.id.split(".").slice(0, -1),
"rev": row.id.split(".").slice(-1)
};
if (row.doc) {
rows.document_revisions[row.id].doc = row.doc;
}
}
}
functions.finished += 1;
for (i in rows.revision_trees) {
if (rows.revision_trees.hasOwnProperty(i)) {
functions.finished += 1;
if (rows.revision_trees[i].doc) {
functions.falseResponseGenerator(
{"data": rows.revision_trees[i].doc},
functions.fillResultGenerator(rows.revision_trees[i].id)
);
} else {
priv.getRevisionTree(
command,
{"_id": rows.revision_trees[i].id},
option,
functions.fillResultGenerator(rows.revision_trees[i].id)
);
}
}
}
functions.success();
});
};
// XXX
that.check = function (command) {
command.success();
};
// XXX
that.repair = function (command) {
command.success();
};
}); // end RevisionStorage
}));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/s3storage.js 0000664 0000000 0000000 00000071371 13037644422 0030014 0 ustar 00root root 0000000 0000000 /*jslint indent: 2, maxlen: 80, nomen: true */
/*global define, jIO, btoa, b64_hmac_sha1, jQuery, XMLHttpRequest, XHRwrapper,
FormData*/
/**
* JIO S3 Storage. Type = "s3".
* Amazon S3 "database" storage.
*/
// define([module_name], [dependencies], module);
(function (dependencies, module) {
"use strict";
if (typeof define === 'function' && define.amd) {
return define(dependencies, module);
}
module(jIO, jQuery, {b64_hmac_sha1: b64_hmac_sha1});
}(['jio', 'jquery', 'sha1'], function (jIO, $, sha1) {
"use strict";
var b64_hmac_sha1 = sha1.b64_hmac_sha1;
jIO.addStorage("s3", function (spec) {
var that, priv = {}, lastDigest, isDelete;
that = this;
//nomenclature param
// param._id,
// ._attachment,
// ._blob
// attributes
priv.username = spec.username || '';
priv.AWSIdentifier = spec.AWSIdentifier || '';
priv.password = spec.password || '';
priv.server = spec.server || '';
/*||> "private,
public-read,
public-read-write,
authenticated-read,
bucket-owner-read,
bucket-owner-full-control" <||*/
priv.acl = spec.acl || '';
priv.actionStatus = spec.actionStatus || '';
priv.contenTType = spec.contenTType || '';
/**
* 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.secureDocId = function (string) {
var split = string.split('/'), i;
if (split[0] === '') {
split = split.slice(1);
}
for (i = 0; i < split.length; i += 1) {
if (split[i] === '') {
return '';
}
}
return split.join('%2F');
};
priv.fileNameToIds = function (resourcename) {
var split, el, id = "", attmt = "", last;
split = resourcename.split('.');
function replaceAndNotLast() {
last = false;
return '.';
}
/*jslint ass: true */
while ((el = split.shift()) !== undefined) {
last = true;
el = el.replace(/__/g, '%2595');
el = el.replace(/_$/, replaceAndNotLast);
id += el.replace(/%2595/g, '_');
if (last) {
break;
}
}
attmt = split.join('.');
return [id, attmt];
};
priv.idsToFileName = function (document_id, attachment_id) {
document_id = encodeURI(document_id).
replace(/\//g, "%2F").
replace(/\?/g, "%3F");
document_id = encodeURI(document_id).
replace(/_/g, "__").
replace(/\./g, "_.");
if (attachment_id) {
attachment_id = encodeURI(attachment_id).
replace(/\//g, "%2F").
replace(/\?/g, "%3F");
return document_id + "." + attachment_id;
}
return document_id;
};
/**
* Removes the last character if it is a "/". "/a/b/c/" become "/a/b/c"
* @method removeSlashIfLast
* @param {string} string The string to modify
* @return {string} The modified string
*/
priv.removeSlashIfLast = function (string) {
if (string[string.length - 1] === "/") {
return string.slice(0, -1);
}
return string;
};
/**
* Generate a new uuid
*
* @method generateUuid
* @private
* @return {String} The new uuid
*/
function generateUuid() {
function S4() {
/* 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();
}
that.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
*/
that.objectIsEmpty = function (obj) {
var k;
for (k in obj) {
if (obj.hasOwnProperty(k)) {
return false;
}
}
return true;
};
// ===================== overrides ======================
that.specToStore = function () {
return {
"username": priv.username,
"password": priv.password,
"server": priv.server,
"acl": priv.acl
};
};
that.validateState = function () {
// xxx complete error message
// jjj completion below
if (typeof priv.AWSIdentifier === "string" && priv.AWSIdentifier === '') {
return 'Need at least one parameter "Aws login".';
}
if (typeof priv.password === "string" && priv.password === '') {
return 'Need at least one parameter "password".';
}
if (typeof priv.server === "string" && priv.server === '') {
return 'Need at least one parameter "server".';
}
return '';
};
// =================== S3 Specifics =================
/**
* Encoding the signature using a stringToSign
* Encoding the policy
* @method buildStringToSign
* @param {string} http_verb The HTTP method
* @param {string} content_md5 The md5 content
* @param {string} content_type The content type
* @param {number} expires The expires time
* @param {string} x_amz_headers The specific amazon headers
* @param {string} path_key The path of the document
* @return {string} The generated signature
*/
// xxx no need to make it public, use private -> "priv" (not "that")
priv.buildStringToSign = function (http_verb, content_md5, content_type,
expires, x_amz_headers, path_key) {
//example :
// var StringToSign = S3.buildStringToSign(S3.httpVerb,'','','',
// 'x-amz-date:'+S3.requestUTC,'/jio1st/prive.json');
var StringToSign = http_verb + '\n'
+ content_md5 + '\n'//content-md5
+ content_type + '\n'//content-type
+ expires + '\n'//expires
+ x_amz_headers + '\n'//x-amz headers
+ path_key;//path key
return StringToSign;
};
that.encodePolicy = function () {
//generates the policy
//enables the choice for the http response code
var s3_policy;
s3_policy = {
"expiration": "2020-01-01T00:00:00Z",
"conditions": [
{"bucket": priv.server },
["starts-with", "$key", ""],
{"acl": priv.acl },
["starts-with", "$Content-Type", ""]
]
};
//base64 encoding of the policy (native base64 js >>
// .btoa() = encode, .atob() = decode)
priv.b64_policy = btoa(JSON.stringify(s3_policy));
//generates the signature value using the policy and the secret access key
//use of sha1.js to generate the signature
//Signature = that.signature(priv.b64_policy);
};
that.signature = function (string) {
var Signature = b64_hmac_sha1(priv.password, string);
return Signature;
};
function xhr_onreadystatechange(docId,
command,
obj,
http,
jio,
isAttachment,
callback) {
obj.onreadystatechange = function () {
var response, err = '';
if (obj.readyState === 4) {
if (this.status === 204 || this.status === 201 ||
this.status === 200) {
switch (http) {
case "POST":
command.success(this.status, {id: docId});
break;
case 'PUT':
if (jio === true) {
command.success(this.status);
} else {
callback(this.responseText);
}
break;
case 'GET':
if (jio === true) {
if (isAttachment === true) {
//méthode that.getAttachment
response = this.response;
command.success(
this.status,
{'data': response, 'digest': lastDigest}
);
} else {
// this is not an attachment
// that.get method
response = JSON.parse(this.responseText);
command.success(this.status, {'data': response});
}
} else {
callback(this.responseText);
}
break;
case 'DELETE':
if (jio === true) {
if (isAttachment === false) {
command.success(this.status);
} else {
command.success(this.status);
}
} else {
callback(this.responseText);
}
break;
}
} else {
err = this;
if (this.status === 405) {
//status
//statustext "Not Found"
//error
//reason "reason"
//message "did not work"
err.error = "not_allowed";
command.error(err);
}
if (this.status === 404) {
if (http === 'GET') {
if (jio === true) {
//status
//statustext "Not Found"
//error
//reason "reason"
//message "did not work"
return command.error(
404,
"Not Found",
"File does not exist"
);
}
//not jio
if (isDelete === true) {
isDelete = false;
return command.error(
404,
"Not Found",
"File does not exist"
);
}
callback('404');
} else {
//status
//statustext "Not Found"
//error
//reason "reason"
//message "did not work"
return command.error(
404,
"Not Found",
"File does not exist"
);
}
//fin 404
}
if (this.status === 409) {
//status
//statustext "Not Found"
//error
//reason "reason"
//message "did not work"
return command.error(
409,
"Already Exists",
"File does exist"
);
}
}
}
};
}
priv.updateMeta = function (doc, docid, attachid, action, data) {
/*jslint unparam: true */
doc._attachments = doc._attachments || {};
switch (action) {
case "add":
doc._attachments[attachid] = data;
//nothing happens
doc = JSON.stringify(doc);
break;
case "remove":
if (doc._attachments !== undefined) {
delete doc._attachments[attachid];
}
doc = JSON.stringify(doc);
break;
case "update":
doc._attachments[attachid] = data;
//update happened in the put request
doc = JSON.stringify(doc);
break;
}
return doc;
};
that.encodeAuthorization = function (key) {
//GET oriented method
var requestUTC, httpVerb, StringToSign, Signature;
requestUTC = new Date().toUTCString();
httpVerb = "GET";
StringToSign = priv.buildStringToSign(
httpVerb,
'',
'application/json',
'',
'x-amz-date:' + requestUTC,
'/' + priv.server + '/' + key
);
Signature = b64_hmac_sha1(priv.password, StringToSign);
return Signature;
};
that.XHRwrapper = function (command,
docId,
attachId,
http,
mime,
data,
jio,
is_attachment,
callback) {
var docFile, requestUTC, StringToSign, url, Signature, xhr;
if (command.method === "alldocs") {
docFile = '';
} else {
docFile = priv.idsToFileName(docId,
attachId || undefined);
}
requestUTC = new Date().toUTCString();
StringToSign = priv.buildStringToSign(
http,
'',
mime,
'',
'x-amz-date:' + requestUTC,
'/' + priv.server + '/' + docFile
);
url = 'http://s3.amazonaws.com/' + priv.server + '/' + docFile;
Signature = b64_hmac_sha1(priv.password, StringToSign);
xhr = new XMLHttpRequest();
xhr.open(http, url, true);
xhr.setRequestHeader("HTTP-status-code", "100");
xhr.setRequestHeader("x-amz-date", requestUTC);
xhr.setRequestHeader("Authorization", "AWS "
+ priv.AWSIdentifier
+ ":"
+ Signature);
xhr.setRequestHeader("Content-Type", mime);
if (http === 'GET' && jio === true && is_attachment === true) {
xhr.responseType = 'blob';
} else {
//défaut
xhr.responseType = 'text';
}
xhr_onreadystatechange(docId,
command,
xhr,
http,
jio,
is_attachment,
callback);
if (http === 'PUT') {
xhr.send(data);
} else {
xhr.send(null);
}
};
// ==================== commands ====================
/**
* Create a document in local storage.
* @method post
* @param {object} command The JIO command
**/
that.post = function (command, metadata) {
//as S3 encoding key are directly inserted within the FormData(),
//use of XHRwrapper function ain't pertinent
var doc, doc_id, mime;
doc = metadata;
//doc_id = (!doc._id) ? generateUuid() : doc._id;
doc._id = doc._id || generateUuid();
doc_id = doc._id;
function postDocument() {
var fd, Signature, xhr;
doc_id = priv.idsToFileName(doc_id);
//Meant to deep-serialize in order to avoid
//conflicts due to the multipart enctype
doc = JSON.stringify(doc);
fd = new FormData();
//virtually builds the form fields
//filename
fd.append('key', doc_id);
//file access authorizations
priv.acl = "";
fd.append('acl', priv.acl);
//content-type
priv.contenTType = "text/plain";
fd.append('Content-Type', priv.contenTType);
//allows specification of a success url redirection
//fd.append('success_action_redirect', '');
//allows to specify the http code response if the request is successful
//fd.append('success_action_status', http_response);
//login AWS
fd.append('AWSAccessKeyId', priv.AWSIdentifier);
//exchange policy with the amazon s3 service
//can be common to all uploads or specific
//that.encodePolicy(fd);
that.encodePolicy(fd);
fd.append('policy', priv.b64_policy);
//signature through the base64.hmac.sha1(secret key, policy) method
Signature = b64_hmac_sha1(priv.password, priv.b64_policy);
fd.append('signature', Signature);
//uploaded content !!may must be a string rather than an object
fd.append('file', doc);
xhr = new XMLHttpRequest();
xhr_onreadystatechange(doc_id, command, xhr, 'POST', true, false, '');
xhr.open('POST', 'https://' + priv.server + '.s3.amazonaws.com/', true);
xhr.send(fd);
}
if (doc_id === '' || doc_id === undefined) {
doc._id = generateUuid();
}
mime = 'text/plain; charset=UTF-8';
that.XHRwrapper(command, doc_id, '', 'GET', mime, '', false, false,
function (response) {
if (response === '404') {
postDocument();
} else {
//si ce n'est pas une 404,
//alors on renvoit une erreur 405
return command.error(
409,
"Document already exists",
"Cannot create document"
);
}
});
};
/**
* Get a document or attachment
* @method get
* @param {object} command The JIO command
**/
that.get = function (command, metadata) {
var docId, isJIO, mime;
docId = metadata._id;
isJIO = true;
mime = 'text/plain; charset=UTF-8';
that.XHRwrapper(command, docId, '', 'GET', mime, '', isJIO, false);
};
that.getAttachment = function (command, param) {
var docId, attachId, isJIO, mime;
function getTheAttachment() {
docId = param._id;
attachId = param._attachment;
isJIO = true;
mime = 'text/plain; charset=UTF-8';
that.XHRwrapper(command, docId, attachId, 'GET', mime, '', isJIO, true);
}
function getDoc() {
docId = param._id;
isJIO = false;
mime = 'text/plain; charset=UTF-8';
that.XHRwrapper(command, docId, '', 'GET', mime, '', isJIO, false,
function (response) {
var responseObj = JSON.parse(response)._attachments;
if (responseObj !== undefined) {
if (responseObj[param._attachment] !== undefined) {
lastDigest = JSON.parse(response).
_attachments[param._attachment].digest;
}
}
getTheAttachment();
});
}
getDoc();
//docId = param._id;
//attachId = param._attachment;
//isJIO = true;
//mime = 'text/plain; charset=UTF-8';
//that.XHRwrapper(command, docId, attachId, 'GET', mime,
// '', isJIO, true);
};
/**
* Create or update a document in local storage.
* @method put
* @param {object} command The JIO command
**/
that.put = function (command, metadata) {
var doc, docId, mime;
doc = metadata;
docId = doc._id;
mime = 'text/plain; charset=UTF-8';
//pas d'attachment dans un put simple
function putDocument() {
var attachId, data, isJIO;
attachId = '';
data = JSON.stringify(doc);
isJIO = true;
that.XHRwrapper(command,
docId,
attachId,
'PUT',
mime,
data,
isJIO,
false);
}
that.XHRwrapper(command, docId, '', 'GET', mime, '', false, false,
function (response) {
var responseObj = JSON.parse(response);
if (responseObj._attachments !== undefined) {
doc._attachments = responseObj._attachments;
}
putDocument();
// XXX control non existing document to throw a 201 http code
});
};
that.putAttachment = function (command, param) {
var my_document,
docId,
attachId,
mime,
attachment_data,
attachment_digest,
attachment_mimetype,
attachment_length;
my_document = null;
docId = param._id;
attachId = param._attachment;
mime = param._blob.type;
attachment_data = param._blob;
jIO.util.readBlobAsBinaryString(param._blob).then(function (e) {
var binary_string = e.target.result;
attachment_digest = jIO.util.makeBinaryStringDigest(binary_string);
function putAttachment() {
that.XHRwrapper(
command,
docId,
attachId,
'PUT',
mime,
attachment_data,
false,
true,
function () {
command.success({
// response
"digest": attachment_digest
});
}
);
}
function putDocument() {
var attachment_obj, data, doc;
attachment_mimetype = param._blob.type;
attachment_length = param._blob.size;
attachment_obj = {
//"revpos": 3, // optional
"digest": attachment_digest,
"content_type": attachment_mimetype,
"length": attachment_length
};
data = JSON.parse(my_document);
doc = priv.updateMeta(data, docId, attachId, "add", attachment_obj);
that.XHRwrapper(command, docId, '', 'PUT', mime, doc, false, false,
function () {
putAttachment();
});
}
function getDocument() {
that.XHRwrapper(command, docId, '', 'GET', mime, '', false, false,
function (response) {
if (response === '404') {
return command.error(
404,
"Document does not exist",
"Cannot find document"
);
}
my_document = response;
putDocument();
});
}
getDocument();
});
};
/**
* Remove a document or attachment
* @method remove
* @param {object} command The JIO command
*/
that.remove = function (command, param) {
var docId, mime;
docId = param._id;
mime = 'text/plain; charset=UTF-8';
isDelete = true;
function deleteDocument() {
isDelete = false;
that.XHRwrapper(command, docId, '', 'DELETE', mime, '', true, false,
function () {
command.success({
// response
"ok": true,
"id": docId
//"rev": current_revision
});
}
);
}
function myCallback() {
return;
}
that.XHRwrapper(command, docId, '', 'GET', mime, '', false, false,
function (response) {
var attachKeys, keys;
attachKeys = (JSON.parse(response))._attachments;
for (keys in attachKeys) {
if (attachKeys.hasOwnProperty(keys)) {
that.XHRwrapper(command,
docId,
keys,
'DELETE',
mime,
'',
false,
false,
myCallback
);
}
}
deleteDocument();
}
);
};
that.removeAttachment = function (command, param) {
var my_document,
docId,
attachId,
mime;
my_document = null;
docId = param._id;
attachId = param._attachment;
mime = 'text/plain; charset=UTF-8';
//récupération des variables de l'attachement
//attachment_id = command.getAttachmentId();
//attachment_data = command.getAttachmentData();
//attachment_md5 = command.md5SumAttachmentData();
//attachment_mimetype = command.getAttachmentMimeType();
//attachment_length = command.getAttachmentLength();
function removeAttachment() {
that.XHRwrapper(command, docId, attachId, 'DELETE', mime, '', true,
true, function () {
return;
});
}
function putDocument() {
var data, doc;
//data = JSON.parse(my_document);
data = my_document;
doc = priv.updateMeta(data, docId, attachId, "remove", '');
that.XHRwrapper(command, docId, '', 'PUT', mime, doc,
false, false, function () {
removeAttachment();
});
}
function getDocument() {
that.XHRwrapper(command, docId, '', 'GET', mime, '', false, false,
function (response) {
if (response === '404') {
return command.error(
404,
"missing document",
"This Document does not exist"
);
}
my_document = JSON.parse(response);
if (my_document._attachments === undefined) {
return command.error(
404,
"missing attachment",
"This Document has no attachments"
);
}
if (my_document._attachments[attachId] !== undefined) {
putDocument();
} else {
return command.error(
404,
"missing attachment",
"This Attachment does not exist"
);
}
});
}
getDocument();
};
/**
* Get all filenames belonging to a user from the document index
* @method allDocs
* @param {object} command The JIO command
**/
that.allDocs = function (command, param, options) {
/*jslint unparam: true */
var _succ, my_document, mime;
_succ = command.success;
command.success = function () {
_succ.apply(this, arguments);
};
my_document = null;
mime = 'text/plain; charset=UTF-8';
function makeJSON() {
var keys,
resultTable,
counter,
allDocResponse,
count,
countB,
dealCallback,
errCallback,
i,
keyId,
Signature,
callURL,
requestUTC;
keys = $($.parseXML(my_document)).find('Key');
if (keys.length === 0) {
return command.success({"data":
{
"total_rows": 0,
"rows": []
}
});
}
resultTable = [];
counter = 0;
keys.each(function () {
var $this, filename, docId;
$this = $(this);
filename = $this.context.textContent;
docId = priv.idsToFileName(priv.fileNameToIds(filename)[0]);
if (counter === 0) {
counter += 1;
resultTable.push(docId);
} else if (docId !== resultTable[counter - 1]) {
counter += 1;
resultTable.push(docId);
}
});
allDocResponse = {
// document content will be added to response
"total_rows": resultTable.length,
"rows": []
};
//needed to save the index within the $.ajax.success() callback
count = resultTable.length - 1;
countB = 0;
dealCallback = function (i, countB, allDoc) {
/*jslint unparam: true */
return function (doc, statustext, response) {
allDoc.rows[i].doc = JSON.parse(response.responseText);
if (count === 0) {
return command.success({
"data": allDoc
});
}
count -= 1;
};
};
errCallback = function (jQxhr) {
command.error(
jQxhr.status,
jQxhr.statusText,
"S3 Alldocs failed."
);
};
i = resultTable.length - 1;
if (options.include_docs) {
for (i; i >= 0; i -= 1) {
keyId = resultTable[i];
Signature = that.encodeAuthorization(keyId);
callURL = 'http://' + priv.server + '.s3.amazonaws.com/' + keyId;
requestUTC = new Date().toUTCString();
allDocResponse.rows[i] = {
"id": priv.fileNameToIds(keyId)[0],
"value": {}
};
$.ajax({
contentType : '',
crossdomain : true,
url : callURL,
type : 'GET',
headers : {
'Authorization' : "AWS"
+ " "
+ priv.AWSIdentifier
+ ":"
+ Signature,
'x-amz-date' : requestUTC,
'Content-Type' : 'application/json'
//'Content-MD5' : ''
//'Content-Length' : ,
//'Expect' : ,
//'x-amz-security-token' : ,
},
success : dealCallback(i, countB, allDocResponse),
error : errCallback
});
countB += 1;
}
} else {
for (i; i >= 0; i -= 1) {
keyId = resultTable[i];
allDocResponse.rows[i] = {
"id": priv.fileNameToIds(keyId)[0],
"value": {}
};
}
allDocResponse = {"data": allDocResponse};
command.success(allDocResponse);
}
}
function getXML() {
command.method = 'alldocs';
that.XHRwrapper(command, '', '', 'GET', mime, '', false, false,
function (response) {
my_document = response;
makeJSON();
}
);
}
getXML();
//fin alldocs
};
});
}));
searchableencryptionstorage.js 0000664 0000000 0000000 00000042331 13037644422 0033606 0 ustar 00root root 0000000 0000000 jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage /***********************************************************************
** Written by Abdullatif Shikfa, Alcatel Lucent Bell-Labs France **
** With the invaluable help of Tristan Cavelier, Nexedi **
** 31/01/2014 **
***********************************************************************/
/*jslint indent:2,maxlen:80,nomen:true*/
/*global jIO, define, exports, require, sjcl*/
(function (dependencies, module) {
"use strict";
if (typeof define === 'function' && define.amd) {
return define(dependencies, module);
}
if (typeof exports === 'object') {
return module(
require('jio'),
require('sjcl')
);
}
module(jIO, sjcl);
}([
'jio',
'sjcl'
], function (jIO, sjcl) {
"use strict";
/**
* SearchableEncryptionStorage is a function that creates
* the searchable encryption storage
*
*keywords: a list of keywords associated with the current document, and which
* can be searched with the searchable encryption algorithm.
*nbMaxKeywords: the maximum number of keywords that any document can contain.
*encryptedIndex: an index that represents the keywords in an encrypted form.
* It is a Bloom Filter, which allows queries, without false
* negatives but with a false positive probability.
*errorRate: the false positive rate is bound to 2 to the power -errorRate.
* Increasing the value of errorRate decreases the rate of false
* positives but increases the size of the encryptedIndex.
*password: a user chosen password used to encrypt metadata as well as to
* generate the encryptedIndex.
*/
function SearchableEncryptionStorage(storage_description) {
this._password = storage_description.password; //string
this._errorRate = storage_description.errorRate || 20; //int
this._nbMaxKeywords = storage_description.nbMaxKeywords || 100; //int
this._url = storage_description.url;
this._keywords = storage_description.keywords; //string array
if (typeof this._password !== 'string') {
throw new TypeError("'password' description property is not a string");
}
}
/**
* computeBFLength is a function that computes the length of a Bloom Filter
* depending on the false positive ratio and the maximum number of elements
* (keywords) that it will represent.
* A Bloom Filter is indeed a data structure which is a bit array that
* represents a set of elements.
* The false positive rate is bound to 2 to the power -errorRate, and the
* length is then computed according to the following formula.
*/
function computeBFLength(errorRate, nbMaxKeywords) {
return Math.ceil((errorRate * nbMaxKeywords) / Math.log(2));
}
/**
* intArrayToString is a helper function that converts an array of integers to
* one big string. It basically concatenates all the integers of the array.
*/
/*function intArrayToString(arr) {
var i, result = "";
for (i = 0; i < arr.length; i += 1) {
result = result + arr[i].toString();
}
return result;
}*/
/**
* bigModulo is a helper function that computes the remainder of a large
* integer divided by an operand. The large integer is represented as several
* regular integers (of 32 bits) in big endian in an array.
* The function leverages the modulo operation on integers implemented in
* javascript to perform the modulo on the large integer : it computes the
* modulo of each integer of the array multiplied by the modulo of the
* base (2 to the power 32) to the power of the position in the array.
* However, since javascript encodes integers on 32 bits we have to add another
* trick: we do the computations on half words and we use the function
* sjcl.bitArray.bitSlice which extracts some bits out of a bit array, and we
* and we thus mutliply by half of the base.
*/
function bigModulo(arr, mod) {
var i, result = 0, base = 1, maxIter = (2 * arr.length);
for (i = 0; i < maxIter; i += 1) {
result = result + (
(sjcl.bitArray.bitSlice(arr, i * 16, (i + 1) * 16)[0]) % mod
) * base;
base = (base * Math.pow(2, 16)) % mod;
}
result = result % mod;
return result;
}
/**
* constructBloomFilter is a function that constructs an encrypted Bloom Filter
* representing a set of elements (keywords) with a given password, a given
* false positive ratio and the maximum number of elements that any Bloom
* Filter can contain in our scenario (this is useful so that all documents
* have the same size of bloom filters).
* This function follows the algorithm proposed by Goh in 2004 in his article
* about "secure indexes" and that allows to perform searchable encryption.
* The function first computes the length of the Bloom Filter depending on the
* errorRate and nbMaxKeywords using an auxiliary function computeBFLength
* previously explained.
* It then creates an array of the said length initialized with 0 at all
* positions.
* The array is then filled with ones at certain positions using the following:
* algorithm:
* For each keyword in the array keywords compute errorRate hashes:
* Each hash is the SHA256 function applied to the keyword concatenated
* with the password and the iterator of the hash function (j). The
* resulting digest is an array that is converted to a base64 string and
* concatenated with the id of the documents (to obtain different results
* if a given keyword is found in several documents). The result is then
* taken modulo the length of the Bloom Filter and indicates a position
* in the array which is set to one.
* In the end there are at most bFLength * errorRate 1s in the array (and in
* fact less because several keywords can lead to the same position for
* different hash functions).
*/
function constructBloomFilter(
password,
errorRate,
nbMaxKeywords,
keywords,
id
) {
var bFLength = computeBFLength(errorRate, nbMaxKeywords), result = [], i, j;
for (i = 0; i < bFLength; i += 1) {
result[i] = 0;
}
for (i = 0; i < keywords.length; i += 1) {
for (j = 0; j < errorRate; j += 1) {
result[bigModulo(sjcl.hash.sha256.hash(sjcl.codec.base64.fromBits(
sjcl.hash.sha256.hash(keywords[i] + password + j)
) + id), bFLength)] = 1;
}
}
return result;
}
/**
* constructEncryptedQuery is a function that constructs an encrypted query
* from a keyword and a password. It basically performs the first step of
* adding a word to a Bloom Filter.
* It hashes the keyword errorRate times using different hash functions.
* Each hash is the SHA256 function applied to the keyword concatenated
* with the password and the iterator of the hash function (j). The
* resulting digest is an array that is converted to a base64 string using the
* sjcl.codec.base64.fromBits function.
* In the end, the encrypted query corresponding to a keyword is an array of
* errorRates base64 strings. Note that the query can only be computed by the
* client as it requires knowledge of the secret key.
*/
function constructEncryptedQuery(
password,
errorRate,
keyword
) {
var result = [], j;
for (j = 0; j < errorRate; j += 1) {
result[j] = sjcl.codec.base64.fromBits(sjcl.hash.sha256.hash(
keyword + password + j
));
}
return result;
}
/* // Encrypt a message
function encrypt(plaintext, password) {
var rp = {}, ct, p;
p = {
adata: "",
iter: 1,
mode: "ccm",
ts: 64,
ks: 128,
iv: "t6vxTD/94Lk7DM87LZkPQA==",
cipher: "aes",
salt: "SdieDA4jA08="
};
ct = sjcl.encrypt(password, plaintext, p, rp);//.replace(/,/g,",\n");
return JSON.parse(ct).ct;
}
// Decrypt a message
function decrypt(ciphertext, password) {
var p, plaintext, rp = {};
p = {
adata: "",
iter: 1,
mode: "ccm",
ts: 64,
ks: 128,
iv: "t6vxTD/94Lk7DM87LZkPQA==",
cipher: "aes",
salt: "SdieDA4jA08=",
ct: ciphertext
};
plaintext = sjcl.decrypt(password, JSON.stringify(p), {}, rp);
return plaintext;
}*/
//Copied from the davstorage connector
/**
* Creates a new document if not already exists
*
* @method post
* @param {Object} command The JIO command
* @param {Object} metadata The metadata to put
* @param {Object} options The command options
*/
/* SearchableEncryptionStorage.prototype.post = function (
command,
metadata
) {
// this.postOrPut('post', command, metadata);
metadata.encryptedIndex = constructBloomFilter(
this._password,
this._errorRate,
this._nbMaxKeywords,
this._keywords,
metadata._id
);
}; */
/**
* Creates or updates a document
*
* @method put
* @param {Object} command The JIO command
* @param {Object} metadata The metadata to post
* @param {Object} options The command options
*/
SearchableEncryptionStorage.prototype.put = function (
command,
metadata
) {
// First create the associated encryptedIndex to allow encrypted queries at a
// later stage
// Then we encrypt the data using sjcl library. This step is independant of
// the searchable encryption features, however it is also related to
// confidentiality hence we added it here as an example of how to use the sjcl
// library.
var encryptedIndex = constructBloomFilter(
this._password,
this._errorRate,
this._nbMaxKeywords,
metadata.keywords,
metadata._id
), data = sjcl.encrypt(this._password, JSON.stringify(metadata));
// The remainder is a classical put using the ajax method
jIO.util.ajax({
"type": "PUT",
"url": this._url + "/" + metadata._id,
"dataType": "json",
"data": {"metadata": data, "encryptedIndex": encryptedIndex}
}).then(function (e) {
command.success(e.target.status);
}, function (e) {
var xhr = e.target;
command.reject(
xhr.status,
xhr.statusText,
"Document update from server failed"
);
});
};
/**
* Creates a document if it does not already exist.
*
* @method post
* @param {Object} command The JIO command
* @param {Object} metadata The metadata to post
* @param {Object} options The command options
*/
SearchableEncryptionStorage.prototype.post = function (
command,
metadata
) {
// First create the associated encryptedIndex to allow encrypted queries at a
// later stage
// Then we encrypt the data using sjcl library. This step is independant of
// the searchable encryption features, however it is also related to
// confidentiality hence we added it here as an example of how to use the sjcl
// library.
var encryptedIndex = constructBloomFilter(
this._password,
this._errorRate,
this._nbMaxKeywords,
metadata.keywords,
metadata._id
), data = sjcl.encrypt(this._password, JSON.stringify(metadata));
// The remainder is a classical put using the ajax method
jIO.util.ajax({
"type": "POST",
"url": this._url + "/" + metadata._id,
"dataType": "json",
"data": {"metadata": data, "encryptedIndex": encryptedIndex}
}).then(function (e) {
command.success(e.target.status);
}, function (e) {
var xhr = e.target;
command.reject(
xhr.status,
xhr.statusText,
"Document update from server failed"
);
});
};
/**
* Adds attachments to a document
*
* @method putAttachment
* @param {Object} command The JIO command
* @param {Object} metadata The metadata to putAttachment
* @param {Object} options The command options
*/
SearchableEncryptionStorage.prototype.putAttachment = function (
command,
param
) {
// This function adds an attachment to a document, it has nothing specific to
// searchable encryption. Optionally the attachment could be encrypted as well
// using the same primitive shown in previous methods.
jIO.util.ajax({
"type": "PUT",
"url": this._url + "/" + param._id + "/" + param._attachment,
"dataType": "blob",
"data": param._blob
}).then(function (e) {
command.success(e.target.status);
}, function (e) {
var xhr = e.target;
command.reject(
xhr.status,
xhr.statusText,
"Document update from server failed"
);
});
};
/**
* Retrieve metadata
*
* @method get
* @param {Object} command The JIO command
* @param {Object} param The command parameters
* @param {Object} options The command options
*/
SearchableEncryptionStorage.prototype.get = function (
command,
param
) {
// This function retrieves a document given its ID. It is not specific to
// searchable encryption. Here we also have to decrypt the metadata as we
// encrypted them in the put or post methods.
var that = this;
jIO.util.ajax({
"type": "GET",
"url": this._url + "/" + param._id
}).then(function (e) {
var data = JSON.parse(sjcl.decrypt(
that._password,
e.target.responseText
));
command.success(e.target.status, {"data": data});
}, function (e) {
var xhr = e.target;
command.reject(
xhr.status,
xhr.statusText,
"Document retrieval from server failed"
);
});
};
SearchableEncryptionStorage.prototype.getAttachment = function (
command,
param
) {
// This function retrieves an attachment of a document. Nothing specific to
// searchable encryption either.
jIO.util.ajax({
"type": "GET",
"url": this._url + "/" + param._id + "/" + param._attachment
}).then(function (e) {
command.success(e.target.status, {"data": e.target.response});
}, function (e) {
var xhr = e.target;
command.reject(
xhr.status,
xhr.statusText,
"Document retrieval from server failed"
);
});
};
SearchableEncryptionStorage.prototype.remove = function (
command,
param
) {
// This function removes a document. Nothing specific to
// searchable encryption either.
jIO.util.ajax({
"type": "DELETE",
"url": this._url + "/" + param._id
}).then(function (e) {
command.success(e.target.status);
}, function (e) {
var xhr = e.target;
command.reject(
xhr.status,
xhr.statusText,
"Document removal from server failed"
);
});
};
SearchableEncryptionStorage.prototype.removeAttachment = function (
command,
param
) {
// This function removes an attachment of a document. Nothing specific to
// searchable encryption either.
jIO.util.ajax({
"type": "DELETE",
"url": this._url + "/" + param._id + "/" + param._attachment
}).then(function (e) {
command.success(e.target.status);
}, function (e) {
var xhr = e.target;
command.reject(
xhr.status,
xhr.statusText,
"Document removal from server failed"
);
});
};
/**
* AllDocs deals with encrypted queries. This is a core function of the
* searchable encryption connector.
* In this version, AllDOcs enables to retrieve all documents containing a
* keyword. However the server should not learn the keyword, and it should not
* learn anything with respect to the documents either if they are encrypted.
* The trick here is that the function encrypts the query (composed of a single
* keyword) by performing the first steps of the construction of the Bloom
* Filters: it hashes the keyword concatenated with the password and an
* iterator (which takes values between 0 and errorRate) and thus obtains an
* array of errorRate rows, each row is converted to a base64 string.
* Using this encrypted query the servers tests all documents it has stored with
* their respective encrypted indexes, and it returns the list of documents
* that match the query (without understanding the query though!). AllDocs
* simply has to decrypt all documents at this stage (since we encrypted them in
* the put and post steps): the user gets the documents he searched for with a
* high level of confidentiality against the server.
*/
SearchableEncryptionStorage.prototype.allDocs = function (
command,
param,
option
) {
/*jslint unparam: true */
var query, that = this;
query = constructEncryptedQuery(
this._password,
this._errorRate,
option.query
);
jIO.util.ajax({
"type": "POST",
"url": this._url,
"dataType": "json",
"data": {"query": query}
}).then(function (e) {
var document_list = e.target.response;
document_list = document_list.map(function (param) {
param = JSON.parse(sjcl.decrypt(that._password, param));
var row = {
"id": param._id,
"value": {}
};
if (option.include_docs === true) {
row.doc = param;
}
return row;
});
command.success(e.target.status, {"data": {
"total_rows": document_list.length,
"rows": document_list
}});
}, function (e) {
var xhr = e.target;
command.reject(
xhr.status,
xhr.statusText,
"Documents retrieval from server failed"
);
});
};
jIO.addStorage("searchableencryption", SearchableEncryptionStorage);
}));
// Methods remaining to be defined: only check and repair
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/shastorage.js 0000664 0000000 0000000 00000003422 13037644422 0030232 0 ustar 00root root 0000000 0000000 /*
* Copyright 2015, Nexedi SA
* Released under the LGPL license.
* http://www.gnu.org/licenses/lgpl.html
*/
/*jslint nomen: true*/
/*global Rusha*/
/**
* JIO Sha Storage. Type = 'sha'.
*/
(function (Rusha) {
"use strict";
var rusha = new Rusha();
function ShaStorage(spec) {
this._sub_storage = jIO.createJIO(spec.sub_storage);
}
ShaStorage.prototype.post = function (param) {
return this._sub_storage.put(
rusha.digestFromString(JSON.stringify(param)),
param
);
};
ShaStorage.prototype.get = function () {
return this._sub_storage.get.apply(this._sub_storage, arguments);
};
ShaStorage.prototype.remove = function () {
return this._sub_storage.remove.apply(this._sub_storage, arguments);
};
ShaStorage.prototype.hasCapacity = function () {
return this._sub_storage.hasCapacity.apply(this._sub_storage, arguments);
};
ShaStorage.prototype.buildQuery = function () {
return this._sub_storage.buildQuery.apply(this._sub_storage, arguments);
};
ShaStorage.prototype.getAttachment = function () {
return this._sub_storage.getAttachment.apply(this._sub_storage, arguments);
};
ShaStorage.prototype.putAttachment = function () {
return this._sub_storage.putAttachment.apply(this._sub_storage, arguments);
};
ShaStorage.prototype.removeAttachment = function () {
return this._sub_storage.removeAttachment.apply(this._sub_storage,
arguments);
};
ShaStorage.prototype.allAttachments = function () {
return this._sub_storage.allAttachments.apply(this._sub_storage, arguments);
};
ShaStorage.prototype.repair = function () {
return this._sub_storage.repair.apply(this._sub_storage, arguments);
};
jIO.addStorage('sha', ShaStorage);
}(Rusha));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/splitstorage.js 0000664 0000000 0000000 00000037613 13037644422 0030623 0 ustar 00root root 0000000 0000000 /*
* Copyright 2013, Nexedi SA
* Released under the LGPL license.
* http://www.gnu.org/licenses/lgpl.html
*/
/*jslint indent:2, maxlen: 80, nomen: true */
/*global jIO, define, Blob */
/**
* Provides a split storage for JIO. This storage splits data
* and store them in the sub storages defined on the description.
*
* {
* "type": "split",
* "storage_list": [, ...]
* }
*/
// define([dependencies], module);
(function (dependencies, module) {
"use strict";
if (typeof define === 'function' && define.amd) {
return define(dependencies, module);
}
module(jIO);
}(['jio'], function (jIO) {
"use strict";
/**
* Generate a new uuid
*
* @method generateUuid
* @private
* @return {String} The new uuid
*/
function generateUuid() {
function S4() {
/* 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();
}
/**
* Class to merge allDocs responses from several sub storages.
*
* @class AllDocsResponseMerger
* @constructor
*/
function AllDocsResponseMerger() {
/**
* A list of allDocs response.
*
* @attribute response_list
* @type {Array} Contains allDocs responses
* @default []
*/
this.response_list = [];
}
AllDocsResponseMerger.prototype.constructor = AllDocsResponseMerger;
/**
* Add an allDocs response to the response list.
*
* @method addResponse
* @param {Object} response The allDocs response.
* @return {AllDocsResponseMerger} This
*/
AllDocsResponseMerger.prototype.addResponse = function (response) {
this.response_list.push(response);
return this;
};
/**
* Add several allDocs responses to the response list.
*
* @method addResponseList
* @param {Array} response_list An array of allDocs responses.
* @return {AllDocsResponseMerger} This
*/
AllDocsResponseMerger.prototype.addResponseList = function (response_list) {
var i;
for (i = 0; i < response_list.length; i += 1) {
this.response_list.push(response_list[i]);
}
return this;
};
/**
* Merge the response_list to one allDocs response.
*
* The merger will find rows with the same id in order to merge them, thanks
* to the onRowToMerge method. If no row correspond to an id, rows with the
* same id will be ignored.
*
* @method merge
* @param {Object} [option={}] The merge options
* @param {Boolean} [option.include_docs=false] Tell the merger to also
* merge metadata if true.
* @return {Object} The merged allDocs response.
*/
AllDocsResponseMerger.prototype.merge = function (option) {
var result = [], row, to_merge = [], tmp, i;
if (this.response_list.length === 0) {
return [];
}
/*jslint ass: true */
while ((row = this.response_list[0].data.rows.shift()) !== undefined) {
to_merge[0] = row;
for (i = 1; i < this.response_list.length; i += 1) {
to_merge[i] = AllDocsResponseMerger.listPopFromRowId(
this.response_list[i].data.rows,
row.id
);
if (to_merge[i] === undefined) {
break;
}
}
tmp = this.onRowToMerge(to_merge, option || {});
if (tmp !== undefined) {
result[result.length] = tmp;
}
}
this.response_list = [];
return {"total_rows": result.length, "rows": result};
};
/**
* This method is called when the merger want to merge several rows with the
* same id.
*
* @method onRowToMerge
* @param {Array} row_list An array of rows.
* @param {Object} [option={}] The merge option.
* @param {Boolean} [option.include_docs=false] Also merge the metadata if
* true
* @return {Object} The merged row
*/
AllDocsResponseMerger.prototype.onRowToMerge = function (row_list, option) {
var i, k, new_row = {"value": {}}, data = "";
option = option || {};
for (i = 0; i < row_list.length; i += 1) {
new_row.id = row_list[i].id;
if (row_list[i].key) {
new_row.key = row_list[i].key;
}
if (option.include_docs) {
new_row.doc = new_row.doc || {};
for (k in row_list[i].doc) {
if (row_list[i].doc.hasOwnProperty(k)) {
if (k[0] === "_") {
new_row.doc[k] = row_list[i].doc[k];
}
}
}
data += row_list[i].doc.data;
}
}
if (option.include_docs) {
try {
data = JSON.parse(data);
} catch (e) { return undefined; }
for (k in data) {
if (data.hasOwnProperty(k)) {
new_row.doc[k] = data[k];
}
}
}
return new_row;
};
/**
* Search for a specific row and pop it. During the search operation, all
* parsed rows are stored on a dictionnary in order to be found instantly
* later.
*
* @method listPopFromRowId
* @param {Array} rows The row list
* @param {String} doc_id The document/row id
* @return {Object/undefined} The poped row
*/
AllDocsResponseMerger.listPopFromRowId = function (rows, doc_id) {
var row;
if (!rows.dict) {
rows.dict = {};
}
if (rows.dict[doc_id]) {
row = rows.dict[doc_id];
delete rows.dict[doc_id];
return row;
}
/*jslint ass: true*/
while ((row = rows.shift()) !== undefined) {
if (row.id === doc_id) {
return row;
}
rows.dict[row.id] = row;
}
};
/**
* The split storage class used by JIO.
*
* A split storage instance is able to i/o on several sub storages with
* split documents.
*
* @class SplitStorage
*/
function SplitStorage(spec) {
var that = this, priv = {};
/**
* The list of sub storages we want to use to store part of documents.
*
* @attribute storage_list
* @private
* @type {Array} Array of storage descriptions
*/
priv.storage_list = spec.storage_list;
//////////////////////////////////////////////////////////////////////
// Tools
/**
* Send a command to all sub storages. All the response are returned
* in a list. The index of the response correspond to the storage_list
* index. If an error occurs during operation, the callback is called with
* `callback(err, undefined)`. The response is given with
* `callback(undefined, response_list)`.
*
* `doc` is the document informations but can also be a list of dedicated
* document informations. In this case, each document is associated to one
* sub storage.
*
* @method send
* @private
* @param {String} method The command method
* @param {Object,Array} doc The document information to send to each sub
* storages or a list of dedicated document
* @param {Object} option The command option
* @param {Function} callback Called at the end
*/
priv.send = function (command, method, doc, option, callback) {
var i, answer_list = [], failed = false;
function onEnd() {
i += 1;
if (i === priv.storage_list.length) {
callback(undefined, answer_list);
}
}
function onSuccess(i) {
return function (response) {
if (!failed) {
answer_list[i] = response;
}
onEnd();
};
}
function onError(i) {
return function (err) {
if (!failed) {
failed = true;
err.index = i;
callback(err, undefined);
}
};
}
if (!Array.isArray(doc)) {
for (i = 0; i < priv.storage_list.length; i += 1) {
if (method === 'allDocs') {
command.storage(priv.storage_list[i])[method](option).
then(onSuccess(i), onError(i));
} else {
command.storage(priv.storage_list[i])[method](doc, option).
then(onSuccess(i), onError(i));
}
}
} else {
for (i = 0; i < priv.storage_list.length; i += 1) {
// we assume that alldocs is not called if the there is several docs
command.storage(priv.storage_list[i])[method](doc[i], option).
then(onSuccess(i), onError(i));
}
}
i = 0;
};
/**
* Split document metadata then store them to the sub storages.
*
* @method postOrPut
* @private
* @param {Object} doc A serialized document object
* @param {Object} option Command option properties
* @param {String} method The command method ('post' or 'put')
*/
priv.postOrPut = function (command, doc, option, method) {
var i, data, doc_list = [], doc_underscores = {};
if (!doc._id) {
doc._id = generateUuid(); // XXX should let gidstorage guess uid
}
for (i in doc) {
if (doc.hasOwnProperty(i)) {
if (i[0] === "_") {
doc_underscores[i] = doc[i];
delete doc[i];
}
}
}
data = JSON.stringify(doc);
for (i = 0; i < priv.storage_list.length; i += 1) {
doc_list[i] = JSON.parse(JSON.stringify(doc_underscores));
doc_list[i].data = data.slice(
(data.length / priv.storage_list.length) * i,
(data.length / priv.storage_list.length) * (i + 1)
);
}
priv.send(command, method, doc_list, option, function (err) {
if (err) {
err.message = "Unable to " + method + " document";
delete err.index;
return command.error(err);
}
command.success({"id": doc_underscores._id});
});
};
//////////////////////////////////////////////////////////////////////
// JIO commands
/**
* Split document metadata then store them to the sub storages.
*
* @method post
* @param {Object} command The JIO command
*/
that.post = function (command, metadata, option) {
priv.postOrPut(command, metadata, option, 'post');
};
/**
* Split document metadata then store them to the sub storages.
*
* @method put
* @param {Object} command The JIO command
*/
that.put = function (command, metadata, option) {
priv.postOrPut(command, metadata, option, 'put');
};
/**
* Puts an attachment to the sub storages.
*
* @method putAttachment
* @param {Object} command The JIO command
*/
that.putAttachment = function (command, param, option) {
var i, attachment_list = [], data = param._blob;
for (i = 0; i < priv.storage_list.length; i += 1) {
attachment_list[i] = jIO.util.deepClone(param);
attachment_list[i]._blob = data.slice(
data.size * i / priv.storage_list.length,
data.size * (i + 1) / priv.storage_list.length,
data.type
);
}
priv.send(
command,
'putAttachment',
attachment_list,
option,
function (err) {
if (err) {
err.message = "Unable to put attachment";
delete err.index;
return command.error(err);
}
command.success();
}
);
};
/**
* Gets splited document metadata then returns real document.
*
* @method get
* @param {Object} command The JIO command
*/
that.get = function (command, param, option) {
var doc = param;
priv.send(command, 'get', doc, option, function (err, response) {
var i, k;
if (err) {
err.message = "Unable to get document";
delete err.index;
return command.error(err);
}
doc = '';
for (i = 0; i < response.length; i += 1) {
response[i] = response[i].data;
doc += response[i].data;
}
doc = JSON.parse(doc);
for (i = 0; i < response.length; i += 1) {
for (k in response[i]) {
if (response[i].hasOwnProperty(k)) {
if (k[0] === "_") {
doc[k] = response[i][k];
}
}
}
}
delete doc._attachments;
for (i = 0; i < response.length; i += 1) {
if (response[i]._attachments) {
for (k in response[i]._attachments) {
if (response[i]._attachments.hasOwnProperty(k)) {
doc._attachments = doc._attachments || {};
doc._attachments[k] = doc._attachments[k] || {
"length": 0,
"content_type": ""
};
doc._attachments[k].length += response[i]._attachments[k].
length;
// if (response[i]._attachments[k].digest) {
// if (doc._attachments[k].digest) {
// doc._attachments[k].digest += " " + response[i].
// _attachments[k].digest;
// } else {
// doc._attachments[k].digest = response[i].
// _attachments[k].digest;
// }
// }
doc._attachments[k].content_type = response[i]._attachments[k].
content_type;
}
}
}
}
command.success({"data": doc});
});
};
/**
* Gets splited document attachment then returns real attachment data.
*
* @method getAttachment
* @param {Object} command The JIO command
*/
that.getAttachment = function (command, param, option) {
priv.send(command, 'getAttachment', param, option, function (
err,
response
) {
if (err) {
err.message = "Unable to get attachment";
delete err.index;
return command.error(err);
}
command.success({"data": new Blob(response.map(function (answer) {
return answer.data;
}), {"type": response[0].data.type})});
});
};
/**
* Removes a document from the sub storages.
*
* @method remove
* @param {Object} command The JIO command
*/
that.remove = function (command, param, option) {
priv.send(
command,
'remove',
param,
option,
function (err) {
if (err) {
err.message = "Unable to remove document";
delete err.index;
return command.error(err);
}
command.success();
}
);
};
/**
* Removes an attachment from the sub storages.
*
* @method removeAttachment
* @param {Object} command The JIO command
*/
that.removeAttachment = function (command, param, option) {
priv.send(
command,
'removeAttachment',
param,
option,
function (err) {
if (err) {
err.message = "Unable to remove attachment";
delete err.index;
return command.error(err);
}
command.success();
}
);
};
/**
* Retreive a list of all document in the sub storages.
*
* If include_docs option is false, then it returns the document list from
* the first sub storage. Else, it will merge results and return.
*
* @method allDocs
* @param {Object} command The JIO command
*/
that.allDocs = function (command, param, option) {
option = {"include_docs": option.include_docs};
priv.send(
command,
'allDocs',
param,
option,
function (err, response_list) {
var all_docs_merger;
if (err) {
err.message = "Unable to retrieve document list";
delete err.index;
return command.error(err);
}
all_docs_merger = new AllDocsResponseMerger();
all_docs_merger.addResponseList(response_list);
return command.success({"data": all_docs_merger.merge(option)});
}
);
};
} // end of splitStorage
jIO.addStorage('split', SplitStorage);
}));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/splitstorage.tests.js 0000664 0000000 0000000 00000060241 13037644422 0031755 0 ustar 00root root 0000000 0000000 /*jslint indent: 2, maxlen: 80, nomen: true */
/*global define, jIO, test_util, RSVP, test, ok, deepEqual, module, stop,
start, hex_sha256 */
// define([module_name], [dependencies], module);
(function (dependencies, module) {
"use strict";
if (typeof define === 'function' && define.amd) {
return define(dependencies, module);
}
module(jIO, test_util, RSVP);
}([
'jio',
'test_util',
'rsvp',
'localstorage',
'splitstorage'
], function (jIO, util, RSVP) {
"use strict";
var tool = {
"readBlobAsBinaryString": jIO.util.readBlobAsBinaryString
};
function reverse(promise) {
return new RSVP.Promise(function (resolve, reject, notify) {
promise.then(reject, resolve, notify);
}, function () {
promise.cancel();
});
}
/**
* sequence(thens): Promise
*
* Executes a sequence of *then* callbacks. It acts like
* `smth().then(callback).then(callback)...`. The first callback is called
* with no parameter.
*
* Elements of `thens` array can be a function or an array contaning at most
* three *then* callbacks: *onFulfilled*, *onRejected*, *onNotified*.
*
* When `cancel()` is executed, each then promises are cancelled at the same
* time.
*
* @param {Array} thens An array of *then* callbacks
* @return {Promise} A new promise
*/
function sequence(thens) {
var promises = [];
return new RSVP.Promise(function (resolve, reject, notify) {
var i;
promises[0] = new RSVP.Promise(function (resolve) {
resolve();
});
for (i = 0; i < thens.length; i += 1) {
if (Array.isArray(thens[i])) {
promises[i + 1] = promises[i].
then(thens[i][0], thens[i][1], thens[i][2]);
} else {
promises[i + 1] = promises[i].then(thens[i]);
}
}
promises[i].then(resolve, reject, notify);
}, function () {
var i;
for (i = 0; i < promises.length; i += 1) {
promises[i].cancel();
}
});
}
function unexpectedError(error) {
if (error instanceof Error) {
deepEqual([
error.name + ": " + error.message,
error
], "UNEXPECTED ERROR", "Unexpected error");
} else {
deepEqual(error, "UNEXPECTED ERROR", "Unexpected error");
}
}
module("SplitStorage + LocalStorage");
test("Post", function () {
var shared = {}, jio, jio_local_list = [];
shared.workspace = {};
shared.local_storage_description1 = {
"type": "local",
"username": "splitstorage",
"application_name": "post1",
"mode": "memory"
};
shared.local_storage_description2 = {
"type": "local",
"username": "splitstorage",
"application_name": "post2",
"mode": "memory"
};
jio = jIO.createJIO({
"type": "split",
"storage_list": [
shared.local_storage_description1,
shared.local_storage_description2
]
}, {"workspace": shared.workspace});
jio_local_list[0] = jIO.createJIO(shared.local_storage_description1, {
"workspace": shared.workspace
});
jio_local_list[1] = jIO.createJIO(shared.local_storage_description2, {
"workspace": shared.workspace
});
jio_local_list.run = function (method, argument) {
var i, promises = [];
for (i = 0; i < this.length; i += 1) {
promises[i] = this[i][method].apply(this[i], argument);
}
return RSVP.all(promises);
};
jio_local_list.get = function () {
return this.run("get", arguments);
};
stop();
// post without id
jio.post({
"_underscored_meta": "uvalue",
"meta": "data"
}).then(function (answer) {
shared.uuid = answer.id;
answer.id = "";
ok(util.isUuid(shared.uuid), "Uuid should look like " +
"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx : " + shared.uuid);
deepEqual(answer, {
"id": "",
"method": "post",
"result": "success",
"status": 201,
"statusText": "Created"
}, "Post document without id");
// check uploaded documents
return jio_local_list.get({"_id": shared.uuid});
}).then(function (answers) {
var i;
for (i = 0; i < answers.length; i += 1) {
deepEqual(answers[i].data, {
"_id": shared.uuid,
"_underscored_meta": "uvalue",
"data": i === 0 ? "{\"meta\"" : ":\"data\"}"
}, "Check uploaded document in sub storage " + (i + 1));
}
// post with id
return jio.post({
"_id": "one",
"_underscored_meta": "uvalue",
"meta": "data",
"hello": "world"
});
}).then(function (answer) {
deepEqual(answer, {
"id": "one",
"method": "post",
"result": "success",
"status": 201,
"statusText": "Created"
}, "Post document with id");
// check uploaded documents
return jio_local_list.get({"_id": "one"});
}).then(function (answers) {
deepEqual(answers[0].data, {
"_id": "one",
"_underscored_meta": "uvalue",
"data": "{\"meta\":\"data\","
}, "Check uploaded document in sub storage 1");
deepEqual(answers[1].data, {
"_id": "one",
"_underscored_meta": "uvalue",
"data": "\"hello\":\"world\"}"
}, "Check uploaded document in sub storage 2");
// post with id
return reverse(jio.post({
"_id": "one",
"_underscored_meta": "uvalue",
"meta": "data",
"hello": "world"
}));
}).then(function (answer) {
deepEqual(answer, {
"error": "conflict",
"id": "one",
"message": "Unable to post document",
"method": "post",
"reason": "document exists",
"result": "error",
"status": 409,
"statusText": "Conflict"
}, "Post document with same id -> 409 Conflict");
}).fail(unexpectedError).always(start);
});
test("PutAttachment", function () {
var shared = {}, jio, jio_local_list = [];
shared.workspace = {};
shared.local_storage_description1 = {
"type": "local",
"username": "splitstorage",
"application_name": "putAttachment1",
"mode": "memory"
};
shared.local_storage_description2 = {
"type": "local",
"username": "splitstorage",
"application_name": "putAttachment2",
"mode": "memory"
};
jio = jIO.createJIO({
"type": "split",
"storage_list": [
shared.local_storage_description1,
shared.local_storage_description2
]
}, {"workspace": shared.workspace});
jio_local_list[0] = jIO.createJIO(shared.local_storage_description1, {
"workspace": shared.workspace
});
jio_local_list[1] = jIO.createJIO(shared.local_storage_description2, {
"workspace": shared.workspace
});
jio_local_list.run = function (method, argument) {
var i, promises = [];
for (i = 0; i < this.length; i += 1) {
promises[i] = this[i][method].apply(this[i], argument);
}
return RSVP.all(promises);
};
jio_local_list.get = function () {
return this.run("get", arguments);
};
jio_local_list.getAttachmentAsBinaryString = function () {
return this.run("getAttachment", arguments).then(function (answers) {
var i, promises = [];
for (i = 0; i < answers.length; i += 1) {
promises[i] = tool.readBlobAsBinaryString(answers[i].data);
}
return RSVP.all(promises);
});
};
stop();
return reverse(jio.putAttachment({
"_id": "one",
"_attachment": "my_attachment",
"_data": "My Data",
"_content_type": "text/plain"
})).then(function (answer) {
deepEqual(answer, {
"attachment": "my_attachment",
"error": "not_found",
"id": "one",
"message": "Unable to put attachment",
"method": "putAttachment",
"reason": "missing",
"result": "error",
"status": 404,
"statusText": "Not Found"
}, "Put attachment on a inexistent document -> 404 Not Found");
return jio.post({
"_id": "one",
"_underscored_meta": "uvalue",
"meta": "data"
});
}).then(function () {
return jio.putAttachment({
"_id": "one",
"_attachment": "my_attachment",
"_data": "My Data",
"_mimetype": "text/plain"
});
}).then(function (answer) {
deepEqual(answer, {
"attachment": "my_attachment",
"id": "one",
"method": "putAttachment",
"result": "success",
"status": 204,
"statusText": "No Content"
}, "Put attachment on a document");
// check uploaded documents
return jio_local_list.get({"_id": "one"});
}).then(function (answers) {
deepEqual(answers[0].data, {
"_attachments": {
"my_attachment": {
"content_type": "text/plain",
"digest": "sha256-ebf2d770a6a2dfa135f6c81431f22fc3cbcde9ae" +
"e52759ca9e520d4d964c1322", // sha256("My ")
"length": 3
}
},
"_id": "one",
"_underscored_meta": "uvalue",
"data": "{\"meta\""
}, "Check uploaded document in sub storage 1");
deepEqual(answers[1].data, {
"_attachments": {
"my_attachment": {
"content_type": "text/plain",
"digest": "sha256-cec3a9b89b2e391393d0f68e4bc12a9fa6cf358b" +
"3cdf79496dc442d52b8dd528", // sha256("Data")
"length": 4
}
},
"_id": "one",
"_underscored_meta": "uvalue",
"data": ":\"data\"}"
}, "Check uploaded document in sub storage 2");
return jio_local_list.getAttachmentAsBinaryString({
"_id": "one",
"_attachment": "my_attachment"
});
}).then(function (events) {
deepEqual(events[0].target.result, "My ",
"Check uploaded document in sub storage 1");
deepEqual(events[1].target.result, "Data",
"Check uploaded document in sub storage 1");
}).fail(unexpectedError).always(start);
});
test("Get", function () {
var shared = {}, jio;
shared.workspace = {};
shared.local_storage_description1 = {
"type": "local",
"username": "splitstorage",
"application_name": "get1",
"mode": "memory"
};
shared.local_storage_description2 = {
"type": "local",
"username": "splitstorage",
"application_name": "get2",
"mode": "memory"
};
jio = jIO.createJIO({
"type": "split",
"storage_list": [
shared.local_storage_description1,
shared.local_storage_description2
]
}, {"workspace": shared.workspace});
stop();
reverse(jio.get({"_id": "one"})).then(function (answer) {
deepEqual(answer, {
"error": "not_found",
"id": "one",
"message": "Unable to get document",
"method": "get",
"reason": "missing",
"result": "error",
"status": 404,
"statusText": "Not Found"
}, "Get missing document");
return jio.post({
"_id": "one",
"_underscored_meta": "uvalue",
"meta": "data"
});
}).then(function () {
return jio.get({"_id": "one"});
}).then(function (answer) {
deepEqual(answer.data, {
"_id": "one",
"_underscored_meta": "uvalue",
"meta": "data"
}, "Get posted document");
return jio.putAttachment({
"_id": "one",
"_attachment": "my_attachment",
"_data": "My Data",
"_content_type": "text/plain"
});
}).then(function () {
return jio.get({"_id": "one"});
}).then(function (answer) {
deepEqual(answer.data, {
"_id": "one",
"_underscored_meta": "uvalue",
"meta": "data",
"_attachments": {
"my_attachment": {
"length": 7,
"content_type": "text/plain"
}
}
}, "Get document with attachment informations");
}).fail(unexpectedError).always(start);
});
test("GetAttachment", function () {
var shared = {}, jio;
shared.workspace = {};
shared.local_storage_description1 = {
"type": "local",
"username": "splitstorage",
"application_name": "getAttachment1",
"mode": "memory"
};
shared.local_storage_description2 = {
"type": "local",
"username": "splitstorage",
"application_name": "getAttachment2",
"mode": "memory"
};
jio = jIO.createJIO({
"type": "split",
"storage_list": [
shared.local_storage_description1,
shared.local_storage_description2
]
}, {"workspace": shared.workspace});
stop();
reverse(jio.getAttachment({
"_id": "one",
"_attachment": "my_attachment"
})).then(function (answer) {
deepEqual(answer, {
"attachment": "my_attachment",
"error": "not_found",
"id": "one",
"message": "Unable to get attachment",
"method": "getAttachment",
"reason": "missing document",
"result": "error",
"status": 404,
"statusText": "Not Found"
}, "Get attachment from missing document -> 404 Not Found");
return jio.post({
"_id": "one",
"_underscored_meta": "uvalue",
"meta": "data"
});
}).then(function () {
return reverse(jio.getAttachment({
"_id": "one",
"_attachment": "my_attachment"
}));
}).then(function (answer) {
deepEqual(answer, {
"attachment": "my_attachment",
"error": "not_found",
"id": "one",
"message": "Unable to get attachment",
"method": "getAttachment",
"reason": "missing attachment",
"result": "error",
"status": 404,
"statusText": "Not Found"
}, "Get missing attachment from document");
return jio.putAttachment({
"_id": "one",
"_attachment": "my_attachment",
"_data": "My Data",
"_mimetype": "text/plain"
});
}).then(function () {
return jio.getAttachment({"_id": "one", "_attachment": "my_attachment"});
}).then(function (answer) {
return tool.readBlobAsBinaryString(answer.data);
}).then(function (event) {
deepEqual(event.target.result, "My Data", "Get attachment");
}).fail(unexpectedError).always(start);
});
test("RemoveAttachment", function () {
var shared = {}, jio;
shared.workspace = {};
shared.local_storage_description1 = {
"type": "local",
"username": "splitstorage",
"application_name": "removeAttachment1",
"mode": "memory"
};
shared.local_storage_description2 = {
"type": "local",
"username": "splitstorage",
"application_name": "removeAttachment2",
"mode": "memory"
};
jio = jIO.createJIO({
"type": "split",
"storage_list": [
shared.local_storage_description1,
shared.local_storage_description2
]
}, {"workspace": shared.workspace});
stop();
reverse(jio.removeAttachment({
"_id": "one",
"_attachment": "my_attachment"
})).then(function (answer) {
deepEqual(answer, {
"attachment": "my_attachment",
"error": "not_found",
"id": "one",
"message": "Unable to remove attachment",
"method": "removeAttachment",
"reason": "missing document",
"result": "error",
"status": 404,
"statusText": "Not Found"
}, "Remove attachment from inexistent document -> 404 Not Found");
return jio.post({
"_id": "one",
"_underscored_meta": "uvalue",
"meta": "data"
});
}).then(function () {
return reverse(jio.removeAttachment({
"_id": "one",
"_attachment": "my_attachment"
}));
}).then(function (answer) {
deepEqual(answer, {
"attachment": "my_attachment",
"error": "not_found",
"id": "one",
"message": "Unable to remove attachment",
"method": "removeAttachment",
"reason": "missing attachment",
"result": "error",
"status": 404,
"statusText": "Not Found"
}, "Remove inexistent attachment -> 404 Not Found");
return jio.putAttachment({
"_id": "one",
"_attachment": "my_attachment",
"_data": "My Data",
"_mimetype": "text/plain"
});
}).then(function () {
return jio.removeAttachment({
"_id": "one",
"_attachment": "my_attachment"
});
}).then(function (answer) {
deepEqual(answer, {
"attachment": "my_attachment",
"id": "one",
"method": "removeAttachment",
"result": "success",
"status": 204,
"statusText": "No Content"
}, "Remove attachment");
return jio.get({"_id": "one"});
}).then(function (answer) {
deepEqual(answer.data, {
"_id": "one",
"_underscored_meta": "uvalue",
"meta": "data"
}, "Check document");
return reverse(jio.getAttachment({
"_id": "one",
"_attachment": "my_attachment"
}));
}).then(function (answer) {
deepEqual(answer, {
"attachment": "my_attachment",
"error": "not_found",
"id": "one",
"message": "Unable to get attachment",
"method": "getAttachment",
"reason": "missing attachment",
"result": "error",
"status": 404,
"statusText": "Not Found"
}, "Check attachment -> 404 Not Found");
}).fail(unexpectedError).always(start);
});
test("Remove", function () {
var shared = {}, jio;
shared.workspace = {};
shared.local_storage_description1 = {
"type": "local",
"username": "splitstorage",
"application_name": "remove1",
"mode": "memory"
};
shared.local_storage_description2 = {
"type": "local",
"username": "splitstorage",
"application_name": "remove2",
"mode": "memory"
};
jio = jIO.createJIO({
"type": "split",
"storage_list": [
shared.local_storage_description1,
shared.local_storage_description2
]
}, {"workspace": shared.workspace});
stop();
reverse(jio.remove({"_id": "one"})).then(function (answer) {
deepEqual(answer, {
"error": "not_found",
"id": "one",
"message": "Unable to remove document",
"method": "remove",
"reason": "missing",
"result": "error",
"status": 404,
"statusText": "Not Found"
}, "Remove missing document -> 404 Not Found");
return jio.post({
"_id": "one",
"_underscored_meta": "uvalue",
"meta": "data"
});
}).then(function () {
return jio.putAttachment({
"_id": "one",
"_attachment": "my_attachment",
"_data": "My Data",
"_mimetype": "text/plain"
});
}).then(function () {
return jio.remove({"_id": "one"});
}).then(function (answer) {
deepEqual(answer, {
"id": "one",
"method": "remove",
"result": "success",
"status": 204,
"statusText": "No Content"
}, "Remove document");
return reverse(jio.getAttachment({
"_id": "one",
"_attachment": "my_attachment"
}));
}).then(function (answer) {
deepEqual(answer, {
"attachment": "my_attachment",
"error": "not_found",
"id": "one",
"message": "Unable to get attachment",
"method": "getAttachment",
"reason": "missing document",
"result": "error",
"status": 404,
"statusText": "Not Found"
}, "Check attachment -> 404 Not Found");
return reverse(jio.get({"_id": "one"}));
}).then(function (answer) {
deepEqual(answer, {
"error": "not_found",
"id": "one",
"message": "Unable to get document",
"method": "get",
"reason": "missing",
"result": "error",
"status": 404,
"statusText": "Not Found"
}, "Check document -> 404 Not Found");
}).fail(unexpectedError).always(start);
});
test("Put", function () {
var shared = {}, jio;
shared.workspace = {};
shared.local_storage_description1 = {
"type": "local",
"username": "splitstorage",
"application_name": "put1",
"mode": "memory"
};
shared.local_storage_description2 = {
"type": "local",
"username": "splitstorage",
"application_name": "put2",
"mode": "memory"
};
jio = jIO.createJIO({
"type": "split",
"storage_list": [
shared.local_storage_description1,
shared.local_storage_description2
]
}, {"workspace": shared.workspace});
stop();
jio.put({
"_id": "one",
"_underscored_meta": "uvalue",
"meta": "data"
}).then(function (answer) {
deepEqual(answer, {
"id": "one",
"method": "put",
"result": "success",
"status": 204,
"statusText": "No Content"
}, "Put new document");
return jio.get({"_id": "one"});
}).then(function (answer) {
deepEqual(answer.data, {
"_id": "one",
"_underscored_meta": "uvalue",
"meta": "data"
}, "Check document");
return jio.put({
"_id": "one",
"_underscored_meta": "uvalue",
"meow": "dog"
});
}).then(function (answer) {
deepEqual(answer, {
"id": "one",
"method": "put",
"result": "success",
"status": 204,
"statusText": "No Content"
}, "Put same document again");
return jio.get({"_id": "one"});
}).then(function (answer) {
deepEqual(answer.data, {
"_id": "one",
"_underscored_meta": "uvalue",
"meow": "dog"
}, "Get document for check");
}).fail(unexpectedError).always(start);
});
test("AllDocs", function () {
var shared = {}, jio;
shared.workspace = {};
shared.local_storage_description1 = {
"type": "local",
"username": "splitstorage",
"application_name": "alldocs1",
"mode": "memory"
};
shared.local_storage_description2 = {
"type": "local",
"username": "splitstorage",
"application_name": "alldocs2",
"mode": "memory"
};
jio = jIO.createJIO({
"type": "split",
"storage_list": [
shared.local_storage_description1,
shared.local_storage_description2
]
}, {"workspace": shared.workspace});
stop();
function prepareDatabase() {
var i, do_list = [];
function post(i) {
return function () {
return jio.post({
"_id": "doc" + i,
"_underscored_meta": "uvalue" + i,
"meta": "data" + i
});
};
}
function putAttachment(i) {
return function () {
return jio.putAttachment({
"_id": "doc" + i,
"_attachment": "my_attachment" + i,
"_data": "My Data" + i,
"_content_type": "text/plain"
});
};
}
for (i = 0; i < 5; i += 1) {
do_list.push(post(i));
}
for (i = 0; i < 2; i += 1) {
do_list.push(putAttachment(i));
}
return sequence(do_list);
}
prepareDatabase().then(function () {
return jio.get({"_id": "doc1"});
}).then(function (answer) {
deepEqual(answer.data, {
"_id": "doc1",
"_underscored_meta": "uvalue1",
"meta": "data1",
"_attachments": {
"my_attachment1": {
"length": 8,
"content_type": "text/plain"
}
}
}, "Check document");
return jio.allDocs();
}).then(function (answer) {
answer.data.rows.sort(function (a, b) {
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
});
deepEqual(answer.data, {
"total_rows": 5,
"rows": [{
"id": "doc0",
"key": "doc0",
"value": {}
}, {
"id": "doc1",
"key": "doc1",
"value": {}
}, {
"id": "doc2",
"key": "doc2",
"value": {}
}, {
"id": "doc3",
"key": "doc3",
"value": {}
}, {
"id": "doc4",
"key": "doc4",
"value": {}
}]
}, "AllDocs with document ids only");
}).fail(unexpectedError).always(start);
});
}));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/unionstorage.js 0000664 0000000 0000000 00000015257 13037644422 0030620 0 ustar 00root root 0000000 0000000 /*jslint nomen: true */
/*global RSVP*/
/**
* JIO Union Storage. Type = 'union'.
* This provide a unified access other multiple storage.
* New document are created in the first sub storage.
* Document are searched in each sub storage until it is found.
*
*
* Storage Description:
*
* {
* "type": "union",
* "storage_list": [
* sub_storage_description_1,
* sub_storage_description_2,
*
* sub_storage_description_X,
* ]
* }
*
* @class UnionStorage
*/
(function (jIO, RSVP) {
"use strict";
/**
* The JIO UnionStorage extension
*
* @class UnionStorage
* @constructor
*/
function UnionStorage(spec) {
if (!Array.isArray(spec.storage_list)) {
throw new jIO.util.jIOError("storage_list is not an Array", 400);
}
var i;
this._storage_list = [];
for (i = 0; i < spec.storage_list.length; i += 1) {
this._storage_list.push(jIO.createJIO(spec.storage_list[i]));
}
}
UnionStorage.prototype._getWithStorageIndex = function () {
var i,
index = 0,
context = this,
arg = arguments,
result = this._storage_list[0].get.apply(this._storage_list[0], arg);
function handle404(j) {
result
.push(undefined, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return context._storage_list[j].get.apply(context._storage_list[j],
arg)
.push(function (doc) {
index = j;
return doc;
});
}
throw error;
});
}
for (i = 1; i < this._storage_list.length; i += 1) {
handle404(i);
}
return result
.push(function (doc) {
return [index, doc];
});
};
/*
* Get a document
* Try on each substorage on after the other
*/
UnionStorage.prototype.get = function () {
return this._getWithStorageIndex.apply(this, arguments)
.push(function (result) {
return result[1];
});
};
/*
* Get attachments list
* Try on each substorage on after the other
*/
UnionStorage.prototype.allAttachments = function () {
var argument_list = arguments,
context = this;
return this._getWithStorageIndex.apply(this, arguments)
.push(function (result) {
var sub_storage = context._storage_list[result[0]];
return sub_storage.allAttachments.apply(sub_storage, argument_list);
});
};
/*
* Post a document
* Simply store on the first substorage
*/
UnionStorage.prototype.post = function () {
return this._storage_list[0].post.apply(this._storage_list[0], arguments);
};
/*
* Put a document
* Search the document location, and modify it in its storage.
*/
UnionStorage.prototype.put = function () {
var arg = arguments,
context = this;
return this._getWithStorageIndex(arg[0])
.push(undefined, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
// Document does not exist, create in first substorage
return [0];
}
throw error;
})
.push(function (result) {
// Storage found, modify in it directly
var sub_storage = context._storage_list[result[0]];
return sub_storage.put.apply(sub_storage, arg);
});
};
/*
* Remove a document
* Search the document location, and remove it from its storage.
*/
UnionStorage.prototype.remove = function () {
var arg = arguments,
context = this;
return this._getWithStorageIndex(arg[0])
.push(function (result) {
// Storage found, remove from it directly
var sub_storage = context._storage_list[result[0]];
return sub_storage.remove.apply(sub_storage, arg);
});
};
UnionStorage.prototype.buildQuery = function () {
var promise_list = [],
i,
id_dict = {},
len = this._storage_list.length,
sub_storage;
for (i = 0; i < len; i += 1) {
sub_storage = this._storage_list[i];
promise_list.push(sub_storage.buildQuery.apply(sub_storage, arguments));
}
return new RSVP.Queue()
.push(function () {
return RSVP.all(promise_list);
})
.push(function (result_list) {
var result = [],
sub_result,
sub_result_len,
j;
len = result_list.length;
for (i = 0; i < len; i += 1) {
sub_result = result_list[i];
sub_result_len = sub_result.length;
for (j = 0; j < sub_result_len; j += 1) {
if (!id_dict.hasOwnProperty(sub_result[j].id)) {
id_dict[sub_result[j].id] = null;
result.push(sub_result[j]);
}
}
}
return result;
});
};
UnionStorage.prototype.hasCapacity = function (name) {
var i,
len,
result,
sub_storage;
if ((name === "list") ||
(name === "query") ||
(name === "select")) {
result = true;
len = this._storage_list.length;
for (i = 0; i < len; i += 1) {
sub_storage = this._storage_list[i];
result = result && sub_storage.hasCapacity(name);
}
return result;
}
return false;
};
UnionStorage.prototype.repair = function () {
var i,
promise_list = [];
for (i = 0; i < this._storage_list.length; i += 1) {
promise_list.push(this._storage_list[i].repair.apply(
this._storage_list[i],
arguments
));
}
return RSVP.all(promise_list);
};
UnionStorage.prototype.getAttachment = function () {
var argument_list = arguments,
context = this;
return this._getWithStorageIndex.apply(this, arguments)
.push(function (result) {
var sub_storage = context._storage_list[result[0]];
return sub_storage.getAttachment.apply(sub_storage, argument_list);
});
};
UnionStorage.prototype.putAttachment = function () {
var argument_list = arguments,
context = this;
return this._getWithStorageIndex.apply(this, arguments)
.push(function (result) {
var sub_storage = context._storage_list[result[0]];
return sub_storage.putAttachment.apply(sub_storage, argument_list);
});
};
UnionStorage.prototype.removeAttachment = function () {
var argument_list = arguments,
context = this;
return this._getWithStorageIndex.apply(this, arguments)
.push(function (result) {
var sub_storage = context._storage_list[result[0]];
return sub_storage.removeAttachment.apply(sub_storage, argument_list);
});
};
jIO.addStorage('union', UnionStorage);
}(jIO, RSVP));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/uuidstorage.js 0000664 0000000 0000000 00000003757 13037644422 0030440 0 ustar 00root root 0000000 0000000 /*jslint nomen: true*/
(function (jIO) {
"use strict";
/**
* The jIO UUIDStorage extension
*
* @class UUIDStorage
* @constructor
*/
function UUIDStorage(spec) {
this._sub_storage = jIO.createJIO(spec.sub_storage);
}
UUIDStorage.prototype.get = function () {
return this._sub_storage.get.apply(this._sub_storage, arguments);
};
UUIDStorage.prototype.allAttachments = function () {
return this._sub_storage.allAttachments.apply(this._sub_storage, arguments);
};
UUIDStorage.prototype.post = function (param) {
function S4() {
return ('0000' + Math.floor(
Math.random() * 0x10000 /* 65536 */
).toString(16)).slice(-4);
}
var id = S4() + S4() + "-" +
S4() + "-" +
S4() + "-" +
S4() + "-" +
S4() + S4() + S4();
return this.put(id, param);
};
UUIDStorage.prototype.put = function () {
return this._sub_storage.put.apply(this._sub_storage, arguments);
};
UUIDStorage.prototype.remove = function () {
return this._sub_storage.remove.apply(this._sub_storage, arguments);
};
UUIDStorage.prototype.getAttachment = function () {
return this._sub_storage.getAttachment.apply(this._sub_storage, arguments);
};
UUIDStorage.prototype.putAttachment = function () {
return this._sub_storage.putAttachment.apply(this._sub_storage, arguments);
};
UUIDStorage.prototype.removeAttachment = function () {
return this._sub_storage.removeAttachment.apply(this._sub_storage,
arguments);
};
UUIDStorage.prototype.repair = function () {
return this._sub_storage.repair.apply(this._sub_storage, arguments);
};
UUIDStorage.prototype.hasCapacity = function (name) {
return this._sub_storage.hasCapacity(name);
};
UUIDStorage.prototype.buildQuery = function () {
return this._sub_storage.buildQuery.apply(this._sub_storage,
arguments);
};
jIO.addStorage('uuid', UUIDStorage);
}(jIO));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/websqlstorage.js 0000664 0000000 0000000 00000025145 13037644422 0030762 0 ustar 00root root 0000000 0000000 /*
* Copyright 2013, Nexedi SA
* Released under the LGPL license.
* http://www.gnu.org/licenses/lgpl.html
*/
/**
* JIO Websql Storage. Type = "websql".
* websql "database" storage.
*/
/*global Blob, jIO, RSVP, openDatabase*/
/*jslint nomen: true*/
(function (jIO, RSVP, Blob, openDatabase) {
"use strict";
/**
* The JIO Websql Storage extension
*
* @class WebSQLStorage
* @constructor
*/
function queueSql(db, query_list, argument_list) {
return new RSVP.Promise(function (resolve, reject) {
/*jslint unparam: true*/
db.transaction(function (tx) {
var len = query_list.length,
result_list = [],
i;
function resolveTransaction(tx, result) {
result_list.push(result);
if (result_list.length === len) {
resolve(result_list);
}
}
function rejectTransaction(tx, error) {
reject(error);
return true;
}
for (i = 0; i < len; i += 1) {
tx.executeSql(query_list[i], argument_list[i], resolveTransaction,
rejectTransaction);
}
}, function (tx, error) {
reject(error);
});
/*jslint unparam: false*/
});
}
function initDatabase(db) {
var query_list = [
"CREATE TABLE IF NOT EXISTS document" +
"(id VARCHAR PRIMARY KEY NOT NULL, data TEXT)",
"CREATE TABLE IF NOT EXISTS attachment" +
"(id VARCHAR, attachment VARCHAR, part INT, blob TEXT)",
"CREATE TRIGGER IF NOT EXISTS removeAttachment " +
"BEFORE DELETE ON document FOR EACH ROW " +
"BEGIN DELETE from attachment WHERE id = OLD.id;END;",
"CREATE INDEX IF NOT EXISTS index_document ON document (id);",
"CREATE INDEX IF NOT EXISTS index_attachment " +
"ON attachment (id, attachment);"
];
return new RSVP.Queue()
.push(function () {
return queueSql(db, query_list, []);
});
}
function WebSQLStorage(spec) {
if (typeof spec.database !== 'string' || !spec.database) {
throw new TypeError("database must be a string " +
"which contains more than one character.");
}
this._database = openDatabase("jio:" + spec.database,
'1.0', '', 2 * 1024 * 1024);
if (spec.blob_length &&
(typeof spec.blob_length !== "number" ||
spec.blob_length < 20)) {
throw new TypeError("blob_len parameter must be a number >= 20");
}
this._blob_length = spec.blob_length || 2000000;
this._init_db_promise = initDatabase(this._database);
}
WebSQLStorage.prototype.put = function (id, param) {
var db = this._database,
that = this,
data_string = JSON.stringify(param);
return new RSVP.Queue()
.push(function () {
return that._init_db_promise;
})
.push(function () {
return queueSql(db, ["INSERT OR REPLACE INTO " +
"document(id, data) VALUES(?,?)"],
[[id, data_string]]);
})
.push(function () {
return id;
});
};
WebSQLStorage.prototype.remove = function (id) {
var db = this._database,
that = this;
return new RSVP.Queue()
.push(function () {
return that._init_db_promise;
})
.push(function () {
return queueSql(db, ["DELETE FROM document WHERE id = ?"], [[id]]);
})
.push(function (result_list) {
if (result_list[0].rowsAffected === 0) {
throw new jIO.util.jIOError("Cannot find document", 404);
}
return id;
});
};
WebSQLStorage.prototype.get = function (id) {
var db = this._database,
that = this;
return new RSVP.Queue()
.push(function () {
return that._init_db_promise;
})
.push(function () {
return queueSql(db, ["SELECT data FROM document WHERE id = ?"],
[[id]]);
})
.push(function (result_list) {
if (result_list[0].rows.length === 0) {
throw new jIO.util.jIOError("Cannot find document", 404);
}
return JSON.parse(result_list[0].rows[0].data);
});
};
WebSQLStorage.prototype.allAttachments = function (id) {
var db = this._database,
that = this;
return new RSVP.Queue()
.push(function () {
return that._init_db_promise;
})
.push(function () {
return queueSql(db, [
"SELECT id FROM document WHERE id = ?",
"SELECT DISTINCT attachment FROM attachment WHERE id = ?"
], [[id], [id]]);
})
.push(function (result_list) {
if (result_list[0].rows.length === 0) {
throw new jIO.util.jIOError("Cannot find document", 404);
}
var len = result_list[1].rows.length,
obj = {},
i;
for (i = 0; i < len; i += 1) {
obj[result_list[1].rows[i].attachment] = {};
}
return obj;
});
};
function sendBlobPart(blob, argument_list, index, queue) {
queue.push(function () {
return jIO.util.readBlobAsDataURL(blob);
})
.push(function (strBlob) {
argument_list[index + 2].push(strBlob.currentTarget.result);
return;
});
}
WebSQLStorage.prototype.putAttachment = function (id, name, blob) {
var db = this._database,
that = this,
part_size = this._blob_length;
return new RSVP.Queue()
.push(function () {
return that._init_db_promise;
})
.push(function () {
return queueSql(db, ["SELECT id FROM document WHERE id = ?"], [[id]]);
})
.push(function (result) {
var query_list = [],
argument_list = [],
blob_size = blob.size,
queue = new RSVP.Queue(),
i,
index;
if (result[0].rows.length === 0) {
throw new jIO.util.jIOError("Cannot access subdocument", 404);
}
query_list.push("DELETE FROM attachment WHERE id = ? " +
"AND attachment = ?");
argument_list.push([id, name]);
query_list.push("INSERT INTO attachment(id, attachment, part, blob)" +
"VALUES(?, ?, ?, ?)");
argument_list.push([id, name, -1,
blob.type || "application/octet-stream"]);
for (i = 0, index = 0; i < blob_size; i += part_size, index += 1) {
query_list.push("INSERT INTO attachment(id, attachment, part, blob)" +
"VALUES(?, ?, ?, ?)");
argument_list.push([id, name, index]);
sendBlobPart(blob.slice(i, i + part_size), argument_list, index,
queue);
}
queue.push(function () {
return queueSql(db, query_list, argument_list);
});
return queue;
});
};
WebSQLStorage.prototype.getAttachment = function (id, name, options) {
var db = this._database,
that = this,
part_size = this._blob_length,
start,
end,
start_index,
end_index;
if (options === undefined) { options = {}; }
start = options.start || 0;
end = options.end || -1;
if (start < 0 || (options.end !== undefined && options.end < 0)) {
throw new jIO.util.jIOError("_start and _end must be positive",
400);
}
if (start > end && end !== -1) {
throw new jIO.util.jIOError("_start is greater than _end",
400);
}
start_index = Math.floor(start / part_size);
if (start === 0) { start_index -= 1; }
end_index = Math.floor(end / part_size);
if (end % part_size === 0) {
end_index -= 1;
}
return new RSVP.Queue()
.push(function () {
return that._init_db_promise;
})
.push(function () {
var command = "SELECT part, blob FROM attachment WHERE id = ? AND " +
"attachment = ? AND part >= ?",
argument_list = [id, name, start_index];
if (end !== -1) {
command += " AND part <= ?";
argument_list.push(end_index);
}
return queueSql(db, [command], [argument_list]);
})
.push(function (response_list) {
var i,
response,
blob_array = [],
blob,
type;
response = response_list[0].rows;
if (response.length === 0) {
throw new jIO.util.jIOError("Cannot find document", 404);
}
for (i = 0; i < response.length; i += 1) {
if (response[i].part === -1) {
type = response[i].blob;
start_index += 1;
} else {
blob_array.push(jIO.util.dataURItoBlob(response[i].blob));
}
}
if ((start === 0) && (options.end === undefined)) {
return new Blob(blob_array, {type: type});
}
blob = new Blob(blob_array, {});
return blob.slice(start - (start_index * part_size),
end === -1 ? blob.size :
end - (start_index * part_size),
"application/octet-stream");
});
};
WebSQLStorage.prototype.removeAttachment = function (id, name) {
var db = this._database,
that = this;
return new RSVP.Queue()
.push(function () {
return that._init_db_promise;
})
.push(function () {
return queueSql(db, ["DELETE FROM attachment WHERE " +
"id = ? AND attachment = ?"], [[id, name]]);
})
.push(function (result) {
if (result[0].rowsAffected === 0) {
throw new jIO.util.jIOError("Cannot find document", 404);
}
return name;
});
};
WebSQLStorage.prototype.hasCapacity = function (name) {
return (name === "list" || (name === "include"));
};
WebSQLStorage.prototype.buildQuery = function (options) {
var db = this._database,
that = this,
query = "SELECT id";
return new RSVP.Queue()
.push(function () {
return that._init_db_promise;
})
.push(function () {
if (options === undefined) { options = {}; }
if (options.include_docs === true) {
query += ", data AS doc";
}
query += " FROM document";
return queueSql(db, [query], [[]]);
})
.push(function (result) {
var array = [],
len = result[0].rows.length,
i;
for (i = 0; i < len; i += 1) {
array.push(result[0].rows[i]);
array[i].value = {};
if (array[i].doc !== undefined) {
array[i].doc = JSON.parse(array[i].doc);
}
}
return array;
});
};
jIO.addStorage('websql', WebSQLStorage);
}(jIO, RSVP, Blob, openDatabase));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/xwikistorage.js 0000664 0000000 0000000 00000101222 13037644422 0030607 0 ustar 00root root 0000000 0000000 /*jslint
indent: 2,
maxlen: 80,
plusplus: true,
nomen: true,
regexp: true
*/
/*global
define: true,
exports: true,
require: true,
jIO: true,
jQuery: true,
window: true,
XMLHttpRequest,
FormData
*/
/**
* JIO XWiki Storage. Type = 'xwiki'.
* XWiki Document/Attachment storage.
*/
(function (dependencies, module) {
"use strict";
if (typeof define === 'function' && define.amd) {
return define(dependencies, module);
}
if (typeof exports === 'object') {
return module(require('jio'), require('jquery'));
}
module(jIO, jQuery);
}([
'jio',
'jquery'
], function (jIO, $) {
"use strict";
function detectWiki() {
// try first the meta tag, then look for js,
// then finally fail over to 'xwiki'...
return $('meta[name="wiki"]').attr('content') ||
(window.XWiki || {}).currentWiki ||
'xwiki';
}
function detectXWikiURL(wiki) {
var loc, action, idx;
loc = window.location.href;
action = (window.XWiki || {}).contextAction || 'view';
idx = loc.indexOf('/wiki/' + wiki + '/' + action + '/');
if (idx !== -1) {
return loc.substring(0, idx);
}
idx = loc.indexOf('/bin/' + action + '/');
if (idx !== -1) {
// single wiki host:port/xwiki/bin/view/Main/WebHome
return loc.substring(0, idx);
}
throw new Error("Unable to detect XWiki URL");
}
/**
* 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;
}
function detectIsPathBasedMultiwiki(xwikiurl) {
var loc = window.location.href;
if (loc.indexOf(xwikiurl + '/wiki/') === 0) { return true; }
if (loc.indexOf(xwikiurl + '/bin/') === 0) { return false; }
// warn the user that we're unusure?
return false;
}
/**
* The JIO XWikiStorage extension
*
* @class XWikiStorage
* @constructor
*/
function XWikiStorage(spec) {
spec = spec || {};
var priv = {};
// the wiki to store stuff in
priv.wiki = spec.wiki || detectWiki();
// URL location of the wiki
// XWiki doesn't currently allow cross-domain requests.
priv.xwikiurl = (spec.xwikiurl !== undefined)
? spec.xwikiurl : detectXWikiURL(priv.wiki);
// Which URL to load for getting the Anti-CSRF form token, used for testing.
priv.formTokenPath = spec.formTokenPath || priv.xwikiurl;
priv.pathBasedMultiwiki = (spec.pathBasedMultiwiki !== undefined)
? spec.pathBasedMultiwiki : detectIsPathBasedMultiwiki(priv.xwikiurl);
/**
* Get the Space and Page components of a documkent ID.
*
* @param id the document id.
* @return a map of { 'space':, 'page': }
*/
priv.getParts = function (id) {
if (id.indexOf('/') === -1) {
return {
space: 'Main',
page: id
};
}
return {
space: id.substring(0, id.indexOf('/')),
page: id.substring(id.indexOf('/') + 1)
};
};
/**
* Get the Anti-CSRF token and do something with it.
*
* @param andThen function which is called with (formToken, err)
* as parameters.
*/
priv.doWithFormToken = function (andThen) {
$.ajax({
url: priv.formTokenPath,
type: "GET",
async: true,
dataType: 'text',
success: function (html) {
var m, token;
// this is unreliable
//var token = $('meta[name=form_token]', html).attr("content");
m = html.match(//);
token = (m && m[1]) || null;
if (!token) {
andThen(null, {
"status": 404,
"statusText": "Not Found",
"error": "err_form_token_not_found",
"message": "Anti-CSRF form token was not found in page",
"reason": "XWiki main page did not contain expected " +
"Anti-CSRF form token"
});
} else {
andThen(token, null);
}
},
error: function (jqxhr, err, cause) {
andThen(null, {
"status": jqxhr.status,
"statusText": jqxhr.statusText,
"error": err,
"message": "Could not get Anti-CSRF form token from [" +
priv.xwikiurl + "]",
"reason": cause
});
}
});
};
/**
* Get the REST read URL for a document.
*
* @param docId the id of the document.
* @return the REST URL for accessing this document.
*/
priv.getDocRestURL = function (docId) {
var parts = priv.getParts(docId);
return priv.xwikiurl + '/rest/wikis/'
+ priv.wiki + '/spaces/' + parts.space + '/pages/' + parts.page;
};
priv.getURL = function (action, space, page) {
var out = [priv.xwikiurl];
if (!priv.pathBasedMultiwiki) {
out.push('bin');
} else {
out.push('wiki', priv.wiki);
}
out.push(action, space, page);
return out.join('/');
};
/*
* Wrapper for the xwikistorage based on localstorage JiO store.
*/
priv._storage = this._storage = {
/**
* Get content of an XWikiDocument.
*
* @param docId the document ID.
* @param andThen a callback taking (doc, err), doc being the document
* json object and err being the error if any.
*/
getItem: function (docId, andThen) {
function attachSuccess(doc, jqxhr) {
var out = {}, xd;
if (jqxhr.status !== 200) {
andThen(null, {
"status": jqxhr.status,
"statusText": jqxhr.statusText,
"error": "",
"message": "Failed to get attachments for document [" +
docId + "]",
"reason": ""
});
return;
}
try {
xd = $(jqxhr.responseText);
xd.find('attachment').each(function () {
var attach = {}, attachXML = $(this);
attachXML.find('mimeType').each(function () {
attach.content_type = $(this).text();
});
attachXML.find('size').each(function () {
attach.length = Number($(this).text());
});
attach.digest = "unknown-0";
attachXML.find('name').each(function () {
out[$(this).text()] = attach;
});
});
doc._attachments = out;
andThen(doc, null);
} catch (err) {
andThen(null, {
status: 500,
statusText: "internal error",
error: err,
message: err.message,
reason: ""
});
}
}
function getAttachments(doc) {
$.ajax({
url: priv.getDocRestURL(docId) + '/attachments',
type: "GET",
async: true,
dataType: 'xml',
complete: function (jqxhr) {
attachSuccess(doc, jqxhr);
}
});
}
function success(jqxhr) {
var out, xd;
out = {};
try {
xd = $(jqxhr.responseText);
xd.find('modified').each(function () {
out._last_modified = Date.parse($(this).text());
});
xd.find('created').each(function () {
out._creation_date = Date.parse($(this).text());
});
xd.find('title').each(function () { out.title = $(this).text(); });
xd.find('parent').each(function () {
out.parent = $(this).text();
});
xd.find('syntax').each(function () {
out.syntax = $(this).text();
});
xd.find('content').each(function () {
out.content = $(this).text();
});
out._id = docId;
getAttachments(out);
} catch (err) {
andThen(null, {
status: 500,
statusText: "internal error",
error: err,
message: err.message,
reason: ""
});
}
}
$.ajax({
url: priv.getDocRestURL(docId),
type: "GET",
async: true,
dataType: 'xml',
// Use complete instead of success and error because phantomjs
// sometimes causes error to be called with http return code 200.
complete: function (jqxhr) {
if (jqxhr.status === 404) {
andThen(null, null);
return;
}
if (jqxhr.status !== 200) {
andThen(null, {
"status": jqxhr.status,
"statusText": jqxhr.statusText,
"error": "",
"message": "Failed to get document [" + docId + "]",
"reason": ""
});
return;
}
success(jqxhr);
}
});
},
/**
* Get content of an XWikiAttachment.
*
* @param attachId the attachment ID.
* @param andThen a callback taking (attach, err), attach being the
* attachment blob and err being the error if any.
*/
getAttachment: function (docId, fileName, andThen) {
var xhr, parts, url;
// need to do this manually, jquery doesn't support returning blobs.
xhr = new XMLHttpRequest();
parts = priv.getParts(docId);
url = priv.getURL('download', parts.space, parts.page) +
'/' + fileName + '?cb=' + Math.random();
xhr.open('GET', url, true);
xhr.responseType = 'blob';
xhr.onload = function () {
if (xhr.status === 200) {
var contentType = xhr.getResponseHeader("Content-Type");
if (contentType.indexOf(';') > -1) {
contentType = contentType.substring(0, contentType.indexOf(';'));
}
andThen(xhr.response);
} else {
andThen(null, {
"status": xhr.status,
"statusText": xhr.statusText,
"error": "err_network_error",
"message": "Failed to get attachment ["
+ docId + "/" + fileName + "]",
"reason": "Error getting data from network"
});
}
};
xhr.send();
},
/**
* Store an XWikiDocument.
*
* @param id the document identifier.
* @param doc the document JSON object containing
* "parent", "title", "content", and/or "syntax" keys.
* @param andThen a callback taking (err), err being the error if any.
*/
setItem: function (id, doc, andThen) {
priv.doWithFormToken(function (formToken, err) {
if (err) {
andThen(err);
return;
}
var parts = priv.getParts(id);
$.ajax({
url: priv.getURL('preview', parts.space, parts.page),
type: "POST",
async: true,
dataType: 'text',
data: {
parent: doc.parent || '',
title: doc.title || '',
xredirect: '',
language: 'en',
// RequiresHTMLConversion: 'content',
// content_syntax: doc.syntax || 'xwiki/2.1',
content: doc.content || '',
xeditaction: 'edit',
comment: 'Saved by JiO',
action_saveandcontinue: 'Save & Continue',
syntaxId: doc.syntax || 'xwiki/2.1',
xhidden: 0,
minorEdit: 0,
ajax: true,
form_token: formToken
},
success: function () {
andThen(null);
},
error: function (jqxhr, err, cause) {
andThen({
"status": jqxhr.status,
"statusText": jqxhr.statusText,
"error": err,
"message": "Failed to store document [" + id + "]",
"reason": cause
});
}
});
});
},
/**
* Store an XWikiAttachment.
*
* @param docId the ID of the document to attach to.
* @param fileName the attachment file name.
* @param mimeType the MIME type of the attachment content.
* @param blob the attachment content.
* @param andThen a callback taking one parameter, the error if any.
*/
setAttachment: function (docId, fileName, blob, andThen) {
priv.doWithFormToken(function (formToken, err) {
var parts, fd, xhr;
if (err) {
andThen(err);
return;
}
parts = priv.getParts(docId);
fd = new FormData();
fd.append("filepath", blob, fileName);
fd.append("form_token", formToken);
xhr = new XMLHttpRequest();
xhr.open(
'POST',
priv.getURL('upload', parts.space, parts.page),
true
);
xhr.onload = function () {
if (xhr.status === 302 || xhr.status === 200) {
andThen(null);
} else {
andThen({
"status": xhr.status,
"statusText": xhr.statusText,
"error": "err_network_error",
"message": "Failed to store attachment ["
+ docId + "/" + fileName + "]",
"reason": "Error posting data"
});
}
};
xhr.send(fd);
});
},
removeItem: function (id, andThen) {
priv.doWithFormToken(function (formToken, err) {
if (err) {
andThen(err);
return;
}
var parts = priv.getParts(id);
$.ajax({
url: priv.getURL('delete', parts.space, parts.page),
type: "POST",
async: true,
dataType: 'text',
data: {
confirm: '1',
form_token: formToken
},
success: function () {
andThen(null);
},
error: function (jqxhr, err, cause) {
andThen({
"status": jqxhr.status,
"statusText": jqxhr.statusText,
"error": err,
"message": "Failed to delete document [" + id + "]",
"reason": cause
});
}
});
});
},
removeAttachment: function (docId, fileName, andThen) {
var parts = priv.getParts(docId);
priv.doWithFormToken(function (formToken, err) {
if (err) {
andThen(err);
return;
}
$.ajax({
url: priv.getURL('delattachment', parts.space, parts.page) +
'/' + fileName,
type: "POST",
async: true,
dataType: 'text',
data: {
ajax: '1',
form_token: formToken
},
success: function () {
andThen(null);
},
error: function (jqxhr, err, cause) {
andThen({
"status": jqxhr.status,
"statusText": jqxhr.statusText,
"error": err,
"message": "Failed to delete attachment ["
+ docId + '/' + fileName + "]",
"reason": cause
});
}
});
});
},
/**
* Gets a document list from the xwiki storage.
* It will retreive an array containing files meta data owned by
* the user.
* @method allDocs
*/
allDocs: function (includeDocs, andThen) {
var getData = function (callback) {
$.ajax({
url: priv.xwikiurl + '/rest/wikis/xwiki/pages?cb=' + Date.now(),
type: "GET",
async: true,
dataType: 'xml',
success: function (xmlData) {
var data = [];
$(xmlData).find('fullName').each(function () {
data[data.length] = $(this).text();
});
callback(data);
},
error: function (error) {
andThen(null, error);
}
});
};
getData(function (rows, err) {
var i, next;
next = function (i) {
priv._storage.getItem(rows[i].id, function (doc, err) {
if (err) {
andThen(null, err);
return;
}
rows[i].doc = doc;
if (i < rows.length) {
next(i + 1);
} else {
andThen(rows);
}
});
};
if (err) {
return andThen(null, err);
}
for (i = 0; i < rows.length; i++) {
rows[i] = {
id: rows[i],
key: rows[i],
value: {}
};
}
if (includeDocs) {
next(0);
} else {
andThen(rows);
}
});
}
};
}
/**
* Create a document in local storage.
*
* @method post
* @param {Object} command The JIO command
* @param {Object} metadata The metadata to store
* @param {Object} options The command options
*/
XWikiStorage.prototype.post = function (command, metadata) {
var doc_id = metadata._id, that = this;
if (!doc_id) {
doc_id = jIO.util.generateUuid();
}
that._storage.getItem(doc_id, function (doc, err) {
if (err) {
command.error(err);
return;
}
if (doc === null) {
// the document does not exist
doc = jIO.util.deepClone(metadata);
doc._id = doc_id;
delete doc._attachments;
that._storage.setItem(doc_id, doc, function (err) {
if (err) {
command.error(
"failed",
"failed to upload document",
String(err)
);
} else {
command.success({"id": doc_id});
}
});
} else {
// the document already exists
command.error(
"conflict",
"document exists",
"Cannot create a new document"
);
}
});
};
/**
* 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
*/
XWikiStorage.prototype.put = function (command, metadata) {
var tmp, status, that = this;
that._storage.getItem(metadata._id, function (doc, err) {
if (err) {
command.error(err);
return;
}
if (doc === null || doc === undefined) {
// the document does not exist
doc = jIO.util.deepClone(metadata);
delete doc._attachments;
status = "created";
} else {
// the document already exists
tmp = jIO.util.deepClone(metadata);
tmp._attachments = doc._attachments;
doc = tmp;
status = "no_content";
}
// write
that._storage.setItem(metadata._id, doc, function (err) {
if (err) { command.error(err); return; }
command.success(status);
});
});
};
/**
* Add an attachment to a document
*
* @method putAttachment
* @param {Object} command The JIO command
* @param {Object} param The given parameters
* @param {Object} options The command options
*/
XWikiStorage.prototype.putAttachment = function (command, param) {
var that = this, status = "created";
that._storage.getItem(param._id, function (doc, err) {
if (err) {
return command.error(err);
}
if (doc === null) {
// the document does not exist
return command.error(
"not_found",
"missing",
"Impossible to add attachment"
);
}
// the document already exists
// download data
if ((doc._attachments || {})[param._attachment]) {
status = "no_content";
}
that._storage.setAttachment(param._id,
param._attachment,
param._blob,
function (err) {
if (err) {
command.error(err);
} else {
// XWiki doesn't do digests of attachments
// so we'll calculate it on the client side.
jIO.util.readBlobAsBinaryString(param._blob).then(function (e) {
command.success(status,
{"digest": jIO.util.makeBinaryStringDigest(e.target.result)}
);
});
}
});
});
};
/**
* Get a document
*
* @method get
* @param {Object} command The JIO command
* @param {Object} param The given parameters
* @param {Object} options The command options
*/
XWikiStorage.prototype.get = function (command, param) {
this._storage.getItem(param._id, function (ret, err) {
if (err) { command.error(err); return; }
if (ret === null) {
command.error(
"not_found",
"missing",
"Cannot find document"
);
} else {
command.success({"data": ret});
}
});
};
/**
* Get an attachment
*
* @method getAttachment
* @param {Object} command The JIO command
* @param {Object} param The given parameters
* @param {Object} options The command options
*/
XWikiStorage.prototype.getAttachment = function (command, param) {
var that = this;
that._storage.getItem(param._id, function (doc, err) {
if (err) {
return command.error(err);
}
if (doc === null) {
return command.error(
"not_found",
"missing document",
"Cannot find document"
);
}
if (typeof doc._attachments !== 'object' ||
typeof doc._attachments[param._attachment] !== 'object') {
return command.error(
"not_found",
"missing attachment",
"Cannot find attachment"
);
}
that._storage.getAttachment(param._id, param._attachment,
function (blob, err) {
var attach = doc._attachments[param._attachment];
if (err) {
return command.error(err);
}
if (blob.size !== attach.length) {
return command.error(
"incomplete",
"attachment size incorrect",
"expected [" + attach.size + "] bytes, got [" + blob.size + "]"
);
}
command.success({
"data": blob,
"digest": attach.digest || "",
"content_type": attach.content_type || ""
});
});
});
};
/**
* Remove a document
*
* @method remove
* @param {Object} command The JIO command
* @param {Object} param The given parameters
* @param {Object} options The command options
*/
XWikiStorage.prototype.remove = function (command, param) {
this._storage.removeItem(param._id, function (err) {
if (err) {
command.error(err);
} else {
command.success();
}
});
};
/**
* Remove an attachment
*
* @method removeAttachment
* @param {Object} command The JIO command
* @param {Object} param The given parameters
* @param {Object} options The command options
*/
XWikiStorage.prototype.removeAttachment = function (command, param) {
var that = this;
that._storage.getItem(param._id, function (doc, err) {
if (err) {
return command.error(err);
}
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"
);
}
that._storage.removeAttachment(param._id, param._attachment,
function (err) {
if (err) {
command.error(err);
} else {
command.success();
}
});
});
};
/**
* 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
*/
XWikiStorage.prototype.allDocs = function (command, param, options) {
var i, document_list, document_object, delete_id, that = this;
param.unused = true;
document_list = [];
if (options.query === undefined && options.sort_on === undefined &&
options.select_list === undefined &&
options.include_docs === undefined) {
that._storage.allDocs(options.include_docs, function (rows, err) {
if (err) {
return command.error(err);
}
command.success({"data": {"rows": rows, "total_rows": rows.length}});
});
} else {
that._storage.allDocs(true, function (rows, err) {
if (err) {
return command.error(err);
}
for (i = 0; i < rows.length; i++) {
document_list.push(rows[i].doc);
}
});
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
*/
XWikiStorage.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
*/
XWikiStorage.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
*/
XWikiStorage.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(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(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(param._id, doc);
return; // OK
}
for (i in doc._attachments) {
if (doc._attachments.hasOwnProperty(i)) {
// check attachment existence
if (that._storage.getItem(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(param._id, doc);
}
// OK
}
function repairAll(param, repair) {
var i, result;
for (i in that._database) {
if (that._database.hasOwnProperty(i)) {
// browsing every entry
// is part of the user space
if (/^[^\/]+\/[^\/]+$/.test(i)) {
// this is an attachment
attachmentFound(param, i);
} else if (/^[^\/]+$/.test(i)) {
// this is a document
param._id = i;
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(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('xwiki', XWikiStorage);
}));
jio-8a170cabe382a651ac2b8394e47f04fbc8fc01fe-src-jio.storage/src/jio.storage/zipstorage.js 0000664 0000000 0000000 00000006732 13037644422 0030270 0 ustar 00root root 0000000 0000000 /*jslint nomen: true*/
/*global RSVP, Blob, LZString, DOMException*/
(function (RSVP, Blob, LZString, DOMException) {
"use strict";
/**
* The jIO ZipStorage extension
*
* @class ZipStorage
* @constructor
*/
var MIME_TYPE = "application/x-jio-utf16_lz_string";
function ZipStorage(spec) {
this._sub_storage = jIO.createJIO(spec.sub_storage);
}
ZipStorage.prototype.get = function () {
return this._sub_storage.get.apply(this._sub_storage,
arguments);
};
ZipStorage.prototype.post = function () {
return this._sub_storage.post.apply(this._sub_storage,
arguments);
};
ZipStorage.prototype.put = function () {
return this._sub_storage.put.apply(this._sub_storage,
arguments);
};
ZipStorage.prototype.remove = function () {
return this._sub_storage.remove.apply(this._sub_storage,
arguments);
};
ZipStorage.prototype.hasCapacity = function () {
return this._sub_storage.hasCapacity.apply(this._sub_storage,
arguments);
};
ZipStorage.prototype.buildQuery = function () {
return this._sub_storage.buildQuery.apply(this._sub_storage,
arguments);
};
ZipStorage.prototype.getAttachment = function (id, name) {
var that = this;
return that._sub_storage.getAttachment(id, name)
.push(function (blob) {
if (blob.type !== MIME_TYPE) {
return blob;
}
return new RSVP.Queue()
.push(function () {
return jIO.util.readBlobAsText(blob, 'utf16');
})
.push(function (evt) {
var result =
LZString.decompressFromUTF16(evt.target.result);
if (result === '') {
return blob;
}
try {
return jIO.util.dataURItoBlob(
result
);
} catch (error) {
if (error instanceof DOMException) {
return blob;
}
throw error;
}
});
});
};
function myEndsWith(str, query) {
return (str.indexOf(query) === str.length - query.length);
}
ZipStorage.prototype.putAttachment = function (id, name, blob) {
var that = this;
if ((blob.type.indexOf("text/") === 0) || myEndsWith(blob.type, "xml") ||
myEndsWith(blob.type, "json")) {
return new RSVP.Queue()
.push(function () {
return jIO.util.readBlobAsDataURL(blob);
})
.push(function (data) {
var result = LZString.compressToUTF16(data.target.result);
blob = new Blob([result],
{type: MIME_TYPE});
return that._sub_storage.putAttachment(id, name, blob);
});
}
return this._sub_storage.putAttachment.apply(this._sub_storage,
arguments);
};
ZipStorage.prototype.removeAttachment = function () {
return this._sub_storage.removeAttachment.apply(this._sub_storage,
arguments);
};
ZipStorage.prototype.allAttachments = function () {
return this._sub_storage.allAttachments.apply(this._sub_storage,
arguments);
};
jIO.addStorage('zip', ZipStorage);
}(RSVP, Blob, LZString, DOMException));