├── cjs ├── package.json └── index.js ├── test ├── package.json └── index.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── package.json ├── types.d.ts ├── README.md └── esm └── index.js /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | node_modules/ 3 | test/dest/ 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | node_modules/ 3 | rollup/ 4 | test/ 5 | package-lock.json 6 | .travis.yml 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | git: 5 | depth: 1 6 | branches: 7 | only: 8 | - master 9 | after_success: 10 | - "npm run coveralls" 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2018, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "perseverant", 3 | "version": "4.0.0", 4 | "description": "An asynchronous, persistent, localForage inspired, filesystem based storage solution for NodeJS.", 5 | "main": "./cjs/index.js", 6 | "module": "./esm/index.js", 7 | "type": "module", 8 | "exports": { 9 | "import": "./esm/index.js", 10 | "default": "./cjs/index.js" 11 | }, 12 | "types": "types.d.ts", 13 | "scripts": { 14 | "build": "npm run cjs && npm run test", 15 | "cjs": "ascjs --no-default esm cjs", 16 | "coveralls": "nyc report --reporter=text-lcov | coveralls", 17 | "test": "nyc node test/index.js" 18 | }, 19 | "keywords": [ 20 | "localStorage", 21 | "localForage", 22 | "persistent", 23 | "storage", 24 | "async", 25 | "node" 26 | ], 27 | "author": "Andrea Giammarchi", 28 | "license": "ISC", 29 | "dependencies": { 30 | "secretly": "^1.0.0" 31 | }, 32 | "devDependencies": { 33 | "ascjs": "^4.0.0", 34 | "coveralls": "^3.1.0", 35 | "nyc": "^15.0.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | interface IPerseverantOptions { 2 | /** 3 | * Name of the storage. Default is `'global'` 4 | * Hightly suggested to use non-default name 5 | */ 6 | name?: string; 7 | /** 8 | * Name of storage folder. Default is `'$HOME/.config/perseverant'` 9 | */ 10 | folder?: string; 11 | /** 12 | * Serializer / Derserializer. Default is `JSON` 13 | */ 14 | serializer?: TSerializer; 15 | /** 16 | * Enable key/value encryption through a password 17 | */ 18 | password?: string; 19 | /** 20 | * If there is a password, it specifies the algorithm to use. 21 | * Default is `"aes256"` 22 | */ 23 | cipher?: string; 24 | } 25 | 26 | declare class Perseverant { 27 | 28 | /** 29 | * 30 | * @param instanceNameOrOptions name or instance options of Perseverant instance. Can be omitted entirely to set default values for everything 31 | */ 32 | createInstance(instanceNameOrOptions?: string | IPerseverantOptions): Perseverant; 33 | 34 | /** 35 | * retrieve a key (read key file) 36 | */ 37 | getItem(key: string, callback: (item: T | null) => TCallbackReturn): Promise; 38 | /** 39 | * retrieve a key (read key file) 40 | */ 41 | getItem(key: string): Promise; 42 | 43 | /** 44 | * store a key (write key file) 45 | */ 46 | setItem(key: string, value: T, callback: (savedItem: T) => TCallbackReturn): Promise; 47 | /** 48 | * store a key (write key file) 49 | */ 50 | setItem(key: string, value: T): Promise; 51 | 52 | /** 53 | * remove a key (unlink key file) 54 | */ 55 | removeItem(key: string, callback: () => TCallbackReturn): Promise; 56 | /** 57 | * remove a key (unlink key file) 58 | */ 59 | removeItem(key: string): Promise; 60 | 61 | /** 62 | * clear all keys for the named storage (rm -rf folder and its files) 63 | * WARNING: if you call this on global name, it'll clean it all 64 | */ 65 | clear(): Promise; 66 | /** 67 | * clear all keys for the named storage (rm -rf folder and its files) 68 | * WARNING: if you call this on global name, it'll clean it all 69 | */ 70 | clear(callback?: () => T): Promise; 71 | 72 | /** 73 | * returns the length of keys 74 | */ 75 | length(callback?: (length: number) => any): Promise; 76 | 77 | /** 78 | * returns all keys 79 | */ 80 | keys(callback: (keys: string[]) => TCallbackReturn): Promise; 81 | /** 82 | * returns all keys 83 | */ 84 | keys(): Promise; 85 | } 86 | 87 | export = Perseverant; 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Perseverant [![Build Status](https://travis-ci.com/WebReflection/perseverant.svg?branch=master)](https://travis-ci.com/WebReflection/perseverant) [![Coverage Status](https://coveralls.io/repos/github/WebReflection/perseverant/badge.svg?branch=master)](https://coveralls.io/github/WebReflection/perseverant?branch=master) [![License: ISC](https://img.shields.io/badge/License-ISC-yellow.svg)](https://opensource.org/licenses/ISC) 2 | 3 | An asynchronous, persistent, [localForage](https://github.com/localForage/localForage) inspired, filesystem based storage solution for NodeJS. 4 | 5 | 6 | ### Concept 7 | 8 | Each key will be stored as regular file with optionally encrypted serialized data. 9 | 10 | By default, everything is kept super simple so whatever value will be saved as JSON. 11 | 12 | It is possible to create a new instance with a different name, folder, encryption or serialization. 13 | 14 | 15 | ### API 16 | 17 | Following the **meta description** of the API. 18 | 19 | ```js 20 | const perseverant = require('perseverant'); 21 | // or import perseverant from 'perseverant'; 22 | 23 | // by default, the name of the storage is 'global' but 24 | // it is highly suggested to use your own project name instead 25 | const storage = perseverant.createInstance('my-project'):Perseverant 26 | 27 | // or ... 28 | const storage = perseverant.createInstance({ 29 | name, // by default 'global' 30 | folder, // by default $HOME/.config/perseverant 31 | serializer, // by default JSON 32 | password, // if provided, it's used to encrypt 33 | salt // if there is a password is used as salt 34 | // by default it's perseverant 35 | }):Perseverant 36 | 37 | // retrieve a key (read key file) 38 | storage.getItem( 39 | key 40 | [,callback(value || null)] 41 | ):Promise 42 | 43 | // store a key (write key file) 44 | storage.setItem( 45 | key, 46 | value 47 | [, callback(value)] 48 | ):Promise 49 | 50 | // remove a key (unlink key file) 51 | storage.removeItem( 52 | key 53 | [, callback] 54 | ):Promise 55 | 56 | // clear all keys for the named storage (rm -rf folder and its files) 57 | // WARNING: if you call this on global name, it'll clean it all 58 | storage.clear([callback]):Promise 59 | 60 | // returns the length of keys, from 0 to N 61 | storage.length([callback(length)]):Promise 62 | 63 | // returns all keys 64 | storage.keys([callback(keys[])]):Promise 65 | ``` 66 | 67 | ### Things to consider 68 | 69 | This project is not a database replacement, neither a secure way to store credentials, passwords, or any relevant data if you do not provide at least a password. 70 | 71 | ```js 72 | // insecure! 73 | const storage = require('perseverant'); 74 | 75 | // secure \o/ 76 | const storage = require('perseverant').createInstance({ 77 | name: process.env.APP_NAME, 78 | password: process.env.APP_SECRET 79 | }); 80 | ``` 81 | 82 | By default, everything is indeed stored as plain JSON, and in a location any other software can reach. 83 | 84 | The goal of this project is to provide, specially to NodeJS CLI, a way to persist data in any kind of device. 85 | 86 | 87 | ### Technically speaking 88 | 89 | * each key is converted into its _base64_ or encrypted counterpart, and its value stored via `JSON.stringify` 90 | * if you provide your own `serializer`, you can also store [recursive data](https://github.com/WebReflection/flatted#flatted) or buffers and binaries, currently not supported in core (to keep it simple) 91 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var storage = require('../cjs'); 4 | var invokes = []; 5 | 6 | (function test(storage) { 7 | 8 | storage 9 | .getItem('nope', function (value) { 10 | invokes.push(value === null); 11 | return value; 12 | }) 13 | .then(function (value) { 14 | console.assert(value === null, 'unknown items return null'); 15 | storage.setItem('yep', 123, function (value) { 16 | invokes.push(value === 123); 17 | return value; 18 | }).then(function (value) { 19 | console.assert(value === 123, 'setItem pass along the value'); 20 | storage 21 | .getItem('yep', function (value) { 22 | invokes.push(value === 123); 23 | return value; 24 | }) 25 | .then(function (value) { 26 | console.assert(value === 123, 'setItem did save the value'); 27 | storage.keys(function (keys) { 28 | invokes.push(keys.join(',') === 'yep'); 29 | return keys; 30 | }).then(function (keys) { 31 | console.assert(keys.length === 1, 'there is one key stored'); 32 | console.assert(keys[0] === 'yep', 'the key is correct'); 33 | storage.removeItem('yep', function (value) { 34 | invokes.push(value === void 0); 35 | }).then(function () { 36 | storage 37 | .getItem('yep') 38 | .then(function (value) { 39 | console.assert(value === null, 'removeItem dropped the key'); 40 | storage.length(function (value) { 41 | invokes.push(value === 0); 42 | return value; 43 | }).then(function (value) { 44 | console.assert(value === 0, 'the key is indeed removed'); 45 | fs.exists(storage.folder, function (result) { 46 | console.assert(result, 'the folder is still there'); 47 | storage.setItem('any', 'value').then(function () { 48 | storage.clear(function (value) { 49 | invokes.push(value === void 0); 50 | }).then(function () { 51 | fs.exists(storage.folder, function (result) { 52 | console.assert(!result, 'the whole folder has been cleared'); 53 | console.assert( 54 | invokes.length === 7 && 55 | invokes.every(Boolean), 56 | 'all callbacks executed OK' 57 | ); 58 | try { 59 | storage.createInstance({name: 'this throws'}); 60 | } catch(e) { 61 | storage.createInstance(); 62 | const local = storage.createInstance({folder: '.'}); 63 | console.assert(local.folder === path.join(__dirname, '..', 'global'), 'expected folder'); 64 | local.length().then(function (value) { 65 | console.assert(value === 0, 'no keys in the local folder'); 66 | local.clear().then(function () { 67 | storage.createInstance('global'); 68 | storage.createInstance({ 69 | folder: path.dirname(process.execPath), 70 | name: path.basename(process.execPath) 71 | }).length().catch( 72 | function () { 73 | console.assert(true, 'all good, there is an error'); 74 | if (!storage.encrypted) { 75 | invokes.splice(0); 76 | test(storage.createInstance({password: 1234})); 77 | } 78 | } 79 | ); 80 | }); 81 | }); 82 | } 83 | }); 84 | }); 85 | }); 86 | }); 87 | }); 88 | }); 89 | }); 90 | }); 91 | }); 92 | }); 93 | }); 94 | 95 | })(storage); 96 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ISC License 3 | * 4 | * Copyright (c) 2018, Andrea Giammarchi, @WebReflection 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 11 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 12 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 13 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 14 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 15 | * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 16 | * PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | 19 | import {mkdir, readdir, readFile, rmdir, stat, unlink, writeFile} from 'fs'; 20 | import {homedir, tmpdir} from 'os'; 21 | import {cwd, env} from 'process'; 22 | import {isAbsolute, join} from 'path'; 23 | 24 | import Secretly from 'secretly'; 25 | 26 | // constants 27 | const HOME = env.XDG_CONFIG_HOME || 28 | join( 29 | homedir() || 30 | /* istanbul ignore next */ 31 | tmpdir(), 32 | '.config' 33 | ); 34 | 35 | // helpers 36 | const after = (resolve, reject) => err => { 37 | /* istanbul ignore if */ 38 | if (isError(err)) reject(err); 39 | else resolve(); 40 | }; 41 | const asBase64 = key => Buffer.from(key).toString('base64'); 42 | const decrypt = (self, buffer) => encrypted.get(self).decrypt(buffer); 43 | const encrypt = (self, buffer) => encrypted.get(self).encrypt(buffer); 44 | const error = err => { throw err; }; 45 | const isError = err => (!!err && !/^(?:ENOTDIR|ENOENT)$/.test(err.code)); 46 | const noop = id => id; 47 | 48 | // privates 49 | const encrypted = new WeakMap; 50 | 51 | class Perseverant { 52 | constructor(options = {}) { 53 | if (options.password) { 54 | encrypted.set( 55 | this, 56 | new Secretly( 57 | options.password, 58 | options.salt || 'perseverant', 59 | false 60 | ) 61 | ); 62 | this.encrypted = true; 63 | } 64 | const folder = options.folder || join(HOME, 'perseverant'); 65 | this.serializer = options.serializer || JSON; 66 | this.name = options.name || 'global'; 67 | if (!/^[a-z0-9_-]+$/i.test(this.name)) 68 | throw new Error('Invalid storage name: ' + this.name); 69 | this.folder = join( 70 | isAbsolute(folder) ? folder : join(cwd(), folder), 71 | this.name 72 | ); 73 | this._init = null; 74 | } 75 | 76 | // .createInstance('name'):Perseverant 77 | // .createInstance({name, folder, serializer}):Perseverant 78 | createInstance(options) { 79 | return new Perseverant( 80 | typeof options === 'string' ? {name: options} : options 81 | ); 82 | } 83 | 84 | // .getItem(key[, callback]):Promise(value) 85 | getItem(key, callback) { 86 | return exec.call( 87 | this, 88 | folder => new Promise(resolve => { 89 | readFile( 90 | join( 91 | folder, 92 | this.encrypted ? encrypt(this, key) : asBase64(key) 93 | ), 94 | (err, buffer) => { 95 | if (err) 96 | resolve(null); 97 | else 98 | resolve( 99 | this.serializer.parse( 100 | this.encrypted ? 101 | decrypt(this, buffer) : 102 | buffer 103 | ) 104 | ); 105 | } 106 | ); 107 | }), 108 | callback 109 | ); 110 | } 111 | 112 | // .setItem(key, value[, callback]):Promise(value) 113 | setItem(key, value, callback) { 114 | return exec.call( 115 | this, 116 | folder => new Promise((resolve, reject) => { 117 | writeFile( 118 | join( 119 | folder, 120 | this.encrypted ? encrypt(this, key) : asBase64(key) 121 | ), 122 | this.encrypted ? 123 | encrypt(this, this.serializer.stringify(value)) : 124 | this.serializer.stringify(value) 125 | , 126 | err => { 127 | /* istanbul ignore if */ 128 | if (err) reject(err); 129 | else resolve(value); 130 | } 131 | ); 132 | }), 133 | callback 134 | ); 135 | } 136 | 137 | // .removeItem(key[, callback]):Promise 138 | removeItem(key, callback) { 139 | return exec.call( 140 | this, 141 | folder => new Promise((resolve, reject) => { 142 | unlink( 143 | join( 144 | folder, 145 | this.encrypted ? encrypt(this, key) : asBase64(key) 146 | ), 147 | after(resolve, reject) 148 | ); 149 | }), 150 | callback 151 | ); 152 | } 153 | 154 | // .clear([callback]):Promise 155 | clear(callback) { 156 | return exec.call( 157 | this, 158 | folder => this.keys().then( 159 | keys => Promise.all(keys.map(key => this.removeItem(key))) 160 | ) 161 | .then( 162 | () => new Promise((resolve, reject) => { 163 | rmdir(folder, after(resolve, reject)); 164 | }), 165 | error 166 | ), 167 | callback 168 | ); 169 | } 170 | 171 | // .length([callback]):Promise(length) 172 | length(callback) { 173 | return this.keys().then( 174 | ({length}) => { 175 | (callback || noop)(length); 176 | return length; 177 | }, 178 | error 179 | ); 180 | } 181 | 182 | // .keys([callback]):Promise(keys) 183 | keys(callback) { 184 | return exec.call( 185 | this, 186 | folder => new Promise((resolve, reject) => { 187 | readdir( 188 | folder, 189 | (err, files) => { 190 | /* istanbul ignore if */ 191 | if (err) reject(err); 192 | else resolve(files.map(asKey, this)); 193 | } 194 | ); 195 | }), 196 | callback 197 | ); 198 | } 199 | } 200 | 201 | function asKey(fileName) { 202 | return this.encrypted ? 203 | decrypt(this, fileName) : 204 | Buffer.from(fileName, 'base64').toString(); 205 | } 206 | 207 | function exec(onInit, callback) { 208 | return init 209 | .call(this) 210 | .then(onInit) 211 | .then(callback || noop) 212 | .catch(error); 213 | } 214 | 215 | function init() { 216 | const self = this; 217 | const {folder} = self; 218 | return self._init || (self._init = new Promise( 219 | (resolve, reject) => { 220 | stat(folder, (err, stat) => { 221 | if (isError(err) || (stat && !stat.isDirectory())) 222 | reject(err || new Error('Invalid folder: ' + folder)); 223 | else if (err) 224 | mkdir(folder, {recursive: true}, (err) => { 225 | /* istanbul ignore if */ 226 | if (err) reject(err); 227 | else { 228 | self._init = null; 229 | resolve(folder); 230 | } 231 | }); 232 | else { 233 | self._init = null; 234 | resolve(folder); 235 | } 236 | }); 237 | } 238 | )); 239 | } 240 | 241 | export default new Perseverant; 242 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*! 3 | * ISC License 4 | * 5 | * Copyright (c) 2018, Andrea Giammarchi, @WebReflection 6 | * 7 | * Permission to use, copy, modify, and/or distribute this software for any 8 | * purpose with or without fee is hereby granted, provided that the above 9 | * copyright notice and this permission notice appear in all copies. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 12 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 13 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 14 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 15 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 16 | * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 17 | * PERFORMANCE OF THIS SOFTWARE. 18 | */ 19 | 20 | const {mkdir, readdir, readFile, rmdir, stat, unlink, writeFile} = require('fs'); 21 | const {homedir, tmpdir} = require('os'); 22 | const {cwd, env} = require('process'); 23 | const {isAbsolute, join} = require('path'); 24 | 25 | const Secretly = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('secretly')); 26 | 27 | // constants 28 | const HOME = env.XDG_CONFIG_HOME || 29 | join( 30 | homedir() || 31 | /* istanbul ignore next */ 32 | tmpdir(), 33 | '.config' 34 | ); 35 | 36 | // helpers 37 | const after = (resolve, reject) => err => { 38 | /* istanbul ignore if */ 39 | if (isError(err)) reject(err); 40 | else resolve(); 41 | }; 42 | const asBase64 = key => Buffer.from(key).toString('base64'); 43 | const decrypt = (self, buffer) => encrypted.get(self).decrypt(buffer); 44 | const encrypt = (self, buffer) => encrypted.get(self).encrypt(buffer); 45 | const error = err => { throw err; }; 46 | const isError = err => (!!err && !/^(?:ENOTDIR|ENOENT)$/.test(err.code)); 47 | const noop = id => id; 48 | 49 | // privates 50 | const encrypted = new WeakMap; 51 | 52 | class Perseverant { 53 | constructor(options = {}) { 54 | if (options.password) { 55 | encrypted.set( 56 | this, 57 | new Secretly( 58 | options.password, 59 | options.salt || 'perseverant', 60 | false 61 | ) 62 | ); 63 | this.encrypted = true; 64 | } 65 | const folder = options.folder || join(HOME, 'perseverant'); 66 | this.serializer = options.serializer || JSON; 67 | this.name = options.name || 'global'; 68 | if (!/^[a-z0-9_-]+$/i.test(this.name)) 69 | throw new Error('Invalid storage name: ' + this.name); 70 | this.folder = join( 71 | isAbsolute(folder) ? folder : join(cwd(), folder), 72 | this.name 73 | ); 74 | this._init = null; 75 | } 76 | 77 | // .createInstance('name'):Perseverant 78 | // .createInstance({name, folder, serializer}):Perseverant 79 | createInstance(options) { 80 | return new Perseverant( 81 | typeof options === 'string' ? {name: options} : options 82 | ); 83 | } 84 | 85 | // .getItem(key[, callback]):Promise(value) 86 | getItem(key, callback) { 87 | return exec.call( 88 | this, 89 | folder => new Promise(resolve => { 90 | readFile( 91 | join( 92 | folder, 93 | this.encrypted ? encrypt(this, key) : asBase64(key) 94 | ), 95 | (err, buffer) => { 96 | if (err) 97 | resolve(null); 98 | else 99 | resolve( 100 | this.serializer.parse( 101 | this.encrypted ? 102 | decrypt(this, buffer) : 103 | buffer 104 | ) 105 | ); 106 | } 107 | ); 108 | }), 109 | callback 110 | ); 111 | } 112 | 113 | // .setItem(key, value[, callback]):Promise(value) 114 | setItem(key, value, callback) { 115 | return exec.call( 116 | this, 117 | folder => new Promise((resolve, reject) => { 118 | writeFile( 119 | join( 120 | folder, 121 | this.encrypted ? encrypt(this, key) : asBase64(key) 122 | ), 123 | this.encrypted ? 124 | encrypt(this, this.serializer.stringify(value)) : 125 | this.serializer.stringify(value) 126 | , 127 | err => { 128 | /* istanbul ignore if */ 129 | if (err) reject(err); 130 | else resolve(value); 131 | } 132 | ); 133 | }), 134 | callback 135 | ); 136 | } 137 | 138 | // .removeItem(key[, callback]):Promise 139 | removeItem(key, callback) { 140 | return exec.call( 141 | this, 142 | folder => new Promise((resolve, reject) => { 143 | unlink( 144 | join( 145 | folder, 146 | this.encrypted ? encrypt(this, key) : asBase64(key) 147 | ), 148 | after(resolve, reject) 149 | ); 150 | }), 151 | callback 152 | ); 153 | } 154 | 155 | // .clear([callback]):Promise 156 | clear(callback) { 157 | return exec.call( 158 | this, 159 | folder => this.keys().then( 160 | keys => Promise.all(keys.map(key => this.removeItem(key))) 161 | ) 162 | .then( 163 | () => new Promise((resolve, reject) => { 164 | rmdir(folder, after(resolve, reject)); 165 | }), 166 | error 167 | ), 168 | callback 169 | ); 170 | } 171 | 172 | // .length([callback]):Promise(length) 173 | length(callback) { 174 | return this.keys().then( 175 | ({length}) => { 176 | (callback || noop)(length); 177 | return length; 178 | }, 179 | error 180 | ); 181 | } 182 | 183 | // .keys([callback]):Promise(keys) 184 | keys(callback) { 185 | return exec.call( 186 | this, 187 | folder => new Promise((resolve, reject) => { 188 | readdir( 189 | folder, 190 | (err, files) => { 191 | /* istanbul ignore if */ 192 | if (err) reject(err); 193 | else resolve(files.map(asKey, this)); 194 | } 195 | ); 196 | }), 197 | callback 198 | ); 199 | } 200 | } 201 | 202 | function asKey(fileName) { 203 | return this.encrypted ? 204 | decrypt(this, fileName) : 205 | Buffer.from(fileName, 'base64').toString(); 206 | } 207 | 208 | function exec(onInit, callback) { 209 | return init 210 | .call(this) 211 | .then(onInit) 212 | .then(callback || noop) 213 | .catch(error); 214 | } 215 | 216 | function init() { 217 | const self = this; 218 | const {folder} = self; 219 | return self._init || (self._init = new Promise( 220 | (resolve, reject) => { 221 | stat(folder, (err, stat) => { 222 | if (isError(err) || (stat && !stat.isDirectory())) 223 | reject(err || new Error('Invalid folder: ' + folder)); 224 | else if (err) 225 | mkdir(folder, {recursive: true}, (err) => { 226 | /* istanbul ignore if */ 227 | if (err) reject(err); 228 | else { 229 | self._init = null; 230 | resolve(folder); 231 | } 232 | }); 233 | else { 234 | self._init = null; 235 | resolve(folder); 236 | } 237 | }); 238 | } 239 | )); 240 | } 241 | 242 | module.exports = new Perseverant; 243 | --------------------------------------------------------------------------------