From 8761e64f403e983e2aaf6565c9bdf12db8259566 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Fri, 1 Dec 2023 17:49:24 +0100 Subject: [PATCH] feat!: improve file client implementation (#1175) * feat!: improve file client implementation * fixup! feat!: improve file client implementation * fixup! feat!: improve file client implementation * fixup! feat!: improve file client implementation * fixup! feat!: improve file client implementation * fixup! feat!: improve file client implementation * fixup! feat!: improve file client implementation Co-authored-by: danielpeintner * fixup! feat!: improve file client implementation * fixup! feat!: improve file client implementation --------- Co-authored-by: danielpeintner --- packages/binding-file/README.md | 27 +++--- packages/binding-file/package.json | 7 +- packages/binding-file/src/file-client.ts | 62 +++++------- packages/binding-file/test/.gitignore | 2 + .../binding-file/test/file-client-test.ts | 96 +++++++++++++++++++ 5 files changed, 138 insertions(+), 56 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..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 = { @@ -41,7 +41,7 @@ td = { observable: false, forms: [ { - href: "file://test.txt", + href: "file:///test.txt", contentType: "text/plain", op: ["readproperty"], }, @@ -81,22 +81,19 @@ 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 -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 (WoT) => { + // 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 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..cc9b34aa5 100644 --- a/packages/binding-file/src/file-client.ts +++ b/packages/binding-file/src/file-client.ts @@ -17,59 +17,43 @@ * 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(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 = new URL(form.href).pathname; - return this.readFile(filepath, form.contentType); + const formContentType = form.contentType; + if (formContentType == null) { + debug(`Found no Content-Type for Form, defaulting to ${ContentSerdes.DEFAULT}`); + } + const contentType = formContentType ?? ContentSerdes.DEFAULT; + + return this.readFromFile(form.href, contentType); } public async writeResource(form: Form, content: Content): Promise { - throw new Error("FileClient does not implement write"); + const filePath = fileURLToPath(form.href); + + const writeStream = fs.createWriteStream(filePath); + const buffer = await content.toBuffer(); + + writeStream.end(buffer); } public async invokeResource(form: Form, content: Content): Promise { @@ -84,7 +68,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..eaad84fe9 --- /dev/null +++ b/packages/binding-file/test/.gitignore @@ -0,0 +1,2 @@ +# Ignore auxiliary files created by the FileClient implementation tests +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..d6ebaa39d --- /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 [index, testData] of [ + { + value: jsonValue, + contentType: "application/json", + fileExtension: "json", + }, + { 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 = `test${index}.${testData.fileExtension}`; + + // 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, () => {}); + }); + } + } +});