@eik/sink-file-system
Advanced tools
Comparing version 1.0.1 to 1.0.2
@@ -0,1 +1,8 @@ | ||
## [1.0.2](https://github.com/eik-lib/sink-file-system/compare/v1.0.1...v1.0.2) (2024-11-13) | ||
### Bug Fixes | ||
* **deps:** update dependency @metrics/client to v2.5.4 ([#16](https://github.com/eik-lib/sink-file-system/issues/16)) ([800b226](https://github.com/eik-lib/sink-file-system/commit/800b22602af2d7e326948d8914262ee0226356b2)) | ||
## [1.0.1](https://github.com/eik-lib/sink-file-system/compare/v1.0.0...v1.0.1) (2024-07-29) | ||
@@ -2,0 +9,0 @@ |
536
lib/main.js
@@ -1,9 +0,9 @@ | ||
import fs from 'node:fs'; | ||
import os from 'node:os'; | ||
import path from 'node:path'; | ||
import { ReadFile } from '@eik/common'; | ||
import Sink from '@eik/sink'; | ||
import Metrics from '@metrics/client'; | ||
import mime from 'mime'; | ||
import { rimraf } from 'rimraf'; | ||
import fs from "node:fs"; | ||
import os from "node:os"; | ||
import path from "node:path"; | ||
import { ReadFile } from "@eik/common"; | ||
import Sink from "@eik/sink"; | ||
import Metrics from "@metrics/client"; | ||
import mime from "mime"; | ||
import { rimraf } from "rimraf"; | ||
@@ -15,5 +15,5 @@ /** | ||
const etagFromFsStat = (stat) => { | ||
const mtime = stat.mtime.getTime().toString(16); | ||
const size = stat.size.toString(16); | ||
return `W/"${size}-${mtime}"`; | ||
const mtime = stat.mtime.getTime().toString(16); | ||
const size = stat.size.toString(16); | ||
return `W/"${size}-${mtime}"`; | ||
}; | ||
@@ -42,298 +42,298 @@ | ||
export default class SinkFileSystem extends Sink { | ||
/** | ||
* @type {Required<SinkFileSystemOptions>} | ||
*/ | ||
_config; | ||
/** | ||
* @type {Required<SinkFileSystemOptions>} | ||
*/ | ||
_config; | ||
/** @type {import('@metrics/client')} */ | ||
_metrics; | ||
/** @type {import('@metrics/client')} */ | ||
_metrics; | ||
/** | ||
* @param {SinkFileSystemOptions} options | ||
*/ | ||
constructor(options = {}) { | ||
super(); | ||
this._config = { | ||
sinkFsRootPath: path.join(os.tmpdir(), '/eik-files'), | ||
...options, | ||
}; | ||
this._metrics = new Metrics(); | ||
this._counter = this._metrics.counter({ | ||
name: 'eik_core_sink_fs', | ||
description: | ||
'Counter measuring access to the file system storage sink', | ||
labels: { | ||
operation: 'n/a', | ||
success: false, | ||
access: false, | ||
}, | ||
}); | ||
} | ||
/** | ||
* @param {SinkFileSystemOptions} options | ||
*/ | ||
constructor(options = {}) { | ||
super(); | ||
this._config = { | ||
sinkFsRootPath: path.join(os.tmpdir(), "/eik-files"), | ||
...options, | ||
}; | ||
this._metrics = new Metrics(); | ||
this._counter = this._metrics.counter({ | ||
name: "eik_core_sink_fs", | ||
description: | ||
"Counter measuring access to the file system 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<import('node:stream').Writable>} | ||
*/ | ||
write(filePath, contentType) { | ||
return new Promise((resolve, reject) => { | ||
const operation = 'write'; | ||
/** | ||
* @param {string} filePath | ||
* @param {string} contentType | ||
* @returns {Promise<import('node:stream').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 = path.join(this._config.sinkFsRootPath, filePath); | ||
const pathname = path.join(this._config.sinkFsRootPath, filePath); | ||
if (pathname.indexOf(this._config.sinkFsRootPath) !== 0) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(new Error(`Directory traversal - ${filePath}`)); | ||
return; | ||
} | ||
if (pathname.indexOf(this._config.sinkFsRootPath) !== 0) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(new Error(`Directory traversal - ${filePath}`)); | ||
return; | ||
} | ||
const dir = path.dirname(pathname); | ||
const dir = path.dirname(pathname); | ||
fs.mkdir( | ||
dir, | ||
{ | ||
recursive: true, | ||
}, | ||
(error) => { | ||
if (error) { | ||
this._counter.inc({ | ||
labels: { access: true, operation }, | ||
}); | ||
reject( | ||
new Error(`Could not create directory - ${dir}`), | ||
); | ||
return; | ||
} | ||
fs.mkdir( | ||
dir, | ||
{ | ||
recursive: true, | ||
}, | ||
(error) => { | ||
if (error) { | ||
this._counter.inc({ | ||
labels: { access: true, operation }, | ||
}); | ||
reject( | ||
new Error(`Could not create directory - ${dir}`), | ||
); | ||
return; | ||
} | ||
const stream = fs.createWriteStream(pathname, { | ||
autoClose: true, | ||
emitClose: true, | ||
}); | ||
const stream = fs.createWriteStream(pathname, { | ||
autoClose: true, | ||
emitClose: true, | ||
}); | ||
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 = path.join(this._config.sinkFsRootPath, filePath); | ||
const pathname = path.join(this._config.sinkFsRootPath, filePath); | ||
if (pathname.indexOf(this._config.sinkFsRootPath) !== 0) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(new Error(`Directory traversal - ${filePath}`)); | ||
return; | ||
} | ||
if (pathname.indexOf(this._config.sinkFsRootPath) !== 0) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(new Error(`Directory traversal - ${filePath}`)); | ||
return; | ||
} | ||
const closeFd = (fd) => { | ||
fs.close(fd, (error) => { | ||
if (error) { | ||
this._counter.inc({ | ||
labels: { | ||
access: true, | ||
operation, | ||
}, | ||
}); | ||
return; | ||
} | ||
this._counter.inc({ | ||
labels: { | ||
success: true, | ||
access: true, | ||
operation, | ||
}, | ||
}); | ||
}); | ||
}; | ||
const closeFd = (fd) => { | ||
fs.close(fd, (error) => { | ||
if (error) { | ||
this._counter.inc({ | ||
labels: { | ||
access: true, | ||
operation, | ||
}, | ||
}); | ||
return; | ||
} | ||
this._counter.inc({ | ||
labels: { | ||
success: true, | ||
access: true, | ||
operation, | ||
}, | ||
}); | ||
}); | ||
}; | ||
fs.open(pathname, 'r', (error, fd) => { | ||
if (error) { | ||
this._counter.inc({ | ||
labels: { | ||
access: true, | ||
operation, | ||
}, | ||
}); | ||
reject(error); | ||
return; | ||
} | ||
fs.open(pathname, "r", (error, fd) => { | ||
if (error) { | ||
this._counter.inc({ | ||
labels: { | ||
access: true, | ||
operation, | ||
}, | ||
}); | ||
reject(error); | ||
return; | ||
} | ||
fs.fstat(fd, (err, stat) => { | ||
if (err) { | ||
closeFd(fd); | ||
reject(err); | ||
return; | ||
} | ||
fs.fstat(fd, (err, stat) => { | ||
if (err) { | ||
closeFd(fd); | ||
reject(err); | ||
return; | ||
} | ||
if (!stat.isFile()) { | ||
closeFd(fd); | ||
reject(new Error(`Not a file - ${pathname}`)); | ||
return; | ||
} | ||
if (!stat.isFile()) { | ||
closeFd(fd); | ||
reject(new Error(`Not a file - ${pathname}`)); | ||
return; | ||
} | ||
const mimeType = | ||
mime.getType(pathname) || 'application/octet-stream'; | ||
const etag = etagFromFsStat(stat); | ||
const mimeType = | ||
mime.getType(pathname) || "application/octet-stream"; | ||
const etag = etagFromFsStat(stat); | ||
const obj = new ReadFile({ | ||
mimeType, | ||
etag, | ||
}); | ||
const obj = new ReadFile({ | ||
mimeType, | ||
etag, | ||
}); | ||
obj.stream = fs.createReadStream(pathname, { | ||
autoClose: true, | ||
fd, | ||
}); | ||
obj.stream = fs.createReadStream(pathname, { | ||
autoClose: true, | ||
fd, | ||
}); | ||
obj.stream.on('error', () => { | ||
this._counter.inc({ | ||
labels: { | ||
access: true, | ||
operation, | ||
}, | ||
}); | ||
}); | ||
obj.stream.on("error", () => { | ||
this._counter.inc({ | ||
labels: { | ||
access: true, | ||
operation, | ||
}, | ||
}); | ||
}); | ||
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 | ||
* @returns {Promise<void>} | ||
*/ | ||
delete(filePath) { | ||
return new Promise((resolve, reject) => { | ||
const operation = 'delete'; | ||
/** | ||
* @param {string} filePath | ||
* @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 = path.join(this._config.sinkFsRootPath, filePath); | ||
const pathname = path.join(this._config.sinkFsRootPath, filePath); | ||
if (pathname.indexOf(this._config.sinkFsRootPath) !== 0) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(new Error(`Directory traversal - ${filePath}`)); | ||
return; | ||
} | ||
if (pathname.indexOf(this._config.sinkFsRootPath) !== 0) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(new Error(`Directory traversal - ${filePath}`)); | ||
return; | ||
} | ||
rimraf(pathname) | ||
.then(() => { | ||
this._counter.inc({ | ||
labels: { | ||
success: true, | ||
access: true, | ||
operation, | ||
}, | ||
}); | ||
resolve(); | ||
}) | ||
.catch((error) => { | ||
this._counter.inc({ labels: { access: true, operation } }); | ||
reject(error); | ||
}); | ||
}); | ||
} | ||
rimraf(pathname) | ||
.then(() => { | ||
this._counter.inc({ | ||
labels: { | ||
success: true, | ||
access: true, | ||
operation, | ||
}, | ||
}); | ||
resolve(); | ||
}) | ||
.catch((error) => { | ||
this._counter.inc({ labels: { access: true, operation } }); | ||
reject(error); | ||
}); | ||
}); | ||
} | ||
/** | ||
* @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 = path.join(this._config.sinkFsRootPath, filePath); | ||
const pathname = path.join(this._config.sinkFsRootPath, filePath); | ||
if (pathname.indexOf(this._config.sinkFsRootPath) !== 0) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(new Error(`Directory traversal - ${filePath}`)); | ||
return; | ||
} | ||
if (pathname.indexOf(this._config.sinkFsRootPath) !== 0) { | ||
this._counter.inc({ labels: { operation } }); | ||
reject(new Error(`Directory traversal - ${filePath}`)); | ||
return; | ||
} | ||
fs.stat(pathname, (error, stat) => { | ||
this._counter.inc({ | ||
labels: { success: true, access: true, operation }, | ||
}); | ||
fs.stat(pathname, (error, stat) => { | ||
this._counter.inc({ | ||
labels: { success: true, access: true, operation }, | ||
}); | ||
if (stat && stat.isFile()) { | ||
resolve(); | ||
return; | ||
} | ||
if (stat && stat.isFile()) { | ||
resolve(); | ||
return; | ||
} | ||
if (error) { | ||
reject(error); | ||
return; | ||
} | ||
reject(); | ||
}); | ||
}); | ||
} | ||
if (error) { | ||
reject(error); | ||
return; | ||
} | ||
reject(); | ||
}); | ||
}); | ||
} | ||
get [Symbol.toStringTag]() { | ||
return 'SinkFileSystem'; | ||
} | ||
get [Symbol.toStringTag]() { | ||
return "SinkFileSystem"; | ||
} | ||
} |
{ | ||
"name": "@eik/sink-file-system", | ||
"version": "1.0.1", | ||
"version": "1.0.2", | ||
"description": "Sink implementation that persists files on the local file system.", | ||
@@ -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" | ||
}, | ||
@@ -39,20 +40,18 @@ "repository": { | ||
"@eik/sink": "1.2.5", | ||
"@metrics/client": "2.5.3", | ||
"mime": "3.0.0", | ||
"rimraf": "5.0.8" | ||
"@metrics/client": "2.5.4", | ||
"mime": "3.0.0" | ||
}, | ||
"devDependencies": { | ||
"@semantic-release/changelog": "6.0.3", | ||
"@semantic-release/git": "10.0.1", | ||
"@eik/eslint-config": "1.0.4", | ||
"@eik/prettier-config": "1.0.1", | ||
"@eik/semantic-release-config": "1.0.0", | ||
"@types/mime": "3.0.4", | ||
"@types/readable-stream": "4.0.15", | ||
"eslint": "9.1.1", | ||
"eslint-config-prettier": "9.1.0", | ||
"eslint-plugin-prettier": "5.1.3", | ||
"globals": "15.0.0", | ||
"npm-run-all": "4.1.5", | ||
"prettier": "3.3.2", | ||
"semantic-release": "24.0.0", | ||
"@types/readable-stream": "4.0.18", | ||
"eslint": "9.11.1", | ||
"npm-run-all2": "5.0.2", | ||
"prettier": "3.3.3", | ||
"rimraf": "6.0.1", | ||
"semantic-release": "24.1.3", | ||
"tap": "18.8.0" | ||
} | ||
} |
4
11
13183
+ Added@metrics/client@2.5.4(transitive)
+ Added@types/node@22.13.4(transitive)
+ Added@types/readable-stream@4.0.18(transitive)
+ Addedsafe-buffer@5.1.2(transitive)
+ Addedundici-types@6.20.0(transitive)
- Removedrimraf@5.0.8
- Removed@isaacs/cliui@8.0.2(transitive)
- Removed@metrics/client@2.5.3(transitive)
- Removed@pkgjs/parseargs@0.11.0(transitive)
- Removedansi-regex@5.0.16.1.0(transitive)
- Removedansi-styles@4.3.06.2.1(transitive)
- Removedcolor-convert@2.0.1(transitive)
- Removedcolor-name@1.1.4(transitive)
- Removedcross-spawn@7.0.6(transitive)
- Removedeastasianwidth@0.2.0(transitive)
- Removedemoji-regex@8.0.09.2.2(transitive)
- Removedforeground-child@3.3.0(transitive)
- Removedglob@10.4.5(transitive)
- Removedis-fullwidth-code-point@3.0.0(transitive)
- Removedisexe@2.0.0(transitive)
- Removedjackspeak@3.4.3(transitive)
- Removedlru-cache@10.4.3(transitive)
- Removedminimatch@9.0.5(transitive)
- Removedminipass@7.1.2(transitive)
- Removedpackage-json-from-dist@1.0.1(transitive)
- Removedpath-key@3.1.1(transitive)
- Removedpath-scurry@1.11.1(transitive)
- Removedrimraf@5.0.8(transitive)
- Removedshebang-command@2.0.0(transitive)
- Removedshebang-regex@3.0.0(transitive)
- Removedsignal-exit@4.1.0(transitive)
- Removedstring-width@4.2.35.1.2(transitive)
- Removedstrip-ansi@6.0.17.1.0(transitive)
- Removedwhich@2.0.2(transitive)
- Removedwrap-ansi@7.0.08.1.0(transitive)
Updated@metrics/client@2.5.4