Commit 2fe3ea44 authored by Tristan Cavelier's avatar Tristan Cavelier

all storage removed except localstorage

parent 3356688e
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global jIO: true, hex_sha256: true, setTimeout: true */
jIO.addStorageType('conflictmanager', function (spec, my) {
var that, priv, storage_exists, local_namespace, empty_fun,
super_serialized;
spec = spec || {};
that = my.basicStorage(spec, my);
priv = {};
storage_exists = (spec.storage ? true : false);
priv.sub_storage_spec = spec.storage || {
type: 'base'
};
priv.sub_storage_string = JSON.stringify(priv.sub_storage_spec);
local_namespace = 'jio/conflictmanager/' + priv.sub_storage_string + '/';
empty_fun = function () {};
super_serialized = that.serialized;
that.serialized = function () {
var o = super_serialized();
o.storage = priv.sub_storage_spec;
return o;
};
that.validateState = function () {
if (storage_exists) {
return '';
}
return 'Need at least one parameter: "storage".';
};
priv.getDistantMetadata = function (command, path, success, error) {
var cloned_option = command.cloneOption();
cloned_option.metadata_only = false;
that.addJob('get', priv.sub_storage_spec, path, cloned_option,
success, error);
};
priv.saveMetadataToDistant = function (command, path, content, success,
error) {
that.addJob('put', priv.sub_storage_spec, {
_id: path,
content: JSON.stringify(content)
},
command.cloneOption(), success, error);
};
priv.saveNewRevision = function (command, path, content, success, error) {
that.addJob('post', priv.sub_storage_spec, {
_id: path,
content: content
},
command.cloneOption(), success, error);
};
priv.loadRevision = function (command, path, success, error) {
that.addJob('get', priv.sub_storage_spec, path, command.cloneOption(),
success, error);
};
priv.deleteAFile = function (command, path, success, error) {
that.addJob('remove', priv.sub_storage_spec, {
_id: path
},
command.cloneOption(), success, error);
};
priv.chooseARevision = function (metadata) {
var tmp_last_modified = 0,
ret_rev = '',
rev;
for (rev in metadata) {
if (metadata.hasOwnProperty(rev)) {
if (tmp_last_modified < metadata[rev]._last_modified) {
tmp_last_modified = metadata[rev]._last_modified;
ret_rev = rev;
}
}
}
return ret_rev;
};
priv._revs = function (metadata, revision) {
if (!(metadata && revision)) {
return null;
}
if (metadata[revision]) {
return {
start: metadata[revision]._revisions.length,
ids: metadata[revision]._revisions
};
}
return null;
};
priv._revs_info = function (metadata) {
if (!metadata) {
return null;
}
var k, l = [];
for (k in metadata) {
if (metadata.hasOwnProperty(k)) {
l.push({
rev: k,
status: (
metadata[k] ? (
metadata[k]._deleted ? 'deleted' : 'available'
) : 'missing'
)
});
}
}
return l;
};
priv.solveConflict = function (doc, option, param) {
var o = {}, am = priv.newAsyncModule(),
command = param.command,
metadata_file_path = param.docid + '.metadata',
current_revision = '',
current_revision_file_path = '',
metadata_file_content = null,
on_conflict = false,
conflict_object = {
total_rows: 0,
rows: []
},
on_remove = param._deleted,
previous_revision = param.previous_revision,
previous_revision_content_object = null,
now = new Date(),
failerror;
o.getDistantMetadata = function () {
priv.getDistantMetadata(
command,
metadata_file_path,
function (result) {
var previous_revision_number =
parseInt(previous_revision.split('-')[0], 10);
metadata_file_content = JSON.parse(result.content);
// set current revision
// jslint: removed '' in hex_sha256(''...
current_revision = (previous_revision_number + 1) + '-' +
hex_sha256(doc.content + previous_revision +
JSON.stringify(metadata_file_content));
current_revision_file_path = param.docid + '.' + current_revision;
previous_revision_content_object = metadata_file_content[
previous_revision
] || {};
if (!on_remove) {
am.wait(o, 'saveMetadataOnDistant', 1);
am.call(o, 'saveNewRevision');
}
am.call(o, 'previousUpdateMetadata');
},
function (error) {
am.call(o, 'error', [error]);
}
);
};
o.saveNewRevision = function () {
priv.saveNewRevision(
command,
current_revision_file_path,
doc.content,
function () {
am.call(o, 'saveMetadataOnDistant');
},
function (error) {
am.call(o, 'error', [error]);
}
);
};
o.previousUpdateMetadata = function () {
var i;
for (i = 0; i < param.key.length; i += 1) {
delete metadata_file_content[param.key[i]];
}
am.call(o, 'checkForConflicts');
};
o.checkForConflicts = function () {
var rev;
for (rev in metadata_file_content) {
if (metadata_file_content.hasOwnProperty(rev)) {
on_conflict = true;
failerror = {
status: 409,
error: 'conflict',
statusText: 'Conflict',
reason: 'document update conflict',
message: 'There is one or more conflicts'
};
break;
}
}
am.call(o, 'updateMetadata');
};
o.updateMetadata = function () {
var revision_history, id = '';
id = current_revision.split('-');
id.shift();
id = id.join('-');
revision_history = previous_revision_content_object._revisions;
revision_history.unshift(id);
metadata_file_content[current_revision] = {
_creation_date: previous_revision_content_object._creation_date ||
now.getTime(),
_last_modified: now.getTime(),
_revisions: revision_history,
_conflict: on_conflict,
_deleted: on_remove
};
if (on_conflict) {
conflict_object = priv.createConflictObject(
command,
metadata_file_content,
current_revision
);
}
am.call(o, 'saveMetadataOnDistant');
};
o.saveMetadataOnDistant = function () {
priv.saveMetadataToDistant(
command,
metadata_file_path,
metadata_file_content,
function () {
am.call(o, 'deleteAllConflictingRevision');
if (on_conflict) {
am.call(o, 'error');
} else {
am.call(o, 'success');
}
},
function (error) {
am.call(o, 'error', [error]);
}
);
};
o.deleteAllConflictingRevision = function () {
var i;
for (i = 0; i < param.key.length; i += 1) {
priv.deleteAFile(
command,
param.docid + '.' + param.key[i],
empty_fun,
empty_fun
);
}
};
o.success = function () {
var a = {
ok: true,
id: param.docid,
rev: current_revision
};
am.neverCall(o, 'error');
am.neverCall(o, 'success');
if (option.revs) {
a.revisions = priv._revs(
metadata_file_content,
current_revision
);
}
if (option.revs_info) {
a.revs_info = priv._revs_info(metadata_file_content);
}
if (option.conflicts) {
a.conflicts = conflict_object;
}
param.success(a);
};
o.error = function (error) {
var err = error || failerror || {
status: 0,
statusText: 'Unknown',
error: 'unknown_error',
message: 'Unknown error.',
reason: 'unknown error'
};
if (current_revision) {
err.rev = current_revision;
}
if (option.revs) {
err.revisions = priv._revs(
metadata_file_content,
current_revision
);
}
if (option.revs_info) {
err.revs_info = priv._revs_info(
metadata_file_content
);
}
if (option.conflicts) {
err.conflicts = conflict_object;
}
am.neverCall(o, 'error');
am.neverCall(o, 'success');
param.error(err);
};
am.call(o, 'getDistantMetadata');
};
priv.createConflictObject = function (command, metadata, revision) {
return {
total_rows: 1,
rows: [priv.createConflictRow(
command,
command.getDocId(),
metadata,
revision
)]
};
};
priv.getParam = function (list) {
var param = {}, i = 0;
if (typeof list[i] === 'string') {
param.content = list[i];
i += 1;
}
if (typeof list[i] === 'object') {
param.options = list[i];
i += 1;
} else {
param.options = {};
}
param.callback = function () {};
param.success = function (val) {
param.callback(undefined, val);
};
param.error = function (err) {
param.callback(err, undefined);
};
if (typeof list[i] === 'function') {
if (typeof list[i + 1] === 'function') {
param.success = list[i];
param.error = list[i + 1];
} else {
param.callback = list[i];
}
}
return param;
};
priv.createConflictRow = function (command, docid, metadata, revision) {
var row = {
id: docid,
key: [],
value: {
// jslint: removed params /* content, option, success, error */
_solveConflict: function () {
var param = {}, got = priv.getParam(arguments);
if (got.content === undefined) {
param._deleted = true;
} else {
param._deleted = false;
}
param.success = got.success;
param.error = got.error;
param.previous_revision = revision;
param.docid = docid;
param.key = row.key;
param.command = command.clone();
return priv.solveConflict({
_id: docid,
content: got.content,
_rev: revision
},
got.options, param);
}
}
}, k;
for (k in metadata) {
if (metadata.hasOwnProperty(k)) {
row.key.push(k);
}
}
return row;
};
priv.newAsyncModule = function () {
var async = {};
async.call = function (obj, function_name, arglist) {
obj._wait = obj._wait || {};
if (obj._wait[function_name]) {
obj._wait[function_name] -= 1;
return empty_fun;
}
// ok if undef or 0
arglist = arglist || [];
setTimeout(function () {
obj[function_name].apply(obj[function_name], arglist);
});
};
async.neverCall = function (obj, function_name) {
obj._wait = obj._wait || {};
obj._wait[function_name] = -1;
};
async.wait = function (obj, function_name, times) {
obj._wait = obj._wait || {};
obj._wait[function_name] = times;
};
async.end = function () {
async.call = empty_fun;
};
return async;
};
that.post = function (command) {
that.put(command);
};
/**
* Save a document and can manage conflicts.
* @method put
*/
that.put = function (command) {
var o = {}, am = priv.newAsyncModule(),
metadata_file_path = command.getDocId() + '.metadata',
current_revision = '',
current_revision_file_path = '',
metadata_file_content = null,
on_conflict = false,
conflict_object = {
total_rows: 0,
rows: []
},
previous_revision = command.getDocInfo('_rev') || '0',
previous_revision_file_path = command.getDocId() + '.' +
previous_revision,
now = new Date(),
failerror;
o.getDistantMetadata = function () {
priv.getDistantMetadata(
command,
metadata_file_path,
function (result) {
var previous_revision_number =
parseInt(previous_revision.split('-')[0], 10);
metadata_file_content = JSON.parse(result.content);
// set current revision
current_revision = (previous_revision_number + 1) + '-' +
// jslint: removed hex_sha256(''+...
hex_sha256(command.getDocContent() + previous_revision +
JSON.stringify(metadata_file_content));
current_revision_file_path = command.getDocId() + '.' +
current_revision;
am.wait(o, 'saveMetadataOnDistant', 1);
am.call(o, 'saveNewRevision');
am.call(o, 'checkForConflicts');
},
function (error) {
if (error.status === 404) {
current_revision = '1-' + hex_sha256(command.getDocContent());
current_revision_file_path = command.getDocId() + '.' +
current_revision;
am.wait(o, 'saveMetadataOnDistant', 1);
am.call(o, 'saveNewRevision');
am.call(o, 'createMetadata');
} else {
am.call(o, 'error', [error]);
}
}
);
};
o.saveNewRevision = function () {
priv.saveNewRevision(
command,
current_revision_file_path,
command.getDocContent(),
function () {
am.call(o, 'saveMetadataOnDistant');
},
function (error) {
am.call(o, 'error', [error]);
}
);
};
o.checkForConflicts = function () {
var rev;
for (rev in metadata_file_content) {
if (metadata_file_content.hasOwnProperty(rev) &&
rev !== previous_revision) {
on_conflict = true;
failerror = {
status: 409,
error: 'conflict',
statusText: 'Conflict',
reason: 'document update conflict',
message: 'Document update conflict.'
};
break;
}
}
am.call(o, 'updateMetadata');
};
o.createMetadata = function () {
var id = current_revision;
id = id.split('-');
id.shift();
id = id.join('-');
metadata_file_content = {};
metadata_file_content[current_revision] = {
_creation_date: now.getTime(),
_last_modified: now.getTime(),
_revisions: [id],
_conflict: false,
_deleted: false
};
am.call(o, 'saveMetadataOnDistant');
};
o.updateMetadata = function () {
var previous_creation_date, revision_history = [],
id = '';
if (metadata_file_content[previous_revision]) {
previous_creation_date = metadata_file_content[
previous_revision
]._creation_date;
revision_history = metadata_file_content[
previous_revision
]._revisions;
delete metadata_file_content[previous_revision];
}
id = current_revision.split('-');
id.shift();
id = id.join('-');
revision_history.unshift(id);
metadata_file_content[current_revision] = {
_creation_date: previous_creation_date || now.getTime(),
_last_modified: now.getTime(),
_revisions: revision_history,
_conflict: on_conflict,
_deleted: false
};
if (on_conflict) {
conflict_object = priv.createConflictObject(
command,
metadata_file_content,
current_revision
);
}
am.call(o, 'saveMetadataOnDistant');
};
o.saveMetadataOnDistant = function () {
priv.saveMetadataToDistant(
command,
metadata_file_path,
metadata_file_content,
function () {
am.call(o, 'deletePreviousRevision');
if (on_conflict) {
am.call(o, 'error');
} else {
am.call(o, 'success');
}
},
function (error) {
am.call(o, 'error', [error]);
}
);
};
o.deletePreviousRevision = function () {
// jslint: removed /*&& !on_conflict*/
if (previous_revision !== '0') {
priv.deleteAFile(
command,
previous_revision_file_path,
empty_fun,
empty_fun
);
}
};
o.success = function () {
var a = {
ok: true,
id: command.getDocId(),
rev: current_revision
};
am.neverCall(o, 'error');
am.neverCall(o, 'success');
if (command.getOption('revs')) {
a.revisions = priv._revs(
metadata_file_content,
current_revision
);
}
if (command.getOption('revs_info')) {
a.revs_info = priv._revs_info(metadata_file_content);
}
if (command.getOption('conflicts')) {
a.conflicts = conflict_object;
}
that.success(a);
};
o.error = function (error) {
var err = error || failerror || {
status: 0,
statusText: 'Unknown',
error: 'unknown_error',
message: 'Unknown error.',
reason: 'unknown error'
};
if (current_revision) {
err.rev = current_revision;
}
if (command.getOption('revs')) {
err.revisions = priv._revs(
metadata_file_content,
current_revision
);
}
if (command.getOption('revs_info')) {
err.revs_info = priv._revs_info(metadata_file_content);
}
if (command.getOption('conflicts')) {
err.conflicts = conflict_object;
}
am.neverCall(o, 'error');
am.neverCall(o, 'success');
that.error(err);
};
am.call(o, 'getDistantMetadata');
}; // end put
/**
* Load a document from several storages, and send the first retreived
* document.
* @method get
*/
that.get = function (command) {
var o = {}, am = priv.newAsyncModule(),
metadata_file_path = command.getDocId() + '.metadata',
current_revision = command.getOption('rev') || '',
metadata_file_content = null,
metadata_only = command.getOption('metadata_only'),
on_conflict = false,
conflict_object = {
total_rows: 0,
rows: []
},
doc = {
_id: command.getDocId()
},
call404 = function (message) {
am.call(o, 'error', [{
status: 404,
statusText: 'Not Found',
error: 'not_found',
message: message,
reason: message
}]);
};
o.getDistantMetadata = function () {
priv.getDistantMetadata(
command,
metadata_file_path,
function (result) {
metadata_file_content = JSON.parse(result.content);
if (!metadata_only) {
am.wait(o, 'success', 1);
}
am.call(o, 'affectMetadata');
am.call(o, 'checkForConflicts');
},
function (error) {
am.call(o, 'error', [error]);
}
);
};
o.affectMetadata = function () {
if (current_revision) {
if (!metadata_file_content[current_revision]) {
return call404('Document revision does not exists.');
}
} else {
current_revision = priv.chooseARevision(metadata_file_content);
}
doc._last_modified =
metadata_file_content[current_revision]._last_modified;
doc._creation_date =
metadata_file_content[current_revision]._creation_date;
doc._rev = current_revision;
if (metadata_only) {
am.call(o, 'success');
} else {
am.call(o, 'loadRevision');
}
};
o.loadRevision = function () {
if (!current_revision ||
metadata_file_content[current_revision]._deleted) {
return call404('Document has been removed.');
}
priv.loadRevision(
command,
doc._id + '.' + current_revision,
function (result) {
doc.content = result.content;
am.call(o, 'success');
},
function (error) {
am.call(o, 'error', [error]);
}
);
};
o.checkForConflicts = function () {
if (metadata_file_content[current_revision]._conflict) {
on_conflict = true;
conflict_object = priv.createConflictObject(
command,
metadata_file_content,
current_revision
);
}
am.call(o, 'success');
};
o.success = function () {
am.neverCall(o, 'error');
am.neverCall(o, 'success');
if (command.getOption('revs')) {
doc._revisions = priv._revs(
metadata_file_content,
current_revision
);
}
if (command.getOption('revs_info')) {
doc._revs_info = priv._revs_info(metadata_file_content);
}
if (command.getOption('conflicts')) {
doc._conflicts = conflict_object;
}
that.success(doc);
};
o.error = function (error) {
var err = error || {
status: 0,
statusText: 'Unknown',
message: 'Unknown error.'
};
if (command.getOption('revs')) {
err._revisions = priv._revs(
metadata_file_content,
current_revision
);
}
if (command.getOption('revs_info')) {
err._revs_info = priv._revs_info(metadata_file_content);
}
if (command.getOption('conflicts')) {
err._conflicts = conflict_object;
}
am.neverCall(o, 'error');
am.neverCall(o, 'success');
that.error(err);
};
am.call(o, 'getDistantMetadata');
};
/**
* Get a document list from several storages, and returns the first
* retreived document list.
* @method allDocs
*/
that.allDocs = function (command) {
var o = {}, am = priv.newAsyncModule(),
metadata_only = command.getOption('metadata_only'),
result_list = [],
conflict_object = {
total_rows: 0,
rows: []
},
success_count = 0,
success_max = 0;
o.retreiveList = function () {
var cloned_option = command.cloneOption(),
success = function (result) {
am.call(o, 'filterTheList', [result]);
},
error = function (error) {
am.call(o, 'error', [error]);
};
cloned_option.metadata_only = true;
that.addJob('allDocs', priv.sub_storage_spec, null, cloned_option,
success, error
);
};
o.filterTheList = function (result) {
var i, splitname;
success_max += 1;
for (i = 0; i < result.total_rows; i += 1) {
splitname = result.rows[i].id.split('.') || [];
if (splitname.length > 0 && splitname[splitname.length - 1] ===
'metadata') {
success_max += 1;
splitname.length -= 1;
am.call(o, 'loadMetadataFile', [splitname.join('.')]);
}
}
am.call(o, 'success');
};
o.loadMetadataFile = function (path) {
priv.getDistantMetadata(
command,
path + '.metadata',
function (data) {
data = JSON.parse(data.content);
var revision = priv.chooseARevision(data);
if (!data[revision]._deleted) {
am.call(o, 'loadFile', [path, revision, data]);
} else {
am.call(o, 'success');
}
},
function (error) {
am.call(o, 'error', [error]);
}
);
};
o.loadFile = function (path, revision, data) {
var doc = {
id: path,
key: path,
value: {
_last_modified: data[revision]._last_modified,
_creation_date: data[revision]._creation_date,
_rev: revision
}
};
if (command.getOption('revs')) {
doc.value._revisions = priv._revs(data, revision);
}
if (command.getOption('revs_info')) {
doc.value._revs_info = priv._revs_info(data, revision);
}
if (command.getOption('conflicts')) {
if (data[revision]._conflict) {
conflict_object.total_rows += 1;
conflict_object.rows.push(
priv.createConflictRow(command, path, data, revision)
);
}
}
if (!metadata_only) {
priv.loadRevision(
command,
path + '.' + revision,
function (data) {
doc.content = data.content;
result_list.push(doc);
am.call(o, 'success');
},
function (error) {
am.call(o, 'error', [error]);
}
);
} else {
result_list.push(doc);
am.call(o, 'success');
}
};
o.success = function () {
var obj;
success_count += 1;
if (success_count >= success_max) {
am.end();
obj = {
total_rows: result_list.length,
rows: result_list
};
if (command.getOption('conflicts')) {
obj.conflicts = conflict_object;
}
that.success(obj);
}
};
o.error = function (error) {
am.end();
that.error(error);
};
am.call(o, 'retreiveList');
}; // end allDocs
/**
* Remove a document from several storages.
* @method remove
*/
that.remove = function (command) {
var o = {}, am = priv.newAsyncModule(),
metadata_file_path = command.getDocId() + '.metadata',
current_revision = '',
current_revision_file_path = '',
metadata_file_content = null,
on_conflict = false,
conflict_object = {
total_rows: 0,
rows: []
},
previous_revision = command.getOption('rev') || '0',
previous_revision_file_path = command.getDocId() + '.' +
previous_revision,
now = new Date(),
failerror;
o.getDistantMetadata = function () {
priv.getDistantMetadata(
command,
metadata_file_path,
function (result) {
metadata_file_content = JSON.parse(result.content);
if (previous_revision === 'last') {
previous_revision = priv.chooseARevision(metadata_file_content);
previous_revision_file_path = command.getDocId() + '.' +
previous_revision;
}
var previous_revision_number =
parseInt(previous_revision.split('-')[0], 10) || 0;
// set current revision
current_revision = (previous_revision_number + 1) + '-' +
//jslint: removed hex_sha256(''...
hex_sha256(previous_revision +
JSON.stringify(metadata_file_content)
);
current_revision_file_path = command.getDocId() + '.' +
current_revision;
am.call(o, 'checkForConflicts');
},
function (error) {
if (error.status === 404) {
am.call(o, 'error', [{
status: 404,
statusText: 'Not Found',
error: 'not_found',
reason: 'missing',
message: 'Document not found.'
}]);
} else {
am.call(o, 'error', [error]);
}
}
);
};
o.checkForConflicts = function () {
var rev;
for (rev in metadata_file_content) {
if (metadata_file_content.hasOwnProperty(rev) &&
rev !== previous_revision) {
on_conflict = true;
failerror = {
status: 409,
error: 'conflict',
statusText: 'Conflict',
reason: 'document update conflict',
message: 'There is one or more conflicts'
};
break;
}
}
am.call(o, 'updateMetadata');
};
o.updateMetadata = function () {
var previous_creation_date, revision_history = [],
id = '';
if (metadata_file_content[previous_revision]) {
previous_creation_date = metadata_file_content[
previous_revision
]._creation_date;
revision_history = metadata_file_content[
previous_revision
]._revisions;
delete metadata_file_content[previous_revision];
}
id = current_revision;
id = id.split('-');
id.shift();
id = id.join('-');
revision_history.unshift(id);
metadata_file_content[current_revision] = {
_creation_date: previous_creation_date || now.getTime(),
_last_modified: now.getTime(),
_revisions: revision_history,
_conflict: on_conflict,
_deleted: true
};
if (on_conflict) {
conflict_object = priv.createConflictObject(
command,
metadata_file_content,
current_revision
);
}
am.call(o, 'saveMetadataOnDistant');
};
o.saveMetadataOnDistant = function () {
priv.saveMetadataToDistant(
command,
metadata_file_path,
metadata_file_content,
function () {
am.call(o, 'deletePreviousRevision');
if (on_conflict) {
am.call(o, 'error');
} else {
am.call(o, 'success');
}
},
function (error) {
am.call(o, 'error', [error]);
}
);
};
o.deletePreviousRevision = function () {
// jslint: removed /*&& !on_conflict*/
if (previous_revision !== '0') {
priv.deleteAFile(
command,
previous_revision_file_path,
empty_fun,
empty_fun
);
}
};
o.success = function (revision) {
var a = {
ok: true,
id: command.getDocId(),
rev: revision || current_revision
};
am.neverCall(o, 'error');
am.neverCall(o, 'success');
if (command.getOption('revs')) {
a.revisions = priv._revs(
metadata_file_content,
current_revision
);
}
if (command.getOption('revs_info')) {
a.revs_info = priv._revs_info(metadata_file_content);
}
if (command.getOption('conflicts')) {
a.conflicts = conflict_object;
}
that.success(a);
};
o.error = function (error) {
var err = error || failerror || {
status: 0,
statusText: 'Unknown',
error: 'unknown_error',
message: 'Unknown error.',
reason: 'unknown error'
};
if (current_revision) {
err.rev = current_revision;
}
if (command.getOption('revs')) {
err.revisions = priv._revs(
metadata_file_content,
current_revision
);
}
if (command.getOption('revs_info')) {
err.revs_info = priv._revs_info(metadata_file_content);
}
if (command.getOption('conflicts')) {
err.conflicts = conflict_object;
}
am.neverCall(o, 'error');
am.neverCall(o, 'success');
that.error(err);
};
am.call(o, 'getDistantMetadata');
}; // end remove
return that;
});
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global jIO: true, sjcl: true, $: true, setTimeout: true */
jIO.addStorageType('crypt', function (spec, my) {
spec = spec || {};
var that = my.basicStorage(spec, my),
priv = {},
is_valid_storage = (spec.storage ? true : false),
super_serialized = that.serialized;
priv.username = spec.username || '';
priv.password = spec.password || '';
priv.sub_storage_spec = spec.storage || {
type: 'base'
};
priv.sub_storage_string = JSON.stringify(priv.sub_storage_string);
that.serialized = function () {
var o = super_serialized();
o.username = priv.username;
o.password = priv.password; // TODO : unsecured !!!
o.storage = priv.sub_storage_string;
return o;
};
that.validateState = function () {
if (priv.username && is_valid_storage) {
return '';
}
return 'Need at least two parameters: "username" and "storage".';
};
// TODO : IT IS NOT SECURE AT ALL!
// WE MUST REWORK CRYPTED STORAGE!
priv.encrypt_param_object = {
"iv": "kaprWwY/Ucr7pumXoTHbpA",
"v": 1,
"iter": 1000,
"ks": 256,
"ts": 128,
"mode": "ccm",
"adata": "",
"cipher": "aes",
"salt": "K4bmZG9d704"
};
priv.decrypt_param_object = {
"iv": "kaprWwY/Ucr7pumXoTHbpA",
"ks": 256,
"ts": 128,
"salt": "K4bmZG9d704"
};
priv.encrypt = function (data, callback) {
// end with a callback in order to improve encrypt to an
// asynchronous encryption.
var tmp = sjcl.encrypt(priv.username + ':' + priv.password, data,
priv.encrypt_param_object);
callback(JSON.parse(tmp).ct);
};
priv.decrypt = function (data, callback) {
var tmp, param = $.extend(true, {}, priv.decrypt_param_object);
param.ct = data || '';
param = JSON.stringify(param);
try {
tmp = sjcl.decrypt(priv.username + ':' + priv.password, param);
} catch (e) {
callback({
status: 403,
statusText: 'Forbidden',
error: 'forbidden',
message: 'Unable to decrypt.',
reason: 'unable to decrypt'
});
return;
}
callback(undefined, tmp);
};
priv.newAsyncModule = function () {
var async = {};
async.call = function (obj, function_name, arglist) {
obj._wait = obj._wait || {};
if (obj._wait[function_name]) {
obj._wait[function_name] -= 1;
return function () {};
}
// ok if undef or 0
arglist = arglist || [];
setTimeout(function () {
obj[function_name].apply(obj[function_name], arglist);
});
};
async.neverCall = function (obj, function_name) {
obj._wait = obj._wait || {};
obj._wait[function_name] = -1;
};
async.wait = function (obj, function_name, times) {
obj._wait = obj._wait || {};
obj._wait[function_name] = times;
};
async.end = function () {
async.call = function () {};
};
return async;
};
that.post = function (command) {
that.put(command);
};
/**
* Saves a document.
* @method put
*/
that.put = function (command) {
var new_file_name, new_file_content, am = priv.newAsyncModule(),
o = {};
o.encryptFilePath = function () {
priv.encrypt(command.getDocId(), function (res) {
new_file_name = res;
am.call(o, 'save');
});
};
o.encryptFileContent = function () {
priv.encrypt(command.getDocContent(), function (res) {
new_file_content = res;
am.call(o, 'save');
});
};
o.save = function () {
var success = function (val) {
val.id = command.getDocId();
that.success(val);
},
error = function (err) {
that.error(err);
},
cloned_doc = command.cloneDoc();
cloned_doc._id = new_file_name;
cloned_doc.content = new_file_content;
that.addJob('put', priv.sub_storage_spec, cloned_doc,
command.cloneOption(), success, error);
};
am.wait(o, 'save', 1);
am.call(o, 'encryptFilePath');
am.call(o, 'encryptFileContent');
}; // end put
/**
* Loads a document.
* @method get
*/
that.get = function (command) {
var new_file_name, am = priv.newAsyncModule(),
o = {};
o.encryptFilePath = function () {
priv.encrypt(command.getDocId(), function (res) {
new_file_name = res;
am.call(o, 'get');
});
};
o.get = function () {
that.addJob('get', priv.sub_storage_spec, new_file_name,
command.cloneOption(), o.success, o.error);
};
o.success = function (val) {
val._id = command.getDocId();
if (command.getOption('metadata_only')) {
that.success(val);
} else {
priv.decrypt(val.content, function (err, res) {
if (err) {
that.error(err);
} else {
val.content = res;
that.success(val);
}
});
}
};
o.error = function (error) {
that.error(error);
};
am.call(o, 'encryptFilePath');
}; // end get
/**
* Gets a document list.
* @method allDocs
*/
that.allDocs = function (command) {
var result_array = [],
am = priv.newAsyncModule(),
o = {};
o.allDocs = function () {
that.addJob('allDocs', priv.sub_storage_spec, null,
command.cloneOption(), o.onSuccess, o.error);
};
o.onSuccess = function (val) {
if (val.total_rows === 0) {
return am.call(o, 'success');
}
result_array = val.rows;
var i, decrypt = function (c) {
priv.decrypt(result_array[c].id, function (err, res) {
if (err) {
am.call(o, 'error', [err]);
} else {
result_array[c].id = res;
result_array[c].key = res;
am.call(o, 'success');
}
});
if (!command.getOption('metadata_only')) {
priv.decrypt(
result_array[c].value.content,
function (err, res) {
if (err) {
am.call(o, 'error', [err]);
} else {
result_array[c].value.content = res;
am.call(o, 'success');
}
}
);
}
};
if (command.getOption('metadata_only')) {
am.wait(o, 'success', val.total_rows - 1);
} else {
am.wait(o, 'success', val.total_rows * 2 - 1);
}
for (i = 0; i < result_array.length; i += 1) {
decrypt(i);
}
};
o.error = function (error) {
am.end();
that.error(error);
};
o.success = function () {
am.end();
that.success({
total_rows: result_array.length,
rows: result_array
});
};
am.call(o, 'allDocs');
}; // end allDocs
/**
* Removes a document.
* @method remove
*/
that.remove = function (command) {
var new_file_name, o = {};
o.encryptDocId = function () {
priv.encrypt(command.getDocId(), function (res) {
new_file_name = res;
o.removeDocument();
});
};
o.removeDocument = function () {
var cloned_doc = command.cloneDoc();
cloned_doc._id = new_file_name;
that.addJob('remove', priv.sub_storage_spec, cloned_doc,
command.cloneOption(), o.success, that.error);
};
o.success = function (val) {
val.id = command.getDocId();
that.success(val);
};
o.encryptDocId();
}; // end remove
return that;
});
/*
* Copyright 2013, Nexedi SA
* Released under the LGPL license.
* http://www.gnu.org/licenses/lgpl.html
*/
/*jslint indent: 2, maxlen: 80, nomen: true */
/*global define, jIO, jQuery, btoa */
// JIO Dav Storage Description :
// {
// type: "dav",
// url: {string}
// }
// {
// type: "dav",
// url: {string},
// auth_type: {string}, (optional)
// - "auto" (default) (not implemented)
// - "basic"
// - "digest" (not implemented)
// realm: {string}, (optional)
// - undefined (default) (not implemented)
// - "<string>" realm name (not implemented)
// username: {string},
// password: {string} (optional)
// }
// {
// type: "dav",
// url: {string},
// encoded_login: {string}
// }
// {
// type: "dav",
// url: {string},
// secured_login: {string} (not implemented)
// }
// NOTE: to get the authentication type ->
// curl --verbose -X OPTION http://domain/
// In the headers: "WWW-Authenticate: Basic realm="DAV-upload"
// URL Characters convertion:
// If I want to retrieve the file which id is -> http://100%.json
// http://domain/collection/http://100%.json cannot be applied
// - '/' is col separator,
// - '?' is url/parameter separator
// - '%' is special char
// - '.' document and attachment separator
// http://100%.json will become
// - http:%2F%2F100%25.json to avoid bad request ('/', '%' -> '%2F', '%25')
// - http:%2F%2F100%25_.json to avoid ids conflicts ('.' -> '_.')
// - http:%252F%252F100%2525_.json to avoid request bad interpretation
// ('%', '%25')
// The file will be saved as http:%2F%2F100%25_.json
// define([module_name], [dependencies], module);
(function (dependencies, module) {
"use strict";
if (typeof define === 'function' && define.amd) {
return define(dependencies, module);
}
module(jIO, jQuery);
}(['jio', 'jquery'], function (jIO, $) {
"use strict";
jIO.addStorageType("dav", function (spec, my) {
var priv = {}, that = my.basicStorage(spec, my), dav = {};
// ATTRIBUTES //
priv.url = null;
priv.username = null;
priv.password = null;
priv.encoded_login = null;
// CONSTRUCTOR //
/**
* Init the dav storage connector thanks to the description
* @method __init__
* @param {object} description The description object
*/
priv.__init__ = function (description) {
priv.url = description.url || "";
priv.url = priv.removeSlashIfLast(priv.url);
// if (description.secured_login) {
// not implemented
// } else
if (description.encoded_login) {
priv.encoded_login = description.encoded_login;
} else if (description.auth_type) {
if (description.auth_type === "basic") {
priv.encoded_login = "Basic " +
btoa((description.username || "") + ":" +
(description.password || ""));
}
} else {
priv.encoded_login = "";
}
};
// OVERRIDES //
that.specToStore = function () {
// TODO: secured password
// The encoded_login can be seen by anyone,
// we must find a way to secure it!
// secured_login = encrypt(encoded_login)
// encoded_login = decrypt(secured_login)
return {
"url": priv.url,
"encoded_login": priv.encoded_login
};
};
that.validateState = function () {
if (typeof priv.url !== "string" || priv.url === "") {
return "The webDav server URL is not provided";
}
if (priv.encoded_login === null) {
return "Impossible to create the authorization";
}
return "";
};
// TOOLS //
/**
* Generate a new uuid
* @method generateUuid
* @return {string} The new uuid
*/
priv.generateUuid = function () {
var S4 = function () {
/* 65536 */
var i, string = Math.floor(
Math.random() * 0x10000
).toString(16);
for (i = string.length; i < 4; i += 1) {
string = "0" + string;
}
return string;
};
return S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() +
S4() + S4();
};
// /**
// * Clones an object in deep
// * @method clone
// * @param {object} object The object to clone
// * @return {object} The cloned object
// */
// priv.clone = function (object) {
// var tmp = JSON.stringify(object);
// if (tmp === undefined) {
// return undefined;
// }
// return JSON.parse(tmp);
// };
/**
* Replace substrings to another strings
* @method recursiveReplace
* @param {string} string The string to do replacement
* @param {array} list_of_replacement An array of couple
* ["substring to select", "selected substring replaced by this string"].
* @return {string} The replaced string
*/
priv.recursiveReplace = function (string, list_of_replacement) {
var i, split_string = string.split(list_of_replacement[0][0]);
if (list_of_replacement[1]) {
for (i = 0; i < split_string.length; i += 1) {
split_string[i] = priv.recursiveReplace(
split_string[i],
list_of_replacement.slice(1)
);
}
}
return split_string.join(list_of_replacement[0][1]);
};
/**
* Changes spaces to %20, / to %2f, % to %25 and ? to %3f
* @method secureName
* @param {string} name The name to secure
* @return {string} The secured name
*/
priv.secureName = function (name) {
return priv.recursiveReplace(name, [
[" ", "%20"],
["/", "%2F"],
["%", "%25"],
["?", "%3F"]
]);
};
/**
* Restores the original name from a secured name
* @method restoreName
* @param {string} secured_name The secured name to restore
* @return {string} The original name
*/
priv.restoreName = function (secured_name) {
return priv.recursiveReplace(secured_name, [
["%20", " "],
["%2F", "/"],
["%25", "%"],
["%3F", "?"]
]);
};
/**
* Convert document id and attachment id to a file name
* @method idsToFileName
* @param {string} doc_id The document id
* @param {string} attachment_id The attachment id (optional)
* @return {string} The file name
*/
priv.idsToFileName = function (doc_id, attachment_id) {
doc_id = priv.secureName(doc_id).split(".").join("_.");
if (typeof attachment_id === "string") {
attachment_id = priv.secureName(attachment_id).split(".").join("_.");
return doc_id + "." + attachment_id;
}
return doc_id;
};
/**
* Convert a file name to a document id (and attachment id if there)
* @method fileNameToIds
* @param {string} file_name The file name to convert
* @return {array} ["document id", "attachment id"] or ["document id"]
*/
priv.fileNameToIds = function (file_name) {
var separator_index = -1, split = file_name.split(".");
split.slice(0, -1).forEach(function (file_name_part, index) {
if (file_name_part.slice(-1) !== "_") {
if (separator_index !== -1) {
separator_index = new TypeError("Corrupted file name");
separator_index.status = 24;
throw separator_index;
}
separator_index = index;
}
});
if (separator_index === -1) {
return [priv.restoreName(priv.restoreName(
file_name
).split("_.").join("."))];
}
return [
priv.restoreName(priv.restoreName(
split.slice(0, separator_index + 1).join(".")
).split("_.").join(".")),
priv.restoreName(priv.restoreName(
split.slice(separator_index + 1).join(".")
).split("_.").join("."))
];
};
/**
* 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;
};
/**
* Modify an ajax object to add default values
* @method makeAjaxObject
* @param {string} file_name The file name to add to the url
* @param {object} ajax_object The ajax object to override
* @return {object} A new ajax object with default values
*/
priv.makeAjaxObject = function (file_name, method, ajax_object) {
ajax_object.type = method || ajax_object.type || "GET";
ajax_object.url = priv.url + "/" + priv.secureName(file_name) +
"?_=" + Date.now();
ajax_object.async = ajax_object.async === false ? false : true;
ajax_object.crossdomain =
ajax_object.crossdomain === false ? false : true;
ajax_object.headers = ajax_object.headers || {};
ajax_object.headers.Authorization = ajax_object.headers.Authorization ||
priv.encoded_login;
return ajax_object;
};
/**
* Runs all ajax requests for davStorage
* @method ajax
* @param {string} doc_id The document id
* @param {string} attachment_id The attachment id, can be undefined
* @param {string} method The request method
* @param {object} ajax_object The request parameters (optional)
*/
priv.ajax = function (doc_id, attachment_id, method, ajax_object) {
var new_ajax_object = JSON.parse(JSON.stringify(ajax_object) || "{}");
return $.ajax(priv.makeAjaxObject(
priv.idsToFileName(doc_id || '', attachment_id),
method,
new_ajax_object
));//.always(then || function () {});
};
/**
* Creates error objects for this storage
* @method createError
* @param {string} url url to clean up
* @return {object} error The error object
*/
priv.createError = function (status, message, reason) {
var error = {
"status": status,
"message": message,
"reason": reason
};
switch (status) {
case 404:
error.statusText = "Not found";
break;
case 405:
error.statusText = "Method Not Allowed";
break;
case 409:
error.statusText = "Conflicts";
break;
case 24:
error.statusText = "Corrupted Document";
break;
}
error.error = error.statusText.toLowerCase().split(" ").join("_");
return error;
};
/**
* Converts ajax error object to a JIO error object
* @method ajaxErrorToJioError
* @param {object} ajax_error_object The ajax error object
* @param {string} message The error message
* @param {string} reason The error reason
* @return {object} The JIO error object
*/
priv.ajaxErrorToJioError = function (ajax_error_object, message, reason) {
var jio_error_object = {};
jio_error_object.status = ajax_error_object.status;
jio_error_object.statusText = ajax_error_object.statusText;
jio_error_object.error =
ajax_error_object.statusText.toLowerCase().split(" ").join("_");
jio_error_object.message = message;
jio_error_object.reason = reason;
return jio_error_object;
};
/**
* Function that create an object containing jQuery like callbacks
* @method makeJQLikeCallback
* @return {object} jQuery like callback methods
*/
priv.makeJQLikeCallback = function () {
var result = null, emptyFun = function () {}, jql = {
"respond": function () {
result = arguments;
},
"to_return": {
"always": function (func) {
if (result) {
func.apply(func, result);
jql.to_return.always = emptyFun;
} else {
jql.respond = func;
}
return jql.to_return;
},
"then": function (func) {
if (result) {
func(result[1]);
jql.to_return.then = emptyFun;
} else {
jql.respond = function (err, response) {
func(response);
};
}
return jql.to_return;
}
}
};
return jql;
};
// DAV REQUESTS //
/**
* Retrieve a document file
* @method dav.getDocument
* @param {string} doc_id The document id
*/
dav.getDocument = function (doc_id) {
var doc, jql = priv.makeJQLikeCallback(), error = null;
priv.ajax(doc_id, undefined, "GET").always(function (one, state, three) {
if (state !== "success") {
error = priv.ajaxErrorToJioError(
one,
"Cannot retrieve document",
"Unknown"
);
if (one.status === 404) {
error.reason = "Not Found";
}
return jql.respond(error, undefined);
}
try {
doc = JSON.parse(one);
} catch (e) {
return jql.respond(priv.createError(
24,
"Cannot parse document",
"Document is corrupted"
), undefined);
}
// document health is good
return jql.respond(undefined, doc);
});
return jql.to_return;
};
/**
* Retrieve an attachment file
* @method dav.getAttachment
* @param {string} doc_id The document id
* @param {string} attachment_id The attachment id
*/
dav.getAttachment = function (doc_id, attachment_id) {
var jql = priv.makeJQLikeCallback(), error = null;
priv.ajax(
doc_id,
attachment_id,
"GET"
).always(function (one, state, three) {
if (state !== "success") {
error = priv.ajaxErrorToJioError(
one,
"Cannot retrieve attachment",
"Unknown"
);
if (one.status === 404) {
error.reason = "Not Found";
}
return jql.respond(error, undefined);
}
return jql.respond(undefined, one);
});
return jql.to_return;
};
/**
* Uploads a document file
* @method dav.putDocument
* @param {object} doc The document object
*/
dav.putDocument = function (doc) {
var jql = priv.makeJQLikeCallback();
priv.ajax(doc._id, undefined, "PUT", {
"dataType": "text",
"data": JSON.stringify(doc)
}).always(function (one, state, three) {
if (state !== "success") {
return jql.respond(priv.ajaxErrorToJioError(
one,
"Cannot upload document",
"Unknown"
), undefined);
}
jql.respond(undefined, {"ok": true, "id": doc._id});
});
return jql.to_return;
};
/**
* Uploads an attachment file
* @method dav.putAttachment
* @param {string} doc_id The document id
* @param {string} attachment_id The attachment id
* @param {string} data The attachment data
*/
dav.putAttachment = function (doc_id, attachment_id, data) {
var jql = priv.makeJQLikeCallback();
priv.ajax(doc_id, attachment_id, "PUT", {
"dataType": "text",
"data": data
}).always(function (one, state, three) {
if (state !== "success") {
return jql.respond(priv.ajaxErrorToJioError(
one,
"Cannot upload attachment",
"Unknown"
), undefined);
}
return jql.respond(undefined, {
"ok": true,
"id": doc_id,
"attachment": attachment_id
});
});
return jql.to_return;
};
/**
* Deletes a document file
* @method dav.removeDocument
* @param {string} doc_id The document id
*/
dav.removeDocument = function (doc_id) {
var jql = priv.makeJQLikeCallback(), error = null;
priv.ajax(
doc_id,
undefined,
"DELETE"
).always(function (one, state, three) {
if (state !== "success") {
error = priv.ajaxErrorToJioError(
one,
"Cannot delete document",
"Unknown"
);
if (one.status === 404) {
error.reason = "Not Found";
}
return jql.respond(error, undefined);
}
jql.respond(undefined, {"ok": true, "id": doc_id});
});
return jql.to_return;
};
/**
* Deletes an attachment file
* @method dav.removeAttachment
* @param {string} doc_id The document id
* @param {string} attachment_id The attachment id
*/
dav.removeAttachment = function (doc_id, attachment_id) {
var jql = priv.makeJQLikeCallback(), error = null;
priv.ajax(
doc_id,
attachment_id,
"DELETE"
).always(function (one, state, three) {
if (state !== "success") {
error = priv.ajaxErrorToJioError(
one,
"Cannot delete attachment",
"Unknown"
);
if (one.status === 404) {
error.reason = "Not Found";
}
return jql.respond(error, undefined);
}
jql.respond(undefined, {"ok": true, "id": doc_id});
});
return jql.to_return;
};
/**
* Get a list of document file
* @method dav.allDocs
*/
dav.allDocs = function () {
var jql = priv.makeJQLikeCallback(), rows = [];
priv.ajax(undefined, undefined, "PROPFIND", {
"dataType": "xml",
"headers": {"Depth": 1}
}).always(function (one, state, three) {
var response, len;
if (state !== "success") {
return jql.respond(priv.ajaxErrorToJioError(
one,
"Cannot get the document list",
"Unknown"
), undefined);
}
response = $(one).find("D\\:response, response");
len = response.length;
if (len === 1) {
return jql.respond({"total_rows": 0, "rows": []});
}
response.each(function (i, data) {
var row;
if (i > 0) { // exclude parent folder
row = {
"id": "",
"key": "",
"value": {}
};
$(data).find("D\\:href, href").each(function () {
row.id = $(this).text().split('/').slice(-1)[0];
try {
row.id = priv.fileNameToIds(row.id);
} catch (e) {
if (e.name === "TypeError" && e.status === 24) {
return;
}
throw e;
}
if (row.id.length !== 1) {
row = undefined;
} else {
row.id = row.id[0];
row.key = row.id;
}
});
if (row !== undefined) {
rows.push(row);
}
}
});
jql.respond(undefined, {
"total_rows": rows.length,
"rows": rows
});
});
return jql.to_return;
};
// 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/
/**
* Creates a new document
* @method post
* @param {object} command The JIO command
*/
that.post = function (command) {
var doc_id = command.getDocId() || priv.generateUuid();
dav.getDocument(doc_id).always(function (err, response) {
if (err) {
if (err.status === 404) {
// the document does not already exist
// updating document
var doc = command.cloneDoc();
doc._id = doc_id;
return dav.putDocument(doc).always(function (err, response) {
if (err) {
return that.retry(err);
}
return that.success(response);
});
}
if (err.status === 24) {
return that.error(err);
}
// an error occured
return that.retry(err);
}
// the document already exists
return that.error(priv.createError(
405,
"Cannot create document",
"Document already exists"
));
});
};
/**
* Creates or updates a document
* @method put
* @param {object} command The JIO command
*/
that.put = function (command) {
dav.putDocument(command.cloneDoc()).always(function (err, response) {
if (err) {
// an error occured
return that.retry(err);
}
// document updated
return that.success(response);
});
};
/**
* Add an attachment to a document
* @method putAttachment
* @param {object} command The JIO command
*/
that.putAttachment = function (command) {
var doc = null, doc_id = command.getDocId(), attachment_id, tmp;
attachment_id = command.getAttachmentId();
dav.getDocument(doc_id).always(function (err, response) {
if (err) {
// document not found or error
tmp = that.retry;
if (err.status === 404 ||
err.status === 24) {
tmp = that.error;
}
return tmp(err);
}
doc = response;
doc._attachments = doc._attachments || {};
doc._attachments[attachment_id] = {
"length": command.getAttachmentLength(),
"digest": "md5-" + command.md5SumAttachmentData(),
"content_type": command.getAttachmentMimeType()
};
// put the attachment
dav.putAttachment(
doc_id,
attachment_id,
command.getAttachmentData()
).always(function (err, response) {
if (err) {
// an error occured
return that.retry(err);
}
// update the document
dav.putDocument(doc).always(function (err, response) {
if (err) {
return that.retry(err);
}
response.attachment = attachment_id;
return that.success(response);
});
});
});
};
/**
* Get a document
* @method get
* @param {object} command The JIO command
*/
that.get = function (command) {
dav.getDocument(command.getDocId()).always(function (err, response) {
if (err) {
if (err.status === 404 ||
err.status === 24) {
return that.error(err);
}
return that.retry(err);
}
return that.success(response);
});
};
/**
* Get an attachment
* @method getAttachment
* @param {object} command The JIO command
*/
that.getAttachment = function (command) {
dav.getAttachment(
command.getDocId(),
command.getAttachmentId()
).always(function (err, response) {
if (err) {
if (err.status === 404) {
return that.error(err);
}
return that.retry(err);
}
return that.success(response);
});
};
/**
* Remove a document
* @method remove
* @param {object} command The JIO command
*/
that.remove = function (command) {
var doc_id = command.getDocId(), count = 0, end;
end = function () {
count -= 1;
if (count === 0) {
that.success({"ok": true, "id": doc_id});
}
};
dav.getDocument(doc_id).always(function (err, response) {
var attachment_id = null;
if (err) {
if (err.status === 404) {
return that.error(err);
}
if (err.status !== 24) { // 24 -> corrupted document
return that.retry(err);
}
response = {};
}
count += 2;
dav.removeDocument(doc_id).always(function (err, response) {
if (err) {
if (err.status === 404) {
return that.error(err);
}
return that.retry(err);
}
return end();
});
for (attachment_id in response._attachments) {
if (response._attachments.hasOwnProperty(attachment_id)) {
count += 1;
dav.removeAttachment(
doc_id,
attachment_id
).always(end);
}
}
end();
});
};
/**
* Remove an attachment
* @method removeAttachment
* @param {object} command The JIO command
*/
that.removeAttachment = function (command) {
var doc_id = command.getDocId(), doc, attachment_id;
attachment_id = command.getAttachmentId();
dav.getDocument(doc_id).always(function (err, response) {
var still_has_attachments;
if (err) {
if (err.status === 404 ||
err.status === 24) {
return that.error(err);
}
return that.retry(err);
}
doc = response;
if (typeof (doc._attachments || {})[attachment_id] !== "object") {
return that.error(priv.createError(
404,
"Cannot remove attachment",
"Not Found"
));
}
delete doc._attachments[attachment_id];
// check if there is still attachments
for (still_has_attachments in doc._attachments) {
if (doc._attachments.hasOwnProperty(still_has_attachments)) {
break;
}
}
if (still_has_attachments === undefined) {
delete doc._attachments;
}
doc._id = doc_id;
dav.putDocument(doc).always(function (err, response) {
if (err) {
return that.retry(err);
}
dav.removeAttachment(
doc_id,
attachment_id
).always(function (err, response) {
that.success({
"ok": true,
"id": doc_id,
"attachment": attachment_id
});
});
});
});
};
/**
* Gets a document list from a distant dav storage
* Options:
* - {boolean} include_docs Also retrieve the actual document content.
* @method allDocs
* @param {object} command The JIO command
*/
that.allDocs = function (command) {
var count = 0, end, rows;
end = function () {
count -= 1;
if (count === 0) {
that.success(rows);
}
};
dav.allDocs().always(function (err, response) {
if (err) {
return that.retry(err);
}
if (command.getOption("include_docs") === true) {
count += 1;
rows = response;
rows.rows.forEach(function (row) {
count += 1;
dav.getDocument(
row.id
).always(function (err, response) {
if (err) {
if (err.status === 404 || err.status === 24) {
return that.error(err);
}
return that.retry(err);
}
row.doc = response;
end();
});
});
end();
} else {
that.success(response);
}
});
};
priv.__init__(spec);
return that;
});
}));
/*
* 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: true, $: true, complex_queries: true */
// JIO Erp5 Storage Description :
// {
// type: "erp5"
// url: {string}
// mode: {string} (optional)
// - "generic" (default)
// - "erp5_only"
//
// with
//
// auth_type: {string} (optional)
// - "none" (default)
// - "basic" (not implemented)
// - "digest" (not implemented)
// username: {string}
// password: {string} (optional)
// - no password (default)
//
// or
//
// encoded_login: {string} (not implemented)
//
// or
//
// secured_login: {string} (not implemented)
// }
jIO.addStorageType("erp5", function (spec, my) {
"use strict";
var priv = {}, that = my.basicStorage(spec, my), erp5 = {};
// ATTRIBUTES //
priv.url = null;
priv.mode = "generic";
priv.auth_type = "none";
priv.encoded_login = null;
// CONSTRUCTOR //
/**
* Init the erp5 storage connector thanks to the description
* @method __init__
* @param {object} description The description object
*/
priv.__init__ = function (description) {
priv.url = description.url || "";
priv.url = priv.removeSlashIfLast(priv.url);
if (description.mode === "erp5_only") {
priv.mode = "erp5_only";
}
if (description.encoded_login) {
priv.encoded_login = description.encoded_login;
} else {
if (description.username) {
priv.encoded_login =
"__ac_name=" + priv.convertToUrlParameter(description.username) +
"&" + (typeof description.password === "string" ?
"__ac_password=" +
priv.convertToUrlParameter(description.password) + "&" : "");
} else {
priv.encoded_login = "";
}
}
};
// OVERRIDES //
that.specToStore = function () {
// TODO: secured password
// The encoded_login can be seen by anyone, we must find a way to secure it!
// secured_login = encrypt(encoded_login)
// encoded_login = decrypt(secured_login)
return {
"url": priv.url,
"mode": priv.mode,
"encoded_login": priv.encoded_login
};
};
that.validateState = function () {
if (typeof priv.url !== "string" || priv.url === "") {
return "The erp5 server URL is not provided";
}
if (priv.encoded_login === null) {
return "Impossible to create the authorization";
}
return "";
};
// TOOLS //
/**
* Replace substrings to another strings
* @method recursiveReplace
* @param {string} string The string to do replacement
* @param {array} list_of_replacement An array of couple
* ["substring to select", "selected substring replaced by this string"].
* @return {string} The replaced string
*/
priv.recursiveReplace = function (string, list_of_replacement) {
var i, split_string = string.split(list_of_replacement[0][0]);
if (list_of_replacement[1]) {
for (i = 0; i < split_string.length; i += 1) {
split_string[i] = priv.recursiveReplace(
split_string[i],
list_of_replacement.slice(1)
);
}
}
return split_string.join(list_of_replacement[0][1]);
};
/**
* Changes & to %26
* @method convertToUrlParameter
* @param {string} parameter The parameter to convert
* @return {string} The converted parameter
*/
priv.convertToUrlParameter = function (parameter) {
return priv.recursiveReplace(parameter, [[" ", "%20"], ["&", "%26"]]);
};
/**
* 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;
};
/**
* Modify an ajax object to add default values
* @method makeAjaxObject
* @param {object} json The JSON object
* @param {object} option The option object
* @param {string} method The erp5 request method
* @param {object} ajax_object The ajax object to override
* @return {object} A new ajax object with default values
*/
priv.makeAjaxObject = function (json, option, method, ajax_object) {
ajax_object.type = "POST";
ajax_object.dataType = "json";
ajax_object.data = [
{"name": "doc", "value": JSON.stringify(json)},
{"name": "option", "value": JSON.stringify(option)},
{"name": "mode", "value": priv.mode}
];
ajax_object.url = priv.url + "/JIO_" + method +
"?" + priv.encoded_login + "_=" + Date.now();
ajax_object.async = ajax_object.async === false ? false : true;
ajax_object.crossdomain = ajax_object.crossdomain === false ? false : true;
ajax_object.headers = ajax_object.headers || {};
return ajax_object;
};
/**
* Runs all ajax requests for erp5Storage
* @method ajax
* @param {object} json The JSON object
* @param {object} option The option object
* @param {string} method The erp5 request method
* @param {object} ajax_object The request parameters (optional)
*/
priv.ajax = function (json, option, method, ajax_object) {
return $.ajax(priv.makeAjaxObject(json, option, method, ajax_object || {}));
//.always(then || function () {});
};
/**
* Creates error objects for this storage
* @method createError
* @param {string} url url to clean up
* @return {object} error The error object
*/
priv.createError = function (status, message, reason) {
var error = {
"status": status,
"message": message,
"reason": reason
};
switch (status) {
case 404:
error.statusText = "Not found";
break;
case 405:
error.statusText = "Method Not Allowed";
break;
case 409:
error.statusText = "Conflicts";
break;
case 24:
error.statusText = "Corrupted Document";
break;
}
error.error = error.statusText.toLowerCase().split(" ").join("_");
return error;
};
/**
* Converts ajax error object to a JIO error object
* @method ajaxErrorToJioError
* @param {object} ajax_error_object The ajax error object
* @param {string} message The error message
* @param {string} reason The error reason
* @return {object} The JIO error object
*/
priv.ajaxErrorToJioError = function (ajax_error_object, message, reason) {
var jio_error_object = {};
jio_error_object.status = ajax_error_object.status;
jio_error_object.statusText = ajax_error_object.statusText;
jio_error_object.error =
ajax_error_object.statusText.toLowerCase().split(" ").join("_");
jio_error_object.message = message;
jio_error_object.reason = reason;
return jio_error_object;
};
/**
* Function that create an object containing jQuery like callbacks
* @method makeJQLikeCallback
* @return {object} jQuery like callback methods
*/
priv.makeJQLikeCallback = function () {
var result = null, emptyFun = function () {}, jql = {
"respond": function () {
result = arguments;
},
"to_return": {
"always": function (func) {
if (result) {
func.apply(func, result);
jql.to_return.always = emptyFun;
} else {
jql.respond = func;
}
return jql.to_return;
}
}
};
return jql;
};
/**
* Use option object and converts a query to a compatible ERP5 Query.
*
* @param {Object} option The command options
*/
priv.convertToErp5Query = function (option) {
option.query = complex_queries.QueryFactory.create(option.query || "");
if (option.wildcard_character === undefined ||
(option.wildcard_character !== null &&
typeof option.wildcard_character !== 'string')) {
option.wildcard_character = '%';
} else {
option.wildcard_character = option.wildcard_character || '';
}
option.query.onParseSimpleQuery = function (object) {
if (option.wildcard_character.length === 1 &&
object.parsed.operator === '=') {
object.parsed.operator = 'like';
if (option.wildcard_character === '%') {
object.parsed.value =
object.parsed.value.replace(/_/g, '\\_');
} else if (option.wildcard_character === '_') {
object.parsed.value =
object.parsed.value.replace(/%/g, '\\%').replace(/_/g, '%');
} else {
object.parsed.value =
object.parsed.value.replace(
/([%_])/g,
'\\$1'
).replace(
new RegExp(complex_queries.stringEscapeRegexpCharacters(
option.wildcard_character
), 'g'),
'%'
);
}
}
};
option.query = option.query.parse();
};
// ERP5 REQUESTS //
/**
* Sends a request to ERP5
* @method erp5.genericRequest
* @param {object} doc The document object
* @param {object} option The option object
* @param {string} method The ERP5 request method
*/
erp5.genericRequest = function (json, option, method) {
var jql = priv.makeJQLikeCallback(), error = null;
priv.ajax(json, option, method).always(function (one, state, three) {
if (state === "parsererror") {
return jql.respond(priv.createError(
24,
"Cannot parse data",
"Corrupted data"
), undefined);
}
if (state !== "success") {
error = priv.ajaxErrorToJioError(
one,
"An error occured on " + method,
"Unknown"
);
if (one.status === 404) {
error.reason = "Not Found";
}
return jql.respond(error, undefined);
}
if (one.err !== null) {
return jql.respond(one.err, undefined);
}
if (one.response !== null) {
return jql.respond(undefined, one.response);
}
return jql.respond(priv.createError(
24,
"Cannot parse data",
"Corrupted data"
), undefined);
});
return jql.to_return;
};
// JIO COMMANDS //
/**
* The ERP5 storage generic command
* @method genericCommand
* @param {object} command The JIO command object
* @param {string} method The ERP5 request method
*/
priv.genericCommand = function (command, method) {
var option = command.cloneOption();
if (complex_queries !== undefined &&
method === 'allDocs' &&
option.query) {
priv.convertToErp5Query(option);
}
erp5.genericRequest(
command.cloneDoc(),
option,
method
).always(function (err, response) {
if (err) {
return that.error(err);
}
return that.success(response);
});
};
/**
* Creates a new document
* @method post
* @param {object} command The JIO command
*/
that.post = function (command) {
priv.genericCommand(command, "post");
};
/**
* Creates or updates a document
* @method put
* @param {object} command The JIO command
*/
that.put = function (command) {
priv.genericCommand(command, "put");
};
/**
* Add an attachment to a document
* @method putAttachment
* @param {object} command The JIO command
*/
that.putAttachment = function (command) {
priv.genericCommand(command, "putAttachment");
};
/**
* Get a document
* @method get
* @param {object} command The JIO command
*/
that.get = function (command) {
priv.genericCommand(command, "get");
};
/**
* Get an attachment
* @method getAttachment
* @param {object} command The JIO command
*/
that.getAttachment = function (command) {
priv.genericCommand(command, "getAttachment");
};
/**
* Remove a document
* @method remove
* @param {object} command The JIO command
*/
that.remove = function (command) {
priv.genericCommand(command, "remove");
};
/**
* Remove an attachment
* @method removeAttachment
* @param {object} command The JIO command
*/
that.removeAttachment = function (command) {
priv.genericCommand(command, "removeAttachment");
};
/**
* Gets a document list from a distant erp5 storage
* Options:
* - {boolean} include_docs Also retrieve the actual document content.
* @method allDocs
* @param {object} command The JIO command
*/
that.allDocs = function (command) {
priv.genericCommand(command, "allDocs");
};
/**
* Checks a document state
* @method check
* @param {object} command The JIO command
*/
that.check = function (command) {
priv.genericCommand(command, "check");
};
/**
* Restore a document state to a coherent state
* @method repair
* @param {object} command The JIO command
*/
that.repair = function (command) {
priv.genericCommand(command, "repair");
};
priv.__init__(spec);
return that;
});
/*
* 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 <http://www.gnu.org/licenses/>.
*/
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global define, jIO, setTimeout */
/**
* JIO GID Storage. Type = 'gid'.
* Identifies document with their global identifier representation
*
* Sub storages must support complex queries and include_docs options.
*
* Storage Description:
*
* {
* "type": "gid",
* "sub_storage": {<storage description>},
* "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;
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 =
/^([a-z]+\/[a-zA-Z0-9\+\-\.]+)(?:\s*;\s*charset\s*=\s*([a-zA-Z0-9\-]+))?$/;
/**
* 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 complex query.
*
* @param {Object,String} gid The gid
* @param {Object} constraints The constraints
* @return {Object} A complex serialized object
*/
function gidToComplexQuery(gid, contraints) {
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, my) {
var that = my.basicStorage(spec, my), priv = {};
priv.sub_storage = spec.sub_storage;
priv.constraints = spec.constraints || {
"default": {
"type": "DCMIType",
"title": "string"
}
};
// Overrides
that.specToStore = function () {
return {
"sub_storage": priv.sub_storage,
"constraints": priv.constraints
};
};
// JIO Commands
/**
* Generic command for post or put one.
*
* This command will check if the document already exist with an allDocs
* and a complex 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, method) {
setTimeout(function () {
var gid, complex_query, doc = command.cloneDoc();
gid = gidFormat(doc, priv.constraints);
if (gid === undefined || (doc._id && gid !== doc._id)) {
return that.error({
"status": 400,
"statusText": "Bad Request",
"error": "bad_request",
"message": "Cannot " + method + " document",
"reason": "metadata should respect constraints"
});
}
complex_query = gidToComplexQuery(gid);
that.addJob('allDocs', priv.sub_storage, {}, {
"query": complex_query,
"wildcard_character": null
}, function (response) {
var update_method = method;
if (response.total_rows !== 0) {
if (method === 'post') {
return that.error({
"status": 409,
"statusText": "Conflict",
"error": "conflict",
"message": "Cannot " + method + " document",
"reason": "Document already exist"
});
}
doc = command.cloneDoc();
doc._id = response.rows[0].id;
} else {
doc = command.cloneDoc();
delete doc._id;
update_method = 'post';
}
that.addJob(update_method, priv.sub_storage, doc, {
}, function (response) {
response.id = gid;
that.success(response);
}, function (err) {
err.message = "Cannot " + method + " document";
that.error(err);
});
}, function (err) {
err.message = "Cannot " + method + " document";
that.error(err);
});
});
};
/**
* Generic command for putAttachment, getAttachment or removeAttachment.
*
* This command will check if the document exist with an allDocs and a
* complex 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 {Command} command The JIO command
* @param {String} method The command method
*/
priv.putGetOrRemoveAttachment = function (command, method) {
setTimeout(function () {
var gid_object, complex_query, doc = command.cloneDoc();
gid_object = gidParse(doc._id, priv.constraints);
if (gid_object === undefined) {
return that.error({
"status": 400,
"statusText": "Bad Request",
"error": "bad_request",
"message": "Cannot " + method + " attachment",
"reason": "metadata should respect constraints"
});
}
complex_query = gidToComplexQuery(gid_object);
that.addJob('allDocs', priv.sub_storage, {}, {
"query": complex_query,
"wildcard_character": null
}, function (response) {
if (response.total_rows === 0) {
return that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Cannot " + method + " attachment",
"reason": "Document already exist"
});
}
gid_object = doc._id;
doc._id = response.rows[0].id;
that.addJob(method + "Attachment", priv.sub_storage, doc, {
}, function (response) {
if (method !== 'get') {
response.id = gid_object;
}
that.success(response);
}, function (err) {
err.message = "Cannot " + method + " attachment";
that.error(err);
});
}, function (err) {
err.message = "Cannot " + method + " attachment";
that.error(err);
});
});
};
/**
* See {{#crossLink "gidStorage/putOrPost:method"}}{{/#crossLink}}.
*
* @method post
* @param {Command} command The JIO command
*/
that.post = function (command) {
priv.putOrPost(command, 'post');
};
/**
* See {{#crossLink "gidStorage/putOrPost:method"}}{{/#crossLink}}.
*
* @method put
* @param {Command} command The JIO command
*/
that.put = function (command) {
priv.putOrPost(command, 'put');
};
/**
* Puts an attachment to a document thank to its gid, a sub allDocs and a
* complex query.
*
* @method putAttachment
* @param {Command} command The JIO command
*/
that.putAttachment = function (command) {
priv.putGetOrRemoveAttachment(command, 'put');
};
/**
* Gets a document thank to its gid, a sub allDocs and a complex query.
*
* @method get
* @param {Command} command The JIO command
*/
that.get = function (command) {
setTimeout(function () {
var gid_object, complex_query;
gid_object = gidParse(command.getDocId(), priv.constraints);
if (gid_object === undefined) {
return that.error({
"status": 400,
"statusText": "Bad Request",
"error": "bad_request",
"message": "Cannot get document",
"reason": "metadata should respect constraints"
});
}
complex_query = gidToComplexQuery(gid_object);
that.addJob('allDocs', priv.sub_storage, {}, {
"query": complex_query,
"wildcard_character": null,
"include_docs": true
}, function (response) {
if (response.total_rows === 0) {
return that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Cannot get document",
"reason": "missing"
});
}
response.rows[0].doc._id = command.getDocId();
return that.success(response.rows[0].doc);
}, function (err) {
err.message = "Cannot get document";
return that.error(err);
});
});
};
/**
* Gets an attachment from a document thank to its gid, a sub allDocs and a
* complex query.
*
* @method getAttachment
* @param {Command} command The JIO command
*/
that.getAttachment = function (command) {
priv.putGetOrRemoveAttachment(command, 'get');
};
/**
* Remove a document thank to its gid, sub allDocs and a complex query.
*
* @method remove
* @param {Command} command The JIO command.
*/
that.remove = function (command) {
setTimeout(function () {
var gid_object, complex_query, doc = command.cloneDoc();
gid_object = gidParse(doc._id, priv.constraints);
if (gid_object === undefined) {
return that.error({
"status": 400,
"statusText": "Bad Request",
"error": "bad_request",
"message": "Cannot remove document",
"reason": "metadata should respect constraints"
});
}
complex_query = gidToComplexQuery(gid_object);
that.addJob('allDocs', priv.sub_storage, {}, {
"query": complex_query,
"wildcard_character": null
}, function (response) {
if (response.total_rows === 0) {
return that.error({
"status": 404,
"statusText": "Not found",
"error": "not_found",
"message": "Cannot remove document",
"reason": "missing"
});
}
gid_object = doc._id;
doc = {"_id": response.rows[0].id};
that.addJob('remove', priv.sub_storage, doc, {
}, function (response) {
response.id = gid_object;
that.success(response);
}, function (err) {
err.message = "Cannot remove document";
that.error(err);
});
}, function (err) {
err.message = "Cannot remove document";
that.error(err);
});
});
};
/**
* Removes an attachment to a document thank to its gid, a sub allDocs and a
* complex query.
*
* @method removeAttachment
* @param {Command} command The JIO command
*/
that.removeAttachment = function (command) {
priv.putGetOrRemoveAttachment(command, 'remove');
};
/**
* Retrieve a list of document which respect gid constraints.
*
* @method allDocs
* @param {Command} command The JIO command
*/
that.allDocs = function (command) {
setTimeout(function () {
var options = command.cloneOption(), include_docs;
include_docs = options.include_docs;
options.include_docs = true;
that.addJob('allDocs', priv.sub_storage, {
}, options, function (response) {
var result = [], doc_gids = {}, i, row, gid;
while ((row = response.rows.shift()) !== undefined) {
if ((gid = gidFormat(row.doc, priv.constraints)) !== 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;
that.success({"total_rows": result.length, "rows": result});
}, function (err) {
err.message = "Cannot get all documents";
return that.error(err);
});
});
};
return that;
}
jIO.addStorageType('gid', gidStorage);
}));
/*
* 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 <http://www.gnu.org/licenses/>.
*/
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true, regexp: true */
/*global jIO, define, complex_queries */
/**
* 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": <sub storage where to store index>
* (default equal to parent sub_storage field)
* }, {
* "id": "index_year.json",
* "index": "year"
* ...
* }],
* "sub_storage": <sub storage description>
* }
*
* 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);
}
module(jIO, complex_queries);
}(['jio', 'complex_queries'], function (jIO, complex_queries) {
"use strict";
var error_dict = {
"Corrupted Index": {
"status": 24,
"statusText": "Corrupt",
"error": "corrupt",
"reason": "corrupted index database"
},
"Corrupted Metadata": {
"status": 24,
"statusText": "Corrupt",
"error": "corrupt",
"reason": "corrupted document"
},
"Not Found": {
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"reason": "missing document"
},
"Conflict": {
"status": 409,
"statusText": "Conflicts",
"error": "conflicts",
"reason": "already exist"
},
"Different Index": {
"status": 40,
"statusText": "Check failed",
"error": "check_failed",
"reason": "incomplete database"
}
};
/**
* Generate a JIO Error Object
*
* @method generateErrorObject
* @param {String} name The error name
* @param {String} message The error message
* @param {String} [reason] The error reason
* @return {Object} A jIO error object
*/
function generateErrorObject(name, message, reason) {
if (!error_dict[name]) {
return {
"status": 0,
"statusText": "Unknown",
"error": "unknown",
"message": message,
"reason": reason || "unknown"
};
}
return {
"status": error_dict[name].status,
"statusText": error_dict[name].statusText,
"error": error_dict[name].error,
"message": message,
"reason": reason || error_dict[name].reason
};
}
/**
* Get the real type of an object
* @method type
* @param {Any} value The value to check
* @return {String} The value type
*/
function type(value) {
// returns "String", "Object", "Array", "RegExp", ...
return (/^\[object ([a-zA-Z]+)\]$/).exec(
Object.prototype.toString.call(value)
)[1];
}
/**
* Generate a new uuid
* @method generateUuid
* @return {string} The new uuid
*/
function generateUuid() {
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();
}
/**
* Tool to get the date in W3C date format "2011-12-13T14:15:16+01:00"
*
* @param {Any} date The new Date() parameter
* @return {String} The date in W3C date format
*/
function w3cDate(date) {
var d = new Date(date), offset = -d.getTimezoneOffset();
return (
d.getFullYear() + "-" +
(d.getMonth() + 1) + "-" +
d.getDate() + "T" +
d.getHours() + ":" +
d.getMinutes() + ":" +
d.getSeconds() +
(offset < 0 ? "-" : "+") +
(offset / 60) + ":" +
(offset % 60)
).replace(/[0-9]+/g, function (found) {
if (found.length < 2) {
return '0' + found;
}
return found;
});
}
/**
* 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 || [];
/**
* 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;
}
return true;
}
if (typeof that._location[meta._id] === "number") {
that.remove(meta);
}
return false;
};
/**
* Removes a metadata object from the database if exist
*
* @method remove
* @param {Object} meta The metadata to remove
*/
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");
}
that._database[that._location[meta._id]] = null;
that._free.push(that._location[meta._id]);
delete that._location[meta._id];
};
/**
* 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 (type(database_meta) !== "Object" ||
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" ||
(db_doc = that._database(that._location[doc._id])._id) !== 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 (type(that._database) !== "Array") {
that._database = [];
}
while (i < that._database.length) {
meta = that._database[i];
if (type(meta) === "Object" &&
typeof meta._id === "string" && meta._id !== "" &&
!that._location[meta._id]) {
that._location[meta._id] = i;
i += 1;
} else {
that._database.splice(i, 1);
}
}
};
/**
* Returns the serialized version of this object (not cloned)
*
* @method serialized
* @return {Object} The serialized version
*/
that.serialized = 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;
});
}
/**
* The JIO index storage constructor
*/
function indexStorage(spec, my) {
var that, priv = {};
that = my.basicStorage(spec, my);
priv.indices = spec.indices;
priv.sub_storage = spec.sub_storage;
// Overrides
that.specToStore = function () {
return {
"indices": priv.indices,
"sub_storage": priv.sub_storage
};
};
/**
* Return the similarity percentage (1 >= p >= 0) between two index lists.
*
* @method similarityPercentage
* @param {Array} list_a An index list
* @param {Array} list_b Another index list
* @return {Number} The similarity percentage
*/
priv.similarityPercentage = function (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;
}
}
}
return count / (list_a.length > list_b.length ?
list_a.length : list_b.length);
};
/**
* 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
*/
priv.selectIndex = function (select_list) {
var i, tmp, selector = {"index": 0, "similarity": 0};
for (i = 0; i < priv.indices.length; i += 1) {
tmp = priv.similarityPercentage(select_list,
priv.indices[i].index);
if (tmp > selector.similarity) {
selector.index = i;
selector.similarity = tmp;
}
}
return selector.index;
};
/**
* Get a database
*
* @method getIndexDatabase
* @param {Object} option The command option
* @param {Number} number The location in priv.indices
* @param {Function} callback The callback
*/
priv.getIndexDatabase = function (option, number, callback) {
that.addJob(
"getAttachment",
priv.indices[number].sub_storage || priv.sub_storage,
{
"_id": priv.indices[number].id,
"_attachment": priv.indices[number].attachment || "body"
},
option,
function (response) {
try {
response = JSON.parse(response);
response._id = priv.indices[number].id;
response._attachment = priv.indices[number].attachment || "body";
callback(new JSONIndex(response));
} catch (e) {
return that.error(generateErrorObject(
e.message,
"Repair is necessary",
"corrupt"
));
}
},
function (err) {
if (err.status === 404) {
callback(new JSONIndex({
"_id": priv.indices[number].id,
"_attachment": priv.indices[number].attachment || "body",
"indexing": priv.indices[number].index
}));
return;
}
err.message = "Unable to get index database.";
that.error(err);
}
);
};
/**
* Gets a list containing all the databases set in the storage description.
*
* @method getIndexDatabaseList
* @param {Object} option The command option
* @param {Function} callback The result callback(database_list)
*/
priv.getIndexDatabaseList = function (option, callback) {
var i, count = 0, callbacks = {}, response_list = [];
callbacks.error = function (index) {
return function (err) {
if (err.status === 404) {
response_list[index] = new JSONIndex({
"_id": priv.indices[index].id,
"_attachment": priv.indices[index].attachment || "body",
"indexing": priv.indices[index].index
});
count += 1;
if (count === priv.indices.length) {
callback(response_list);
}
return;
}
err.message = "Unable to get index database.";
that.error(err);
};
};
callbacks.success = function (index) {
return function (response) {
try {
response = JSON.parse(response);
response._id = priv.indices[index].id;
response._attachment = priv.indices[index].attachment || "body";
response_list[index] = new JSONIndex(response);
} catch (e) {
return that.error(generateErrorObject(
e.message,
"Repair is necessary",
"corrupt"
));
}
count += 1;
if (count === priv.indices.length) {
callback(response_list);
}
};
};
for (i = 0; i < priv.indices.length; i += 1) {
that.addJob(
"getAttachment",
priv.indices[i].sub_storage || priv.sub_storage,
{
"_id": priv.indices[i].id,
"_attachment": priv.indices[i].attachment || "body"
},
option,
callbacks.success(i),
callbacks.error(i)
);
}
};
/**
* Saves all the databases to the remote(s).
*
* @method storeIndexDatabaseList
* @param {Array} database_list The database list
* @param {Object} option The command option
* @param {Function} callback The result callback(err, response)
*/
priv.storeIndexDatabaseList = function (database_list, option, callback) {
var i, count = 0, count_max = 0;
function onAttachmentResponse(response) {
count += 1;
if (count === count_max) {
callback({"ok": true});
}
}
function onAttachmentError(err) {
err.message = "Unable to store index database.";
that.error(err);
}
function putAttachment(i) {
that.addJob(
"putAttachment",
priv.indices[i].sub_storage || priv.sub_storage,
{
"_id": database_list[i]._id,
"_attachment": database_list[i]._attachment,
"_data": JSON.stringify(database_list[i].serialized()),
"_mimetype": "application/json"
},
option,
onAttachmentResponse,
onAttachmentError
);
}
function post(i) {
var doc = priv.indices[i].metadata || {};
doc._id = database_list[i]._id;
that.addJob(
"post", // with id
priv.indices[i].sub_storage || priv.sub_storage,
doc,
option,
function (response) {
putAttachment(i);
},
function (err) {
if (err.status === 409) {
return putAttachment(i);
}
err.message = "Unable to store index database.";
that.error(err);
}
);
}
for (i = 0; i < priv.indices.length; i += 1) {
if (database_list[i] !== undefined) {
count_max += 1;
post(i);
}
}
};
/**
* A generic request method which delegates the request to the sub storage.
* On response, it will index the document from the request and update all
* the databases.
*
* @method genericRequest
* @param {Command} command The JIO command
* @param {Function} method The request method
*/
priv.genericRequest = function (command, method) {
var doc = command.cloneDoc(), option = command.cloneOption();
that.addJob(
method,
priv.sub_storage,
doc,
option,
function (response) {
switch (method) {
case "post":
case "put":
case "remove":
doc._id = response.id;
priv.getIndexDatabaseList(option, function (database_list) {
var i;
switch (method) {
case "post":
case "put":
for (i = 0; i < database_list.length; i += 1) {
database_list[i].put(doc);
}
break;
case "remove":
for (i = 0; i < database_list.length; i += 1) {
database_list[i].remove(doc);
}
break;
default:
break;
}
priv.storeIndexDatabaseList(database_list, option, function () {
that.success({"ok": true, "id": doc._id});
});
});
break;
default:
that.success(response);
break;
}
},
function (err) {
return that.error(err);
}
);
};
/**
* Post the document metadata and update the index
* @method post
* @param {object} command The JIO command
*/
that.post = function (command) {
priv.genericRequest(command, 'post');
};
/**
* Update the document metadata and update the index
* @method put
* @param {object} command The JIO command
*/
that.put = function (command) {
priv.genericRequest(command, 'put');
};
/**
* Add an attachment to a document (no index modification)
* @method putAttachment
* @param {object} command The JIO command
*/
that.putAttachment = function (command) {
priv.genericRequest(command, 'putAttachment');
};
/**
* Get the document metadata
* @method get
* @param {object} command The JIO command
*/
that.get = function (command) {
priv.genericRequest(command, 'get');
};
/**
* Get the attachment.
* @method getAttachment
* @param {object} command The JIO command
*/
that.getAttachment = function (command) {
priv.genericRequest(command, 'getAttachment');
};
/**
* Remove document - removing documents updates index!.
* @method remove
* @param {object} command The JIO command
*/
that.remove = function (command) {
priv.genericRequest(command, 'remove');
};
/**
* Remove attachment
* @method removeAttachment
* @param {object} command The JIO command
*/
that.removeAttachment = function (command) {
priv.genericRequest(command, 'removeAttachment');
};
/**
* Gets a document list from the substorage
* Options:
* - {boolean} include_docs Also retrieve the actual document content.
* @method allDocs
* @param {object} command The JIO command
*/
that.allDocs = function (command) {
var option = command.cloneOption(),
index = priv.selectIndex(option.select_list || []);
// Include docs option is ignored, if you want to get all the document,
// don't use index storage!
option.select_list = option.select_list || [];
option.select_list.push("_id");
priv.getIndexDatabase(option, index, function (db) {
var i, id;
db = db._database;
complex_queries.QueryFactory.create(option.query || '').
exec(db, option);
for (i = 0; i < db.length; i += 1) {
id = db[i]._id;
delete db[i]._id;
db[i] = {
"id": id,
"key": id,
"value": db[i],
};
}
that.success({"total_rows": db.length, "rows": db});
});
};
that.check = function (command) {
that.repair(command, true);
};
priv.repairIndexDatabase = function (command, index, just_check) {
var i, option = command.cloneOption();
that.addJob(
'allDocs',
priv.sub_storage,
{},
{'include_docs': true},
function (response) {
var db_list = [], db = new JSONIndex({
"_id": command.getDocId(),
"_attachment": priv.indices[index].attachment || "body",
"indexing": priv.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) {
priv.getIndexDatabase(option, index, function (current_db) {
if (db.equals(current_db)) {
return that.success({"ok": true, "id": command.getDocId()});
}
return that.error(generateErrorObject(
"Different Index",
"Check failed",
"corrupt index database"
));
});
} else {
priv.storeIndexDatabaseList(db_list, {}, function () {
that.success({"ok": true, "id": command.getDocId()});
});
}
},
function (err) {
err.message = "Unable to repair the index database";
that.error(err);
}
);
};
priv.repairDocument = function (command, just_check) {
var i, option = command.cloneOption();
that.addJob(
"get",
priv.sub_storage,
command.cloneDoc(),
{},
function (response) {
response._id = command.getDocId();
priv.getIndexDatabaseList(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 that.error(generateErrorObject(
e.message,
"Check failed",
"corrupt index database"
));
}
}
that.success({"_id": command.getDocId(), "ok": true});
} else {
for (i = 0; i < database_list.length; i += 1) {
database_list[i].put(response);
}
priv.storeIndexDatabaseList(database_list, option, function () {
that.success({"ok": true, "id": command.getDocId()});
});
}
});
},
function (err) {
err.message = "Unable to repair document";
return that.error(err);
}
);
};
that.repair = function (command, just_check) {
var database_index = -1, i;
for (i = 0; i < priv.indices.length; i += 1) {
if (priv.indices[i].id === command.getDocId()) {
database_index = i;
break;
}
}
that.addJob(
"repair",
priv.sub_storage,
command.cloneDoc(),
command.cloneOption(),
function (response) {
if (database_index !== -1) {
priv.repairIndexDatabase(command, database_index, just_check);
} else {
priv.repairDocument(command, just_check);
}
},
function (err) {
err.message = "Could not repair sub storage";
that.error(err);
}
);
};
return that;
}
jIO.addStorageType("indexed", indexStorage);
}));
/*
* Copyright 2013, Nexedi SA
* Released under the LGPL license.
* http://www.gnu.org/licenses/lgpl.html
*/
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global define, jIO, setTimeout, complex_queries */
// 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) {
var storage = {};
/**
* Returns 4 hexadecimal random characters.
*
* @return {String} The characters
*/
function S4() {
return ('0000' + Math.floor(
Math.random() * 0x10000 /* 65536 */
).toString(16)).slice(-4);
}
/**
* An Universal Unique ID generator
*
* @return {String} The new UUID.
*/
function generateUuid() {
return S4() + S4() + "-" +
S4() + "-" +
S4() + "-" +
S4() + "-" +
S4() + S4() + S4();
}
/**
* 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;
}
/**
* JIO Ram Storage. Type = 'ram'.
* Memory "database" storage.
*
* Storage Description:
*
* {
* "type": "ram",
* "namespace": <string>, // default 'default'
* }
*
* Document are stored in path
* 'namespace/document_id' like this:
*
* {
* "_id": "document_id",
* "_attachments": {
* "attachment_name": {
* "length": data_length,
* "digest": "md5-XXX",
* "content_type": "mime/type"
* },
* "attachment_name2": {..}, ...
* },
* "metadata_name": "metadata_value"
* "metadata_name2": ...
* ...
* }
*
* Only "_id" and "_attachments" are specific metadata keys, other one can be
* added without loss.
*
* @class RamStorage
*/
function ramStorage(spec, my) {
var that, priv = {}, ramstorage;
that = my.basicStorage(spec, my);
/*
* Wrapper for the localStorage used to simplify instion of any kind of
* values
*/
ramstorage = {
getItem: function (item) {
var value = storage[item];
return value === undefined ? null : JSON.parse(value);
},
setItem: function (item, value) {
storage[item] = JSON.stringify(value);
},
removeItem: function (item) {
delete storage[item];
}
};
// attributes
if (typeof spec.namespace !== 'string') {
priv.namespace = 'default';
} else {
priv.namespace = spec.namespace;
}
// ===================== overrides ======================
that.specToStore = function () {
return {
"namespace": priv.namespace
};
};
that.validateState = function () {
return '';
};
// ==================== commands ====================
/**
* Create a document in local storage.
* @method post
* @param {object} command The JIO command
*/
that.post = function (command) {
setTimeout(function () {
var doc, doc_id = command.getDocId();
if (!doc_id) {
doc_id = generateUuid();
}
doc = ramstorage.getItem(priv.namespace + "/" + doc_id);
if (doc === null) {
// the document does not exist
doc = command.cloneDoc();
doc._id = doc_id;
delete doc._attachments;
ramstorage.setItem(priv.namespace + "/" + doc_id, doc);
that.success({
"ok": true,
"id": doc_id
});
} else {
// the document already exists
that.error({
"status": 409,
"statusText": "Conflicts",
"error": "conflicts",
"message": "Cannot create a new document",
"reason": "Document already exists"
});
}
});
};
/**
* Create or update a document in local storage.
* @method put
* @param {object} command The JIO command
*/
that.put = function (command) {
setTimeout(function () {
var doc, tmp;
doc = ramstorage.getItem(priv.namespace + "/" + command.getDocId());
if (doc === null) {
// the document does not exist
doc = command.cloneDoc();
delete doc._attachments;
} else {
// the document already exists
tmp = command.cloneDoc();
tmp._attachments = doc._attachments;
doc = tmp;
}
// write
ramstorage.setItem(priv.namespace + "/" + command.getDocId(), doc);
that.success({
"ok": true,
"id": command.getDocId()
});
});
};
/**
* Add an attachment to a document
* @method putAttachment
* @param {object} command The JIO command
*/
that.putAttachment = function (command) {
setTimeout(function () {
var doc;
doc = ramstorage.getItem(priv.namespace + "/" + command.getDocId());
if (doc === null) {
// the document does not exist
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Impossible to add attachment",
"reason": "Document not found"
});
return;
}
// the document already exists
doc._attachments = doc._attachments || {};
doc._attachments[command.getAttachmentId()] = {
"content_type": command.getAttachmentMimeType(),
"digest": "md5-" + command.md5SumAttachmentData(),
"length": command.getAttachmentLength()
};
// upload data
ramstorage.setItem(priv.namespace + "/" + command.getDocId() + "/" +
command.getAttachmentId(),
command.getAttachmentData());
// write document
ramstorage.setItem(priv.namespace + "/" + command.getDocId(), doc);
that.success({
"ok": true,
"id": command.getDocId(),
"attachment": command.getAttachmentId()
});
});
};
/**
* Get a document
* @method get
* @param {object} command The JIO command
*/
that.get = function (command) {
setTimeout(function () {
var doc = ramstorage.getItem(priv.namespace + "/" + command.getDocId());
if (doc !== null) {
that.success(doc);
} else {
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Cannot find the document",
"reason": "Document does not exist"
});
}
});
};
/**
* Get a attachment
* @method getAttachment
* @param {object} command The JIO command
*/
that.getAttachment = function (command) {
setTimeout(function () {
var doc = ramstorage.getItem(priv.namespace + "/" + command.getDocId() +
"/" + command.getAttachmentId());
if (doc !== null) {
that.success(doc);
} else {
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Cannot find the attachment",
"reason": "Attachment does not exist"
});
}
});
};
/**
* Remove a document
* @method remove
* @param {object} command The JIO command
*/
that.remove = function (command) {
setTimeout(function () {
var doc, i, attachment_list;
doc = ramstorage.getItem(priv.namespace + "/" + command.getDocId());
attachment_list = [];
if (doc !== null && typeof doc === "object") {
if (typeof doc._attachments === "object") {
// prepare list of attachments
for (i in doc._attachments) {
if (doc._attachments.hasOwnProperty(i)) {
attachment_list.push(i);
}
}
}
} else {
return that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Document not found",
"reason": "missing"
});
}
ramstorage.removeItem(priv.namespace + "/" + command.getDocId());
// delete all attachments
for (i = 0; i < attachment_list.length; i += 1) {
ramstorage.removeItem(priv.namespace + "/" + command.getDocId() +
"/" + attachment_list[i]);
}
that.success({
"ok": true,
"id": command.getDocId()
});
});
};
/**
* Remove an attachment
* @method removeAttachment
* @param {object} command The JIO command
*/
that.removeAttachment = function (command) {
setTimeout(function () {
var doc, error, i, attachment_list;
error = function (word) {
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": word + " not found",
"reason": "missing"
});
};
doc = ramstorage.getItem(priv.namespace + "/" + command.getDocId());
// remove attachment from document
if (doc !== null && typeof doc === "object" &&
typeof doc._attachments === "object") {
if (typeof doc._attachments[command.getAttachmentId()] ===
"object") {
delete doc._attachments[command.getAttachmentId()];
if (priv.objectIsEmpty(doc._attachments)) {
delete doc._attachments;
}
ramstorage.setItem(priv.namespace + "/" + command.getDocId(),
doc);
ramstorage.removeItem(priv.namespace + "/" + command.getDocId() +
"/" + command.getAttachmentId());
that.success({
"ok": true,
"id": command.getDocId(),
"attachment": command.getAttachmentId()
});
} else {
error("Attachment");
}
} else {
error("Document");
}
});
};
/**
* Get all filenames belonging to a user from the document index
* @method allDocs
* @param {object} command The JIO command
*/
that.allDocs = function (command) {
var i, row, path_re, rows = [], document_list, option, document_object;
document_list = [];
path_re = new RegExp(
"^" + complex_queries.stringEscapeRegexpCharacters(priv.namespace) +
"/[^/]+$"
);
option = command.cloneOption();
if (typeof complex_queries !== "object" ||
(option.query === undefined && option.sort_on === undefined &&
option.select_list === undefined &&
option.include_docs === undefined)) {
rows = [];
for (i in storage) {
if (storage.hasOwnProperty(i)) {
// filter non-documents
if (path_re.test(i)) {
row = {"value": {}};
row.id = i.split('/').slice(-1)[0];
row.key = row.id;
if (command.getOption('include_docs')) {
row.doc = ramstorage.getItem(i);
}
rows.push(row);
}
}
}
that.success({"rows": rows, "total_rows": rows.length});
} else {
// create complex query object from returned results
for (i in storage) {
if (storage.hasOwnProperty(i)) {
if (path_re.test(i)) {
document_list.push(ramstorage.getItem(i));
}
}
}
option.select_list = option.select_list || [];
option.select_list.push("_id");
if (option.include_docs === true) {
document_object = {};
document_list.forEach(function (meta) {
document_object[meta._id] = meta;
});
}
complex_queries.QueryFactory.create(option.query || "").
exec(document_list, option);
document_list = document_list.map(function (value) {
var o = {
"id": value._id,
"key": value._id
};
if (option.include_docs === true) {
o.doc = document_object[value._id];
delete document_object[value._id];
}
delete value._id;
o.value = value;
return o;
});
that.success({"total_rows": document_list.length,
"rows": document_list});
}
};
return that;
}
jIO.addStorageType('ram', ramStorage);
}));
/*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": [
* <sub storage description>,
* ...
* ]
* }
*/
// 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.addStorageType('replicaterevision', function (spec, my) {
var that, priv = {};
spec = spec || {};
that = my.basicStorage(spec, my);
priv.storage_list_key = "storage_list";
priv.storage_list = spec[priv.storage_list_key];
priv.emptyFunction = function () {};
that.specToStore = function () {
var o = {};
o[priv.storage_list_key] = priv.storage_list;
return o;
};
/**
* Generate a new uuid
* @method generateUuid
* @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
* @method dictKeys2Array
* @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
* @method checkRevisionFormat
* @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)
* @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);
};
/**
* Like addJob but also return the method and the index of the storage
* @method send
* @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 (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);
};
that.addJob(
method,
priv.storage_list[index],
doc,
option,
wrapped_callback_success,
wrapped_callback_error
);
};
/**
* Use "send" method to all sub storages.
* Calling "callback" for each storage response.
* @method sendToAll
* @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 (method, doc, option, callback) {
var i;
for (i = 0; i < priv.storage_list.length; i += 1) {
priv.send(method, i, doc, option, callback);
}
};
/**
* Use "send" method to all sub storages.
* Calling "callback" only with the first response
* @method sendToAllFastestResponseOnly
* @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 (
method,
doc,
option,
callback
) {
var i, callbackWrapper, error_count, last_error;
error_count = 0;
callbackWrapper = function (method, index, err, response) {
if (err) {
error_count += 1;
last_error = err;
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(method, i, doc, option, callbackWrapper);
}
};
/**
* Use "sendToAll" method, calling "callback" at the last response with
* the response list
* @method sendToAllGetResponseList
* @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 (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) {
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(method, doc, option, wrapper);
};
/**
* Checks if the sub storage are identical
* @method check
* @param {object} command The JIO command
*/
that.check = function (command) {
function callback(err, response) {
if (err) {
return that.error(err);
}
that.success(response);
}
priv.check(
command.cloneDoc(),
command.cloneOption(),
callback
);
};
/**
* Repair the sub storages to make them identical
* @method repair
* @param {object} command The JIO command
*/
that.repair = function (command) {
function callback(err, response) {
if (err) {
return that.error(err);
}
that.success(response);
}
priv.repair(
command.cloneDoc(),
command.cloneOption(),
true,
callback
);
};
priv.check = function (doc, option, success, error) {
priv.repair(doc, option, false, success, error);
};
priv.repair = function (doc, option, repair, callback) {
var functions = {};
callback = callback || priv.emptyFunction;
option = option || {};
functions.begin = function () {
// };
// functions.repairAllSubStorages = function () {
var i;
for (i = 0; i < priv.storage_list.length; i += 1) {
priv.send(
repair ? "repair" : "check",
i,
doc,
option,
functions.repairAllSubStoragesCallback
);
}
};
functions.repair_sub_storages_count = 0;
functions.repairAllSubStoragesCallback = function (method,
index, err, response) {
if (err) {
return that.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, _mimetype: ..}
// attachmentB : {_id: attachmentB, _revs_info, _mimetype: ..}
}
},
"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, doc = priv.clone(param.doc), option = priv.clone(param.option);
option.conflicts = true;
option.revs = true;
option.revs_info = true;
for (i = 0; i < priv.storage_list.length; i += 1) {
// if the document is not loaded
priv.send("get", i, doc, option, functions.dealResults(param));
}
functions.finished_count += 1;
};
functions.dealResults = function (param) {
return function (method, index, err, response) {
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": 40,
"statusText": "Check Failed",
"error": "check_failed",
"message": "An error occured on the sub storage",
"reason": err.reason
}, undefined);
return;
}
}
// 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);
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": 41,
"statusText": "Check Not Ok",
"error": "check_not_ok",
"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(
"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,
index_list
) {
return function (method, index, err, response) {
if (err) {
callback({
"status": 40,
"statusText": "Check Failed",
"error": "check_failed",
"message": "Unable to retreive attachments",
"reason": err.reason
}, undefined);
return;
}
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,
"_mimetype": 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(
"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) {
var i, attachment;
if (err) {
return callback({
"status": 40,
"statusText": "Check Failed",
"error": "check_failed",
"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,
"_mimetype": attachment_to_put[i]._mimetype,
"_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(
"putAttachment",
index,
attachment,
option,
functions.putAttachmentCallback(param)
);
}
if (attachment_to_put.length === 0) {
functions.finished();
}
};
};
functions.putAttachmentCallback = function (param) {
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) {
var doc = command.cloneDoc();
doc._id = doc._id || priv.generateUuid();
priv.sendToAllFastestResponseOnly(
method,
doc,
command.cloneOption(),
function (method, err, response) {
if (err) {
return that.error(err);
}
that.success(response);
}
);
};
/**
* Post the document metadata to all sub storages
* @method post
* @param {object} command The JIO command
*/
that.post = function (command) {
that.genericRequest(command, "put");
};
/**
* Put the document metadata to all sub storages
* @method put
* @param {object} command The JIO command
*/
that.put = function (command) {
that.genericRequest(command, "post");
};
/**
* Put an attachment to a document to all sub storages
* @method putAttachment
* @param {object} command The JIO command
*/
that.putAttachment = function (command) {
that.genericRequest(command, "putAttachment");
};
/**
* Get the document from all sub storages, get the fastest.
* @method get
* @param {object} command The JIO command
*/
that.get = function (command) {
that.genericRequest(command, "get");
};
/**
* Get the attachment from all sub storages, get the fastest.
* @method getAttachment
* @param {object} command The JIO command
*/
that.getAttachment = function (command) {
that.genericRequest(command, "getAttachment");
};
/**
* Remove the document from all sub storages.
* @method remove
* @param {object} command The JIO command
*/
that.remove = function (command) {
that.genericRequest(command, "remove");
};
/**
* Remove the attachment from all sub storages.
* @method remove
* @param {object} command The JIO command
*/
that.removeAttachment = function (command) {
that.genericRequest(command, "removeAttachment");
};
return that;
});
}));
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global jIO: true */
jIO.addStorageType('replicate', function (spec, my) {
var that, cloned_option, priv = {},
super_serialized = that.serialized;
spec = spec || {};
that = my.basicStorage(spec, my);
priv.return_value_array = [];
priv.storagelist = spec.storagelist || [];
priv.nb_storage = priv.storagelist.length;
that.serialized = function () {
var o = super_serialized();
o.storagelist = priv.storagelist;
return o;
};
that.validateState = function () {
if (priv.storagelist.length === 0) {
return 'Need at least one parameter: "storagelist" ' +
'containing at least one storage.';
}
return '';
};
priv.isTheLast = function (error_array) {
return (error_array.length === priv.nb_storage);
};
priv.doJob = function (command, errormessage, nodocid) {
var done = false,
error_array = [],
i,
error = function (err) {
if (!done) {
error_array.push(err);
if (priv.isTheLast(error_array)) {
that.error({
status: 207,
statusText: 'Multi-Status',
error: 'multi_status',
message: 'All ' + errormessage + (!nodocid ? ' "' +
command.getDocId() + '"' : ' ') + ' requests have failed.',
reason: 'requests fail',
array: error_array
});
}
}
},
success = function (val) {
if (!done) {
done = true;
that.success(val);
}
};
for (i = 0; i < priv.nb_storage; i += 1) {
cloned_option = command.cloneOption();
that.addJob(command.getLabel(), priv.storagelist[i],
command.cloneDoc(), cloned_option, success, error);
}
};
that.post = function (command) {
priv.doJob(command, 'post');
that.end();
};
/**
* Save a document in several storages.
* @method put
*/
that.put = function (command) {
priv.doJob(command, 'put');
that.end();
};
/**
* Load a document from several storages, and send the first retreived
* document.
* @method get
*/
that.get = function (command) {
priv.doJob(command, 'get');
that.end();
};
/**
* Get a document list from several storages, and returns the first
* retreived document list.
* @method allDocs
*/
that.allDocs = function (command) {
priv.doJob(command, 'allDocs', true);
that.end();
};
/**
* Remove a document from several storages.
* @method remove
*/
that.remove = function (command) {
priv.doJob(command, 'remove');
that.end();
};
return that;
});
/*jslint indent: 2, maxlen: 80, nomen: true */
/*global jIO, hex_sha256, setTimeout, define */
/**
* JIO Revision Storage.
* It manages document version and can generate conflicts.
* Description:
* {
* "type": "revision",
* "sub_storage": <sub storage description>
* }
*/
// 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";
jIO.addStorageType("revision", function (spec, my) {
var that = {}, priv = {};
spec = spec || {};
that = my.basicStorage(spec, my);
// ATTRIBUTES //
priv.doc_tree_suffix = ".revision_tree.json";
priv.sub_storage = spec.sub_storage;
// METHODS //
/**
* Description to store in order to be restored later
* @method specToStore
* @return {object} Descriptions to store
*/
that.specToStore = function () {
return {
"sub_storage": priv.sub_storage
};
};
/**
* 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": 31,
"statusText": "Wrong Revision Format",
"error": "wrong_revision_format",
"message": message,
"reason": "Revision is wrong"
};
};
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, pseudo_revision;
doc = priv.clone(doc) || {};
revision_history = doc._revs;
revs_info = doc._revs_info;
delete doc._rev;
delete doc._revs;
delete doc._revs_info;
string = JSON.stringify(doc) + JSON.stringify(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, next_rev;
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 (method, doc, option, callback) {
that.addJob(
method,
priv.sub_storage,
doc,
option,
function (success) {
callback(undefined, success);
},
function (err) {
callback(err, undefined);
}
);
};
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 (doc, option, callback) {
priv.send("get", doc, option, callback);
};
priv.put = function (doc, option, callback) {
priv.send("put", doc, option, callback);
};
priv.remove = function (doc, option, callback) {
priv.send("remove", doc, option, callback);
};
priv.getAttachment = function (attachment, option, callback) {
priv.send("getAttachment", attachment, option, callback);
};
priv.putAttachment = function (attachment, option, callback) {
priv.send("putAttachment", attachment, option, callback);
};
priv.removeAttachment = function (attachment, option, callback) {
priv.send("removeAttachment", attachment, option, callback);
};
priv.getDocument = function (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(doc, option, callback);
};
priv.putDocument = function (doc, option, callback) {
doc = priv.clone(doc);
doc._id = doc._id + "." + doc._rev;
delete doc._attachment;
delete doc._data;
delete doc._mimetype;
delete doc._rev;
delete doc._revs;
delete doc._revs_info;
priv.put(doc, option, callback);
};
priv.getRevisionTree = function (doc, option, callback) {
doc = priv.clone(doc);
doc._id = doc._id + priv.doc_tree_suffix;
priv.get(doc, option, callback);
};
priv.getAttachmentList = function (doc, option, callback) {
var attachment_id, dealResults, state = "ok", result_list = [], count = 0;
dealResults = function (attachment_id, attachment_meta) {
return function (err, attachment) {
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": attachment,
"_mimetype": attachment_meta.content_type
});
if (count === 0) {
state = "finished";
callback(undefined, result_list);
}
};
};
for (attachment_id in doc._attachments) {
if (doc._attachments.hasOwnProperty(attachment_id)) {
count += 1;
priv.getAttachment(
{"_id": doc._id, "_attachment": attachment_id},
option,
dealResults(attachment_id, doc._attachments[attachment_id])
);
}
}
if (count === 0) {
callback(undefined, []);
}
};
priv.putAttachmentList = function (doc, option, attachment_list, callback) {
var i, dealResults, state = "ok", count = 0, attachment;
attachment_list = attachment_list || [];
dealResults = function (index) {
return function (err, response) {
if (state !== "ok") {
return;
}
count -= 1;
if (err) {
state = "error";
return callback(err, undefined);
}
if (count === 0) {
state = "finished";
callback(undefined, {"id": doc._id, "ok": true});
}
};
};
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(attachment, option, dealResults(i));
}
}
if (count === 0) {
return callback(undefined, {"id": doc._id, "ok": true});
}
};
priv.putDocumentTree = function (doc, option, doc_tree, callback) {
doc_tree = priv.clone(doc_tree);
doc_tree._id = doc._id + priv.doc_tree_suffix;
priv.put(doc_tree, option, callback);
};
priv.notFoundError = function (message, reason) {
return {
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": message,
"reason": reason
};
};
priv.conflictError = function (message, reason) {
return {
"status": 409,
"statusText": "Conflict",
"error": "conflict",
"message": message,
"reason": reason
};
};
priv.revisionGenericRequest = function (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();
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(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 || 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(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(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(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 = {};
} else {
err.message = "Cannot get document";
return onEnd(err, undefined);
}
}
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, 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(doc, option, doc_tree, callback.putDocumentTree);
} else {
priv.getAttachmentList(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);
}
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, 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(doc, option, callback.putDocument);
};
callback.putDocument = function (err, response) {
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(
doc,
option,
attachment_list,
callback.putAttachmentList
);
};
callback.putAttachmentList = function (err, response) {
if (err) {
err.message = "Cannot copy attacments to the document";
return onEnd(err, undefined);
}
priv.putDocumentTree(doc, option, doc_tree, callback.putDocumentTree);
};
callback.putDocumentTree = function (err, response) {
var response_object;
if (err) {
err.message = "Cannot update the document history";
return onEnd(err, undefined);
}
response_object = {
"ok": true,
"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(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) {
priv.revisionGenericRequest(
command.cloneDoc(),
command.cloneOption(),
{},
function (err, response) {
if (err) {
return that.error(err);
}
that.success(response);
}
);
};
/**
* 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) {
priv.revisionGenericRequest(
command.cloneDoc(),
command.cloneOption(),
{},
function (err, response) {
if (err) {
return that.error(err);
}
that.success(response);
}
);
};
that.putAttachment = function (command) {
priv.revisionGenericRequest(
command.cloneDoc(),
command.cloneOption(),
{
"doc_id": command.getDocId(),
"attachment_id": command.getAttachmentId(),
"add_to_attachment_list": {
"_attachment": command.getAttachmentId(),
"_mimetype": command.getAttachmentMimeType(),
"_data": command.getAttachmentData()
},
"putAttachment": true
},
function (err, response) {
if (err) {
return that.error(err);
}
that.success(response);
}
);
};
that.remove = function (command) {
if (command.getAttachmentId()) {
return that.removeAttachment(command);
}
priv.revisionGenericRequest(
command.cloneDoc(),
command.cloneOption(),
{
"revision_needed": true,
"remove": true
},
function (err, response) {
if (err) {
return that.error(err);
}
that.success(response);
}
);
};
that.removeAttachment = function (command) {
priv.revisionGenericRequest(
command.cloneDoc(),
command.cloneOption(),
{
"doc_id": command.getDocId(),
"attachment_id": command.getAttachmentId(),
"revision_needed": true,
"removeAttachment": true,
"remove_from_attachment_list": {
"_attachment": command.getAttachmentId()
}
},
function (err, response) {
if (err) {
return that.error(err);
}
that.success(response);
}
);
};
that.get = function (command) {
if (command.getAttachmentId()) {
return that.getAttachment(command);
}
priv.revisionGenericRequest(
command.cloneDoc(),
command.cloneOption(),
{
"get": true
},
function (err, response) {
if (err) {
return that.error(err);
}
that.success(response);
}
);
};
that.getAttachment = function (command) {
priv.revisionGenericRequest(
command.cloneDoc(),
command.cloneOption(),
{
"doc_id": command.getDocId(),
"attachment_id": command.getAttachmentId(),
"getAttachment": true
},
function (err, response) {
if (err) {
return that.error(err);
}
that.success(response);
}
);
};
that.allDocs = function (command) {
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 that.error(err);
}
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 && command.getOption("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) {
that.success(result);
}
};
priv.send("allDocs", null, command.cloneOption(
), function (err, response) {
var i, j, row, selector, selected;
if (err) {
return that.error(err);
}
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(
rows.revision_trees[i].doc,
functions.fillResultGenerator(rows.revision_trees[i].id)
);
} else {
priv.getRevisionTree(
{"_id": rows.revision_trees[i].id},
command.cloneOption(),
functions.fillResultGenerator(rows.revision_trees[i].id)
);
}
}
}
functions.success();
});
};
// END //
return that;
}); // end RevisionStorage
}));
/*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.addStorageType("s3", function (spec, my) {
var evt, that, priv = {};
spec = spec || {};
that = my.basicStorage(spec, my);
// attributes
priv.username = spec.username || '';
priv.AWSIdentifier = spec.AWSIdentifier || '';
priv.password = spec.password || '';
priv.server = spec.server || ''; /*|| jiobucket ||*/
priv.acl = spec.acl || '';
/*||> "private,
public-read,
public-read-write,
authenticated-read,
bucket-owner-read,
bucket-owner-full-control" <||*/
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');
};
/**
* Replace substrings to another strings
* @method recursiveReplace
* @param {string} string The string to do replacement
* @param {array} list_of_replacement An array of couple
* ["substring to select", "selected substring replaced by this string"].
* @return {string} The replaced string
*/
priv.recursiveReplace = function (string, list_of_replacement) {
var i, split_string = string.split(list_of_replacement[0][0]);
if (list_of_replacement[1]) {
for (i = 0; i < split_string.length; i += 1) {
split_string[i] = priv.recursiveReplace(
split_string[i],
list_of_replacement.slice(1)
);
}
}
return split_string.join(list_of_replacement[0][1]);
};
/**
* Changes / to %2F, % to %25 and . to _.
* @method secureName
* @param {string} name The name to secure
* @return {string} The secured name
*/
priv.secureName = function (name) {
return priv.recursiveReplace(name, [["/", "%2F"], ["%", "%25"]]);
};
/**
* Restores the original name from a secured name
* @method restoreName
* @param {string} secured_name The secured name to restore
* @return {string} The original name
*/
priv.restoreName = function (secured_name) {
return priv.recursiveReplace(secured_name, [["%2F", "/"], ["%25", "%"]]);
};
/**
* Convert document id and attachment id to a file name
* @method idsToFileName
* @param {string} doc_id The document id
* @param {string} attachment_id The attachment id (optional)
* @return {string} The file name
*/
priv.idsToFileName = function (doc_id, attachment_id) {
doc_id = priv.secureName(doc_id).split(".").join("_.");
if (typeof attachment_id === "string") {
attachment_id = priv.secureName(attachment_id).split(".").join("_.");
return doc_id + "." + attachment_id;
}
return doc_id;
};
/**
* Convert a file name to a document id (and attachment id if there)
* @method fileNameToIds
* @param {string} file_name The file name to convert
* @return {array} ["document id", "attachment id"] or ["document id"]
*/
priv.fileNameToIds = function (file_name) {
var separator_index = -1, split = file_name.split(".");
split.slice(0, -1).forEach(function (file_name_part, index) {
if (file_name_part.slice(-1) !== "_") {
separator_index = index;
}
});
if (separator_index === -1) {
return [priv.restoreName(priv.restoreName(
file_name
).split("_.").join("."))];
}
return [
priv.restoreName(priv.restoreName(
split.slice(0, separator_index + 1).join(".")
).split("_.").join(".")),
priv.restoreName(priv.restoreName(
split.slice(separator_index + 1).join(".")
).split("_.").join("."))
];
};
/**
* 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;
};
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 (form) {
//generates the policy
//enables the choice for the http response code
var http_code, s3_policy, Signature = '';
s3_policy = {
"expiration": "2020-01-01T00:00:00Z",
"conditions": [
{"bucket": priv.server },
["starts-with", "$key", ""],
{"acl": priv.acl },
{"success_action_redirect": ""},
{"success_action_status": http_code },
["starts-with", "$Content-Type", ""],
["content-length-range", 0, 524288000]
]
};
//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":
that.success({
ok: true,
id: docId
});
break;
case 'PUT':
if (jio === true) {
that.success({
ok: true,
id: command.getDocId()
});
} else {
callback(this.responseText);
}
break;
case 'GET':
if (jio === true) {
if (typeof this.responseText !== 'string') {
response = JSON.parse(this.responseText);
response._attachments = response._attachments || {};
delete response._attachments;
that.success(JSON.stringify(response));
} else {
if (isAttachment === true) {
that.success(this.responseText);
} else {
that.success(JSON.parse(this.responseText));
}
}
} else {
callback(this.responseText);
}
break;
case 'DELETE':
if (jio === true) {
if (isAttachment === false) {
that.success({
ok: true,
id: command.getDocId()
});
} else {
that.success({
ok: true,
id: command.getDocId(),
attachment: command.getAttachmentId()
});
}
} 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";
that.error(err);
}
if (this.status === 404) {
if (http === 'GET') {
if (jio === true) {
//status
//statustext "Not Found"
//error
//reason "reason"
//message "did not work"
err.statustext = "not_foud";
err.reason = "file does not exist";
err.error = "not_found";
that.error(err);
} else {
callback('404');
}
} else {
//status
//statustext "Not Found"
//error
//reason "reason"
//message "did not work"
err.error = "not_found";
that.error(err);
}
}
if (this.status === 409) {
//status
//statustext "Not Found"
//error
//reason "reason"
//message "did not work"
err.error = "already_exists";
that.error(err);
}
}
}
};
}
priv.updateMeta = function (doc, docid, attachid, action, data) {
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;
};
priv.createError = function (status, message, reason) {
var error = {
"status": status,
"message": message,
"reason": reason
};
switch (status) {
case 404:
error.statusText = "Not found";
break;
case 405:
error.statusText = "Method Not Allowed";
break;
case 409:
error.statusText = "Conflicts";
break;
case 24:
error.statusText = "Corrupted Document";
break;
}
error.error = error.statusText.toLowerCase().split(" ").join("_");
return error;
};
that.encodeAuthorization = function (key, mime) {
//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;
docFile = priv.secureName(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);
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) {
//as S3 encoding key are directly inserted within the FormData(),
//use of XHRwrapper function ain't pertinent
var doc, doc_id, mime;
doc = command.cloneDoc();
doc_id = command.getDocId();
function postDocument() {
var http_response, fd, Signature, xhr;
doc_id = priv.secureName(priv.idsToFileName(doc_id));
//Meant to deep-serialize in order to avoid
//conflicts due to the multipart enctype
doc = JSON.stringify(doc);
http_response = '';
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);
//priv.b64_policy = 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 = 'no_document_id_'
+ ((Math.random() * 10).toString().split('.'))[1];
doc._id = doc_id;
}
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 that.error(priv.createError(
409,
"Cannot create document",
"Document already exists"
));
}
}
);
};
/**
* Get a document or attachment
* @method get
* @param {object} command The JIO command
**/
that.get = function (command) {
var docId, attachId, isJIO, mime;
docId = command.getDocId();
attachId = command.getAttachmentId() || '';
isJIO = true;
mime = 'text/plain; charset=UTF-8';
that.XHRwrapper(command, docId, attachId, 'GET', mime, '', isJIO, false);
};
that.getAttachment = function (command) {
var docId, attachId, isJIO, mime;
docId = command.getDocId();
attachId = command.getAttachmentId();
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) {
var doc, docId, mime;
doc = command.cloneDoc();
docId = command.getDocId();
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) {
//if (response === '404') {}
if (response._attachments !== undefined) {
doc._attachments = response._attachments;
}
putDocument();
}
);
};
that.putAttachment = function (command) {
var mon_document,
docId,
attachId,
mime,
attachment_id,
attachment_data,
attachment_md5,
attachment_mimetype,
attachment_length;
mon_document = null;
docId = command.getDocId();
attachId = command.getAttachmentId() || '';
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 putAttachment() {
that.XHRwrapper(command,
docId,
attachId,
'PUT',
mime,
attachment_data,
false,
true,
function (reponse) {
that.success({
// response
"ok": true,
"id": docId,
"attachment": attachId
//"rev": current_revision
});
}
);
}
function putDocument() {
var attachment_obj, data, doc;
attachment_obj = {
//"revpos": 3, // optional
"digest": attachment_md5,
"content_type": attachment_mimetype,
"length": attachment_length
};
data = JSON.parse(mon_document);
doc = priv.updateMeta(data, docId, attachId, "add", attachment_obj);
that.XHRwrapper(command, docId, '', 'PUT', mime, doc, false, false,
function (reponse) {
putAttachment();
}
);
}
function getDocument() {
//XHRwrapper(command,'PUT','text/plain; charset=UTF-8',true);
that.XHRwrapper(command, docId, '', 'GET', mime, '', false, false,
function (reponse) {
if (reponse === '404') {
return that.error(priv.createError(
404,
"Cannot find document",
"Document does not exist"
));
}
mon_document = reponse;
putDocument();
}
);
}
getDocument();
};
/**
* Remove a document or attachment
* @method remove
* @param {object} command The JIO command
*/
that.remove = function (command) {
var docId, mime;
docId = command.getDocId();
mime = 'text/plain; charset=UTF-8';
function deleteDocument() {
that.XHRwrapper(command, docId, '', 'DELETE', mime, '', true, false,
function (reponse) {
that.success({
// response
"ok": true,
"id": docId
//"rev": current_revision
});
}
);
}
function myCallback(response) {
}
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) {
var mon_document,
docId,
attachId,
mime,
attachment_id,
attachment_data,
attachment_md5,
attachment_mimetype,
attachment_length;
mon_document = null;
docId = command.getDocId();
attachId = command.getAttachmentId() || '';
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 (reponse) {
}
);
}
function putDocument() {
var data, doc;
data = JSON.parse(mon_document);
doc = priv.updateMeta(data, docId, attachId, "remove", '');
that.XHRwrapper(command, docId, '', 'PUT', mime, doc,
false, false, function (reponse) {
removeAttachment();
}
);
}
function getDocument() {
that.XHRwrapper(command, docId, '', 'GET', mime, '', false, false,
function (reponse) {
mon_document = reponse;
putDocument();
}
);
}
getDocument();
};
/**
* Get all filenames belonging to a user from the document index
* @method allDocs
* @param {object} command The JIO command
**/
that.allDocs = function (command) {
var mon_document, mime;
mon_document = null;
mime = 'text/plain; charset=UTF-8';
function makeJSON() {
var keys,
resultTable,
counter,
allDocResponse,
count,
countB,
dealCallback,
errCallback,
i,
keyId,
Signature,
callURL,
requestUTC,
parse,
checkCounter;
keys = $(mon_document).find('Key');
resultTable = [];
counter = 0;
keys.each(function (index) {
var that, filename, docId;
that = $(this);
filename = that.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,
"offset": 0,
"rows": []
};
//needed to save the index within the $.ajax.success() callback
count = resultTable.length - 1;
countB = 0;
dealCallback = function (i, countB, allDoc) {
return function (doc, statustext, response) {
allDoc.rows[i].doc = response.responseText;
if (count === 0) {
that.success(allDoc);
} else {
count -= 1;
}
};
};
errCallback = function (err) {
if (err.status === 404) {
//status
//statustext "Not Found"
//error
//reason "reason"
//message "did not work"
err.error = "not_found";
that.error(err);
} else {
return that.retry(err);
}
};
i = resultTable.length - 1;
if (command.getOption("include_docs") === true) {
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();
parse = true;
allDocResponse.rows[i] = {
"id": priv.fileNameToIds(keyId).join(),
"key": keyId,
"value": {}
};
checkCounter = i;
$.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(that.error)
});
countB += 1;
}
} else {
for (i; i >= 0; i -= 1) {
keyId = resultTable[i];
allDocResponse.rows[i] = {
"id": priv.fileNameToIds(keyId).join(),
"key": keyId,
"value": {}
};
}
that.success(allDocResponse);
}
}
function getXML() {
//XHRwrapper(command,'PUT','text/plain; charset=UTF-8',true);
that.XHRwrapper(command, '', '', 'GET', mime, '', false, false,
function (reponse) {
mon_document = reponse;
makeJSON();
}
);
}
getXML();
//fin alldocs
};
return that;
});
}));
/*
* 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: true, exports: true, define: true */
/**
* 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": [<storage description>, ...]
* }
*/
// 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";
/**
* 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 [];
}
while ((row = this.response_list[0].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].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;
}
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, my) {
var that = my.basicStorage(spec, my), 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;
//////////////////////////////////////////////////////////////////////
// Overrides
/**
* Overrides the original {{#crossLink "storage/specToStore:method"}}
* specToStore method{{/crossLink}}.
*
* @method specToStore
* @return {Object} The specificities to store
*/
that.specToStore = function () {
return {"storage_list": priv.storage_list};
};
/**
* TODO validateState
*/
//////////////////////////////////////////////////////////////////////
// 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 (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) {
that.addJob(
method,
priv.storage_list[i],
doc,
option,
onSuccess(i),
onError(i)
);
}
} else {
for (i = 0; i < priv.storage_list.length; i += 1) {
that.addJob(
method,
priv.storage_list[i],
doc[i],
option,
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 (doc, option, method) {
var i, data, doc_list = [], doc_underscores = {};
if (!doc._id) {
doc._id = generateUuid();
}
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(method, doc_list, option, function (err, response) {
if (err) {
err.message = "Unable to " + method + " document";
delete err.index;
return that.error(err);
}
that.success({"ok": true, "id": doc_underscores._id});
});
};
//////////////////////////////////////////////////////////////////////
// JIO commands
/**
* Split document metadata then store them to the sub storages.
*
* @method post
* @param {Command} command The JIO command
*/
that.post = function (command) {
priv.postOrPut(command.cloneDoc(), command.cloneOption(), 'post');
};
/**
* Split document metadata then store them to the sub storages.
*
* @method put
* @param {Command} command The JIO command
*/
that.put = function (command) {
priv.postOrPut(command.cloneDoc(), command.cloneOption(), 'put');
};
/**
* Puts an attachment to the sub storages.
*
* @method putAttachment
* @param {Command} command The JIO command
*/
that.putAttachment = function (command) {
var i, attachment_list = [], data = command.getAttachmentData();
for (i = 0; i < priv.storage_list.length; i += 1) {
attachment_list[i] = command.cloneDoc();
attachment_list[i]._data = data.slice(
(data.length / priv.storage_list.length) * i,
(data.length / priv.storage_list.length) * (i + 1)
);
}
priv.send(
'putAttachment',
attachment_list,
command.cloneOption(),
function (err, response) {
if (err) {
err.message = "Unable to put attachment";
delete err.index;
return that.error(err);
}
that.success({
"ok": true,
"id": command.getDocId(),
"attachment": command.getAttachmentId()
});
}
);
};
/**
* Gets splited document metadata then returns real document.
*
* @method get
* @param {Command} command The JIO command
*/
that.get = function (command) {
var doc, option, data, attachments;
doc = command.cloneDoc();
option = command.cloneOption();
priv.send('get', doc, option, function (err, response) {
var i, k;
if (err) {
err.message = "Unable to get document";
delete err.index;
return that.error(err);
}
doc = '';
for (i = 0; i < response.length; i += 1) {
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;
}
}
}
}
doc._id = command.getDocId();
that.success(doc);
});
};
/**
* Gets splited document attachment then returns real attachment data.
*
* @method getAttachment
* @param {Command} command The JIO command
*/
that.getAttachment = function (command) {
var doc, option;
doc = command.cloneDoc();
option = command.cloneOption();
priv.send('getAttachment', doc, option, function (err, response) {
var i, k;
if (err) {
err.message = "Unable to get attachment";
delete err.index;
return that.error(err);
}
doc = '';
for (i = 0; i < response.length; i += 1) {
doc += response[i];
}
that.success(doc);
});
};
/**
* Removes a document from the sub storages.
*
* @method remove
* @param {Command} command The JIO command
*/
that.remove = function (command) {
priv.send(
'remove',
command.cloneDoc(),
command.cloneOption(),
function (err, response_list) {
if (err) {
err.message = "Unable to remove document";
delete err.index;
return that.error(err);
}
that.success({"id": command.getDocId(), "ok": true});
}
);
};
/**
* Removes an attachment from the sub storages.
*
* @method removeAttachment
* @param {Command} command The JIO command
*/
that.removeAttachment = function (command) {
var doc = command.cloneDoc();
priv.send(
'removeAttachment',
doc,
command.cloneOption(),
function (err, response_list) {
if (err) {
err.message = "Unable to remove attachment";
delete err.index;
return that.error(err);
}
that.success({
"id": doc._id,
"attachment": doc._attachment,
"ok": true
});
}
);
};
/**
* 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 {Command} command The JIO command
*/
that.allDocs = function (command) {
var option = command.cloneOption();
option = {"include_docs": option.include_docs};
priv.send(
'allDocs',
command.cloneDoc(),
option,
function (err, response_list) {
var all_docs_merger;
if (err) {
err.message = "Unable to retrieve document list";
delete err.index;
return that.error(err);
}
all_docs_merger = new AllDocsResponseMerger();
all_docs_merger.addResponseList(response_list);
return that.success(all_docs_merger.merge(option));
}
);
};
return that;
} // end of splitStorage
jIO.addStorageType('split', splitStorage);
}));
/*jslint indent: 2,
maxlen: 80,
nomen: true
*/
/*global
define: true,
jIO: true,
jQuery: true,
XMLHttpRequest: true,
Blob: true,
FormData: true,
window: true
*/
/**
* JIO XWiki Storage. Type = 'xwiki'.
* XWiki Document/Attachment storage.
*/
(function () {
"use strict";
var $, store;
store = function (spec, my) {
spec = spec || {};
var that, priv, xwikistorage;
that = my.basicStorage(spec, my);
priv = {};
/**
* Get the Space and Page components of a documkent ID.
*
* @param id the document id.
* @return a map of { 'space':<Space>, 'page':<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(/<meta name="form_token" content="(\w*)"\/>/);
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;
};
/**
* Make an HTML5 Blob object.
* Equivilant to the `new Blob()` constructor.
* Will fall back on deprecated BlobBuilder if necessary.
*/
priv.makeBlob = function (contentArray, options) {
var i, bb, BB;
try {
// use the constructor if possible.
return new Blob(contentArray, options);
} catch (err) {
// fall back on the blob builder.
BB = (window.MozBlobBuilder || window.WebKitBlobBuilder
|| window.BlobBuilder);
bb = new BB();
for (i = 0; i < contentArray.length; i += 1) {
bb.append(contentArray[i]);
}
return bb.getBlob(options ? options.type : undefined);
}
};
priv.isBlob = function (potentialBlob) {
return typeof (potentialBlob) !== 'undefined' &&
potentialBlob.toString() === "[object Blob]";
};
/*
* Wrapper for the xwikistorage based on localstorage JiO store.
*/
xwikistorage = {
/**
* 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) {
var success = function (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;
andThen(out, null);
} 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 html 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.xwikiurl + '/bin/download/' + parts.space +
"/" + parts.page + "/" + fileName + '?cb=' + Math.random();
xhr.open('GET', url, true);
if (priv.useBlobs) {
xhr.responseType = 'blob';
} else {
xhr.responseType = 'text';
}
xhr.onload = function (e) {
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) {
that.error(err);
return;
}
var parts = priv.getParts(id);
$.ajax({
url: priv.xwikiurl + "/bin/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 content the attachment content.
* @param andThen a callback taking one parameter, the error if any.
*/
setAttachment: function (docId, fileName, mimeType, content, andThen) {
priv.doWithFormToken(function (formToken, err) {
var parts, blob, fd, xhr;
if (err) {
that.error(err);
return;
}
parts = priv.getParts(docId);
blob = priv.isBlob(content)
? content
: priv.makeBlob([content], {type: mimeType});
fd = new FormData();
fd.append("filepath", blob, fileName);
fd.append("form_token", formToken);
xhr = new XMLHttpRequest();
xhr.open('POST', priv.xwikiurl + "/bin/upload/" +
parts.space + '/' + parts.page, true);
xhr.onload = function (e) {
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) {
that.error(err);
return;
}
var parts = priv.getParts(id);
$.ajax({
url: priv.xwikiurl + "/bin/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) {
that.error(err);
return;
}
$.ajax({
url: priv.xwikiurl + "/bin/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
});
}
});
});
}
};
// ==================== Tools ====================
/**
* Update [doc] the document object and remove [doc] keys
* which are not in [new_doc]. It only changes [doc] keys not starting
* with an underscore.
* ex: doc: {key:value1,_key:value2} with
* new_doc: {key:value3,_key:value4} updates
* doc: {key:value3,_key:value2}.
* @param {object} doc The original document object.
* @param {object} new_doc The new document object
*/
priv.documentObjectUpdate = function (doc, new_doc) {
var k;
for (k in doc) {
if (doc.hasOwnProperty(k)) {
if (k[0] !== '_') {
delete doc[k];
}
}
}
for (k in new_doc) {
if (new_doc.hasOwnProperty(k)) {
if (k[0] !== '_') {
doc[k] = new_doc[k];
}
}
}
};
/**
* Checks if an object has no enumerable keys
* @method objectIsEmpty
* @param {object} obj The object
* @return {boolean} true if empty, else false
*/
priv.objectIsEmpty = function (obj) {
var k;
for (k in obj) {
if (obj.hasOwnProperty(k)) {
return false;
}
}
return true;
};
// ==================== attributes ====================
// the wiki to store stuff in
priv.wiki = spec.wiki || 'xwiki';
// unused
priv.username = spec.username;
priv.language = spec.language;
// URL location of the wiki, unused since
// XWiki doesn't currently allow cross-domain requests.
priv.xwikiurl = spec.xwikiurl ||
window.location.href.replace(/\/xwiki\/bin\//, '/xwiki\n')
.split('\n')[0];
// should be: s@/xwiki/bin/.*$@/xwiki@
// but jslint gets in the way.
// Which URL to load for getting the Anti-CSRF form token, used for testing.
priv.formTokenPath = spec.formTokenPath || priv.xwikiurl;
// If true then Blob objects will be returned by
// getAttachment() rather than strings.
priv.useBlobs = spec.useBlobs || false;
// If true then Blob objects will be returned by
// getAttachment() rather than strings.
priv.useBlobs = spec.useBlobs || false;
that.specToStore = function () {
return {
"username": priv.username,
"language": priv.language,
"xwikiurl": priv.xwikiurl,
};
};
// can't fo wrong since no parameters are required.
that.validateState = function () {
return '';
};
// ==================== commands ====================
/**
* Create a document in local storage.
* @method post
* @param {object} command The JIO command
*/
that.post = function (command) {
var docId = command.getDocId();
if (!(typeof docId === "string" && docId !== "")) {
setTimeout(function () {
that.error({
"status": 405,
"statusText": "Method Not Allowed",
"error": "method_not_allowed",
"message": "Cannot create document which id is undefined",
"reason": "Document id is undefined"
});
});
return;
}
xwikistorage.getItem(docId, function (doc, err) {
if (err) {
that.error(err);
} else if (doc === null) {
// the document does not exist
xwikistorage.setItem(command.getDocId(),
command.cloneDoc(),
function (err) {
if (err) {
that.error(err);
} else {
that.success({
"ok": true,
"id": command.getDocId()
});
}
});
} else {
// the document already exists
that.error({
"status": 409,
"statusText": "Conflicts",
"error": "conflicts",
"message": "Cannot create a new document",
"reason": "Document already exists (use 'put' to modify it)"
});
}
});
};
/**
* Create or update a document in local storage.
* @method put
* @param {object} command The JIO command
*/
that.put = function (command) {
xwikistorage.getItem(command.getDocId(), function (doc, err) {
if (err) {
that.error(err);
} else if (doc === null) {
doc = command.cloneDoc();
} else {
priv.documentObjectUpdate(doc, command.cloneDoc());
}
// write
xwikistorage.setItem(command.getDocId(), doc, function (err) {
if (err) {
that.error(err);
} else {
that.success({
"ok": true,
"id": command.getDocId()
});
}
});
});
};
/**
* Add an attachment to a document
* @method putAttachment
* @param {object} command The JIO command
*/
that.putAttachment = function (command) {
xwikistorage.getItem(command.getDocId(), function (doc, err) {
if (err) {
that.error(err);
} else if (doc === null) {
// the document does not exist
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Impossible to add attachment",
"reason": "Document not found"
});
} else {
// Document exists, upload attachment.
xwikistorage.setAttachment(command.getDocId(),
command.getAttachmentId(),
command.getAttachmentMimeType(),
command.getAttachmentData(),
function (err) {
if (err) {
that.error(err);
} else {
that.success({
"ok": true,
"id": command.getDocId() + "/" + command.getAttachmentId()
});
}
});
}
});
};
/**
* Get a document or attachment
* @method get
* @param {object} command The JIO command
*/
that.get = that.getAttachment = function (command) {
if (typeof command.getAttachmentId() === "string") {
// seeking for an attachment
xwikistorage.getAttachment(command.getDocId(),
command.getAttachmentId(),
function (attach, err) {
if (err) {
that.error(err);
} else if (attach !== null) {
that.success(attach);
} else {
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Cannot find the attachment",
"reason": "Attachment does not exist"
});
}
});
} else {
// seeking for a document
xwikistorage.getItem(command.getDocId(), function (doc, err) {
if (err) {
that.error(err);
} else if (doc !== null) {
that.success(doc);
} else {
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Cannot find the document",
"reason": "Document does not exist"
});
}
});
}
};
/**
* Remove a document or attachment
* @method remove
* @param {object} command The JIO command
*/
that.remove = that.removeAttachment = function (command) {
var notFoundError, objId, complete;
notFoundError = function (word) {
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": word + " not found",
"reason": "missing"
});
};
objId = command.getDocId();
complete = function (err) {
if (err) {
that.error(err);
} else {
that.success({
"ok": true,
"id": objId
});
}
};
if (typeof command.getAttachmentId() === "string") {
objId += '/' + command.getAttachmentId();
xwikistorage.removeAttachment(command.getDocId(),
command.getAttachmentId(),
complete);
} else {
xwikistorage.removeItem(objId, complete);
}
};
/**
* Get all filenames belonging to a user from the document index
* @method allDocs
* @param {object} command The JIO command
*/
that.allDocs = function () {
setTimeout(function () {
that.error({
"status": 405,
"statusText": "Method Not Allowed",
"error": "method_not_allowed",
"message": "Your are not allowed to use this command",
"reason": "xwikistorage forbids AllDocs command executions"
});
});
};
return that;
};
if (typeof (define) === 'function' && define.amd) {
define(['jquery', 'jio'], function (jquery, jIO) {
$ = jquery;
jIO.addStorageType('xwiki', store);
});
} else {
jIO.addStorageType('xwiki', store);
$ = jQuery;
}
}());
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment