From 7e38731cb7922cab94a9d993e7b285b06294b179 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Tue, 28 Nov 2023 14:10:09 +0100 Subject: [PATCH 1/9] feat!: improve file client implementation --- packages/binding-file/README.md | 4 +- packages/binding-file/package.json | 7 +- packages/binding-file/src/file-client.ts | 58 ++++------- packages/binding-file/test/.gitignore | 1 + .../binding-file/test/file-client-test.ts | 96 +++++++++++++++++++ 5 files changed, 123 insertions(+), 43 deletions(-) create mode 100644 packages/binding-file/test/.gitignore create mode 100644 packages/binding-file/test/file-client-test.ts diff --git a/packages/binding-file/README.md b/packages/binding-file/README.md index 859bd9e9d..a64a6a396 100644 --- a/packages/binding-file/README.md +++ b/packages/binding-file/README.md @@ -41,7 +41,7 @@ td = { observable: false, forms: [ { - href: "file://test.txt", + href: "file:///test.txt", contentType: "text/plain", op: ["readproperty"], }, @@ -92,7 +92,7 @@ servient.addClientFactory(new FileClientFactory(null)); let wotHelper = new Helpers(servient); wotHelper - .fetch("file://TD.jsonld") + .fetch("file:///TD.jsonld") .then(async (td) => { // using await for serial execution (note 'async' in then() of fetch()) try { diff --git a/packages/binding-file/package.json b/packages/binding-file/package.json index 961a95167..524a1781f 100644 --- a/packages/binding-file/package.json +++ b/packages/binding-file/package.json @@ -21,7 +21,7 @@ }, "scripts": { "build": "tsc -b", - "test": "", + "test": "mocha --require ts-node/register --extension ts", "lint": "eslint .", "lint:fix": "eslint . --fix", "format": "prettier --write \"src/**/*.ts\" \"**/*.json\"" @@ -29,5 +29,8 @@ "bugs": { "url": "https://github.com/eclipse-thingweb/node-wot/issues" }, - "homepage": "https://github.com/eclipse-thingweb/node-wot/tree/master/packages/binding-file#readme" + "homepage": "https://github.com/eclipse-thingweb/node-wot/tree/master/packages/binding-file#readme", + "directories": { + "test": "test" + } } diff --git a/packages/binding-file/src/file-client.ts b/packages/binding-file/src/file-client.ts index f52c15c42..267cb4309 100644 --- a/packages/binding-file/src/file-client.ts +++ b/packages/binding-file/src/file-client.ts @@ -17,59 +17,39 @@ * File protocol binding */ import { Form, SecurityScheme } from "@node-wot/td-tools"; -import { ProtocolClient, Content, createLoggers } from "@node-wot/core"; +import { ProtocolClient, Content, createLoggers, ContentSerdes } from "@node-wot/core"; import { Subscription } from "rxjs/Subscription"; import fs = require("fs"); -import path = require("path"); +import { fileURLToPath } from "node:url"; -const { debug, warn } = createLoggers("binding-file", "file-client"); - -/** - * Used to determine the Content-Type of a file from the extension in its - * {@link filePath} if no explicit Content-Type is defined. - * - * @param filepath The file path the Content-Type is determined for. - * @returns An appropriate Content-Type or `application/octet-stream` as a fallback. - */ -function mapFileExtensionToContentType(filepath: string) { - const fileExtension = path.extname(filepath); - debug(`FileClient found '${fileExtension}' extension`); - switch (fileExtension) { - case ".txt": - case ".log": - case ".ini": - case ".cfg": - return "text/plain"; - case ".json": - return "application/json"; - case ".jsontd": - return "application/td+json"; - case ".jsonld": - return "application/ld+json"; - default: - warn(`FileClient cannot determine media type for path '${filepath}'`); - return "application/octet-stream"; - } -} +const { debug } = createLoggers("binding-file", "file-client"); export default class FileClient implements ProtocolClient { public toString(): string { return "[FileClient]"; } - private async readFile(filepath: string, contentType?: string): Promise { - const resource = fs.createReadStream(filepath); - const resourceContentType = contentType ?? mapFileExtensionToContentType(filepath); - return new Content(resourceContentType, resource); + private async readFromFile(filePath: string, contentType: string) { + debug(`Reading file of Content-Type ${contentType} from path ${filePath}.`); + const resource = fs.createReadStream(filePath); + return new Content(contentType, resource); } public async readResource(form: Form): Promise { - const filepath = new URL(form.href).pathname; - return this.readFile(filepath, form.contentType); + const filePath = fileURLToPath(form.href); + const contentType = form.contentType ?? ContentSerdes.DEFAULT; + + return this.readFromFile(filePath, contentType); } public async writeResource(form: Form, content: Content): Promise { - throw new Error("FileClient does not implement write"); + const filePath = fileURLToPath(form.href); + content.toBuffer(); + + const writeStream = fs.createWriteStream(filePath); + const buffer = await content.toBuffer(); + + writeStream.end(buffer); } public async invokeResource(form: Form, content: Content): Promise { @@ -84,7 +64,7 @@ export default class FileClient implements ProtocolClient { * @inheritdoc */ public async requestThingDescription(uri: string): Promise { - return this.readFile(uri, "application/td+json"); + return this.readFromFile(uri, "application/td+json"); } public async subscribeResource( diff --git a/packages/binding-file/test/.gitignore b/packages/binding-file/test/.gitignore new file mode 100644 index 000000000..653f16014 --- /dev/null +++ b/packages/binding-file/test/.gitignore @@ -0,0 +1 @@ +test.* diff --git a/packages/binding-file/test/file-client-test.ts b/packages/binding-file/test/file-client-test.ts new file mode 100644 index 000000000..b5d39152e --- /dev/null +++ b/packages/binding-file/test/file-client-test.ts @@ -0,0 +1,96 @@ +/******************************************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ + +import { Content, ContentSerdes } from "@node-wot/core"; + +import FileClient from "../src/file-client"; +import { Form } from "@node-wot/td-tools"; +import { expect } from "chai"; +import { unlink } from "fs"; +import { fileURLToPath } from "node:url"; + +const jsonValue = { + foo: "bar", +}; + +function formatContentType(contentType?: string) { + if (contentType == null) { + return "no Content-Type"; + } + + return `Content-Type ${contentType}`; +} + +describe("File Client Implementation", () => { + let fileClient: FileClient; + + beforeEach(async () => { + fileClient = new FileClient(); + await fileClient.start(); + }); + + afterEach(async () => { + await fileClient.stop(); + }); + + for (const uriScheme of ["file:///", "file://"]) { + for (const testData of [ + { + value: jsonValue, + contentType: "application/json", + fileName: "test.json", + }, + { value: jsonValue, contentType: undefined, fileName: "test.json" }, + { value: "Lorem ipsum dolor sit amet.", contentType: "text/plain", fileName: "test.txt" }, + ]) { + it(`should be able to write and read files using URI scheme ${uriScheme} with ${formatContentType( + testData.contentType + )}`, async () => { + const contentType = testData.contentType; + const originalValue = testData.value; + const fileName = testData.fileName; + + // eslint-disable-next-line n/no-path-concat + const href = `${uriScheme}${__dirname}/${fileName}`; + const filePath = fileURLToPath(href); + + const form: Form = { + href, + contentType, + }; + + const writeContent = ContentSerdes.get().valueToContent( + originalValue, + undefined, + contentType ?? ContentSerdes.DEFAULT + ); + + await fileClient.writeResource(form, writeContent); + + const rawContent: Content = await fileClient.readResource(form); + + const readContent = { + body: await rawContent.toBuffer(), + type: writeContent.type, + }; + + const readValue = ContentSerdes.get().contentToValue(readContent, {}); + expect(readValue).to.deep.eq(originalValue); + + unlink(filePath, () => {}); + }); + } + } +}); From 3aee0ca4d7a43fd7978733208eb9b9a9a6aeb273 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Wed, 29 Nov 2023 11:45:53 +0100 Subject: [PATCH 2/9] fixup! feat!: improve file client implementation --- packages/binding-file/src/file-client.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/binding-file/src/file-client.ts b/packages/binding-file/src/file-client.ts index 267cb4309..c640e43e2 100644 --- a/packages/binding-file/src/file-client.ts +++ b/packages/binding-file/src/file-client.ts @@ -44,7 +44,6 @@ export default class FileClient implements ProtocolClient { public async writeResource(form: Form, content: Content): Promise { const filePath = fileURLToPath(form.href); - content.toBuffer(); const writeStream = fs.createWriteStream(filePath); const buffer = await content.toBuffer(); From 0780ba4585858dafda5db7abd5dcc87d871e7460 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Wed, 29 Nov 2023 11:51:21 +0100 Subject: [PATCH 3/9] fixup! feat!: improve file client implementation --- packages/binding-file/src/file-client.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/binding-file/src/file-client.ts b/packages/binding-file/src/file-client.ts index c640e43e2..f29c106b8 100644 --- a/packages/binding-file/src/file-client.ts +++ b/packages/binding-file/src/file-client.ts @@ -37,7 +37,12 @@ export default class FileClient implements ProtocolClient { public async readResource(form: Form): Promise { const filePath = fileURLToPath(form.href); - const contentType = form.contentType ?? ContentSerdes.DEFAULT; + + const formContentType = form.contentType; + if (form.contentType == null) { + debug(`Found no Content-Type for Form, defaulting to ${ContentSerdes.DEFAULT}`); + } + const contentType = formContentType ?? ContentSerdes.DEFAULT; return this.readFromFile(filePath, contentType); } From 1b79e91ea7696b86429f028fa0a0db84fdd88e73 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Wed, 29 Nov 2023 12:01:59 +0100 Subject: [PATCH 4/9] fixup! feat!: improve file client implementation --- packages/binding-file/README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/binding-file/README.md b/packages/binding-file/README.md index a64a6a396..696cd2fe3 100644 --- a/packages/binding-file/README.md +++ b/packages/binding-file/README.md @@ -87,16 +87,14 @@ FileClientFactory = require("@node-wot/binding-file").FileClientFactory; Helpers = require("@node-wot/core").Helpers; // create Servient and add File binding -let servient = new Servient(); +const servient = new Servient(); servient.addClientFactory(new FileClientFactory(null)); -let wotHelper = new Helpers(servient); -wotHelper - .fetch("file:///TD.jsonld") - .then(async (td) => { - // using await for serial execution (note 'async' in then() of fetch()) +servient.start() + .then(async () => { + // using await for serial execution (note 'async' in then() of start()) try { - const WoT = await servient.start(); + const td = await WoT.requestThingDescription("file:///TD.jsonld") const thing = await WoT.consume(td); // read property "fileContent" and print the content From 1a817ab5b0afa078ccd847a0906128e365ba5341 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Wed, 29 Nov 2023 12:08:39 +0100 Subject: [PATCH 5/9] fixup! feat!: improve file client implementation --- packages/binding-file/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/binding-file/README.md b/packages/binding-file/README.md index 696cd2fe3..f5d4b1fc6 100644 --- a/packages/binding-file/README.md +++ b/packages/binding-file/README.md @@ -90,11 +90,12 @@ Helpers = require("@node-wot/core").Helpers; const servient = new Servient(); servient.addClientFactory(new FileClientFactory(null)); -servient.start() +servient + .start() .then(async () => { // using await for serial execution (note 'async' in then() of start()) try { - const td = await WoT.requestThingDescription("file:///TD.jsonld") + const td = await WoT.requestThingDescription("file:///TD.jsonld"); const thing = await WoT.consume(td); // read property "fileContent" and print the content From 3793b6ecbb76883e91dd2bcf25dee88d87cea1c5 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Wed, 29 Nov 2023 13:37:32 +0100 Subject: [PATCH 6/9] fixup! feat!: improve file client implementation --- packages/binding-file/README.md | 14 ++++++-------- packages/binding-file/src/file-client.ts | 8 ++++---- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/binding-file/README.md b/packages/binding-file/README.md index f5d4b1fc6..831f5b00b 100644 --- a/packages/binding-file/README.md +++ b/packages/binding-file/README.md @@ -22,11 +22,11 @@ The example tries to load an internal TestThing TD and reads the `fileContent` p ```js // example.js1 -Servient = require("@node-wot/core").Servient; -FileClientFactory = require("@node-wot/binding-file").FileClientFactory; +const Servient = require("@node-wot/core").Servient; +const FileClientFactory = require("@node-wot/binding-file").FileClientFactory; // create Servient and add File binding -let servient = new Servient(); +const servient = new Servient(); servient.addClientFactory(new FileClientFactory(null)); td = { @@ -81,10 +81,8 @@ The example tries to load a locally stored TestThing TD and reads the `fileConte ```js // example2.js -Servient = require("@node-wot/core").Servient; -FileClientFactory = require("@node-wot/binding-file").FileClientFactory; - -Helpers = require("@node-wot/core").Helpers; +const Servient = require("@node-wot/core").Servient; +const FileClientFactory = require("@node-wot/binding-file").FileClientFactory; // create Servient and add File binding const servient = new Servient(); @@ -92,7 +90,7 @@ servient.addClientFactory(new FileClientFactory(null)); servient .start() - .then(async () => { + .then(async (WoT) => { // using await for serial execution (note 'async' in then() of start()) try { const td = await WoT.requestThingDescription("file:///TD.jsonld"); diff --git a/packages/binding-file/src/file-client.ts b/packages/binding-file/src/file-client.ts index f29c106b8..2ae3e801b 100644 --- a/packages/binding-file/src/file-client.ts +++ b/packages/binding-file/src/file-client.ts @@ -29,22 +29,22 @@ export default class FileClient implements ProtocolClient { return "[FileClient]"; } - private async readFromFile(filePath: string, contentType: string) { + private async readFromFile(uri: string, contentType: string) { + const filePath = fileURLToPath(uri); debug(`Reading file of Content-Type ${contentType} from path ${filePath}.`); + const resource = fs.createReadStream(filePath); return new Content(contentType, resource); } public async readResource(form: Form): Promise { - const filePath = fileURLToPath(form.href); - const formContentType = form.contentType; if (form.contentType == null) { debug(`Found no Content-Type for Form, defaulting to ${ContentSerdes.DEFAULT}`); } const contentType = formContentType ?? ContentSerdes.DEFAULT; - return this.readFromFile(filePath, contentType); + return this.readFromFile(form.href, contentType); } public async writeResource(form: Form, content: Content): Promise { From 385760b8885e00e8affc9787ba1bc313ad1227e5 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 30 Nov 2023 09:39:07 +0100 Subject: [PATCH 7/9] fixup! feat!: improve file client implementation Co-authored-by: danielpeintner --- packages/binding-file/src/file-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/binding-file/src/file-client.ts b/packages/binding-file/src/file-client.ts index 2ae3e801b..cc9b34aa5 100644 --- a/packages/binding-file/src/file-client.ts +++ b/packages/binding-file/src/file-client.ts @@ -39,7 +39,7 @@ export default class FileClient implements ProtocolClient { public async readResource(form: Form): Promise { const formContentType = form.contentType; - if (form.contentType == null) { + if (formContentType == null) { debug(`Found no Content-Type for Form, defaulting to ${ContentSerdes.DEFAULT}`); } const contentType = formContentType ?? ContentSerdes.DEFAULT; From 832d69c35b0302527b8b6cda8e550049493be6c6 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 30 Nov 2023 11:44:00 +0100 Subject: [PATCH 8/9] fixup! feat!: improve file client implementation --- packages/binding-file/test/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/binding-file/test/.gitignore b/packages/binding-file/test/.gitignore index 653f16014..eaad84fe9 100644 --- a/packages/binding-file/test/.gitignore +++ b/packages/binding-file/test/.gitignore @@ -1 +1,2 @@ +# Ignore auxiliary files created by the FileClient implementation tests test.* From 5bdcb7c052b587772b1cfc715ce26be7f10f8482 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 30 Nov 2023 12:29:25 +0100 Subject: [PATCH 9/9] fixup! feat!: improve file client implementation --- packages/binding-file/test/file-client-test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/binding-file/test/file-client-test.ts b/packages/binding-file/test/file-client-test.ts index b5d39152e..d6ebaa39d 100644 --- a/packages/binding-file/test/file-client-test.ts +++ b/packages/binding-file/test/file-client-test.ts @@ -46,21 +46,21 @@ describe("File Client Implementation", () => { }); for (const uriScheme of ["file:///", "file://"]) { - for (const testData of [ + for (const [index, testData] of [ { value: jsonValue, contentType: "application/json", - fileName: "test.json", + fileExtension: "json", }, - { value: jsonValue, contentType: undefined, fileName: "test.json" }, - { value: "Lorem ipsum dolor sit amet.", contentType: "text/plain", fileName: "test.txt" }, - ]) { + { value: jsonValue, contentType: undefined, fileExtension: "json" }, + { value: "Lorem ipsum dolor sit amet.", contentType: "text/plain", fileExtension: "txt" }, + ].entries()) { it(`should be able to write and read files using URI scheme ${uriScheme} with ${formatContentType( testData.contentType )}`, async () => { const contentType = testData.contentType; const originalValue = testData.value; - const fileName = testData.fileName; + const fileName = `test${index}.${testData.fileExtension}`; // eslint-disable-next-line n/no-path-concat const href = `${uriScheme}${__dirname}/${fileName}`;