@eik/sink-memory
Advanced tools
Comparing version 1.1.1 to 1.1.2
@@ -0,1 +1,8 @@ | ||
## [1.1.2](https://github.com/eik-lib/sink-memory/compare/v1.1.1...v1.1.2) (2024-08-15) | ||
### Bug Fixes | ||
* generate URL pathnames on Windows hosts ([#5](https://github.com/eik-lib/sink-memory/issues/5)) ([0f37c07](https://github.com/eik-lib/sink-memory/commit/0f37c072b798611e97e911f37b74b25a4a08e744)) | ||
## [1.1.1](https://github.com/eik-lib/sink-memory/compare/v1.1.0...v1.1.1) (2024-07-29) | ||
@@ -2,0 +9,0 @@ |
@@ -1,33 +0,33 @@ | ||
import crypto from 'node:crypto'; | ||
import crypto from "node:crypto"; | ||
export default class Entry { | ||
constructor({ mimeType = 'application/octet-stream', payload = [] } = {}) { | ||
this._mimeType = mimeType; | ||
this._payload = payload; | ||
this._hash = ''; | ||
constructor({ mimeType = "application/octet-stream", payload = [] } = {}) { | ||
this._mimeType = mimeType; | ||
this._payload = payload; | ||
this._hash = ""; | ||
if (Array.isArray(payload)) { | ||
const hash = crypto.createHash('sha512'); | ||
payload.forEach((buffer) => { | ||
hash.update(buffer.toString()); | ||
}); | ||
this._hash = `sha512-${hash.digest('base64')}`; | ||
} | ||
} | ||
if (Array.isArray(payload)) { | ||
const hash = crypto.createHash("sha512"); | ||
payload.forEach((buffer) => { | ||
hash.update(buffer.toString()); | ||
}); | ||
this._hash = `sha512-${hash.digest("base64")}`; | ||
} | ||
} | ||
get mimeType() { | ||
return this._mimeType; | ||
} | ||
get mimeType() { | ||
return this._mimeType; | ||
} | ||
get payload() { | ||
return this._payload; | ||
} | ||
get payload() { | ||
return this._payload; | ||
} | ||
get hash() { | ||
return this._hash; | ||
} | ||
get hash() { | ||
return this._hash; | ||
} | ||
get [Symbol.toStringTag]() { | ||
return 'Entry'; | ||
} | ||
get [Symbol.toStringTag]() { | ||
return "Entry"; | ||
} | ||
} |
395
lib/main.js
@@ -1,7 +0,7 @@ | ||
import { join } from 'node:path'; | ||
import { Readable, Writable } from 'node:stream'; | ||
import { ReadFile } from '@eik/common'; | ||
import Sink from '@eik/sink'; | ||
import MetricsClient from '@metrics/client'; | ||
import Entry from './entry.js'; | ||
import { join } from "node:path"; | ||
import { Readable, Writable } from "node:stream"; | ||
import { ReadFile } from "@eik/common"; | ||
import Sink from "@eik/sink"; | ||
import MetricsClient from "@metrics/client"; | ||
import Entry from "./entry.js"; | ||
@@ -14,228 +14,227 @@ /** | ||
export default class SinkMemory extends Sink { | ||
_metrics = new MetricsClient(); | ||
/** @type {Map<string, Entry>}*/ | ||
_state = new Map(); | ||
_metrics = new MetricsClient(); | ||
/** @type {Map<string, Entry>}*/ | ||
_state = new Map(); | ||
/** | ||
* | ||
* @param {SinkMemoryOptions} options | ||
*/ | ||
constructor(options = {}) { | ||
super(); | ||
this._rootPath = options.rootPath || '/'; | ||
this._counter = this._metrics.counter({ | ||
name: 'eik_core_sink_mem', | ||
description: | ||
'Counter measuring access to the in memory storage sink', | ||
labels: { | ||
operation: 'n/a', | ||
success: false, | ||
access: false, | ||
}, | ||
}); | ||
} | ||
/** | ||
* | ||
* @param {SinkMemoryOptions} options | ||
*/ | ||
constructor(options = {}) { | ||
super(); | ||
this._rootPath = options.rootPath || "/"; | ||
this._counter = this._metrics.counter({ | ||
name: "eik_core_sink_mem", | ||
description: "Counter measuring access to the in memory storage sink", | ||
labels: { | ||
operation: "n/a", | ||
success: false, | ||
access: false, | ||
}, | ||
}); | ||
} | ||
get metrics() { | ||
return this._metrics; | ||
} | ||
get metrics() { | ||
return this._metrics; | ||
} | ||
/** | ||
* @param {string} filePath | ||
* @param {string} contentType | ||
* @returns {Promise<Writable>} | ||
*/ | ||
write(filePath, contentType) { | ||
return new Promise((resolve, reject) => { | ||
const operation = 'write'; | ||
/** | ||
* @param {string} filePath | ||
* @param {string} contentType | ||
* @returns {Promise<Writable>} | ||
*/ | ||
write(filePath, contentType) { | ||
return new Promise((resolve, reject) => { | ||
const operation = "write"; | ||
try { | ||
Sink.validateFilePath(filePath); | ||
Sink.validateContentType(contentType); | ||
} catch (error) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(error); | ||
return; | ||
} | ||
try { | ||
Sink.validateFilePath(filePath); | ||
Sink.validateContentType(contentType); | ||
} catch (error) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(error); | ||
return; | ||
} | ||
const pathname = join(this._rootPath, filePath); | ||
const pathname = join(this._rootPath, filePath).replace(/\\/g, "/"); | ||
if (pathname.indexOf(this._rootPath) !== 0) { | ||
reject(new Error(`Directory traversal - ${filePath}`)); | ||
this._counter.inc({ labels: { operation } }); | ||
return; | ||
} | ||
if (pathname.indexOf(this._rootPath) !== 0) { | ||
reject(new Error(`Directory traversal - ${filePath}`)); | ||
this._counter.inc({ labels: { operation } }); | ||
return; | ||
} | ||
const payload = []; | ||
const stream = new Writable({ | ||
write(chunk, encoding, cb) { | ||
payload.push(chunk); | ||
cb(); | ||
}, | ||
}); | ||
const payload = []; | ||
const stream = new Writable({ | ||
write(chunk, encoding, cb) { | ||
payload.push(chunk); | ||
cb(); | ||
}, | ||
}); | ||
stream.on('finish', () => { | ||
const entry = new Entry({ | ||
mimeType: contentType, | ||
payload, | ||
}); | ||
stream.on("finish", () => { | ||
const entry = new Entry({ | ||
mimeType: contentType, | ||
payload, | ||
}); | ||
this._state.set(pathname, entry); | ||
this._state.set(pathname, entry); | ||
this._counter.inc({ | ||
labels: { | ||
success: true, | ||
access: true, | ||
operation, | ||
}, | ||
}); | ||
}); | ||
this._counter.inc({ | ||
labels: { | ||
success: true, | ||
access: true, | ||
operation, | ||
}, | ||
}); | ||
}); | ||
resolve(stream); | ||
}); | ||
} | ||
resolve(stream); | ||
}); | ||
} | ||
/** | ||
* @param {string} filePath | ||
* @throws {Error} if the file does not exist | ||
* @returns {Promise<import('@eik/common').ReadFile>} | ||
*/ | ||
read(filePath) { | ||
return new Promise((resolve, reject) => { | ||
const operation = 'read'; | ||
/** | ||
* @param {string} filePath | ||
* @throws {Error} if the file does not exist | ||
* @returns {Promise<import('@eik/common').ReadFile>} | ||
*/ | ||
read(filePath) { | ||
return new Promise((resolve, reject) => { | ||
const operation = "read"; | ||
try { | ||
Sink.validateFilePath(filePath); | ||
} catch (error) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(error); | ||
return; | ||
} | ||
try { | ||
Sink.validateFilePath(filePath); | ||
} catch (error) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(error); | ||
return; | ||
} | ||
const pathname = join(this._rootPath, filePath); | ||
const pathname = join(this._rootPath, filePath).replace(/\\/g, "/"); | ||
if (pathname.indexOf(this._rootPath) !== 0) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(new Error(`Directory traversal - ${filePath}`)); | ||
return; | ||
} | ||
if (pathname.indexOf(this._rootPath) !== 0) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(new Error(`Directory traversal - ${filePath}`)); | ||
return; | ||
} | ||
const entry = this._state.get(pathname); | ||
if (!entry) { | ||
reject(new Error(`${filePath} does not exist`)); | ||
} | ||
const entry = this._state.get(pathname); | ||
if (!entry) { | ||
reject(new Error(`${filePath} does not exist`)); | ||
} | ||
const payload = entry.payload || []; | ||
const obj = new ReadFile({ | ||
mimeType: entry.mimeType, | ||
etag: entry.hash, | ||
}); | ||
const payload = entry.payload || []; | ||
const obj = new ReadFile({ | ||
mimeType: entry.mimeType, | ||
etag: entry.hash, | ||
}); | ||
obj.stream = new Readable({ | ||
read() { | ||
payload.forEach((item) => { | ||
this.push(item); | ||
}); | ||
this.push(null); | ||
}, | ||
}); | ||
obj.stream = new Readable({ | ||
read() { | ||
payload.forEach((item) => { | ||
this.push(item); | ||
}); | ||
this.push(null); | ||
}, | ||
}); | ||
obj.stream.on('end', () => { | ||
this._counter.inc({ | ||
labels: { | ||
success: true, | ||
access: true, | ||
operation, | ||
}, | ||
}); | ||
}); | ||
obj.stream.on("end", () => { | ||
this._counter.inc({ | ||
labels: { | ||
success: true, | ||
access: true, | ||
operation, | ||
}, | ||
}); | ||
}); | ||
resolve(obj); | ||
}); | ||
} | ||
resolve(obj); | ||
}); | ||
} | ||
/** | ||
* @param {string} filePath | ||
* @throws {Error} if the file does not exist | ||
* @returns {Promise<void>} | ||
*/ | ||
delete(filePath) { | ||
return new Promise((resolve, reject) => { | ||
const operation = 'delete'; | ||
/** | ||
* @param {string} filePath | ||
* @throws {Error} if the file does not exist | ||
* @returns {Promise<void>} | ||
*/ | ||
delete(filePath) { | ||
return new Promise((resolve, reject) => { | ||
const operation = "delete"; | ||
try { | ||
Sink.validateFilePath(filePath); | ||
} catch (error) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(error); | ||
return; | ||
} | ||
try { | ||
Sink.validateFilePath(filePath); | ||
} catch (error) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(error); | ||
return; | ||
} | ||
const pathname = join(this._rootPath, filePath); | ||
const pathname = join(this._rootPath, filePath).replace(/\\/g, "/"); | ||
if (pathname.indexOf(this._rootPath) !== 0) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(new Error(`Directory traversal - ${filePath}`)); | ||
return; | ||
} | ||
if (pathname.indexOf(this._rootPath) !== 0) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(new Error(`Directory traversal - ${filePath}`)); | ||
return; | ||
} | ||
// Delete recursively | ||
Array.from(this._state.keys()).forEach((key) => { | ||
if (key.startsWith(pathname)) { | ||
this._state.delete(key); | ||
} | ||
}); | ||
// Delete recursively | ||
Array.from(this._state.keys()).forEach((key) => { | ||
if (key.startsWith(pathname)) { | ||
this._state.delete(key); | ||
} | ||
}); | ||
this._counter.inc({ | ||
labels: { | ||
success: true, | ||
access: true, | ||
operation, | ||
}, | ||
}); | ||
this._counter.inc({ | ||
labels: { | ||
success: true, | ||
access: true, | ||
operation, | ||
}, | ||
}); | ||
resolve(); | ||
}); | ||
} | ||
resolve(); | ||
}); | ||
} | ||
/** | ||
* @param {string} filePath | ||
* @throws {Error} if the file does not exist | ||
* @returns {Promise<void>} | ||
*/ | ||
exist(filePath) { | ||
return new Promise((resolve, reject) => { | ||
const operation = 'exist'; | ||
/** | ||
* @param {string} filePath | ||
* @throws {Error} if the file does not exist | ||
* @returns {Promise<void>} | ||
*/ | ||
exist(filePath) { | ||
return new Promise((resolve, reject) => { | ||
const operation = "exist"; | ||
try { | ||
Sink.validateFilePath(filePath); | ||
} catch (error) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(error); | ||
return; | ||
} | ||
try { | ||
Sink.validateFilePath(filePath); | ||
} catch (error) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(error); | ||
return; | ||
} | ||
const pathname = join(this._rootPath, filePath); | ||
const pathname = join(this._rootPath, filePath).replace(/\\/g, "/"); | ||
if (pathname.indexOf(this._rootPath) !== 0) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(new Error(`Directory traversal - ${filePath}`)); | ||
return; | ||
} | ||
if (pathname.indexOf(this._rootPath) !== 0) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(new Error(`Directory traversal - ${filePath}`)); | ||
return; | ||
} | ||
this._counter.inc({ | ||
labels: { | ||
success: true, | ||
access: true, | ||
operation, | ||
}, | ||
}); | ||
this._counter.inc({ | ||
labels: { | ||
success: true, | ||
access: true, | ||
operation, | ||
}, | ||
}); | ||
if (this._state.has(pathname)) { | ||
resolve(); | ||
return; | ||
} | ||
if (this._state.has(pathname)) { | ||
resolve(); | ||
return; | ||
} | ||
reject(new Error(`${filePath} does not exist`)); | ||
}); | ||
} | ||
reject(new Error(`${filePath} does not exist`)); | ||
}); | ||
} | ||
} |
{ | ||
"name": "@eik/sink-memory", | ||
"version": "1.1.1", | ||
"version": "1.1.2", | ||
"description": "In-memory sink designed for tests.", | ||
@@ -15,8 +15,9 @@ "main": "lib/main.js", | ||
"scripts": { | ||
"clean": "rimraf .tap node_modules types", | ||
"lint": "eslint .", | ||
"lint:fix": "eslint --fix .", | ||
"test": "run-s test:*", | ||
"test:unit": "tap --disable-coverage --allow-empty-coverage tests/**/*.js", | ||
"test:types": "tsc --project tsconfig.test.json", | ||
"types": "tsc --declaration --emitDeclarationOnly" | ||
"test": "tap --disable-coverage --allow-empty-coverage tests/**/*.js", | ||
"types": "run-s types:module types:test", | ||
"types:module": "tsc", | ||
"types:test": "tsc --project tsconfig.test.json" | ||
}, | ||
@@ -42,13 +43,14 @@ "repository": { | ||
"devDependencies": { | ||
"@semantic-release/changelog": "6.0.3", | ||
"@semantic-release/git": "10.0.1", | ||
"eslint": "9.1.1", | ||
"eslint-config-prettier": "9.1.0", | ||
"eslint-plugin-prettier": "5.1.3", | ||
"globals": "15.0.0", | ||
"@eik/eslint-config": "1.0.0", | ||
"@eik/prettier-config": "1.0.1", | ||
"@eik/semantic-release-config": "1.0.0", | ||
"@eik/typescript-config": "1.0.0", | ||
"eslint": "9.8.0", | ||
"npm-run-all": "4.1.5", | ||
"prettier": "3.3.2", | ||
"prettier": "3.3.3", | ||
"rimraf": "6.0.1", | ||
"semantic-release": "24.0.0", | ||
"tap": "18.8.0" | ||
"tap": "18.8.0", | ||
"typescript": "5.5.4" | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
12092
11
230