Skip to content

Commit

Permalink
feat!: improve file client implementation (#1175)
Browse files Browse the repository at this point in the history
* 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 <daniel.peintner@gmail.com>

* fixup! feat!: improve file client implementation

* fixup! feat!: improve file client implementation

---------

Co-authored-by: danielpeintner <daniel.peintner@gmail.com>
  • Loading branch information
JKRhb and danielpeintner authored Dec 1, 2023
1 parent 5c06db1 commit 8761e64
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 56 deletions.
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 (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<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 @@ export default class FileClient implements ProtocolClient {
* @inheritdoc
*/
public async requestThingDescription(uri: string): Promise<Content> {
return this.readFile(uri, "application/td+json");
return this.readFromFile(uri, "application/td+json");
}

public async subscribeResource(
Expand Down
2 changes: 2 additions & 0 deletions packages/binding-file/test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Ignore auxiliary files created by the FileClient implementation tests
test.*
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 [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, () => {});
});
}
}
});

0 comments on commit 8761e64

Please sign in to comment.