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

View File

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

View File

@ -8,6 +8,10 @@ module.exports = {
"json",
"node"
],
watchPathIgnorePatterns: [
"<rootDir>/dist/",
"<rootDir>/node_modules/",
],
restoreMocks: true,
collectCoverage: true,
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",
"version": "0.1.0",
"version": "0.2.0",
"description": "Javascript/Typescript persistent storage, with key-value stores as foundation",
"main": "dist/tk-storage.umd.js",
"source": "src/index.ts",
@ -14,11 +14,12 @@
"dev:test": "jest --watchAll",
"dev:build": "microbundle watch -f modern,umd",
"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": {
"@types/uuid": "^3.4.5",
"tk-base": "^0.2.1",
"@types/uuid": "^3.4.6",
"tk-base": "^0.2.5",
"uuid": "^3.3.3"
},
"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> {
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 () => {
const reference1 = new MemoryStorage();
const reference2 = new MemoryStorage();
const target = new MemoryStorage();
let storage = new ScopedStorage(reference1, () => target);
let storage = new RefScopedStorage(reference1, () => target);
await basicCheck(storage);
await storage.set("persist", "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");
storage = new ScopedStorage(reference2, target);
storage = new RefScopedStorage(reference2, target);
await storage.set("other", "thing");
expect(await storage.get("persist")).toBe(null);
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("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)
*/
export class ScopedStorage implements KeyValueStorage {
export class RefScopedStorage implements KeyValueStorage {
private readonly reference: KeyValueStorage
private readonly target: KeyValueStorage
private suffix?: string
private inner?: KeyValueStorage
constructor(reference: StorageDelegate, target: StorageDelegate) {
this.reference = fromDelegate(reference);
this.target = fromDelegate(target);
}
private async init(): Promise<void> {
const refkey = "tk-storage-scope-suffix"
private async init(): Promise<KeyValueStorage> {
const refkey = "tk-storage-scope-ref";
const suffix = await this.reference.get(refkey);
if (suffix) {
this.suffix = suffix;
return new ScopedStorage(this.target, suffix);
} else {
const suffix = "#" + uuid1();
const suffix = uuid1();
await this.reference.set(refkey, suffix);
this.suffix = suffix;
return new ScopedStorage(this.target, suffix);
}
}
async get(key: string): Promise<string | null> {
if (!this.suffix) {
await this.init();
if (!this.inner) {
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> {
if (!this.suffix) {
await this.init();
if (!this.inner) {
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";
/**
* Key-value store
* Key-value store using localStorage
*/
export class BrowserLocalStorage implements KeyValueStorage {
constructor() {

View File

@ -1,24 +1,43 @@
import { getLocalStorage } from ".";
import { MemoryStorage } from "./basic";
import { MemoryStorage, ScopedStorage } from "./basic";
import { BrowserLocalStorage } from "./browser";
import { NodeDirectoryStorage } from "./node";
import { forceNodeStoragesInTempDir } from "./node.test";
const localStorage = (window as any).localStorage;
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();
let storage = getLocalStorage();
expect(storage).toBeInstanceOf(BrowserLocalStorage);
let storage = getLocalStorage("app1");
expect(storage).toBeInstanceOf(ScopedStorage);
expect((storage as any).target).toBeInstanceOf(BrowserLocalStorage);
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"];
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(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 { NodeDirectoryStorage } from "./node";
/**
* Récupère le meilleur stockage "local" disponible
*/
export function getLocalStorage(): KeyValueStorage {
if (typeof localStorage != "undefined") {
return new BrowserLocalStorage();
} else {
console.warn("No persistent storage available, using in-memory volatile storage");
return new MemoryStorage();
export function getLocalStorage(appname: string): KeyValueStorage {
try {
return new ScopedStorage(new BrowserLocalStorage(), appname);
} catch {
try {
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",
"es6"
]
}
},
"exclude": [
"node_modules",
"dist",
"src/**/*.test.ts"
]
}