simple-storage.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. module.metadata = {
  6. "stability": "stable"
  7. };
  8. const { Cc, Ci } = require("chrome");
  9. const file = require("./io/file");
  10. const prefs = require("./preferences/service");
  11. const jpSelf = require("./self");
  12. const timer = require("./timers");
  13. const unload = require("./system/unload");
  14. const { emit, on, off } = require("./event/core");
  15. const WRITE_PERIOD_PREF = "extensions.addon-sdk.simple-storage.writePeriod";
  16. const WRITE_PERIOD_DEFAULT = 300000; // 5 minutes
  17. const QUOTA_PREF = "extensions.addon-sdk.simple-storage.quota";
  18. const QUOTA_DEFAULT = 5242880; // 5 MiB
  19. const JETPACK_DIR_BASENAME = "jetpack";
  20. Object.defineProperties(exports, {
  21. storage: {
  22. enumerable: true,
  23. get: function() { return manager.root; },
  24. set: function(value) { manager.root = value; }
  25. },
  26. quotaUsage: {
  27. get: function() { return manager.quotaUsage; }
  28. }
  29. });
  30. // A generic JSON store backed by a file on disk. This should be isolated
  31. // enough to move to its own module if need be...
  32. function JsonStore(options) {
  33. this.filename = options.filename;
  34. this.quota = options.quota;
  35. this.writePeriod = options.writePeriod;
  36. this.onOverQuota = options.onOverQuota;
  37. this.onWrite = options.onWrite;
  38. unload.ensure(this);
  39. this.writeTimer = timer.setInterval(this.write.bind(this),
  40. this.writePeriod);
  41. }
  42. JsonStore.prototype = {
  43. // The store's root.
  44. get root() {
  45. return this.isRootInited ? this._root : {};
  46. },
  47. // Performs some type checking.
  48. set root(val) {
  49. let types = ["array", "boolean", "null", "number", "object", "string"];
  50. if (types.indexOf(typeof(val)) < 0) {
  51. throw new Error("storage must be one of the following types: " +
  52. types.join(", "));
  53. }
  54. this._root = val;
  55. return val;
  56. },
  57. // True if the root has ever been set (either via the root setter or by the
  58. // backing file's having been read).
  59. get isRootInited() {
  60. return this._root !== undefined;
  61. },
  62. // Percentage of quota used, as a number [0, Inf). > 1 implies over quota.
  63. // Undefined if there is no quota.
  64. get quotaUsage() {
  65. return this.quota > 0 ?
  66. JSON.stringify(this.root).length / this.quota :
  67. undefined;
  68. },
  69. // Removes the backing file and all empty subdirectories.
  70. purge: function JsonStore_purge() {
  71. try {
  72. // This'll throw if the file doesn't exist.
  73. file.remove(this.filename);
  74. let parentPath = this.filename;
  75. do {
  76. parentPath = file.dirname(parentPath);
  77. // This'll throw if the dir isn't empty.
  78. file.rmdir(parentPath);
  79. } while (file.basename(parentPath) !== JETPACK_DIR_BASENAME);
  80. }
  81. catch (err) {}
  82. },
  83. // Initializes the root by reading the backing file.
  84. read: function JsonStore_read() {
  85. try {
  86. let str = file.read(this.filename);
  87. // Ideally we'd log the parse error with console.error(), but logged
  88. // errors cause tests to fail. Supporting "known" errors in the test
  89. // harness appears to be non-trivial. Maybe later.
  90. this.root = JSON.parse(str);
  91. }
  92. catch (err) {
  93. this.root = {};
  94. }
  95. },
  96. // If the store is under quota, writes the root to the backing file.
  97. // Otherwise quota observers are notified and nothing is written.
  98. write: function JsonStore_write() {
  99. if (this.quotaUsage > 1)
  100. this.onOverQuota(this);
  101. else
  102. this._write();
  103. },
  104. // Cleans up on unload. If unloading because of uninstall, the store is
  105. // purged; otherwise it's written.
  106. unload: function JsonStore_unload(reason) {
  107. timer.clearInterval(this.writeTimer);
  108. this.writeTimer = null;
  109. if (reason === "uninstall")
  110. this.purge();
  111. else
  112. this._write();
  113. },
  114. // True if the root is an empty object.
  115. get _isEmpty() {
  116. if (this.root && typeof(this.root) === "object") {
  117. let empty = true;
  118. for (let key in this.root) {
  119. empty = false;
  120. break;
  121. }
  122. return empty;
  123. }
  124. return false;
  125. },
  126. // Writes the root to the backing file, notifying write observers when
  127. // complete. If the store is over quota or if it's empty and the store has
  128. // never been written, nothing is written and write observers aren't notified.
  129. _write: function JsonStore__write() {
  130. // Don't write if the root is uninitialized or if the store is empty and the
  131. // backing file doesn't yet exist.
  132. if (!this.isRootInited || (this._isEmpty && !file.exists(this.filename)))
  133. return;
  134. // If the store is over quota, don't write. The current under-quota state
  135. // should persist.
  136. if (this.quotaUsage > 1)
  137. return;
  138. // Finally, write.
  139. let stream = file.open(this.filename, "w");
  140. try {
  141. stream.writeAsync(JSON.stringify(this.root), function writeAsync(err) {
  142. if (err)
  143. console.error("Error writing simple storage file: " + this.filename);
  144. else if (this.onWrite)
  145. this.onWrite(this);
  146. }.bind(this));
  147. }
  148. catch (err) {
  149. // writeAsync closes the stream after it's done, so only close on error.
  150. stream.close();
  151. }
  152. }
  153. };
  154. // This manages a JsonStore singleton and tailors its use to simple storage.
  155. // The root of the JsonStore is lazy-loaded: The backing file is only read the
  156. // first time the root's gotten.
  157. let manager = ({
  158. jsonStore: null,
  159. // The filename of the store, based on the profile dir and extension ID.
  160. get filename() {
  161. let storeFile = Cc["@mozilla.org/file/directory_service;1"].
  162. getService(Ci.nsIProperties).
  163. get("ProfD", Ci.nsIFile);
  164. storeFile.append(JETPACK_DIR_BASENAME);
  165. storeFile.append(jpSelf.id);
  166. storeFile.append("simple-storage");
  167. file.mkpath(storeFile.path);
  168. storeFile.append("store.json");
  169. return storeFile.path;
  170. },
  171. get quotaUsage() {
  172. return this.jsonStore.quotaUsage;
  173. },
  174. get root() {
  175. if (!this.jsonStore.isRootInited)
  176. this.jsonStore.read();
  177. return this.jsonStore.root;
  178. },
  179. set root(val) {
  180. return this.jsonStore.root = val;
  181. },
  182. unload: function manager_unload() {
  183. off(this);
  184. },
  185. new: function manager_constructor() {
  186. let manager = Object.create(this);
  187. unload.ensure(manager);
  188. manager.jsonStore = new JsonStore({
  189. filename: manager.filename,
  190. writePeriod: prefs.get(WRITE_PERIOD_PREF, WRITE_PERIOD_DEFAULT),
  191. quota: prefs.get(QUOTA_PREF, QUOTA_DEFAULT),
  192. onOverQuota: emit.bind(null, exports, "OverQuota")
  193. });
  194. return manager;
  195. }
  196. }).new();
  197. exports.on = on.bind(null, exports);
  198. exports.removeListener = function(type, listener) {
  199. off(exports, type, listener);
  200. };