├── .editorconfig ├── .gitignore ├── .hound.yml ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── appveyor.yml ├── doc └── README.hbs ├── lib ├── lock.js ├── storage.js └── utils.js ├── package-lock.json ├── package.json ├── stress ├── process.js └── start.sh └── tests ├── storage.spec.js └── utils.spec.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 28 | node_modules 29 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | javascript: 2 | config_file: .jshintrc 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // -------------------------------------------------------------------- 3 | // JSHint Configuration, Strict Edition 4 | // -------------------------------------------------------------------- 5 | // 6 | // This is a options template for [JSHint][1], using [JSHint example][2] 7 | // and [Ory Band's example][3] as basis and setting config values to 8 | // be most strict: 9 | // 10 | // * set all enforcing options to true 11 | // * set all relaxing options to false 12 | // * set all environment options to false, except the browser value 13 | // * set all JSLint legacy options to false 14 | // 15 | // [1]: http://www.jshint.com/ 16 | // [2]: https://github.com/jshint/node-jshint/blob/master/example/config.json 17 | // [3]: https://github.com/oryband/dotfiles/blob/master/jshintrc 18 | // 19 | // @author http://michael.haschke.biz/ 20 | // @license http://unlicense.org/ 21 | 22 | // == Enforcing Options =============================================== 23 | // 24 | // These options tell JSHint to be more strict towards your code. Use 25 | // them if you want to allow only a safe subset of JavaScript, very 26 | // useful when your codebase is shared with a big number of developers 27 | // with different skill levels. 28 | 29 | "bitwise" : true, // Prohibit bitwise operators (&, |, ^, etc.). 30 | "curly" : true, // Require {} for every new block or scope. 31 | "eqeqeq" : true, // Require triple equals i.e. `===`. 32 | "forin" : true, // Tolerate `for in` loops without `hasOwnPrototype`. 33 | "immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 34 | "latedef" : true, // Prohibit variable use before definition. 35 | "newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`. 36 | "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`. 37 | "noempty" : true, // Prohibit use of empty blocks. 38 | "nonew" : true, // Prohibit use of constructors for side-effects. 39 | "plusplus" : true, // Prohibit use of `++` & `--`. 40 | "regexp" : true, // Prohibit `.` and `[^...]` in regular expressions. 41 | "undef" : true, // Require all non-global variables be declared before they are used. 42 | "strict" : true, // Require `use strict` pragma in every file. 43 | "trailing" : true, // Prohibit trailing whitespaces. 44 | 45 | // == Relaxing Options ================================================ 46 | // 47 | // These options allow you to suppress certain types of warnings. Use 48 | // them only if you are absolutely positive that you know what you are 49 | // doing. 50 | 51 | "asi" : false, // Tolerate Automatic Semicolon Insertion (no semicolons). 52 | "boss" : false, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments. 53 | "debug" : false, // Allow debugger statements e.g. browser breakpoints. 54 | "eqnull" : false, // Tolerate use of `== null`. 55 | "es5" : false, // Allow EcmaScript 5 syntax. 56 | "esnext" : true, // Allow ES.next specific features such as `const` and `let`. 57 | "evil" : false, // Tolerate use of `eval`. 58 | "expr" : true, // Tolerate `ExpressionStatement` as Programs. 59 | "funcscope" : false, // Tolerate declarations of variables inside of control structures while accessing them later from the outside. 60 | "globalstrict" : true, // Allow global "use strict" (also enables 'strict'). 61 | "iterator" : false, // Allow usage of __iterator__ property. 62 | "lastsemic" : false, // Tolerat missing semicolons when the it is omitted for the last statement in a one-line block. 63 | "laxbreak" : false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons. 64 | "laxcomma" : false, // Suppress warnings about comma-first coding style. 65 | "loopfunc" : false, // Allow functions to be defined within loops. 66 | "multistr" : false, // Tolerate multi-line strings. 67 | "onecase" : false, // Tolerate switches with just one case. 68 | "proto" : false, // Tolerate __proto__ property. This property is deprecated. 69 | "regexdash" : false, // Tolerate unescaped last dash i.e. `[-...]`. 70 | "scripturl" : false, // Tolerate script-targeted URLs. 71 | "smarttabs" : false, // Tolerate mixed tabs and spaces when the latter are used for alignmnent only. 72 | "shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`. 73 | "sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`. 74 | "supernew" : false, // Tolerate `new function () { ... };` and `new Object;`. 75 | "validthis" : false, // Tolerate strict violations when the code is running in strict mode and you use this in a non-constructor function. 76 | 77 | // == Environments ==================================================== 78 | // 79 | // These options pre-define global variables that are exposed by 80 | // popular JavaScript libraries and runtime environments—such as 81 | // browser or node.js. 82 | 83 | "browser" : true, // Standard browser globals e.g. `window`, `document`. 84 | "couch" : false, // Enable globals exposed by CouchDB. 85 | "devel" : false, // Allow development statements e.g. `console.log();`. 86 | "dojo" : false, // Enable globals exposed by Dojo Toolkit. 87 | "mocha" : true, // Enable globals exposed by Mocha. 88 | "jquery" : false, // Enable globals exposed by jQuery JavaScript library. 89 | "mootools" : false, // Enable globals exposed by MooTools JavaScript framework. 90 | "node" : true, // Enable globals available when code is running inside of the NodeJS runtime environment. 91 | "nonstandard" : false, // Define non-standard but widely adopted globals such as escape and unescape. 92 | "prototypejs" : false, // Enable globals exposed by Prototype JavaScript framework. 93 | "rhino" : false, // Enable globals available when your code is running inside of the Rhino runtime environment. 94 | "wsh" : false, // Enable globals available when your code is running as a script for the Windows Script Host. 95 | 96 | // == JSLint Legacy =================================================== 97 | // 98 | // These options are legacy from JSLint. Aside from bug fixes they will 99 | // not be improved in any way and might be removed at any point. 100 | 101 | "nomen" : false, // Prohibit use of initial or trailing underbars in names. 102 | "onevar" : false, // Allow only one `var` statement per function. 103 | "passfail" : false, // Stop on first error. 104 | "white" : false, // Check against strict whitespace and indentation rules. 105 | 106 | // == Undocumented Options ============================================ 107 | // 108 | // While I've found these options in [example1][2] and [example2][3] 109 | // they are not described in the [JSHint Options documentation][4]. 110 | // 111 | // [4]: http://www.jshint.com/options/ 112 | 113 | "maxerr" : 100, // Maximum errors before stopping. 114 | "maxlen" : 120, // Maximum line length. 115 | "indent" : 4 // Specify indentation spacing 116 | } 117 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: xenial 3 | node_js: 4 | - 12 5 | services: 6 | - xvfb 7 | before_install: 8 | - sudo apt-get install -y libgconf-2-4 9 | script: 10 | - npm test 11 | - ./stress/start.sh 12 | notifications: 13 | email: false 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [4.6.0] - 2022-10-12 7 | 8 | - Add a `.setSync()` function 9 | 10 | ## [4.5.0] - 2021-04-13 11 | 12 | - Add a `.getSync()` function 13 | 14 | ## [4.4.0] - 2021-02-22 15 | 16 | - Gracefully require the user to call `.setDataPath()` if the `remote` module 17 | is not available when running on a renderer process 18 | 19 | ## [4.3.0] - 2020-11-04 20 | 21 | - Add a `prettyPrinting` option to `.set()` 22 | 23 | ## [4.2.0] - 2020-07-09 24 | 25 | - Support a `validate` boolean option in `.set()` to validate writes by reading 26 | the key back after a short period of time and re-trying the write if the 27 | contents do not match 28 | 29 | ## [4.1.8] - 2019-09-13 30 | 31 | - Don't list non-JSON files in `.keys()` 32 | 33 | ## [4.1.7] - 2019-08-12 34 | 35 | - Don't store data as UTF-8 36 | 37 | ## [4.1.6] - 2019-01-26 38 | 39 | - Implement atomic writes 40 | 41 | ## [4.1.5] - 2018-12-02 42 | 43 | - Retry on `EPERM` when locking and reading 44 | 45 | ## [4.1.4] - 2018-10-09 46 | 47 | ### Changed 48 | 49 | - Set `electron` as a `devDependency` 50 | 51 | ## [4.1.3] - 2018-10-01 52 | 53 | ### Changed 54 | 55 | - Retry lock release if OS reports `EPERM` 56 | 57 | ## [4.1.2] - 2018-08-26 58 | 59 | ### Changed 60 | 61 | - Set `electron` as a `peerDependency` 62 | 63 | ## [4.1.1] - 2018-07-11 64 | 65 | ### Changed 66 | 67 | - Ensure parallel writes from multiple processes don't corrupt data 68 | 69 | ## [4.1.0] - 2018-04-15 70 | 71 | ### Changed 72 | 73 | - Support spaces in keys 74 | 75 | ## [4.0.3] - 2018-04-07 76 | 77 | ### Changed 78 | 79 | - Remove unnecessary ES6 features from the code base to keep it ES5 80 | 81 | ## [4.0.2] - 2017-10-20 82 | 83 | ### Changed 84 | 85 | - Ensure the `options` argument always defaults to an empty object. 86 | 87 | ## [4.0.1] - 2017-10-19 88 | 89 | ### Changed 90 | 91 | - Don't throw if the user doesn't pass a callback function. 92 | 93 | ## [4.0.0] - 2017-10-18 94 | 95 | ### Changed 96 | 97 | - React to external changes to the `userData` path. 98 | - Replace `storage.DEFAULT_DATA_PATH` with `storage.getDefaultDataPath()`. 99 | 100 | ## [3.2.0] - 2017-10-07 101 | 102 | ### Added 103 | 104 | - Add `dataPath` options to every function. 105 | 106 | ## [3.1.1] - 2017-09-27 107 | 108 | ### Changed 109 | 110 | - Replace asterisks with hyphens in file names to avoid Windows path problems. 111 | 112 | ## [3.1.0] - 2017-08-29 113 | 114 | ### Added 115 | 116 | - Support storing values in a custom directory. 117 | 118 | ## [3.0.7] - 2017-07-27 119 | 120 | ### Changed 121 | 122 | - Decode URI encoded file names on `.keys()` 123 | 124 | ## [3.0.6] - 2017-06-29 125 | 126 | ### Changed 127 | 128 | - Ensure parallel writes don't corrupt the data. 129 | 130 | ## [3.0.5] - 2017-04-14 131 | 132 | ### Changed 133 | 134 | - Make the module work on Spectron tests. 135 | 136 | ## [3.0.4] - 2017-03-30 137 | 138 | ### Changed 139 | 140 | - Get rid of `exists-file`, which is known to cause UglifyJS issues. 141 | 142 | ## [3.0.3] - 2017-03-30 143 | 144 | ### Changed 145 | 146 | - Remove ES6 features from the codebase. 147 | 148 | ## [3.0.2] - 2017-03-24 149 | 150 | ### Changed 151 | 152 | - Ignore `.DS_Store` in settings directory 153 | - Include the invalid error object on "invalid data" errors 154 | 155 | ## [3.0.1] - 2017-01-30 156 | 157 | ### Changed 158 | 159 | - Don't throw `ENOENT` on `.getAll()` if the user data path directory doesn't exist. 160 | 161 | ## [3.0.0] - 2017-01-08 162 | 163 | ### Changed 164 | 165 | - Store settings inside a `storage/` directory inside `userPath`. 166 | 167 | ## [2.1.1] - 2017-01-08 168 | 169 | ### Changed 170 | 171 | - Don't throw `ENOENT` on `.set()` if `userPath` doesn't exist. 172 | 173 | ## [2.1.0] - 2016-11-13 174 | 175 | ### Added 176 | 177 | - Implement `.getAll()`. 178 | - Implement `.getMany()`. 179 | 180 | ## [2.0.3] - 2016-10-27 181 | 182 | ### Changed 183 | 184 | - Change `let` to `var` for compatibility purposes. 185 | 186 | ## [2.0.2] - 2016-10-24 187 | 188 | ### Changed 189 | 190 | - Fix "Callback has already been called" error in `.get()`. 191 | 192 | ## [2.0.1] - 2016-10-05 193 | 194 | ### Changed 195 | 196 | - Prevent errors when using reserved characters in keys in Windows. 197 | 198 | ## [2.0.0] - 2016-02-26 199 | 200 | ### Changed 201 | 202 | - Ignore `GPUCache` key, saved by Electron. 203 | 204 | ### Removed 205 | 206 | - Remove promises support. 207 | 208 | ## [1.1.0] - 2016-02-17 209 | 210 | ### Added 211 | 212 | - Implement `.keys() function`. 213 | 214 | ### Changed 215 | 216 | - Fix error when requiring this module from the renderer process. 217 | 218 | [4.6.0]: https://github.com/electron-userland/electron-json-storage/compare/v4.5.0...v4.6.0 219 | [4.5.0]: https://github.com/electron-userland/electron-json-storage/compare/v4.4.0...v4.5.0 220 | [4.4.0]: https://github.com/electron-userland/electron-json-storage/compare/v4.3.0...v4.4.0 221 | [4.3.0]: https://github.com/electron-userland/electron-json-storage/compare/v4.2.0...v4.3.0 222 | [4.2.0]: https://github.com/electron-userland/electron-json-storage/compare/v4.1.8...v4.2.0 223 | [4.1.8]: https://github.com/electron-userland/electron-json-storage/compare/v4.1.7...v4.1.8 224 | [4.1.7]: https://github.com/electron-userland/electron-json-storage/compare/v4.1.6...v4.1.7 225 | [4.1.6]: https://github.com/electron-userland/electron-json-storage/compare/v4.1.5...v4.1.6 226 | [4.1.5]: https://github.com/electron-userland/electron-json-storage/compare/v4.1.4...v4.1.5 227 | [4.1.4]: https://github.com/electron-userland/electron-json-storage/compare/v4.1.3...v4.1.4 228 | [4.1.3]: https://github.com/electron-userland/electron-json-storage/compare/v4.1.2...v4.1.3 229 | [4.1.2]: https://github.com/electron-userland/electron-json-storage/compare/v4.1.1...v4.1.2 230 | [4.1.1]: https://github.com/electron-userland/electron-json-storage/compare/v4.1.0...v4.1.1 231 | [4.1.0]: https://github.com/electron-userland/electron-json-storage/compare/v4.0.3...v4.1.0 232 | [4.0.3]: https://github.com/electron-userland/electron-json-storage/compare/v4.0.2...v4.0.3 233 | [4.0.2]: https://github.com/electron-userland/electron-json-storage/compare/v4.0.1...v4.0.2 234 | [4.0.1]: https://github.com/electron-userland/electron-json-storage/compare/v4.0.0...v4.0.1 235 | [4.0.0]: https://github.com/electron-userland/electron-json-storage/compare/v3.2.0...v4.0.0 236 | [3.2.0]: https://github.com/electron-userland/electron-json-storage/compare/v3.1.1...v3.2.0 237 | [3.1.1]: https://github.com/electron-userland/electron-json-storage/compare/v3.1.0...v3.1.1 238 | [3.1.0]: https://github.com/electron-userland/electron-json-storage/compare/v3.0.7...v3.1.0 239 | [3.0.7]: https://github.com/electron-userland/electron-json-storage/compare/v3.0.6...v3.0.7 240 | [3.0.6]: https://github.com/electron-userland/electron-json-storage/compare/v3.0.5...v3.0.6 241 | [3.0.5]: https://github.com/electron-userland/electron-json-storage/compare/v3.0.4...v3.0.5 242 | [3.0.4]: https://github.com/electron-userland/electron-json-storage/compare/v3.0.3...v3.0.4 243 | [3.0.3]: https://github.com/electron-userland/electron-json-storage/compare/v3.0.2...v3.0.3 244 | [3.0.2]: https://github.com/electron-userland/electron-json-storage/compare/v3.0.1...v3.0.2 245 | [3.0.1]: https://github.com/electron-userland/electron-json-storage/compare/v3.0.0...v3.0.1 246 | [3.0.0]: https://github.com/electron-userland/electron-json-storage/compare/v2.1.1...v3.0.0 247 | [2.1.1]: https://github.com/electron-userland/electron-json-storage/compare/v2.1.0...v2.1.1 248 | [2.1.0]: https://github.com/electron-userland/electron-json-storage/compare/v2.0.3...v2.1.0 249 | [2.0.3]: https://github.com/electron-userland/electron-json-storage/compare/v2.0.2...v2.0.3 250 | [2.0.2]: https://github.com/electron-userland/electron-json-storage/compare/v2.0.1...v2.0.2 251 | [2.0.1]: https://github.com/electron-userland/electron-json-storage/compare/v2.0.0...v2.0.1 252 | [2.0.0]: https://github.com/electron-userland/electron-json-storage/compare/v1.1.0...v2.0.0 253 | [1.1.0]: https://github.com/electron-userland/electron-json-storage/compare/v1.0.0...v1.1.0 254 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | electron-json-storage 2 | ===================== 3 | 4 | > Easily write and read user settings in Electron apps 5 | 6 | [![npm version](https://badge.fury.io/js/electron-json-storage.svg)](http://badge.fury.io/js/electron-json-storage) 7 | [![dependencies](https://david-dm.org/jviotti/electron-json-storage.svg)](https://david-dm.org/jviotti/electron-json-storage.svg) 8 | [![Build Status](https://travis-ci.org/electron-userland/electron-json-storage.svg?branch=master)](https://travis-ci.org/electron-userland/electron-json-storage) 9 | [![Build status](https://ci.appveyor.com/api/projects/status/ulwk1nnh7l8209xg/branch/master?svg=true)](https://ci.appveyor.com/project/electron-userland/electron-json-storage/branch/master) 10 | 11 | [Electron](http://electron.atom.io) lacks an easy way to persist and read user settings for your application. `electron-json-storage` implements an API somewhat similar to [localStorage](https://developer.mozilla.org/en/docs/Web/API/Window/localStorage) to write and read JSON objects to/from the operating system application data directory, as defined by `app.getPath('userData')`. 12 | 13 | Related modules: 14 | 15 | - [electron-settings](https://github.com/nathanbuchar/electron-settings) 16 | - [electron-store](https://github.com/sindresorhus/electron-store) 17 | - [electron-storage](https://github.com/Cocycles/electron-storage) 18 | 19 | Installation 20 | ------------ 21 | 22 | Install `electron-json-storage` by running: 23 | 24 | ```sh 25 | $ npm install --save electron-json-storage 26 | ``` 27 | 28 | You can require this module from either the **main** or **renderer** process (with and without `remote`). 29 | 30 | Running on Electron >10 renderer processes 31 | ------------------------------------------ 32 | 33 | When loaded in renderer processes, this module will try to make use of 34 | `electron.remote` in order to fetch the `userData` path. 35 | 36 | Electron 10 now [defaults `enableRemoteModule` to 37 | false](https://www.electronjs.org/docs/breaking-changes#default-changed-enableremotemodule-defaults-to-false), 38 | which means that `electron-json-storage` will be able to calculate a data path by default. 39 | 40 | The solution is to manually call `storage.setDataPath()` before reading or 41 | writing any values or setting `enableRemoteModule` to `true`. 42 | 43 | Documentation 44 | ------------- 45 | 46 | 47 | * [storage](#module_storage) 48 | * [.getDefaultDataPath()](#module_storage.getDefaultDataPath) ⇒ String \| Null 49 | * [.setDataPath(directory)](#module_storage.setDataPath) 50 | * [.getDataPath()](#module_storage.getDataPath) ⇒ String 51 | * [.get(key, [options], callback)](#module_storage.get) 52 | * [.getSync(key, [options])](#module_storage.getSync) 53 | * [.getMany(keys, [options], callback)](#module_storage.getMany) 54 | * [.getAll([options], callback)](#module_storage.getAll) 55 | * [.set(key, json, [options], callback)](#module_storage.set) 56 | * [.has(key, [options], callback)](#module_storage.has) 57 | * [.keys([options], callback)](#module_storage.keys) 58 | * [.remove(key, [options], callback)](#module_storage.remove) 59 | * [.clear([options], callback)](#module_storage.clear) 60 | 61 | 62 | 63 | ### storage.getDefaultDataPath() ⇒ String \| Null 64 | This function will return `null` when running in the 65 | renderer process without support for the `remote` IPC 66 | mechanism. You have to explicitly set a data path using 67 | `.setDataPath()` in these cases. 68 | 69 | **Kind**: static method of [storage](#module_storage) 70 | **Summary**: Get the default data path 71 | **Returns**: String \| Null - default data path 72 | **Access**: public 73 | **Example** 74 | ```js 75 | const defaultDataPath = storage.getDefaultDataPath() 76 | ``` 77 | 78 | 79 | ### storage.setDataPath(directory) 80 | The default value will be used if the directory is undefined. 81 | 82 | **Kind**: static method of [storage](#module_storage) 83 | **Summary**: Set current data path 84 | **Access**: public 85 | 86 | | Param | Type | Description | 87 | | --- | --- | --- | 88 | | directory | String \| Undefined | directory | 89 | 90 | **Example** 91 | ```js 92 | const os = require('os'); 93 | const storage = require('electron-json-storage'); 94 | 95 | storage.setDataPath(os.tmpdir()); 96 | ``` 97 | 98 | 99 | ### storage.getDataPath() ⇒ String 100 | Returns the current data path. It defaults to a directory called 101 | "storage" inside Electron's `userData` path. 102 | 103 | **Kind**: static method of [storage](#module_storage) 104 | **Summary**: Get current user data path 105 | **Returns**: String - the user data path 106 | **Access**: public 107 | **Example** 108 | ```js 109 | const storage = require('electron-json-storage'); 110 | 111 | const dataPath = storage.getDataPath(); 112 | console.log(dataPath); 113 | ``` 114 | 115 | 116 | ### storage.get(key, [options], callback) 117 | If the key doesn't exist in the user data, an empty object is returned. 118 | Also notice that the `.json` extension is added automatically, but it's 119 | ignored if you pass it yourself. 120 | 121 | Passing an extension other than `.json` will result in a file created 122 | with both extensions. For example, the key `foo.data` will result in a file 123 | called `foo.data.json`. 124 | 125 | **Kind**: static method of [storage](#module_storage) 126 | **Summary**: Read user data 127 | **Access**: public 128 | 129 | | Param | Type | Description | 130 | | --- | --- | --- | 131 | | key | String | key | 132 | | [options] | Object | options | 133 | | [options.dataPath] | String | data path | 134 | | callback | function | callback (error, data) | 135 | 136 | **Example** 137 | ```js 138 | const storage = require('electron-json-storage'); 139 | 140 | storage.get('foobar', function(error, data) { 141 | if (error) throw error; 142 | 143 | console.log(data); 144 | }); 145 | ``` 146 | 147 | 148 | ### storage.getSync(key, [options]) 149 | See `.get()`. 150 | 151 | **Kind**: static method of [storage](#module_storage) 152 | **Summary**: Read user data (sync) 153 | **Access**: public 154 | 155 | | Param | Type | Description | 156 | | --- | --- | --- | 157 | | key | String | key | 158 | | [options] | Object | options | 159 | | [options.dataPath] | String | data path | 160 | 161 | **Example** 162 | ```js 163 | const storage = require('electron-json-storage'); 164 | 165 | var data = storage.getSync('foobar'); 166 | console.log(data); 167 | ``` 168 | 169 | 170 | ### storage.getMany(keys, [options], callback) 171 | This function returns an object with the data of all the passed keys. 172 | If one of the keys doesn't exist, an empty object is returned for it. 173 | 174 | **Kind**: static method of [storage](#module_storage) 175 | **Summary**: Read many user data keys 176 | **Access**: public 177 | 178 | | Param | Type | Description | 179 | | --- | --- | --- | 180 | | keys | Array.<String> | keys | 181 | | [options] | Object | options | 182 | | [options.dataPath] | String | data path | 183 | | callback | function | callback (error, data) | 184 | 185 | **Example** 186 | ```js 187 | const storage = require('electron-json-storage'); 188 | 189 | storage.getMany([ 'foobar', 'barbaz' ], function(error, data) { 190 | if (error) throw error; 191 | 192 | console.log(data.foobar); 193 | console.log(data.barbaz); 194 | }); 195 | ``` 196 | 197 | 198 | ### storage.getAll([options], callback) 199 | This function returns an empty object if there is no data to be read. 200 | 201 | **Kind**: static method of [storage](#module_storage) 202 | **Summary**: Read all user data 203 | **Access**: public 204 | 205 | | Param | Type | Description | 206 | | --- | --- | --- | 207 | | [options] | Object | options | 208 | | [options.dataPath] | String | data path | 209 | | callback | function | callback (error, data) | 210 | 211 | **Example** 212 | ```js 213 | const storage = require('electron-json-storage'); 214 | 215 | storage.getAll(function(error, data) { 216 | if (error) throw error; 217 | 218 | console.log(data); 219 | }); 220 | ``` 221 | 222 | 223 | ### storage.set(key, json, [options], callback) 224 | **Kind**: static method of [storage](#module_storage) 225 | **Summary**: Write user data 226 | **Access**: public 227 | 228 | | Param | Type | Description | 229 | | --- | --- | --- | 230 | | key | String | key | 231 | | json | Object | json object | 232 | | [options] | Object | options | 233 | | [options.dataPath] | String | data path | 234 | | [options.validate] | String | validate writes by reading the data back | 235 | | [options.prettyPrinting] | boolean | adds line breaks and spacing to the written data | 236 | | callback | function | callback (error) | 237 | 238 | **Example** 239 | ```js 240 | const storage = require('electron-json-storage'); 241 | 242 | storage.set('foobar', { foo: 'bar' }, function(error) { 243 | if (error) throw error; 244 | }); 245 | ``` 246 | 247 | 248 | ### storage.has(key, [options], callback) 249 | **Kind**: static method of [storage](#module_storage) 250 | **Summary**: Check if a key exists 251 | **Access**: public 252 | 253 | | Param | Type | Description | 254 | | --- | --- | --- | 255 | | key | String | key | 256 | | [options] | Object | options | 257 | | [options.dataPath] | String | data path | 258 | | callback | function | callback (error, hasKey) | 259 | 260 | **Example** 261 | ```js 262 | const storage = require('electron-json-storage'); 263 | 264 | storage.has('foobar', function(error, hasKey) { 265 | if (error) throw error; 266 | 267 | if (hasKey) { 268 | console.log('There is data stored as `foobar`'); 269 | } 270 | }); 271 | ``` 272 | 273 | 274 | ### storage.keys([options], callback) 275 | **Kind**: static method of [storage](#module_storage) 276 | **Summary**: Get the list of saved keys 277 | **Access**: public 278 | 279 | | Param | Type | Description | 280 | | --- | --- | --- | 281 | | [options] | Object | options | 282 | | [options.dataPath] | String | data path | 283 | | callback | function | callback (error, keys) | 284 | 285 | **Example** 286 | ```js 287 | const storage = require('electron-json-storage'); 288 | 289 | storage.keys(function(error, keys) { 290 | if (error) throw error; 291 | 292 | for (var key of keys) { 293 | console.log('There is a key called: ' + key); 294 | } 295 | }); 296 | ``` 297 | 298 | 299 | ### storage.remove(key, [options], callback) 300 | Notice this function does nothing, nor throws any error 301 | if the key doesn't exist. 302 | 303 | **Kind**: static method of [storage](#module_storage) 304 | **Summary**: Remove a key 305 | **Access**: public 306 | 307 | | Param | Type | Description | 308 | | --- | --- | --- | 309 | | key | String | key | 310 | | [options] | Object | options | 311 | | [options.dataPath] | String | data path | 312 | | callback | function | callback (error) | 313 | 314 | **Example** 315 | ```js 316 | const storage = require('electron-json-storage'); 317 | 318 | storage.remove('foobar', function(error) { 319 | if (error) throw error; 320 | }); 321 | ``` 322 | 323 | 324 | ### storage.clear([options], callback) 325 | **Kind**: static method of [storage](#module_storage) 326 | **Summary**: Clear all stored data in the current user data path 327 | **Access**: public 328 | 329 | | Param | Type | Description | 330 | | --- | --- | --- | 331 | | [options] | Object | options | 332 | | [options.dataPath] | String | data path | 333 | | callback | function | callback (error) | 334 | 335 | **Example** 336 | ```js 337 | const storage = require('electron-json-storage'); 338 | 339 | storage.clear(function(error) { 340 | if (error) throw error; 341 | }); 342 | ``` 343 | 344 | Support 345 | ------- 346 | 347 | If you're having any problem, please [raise an issue](https://github.com/electron-userland/electron-json-storage/issues/new) on GitHub and we'll be happy to help. 348 | 349 | Tests 350 | ----- 351 | 352 | Run the test suite by doing: 353 | 354 | ```sh 355 | $ npm test 356 | ``` 357 | 358 | Contribute 359 | ---------- 360 | 361 | - Issue Tracker: [github.com/electron-userland/electron-json-storage/issues](https://github.com/electron-userland/electron-json-storage/issues) 362 | - Source Code: [github.com/electron-userland/electron-json-storage](https://github.com/electron-userland/electron-json-storage) 363 | 364 | Before submitting a PR, please make sure that you include tests, and that [jshint](http://jshint.com) runs without any warning: 365 | 366 | ```sh 367 | $ npm run-script lint 368 | ``` 369 | 370 | License 371 | ------- 372 | 373 | The project is licensed under the MIT license. 374 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # appveyor file 2 | # http://www.appveyor.com/docs/appveyor-yml 3 | 4 | init: 5 | - git config --global core.autocrlf input 6 | 7 | cache: 8 | - C:\Users\appveyor\.node-gyp 9 | - '%AppData%\npm-cache' 10 | 11 | # what combinations to test 12 | environment: 13 | global: 14 | ELECTRON_NO_ATTACH_CONSOLE: true 15 | matrix: 16 | - nodejs_version: 12 17 | 18 | branches: 19 | only: 20 | - master 21 | 22 | install: 23 | - ps: Install-Product node $env:nodejs_version x64 24 | - set PATH=%APPDATA%\npm;%PATH% 25 | - npm install 26 | 27 | build: off 28 | 29 | test_script: 30 | - node --version 31 | - npm --version 32 | - cmd: npm test 33 | -------------------------------------------------------------------------------- /doc/README.hbs: -------------------------------------------------------------------------------- 1 | electron-json-storage 2 | ===================== 3 | 4 | > Easily write and read user settings in Electron apps 5 | 6 | [![npm version](https://badge.fury.io/js/electron-json-storage.svg)](http://badge.fury.io/js/electron-json-storage) 7 | [![dependencies](https://david-dm.org/jviotti/electron-json-storage.svg)](https://david-dm.org/jviotti/electron-json-storage.svg) 8 | [![Build Status](https://travis-ci.org/electron-userland/electron-json-storage.svg?branch=master)](https://travis-ci.org/electron-userland/electron-json-storage) 9 | [![Build status](https://ci.appveyor.com/api/projects/status/ulwk1nnh7l8209xg/branch/master?svg=true)](https://ci.appveyor.com/project/electron-userland/electron-json-storage/branch/master) 10 | 11 | [Electron](http://electron.atom.io) lacks an easy way to persist and read user settings for your application. `electron-json-storage` implements an API somewhat similar to [localStorage](https://developer.mozilla.org/en/docs/Web/API/Window/localStorage) to write and read JSON objects to/from the operating system application data directory, as defined by `app.getPath('userData')`. 12 | 13 | Related modules: 14 | 15 | - [electron-settings](https://github.com/nathanbuchar/electron-settings) 16 | - [electron-store](https://github.com/sindresorhus/electron-store) 17 | - [electron-storage](https://github.com/Cocycles/electron-storage) 18 | 19 | Installation 20 | ------------ 21 | 22 | Install `electron-json-storage` by running: 23 | 24 | ```sh 25 | $ npm install --save electron-json-storage 26 | ``` 27 | 28 | You can require this module from either the **main** or **renderer** process (with and without `remote`). 29 | 30 | Running on Electron >10 renderer processes 31 | ------------------------------------------ 32 | 33 | When loaded in renderer processes, this module will try to make use of 34 | `electron.remote` in order to fetch the `userData` path. 35 | 36 | Electron 10 now [defaults `enableRemoteModule` to 37 | false](https://www.electronjs.org/docs/breaking-changes#default-changed-enableremotemodule-defaults-to-false), 38 | which means that `electron-json-storage` will be able to calculate a data path by default. 39 | 40 | The solution is to manually call `storage.setDataPath()` before reading or 41 | writing any values or setting `enableRemoteModule` to `true`. 42 | 43 | Documentation 44 | ------------- 45 | 46 | {{#module name="storage"}} 47 | {{>body~}} 48 | {{>member-index~}} 49 | {{>separator~}} 50 | {{>members~}} 51 | {{/module}} 52 | 53 | Support 54 | ------- 55 | 56 | If you're having any problem, please [raise an issue](https://github.com/electron-userland/electron-json-storage/issues/new) on GitHub and we'll be happy to help. 57 | 58 | Tests 59 | ----- 60 | 61 | Run the test suite by doing: 62 | 63 | ```sh 64 | $ npm test 65 | ``` 66 | 67 | Contribute 68 | ---------- 69 | 70 | - Issue Tracker: [github.com/electron-userland/electron-json-storage/issues](https://github.com/electron-userland/electron-json-storage/issues) 71 | - Source Code: [github.com/electron-userland/electron-json-storage](https://github.com/electron-userland/electron-json-storage) 72 | 73 | Before submitting a PR, please make sure that you include tests, and that [jshint](http://jshint.com) runs without any warning: 74 | 75 | ```sh 76 | $ npm run-script lint 77 | ``` 78 | 79 | License 80 | ------- 81 | 82 | The project is licensed under the MIT license. 83 | -------------------------------------------------------------------------------- /lib/lock.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2018 Juan Cruz Viotti. https://github.com/jviotti 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | 'use strict'; 26 | 27 | const lockFile = require('lockfile'); 28 | 29 | /** 30 | * @summary Lock options 31 | * @type {Object} 32 | * @private 33 | */ 34 | const lockOptions = { 35 | stale: 10000, 36 | retries: 1000, 37 | retryWait: 50 38 | }; 39 | 40 | /** 41 | * @summary Create a lock file 42 | * @function 43 | * @public 44 | * 45 | * @param {String} file - lock file 46 | * @param {Function} callback - callback (error) 47 | * 48 | * @example 49 | * lock.lock('foo.lock', function(error) { 50 | * if (error) { 51 | * throw error; 52 | * } 53 | * }) 54 | */ 55 | exports.lock = function(file, callback, times) { 56 | times = times || 0; 57 | 58 | lockFile.lock(file, lockOptions, function(error) { 59 | if (error && error.code === 'EPERM' && times < 10) { 60 | setTimeout(function() { 61 | exports.lock(file, callback, times + 1); 62 | }, 1000); 63 | return; 64 | } 65 | 66 | return callback(error); 67 | }); 68 | }; 69 | 70 | /** 71 | * @summary Create a lock file (sync) 72 | * @function 73 | * @public 74 | * 75 | * @param {String} file - lock file 76 | * 77 | * @example 78 | * lock.lockSync('foo.lock'); 79 | */ 80 | exports.lockSync = function(file, times) { 81 | times = times || 0; 82 | 83 | try { 84 | lockFile.lockSync(file, { 85 | stale: lockOptions.stale, 86 | retries: lockOptions.retries 87 | }); 88 | } catch (error) { 89 | if (error && error.code === 'EPERM' && times < 10) { 90 | return exports.lockSync(file, times + 1); 91 | } 92 | 93 | throw error; 94 | } 95 | }; 96 | 97 | /** 98 | * @summary Unlock a locked file 99 | * @function 100 | * @public 101 | * 102 | * @param {String} file - lock file 103 | * @param {Function} callback - callback (error) 104 | * 105 | * @example 106 | * lock.unlock('foo.lock', function(error) { 107 | * if (error) { 108 | * throw error; 109 | * } 110 | * }) 111 | */ 112 | exports.unlock = function(file, callback, times) { 113 | times = times || 0; 114 | 115 | lockFile.unlock(file, function(error) { 116 | if (error && error.code === 'EPERM' && times < 10) { 117 | setTimeout(function() { 118 | exports.unlock(file, callback, times + 1); 119 | }, 1000); 120 | return; 121 | } 122 | 123 | return callback(error); 124 | }); 125 | }; 126 | 127 | /** 128 | * @summary Unlock a locked file (sync) 129 | * @function 130 | * @public 131 | * 132 | * @param {String} file - lock file 133 | * 134 | * @example 135 | * lock.unlockSync('foo.lock'); 136 | */ 137 | exports.unlockSync = function(file, times) { 138 | times = times || 0; 139 | 140 | try { 141 | lockFile.unlockSync(file); 142 | } catch (error) { 143 | if (error && error.code === 'EPERM' && times < 10) { 144 | return exports.unlockSync(file, times + 1); 145 | } 146 | 147 | throw error; 148 | } 149 | }; 150 | -------------------------------------------------------------------------------- /lib/storage.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2016 Juan Cruz Viotti. https://github.com/jviotti 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | 'use strict'; 26 | 27 | /** 28 | * @module storage 29 | */ 30 | 31 | const _ = require('lodash'); 32 | const async = require('async'); 33 | const fs = require('fs'); 34 | const rimraf = require('rimraf'); 35 | const mkdirp = require('mkdirp'); 36 | const path = require('path'); 37 | const writeFileAtomic = require('write-file-atomic'); 38 | const utils = require('./utils'); 39 | const lock = require('./lock'); 40 | 41 | const readFile = function (fileName, callback, times) { 42 | times = times || 0; 43 | 44 | fs.readFile(fileName, function (error, object) { 45 | if (!error) { 46 | return callback(null, object); 47 | } 48 | 49 | if (error.code === 'ENOENT') { 50 | return callback(null, JSON.stringify({})); 51 | } 52 | 53 | if (error.code === 'EPERM' && times < 10) { 54 | setTimeout(function () { 55 | readFile(fileName, callback, times + 1); 56 | }, 1000); 57 | return; 58 | } 59 | 60 | return callback(error); 61 | }); 62 | }; 63 | 64 | const readFileSync = function (fileName, times) { 65 | times = times || 0; 66 | try { 67 | return fs.readFileSync(fileName); 68 | } catch (error) { 69 | if (error.code === 'ENOENT') { 70 | return JSON.stringify({}); 71 | } 72 | 73 | if (error.code === 'EPERM' && times < 10) { 74 | return readFileSync(fileName, times + 1); 75 | } 76 | 77 | throw error; 78 | } 79 | }; 80 | 81 | /** 82 | * @summary Get the default data path 83 | * @function 84 | * @public 85 | * 86 | * @description 87 | * This function will return `null` when running in the 88 | * renderer process without support for the `remote` IPC 89 | * mechanism. You have to explicitly set a data path using 90 | * `.setDataPath()` in these cases. 91 | * 92 | * @returns {(String|Null)} default data path 93 | * 94 | * @example 95 | * const defaultDataPath = storage.getDefaultDataPath() 96 | */ 97 | exports.getDefaultDataPath = utils.getDefaultDataPath; 98 | 99 | /** 100 | * @summary Set current data path 101 | * @function 102 | * @public 103 | * 104 | * @description 105 | * The default value will be used if the directory is undefined. 106 | * 107 | * @param {(String|Undefined)} directory - directory 108 | * 109 | * @example 110 | * const os = require('os'); 111 | * const storage = require('electron-json-storage'); 112 | * 113 | * storage.setDataPath(os.tmpdir()); 114 | */ 115 | exports.setDataPath = utils.setDataPath; 116 | 117 | /** 118 | * @summary Get current user data path 119 | * @function 120 | * @public 121 | * 122 | * @description 123 | * Returns the current data path. It defaults to a directory called 124 | * "storage" inside Electron's `userData` path. 125 | * 126 | * @returns {String} the user data path 127 | * 128 | * @example 129 | * const storage = require('electron-json-storage'); 130 | * 131 | * const dataPath = storage.getDataPath(); 132 | * console.log(dataPath); 133 | */ 134 | exports.getDataPath = utils.getDataPath; 135 | 136 | /** 137 | * @summary Read user data 138 | * @function 139 | * @public 140 | * 141 | * @description 142 | * If the key doesn't exist in the user data, an empty object is returned. 143 | * Also notice that the `.json` extension is added automatically, but it's 144 | * ignored if you pass it yourself. 145 | * 146 | * Passing an extension other than `.json` will result in a file created 147 | * with both extensions. For example, the key `foo.data` will result in a file 148 | * called `foo.data.json`. 149 | * 150 | * @param {String} key - key 151 | * @param {Object} [options] - options 152 | * @param {String} [options.dataPath] - data path 153 | * @param {Function} callback - callback (error, data) 154 | * 155 | * @example 156 | * const storage = require('electron-json-storage'); 157 | * 158 | * storage.get('foobar', function(error, data) { 159 | * if (error) throw error; 160 | * 161 | * console.log(data); 162 | * }); 163 | */ 164 | exports.get = function (key, options, callback) { 165 | if (_.isFunction(options)) { 166 | callback = options; 167 | } 168 | 169 | options = options || {}; 170 | callback = callback || _.noop; 171 | var fileName = null; 172 | 173 | async.waterfall([ 174 | async.asyncify(_.partial(utils.getFileName, key, { 175 | dataPath: options.dataPath 176 | })), 177 | function (result, callback) { 178 | fileName = result; 179 | mkdirp(path.dirname(fileName), callback); 180 | }, 181 | function (made, next) { 182 | lock.lock(utils.getLockFileName(fileName), function (error) { 183 | if (error && error.code === 'EEXIST') { 184 | return exports.get(key, options, callback); 185 | } 186 | 187 | return next(error); 188 | }); 189 | }, 190 | function (callback) { 191 | readFile(fileName, callback); 192 | }, 193 | function (object, callback) { 194 | var objectJSON = {}; 195 | try { 196 | objectJSON = JSON.parse(object); 197 | } catch (error) { 198 | return callback(new Error('Invalid data: ' + object)); 199 | } 200 | return callback(null, objectJSON); 201 | } 202 | ], function (error, result) { 203 | lock.unlock(utils.getLockFileName(fileName), function (lockError) { 204 | if (error) { 205 | return callback(error); 206 | } 207 | 208 | return callback(lockError, result); 209 | }); 210 | }); 211 | }; 212 | 213 | /** 214 | * @summary Read user data (sync) 215 | * @function 216 | * @public 217 | * 218 | * @description 219 | * See `.get()`. 220 | * 221 | * @param {String} key - key 222 | * @param {Object} [options] - options 223 | * @param {String} [options.dataPath] - data path 224 | * 225 | * @example 226 | * const storage = require('electron-json-storage'); 227 | * 228 | * var data = storage.getSync('foobar'); 229 | * console.log(data); 230 | */ 231 | exports.getSync = function (key, options) { 232 | options = options || {}; 233 | var fileName = utils.getFileName(key, { 234 | dataPath: options.dataPath 235 | }); 236 | 237 | mkdirp.sync(path.dirname(fileName)); 238 | 239 | try { 240 | lock.lockSync(utils.getLockFileName(fileName)); 241 | } catch (error) { 242 | if (error && error.code === 'EEXIST') { 243 | return exports.getSync(key, options); 244 | } 245 | 246 | throw error; 247 | } 248 | 249 | var object = readFileSync(fileName); 250 | lock.unlockSync(utils.getLockFileName(fileName)); 251 | 252 | try { 253 | return JSON.parse(object); 254 | } catch (error) { 255 | throw new Error('Invalid data: ' + object); 256 | } 257 | }; 258 | 259 | /** 260 | * @summary Read many user data keys 261 | * @function 262 | * @public 263 | * 264 | * @description 265 | * This function returns an object with the data of all the passed keys. 266 | * If one of the keys doesn't exist, an empty object is returned for it. 267 | * 268 | * @param {String[]} keys - keys 269 | * @param {Object} [options] - options 270 | * @param {String} [options.dataPath] - data path 271 | * @param {Function} callback - callback (error, data) 272 | * 273 | * @example 274 | * const storage = require('electron-json-storage'); 275 | * 276 | * storage.getMany([ 'foobar', 'barbaz' ], function(error, data) { 277 | * if (error) throw error; 278 | * 279 | * console.log(data.foobar); 280 | * console.log(data.barbaz); 281 | * }); 282 | */ 283 | exports.getMany = function (keys, options, callback) { 284 | if (_.isFunction(options)) { 285 | callback = options; 286 | options = {}; 287 | } 288 | 289 | options = options || {}; 290 | callback = callback || _.noop; 291 | 292 | async.reduce(keys, {}, function (reducer, key, callback) { 293 | exports.get(key, options, function (error, data) { 294 | if (error) { 295 | return callback(error); 296 | } 297 | return callback(null, _.set(reducer, key, data)); 298 | }); 299 | }, callback); 300 | }; 301 | 302 | /** 303 | * @summary Read all user data 304 | * @function 305 | * @public 306 | * 307 | * @description 308 | * This function returns an empty object if there is no data to be read. 309 | * 310 | * @param {Object} [options] - options 311 | * @param {String} [options.dataPath] - data path 312 | * @param {Function} callback - callback (error, data) 313 | * 314 | * @example 315 | * const storage = require('electron-json-storage'); 316 | * 317 | * storage.getAll(function(error, data) { 318 | * if (error) throw error; 319 | * 320 | * console.log(data); 321 | * }); 322 | */ 323 | exports.getAll = function (options, callback) { 324 | if (_.isFunction(options)) { 325 | callback = options; 326 | options = {}; 327 | } 328 | 329 | options = options || {}; 330 | callback = callback || _.noop; 331 | 332 | async.waterfall([ 333 | _.partial(exports.keys, options), 334 | function (keys, callback) { 335 | async.reduce(keys, {}, function (reducer, key, callback) { 336 | async.waterfall([ 337 | _.partial(exports.get, key, options), 338 | function (contents, callback) { 339 | return callback(null, _.set(reducer, key, contents)); 340 | } 341 | ], callback); 342 | }, callback); 343 | } 344 | ], callback); 345 | }; 346 | 347 | /** 348 | * @summary Write user data 349 | * @function 350 | * @public 351 | * 352 | * @param {String} key - key 353 | * @param {Object} json - json object 354 | * @param {Object} [options] - options 355 | * @param {String} [options.dataPath] - data path 356 | * @param {String} [options.validate] - validate writes by reading the data back 357 | * @param {boolean} [options.prettyPrinting] - adds line breaks and spacing to the written data 358 | * @param {Function} callback - callback (error) 359 | * 360 | * @example 361 | * const storage = require('electron-json-storage'); 362 | * 363 | * storage.set('foobar', { foo: 'bar' }, function(error) { 364 | * if (error) throw error; 365 | * }); 366 | */ 367 | exports.set = function (key, json, options, callback, retries) { 368 | if (!_.isNumber(retries)) { 369 | retries = 10; 370 | } 371 | 372 | if (_.isFunction(options)) { 373 | callback = options; 374 | } 375 | 376 | options = options || {}; 377 | callback = callback || _.noop; 378 | var fileName = null; 379 | 380 | async.waterfall([ 381 | async.asyncify(_.partial(utils.getFileName, key, { 382 | dataPath: options.dataPath 383 | })), 384 | function (result, callback) { 385 | fileName = result; 386 | const data = JSON.stringify(json, null, (options.prettyPrinting ? 2 : 0)); 387 | 388 | if (!data) { 389 | return callback(new Error('Invalid JSON data')); 390 | } 391 | 392 | // Create the directory in case it doesn't exist yet 393 | mkdirp(path.dirname(fileName), function (error) { 394 | return callback(error, data); 395 | }); 396 | }, 397 | function (data, next) { 398 | lock.lock(utils.getLockFileName(fileName), function (error) { 399 | if (error && error.code === 'EEXIST') { 400 | return exports.set(key, json, options, callback); 401 | } 402 | 403 | return next(error, fileName, data); 404 | }); 405 | }, 406 | function (fileName, data, callback) { 407 | writeFileAtomic(fileName, data, callback); 408 | } 409 | ], function (error) { 410 | lock.unlock(utils.getLockFileName(fileName), function (lockError) { 411 | if (error) { 412 | return callback(error); 413 | } 414 | 415 | if (!options.validate) { 416 | return callback(lockError); 417 | } 418 | 419 | // Check that the writes were actually successful 420 | // after a little bit 421 | setTimeout(function () { 422 | exports.get(key, { 423 | dataPath: options.dataPath 424 | }, function (getError, data) { 425 | if (getError) { 426 | return callback(getError); 427 | } 428 | 429 | if (!_.isEqual(data, json)) { 430 | if (retries <= 0) { 431 | throw new Error('Couldn\'t ensure data was written correctly'); 432 | } 433 | 434 | return exports.set(key, json, options, callback, retries - 1); 435 | } 436 | 437 | return callback(); 438 | }); 439 | }, 100); 440 | }); 441 | }); 442 | }; 443 | 444 | /** 445 | * @summary Write user data sync 446 | * @function 447 | * @public 448 | * 449 | * @param {String} key - key 450 | * @param {Object} json - json object 451 | * @param {Object} [options] - options 452 | * @param {String} [options.dataPath] - data path 453 | * @param {boolean} [options.prettyPrinting] - adds line breaks and spacing to the written data 454 | * 455 | * @example 456 | * const storage = require('electron-json-storage'); 457 | * 458 | * storage.setSync('foobar', { foo: 'bar' }); 459 | */ 460 | exports.setSync = function (key, json, options = {}) { 461 | const fileName = utils.getFileName(key, { 462 | dataPath: options.dataPath 463 | }); 464 | const data = JSON.stringify(json, null, (options.prettyPrinting ? 2 : 0)); 465 | 466 | if (!data) { 467 | throw new Error('Invalid JSON data'); 468 | } 469 | 470 | mkdirp.sync(path.dirname(fileName)); 471 | 472 | try { 473 | lock.lockSync(utils.getLockFileName(fileName)); 474 | } catch (error) { 475 | if (error && error.code === 'EEXIST') { 476 | return exports.setSync(key, json, options); 477 | } 478 | throw error; 479 | } 480 | 481 | try { 482 | fs.writeFileSync(fileName, data); 483 | lock.unlockSync(utils.getLockFileName(fileName)); 484 | return; 485 | } 486 | catch (error) { 487 | throw error; 488 | } 489 | }; 490 | 491 | /** 492 | * @summary Check if a key exists 493 | * @function 494 | * @public 495 | * 496 | * @param {String} key - key 497 | * @param {Object} [options] - options 498 | * @param {String} [options.dataPath] - data path 499 | * @param {Function} callback - callback (error, hasKey) 500 | * 501 | * @example 502 | * const storage = require('electron-json-storage'); 503 | * 504 | * storage.has('foobar', function(error, hasKey) { 505 | * if (error) throw error; 506 | * 507 | * if (hasKey) { 508 | * console.log('There is data stored as `foobar`'); 509 | * } 510 | * }); 511 | */ 512 | exports.has = function (key, options, callback) { 513 | if (_.isFunction(options)) { 514 | callback = options; 515 | } 516 | 517 | options = options || {}; 518 | callback = callback || _.noop; 519 | 520 | async.waterfall([ 521 | async.asyncify(_.partial(utils.getFileName, key, { 522 | dataPath: options.dataPath 523 | })), 524 | function (filename, done) { 525 | fs.stat(filename, function (error) { 526 | if (error) { 527 | if (error.code === 'ENOENT') { 528 | return done(null, false); 529 | } 530 | 531 | return done(error); 532 | } 533 | 534 | return done(null, true); 535 | }); 536 | } 537 | ], callback); 538 | }; 539 | 540 | /** 541 | * @summary Get the list of saved keys 542 | * @function 543 | * @public 544 | * 545 | * @param {Object} [options] - options 546 | * @param {String} [options.dataPath] - data path 547 | * @param {Function} callback - callback (error, keys) 548 | * 549 | * @example 550 | * const storage = require('electron-json-storage'); 551 | * 552 | * storage.keys(function(error, keys) { 553 | * if (error) throw error; 554 | * 555 | * for (var key of keys) { 556 | * console.log('There is a key called: ' + key); 557 | * } 558 | * }); 559 | */ 560 | exports.keys = function (options, callback) { 561 | if (_.isFunction(options)) { 562 | callback = options; 563 | options = {}; 564 | } 565 | 566 | options = options || {}; 567 | callback = callback || _.noop; 568 | 569 | async.waterfall([ 570 | function (callback) { 571 | callback(null, options.dataPath || exports.getDataPath()); 572 | }, 573 | function (userDataPath, callback) { 574 | mkdirp(userDataPath, function (error) { 575 | return callback(error, userDataPath); 576 | }); 577 | }, 578 | fs.readdir, 579 | function (keys, callback) { 580 | callback(null, _.map(_.reject(keys, function (key) { 581 | return path.extname(key) !== '.json'; 582 | }), function (key) { 583 | return path.basename(decodeURIComponent(key), '.json'); 584 | })); 585 | } 586 | ], callback); 587 | }; 588 | 589 | /** 590 | * @summary Remove a key 591 | * @function 592 | * @public 593 | * 594 | * @description 595 | * Notice this function does nothing, nor throws any error 596 | * if the key doesn't exist. 597 | * 598 | * @param {String} key - key 599 | * @param {Object} [options] - options 600 | * @param {String} [options.dataPath] - data path 601 | * @param {Function} callback - callback (error) 602 | * 603 | * @example 604 | * const storage = require('electron-json-storage'); 605 | * 606 | * storage.remove('foobar', function(error) { 607 | * if (error) throw error; 608 | * }); 609 | */ 610 | exports.remove = function (key, options, callback) { 611 | if (_.isFunction(options)) { 612 | callback = options; 613 | } 614 | 615 | options = options || {}; 616 | callback = callback || _.noop; 617 | 618 | async.waterfall([ 619 | async.asyncify(_.partial(utils.getFileName, key, { 620 | dataPath: options.dataPath 621 | })), 622 | rimraf 623 | ], callback); 624 | }; 625 | 626 | /** 627 | * @summary Clear all stored data in the current user data path 628 | * @function 629 | * @public 630 | * 631 | * @param {Object} [options] - options 632 | * @param {String} [options.dataPath] - data path 633 | * @param {Function} callback - callback (error) 634 | * 635 | * @example 636 | * const storage = require('electron-json-storage'); 637 | * 638 | * storage.clear(function(error) { 639 | * if (error) throw error; 640 | * }); 641 | */ 642 | exports.clear = function (options, callback) { 643 | if (_.isFunction(options)) { 644 | callback = options; 645 | } 646 | 647 | options = options || {}; 648 | callback = callback || _.noop; 649 | 650 | const userData = options.dataPath || exports.getDataPath(); 651 | const jsonFiles = path.join(userData, '*.json'); 652 | rimraf(jsonFiles, callback); 653 | }; 654 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2016 Juan Cruz Viotti. https://github.com/jviotti 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | 'use strict'; 26 | 27 | const _ = require('lodash'); 28 | const path = require('path'); 29 | const electron = require('electron'); 30 | const app = electron.app || (electron.remote && electron.remote.app) || null; 31 | 32 | /** 33 | * @summary Get the default data path 34 | * @function 35 | * @public 36 | * 37 | * @returns {String} default data path 38 | * 39 | * @example 40 | * const defaultDataPath = utils.getDefaultDataPath() 41 | */ 42 | exports.getDefaultDataPath = function() { 43 | if (!app) { 44 | return null; 45 | } 46 | 47 | return path.join(app.getPath('userData'), 'storage'); 48 | }; 49 | 50 | /** 51 | * @summary The current data path 52 | * @type {String} 53 | */ 54 | var currentDataPath; 55 | 56 | /** 57 | * @summary Set default data path 58 | * @function 59 | * @public 60 | * 61 | * @param {String} directory - directory 62 | * 63 | * @example 64 | * const os = require('os'); 65 | * utils.setDataPath(os.tmpdir()); 66 | */ 67 | exports.setDataPath = function(directory) { 68 | if (_.isNil(directory)) { 69 | currentDataPath = undefined; 70 | return; 71 | } 72 | 73 | if (!path.isAbsolute(directory)) { 74 | throw new Error('The user data path should be an absolute directory'); 75 | } 76 | 77 | currentDataPath = path.normalize(directory); 78 | }; 79 | 80 | /** 81 | * @summary Get data path 82 | * @function 83 | * @public 84 | * 85 | * @returns {Strings} data path 86 | * 87 | * @example 88 | * const dataPath = utils.getDataPath(); 89 | * console.log(dataPath); 90 | */ 91 | exports.getDataPath = function() { 92 | return currentDataPath || exports.getDefaultDataPath(); 93 | }; 94 | 95 | /** 96 | * @summary Get storage file name for a key 97 | * @function 98 | * @public 99 | * 100 | * @param {String} key - key 101 | * @param {Object} [options] - options 102 | * @param {String} [options.dataPath] - custom data path 103 | * @returns {String} file name 104 | * 105 | * @example 106 | * let fileName = utils.getFileName('foo'); 107 | * console.log(fileName); 108 | */ 109 | exports.getFileName = function(key, options) { 110 | options = options || {}; 111 | 112 | if (!key) { 113 | throw new Error('Missing key'); 114 | } 115 | 116 | if (!_.isString(key) || key.trim().length === 0) { 117 | throw new Error('Invalid key'); 118 | } 119 | 120 | // Trick to prevent adding the `.json` twice 121 | // if the key already contains it. 122 | const keyFileName = path.basename(key, '.json') + '.json'; 123 | 124 | // Prevent ENOENT and other similar errors when using 125 | // reserved characters in Windows filenames. 126 | // See: https://en.wikipedia.org/wiki/Filename#Reserved%5Fcharacters%5Fand%5Fwords 127 | const escapedFileName = encodeURIComponent(keyFileName) 128 | .replace(/\*/g, '-').replace(/%20/g, ' '); 129 | 130 | const dataPath = options.dataPath || exports.getDataPath(); 131 | if (!dataPath) { 132 | throw new Error('You must explicitly set a data path'); 133 | } 134 | 135 | return path.join(dataPath, escapedFileName); 136 | }; 137 | 138 | /** 139 | * @summary Get the lock file out of a file name 140 | * @function 141 | * @public 142 | * 143 | * @param {String} fileName - file name 144 | * @returns {String} lock file name 145 | * 146 | * @example 147 | * let lockFileName = utils.getLockFileName('foo'); 148 | * console.log(lockFileName); 149 | */ 150 | exports.getLockFileName = function(fileName) { 151 | return fileName + '.lock'; 152 | }; 153 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-json-storage", 3 | "version": "4.6.0", 4 | "description": "Easily write and read user settings in Electron apps", 5 | "main": "lib/storage.js", 6 | "homepage": "https://github.com/electron-userland/electron-json-storage", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/electron-userland/electron-json-storage.git" 10 | }, 11 | "directories": { 12 | "test": "tests" 13 | }, 14 | "scripts": { 15 | "test": "npm run lint && electron-mocha --recursive tests -R spec && electron-mocha --renderer --recursive tests -R spec", 16 | "lint": "jshint --config .jshintrc --reporter unix lib tests stress", 17 | "readme": "jsdoc2md --template doc/README.hbs lib/storage.js > README.md" 18 | }, 19 | "keywords": [ 20 | "electron", 21 | "json", 22 | "storage", 23 | "user", 24 | "app", 25 | "data" 26 | ], 27 | "author": "Juan Cruz Viotti ", 28 | "license": "MIT", 29 | "devDependencies": { 30 | "chai": "^4.2.0", 31 | "electron": "^10.1.3", 32 | "electron-mocha": "^9.2.0", 33 | "jsdoc-to-markdown": "^6.0.1", 34 | "jshint": "^2.9.1", 35 | "tmp": "0.0.31" 36 | }, 37 | "dependencies": { 38 | "async": "^2.0.0", 39 | "lockfile": "^1.0.4", 40 | "lodash": "^4.0.1", 41 | "mkdirp": "^0.5.1", 42 | "rimraf": "^2.5.1", 43 | "write-file-atomic": "^2.4.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /stress/process.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2016 Juan Cruz Viotti. https://github.com/jviotti 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | 'use strict'; 26 | 27 | const async = require('async'); 28 | const storage = require('..'); 29 | const value = process.argv[2]; 30 | 31 | const retry = function(callback, times) { 32 | if (times === 0) { 33 | return callback(); 34 | } 35 | 36 | async.waterfall([ 37 | function(next) { 38 | storage.set('foo', { 39 | value: value 40 | }, next); 41 | }, 42 | function(next) { 43 | storage.get('foo', next); 44 | } 45 | ], function(error, result) { 46 | if (error) { 47 | return callback(error); 48 | } 49 | 50 | console.log(process.pid, times, result); 51 | retry(callback, times - 1); 52 | }); 53 | }; 54 | 55 | retry(function(error) { 56 | if (error) { 57 | console.error(error); 58 | process.exit(1); 59 | } else { 60 | process.exit(0); 61 | } 62 | }, 2000); 63 | -------------------------------------------------------------------------------- /stress/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | ./node_modules/.bin/electron stress/process.js xxx & PID1=$! 6 | ./node_modules/.bin/electron stress/process.js xxxxxx & PID2=$! 7 | wait $PID1 8 | wait $PID2 9 | -------------------------------------------------------------------------------- /tests/storage.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2016 Juan Cruz Viotti. https://github.com/jviotti 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | 'use strict'; 26 | 27 | const electron = require('electron'); 28 | const _ = require('lodash'); 29 | const async = require('async'); 30 | const fs = require('fs'); 31 | const path = require('path'); 32 | const os = require('os'); 33 | const tmp = require('tmp'); 34 | const rimraf = require('rimraf'); 35 | const mkdirp = require('mkdirp'); 36 | const chai = require('chai'); 37 | const storage = require('../lib/storage'); 38 | const utils = require('../lib/utils'); 39 | const app = electron.app || electron.remote.app; 40 | 41 | describe('Electron JSON Storage', function() { 42 | 43 | this.timeout(100000); 44 | 45 | // Ensure each test case is always ran in a clean state 46 | beforeEach(function(done) { 47 | storage.setDataPath(utils.getDefaultDataPath()); 48 | storage.clear(done); 49 | }); 50 | 51 | describe('stress testing', function() { 52 | 53 | const cases = _.times(1000, () => { 54 | return Math.floor(Math.random() * 100000); 55 | }); 56 | 57 | it('should survive serial stress testing', function(done) { 58 | async.eachSeries(cases, function(number, callback) { 59 | async.waterfall([ 60 | _.partial(storage.set, 'foo', { value: number }), 61 | _.partial(storage.get, 'foo'), 62 | function(data, next) { 63 | chai.expect(data.value).to.equal(number); 64 | next(); 65 | } 66 | ], callback); 67 | }, done); 68 | }); 69 | 70 | it('should survive serial stress testing with validate=true', function(done) { 71 | async.eachSeries(_.times(100, () => { 72 | return Math.floor(Math.random() * 100000); 73 | }), function(number, callback) { 74 | async.waterfall([ 75 | _.partial(storage.set, 'foo', { value: number }, { validate: true }), 76 | _.partial(storage.get, 'foo'), 77 | function(data, next) { 78 | chai.expect(data.value).to.equal(number); 79 | next(); 80 | } 81 | ], callback); 82 | }, done); 83 | }); 84 | 85 | it('should survive parallel stress testing', function(done) { 86 | async.eachSeries(cases, function(number, callback) { 87 | async.parallel([ 88 | _.partial(storage.set, 'foo', { value: [number] }), 89 | _.partial(storage.set, 'foo', { value: [number, number] }) 90 | ], function() { 91 | storage.get('foo', function(error, data) { 92 | chai.expect(error).to.not.exist; 93 | callback(); 94 | }); 95 | }); 96 | }, done); 97 | }); 98 | 99 | }); 100 | 101 | describe('.getDefaultDataPath()', function() { 102 | 103 | it('should be a string', function() { 104 | chai.expect(_.isString(storage.getDefaultDataPath())).to.be.true; 105 | }); 106 | 107 | it('should be an absolute path', function() { 108 | chai.expect(path.isAbsolute(storage.getDefaultDataPath())).to.be.true; 109 | }); 110 | 111 | }); 112 | 113 | describe('.setDataPath()', function() { 114 | 115 | it('should be able to set a custom data path', function() { 116 | const newDataPath = os.tmpdir(); 117 | storage.setDataPath(newDataPath); 118 | const dataPath = storage.getDataPath(); 119 | chai.expect(dataPath).to.equal(newDataPath); 120 | }); 121 | 122 | it('should set the default path if no argument', function() { 123 | storage.setDataPath(); 124 | const dataPath = app.getPath('userData'); 125 | chai.expect(storage.getDataPath().indexOf(dataPath)).to.equal(0); 126 | }); 127 | 128 | it('should throw given a relative path', function() { 129 | chai.expect(function() { 130 | storage.setDataPath('foo'); 131 | }).to.throw('The user data path should be an absolute directory'); 132 | }); 133 | 134 | }); 135 | 136 | describe('.getDataPath()', function() { 137 | 138 | it('should initially return the default data path', function() { 139 | const dataPath = storage.getDataPath(); 140 | chai.expect(dataPath).to.equal(utils.getDefaultDataPath()); 141 | }); 142 | 143 | it('should be able to return new data paths', function() { 144 | const newDataPath = os.tmpdir(); 145 | storage.setDataPath(newDataPath); 146 | const dataPath = storage.getDataPath(); 147 | chai.expect(dataPath).to.equal(newDataPath); 148 | }); 149 | 150 | }); 151 | 152 | describe('.get()', function() { 153 | 154 | it('should yield an error if no key', function(done) { 155 | storage.get(null, function(error, data) { 156 | chai.expect(error).to.be.an.instanceof(Error); 157 | chai.expect(error.message).to.equal('Missing key'); 158 | chai.expect(data).to.not.exist; 159 | done(); 160 | }); 161 | }); 162 | 163 | it('should yield an error if no key (sync)', function(done) { 164 | chai.expect(() => { 165 | storage.getSync(null); 166 | }).to.throw('Missing key'); 167 | done(); 168 | }); 169 | 170 | it('should yield an error if key is not a string', function(done) { 171 | storage.get(123, function(error, data) { 172 | chai.expect(error).to.be.an.instanceof(Error); 173 | chai.expect(error.message).to.equal('Invalid key'); 174 | chai.expect(data).to.not.exist; 175 | done(); 176 | }); 177 | }); 178 | 179 | it('should yield an error if key is not a string (sync)', function(done) { 180 | chai.expect(() => { 181 | storage.getSync(123); 182 | }).to.throw('Invalid key'); 183 | done(); 184 | }); 185 | 186 | it('should yield an error if key is a blank string', function(done) { 187 | storage.get(' ', function(error, data) { 188 | chai.expect(error).to.be.an.instanceof(Error); 189 | chai.expect(error.message).to.equal('Invalid key'); 190 | chai.expect(data).to.not.exist; 191 | done(); 192 | }); 193 | }); 194 | 195 | it('should yield an error if key is a blank string (sync)', function(done) { 196 | chai.expect(() => { 197 | storage.getSync(' '); 198 | }).to.throw('Invalid key'); 199 | done(); 200 | }); 201 | 202 | describe('given the user data path does not exist', function() { 203 | 204 | beforeEach(function(done) { 205 | rimraf(storage.getDataPath(), done); 206 | }); 207 | 208 | afterEach(function(done) { 209 | mkdirp(storage.getDataPath(), done); 210 | }); 211 | 212 | it('should return an empty object for any key', function(done) { 213 | storage.get('foobarbaz', function(error, data) { 214 | chai.expect(error).to.not.exist; 215 | chai.expect(data).to.deep.equal({}); 216 | done(); 217 | }); 218 | }); 219 | 220 | it('should return an empty object for any key (sync)', function(done) { 221 | var data = storage.getSync('foobarbaz'); 222 | chai.expect(data).to.deep.equal({}); 223 | done(); 224 | }); 225 | 226 | }); 227 | 228 | describe('given the same key stored in multiple data paths', function(done) { 229 | 230 | beforeEach(function(done) { 231 | this.newDataPath = path.join(os.tmpdir(), 'electron-json-storage'); 232 | const self = this; 233 | 234 | async.waterfall([ 235 | function(callback) { 236 | storage.setDataPath(self.newDataPath); 237 | callback(); 238 | }, 239 | function(callback) { 240 | storage.set('foo', { location: 'new' }, callback); 241 | }, 242 | function(callback) { 243 | storage.setDataPath(utils.getDefaultDataPath()); 244 | callback(); 245 | }, 246 | function(callback) { 247 | storage.set('foo', { location: 'default' }, callback); 248 | } 249 | ], done); 250 | }); 251 | 252 | it('should initially return the key in the default location', function(done) { 253 | storage.get('foo', function(error, data) { 254 | chai.expect(error).to.not.exist; 255 | chai.expect(data).to.deep.equal({ 256 | location: 'default' 257 | }); 258 | 259 | done(); 260 | }); 261 | }); 262 | 263 | it('should initially return the key in the default location (sync)', function(done) { 264 | var data = storage.getSync('foo'); 265 | chai.expect(data).to.deep.equal({ 266 | location: 'default' 267 | }); 268 | 269 | done(); 270 | }); 271 | 272 | it('should return the new value given the right data path', function(done) { 273 | storage.setDataPath(this.newDataPath); 274 | storage.get('foo', function(error, data) { 275 | chai.expect(error).to.not.exist; 276 | chai.expect(data).to.deep.equal({ 277 | location: 'new' 278 | }); 279 | 280 | done(); 281 | }); 282 | }); 283 | 284 | it('should return the new value given the right data path (sync)', function(done) { 285 | storage.setDataPath(this.newDataPath); 286 | var data = storage.getSync('foo'); 287 | chai.expect(data).to.deep.equal({ 288 | location: 'new' 289 | }); 290 | 291 | done(); 292 | }); 293 | 294 | it('should return nothing given the wrong data path', function(done) { 295 | if (os.platform() === 'win32') { 296 | storage.setDataPath('C:\\tmp\\electron-json-storage'); 297 | } else { 298 | storage.setDataPath('/tmp/electron-json-storage'); 299 | } 300 | 301 | async.waterfall([ 302 | storage.clear, 303 | function(callback) { 304 | storage.get('foo', callback); 305 | } 306 | ], function(error, data) { 307 | chai.expect(error).to.not.exist; 308 | chai.expect(data).to.deep.equal({}); 309 | done(); 310 | }); 311 | }); 312 | 313 | it('should return nothing given the wrong data path (sync)', function(done) { 314 | if (os.platform() === 'win32') { 315 | storage.setDataPath('C:\\tmp\\electron-json-storage'); 316 | } else { 317 | storage.setDataPath('/tmp/electron-json-storage'); 318 | } 319 | 320 | async.waterfall([ 321 | storage.clear, 322 | function(callback) { 323 | callback(null, storage.getSync('foo')); 324 | } 325 | ], function(error, data) { 326 | chai.expect(error).to.not.exist; 327 | chai.expect(data).to.deep.equal({}); 328 | done(); 329 | }); 330 | }); 331 | }); 332 | 333 | describe('given stored keys with a colon', function() { 334 | 335 | beforeEach(function(done) { 336 | async.parallel([ 337 | _.partial(storage.set, 'foo', { name: 'foo' }), 338 | _.partial(storage.set, 'bar:colon', { name: 'bar' }) 339 | ], done); 340 | }); 341 | 342 | it('should return all stored keys', function(done) { 343 | storage.getAll(function(error, data) { 344 | chai.expect(error).to.not.exist; 345 | chai.expect(data).to.deep.equal({ 346 | foo: { name: 'foo' }, 347 | 'bar:colon': { name: 'bar' } 348 | }); 349 | done(); 350 | }); 351 | }); 352 | 353 | }); 354 | 355 | describe('given stored settings', function() { 356 | 357 | beforeEach(function(done) { 358 | storage.set('foo', { data: 'hello world' }, done); 359 | }); 360 | 361 | it('should yield the data', function(done) { 362 | storage.get('foo', function(error, data) { 363 | chai.expect(error).to.not.exist; 364 | chai.expect(data).to.deep.equal({ data: 'hello world' }); 365 | done(); 366 | }); 367 | }); 368 | 369 | it('should yield the data (sync)', function(done) { 370 | var data = storage.getSync('foo'); 371 | chai.expect(data).to.deep.equal({ data: 'hello world' }); 372 | done(); 373 | }); 374 | 375 | it('should yield the data if explicitly passing the extension', function(done) { 376 | storage.get('foo.json', function(error, data) { 377 | chai.expect(error).to.not.exist; 378 | chai.expect(data).to.deep.equal({ data: 'hello world' }); 379 | done(); 380 | }); 381 | }); 382 | 383 | it('should yield the data if explicitly passing the extension (sync)', function(done) { 384 | var data = storage.getSync('foo.json'); 385 | chai.expect(data).to.deep.equal({ data: 'hello world' }); 386 | done(); 387 | }); 388 | 389 | it('should yield an empty object given an incorrect key', function(done) { 390 | storage.get('foobarbaz', function(error, data) { 391 | chai.expect(error).to.not.exist; 392 | chai.expect(data).to.deep.equal({}); 393 | done(); 394 | }); 395 | }); 396 | 397 | it('should yield an empty object given an incorrect key (sync)', function(done) { 398 | var data = storage.getSync('foobarbaz'); 399 | chai.expect(data).to.deep.equal({}); 400 | done(); 401 | }); 402 | 403 | }); 404 | 405 | describe('given invalid stored JSON', function() { 406 | 407 | beforeEach(function(done) { 408 | const fileName = utils.getFileName('foo'); 409 | 410 | // Using fs directly since storage.set() 411 | // contains logic to prevent invalid JSON 412 | // from being written at all 413 | return fs.writeFile(fileName, 'Foo{bar}123', done); 414 | 415 | }); 416 | 417 | it('should yield an error', function(done) { 418 | storage.get('foo', function(error, data) { 419 | chai.expect(error).to.be.an.instanceof(Error); 420 | chai.expect(error.message).to.equal('Invalid data: Foo{bar}123'); 421 | chai.expect(data).to.not.exist; 422 | done(); 423 | }); 424 | }); 425 | 426 | it('should yield an error (sync)', function(done) { 427 | chai.expect(() => { 428 | return storage.getSync('foo'); 429 | }).to.throw('Invalid data: Foo{bar}123'); 430 | done(); 431 | }); 432 | 433 | }); 434 | 435 | describe('given a non-existent user data path', function() { 436 | 437 | beforeEach(function() { 438 | this.oldUserData = app.getPath('userData'); 439 | app.setPath('userData', tmp.tmpNameSync()); 440 | }); 441 | 442 | afterEach(function() { 443 | app.setPath('userData', this.oldUserData); 444 | }); 445 | 446 | it('should return an empty object for any key', function(done) { 447 | async.waterfall([ 448 | function(callback) { 449 | storage.get('foo', callback); 450 | }, 451 | ], function(error, result) { 452 | chai.expect(error).to.not.exist; 453 | chai.expect(result).to.deep.equal({}); 454 | done(); 455 | }); 456 | }); 457 | 458 | it('should return an empty object for any key (sync)', function(done) { 459 | var result = storage.getSync('foo'); 460 | chai.expect(result).to.deep.equal({}); 461 | done(); 462 | }); 463 | 464 | }); 465 | 466 | }); 467 | 468 | describe('.getMany()', function() { 469 | 470 | describe('given many stored keys in a custom data path', function() { 471 | 472 | beforeEach(function(done) { 473 | this.dataPath = os.tmpdir(); 474 | async.parallel([ 475 | _.partial(storage.set, 'foo', { name: 'foo' }, { dataPath: this.dataPath }), 476 | _.partial(storage.set, 'bar', { name: 'bar' }, { dataPath: this.dataPath }), 477 | _.partial(storage.set, 'baz', { name: 'baz' }, { dataPath: this.dataPath }) 478 | ], done); 479 | }); 480 | 481 | it('should return nothing given the wrong data path', function(done) { 482 | storage.getMany([ 'foo', 'baz' ], function(error, data) { 483 | chai.expect(error).to.not.exist; 484 | chai.expect(data).to.deep.equal({ 485 | foo: {}, 486 | baz: {} 487 | }); 488 | done(); 489 | }); 490 | }); 491 | 492 | it('should return the values given the correct data path', function(done) { 493 | storage.getMany([ 'foo', 'baz' ], { 494 | dataPath: this.dataPath 495 | }, function(error, data) { 496 | chai.expect(error).to.not.exist; 497 | chai.expect(data).to.deep.equal({ 498 | foo: { name: 'foo' }, 499 | baz: { name: 'baz' } 500 | }); 501 | done(); 502 | }); 503 | }); 504 | 505 | }); 506 | 507 | describe('given many stored keys', function() { 508 | 509 | beforeEach(function(done) { 510 | async.parallel([ 511 | _.partial(storage.set, 'foo', { name: 'foo' }), 512 | _.partial(storage.set, 'bar', { name: 'bar' }), 513 | _.partial(storage.set, 'baz', { name: 'baz' }) 514 | ], done); 515 | }); 516 | 517 | it('should return an empty object if no passed keys', function(done) { 518 | storage.getMany([], function(error, data) { 519 | chai.expect(error).to.not.exist; 520 | chai.expect(data).to.deep.equal({}); 521 | done(); 522 | }); 523 | }); 524 | 525 | it('should read the passed keys', function(done) { 526 | storage.getMany([ 'foo', 'baz' ], function(error, data) { 527 | chai.expect(error).to.not.exist; 528 | chai.expect(data).to.deep.equal({ 529 | foo: { name: 'foo' }, 530 | baz: { name: 'baz' } 531 | }); 532 | done(); 533 | }); 534 | }); 535 | 536 | it('should be able to read a single key', function(done) { 537 | storage.getMany([ 'foo' ], function(error, data) { 538 | chai.expect(error).to.not.exist; 539 | chai.expect(data).to.deep.equal({ 540 | foo: { name: 'foo' } 541 | }); 542 | done(); 543 | }); 544 | }); 545 | 546 | it('should return empty objects for missing keys', function(done) { 547 | storage.getMany([ 'foo', 'hello' ], function(error, data) { 548 | chai.expect(error).to.not.exist; 549 | chai.expect(data).to.deep.equal({ 550 | foo: { name: 'foo' }, 551 | hello: {} 552 | }); 553 | done(); 554 | }); 555 | }); 556 | 557 | }); 558 | 559 | }); 560 | 561 | describe('.getAll()', function() { 562 | 563 | describe('given the user data path does not exist', function() { 564 | 565 | beforeEach(function(done) { 566 | rimraf(storage.getDataPath(), done); 567 | }); 568 | 569 | afterEach(function(done) { 570 | mkdirp(storage.getDataPath(), done); 571 | }); 572 | 573 | it('should return an empty object', function(done) { 574 | storage.getAll(function(error, data) { 575 | chai.expect(error).to.not.exist; 576 | chai.expect(data).to.deep.equal({}); 577 | done(); 578 | }); 579 | }); 580 | 581 | }); 582 | 583 | describe('given no stored keys', function() { 584 | 585 | beforeEach(storage.clear); 586 | 587 | it('should return an empty object', function(done) { 588 | storage.getAll(function(error, data) { 589 | chai.expect(error).to.not.exist; 590 | chai.expect(data).to.deep.equal({}); 591 | done(); 592 | }); 593 | }); 594 | 595 | }); 596 | 597 | describe('given many stored keys', function() { 598 | 599 | beforeEach(function(done) { 600 | async.parallel([ 601 | _.partial(storage.set, 'foo', { name: 'foo' }), 602 | _.partial(storage.set, 'bar', { name: 'bar' }), 603 | _.partial(storage.set, 'baz', { name: 'baz' }) 604 | ], done); 605 | }); 606 | 607 | it('should return all stored keys', function(done) { 608 | storage.getAll(function(error, data) { 609 | chai.expect(error).to.not.exist; 610 | chai.expect(data).to.deep.equal({ 611 | foo: { name: 'foo' }, 612 | bar: { name: 'bar' }, 613 | baz: { name: 'baz' } 614 | }); 615 | done(); 616 | }); 617 | }); 618 | 619 | }); 620 | 621 | describe('given many stored keys in different locations', function() { 622 | 623 | beforeEach(function(done) { 624 | this.dataPath = path.join(os.tmpdir(), 'hello'); 625 | 626 | async.parallel([ 627 | _.partial(storage.set, 'foo', { name: 'foo' }), 628 | _.partial(storage.set, 'bar', { name: 'bar' }, { 629 | dataPath: this.dataPath 630 | }), 631 | _.partial(storage.set, 'baz', { name: 'baz' }) 632 | ], done); 633 | }); 634 | 635 | it('should return all stored keys in the default location', function(done) { 636 | storage.getAll(function(error, data) { 637 | chai.expect(error).to.not.exist; 638 | chai.expect(data).to.deep.equal({ 639 | foo: { name: 'foo' }, 640 | baz: { name: 'baz' } 641 | }); 642 | done(); 643 | }); 644 | }); 645 | 646 | it('should return all stored keys in a custom location', function(done) { 647 | storage.getAll({ 648 | dataPath: this.dataPath 649 | }, function(error, data) { 650 | chai.expect(error).to.not.exist; 651 | chai.expect(data).to.deep.equal({ 652 | bar: { name: 'bar' } 653 | }); 654 | done(); 655 | }); 656 | }); 657 | 658 | }); 659 | 660 | describe('given many stored keys in different data directories', function() { 661 | 662 | beforeEach(function(done) { 663 | this.newDataPath = path.join(os.tmpdir(), 'electron-json-storage'); 664 | const self = this; 665 | 666 | async.parallel([ 667 | function(callback) { 668 | storage.setDataPath(self.newDataPath); 669 | callback(); 670 | }, 671 | function(callback) { 672 | storage.set('foo', { name: 'foo' }, callback); 673 | }, 674 | function(callback) { 675 | storage.set('bar', { name: 'bar' }, callback); 676 | }, 677 | function(callback) { 678 | storage.setDataPath(utils.getDefaultDataPath()); 679 | callback(); 680 | }, 681 | function(callback) { 682 | storage.set('baz', { name: 'baz' }, callback); 683 | } 684 | ], done); 685 | }); 686 | 687 | it('should return all stored keys depending on the data path', function(done) { 688 | storage.setDataPath(this.newDataPath); 689 | 690 | async.waterfall([ 691 | storage.getAll, 692 | function(keys, callback) { 693 | chai.expect(keys).to.deep.equal({ 694 | foo: { name: 'foo' }, 695 | bar: { name: 'bar' } 696 | }); 697 | 698 | callback(); 699 | }, 700 | function(callback) { 701 | storage.setDataPath(utils.getDefaultDataPath()); 702 | callback(); 703 | }, 704 | storage.getAll, 705 | function(keys, callback) { 706 | chai.expect(keys).to.deep.equal({ 707 | baz: { name: 'baz' } 708 | }); 709 | 710 | callback(); 711 | }, 712 | ], done); 713 | }); 714 | 715 | }); 716 | 717 | }); 718 | 719 | describe('.set()', function() { 720 | 721 | it('should yield an error if no key', function(done) { 722 | storage.set(null, { foo: 'bar' }, function(error) { 723 | chai.expect(error).to.be.an.instanceof(Error); 724 | chai.expect(error.message).to.equal('Missing key'); 725 | done(); 726 | }); 727 | }); 728 | 729 | it('should yield an error if no key (sync)', function(done) { 730 | chai.expect(() => { 731 | storage.setSync(null, { foo: 'bar' }); 732 | }).to.throw('Missing key'); 733 | done(); 734 | }); 735 | 736 | it('should yield an error if key is not a string', function(done) { 737 | storage.set(123, { foo: 'bar' }, function(error) { 738 | chai.expect(error).to.be.an.instanceof(Error); 739 | chai.expect(error.message).to.equal('Invalid key'); 740 | done(); 741 | }); 742 | }); 743 | 744 | it('should yield an error if key is not a string (sync)', function(done) { 745 | chai.expect(() => { 746 | storage.setSync(123, { foo: 'bar' }); 747 | }).to.throw('Invalid key'); 748 | done(); 749 | }); 750 | 751 | it('should yield an error if key is a blank string', function(done) { 752 | storage.set(' ', { foo: 'bar' }, function(error) { 753 | chai.expect(error).to.be.an.instanceof(Error); 754 | chai.expect(error.message).to.equal('Invalid key'); 755 | done(); 756 | }); 757 | }); 758 | 759 | it('should yield an error if key is a blank string (sync)', function(done) { 760 | chai.expect(() => { 761 | storage.setSync(' ', { foo: 'bar' }); 762 | }).to.throw('Invalid key'); 763 | done(); 764 | }); 765 | 766 | it('should yield an error if data is not a valid JSON object', function(done) { 767 | storage.set('foo', _.noop, function(error) { 768 | chai.expect(error).to.be.an.instanceof(Error); 769 | chai.expect(error.message).to.equal('Invalid JSON data'); 770 | done(); 771 | }); 772 | }); 773 | 774 | it('should yield an error if data is not a valid JSON object (sync)', function(done) { 775 | chai.expect(() => { 776 | storage.setSync('foo', _.noop); 777 | }).to.throw('Invalid JSON data'); 778 | done(); 779 | }); 780 | 781 | it('should be able to store a valid JSON object in a file with a colon', function(done) { 782 | async.waterfall([ 783 | function(callback) { 784 | storage.set('test:value', { foo: 'bar' }, callback); 785 | }, 786 | function(callback) { 787 | storage.get('test:value', callback); 788 | } 789 | ], function(error, data) { 790 | chai.expect(error).to.not.exist; 791 | chai.expect(data).to.deep.equal({ foo: 'bar' }); 792 | done(); 793 | }); 794 | }); 795 | 796 | it('should be able to store a valid JSON object in a file with a colon (sync)', function(done) { 797 | async.waterfall([ 798 | function(callback) { 799 | storage.setSync('test:value', { foo: 'bar' }); 800 | callback(); 801 | }, 802 | function(callback) { 803 | callback(null, storage.getSync('test:value')); 804 | } 805 | ], function(error, data) { 806 | chai.expect(error).to.not.exist; 807 | chai.expect(data).to.deep.equal({ foo: 'bar' }); 808 | done(); 809 | }); 810 | }); 811 | 812 | it('should be able to store a valid JSON object', function(done) { 813 | async.waterfall([ 814 | function(callback) { 815 | storage.set('foo', { foo: 'baz' }, callback); 816 | }, 817 | function(callback) { 818 | storage.get('foo', callback); 819 | } 820 | ], function(error, data) { 821 | chai.expect(error).to.not.exist; 822 | chai.expect(data).to.deep.equal({ foo: 'baz' }); 823 | done(); 824 | }); 825 | }); 826 | 827 | it('should be able to store a valid JSON object (sync)', function(done) { 828 | async.waterfall([ 829 | function(callback) { 830 | storage.setSync('foo', { foo: 'baz' }); 831 | callback(); 832 | }, 833 | function(callback) { 834 | callback(null, storage.getSync('foo')); 835 | } 836 | ], function(error, data) { 837 | chai.expect(error).to.not.exist; 838 | chai.expect(data).to.deep.equal({ foo: 'baz' }); 839 | done(); 840 | }); 841 | }); 842 | 843 | it('should minify JSON documents by default', function(done) { 844 | async.waterfall([ 845 | function(callback) { 846 | storage.set('foo', { foo: 'baz' }, callback); 847 | }, 848 | function(callback) { 849 | fs.readFile(utils.getFileName('foo'), { 850 | encoding: 'utf8' 851 | }, callback); 852 | } 853 | ], function(error, data) { 854 | chai.expect(error).to.not.exist; 855 | chai.expect(data).to.deep.equal("{\"foo\":\"baz\"}"); 856 | done(); 857 | }); 858 | }); 859 | 860 | it('should minify JSON documents by default (sync)', function(done) { 861 | async.waterfall([ 862 | function(callback) { 863 | storage.setSync('foo', { foo: 'baz' }); 864 | callback(); 865 | }, 866 | function(callback) { 867 | callback(null, fs.readFileSync(utils.getFileName('foo'), { 868 | encoding: 'utf8' 869 | })); 870 | } 871 | ], function(error, data) { 872 | chai.expect(error).to.not.exist; 873 | chai.expect(data).to.deep.equal("{\"foo\":\"baz\"}"); 874 | done(); 875 | }); 876 | }); 877 | 878 | it('should minify JSON documents when setting prettyPrinting=false', function(done) { 879 | async.waterfall([ 880 | function(callback) { 881 | storage.set('foo', { foo: 'baz' }, { prettyPrinting: false }, callback); 882 | }, 883 | function(callback) { 884 | fs.readFile(utils.getFileName('foo'), { 885 | encoding: 'utf8' 886 | }, callback); 887 | } 888 | ], function(error, data) { 889 | chai.expect(error).to.not.exist; 890 | chai.expect(data).to.deep.equal("{\"foo\":\"baz\"}"); 891 | done(); 892 | }); 893 | }); 894 | 895 | it('should minify JSON documents when setting prettyPrinting=false (sync)', function(done) { 896 | async.waterfall([ 897 | function(callback) { 898 | storage.setSync('foo', { foo: 'baz' }, { prettyPrinting: false }); 899 | callback(); 900 | }, 901 | function(callback) { 902 | callback(null, fs.readFileSync(utils.getFileName('foo'), { 903 | encoding: 'utf8' 904 | })); 905 | } 906 | ], function(error, data) { 907 | chai.expect(error).to.not.exist; 908 | chai.expect(data).to.deep.equal("{\"foo\":\"baz\"}"); 909 | done(); 910 | }); 911 | }); 912 | 913 | it('should not minify JSON documents when setting prettyPrinting=true', function(done) { 914 | async.waterfall([ 915 | function(callback) { 916 | storage.set('foo', { foo: 'baz' }, { prettyPrinting: true }, callback); 917 | }, 918 | function(callback) { 919 | fs.readFile(utils.getFileName('foo'), { 920 | encoding: 'utf8' 921 | }, callback); 922 | } 923 | ], function(error, data) { 924 | chai.expect(error).to.not.exist; 925 | chai.expect(data).to.deep.equal("{\n \"foo\": \"baz\"\n}"); 926 | done(); 927 | }); 928 | }); 929 | 930 | it('should not minify JSON documents when setting prettyPrinting=true (sync)', function(done) { 931 | async.waterfall([ 932 | function(callback) { 933 | storage.setSync('foo', { foo: 'baz' }, { prettyPrinting: true }); 934 | callback(); 935 | }, 936 | function(callback) { 937 | callback(null, fs.readFileSync(utils.getFileName('foo'), { 938 | encoding: 'utf8' 939 | })); 940 | } 941 | ], function(error, data) { 942 | chai.expect(error).to.not.exist; 943 | chai.expect(data).to.deep.equal("{\n \"foo\": \"baz\"\n}"); 944 | done(); 945 | }); 946 | }); 947 | 948 | it('should be able to store a valid JSON pretty-printed object', function(done) { 949 | async.waterfall([ 950 | function(callback) { 951 | storage.set('foo', { foo: 'baz' }, { prettyPrinting: true }, callback); 952 | }, 953 | function(callback) { 954 | storage.get('foo', callback); 955 | } 956 | ], function(error, data) { 957 | chai.expect(error).to.not.exist; 958 | chai.expect(data).to.deep.equal({ foo: 'baz' }); 959 | done(); 960 | }); 961 | }); 962 | 963 | it('should be able to store a valid JSON pretty-printed object (sync)', function(done) { 964 | async.waterfall([ 965 | function(callback) { 966 | storage.setSync('foo', { foo: 'baz' }, { prettyPrinting: true }); 967 | callback(); 968 | }, 969 | function(callback) { 970 | callback(null, storage.getSync('foo')); 971 | } 972 | ], function(error, data) { 973 | chai.expect(error).to.not.exist; 974 | chai.expect(data).to.deep.equal({ foo: 'baz' }); 975 | done(); 976 | }); 977 | }); 978 | 979 | it('should be able to store a valid JSON object using validate=true', function(done) { 980 | async.waterfall([ 981 | function(callback) { 982 | storage.set('foo', { foo: 'baz' }, { validate: true }, callback); 983 | }, 984 | function(callback) { 985 | storage.get('foo', callback); 986 | } 987 | ], function(error, data) { 988 | chai.expect(error).to.not.exist; 989 | chai.expect(data).to.deep.equal({ foo: 'baz' }); 990 | done(); 991 | }); 992 | }); 993 | 994 | it('should be able to store a valid JSON object using validate=true (sync)', function(done) { 995 | async.waterfall([ 996 | function(callback) { 997 | storage.setSync('foo', { foo: 'baz' }, { validate: true }); 998 | callback(); 999 | }, 1000 | function(callback) { 1001 | callback(null, storage.getSync('foo')); 1002 | } 1003 | ], function(error, data) { 1004 | chai.expect(error).to.not.exist; 1005 | chai.expect(data).to.deep.equal({ foo: 'baz' }); 1006 | done(); 1007 | }); 1008 | }); 1009 | 1010 | it('should be able to store an object to a custom location', function(done) { 1011 | const newDataPath = os.tmpdir(); 1012 | 1013 | async.waterfall([ 1014 | function(callback) { 1015 | storage.set('foo', { foo: 'baz' }, { 1016 | dataPath: newDataPath 1017 | }, callback); 1018 | }, 1019 | function(callback) { 1020 | async.parallel({ 1021 | newDataPath: function(callback) { 1022 | storage.get('foo', { 1023 | dataPath: newDataPath 1024 | }, callback); 1025 | }, 1026 | oldDataPath: function(callback) { 1027 | storage.get('foo', callback); 1028 | } 1029 | }, callback); 1030 | } 1031 | ], function(error, results) { 1032 | chai.expect(error).to.not.exist; 1033 | chai.expect(results.newDataPath).to.deep.equal({ foo: 'baz' }); 1034 | chai.expect(results.oldDataPath).to.deep.equal({}); 1035 | done(); 1036 | }); 1037 | }); 1038 | 1039 | it('should be able to store an object to a custom location (sync)', function(done) { 1040 | const newDataPath = os.tmpdir(); 1041 | 1042 | async.waterfall([ 1043 | function(callback) { 1044 | storage.setSync('foo', { foo: 'baz' }, { 1045 | dataPath: newDataPath 1046 | }); 1047 | callback(); 1048 | }, 1049 | function(callback) { 1050 | async.parallel({ 1051 | newDataPath: function(callback) { 1052 | callback(null, storage.getSync('foo', { 1053 | dataPath: newDataPath 1054 | })); 1055 | }, 1056 | oldDataPath: function(callback) { 1057 | callback(null, storage.getSync('foo')); 1058 | } 1059 | }, callback); 1060 | } 1061 | ], function(error, results) { 1062 | chai.expect(error).to.not.exist; 1063 | chai.expect(results.newDataPath).to.deep.equal({ foo: 'baz' }); 1064 | chai.expect(results.oldDataPath).to.deep.equal({}); 1065 | done(); 1066 | }); 1067 | }); 1068 | 1069 | it('should ignore an explicit json extension', function(done) { 1070 | async.waterfall([ 1071 | function(callback) { 1072 | storage.set('foo.json', { foo: 'baz' }, callback); 1073 | }, 1074 | function(callback) { 1075 | storage.get('foo', callback); 1076 | } 1077 | ], function(error, data) { 1078 | chai.expect(error).to.not.exist; 1079 | chai.expect(data).to.deep.equal({ foo: 'baz' }); 1080 | done(); 1081 | }); 1082 | }); 1083 | 1084 | it('should ignore an explicit json extension (sync)', function(done) { 1085 | async.waterfall([ 1086 | function(callback) { 1087 | storage.setSync('foo.json', { foo: 'baz' }); 1088 | callback(); 1089 | }, 1090 | function(callback) { 1091 | callback(null, storage.getSync('foo')); 1092 | } 1093 | ], function(error, data) { 1094 | chai.expect(error).to.not.exist; 1095 | chai.expect(data).to.deep.equal({ foo: 'baz' }); 1096 | done(); 1097 | }); 1098 | }); 1099 | 1100 | it('should accept special characters as the key name', function(done) { 1101 | const key = 'foo?bar:baz'; 1102 | async.waterfall([ 1103 | function(callback) { 1104 | storage.set(key, { foo: 'baz' }, callback); 1105 | }, 1106 | function(callback) { 1107 | storage.get(key, callback); 1108 | } 1109 | ], function(error, data) { 1110 | chai.expect(error).to.not.exist; 1111 | chai.expect(data).to.deep.equal({ foo: 'baz' }); 1112 | done(); 1113 | }); 1114 | }); 1115 | 1116 | it('should accept special characters as the key name (sync)', function(done) { 1117 | const key = 'foo?bar:baz'; 1118 | async.waterfall([ 1119 | function(callback) { 1120 | storage.setSync(key, { foo: 'baz' }); 1121 | callback(); 1122 | }, 1123 | function(callback) { 1124 | callback(null, storage.getSync(key)); 1125 | } 1126 | ], function(error, data) { 1127 | chai.expect(error).to.not.exist; 1128 | chai.expect(data).to.deep.equal({ foo: 'baz' }); 1129 | done(); 1130 | }); 1131 | }); 1132 | 1133 | it('should accept spaces in the key name', function(done){ 1134 | const key = 'foo bar baz'; 1135 | async.waterfall([ 1136 | function(callback) { 1137 | storage.set(key, { foo: 'baz' }, callback); 1138 | }, 1139 | function(callback) { 1140 | storage.get(key, callback); 1141 | } 1142 | ], function(error, data) { 1143 | chai.expect(error).to.not.exist; 1144 | chai.expect(data).to.deep.equal({ foo: 'baz' }); 1145 | done(); 1146 | }); 1147 | }); 1148 | 1149 | it('should accept spaces in the key name (sync)', function(done){ 1150 | const key = 'foo bar baz'; 1151 | async.waterfall([ 1152 | function(callback) { 1153 | storage.setSync(key, { foo: 'baz' }); 1154 | callback(); 1155 | }, 1156 | function(callback) { 1157 | callback(null, storage.getSync(key)); 1158 | } 1159 | ], function(error, data) { 1160 | chai.expect(error).to.not.exist; 1161 | chai.expect(data).to.deep.equal({ foo: 'baz' }); 1162 | done(); 1163 | }); 1164 | }); 1165 | 1166 | 1167 | describe('given an existing stored key', function() { 1168 | 1169 | beforeEach(function(done) { 1170 | storage.set('foo', { foo: 'bar' }, done); 1171 | }); 1172 | 1173 | it('should be able to override the stored key', function(done) { 1174 | async.waterfall([ 1175 | function(callback) { 1176 | storage.get('foo', callback); 1177 | }, 1178 | function(data, callback) { 1179 | chai.expect(data).to.deep.equal({ foo: 'bar' }); 1180 | storage.set('foo', { foo: 'baz' }, callback); 1181 | }, 1182 | function(callback) { 1183 | storage.get('foo', callback); 1184 | } 1185 | ], function(error, data) { 1186 | chai.expect(error).to.not.exist; 1187 | chai.expect(data).to.deep.equal({ foo: 'baz' }); 1188 | done(); 1189 | }); 1190 | }); 1191 | 1192 | it('should not override the stored key if the passed data is invalid', function(done) { 1193 | storage.set('foo', _.noop, function(error) { 1194 | chai.expect(error).to.be.an.instanceof(Error); 1195 | 1196 | storage.get('foo', function(error, data) { 1197 | chai.expect(error).to.not.exist; 1198 | chai.expect(data).to.deep.equal({ foo: 'bar' }); 1199 | done(); 1200 | }); 1201 | }); 1202 | }); 1203 | 1204 | }); 1205 | 1206 | describe('given a non-existent user data path', function() { 1207 | 1208 | beforeEach(function() { 1209 | this.oldUserData = app.getPath('userData'); 1210 | app.setPath('userData', tmp.tmpNameSync()); 1211 | }); 1212 | 1213 | afterEach(function() { 1214 | app.setPath('userData', this.oldUserData); 1215 | }); 1216 | 1217 | it('should be able to set data', function(done) { 1218 | async.waterfall([ 1219 | function(callback) { 1220 | storage.set('foo', { foo: 'bar' }, callback); 1221 | }, 1222 | function(callback) { 1223 | storage.get('foo', callback); 1224 | }, 1225 | ], function(error, result) { 1226 | chai.expect(error).to.not.exist; 1227 | chai.expect(result).to.deep.equal({ foo: 'bar' }); 1228 | done(); 1229 | }); 1230 | }); 1231 | 1232 | }); 1233 | 1234 | }); 1235 | 1236 | describe('.has()', function() { 1237 | 1238 | it('should yield an error if no key', function(done) { 1239 | storage.has(null, function(error, hasKey) { 1240 | chai.expect(error).to.be.an.instanceof(Error); 1241 | chai.expect(error.message).to.equal('Missing key'); 1242 | chai.expect(hasKey).to.not.exist; 1243 | done(); 1244 | }); 1245 | }); 1246 | 1247 | it('should yield an error if key is not a string', function(done) { 1248 | storage.has(123, function(error, hasKey) { 1249 | chai.expect(error).to.be.an.instanceof(Error); 1250 | chai.expect(error.message).to.equal('Invalid key'); 1251 | chai.expect(hasKey).to.not.exist; 1252 | done(); 1253 | }); 1254 | }); 1255 | 1256 | it('should yield an error if key is a blank string', function(done) { 1257 | storage.has(' ', function(error, hasKey) { 1258 | chai.expect(error).to.be.an.instanceof(Error); 1259 | chai.expect(error.message).to.equal('Invalid key'); 1260 | chai.expect(hasKey).to.not.exist; 1261 | done(); 1262 | }); 1263 | }); 1264 | 1265 | describe('given a stored key in a custom location', function() { 1266 | 1267 | beforeEach(function(done) { 1268 | this.dataPath = os.tmpdir(); 1269 | storage.set('foo', { foo: 'bar' }, { 1270 | dataPath: this.dataPath 1271 | }, done); 1272 | }); 1273 | 1274 | it('should yield false given the default data path', function(done) { 1275 | storage.has('foo', function(error, hasKey) { 1276 | chai.expect(error).to.not.exist; 1277 | chai.expect(hasKey).to.equal(false); 1278 | done(); 1279 | }); 1280 | }); 1281 | 1282 | it('should yield true given the custom data path', function(done) { 1283 | storage.has('foo', { 1284 | dataPath: this.dataPath 1285 | }, function(error, hasKey) { 1286 | chai.expect(error).to.not.exist; 1287 | chai.expect(hasKey).to.equal(true); 1288 | done(); 1289 | }); 1290 | }); 1291 | 1292 | }); 1293 | 1294 | describe('given a stored key', function() { 1295 | 1296 | beforeEach(function(done) { 1297 | storage.set('foo', { foo: 'bar' }, done); 1298 | }); 1299 | 1300 | it('should yield true if the key exists', function(done) { 1301 | storage.has('foo', function(error, hasKey) { 1302 | chai.expect(error).to.not.exist; 1303 | chai.expect(hasKey).to.equal(true); 1304 | done(); 1305 | }); 1306 | }); 1307 | 1308 | it('should yield true if the key has a json extension', function(done) { 1309 | storage.has('foo.json', function(error, hasKey) { 1310 | chai.expect(error).to.not.exist; 1311 | chai.expect(hasKey).to.equal(true); 1312 | done(); 1313 | }); 1314 | }); 1315 | 1316 | it('should yield false if the key does not exist', function(done) { 1317 | storage.has('hello', function(error, hasKey) { 1318 | chai.expect(error).to.not.exist; 1319 | chai.expect(hasKey).to.equal(false); 1320 | done(); 1321 | }); 1322 | }); 1323 | 1324 | }); 1325 | 1326 | }); 1327 | 1328 | describe('.keys()', function() { 1329 | 1330 | describe('given a file name with colons', function() { 1331 | 1332 | beforeEach(function(done) { 1333 | async.waterfall([ 1334 | _.partial(storage.set, 'one', 'foo'), 1335 | _.partial(storage.set, 'two', 'bar'), 1336 | _.partial(storage.set, 'three:colon', 'baz') 1337 | ], done); 1338 | }); 1339 | 1340 | afterEach(function(done) { 1341 | rimraf(storage.getDataPath(), done); 1342 | }); 1343 | 1344 | it('should correctly decode the file names', function(done) { 1345 | storage.keys(function(error, keys) { 1346 | chai.expect(error).to.not.exist; 1347 | chai.expect(keys).to.deep.equal([ 1348 | 'one', 1349 | 'three:colon', 1350 | 'two' 1351 | ]); 1352 | 1353 | done(); 1354 | }); 1355 | 1356 | }); 1357 | 1358 | }); 1359 | 1360 | describe('given keys in a custom location', function() { 1361 | 1362 | beforeEach(function(done) { 1363 | this.dataPath = path.join(os.tmpdir(), 'custom-data-path'); 1364 | async.waterfall([ 1365 | _.partial(storage.set, 'one', 'foo', { dataPath: this.dataPath }), 1366 | _.partial(storage.set, 'two', 'bar', { dataPath: this.dataPath }), 1367 | _.partial(storage.set, 'three', 'baz', { dataPath: this.dataPath }) 1368 | ], done); 1369 | }); 1370 | 1371 | it('should return nothing given the default data path', function(done) { 1372 | storage.keys(function(error, keys) { 1373 | chai.expect(error).to.not.exist; 1374 | chai.expect(keys.length).to.equal(0); 1375 | done(); 1376 | }); 1377 | }); 1378 | 1379 | it('should return the keys given the custom location', function(done) { 1380 | storage.keys({ 1381 | dataPath: this.dataPath 1382 | }, function(error, keys) { 1383 | chai.expect(error).to.not.exist; 1384 | chai.expect(keys.length).to.equal(3); 1385 | chai.expect(_.includes(keys, 'one')).to.be.true; 1386 | chai.expect(_.includes(keys, 'two')).to.be.true; 1387 | chai.expect(_.includes(keys, 'three')).to.be.true; 1388 | done(); 1389 | }); 1390 | }); 1391 | 1392 | }); 1393 | 1394 | describe('given invalid files in the settings directory', function() { 1395 | 1396 | beforeEach(function(done) { 1397 | async.waterfall([ 1398 | _.partial(storage.set, 'one', 'foo'), 1399 | _.partial(storage.set, 'two', 'bar'), 1400 | _.partial(storage.set, 'three', 'baz'), 1401 | _.partial(fs.writeFile, path.join(storage.getDataPath(), '.DS_Store'), 'foo'), 1402 | _.partial(fs.writeFile, path.join(storage.getDataPath(), 'one.json.lock.STALE'), 'foo'), 1403 | _.partial(fs.writeFile, path.join(storage.getDataPath(), 'one.json.lock'), 'foo') 1404 | ], done); 1405 | }); 1406 | 1407 | afterEach(function(done) { 1408 | rimraf(storage.getDataPath(), done); 1409 | }); 1410 | 1411 | it('should only include the json files', function(done) { 1412 | storage.keys(function(error, keys) { 1413 | chai.expect(error).to.not.exist; 1414 | chai.expect(keys.length).to.equal(3); 1415 | chai.expect(_.includes(keys, 'one')).to.be.true; 1416 | chai.expect(_.includes(keys, 'two')).to.be.true; 1417 | chai.expect(_.includes(keys, 'three')).to.be.true; 1418 | done(); 1419 | }); 1420 | }); 1421 | 1422 | }); 1423 | 1424 | it('should yield an empty array if no keys', function(done) { 1425 | storage.keys(function(error, keys) { 1426 | chai.expect(error).to.not.exist; 1427 | chai.expect(keys).to.deep.equal([]); 1428 | done(); 1429 | }); 1430 | }); 1431 | 1432 | it('should yield a single key if there is one saved setting', function(done) { 1433 | async.waterfall([ 1434 | function(callback) { 1435 | storage.set('foo', 'bar', callback); 1436 | }, 1437 | storage.keys, 1438 | ], function(error, keys) { 1439 | chai.expect(error).to.not.exist; 1440 | chai.expect(keys).to.deep.equal([ 'foo' ]); 1441 | done(); 1442 | }); 1443 | }); 1444 | 1445 | it('should ignore the .json extension', function(done) { 1446 | async.waterfall([ 1447 | function(callback) { 1448 | storage.set('foo.json', 'bar', callback); 1449 | }, 1450 | storage.keys, 1451 | ], function(error, keys) { 1452 | chai.expect(error).to.not.exist; 1453 | chai.expect(keys).to.deep.equal([ 'foo' ]); 1454 | done(); 1455 | }); 1456 | }); 1457 | 1458 | it('should only remove the .json extension', function(done) { 1459 | async.waterfall([ 1460 | function(callback) { 1461 | storage.set('foo.data', 'bar', callback); 1462 | }, 1463 | storage.keys, 1464 | ], function(error, keys) { 1465 | chai.expect(error).to.not.exist; 1466 | chai.expect(keys).to.deep.equal([ 'foo.data' ]); 1467 | done(); 1468 | }); 1469 | }); 1470 | 1471 | it('should detect multiple saved settings', function(done) { 1472 | async.waterfall([ 1473 | function(callback) { 1474 | async.parallel([ 1475 | _.partial(storage.set, 'one', 'foo'), 1476 | _.partial(storage.set, 'two', 'bar'), 1477 | _.partial(storage.set, 'three', 'baz') 1478 | ], callback); 1479 | }, 1480 | function(result, callback) { 1481 | storage.keys(callback); 1482 | } 1483 | ], function(error, keys) { 1484 | chai.expect(error).to.not.exist; 1485 | chai.expect(keys).to.deep.equal([ 1486 | 'one', 1487 | 'three', 1488 | 'two' 1489 | ]); 1490 | done(); 1491 | }); 1492 | }); 1493 | 1494 | }); 1495 | 1496 | describe('.remove()', function() { 1497 | 1498 | it('should yield an error if no key', function(done) { 1499 | storage.remove(null, function(error) { 1500 | chai.expect(error).to.be.an.instanceof(Error); 1501 | chai.expect(error.message).to.equal('Missing key'); 1502 | done(); 1503 | }); 1504 | }); 1505 | 1506 | it('should yield an error if key is not a string', function(done) { 1507 | storage.remove(123, function(error) { 1508 | chai.expect(error).to.be.an.instanceof(Error); 1509 | chai.expect(error.message).to.equal('Invalid key'); 1510 | done(); 1511 | }); 1512 | }); 1513 | 1514 | it('should yield an error if key is a blank string', function(done) { 1515 | storage.remove(' ', function(error) { 1516 | chai.expect(error).to.be.an.instanceof(Error); 1517 | chai.expect(error.message).to.equal('Invalid key'); 1518 | done(); 1519 | }); 1520 | }); 1521 | 1522 | describe('given a stored key in a custom location', function() { 1523 | 1524 | beforeEach(function(done) { 1525 | this.dataPath = os.tmpdir(); 1526 | storage.set('foo', { foo: 'bar' }, { dataPath: this.dataPath }, done); 1527 | }); 1528 | 1529 | it('should be able to remove the key', function(done) { 1530 | var options = { 1531 | dataPath: this.dataPath 1532 | }; 1533 | 1534 | async.waterfall([ 1535 | function(callback) { 1536 | storage.has('foo', options, callback); 1537 | }, 1538 | function(hasKey, callback) { 1539 | chai.expect(hasKey).to.be.true; 1540 | storage.remove('foo', options, callback); 1541 | }, 1542 | function(callback) { 1543 | storage.has('foo', options, callback); 1544 | } 1545 | ], function(error, hasKey) { 1546 | chai.expect(error).to.not.exist; 1547 | chai.expect(hasKey).to.be.false; 1548 | done(); 1549 | }); 1550 | }); 1551 | 1552 | }); 1553 | 1554 | describe('given a stored key', function() { 1555 | 1556 | beforeEach(function(done) { 1557 | storage.set('foo', { foo: 'bar' }, done); 1558 | }); 1559 | 1560 | it('should be able to remove the key', function(done) { 1561 | async.waterfall([ 1562 | function(callback) { 1563 | storage.has('foo', callback); 1564 | }, 1565 | function(hasKey, callback) { 1566 | chai.expect(hasKey).to.be.true; 1567 | storage.remove('foo', callback); 1568 | }, 1569 | function(callback) { 1570 | storage.has('foo', callback); 1571 | } 1572 | ], function(error, hasKey) { 1573 | chai.expect(error).to.not.exist; 1574 | chai.expect(hasKey).to.be.false; 1575 | done(); 1576 | }); 1577 | }); 1578 | 1579 | it('should do nothing if the key does not exist', function(done) { 1580 | async.waterfall([ 1581 | function(callback) { 1582 | storage.has('bar', callback); 1583 | }, 1584 | function(hasKey, callback) { 1585 | chai.expect(hasKey).to.be.false; 1586 | storage.remove('bar', callback); 1587 | }, 1588 | function(callback) { 1589 | storage.has('bar', callback); 1590 | } 1591 | ], function(error, hasKey) { 1592 | chai.expect(error).to.not.exist; 1593 | chai.expect(hasKey).to.be.false; 1594 | done(); 1595 | }); 1596 | }); 1597 | 1598 | }); 1599 | 1600 | }); 1601 | 1602 | describe('.clear()', function() { 1603 | 1604 | it('should not yield an error if no keys', function(done) { 1605 | storage.clear(function(error) { 1606 | chai.expect(error).to.not.exist; 1607 | done(); 1608 | }); 1609 | }); 1610 | 1611 | describe('given a stored key in a custom location', function() { 1612 | 1613 | beforeEach(function(done) { 1614 | this.dataPath = os.tmpdir(); 1615 | storage.set('foo', { foo: 'bar' }, { dataPath: this.dataPath }, done); 1616 | }); 1617 | 1618 | it('should clear the key', function(done) { 1619 | var options = { 1620 | dataPath: this.dataPath 1621 | }; 1622 | 1623 | async.waterfall([ 1624 | function(callback) { 1625 | storage.has('foo', options, callback); 1626 | }, 1627 | function(hasKey, callback) { 1628 | chai.expect(hasKey).to.be.true; 1629 | storage.clear(options, callback); 1630 | }, 1631 | function(callback) { 1632 | storage.has('foo', options, callback); 1633 | } 1634 | ], function(error, hasKey) { 1635 | chai.expect(error).to.not.exist; 1636 | chai.expect(hasKey).to.be.false; 1637 | done(); 1638 | }); 1639 | }); 1640 | 1641 | }); 1642 | 1643 | describe('given a stored key', function() { 1644 | 1645 | beforeEach(function(done) { 1646 | storage.set('foo', { foo: 'bar' }, done); 1647 | }); 1648 | 1649 | it('should clear the key', function(done) { 1650 | async.waterfall([ 1651 | function(callback) { 1652 | storage.has('foo', callback); 1653 | }, 1654 | function(hasKey, callback) { 1655 | chai.expect(hasKey).to.be.true; 1656 | storage.clear(callback); 1657 | }, 1658 | function(callback) { 1659 | storage.has('foo', callback); 1660 | } 1661 | ], function(error, hasKey) { 1662 | chai.expect(error).to.not.exist; 1663 | chai.expect(hasKey).to.be.false; 1664 | done(); 1665 | }); 1666 | }); 1667 | 1668 | it('should not delete the user data storage directory', function(done) { 1669 | const isDirectory = function(dir, callback) { 1670 | fs.stat(dir, function(error, stat) { 1671 | if (error) { 1672 | if (error.code === 'ENOENT') { 1673 | return callback(null, false); 1674 | } 1675 | 1676 | return callback(error); 1677 | } 1678 | 1679 | return callback(null, stat.isDirectory()); 1680 | }); 1681 | }; 1682 | 1683 | const userDataPath = storage.getDataPath(); 1684 | 1685 | async.waterfall([ 1686 | _.partial(isDirectory, userDataPath), 1687 | function(directory, callback) { 1688 | chai.expect(directory).to.be.true; 1689 | storage.clear(callback); 1690 | }, 1691 | _.partial(isDirectory, userDataPath) 1692 | ], function(error, directory) { 1693 | chai.expect(error).to.not.exist; 1694 | chai.expect(directory).to.be.true; 1695 | done(); 1696 | }); 1697 | }); 1698 | 1699 | it('should not delete other files inside the user data directory', function(done) { 1700 | const userDataPath = app.getPath('userData'); 1701 | 1702 | async.waterfall([ 1703 | function(callback) { 1704 | async.parallel([ 1705 | _.partial(fs.writeFile, path.join(userDataPath, 'foo'), 'foo'), 1706 | _.partial(fs.writeFile, path.join(userDataPath, 'bar'), 'bar.json') 1707 | ], callback); 1708 | }, 1709 | function(results, callback) { 1710 | storage.clear(callback); 1711 | }, 1712 | function(callback) { 1713 | async.parallel([ 1714 | _.partial(fs.readFile, path.join(userDataPath, 'foo'), { encoding: 'utf8' }), 1715 | _.partial(fs.readFile, path.join(userDataPath, 'bar'), { encoding: 'utf8' }) 1716 | ], callback); 1717 | } 1718 | ], function(error, results) { 1719 | chai.expect(error).to.not.exist; 1720 | chai.expect(results).to.deep.equal([ 'foo', 'bar.json' ]); 1721 | done(); 1722 | }); 1723 | }); 1724 | 1725 | }); 1726 | 1727 | describe('given many stored keys', function() { 1728 | 1729 | beforeEach(function(done) { 1730 | async.parallel([ 1731 | _.partial(storage.set, 'foo', { name: 'foo' }), 1732 | _.partial(storage.set, 'bar', { name: 'bar' }), 1733 | _.partial(storage.set, 'baz', { name: 'baz' }) 1734 | ], done); 1735 | }); 1736 | 1737 | it('should clear all stored keys', function(done) { 1738 | async.waterfall([ 1739 | function(callback) { 1740 | async.parallel({ 1741 | foo: _.partial(storage.has, 'foo'), 1742 | bar: _.partial(storage.has, 'bar'), 1743 | baz: _.partial(storage.has, 'baz') 1744 | }, callback); 1745 | }, 1746 | function(results, callback) { 1747 | chai.expect(results.foo).to.be.true; 1748 | chai.expect(results.bar).to.be.true; 1749 | chai.expect(results.baz).to.be.true; 1750 | 1751 | storage.clear(callback); 1752 | }, 1753 | function(callback) { 1754 | async.parallel({ 1755 | foo: _.partial(storage.has, 'foo'), 1756 | bar: _.partial(storage.has, 'bar'), 1757 | baz: _.partial(storage.has, 'baz') 1758 | }, callback); 1759 | }, 1760 | ], function(error, results) { 1761 | chai.expect(error).to.not.exist; 1762 | chai.expect(results.foo).to.be.false; 1763 | chai.expect(results.bar).to.be.false; 1764 | chai.expect(results.baz).to.be.false; 1765 | done(); 1766 | }); 1767 | }); 1768 | 1769 | }); 1770 | 1771 | }); 1772 | 1773 | }); 1774 | -------------------------------------------------------------------------------- /tests/utils.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2016 Juan Cruz Viotti. https://github.com/jviotti 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | 'use strict'; 26 | 27 | const chai = require('chai'); 28 | const path = require('path'); 29 | const utils = require('../lib/utils'); 30 | const electron = require('electron'); 31 | const os = require('os'); 32 | const app = electron.app || electron.remote.app; 33 | 34 | describe('Utils', function() { 35 | 36 | this.timeout(20000); 37 | 38 | describe('.getDataPath()', function() { 39 | 40 | it('should return an absolute path', function() { 41 | chai.expect(path.isAbsolute(utils.getDataPath())).to.be.true; 42 | }); 43 | 44 | it('should equal the dirname of the path returned by getFileName()', function() { 45 | const fileName = utils.getFileName('foo'); 46 | const userDataPath = utils.getDataPath(); 47 | chai.expect(path.dirname(fileName)).to.equal(userDataPath); 48 | }); 49 | 50 | it('should pick up external changes to the userData path', function() { 51 | utils.setDataPath(undefined); 52 | const oldDataPath = app.getPath('userData'); 53 | chai.expect(utils.getDataPath().indexOf(oldDataPath)).to.equal(0); 54 | const newPath = os.platform() === 'win32' ? 'C:\\foo' : '/foo'; 55 | app.setPath('userData', newPath); 56 | chai.expect(utils.getDataPath().indexOf(newPath)).to.equal(0); 57 | app.setPath('userData', oldDataPath); 58 | }); 59 | 60 | }); 61 | 62 | describe('.setDataPath()', function() { 63 | 64 | beforeEach(function() { 65 | utils.setDataPath(utils.getDefaultDataPath()); 66 | }); 67 | 68 | it('should be able to go back to the default', function() { 69 | utils.setDataPath(path.join(os.tmpdir(), 'foo')); 70 | chai.expect(utils.getDataPath()).to.not.equal(utils.getDefaultDataPath()); 71 | utils.setDataPath(utils.getDefaultDataPath()); 72 | chai.expect(utils.getDataPath()).to.equal(utils.getDefaultDataPath()); 73 | }); 74 | 75 | it('should change the user data path', function() { 76 | const newUserDataPath = path.join(utils.getDataPath(), 'foo' , 'bar'); 77 | utils.setDataPath(newUserDataPath); 78 | chai.expect(utils.getDataPath()).to.equal(newUserDataPath); 79 | }); 80 | 81 | it('should throw if path is not absolute', function() { 82 | chai.expect(function() { 83 | utils.setDataPath('testpath/storage'); 84 | }).to.throw('The user data path should be an absolute directory'); 85 | }); 86 | 87 | }); 88 | 89 | describe('.getFileName()', function() { 90 | 91 | it('should throw if no key', function() { 92 | chai.expect(function() { 93 | utils.getFileName(null); 94 | }).to.throw('Missing key'); 95 | }); 96 | 97 | it('should throw if key is not a string', function() { 98 | chai.expect(function() { 99 | utils.getFileName(123); 100 | }).to.throw('Invalid key'); 101 | }); 102 | 103 | it('should throw if key is a blank string', function() { 104 | chai.expect(function() { 105 | utils.getFileName(' '); 106 | }).to.throw('Invalid key'); 107 | }); 108 | 109 | it('should append the .json extension automatically', function() { 110 | const fileName = utils.getFileName('foo'); 111 | chai.expect(path.basename(fileName)).to.equal('foo.json'); 112 | }); 113 | 114 | it('should not add .json twice', function() { 115 | const fileName = utils.getFileName('foo.json'); 116 | chai.expect(path.basename(fileName)).to.equal('foo.json'); 117 | }); 118 | 119 | it('should preserve an extension other than .json', function() { 120 | const fileName = utils.getFileName('foo.data'); 121 | chai.expect(path.basename(fileName)).to.equal('foo.data.json'); 122 | }); 123 | 124 | it('should return an absolute path', function() { 125 | const fileName = utils.getFileName('foo.data'); 126 | chai.expect(path.isAbsolute(fileName)).to.be.true; 127 | }); 128 | 129 | it('should encode special characters', function() { 130 | const fileName = utils.getFileName('foo?bar:baz'); 131 | chai.expect(path.basename(fileName)).to.equal('foo%3Fbar%3Abaz.json'); 132 | }); 133 | 134 | // HTTP encoding doesn't help us here 135 | it('should replace asterisks with hyphens', function() { 136 | const fileName = utils.getFileName('john6638@gmail*dot*com'); 137 | chai.expect(path.basename(fileName)).to.equal('john6638%40gmail-dot-com.json'); 138 | }); 139 | 140 | it('should allow spaces in file names', function() { 141 | const fileName = utils.getFileName('foo bar'); 142 | chai.expect(path.basename(fileName)).to.equal('foo bar.json'); 143 | }); 144 | 145 | it('should react to user data path changes', function() { 146 | const newUserDataPath = path.join(utils.getDataPath(), 'foo' , 'bar'); 147 | utils.setDataPath(newUserDataPath); 148 | const fileName = utils.getFileName('foo'); 149 | chai.expect(path.dirname(fileName)).to.equal(newUserDataPath); 150 | }); 151 | 152 | it('should accept a custom data path', function() { 153 | const dataPath = path.join('my', 'custom', 'data', 'path'); 154 | const fileName = utils.getFileName('foo', { 155 | dataPath: dataPath 156 | }); 157 | 158 | chai.expect(fileName).to.equal(path.join(dataPath, 'foo.json')); 159 | }); 160 | 161 | }); 162 | 163 | }); 164 | --------------------------------------------------------------------------------