localstorage.js 14 KB
Newer Older
1
/*
2 3 4 5
 * Copyright 2013, Nexedi SA
 * Released under the LGPL license.
 * http://www.gnu.org/licenses/lgpl.html
 */
6

Sven Franck's avatar
Sven Franck committed
7
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
8
/*global jIO, localStorage, setTimeout, complex_queries, define */
9

Tristan Cavelier's avatar
Tristan Cavelier committed
10 11
/**
 * JIO Local Storage. Type = 'local'.
Sven Franck's avatar
Sven Franck committed
12
 * Local browser "database" storage.
13 14 15 16 17
 *
 * Storage Description:
 *
 *     {
 *       "type": "local",
18 19 20
 *       "mode": <string>,
 *         // - "localStorage" // default
 *         // - "memory"
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
 *       "username": <non empty string>, // to define user space
 *       "application_name": <string> // default 'untitled'
 *     }
 *
 * Document are stored in path
 * 'jio/localstorage/username/application_name/document_id' like this:
 *
 *     {
 *       "_id": "document_id",
 *       "_attachments": {
 *         "attachment_name": {
 *           "length": data_length,
 *           "digest": "md5-XXX",
 *           "content_type": "mime/type"
 *         },
 *         "attachment_name2": {..}, ...
 *       },
 *       "metadata_name": "metadata_value"
 *       "metadata_name2": ...
 *       ...
 *     }
 *
 * Only "_id" and "_attachments" are specific metadata keys, other one can be
 * added without loss.
 *
 * @class LocalStorage
Tristan Cavelier's avatar
Tristan Cavelier committed
47
 */
Sven Franck's avatar
Sven Franck committed
48

49 50 51 52 53 54 55 56 57
// 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";
58

59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
  /**
   * 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;
  }

99 100
  var ram = {}, memorystorage, localstorage;

101 102 103 104
  /*
   * Wrapper for the localStorage used to simplify instion of any kind of
   * values
   */
105
  localstorage = {
106 107 108 109 110 111 112 113 114 115 116 117
    getItem: function (item) {
      var value = localStorage.getItem(item);
      return value === null ? null : JSON.parse(value);
    },
    setItem: function (item, value) {
      return localStorage.setItem(item, JSON.stringify(value));
    },
    removeItem: function (item) {
      return localStorage.removeItem(item);
    }
  };

118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
  /*
   * Wrapper for the localStorage used to simplify instion of any kind of
   * values
   */
  memorystorage = {
    getItem: function (item) {
      var value = ram[item];
      return value === undefined ? null : JSON.parse(value);
    },
    setItem: function (item, value) {
      ram[item] = JSON.stringify(value);
    },
    removeItem: function (item) {
      delete ram[item];
    }
  };

135
  jIO.addStorageType('local', function (spec, my) {
Tristan Cavelier's avatar
Tristan Cavelier committed
136

137
    spec = spec || {};
138
    var that, priv;
139 140
    that = my.basicStorage(spec, my);
    priv = {};
Tristan Cavelier's avatar
Tristan Cavelier committed
141

142 143 144 145 146 147
    // attributes
    priv.username = spec.username || '';
    priv.application_name = spec.application_name || 'untitled';

    priv.localpath = 'jio/localstorage/' + priv.username + '/' +
      priv.application_name;
148

149 150 151 152 153 154 155 156 157 158 159 160 161 162
    switch (spec.mode) {
    case "memory":
      priv.database = ram;
      priv.storage = memorystorage;
      priv.mode = "memory";
      break;
    default:
      priv.database = localStorage;
      priv.storage = localstorage;
      priv.mode = "localStorage";
      break;
    }


163 164 165 166
    // ===================== overrides ======================
    that.specToStore = function () {
      return {
        "application_name": priv.application_name,
167 168
        "username": priv.username,
        "mode": priv.mode
169 170
      };
    };
Sven Franck's avatar
Sven Franck committed
171

172 173 174
    that.validateState = function () {
      if (typeof priv.username === "string" && priv.username !== '') {
        return '';
Sven Franck's avatar
Sven Franck committed
175
      }
176 177 178 179 180 181 182 183 184 185 186 187 188
      return 'Need at least one parameter: "username".';
    };

    // ==================== commands ====================
    /**
     * Create a document in local storage.
     * @method post
     * @param  {object} command The JIO command
     */
    that.post = function (command) {
      setTimeout(function () {
        var doc, doc_id = command.getDocId();
        if (!doc_id) {
189
          doc_id = generateUuid();
190
        }
191
        doc = priv.storage.getItem(priv.localpath + "/" + doc_id);
192 193 194 195 196
        if (doc === null) {
          // the document does not exist
          doc = command.cloneDoc();
          doc._id = doc_id;
          delete doc._attachments;
197
          priv.storage.setItem(priv.localpath + "/" + doc_id, doc);
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
          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;
223
        doc = priv.storage.getItem(priv.localpath + "/" + command.getDocId());
224 225 226 227 228 229 230 231 232 233 234
        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
235
        priv.storage.setItem(priv.localpath + "/" + command.getDocId(), doc);
Sven Franck's avatar
Sven Franck committed
236 237
        that.success({
          "ok": true,
238
          "id": command.getDocId()
Sven Franck's avatar
Sven Franck committed
239 240
        });
      });
241
    };
Sven Franck's avatar
Sven Franck committed
242

243 244 245 246 247 248 249 250
    /**
     * Add an attachment to a document
     * @method  putAttachment
     * @param  {object} command The JIO command
     */
    that.putAttachment = function (command) {
      setTimeout(function () {
        var doc;
251
        doc = priv.storage.getItem(priv.localpath + "/" + command.getDocId());
252 253 254 255 256 257 258 259 260 261 262
        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;
        }
Sven Franck's avatar
Sven Franck committed
263

264 265 266 267 268 269 270
        // the document already exists
        doc._attachments = doc._attachments || {};
        doc._attachments[command.getAttachmentId()] = {
          "content_type": command.getAttachmentMimeType(),
          "digest": "md5-" + command.md5SumAttachmentData(),
          "length": command.getAttachmentLength()
        };
Sven Franck's avatar
Sven Franck committed
271

272
        // upload data
273
        priv.storage.setItem(priv.localpath + "/" + command.getDocId() + "/" +
274 275 276
                             command.getAttachmentId(),
                             command.getAttachmentData());
        // write document
277
        priv.storage.setItem(priv.localpath + "/" + command.getDocId(), doc);
278 279 280 281 282
        that.success({
          "ok": true,
          "id": command.getDocId(),
          "attachment": command.getAttachmentId()
        });
Sven Franck's avatar
Sven Franck committed
283
      });
284
    };
285

286 287 288 289 290 291 292
    /**
     * Get a document
     * @method get
     * @param  {object} command The JIO command
     */
    that.get = function (command) {
      setTimeout(function () {
293
        var doc = priv.storage.getItem(
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
          priv.localpath + "/" + command.getDocId()
        );
        if (doc !== null) {
          that.success(doc);
        } else {
          that.error({
            "status": 404,
            "statusText": "Not Found",
            "error": "not_found",
            "message": "Cannot find the document",
            "reason": "Document does not exist"
          });
        }
      });
    };
Sven Franck's avatar
Sven Franck committed
309

310 311 312 313 314 315 316
    /**
     * Get a attachment
     * @method getAttachment
     * @param  {object} command The JIO command
     */
    that.getAttachment = function (command) {
      setTimeout(function () {
317
        var doc = priv.storage.getItem(
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
          priv.localpath + "/" + command.getDocId() +
            "/" + command.getAttachmentId()
        );
        if (doc !== null) {
          that.success(doc);
        } else {
          that.error({
            "status": 404,
            "statusText": "Not Found",
            "error": "not_found",
            "message": "Cannot find the attachment",
            "reason": "Attachment does not exist"
          });
        }
      });
    };
334

335 336 337 338 339 340 341 342
    /**
     * Remove a document
     * @method remove
     * @param  {object} command The JIO command
     */
    that.remove = function (command) {
      setTimeout(function () {
        var doc, i, attachment_list;
343
        doc = priv.storage.getItem(priv.localpath + "/" + command.getDocId());
344 345 346 347 348 349 350 351
        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);
              }
352 353
            }
          }
354 355 356 357 358 359 360 361
        } else {
          return that.error({
            "status": 404,
            "statusText": "Not Found",
            "error": "not_found",
            "message": "Document not found",
            "reason": "missing"
          });
362
        }
363
        priv.storage.removeItem(priv.localpath + "/" + command.getDocId());
364 365
        // delete all attachments
        for (i = 0; i < attachment_list.length; i += 1) {
366
          priv.storage.removeItem(priv.localpath + "/" + command.getDocId() +
367 368 369 370 371
                                  "/" + attachment_list[i]);
        }
        that.success({
          "ok": true,
          "id": command.getDocId()
372 373
        });
      });
374
    };
375

376 377 378 379 380 381 382
    /**
     * Remove an attachment
     * @method removeAttachment
     * @param  {object} command The JIO command
     */
    that.removeAttachment = function (command) {
      setTimeout(function () {
Tristan Cavelier's avatar
Tristan Cavelier committed
383
        var doc, error;
384 385 386 387 388 389 390
        error = function (word) {
          that.error({
            "status": 404,
            "statusText": "Not Found",
            "error": "not_found",
            "message": word + " not found",
            "reason": "missing"
391
          });
392
        };
393
        doc = priv.storage.getItem(priv.localpath + "/" + command.getDocId());
394 395 396 397 398 399
        // 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()];
400
            if (objectIsEmpty(doc._attachments)) {
401 402
              delete doc._attachments;
            }
403
            priv.storage.setItem(priv.localpath + "/" + command.getDocId(),
404
                                 doc);
405
            priv.storage.removeItem(priv.localpath + "/" + command.getDocId() +
406 407 408 409 410 411 412 413 414
                                    "/" + command.getAttachmentId());
            that.success({
              "ok": true,
              "id": command.getDocId(),
              "attachment": command.getAttachmentId()
            });
          } else {
            error("Attachment");
          }
Sven Franck's avatar
Sven Franck committed
415
        } else {
416
          error("Document");
Sven Franck's avatar
Sven Franck committed
417
        }
418 419
      });
    };
Sven Franck's avatar
Sven Franck committed
420

421 422 423 424 425 426 427
    /**
     * 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;
428
      rows = [];
429 430 431 432 433 434 435 436 437 438 439
      document_list = [];
      path_re = new RegExp(
        "^" + complex_queries.stringEscapeRegexpCharacters(priv.localpath) +
          "/[^/]+$"
      );
      option = command.cloneOption();
      if (typeof complex_queries !== "object" ||
          (option.query === undefined && option.sort_on === undefined &&
           option.select_list === undefined &&
           option.include_docs === undefined)) {
        rows = [];
440 441
        for (i in priv.database) {
          if (priv.database.hasOwnProperty(i)) {
442 443 444 445 446 447
            // 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')) {
448
                row.doc = JSON.parse(priv.storage.getItem(i));
449 450
              }
              rows.push(row);
451
            }
452 453
          }
        }
454 455 456
        that.success({"rows": rows, "total_rows": rows.length});
      } else {
        // create complex query object from returned results
457 458
        for (i in priv.database) {
          if (priv.database.hasOwnProperty(i)) {
459
            if (path_re.test(i)) {
460
              document_list.push(priv.storage.getItem(i));
461
            }
462 463
          }
        }
464 465
        option.select_list = option.select_list || [];
        option.select_list.push("_id");
466
        if (option.include_docs === true) {
467 468 469 470
          document_object = {};
          document_list.forEach(function (meta) {
            document_object[meta._id] = meta;
          });
471
        }
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490
        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});
      }
    };
Tristan Cavelier's avatar
Tristan Cavelier committed
491

492 493 494
    return that;
  });
}));