Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: improve file client implementation #1175

Merged
merged 9 commits into from
Dec 1, 2023
27 changes: 12 additions & 15 deletions packages/binding-file/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -41,7 +41,7 @@ td = {
observable: false,
forms: [
{
href: "file://test.txt",
href: "file:///test.txt",
contentType: "text/plain",
op: ["readproperty"],
},
Expand Down Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions packages/binding-file/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@
},
"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\""
},
"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"
}
}
62 changes: 23 additions & 39 deletions packages/binding-file/src/file-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Content> {
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<Content> {
const filepath = new URL(form.href).pathname;
return this.readFile(filepath, form.contentType);
const formContentType = form.contentType;
if (form.contentType == null) {
JKRhb marked this conversation as resolved.
Show resolved Hide resolved
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<void> {
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<Content> {
Expand All @@ -84,7 +68,7 @@
* @inheritdoc
*/
public async requestThingDescription(uri: string): Promise<Content> {
return this.readFile(uri, "application/td+json");
return this.readFromFile(uri, "application/td+json");

Check warning on line 71 in packages/binding-file/src/file-client.ts

View check run for this annotation

Codecov / codecov/patch

packages/binding-file/src/file-client.ts#L71

Added line #L71 was not covered by tests
}

public async subscribeResource(
Expand Down
1 change: 1 addition & 0 deletions packages/binding-file/test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test.*
JKRhb marked this conversation as resolved.
Show resolved Hide resolved
96 changes: 96 additions & 0 deletions packages/binding-file/test/file-client-test.ts
Original file line number Diff line number Diff line change
@@ -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, () => {});
});
}
}
});
Loading