Added node.js directory storage

This commit is contained in:
Michaël Lemaire 2019-11-07 22:33:23 +01:00
parent 340043beb7
commit b490f08862
13 changed files with 1207 additions and 458 deletions

View file

@ -26,7 +26,7 @@ npm install tk-storage
```javascript ```javascript
import { getLocalStorage } from "tk-storage"; import { getLocalStorage } from "tk-storage";
const storage = getLocalStorage(); const storage = getLocalStorage("myapp");
``` ```
Import in browser: Import in browser:
@ -36,14 +36,14 @@ Import in browser:
``` ```
```javascript ```javascript
const storage = tkStorage.getLocalStorage(); const storage = tkStorage.getLocalStorage("myapp");
``` ```
Use Use
--- ---
```javascript ```javascript
const storage = getLocalStorage(); const storage = getLocalStorage("myapp");
await storage.get("key"); // => null await storage.get("key"); // => null
await storage.set("key", "value"); await storage.set("key", "value");
await storage.get("key"); // => "value" await storage.get("key"); // => "value"

View file

@ -3,7 +3,7 @@
# source activate_node # source activate_node
vdir="./.venv" vdir="./.venv"
expected="10.16.3" expected="12.13.0"
if [ \! -f "./activate_node" ] if [ \! -f "./activate_node" ]
then then

View file

@ -8,6 +8,10 @@ module.exports = {
"json", "json",
"node" "node"
], ],
watchPathIgnorePatterns: [
"<rootDir>/dist/",
"<rootDir>/node_modules/",
],
restoreMocks: true, restoreMocks: true,
collectCoverage: true, collectCoverage: true,
collectCoverageFrom: [ collectCoverageFrom: [

1408
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "tk-storage", "name": "tk-storage",
"version": "0.1.0", "version": "0.2.0",
"description": "Javascript/Typescript persistent storage, with key-value stores as foundation", "description": "Javascript/Typescript persistent storage, with key-value stores as foundation",
"main": "dist/tk-storage.umd.js", "main": "dist/tk-storage.umd.js",
"source": "src/index.ts", "source": "src/index.ts",
@ -14,11 +14,12 @@
"dev:test": "jest --watchAll", "dev:test": "jest --watchAll",
"dev:build": "microbundle watch -f modern,umd", "dev:build": "microbundle watch -f modern,umd",
"prepare": "npm run build", "prepare": "npm run build",
"prepublishOnly": "npm test" "prepublishOnly": "npm test",
"dev:serve": "live-server --host=localhost --port=5000 --no-browser --ignorePattern='.*\\.d\\.ts' dist"
}, },
"devDependencies": { "devDependencies": {
"@types/uuid": "^3.4.5", "@types/uuid": "^3.4.6",
"tk-base": "^0.2.1", "tk-base": "^0.2.5",
"uuid": "^3.3.3" "uuid": "^3.3.3"
}, },
"files": [ "files": [

View file

@ -1,4 +1,4 @@
import { KeyValueStorage, MemoryStorage, ScopedStorage } from "./basic"; import { KeyValueStorage, MemoryStorage, RefScopedStorage } from "./basic";
export async function basicCheck(storage: KeyValueStorage): Promise<void> { export async function basicCheck(storage: KeyValueStorage): Promise<void> {
expect(await storage.get("test")).toBe(null); expect(await storage.get("test")).toBe(null);
@ -16,27 +16,27 @@ describe(MemoryStorage, () => {
}); });
}); });
describe(ScopedStorage, () => { describe(RefScopedStorage, () => {
it("uses a target storage, scoping its keys", async () => { it("uses a target storage, scoping its keys", async () => {
const reference1 = new MemoryStorage(); const reference1 = new MemoryStorage();
const reference2 = new MemoryStorage(); const reference2 = new MemoryStorage();
const target = new MemoryStorage(); const target = new MemoryStorage();
let storage = new ScopedStorage(reference1, () => target); let storage = new RefScopedStorage(reference1, () => target);
await basicCheck(storage); await basicCheck(storage);
await storage.set("persist", "42"); await storage.set("persist", "42");
expect(await storage.get("persist")).toBe("42"); expect(await storage.get("persist")).toBe("42");
storage = new ScopedStorage(() => reference1, target); storage = new RefScopedStorage(() => reference1, target);
expect(await storage.get("persist")).toBe("42"); expect(await storage.get("persist")).toBe("42");
storage = new ScopedStorage(reference2, target); storage = new RefScopedStorage(reference2, target);
await storage.set("other", "thing"); await storage.set("other", "thing");
expect(await storage.get("persist")).toBe(null); expect(await storage.get("persist")).toBe(null);
expect(await storage.get("other")).toBe("thing"); expect(await storage.get("other")).toBe("thing");
storage = new ScopedStorage(reference1, target); storage = new RefScopedStorage(reference1, target);
expect(await storage.get("persist")).toBe("42"); expect(await storage.get("persist")).toBe("42");
expect(await storage.get("other")).toBe(null); expect(await storage.get("other")).toBe(null);
}); });

View file

@ -35,45 +35,64 @@ function fromDelegate(delegate: StorageDelegate): KeyValueStorage {
} }
/** /**
* Use a target unscoped storage, scoping the keys in a virtual namespace * Wrap a storage, scoping the keys using a suffix
*/
export class ScopedStorage implements KeyValueStorage {
private readonly target: KeyValueStorage
constructor(target: StorageDelegate, private readonly suffix: string) {
this.target = fromDelegate(target);
}
async get(key: string): Promise<string | null> {
return await this.target.get(`${key}#${this.suffix}`);
}
async set(key: string, value: string | null): Promise<void> {
return await this.target.set(`${key}#${this.suffix}`, value);
}
}
/**
* Use a target unscoped storage, scoping the keys in a virtual random namespace
* *
* The namespace is persisted in a reference storage (used unscoped) * The namespace is persisted in a reference storage (used unscoped)
*/ */
export class ScopedStorage implements KeyValueStorage { export class RefScopedStorage implements KeyValueStorage {
private readonly reference: KeyValueStorage private readonly reference: KeyValueStorage
private readonly target: KeyValueStorage private readonly target: KeyValueStorage
private suffix?: string private inner?: KeyValueStorage
constructor(reference: StorageDelegate, target: StorageDelegate) { constructor(reference: StorageDelegate, target: StorageDelegate) {
this.reference = fromDelegate(reference); this.reference = fromDelegate(reference);
this.target = fromDelegate(target); this.target = fromDelegate(target);
} }
private async init(): Promise<void> { private async init(): Promise<KeyValueStorage> {
const refkey = "tk-storage-scope-suffix" const refkey = "tk-storage-scope-ref";
const suffix = await this.reference.get(refkey); const suffix = await this.reference.get(refkey);
if (suffix) { if (suffix) {
this.suffix = suffix; return new ScopedStorage(this.target, suffix);
} else { } else {
const suffix = "#" + uuid1(); const suffix = uuid1();
await this.reference.set(refkey, suffix); await this.reference.set(refkey, suffix);
this.suffix = suffix; return new ScopedStorage(this.target, suffix);
} }
} }
async get(key: string): Promise<string | null> { async get(key: string): Promise<string | null> {
if (!this.suffix) { if (!this.inner) {
await this.init(); this.inner = await this.init();
} }
return await this.target.get(key + this.suffix); return await this.inner.get(key);
} }
async set(key: string, value: string | null): Promise<void> { async set(key: string, value: string | null): Promise<void> {
if (!this.suffix) { if (!this.inner) {
await this.init(); this.inner = await this.init();
} }
return await this.target.set(key + this.suffix, value); return await this.inner.set(key, value);
} }
} }

View file

@ -1,7 +1,7 @@
import { KeyValueStorage } from "./basic"; import { KeyValueStorage } from "./basic";
/** /**
* Key-value store * Key-value store using localStorage
*/ */
export class BrowserLocalStorage implements KeyValueStorage { export class BrowserLocalStorage implements KeyValueStorage {
constructor() { constructor() {

View file

@ -1,24 +1,43 @@
import { getLocalStorage } from "."; import { getLocalStorage } from ".";
import { MemoryStorage } from "./basic"; import { MemoryStorage, ScopedStorage } from "./basic";
import { BrowserLocalStorage } from "./browser"; import { BrowserLocalStorage } from "./browser";
import { NodeDirectoryStorage } from "./node";
import { forceNodeStoragesInTempDir } from "./node.test";
const localStorage = (window as any).localStorage; const localStorage = (window as any).localStorage;
describe(getLocalStorage, () => { describe(getLocalStorage, () => {
it("returns the best storage available", () => { forceNodeStoragesInTempDir();
afterEach(() => {
(window as any).localStorage = localStorage;
});
it("returns the best storage available", async () => {
const mockWarn = jest.spyOn(console, "warn").mockImplementation(); const mockWarn = jest.spyOn(console, "warn").mockImplementation();
let storage = getLocalStorage(); let storage = getLocalStorage("app1");
expect(storage).toBeInstanceOf(BrowserLocalStorage); expect(storage).toBeInstanceOf(ScopedStorage);
expect((storage as any).target).toBeInstanceOf(BrowserLocalStorage);
expect(mockWarn).not.toHaveBeenCalled(); expect(mockWarn).not.toHaveBeenCalled();
await storage.set("testkey", "testvalue");
expect(await storage.get("testkey")).toBe("testvalue");
storage = getLocalStorage("app1");
expect(await storage.get("testkey")).toBe("testvalue");
storage = getLocalStorage("app2");
expect(await storage.get("testkey")).toBeNull();
delete (window as any)["localStorage"]; delete (window as any)["localStorage"];
storage = getLocalStorage(); storage = getLocalStorage("app1");
expect(storage).toBeInstanceOf(NodeDirectoryStorage);
(NodeDirectoryStorage.findUserData as any).mockImplementation(() => { throw new Error() });
storage = getLocalStorage("app1");
expect(storage).toBeInstanceOf(MemoryStorage); expect(storage).toBeInstanceOf(MemoryStorage);
expect(mockWarn).toHaveBeenCalledWith("No persistent storage available, using in-memory volatile storage"); expect(mockWarn).toHaveBeenCalledWith("No persistent storage available, using in-memory volatile storage");
}); });
}); });
afterEach(() => {
(window as any).localStorage = localStorage;
});

View file

@ -1,14 +1,19 @@
import { KeyValueStorage, MemoryStorage } from "./basic"; import { KeyValueStorage, MemoryStorage, ScopedStorage } from "./basic";
import { BrowserLocalStorage } from "./browser"; import { BrowserLocalStorage } from "./browser";
import { NodeDirectoryStorage } from "./node";
/** /**
* Récupère le meilleur stockage "local" disponible * Récupère le meilleur stockage "local" disponible
*/ */
export function getLocalStorage(): KeyValueStorage { export function getLocalStorage(appname: string): KeyValueStorage {
if (typeof localStorage != "undefined") { try {
return new BrowserLocalStorage(); return new ScopedStorage(new BrowserLocalStorage(), appname);
} else { } catch {
console.warn("No persistent storage available, using in-memory volatile storage"); try {
return new MemoryStorage(); return new NodeDirectoryStorage(appname);
} catch {
console.warn("No persistent storage available, using in-memory volatile storage");
return new MemoryStorage();
}
} }
} }

30
src/node.test.ts Normal file
View file

@ -0,0 +1,30 @@
import { basicCheck } from "./basic.test";
import { NodeDirectoryStorage } from "./node";
let tempdir: string | null = null
export function forceNodeStoragesInTempDir() {
beforeEach(() => {
const fs = require("fs");
const os = require("os");
const path = require("path");
const tmpdir = tempdir = fs.mkdtempSync(path.join(os.tmpdir(), "tk-storage-testing"));
jest.spyOn(NodeDirectoryStorage, "findUserData").mockReturnValue(tmpdir);
});
afterEach(() => {
if (tempdir) {
// TODO remove directory
tempdir = null;
}
});
}
describe(NodeDirectoryStorage, () => {
forceNodeStoragesInTempDir();
it("uses a directory as storage", async () => {
const storage = new NodeDirectoryStorage("test-tk-storage");
await basicCheck(storage);
});
});

82
src/node.ts Normal file
View file

@ -0,0 +1,82 @@
import { KeyValueStorage } from "./basic";
/**
* Key-value store, using local file-system
*/
export class NodeDirectoryStorage implements KeyValueStorage {
readonly directory: string
private checked = false
constructor(appname: string) {
if (typeof require === "undefined") {
throw new Error("Storage only available in nodejs");
}
const path = require("path");
this.directory = path.join(NodeDirectoryStorage.findUserData(), "tk-storage", appname);
}
/**
* Find the writable user data directory
*/
static findUserData(): string {
const result = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share");
if (!result) {
throw new Error("Cannot locate user data directory");
}
return result;
}
/**
* Get the file path for a key
*/
getPath(key: string): string {
const path = require("path");
return path.join(this.directory, key);
}
async get(key: string): Promise<string | null> {
const fs = require("fs");
return new Promise((resolve, reject) => {
fs.readFile(this.getPath(key), { encoding: 'utf8' }, (err: any, data: string) => {
resolve(err ? null : data);
});
});
}
async set(key: string, value: string | null): Promise<void> {
const fs = require("fs");
if (!this.checked) {
await new Promise((resolve, reject) => {
fs.mkdir(this.directory, { recursive: true }, (err: any) => {
if (err && err.code !== 'EEXIST') {
reject(err);
} else {
resolve();
}
});
});
this.checked = true;
}
return new Promise((resolve, reject) => {
if (value === null) {
fs.unlink(this.getPath(key), (err: any) => {
if (err) {
reject(err);
} else {
resolve();
}
});
} else {
fs.writeFile(this.getPath(key), value, { encoding: 'utf8' }, (err: any) => {
if (err) {
reject(err);
} else {
resolve();
}
});
}
});
}
}

View file

@ -12,5 +12,10 @@
"dom", "dom",
"es6" "es6"
] ]
} },
"exclude": [
"node_modules",
"dist",
"src/**/*.test.ts"
]
} }