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 91d4a2305..79b5412a1 100644 --- a/packages/binding-file/src/file-client.ts +++ b/packages/binding-file/src/file-client.ts @@ -17,51 +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"); +const { debug } = createLoggers("binding-file", "file-client"); export default class FileClient implements ProtocolClient { public toString(): string { return "[FileClient]"; } - public async readResource(form: Form): Promise { - const filepath = form.href.split("//"); - const resource = fs.createReadStream(filepath[1]); - const extension = path.extname(filepath[1]); - debug(`FileClient found '${extension}' extension`); - let contentType; - if (form.contentType != null) { - contentType = form.contentType; - } else { - // *guess* contentType based on file extension - contentType = "application/octet-stream"; - switch (extension) { - case ".txt": - case ".log": - case ".ini": - case ".cfg": - contentType = "text/plain"; - break; - case ".json": - contentType = "application/json"; - break; - case ".jsonld": - contentType = "application/ld+json"; - break; - default: - warn(`FileClient cannot determine media type of '${form.href}'`); - } - } + 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 = 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 { 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, () => {}); + }); + } + } +});