Skip to content

Commit 8761e64

Browse files
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 <daniel.peintner@gmail.com> * fixup! feat!: improve file client implementation * fixup! feat!: improve file client implementation --------- Co-authored-by: danielpeintner <daniel.peintner@gmail.com>
1 parent 5c06db1 commit 8761e64

5 files changed

Lines changed: 138 additions & 56 deletions

File tree

packages/binding-file/README.md

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ The example tries to load an internal TestThing TD and reads the `fileContent` p
2222

2323
```js
2424
// example.js1
25-
Servient = require("@node-wot/core").Servient;
26-
FileClientFactory = require("@node-wot/binding-file").FileClientFactory;
25+
const Servient = require("@node-wot/core").Servient;
26+
const FileClientFactory = require("@node-wot/binding-file").FileClientFactory;
2727

2828
// create Servient and add File binding
29-
let servient = new Servient();
29+
const servient = new Servient();
3030
servient.addClientFactory(new FileClientFactory(null));
3131

3232
td = {
@@ -41,7 +41,7 @@ td = {
4141
observable: false,
4242
forms: [
4343
{
44-
href: "file://test.txt",
44+
href: "file:///test.txt",
4545
contentType: "text/plain",
4646
op: ["readproperty"],
4747
},
@@ -81,22 +81,19 @@ The example tries to load a locally stored TestThing TD and reads the `fileConte
8181

8282
```js
8383
// example2.js
84-
Servient = require("@node-wot/core").Servient;
85-
FileClientFactory = require("@node-wot/binding-file").FileClientFactory;
86-
87-
Helpers = require("@node-wot/core").Helpers;
84+
const Servient = require("@node-wot/core").Servient;
85+
const FileClientFactory = require("@node-wot/binding-file").FileClientFactory;
8886

8987
// create Servient and add File binding
90-
let servient = new Servient();
88+
const servient = new Servient();
9189
servient.addClientFactory(new FileClientFactory(null));
9290

93-
let wotHelper = new Helpers(servient);
94-
wotHelper
95-
.fetch("file://TD.jsonld")
96-
.then(async (td) => {
97-
// using await for serial execution (note 'async' in then() of fetch())
91+
servient
92+
.start()
93+
.then(async (WoT) => {
94+
// using await for serial execution (note 'async' in then() of start())
9895
try {
99-
const WoT = await servient.start();
96+
const td = await WoT.requestThingDescription("file:///TD.jsonld");
10097
const thing = await WoT.consume(td);
10198

10299
// read property "fileContent" and print the content

packages/binding-file/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@
2121
},
2222
"scripts": {
2323
"build": "tsc -b",
24-
"test": "",
24+
"test": "mocha --require ts-node/register --extension ts",
2525
"lint": "eslint .",
2626
"lint:fix": "eslint . --fix",
2727
"format": "prettier --write \"src/**/*.ts\" \"**/*.json\""
2828
},
2929
"bugs": {
3030
"url": "https://github.com/eclipse-thingweb/node-wot/issues"
3131
},
32-
"homepage": "https://github.com/eclipse-thingweb/node-wot/tree/master/packages/binding-file#readme"
32+
"homepage": "https://github.com/eclipse-thingweb/node-wot/tree/master/packages/binding-file#readme",
33+
"directories": {
34+
"test": "test"
35+
}
3336
}

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

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,59 +17,43 @@
1717
* File protocol binding
1818
*/
1919
import { Form, SecurityScheme } from "@node-wot/td-tools";
20-
import { ProtocolClient, Content, createLoggers } from "@node-wot/core";
20+
import { ProtocolClient, Content, createLoggers, ContentSerdes } from "@node-wot/core";
2121
import { Subscription } from "rxjs/Subscription";
2222
import fs = require("fs");
23-
import path = require("path");
23+
import { fileURLToPath } from "node:url";
2424

25-
const { debug, warn } = createLoggers("binding-file", "file-client");
26-
27-
/**
28-
* Used to determine the Content-Type of a file from the extension in its
29-
* {@link filePath} if no explicit Content-Type is defined.
30-
*
31-
* @param filepath The file path the Content-Type is determined for.
32-
* @returns An appropriate Content-Type or `application/octet-stream` as a fallback.
33-
*/
34-
function mapFileExtensionToContentType(filepath: string) {
35-
const fileExtension = path.extname(filepath);
36-
debug(`FileClient found '${fileExtension}' extension`);
37-
switch (fileExtension) {
38-
case ".txt":
39-
case ".log":
40-
case ".ini":
41-
case ".cfg":
42-
return "text/plain";
43-
case ".json":
44-
return "application/json";
45-
case ".jsontd":
46-
return "application/td+json";
47-
case ".jsonld":
48-
return "application/ld+json";
49-
default:
50-
warn(`FileClient cannot determine media type for path '${filepath}'`);
51-
return "application/octet-stream";
52-
}
53-
}
25+
const { debug } = createLoggers("binding-file", "file-client");
5426

5527
export default class FileClient implements ProtocolClient {
5628
public toString(): string {
5729
return "[FileClient]";
5830
}
5931

60-
private async readFile(filepath: string, contentType?: string): Promise<Content> {
61-
const resource = fs.createReadStream(filepath);
62-
const resourceContentType = contentType ?? mapFileExtensionToContentType(filepath);
63-
return new Content(resourceContentType, resource);
32+
private async readFromFile(uri: string, contentType: string) {
33+
const filePath = fileURLToPath(uri);
34+
debug(`Reading file of Content-Type ${contentType} from path ${filePath}.`);
35+
36+
const resource = fs.createReadStream(filePath);
37+
return new Content(contentType, resource);
6438
}
6539

6640
public async readResource(form: Form): Promise<Content> {
67-
const filepath = new URL(form.href).pathname;
68-
return this.readFile(filepath, form.contentType);
41+
const formContentType = form.contentType;
42+
if (formContentType == null) {
43+
debug(`Found no Content-Type for Form, defaulting to ${ContentSerdes.DEFAULT}`);
44+
}
45+
const contentType = formContentType ?? ContentSerdes.DEFAULT;
46+
47+
return this.readFromFile(form.href, contentType);
6948
}
7049

7150
public async writeResource(form: Form, content: Content): Promise<void> {
72-
throw new Error("FileClient does not implement write");
51+
const filePath = fileURLToPath(form.href);
52+
53+
const writeStream = fs.createWriteStream(filePath);
54+
const buffer = await content.toBuffer();
55+
56+
writeStream.end(buffer);
7357
}
7458

7559
public async invokeResource(form: Form, content: Content): Promise<Content> {
@@ -84,7 +68,7 @@ export default class FileClient implements ProtocolClient {
8468
* @inheritdoc
8569
*/
8670
public async requestThingDescription(uri: string): Promise<Content> {
87-
return this.readFile(uri, "application/td+json");
71+
return this.readFromFile(uri, "application/td+json");
8872
}
8973

9074
public async subscribeResource(
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Ignore auxiliary files created by the FileClient implementation tests
2+
test.*
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/********************************************************************************
2+
* Copyright (c) 2023 Contributors to the Eclipse Foundation
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License v. 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
10+
* Document License (2015-05-13) which is available at
11+
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
12+
*
13+
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
14+
********************************************************************************/
15+
16+
import { Content, ContentSerdes } from "@node-wot/core";
17+
18+
import FileClient from "../src/file-client";
19+
import { Form } from "@node-wot/td-tools";
20+
import { expect } from "chai";
21+
import { unlink } from "fs";
22+
import { fileURLToPath } from "node:url";
23+
24+
const jsonValue = {
25+
foo: "bar",
26+
};
27+
28+
function formatContentType(contentType?: string) {
29+
if (contentType == null) {
30+
return "no Content-Type";
31+
}
32+
33+
return `Content-Type ${contentType}`;
34+
}
35+
36+
describe("File Client Implementation", () => {
37+
let fileClient: FileClient;
38+
39+
beforeEach(async () => {
40+
fileClient = new FileClient();
41+
await fileClient.start();
42+
});
43+
44+
afterEach(async () => {
45+
await fileClient.stop();
46+
});
47+
48+
for (const uriScheme of ["file:///", "file://"]) {
49+
for (const [index, testData] of [
50+
{
51+
value: jsonValue,
52+
contentType: "application/json",
53+
fileExtension: "json",
54+
},
55+
{ value: jsonValue, contentType: undefined, fileExtension: "json" },
56+
{ value: "Lorem ipsum dolor sit amet.", contentType: "text/plain", fileExtension: "txt" },
57+
].entries()) {
58+
it(`should be able to write and read files using URI scheme ${uriScheme} with ${formatContentType(
59+
testData.contentType
60+
)}`, async () => {
61+
const contentType = testData.contentType;
62+
const originalValue = testData.value;
63+
const fileName = `test${index}.${testData.fileExtension}`;
64+
65+
// eslint-disable-next-line n/no-path-concat
66+
const href = `${uriScheme}${__dirname}/${fileName}`;
67+
const filePath = fileURLToPath(href);
68+
69+
const form: Form = {
70+
href,
71+
contentType,
72+
};
73+
74+
const writeContent = ContentSerdes.get().valueToContent(
75+
originalValue,
76+
undefined,
77+
contentType ?? ContentSerdes.DEFAULT
78+
);
79+
80+
await fileClient.writeResource(form, writeContent);
81+
82+
const rawContent: Content = await fileClient.readResource(form);
83+
84+
const readContent = {
85+
body: await rawContent.toBuffer(),
86+
type: writeContent.type,
87+
};
88+
89+
const readValue = ContentSerdes.get().contentToValue(readContent, {});
90+
expect(readValue).to.deep.eq(originalValue);
91+
92+
unlink(filePath, () => {});
93+
});
94+
}
95+
}
96+
});

0 commit comments

Comments
 (0)