node-opcua-file-transfer 2.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mocharc.yml +11 -0
- package/LICENSE +20 -0
- package/dist/client/client_file.d.ts +68 -0
- package/dist/client/client_file.js +310 -0
- package/dist/client/client_file.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/open_mode.d.ts +39 -0
- package/dist/open_mode.js +45 -0
- package/dist/open_mode.js.map +1 -0
- package/dist/server/file_type_helpers.d.ts +68 -0
- package/dist/server/file_type_helpers.js +493 -0
- package/dist/server/file_type_helpers.js.map +1 -0
- package/package.json +58 -0
- package/readme.md +204 -0
- package/source/client/client_file.ts +322 -0
- package/source/index.ts +5 -0
- package/source/open_mode.ts +41 -0
- package/source/server/file_type_helpers.ts +643 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-file-transfer
|
|
3
|
+
*/
|
|
4
|
+
import * as fsOrig from "fs";
|
|
5
|
+
import { Stats, PathLike, OpenMode, NoParamCallback, WriteFileOptions } from "fs";
|
|
6
|
+
|
|
7
|
+
import { callbackify, promisify } from "util";
|
|
8
|
+
|
|
9
|
+
import { assert } from "node-opcua-assert";
|
|
10
|
+
import { AddressSpace, IAddressSpace, ISessionContext, UAFile, UAFile_Base, UAMethod, UAObjectType } from "node-opcua-address-space";
|
|
11
|
+
import { Byte, Int32, UInt32, UInt64 } from "node-opcua-basic-types";
|
|
12
|
+
import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
|
|
13
|
+
import { NodeId, sameNodeId } from "node-opcua-nodeid";
|
|
14
|
+
import { CallMethodResultOptions } from "node-opcua-service-call";
|
|
15
|
+
import { StatusCodes } from "node-opcua-status-code";
|
|
16
|
+
import { DataType, Variant, VariantArrayType } from "node-opcua-variant";
|
|
17
|
+
|
|
18
|
+
import { OpenFileMode, OpenFileModeMask } from "../open_mode";
|
|
19
|
+
|
|
20
|
+
const debugLog = make_debugLog("FileType");
|
|
21
|
+
const errorLog = make_errorLog("FileType");
|
|
22
|
+
const warningLog = make_warningLog("FileType");
|
|
23
|
+
const doDebug = checkDebugFlag("FileType");
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
export interface AbstractFs {
|
|
27
|
+
stat(path: PathLike, callback: (err: NodeJS.ErrnoException | null, stats: Stats) => void): void;
|
|
28
|
+
|
|
29
|
+
open(path: PathLike, flags: OpenMode, callback: (err: NodeJS.ErrnoException | null, fd: number) => void): void;
|
|
30
|
+
|
|
31
|
+
write<TBuffer extends NodeJS.ArrayBufferView>(
|
|
32
|
+
fd: number,
|
|
33
|
+
buffer: TBuffer,
|
|
34
|
+
offset: number | undefined | null,
|
|
35
|
+
length: number | undefined | null,
|
|
36
|
+
position: number | undefined | null,
|
|
37
|
+
callback: (err: NodeJS.ErrnoException | null, bytesWritten: number, buffer: TBuffer) => void
|
|
38
|
+
): void;
|
|
39
|
+
|
|
40
|
+
read<TBuffer extends NodeJS.ArrayBufferView>(
|
|
41
|
+
fd: number,
|
|
42
|
+
buffer: TBuffer,
|
|
43
|
+
offset: number,
|
|
44
|
+
length: number,
|
|
45
|
+
position: number | null,
|
|
46
|
+
callback: (err: NodeJS.ErrnoException | null, bytesRead: number, buffer: TBuffer) => void
|
|
47
|
+
): void;
|
|
48
|
+
|
|
49
|
+
close(fd: number, callback: NoParamCallback): void;
|
|
50
|
+
|
|
51
|
+
writeFile(
|
|
52
|
+
path: PathLike | number,
|
|
53
|
+
data: string | NodeJS.ArrayBufferView,
|
|
54
|
+
options: WriteFileOptions,
|
|
55
|
+
callback: NoParamCallback
|
|
56
|
+
): void;
|
|
57
|
+
|
|
58
|
+
readFile(
|
|
59
|
+
path: PathLike | number,
|
|
60
|
+
options: { encoding: BufferEncoding; flag?: string } | string,
|
|
61
|
+
callback: (err: NodeJS.ErrnoException | null, data: string) => void
|
|
62
|
+
): void;
|
|
63
|
+
// readFile(path: PathLike | number, options: { encoding?: null; flag?: string; } | undefined | null, callback: (err: NodeJS.ErrnoException | null, data: Buffer) => void): void;
|
|
64
|
+
|
|
65
|
+
existsSync(filename: string): boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
*
|
|
70
|
+
*/
|
|
71
|
+
export interface FileOptions {
|
|
72
|
+
/**
|
|
73
|
+
* the filaname of the physical file which is managed by the OPCUA filetpye
|
|
74
|
+
*/
|
|
75
|
+
filename: string;
|
|
76
|
+
/**
|
|
77
|
+
* the maximum allowed size of the phisical file.
|
|
78
|
+
*/
|
|
79
|
+
maxSize?: number;
|
|
80
|
+
/**
|
|
81
|
+
* an optional mimeType
|
|
82
|
+
*/
|
|
83
|
+
mineType?: string;
|
|
84
|
+
|
|
85
|
+
fileSystem?: AbstractFs;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface UAFileType extends UAObjectType, UAFile_Base {
|
|
89
|
+
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
*
|
|
93
|
+
*/
|
|
94
|
+
export class FileTypeData {
|
|
95
|
+
public _fs: AbstractFs;
|
|
96
|
+
public filename= "";
|
|
97
|
+
public maxSize = 0;
|
|
98
|
+
public mimeType = "";
|
|
99
|
+
|
|
100
|
+
private file: UAFile;
|
|
101
|
+
private _openCount = 0;
|
|
102
|
+
private _fileSize = 0;
|
|
103
|
+
|
|
104
|
+
constructor(options: FileOptions, file: UAFile) {
|
|
105
|
+
this.file = file;
|
|
106
|
+
this._fs = options.fileSystem || fsOrig;
|
|
107
|
+
|
|
108
|
+
this.filename = options.filename;
|
|
109
|
+
this.maxSize = options.maxSize!;
|
|
110
|
+
this.mimeType = options.mineType || "";
|
|
111
|
+
// openCount indicates the number of currently valid file handles on the file.
|
|
112
|
+
this._openCount = 0;
|
|
113
|
+
file.openCount.bindVariable(
|
|
114
|
+
{
|
|
115
|
+
get: () => new Variant({ dataType: DataType.UInt16, value: this._openCount })
|
|
116
|
+
},
|
|
117
|
+
true
|
|
118
|
+
);
|
|
119
|
+
file.openCount.minimumSamplingInterval = 0; // changed immediatly
|
|
120
|
+
|
|
121
|
+
file.size.bindVariable(
|
|
122
|
+
{
|
|
123
|
+
get: () => new Variant({ dataType: DataType.UInt64, value: this._fileSize })
|
|
124
|
+
},
|
|
125
|
+
true
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
file.size.minimumSamplingInterval = 0; // changed immediatly
|
|
129
|
+
|
|
130
|
+
this.refresh();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public set openCount(value: number) {
|
|
134
|
+
this._openCount = value;
|
|
135
|
+
this.file.openCount.touchValue();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public get openCount(): number {
|
|
139
|
+
return this._openCount;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public set fileSize(value: number) {
|
|
143
|
+
this._fileSize = value;
|
|
144
|
+
this.file.size.touchValue();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public get fileSize(): number {
|
|
148
|
+
return this._fileSize;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* refresh position and size
|
|
153
|
+
* this method should be call by the server if the file
|
|
154
|
+
* is modified externally
|
|
155
|
+
*
|
|
156
|
+
*/
|
|
157
|
+
public async refresh(): Promise<void> {
|
|
158
|
+
const abstractFs = this._fs;
|
|
159
|
+
|
|
160
|
+
// lauch an async request to update filesize
|
|
161
|
+
await (async function extractFileSize(self: FileTypeData) {
|
|
162
|
+
try {
|
|
163
|
+
if (!abstractFs.existsSync(self.filename)) {
|
|
164
|
+
self._fileSize = 0;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const stat = await promisify(abstractFs.stat)(self.filename);
|
|
168
|
+
self._fileSize = stat.size;
|
|
169
|
+
debugLog("original file size ", self.filename, " size = ", self._fileSize);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
self._fileSize = 0;
|
|
172
|
+
if (err instanceof Error) {
|
|
173
|
+
warningLog("Cannot access file ", self.filename, err.message);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
})(this);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function getFileData(opcuaFile2: UAFileType): FileTypeData {
|
|
181
|
+
return (opcuaFile2 as any).$fileData as FileTypeData;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface FileAccessData {
|
|
185
|
+
handle: number;
|
|
186
|
+
fd: number; // nodejs handler
|
|
187
|
+
position: UInt64; // position in file
|
|
188
|
+
size: number; // size
|
|
189
|
+
openMode: OpenFileMode;
|
|
190
|
+
sessionId: NodeId;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
interface FileTypeM {
|
|
194
|
+
$$currentFileHandle: number;
|
|
195
|
+
$$files: { [key: number]: FileAccessData };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
interface AddressSpacePriv extends IAddressSpace, FileTypeM {}
|
|
199
|
+
function _prepare(addressSpace: IAddressSpace, context: ISessionContext): FileTypeM {
|
|
200
|
+
const _context = addressSpace as AddressSpacePriv;
|
|
201
|
+
_context.$$currentFileHandle = _context.$$currentFileHandle ? _context.$$currentFileHandle : 41;
|
|
202
|
+
_context.$$files = _context.$$files || {};
|
|
203
|
+
return _context as FileTypeM;
|
|
204
|
+
}
|
|
205
|
+
function _getSessionId(context: ISessionContext) {
|
|
206
|
+
if (!context.session) {
|
|
207
|
+
return NodeId.nullNodeId;
|
|
208
|
+
}
|
|
209
|
+
assert(context.session && context.session.getSessionId);
|
|
210
|
+
return context.session?.getSessionId() || NodeId.nullNodeId;
|
|
211
|
+
}
|
|
212
|
+
function _addFile(addressSpace: IAddressSpace, context: ISessionContext, openMode: OpenFileMode): UInt32 {
|
|
213
|
+
const _context = _prepare(addressSpace, context);
|
|
214
|
+
_context.$$currentFileHandle++;
|
|
215
|
+
const fileHandle: number = _context.$$currentFileHandle;
|
|
216
|
+
const sessionId = _getSessionId(context);
|
|
217
|
+
const _fileData: FileAccessData = {
|
|
218
|
+
fd: -1,
|
|
219
|
+
handle: fileHandle,
|
|
220
|
+
openMode,
|
|
221
|
+
position: [0, 0],
|
|
222
|
+
size: 0,
|
|
223
|
+
sessionId
|
|
224
|
+
};
|
|
225
|
+
_context.$$files[fileHandle] = _fileData;
|
|
226
|
+
|
|
227
|
+
return fileHandle;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function _getFileInfo(addressSpace: IAddressSpace, context: ISessionContext, fileHandle: UInt32): FileAccessData | null {
|
|
231
|
+
const _context = _prepare(addressSpace, context);
|
|
232
|
+
const _fileInfo = _context.$$files[fileHandle];
|
|
233
|
+
const sessionId = _getSessionId(context);
|
|
234
|
+
|
|
235
|
+
if (!_fileInfo || !sameNodeId(_fileInfo.sessionId, sessionId)) {
|
|
236
|
+
errorLog("Invalid session ID this file descriptor doesn't belong to this session");
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return _fileInfo;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function _close(addressSpace: IAddressSpace, context: ISessionContext, fileData: FileAccessData) {
|
|
243
|
+
const _context = _prepare(addressSpace, context);
|
|
244
|
+
delete _context.$$files[fileData.fd];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function toNodeJSMode(opcuaMode: OpenFileMode): string {
|
|
248
|
+
let flags: string;
|
|
249
|
+
switch (opcuaMode) {
|
|
250
|
+
case OpenFileMode.Read:
|
|
251
|
+
flags = "r";
|
|
252
|
+
break;
|
|
253
|
+
case OpenFileMode.ReadWrite:
|
|
254
|
+
case OpenFileMode.Write:
|
|
255
|
+
flags = "w+";
|
|
256
|
+
break;
|
|
257
|
+
case OpenFileMode.ReadWriteAppend:
|
|
258
|
+
case OpenFileMode.WriteAppend:
|
|
259
|
+
flags = "a+";
|
|
260
|
+
break;
|
|
261
|
+
case OpenFileMode.WriteEraseExisting:
|
|
262
|
+
case OpenFileMode.ReadWriteEraseExisting:
|
|
263
|
+
flags = "w+";
|
|
264
|
+
break;
|
|
265
|
+
default:
|
|
266
|
+
flags = "?";
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
return flags;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Open is used to open a file represented by an Object of FileType.
|
|
274
|
+
* When a client opens a file it gets a file handle that is valid while the
|
|
275
|
+
* session is open. Clients shall use the Close Method to release the handle
|
|
276
|
+
* when they do not need access to the file anymore. Clients can open the
|
|
277
|
+
* same file several times for read.
|
|
278
|
+
* A request to open for writing shall return Bad_NotWritable when the file is
|
|
279
|
+
* already opened.
|
|
280
|
+
* A request to open for reading shall return Bad_NotReadable
|
|
281
|
+
* when the file is already opened for writing.
|
|
282
|
+
*
|
|
283
|
+
* Method Result Codes (defined in Call Service)
|
|
284
|
+
* Result Code Description
|
|
285
|
+
* BadNotReadable File might be locked and thus not readable.
|
|
286
|
+
* BadNotWritable The file is locked and thus not writable.
|
|
287
|
+
* BadInvalidState
|
|
288
|
+
* BadInvalidArgument Mode setting is invalid.
|
|
289
|
+
* BadNotFound .
|
|
290
|
+
* BadUnexpectedError
|
|
291
|
+
*
|
|
292
|
+
* @private
|
|
293
|
+
*/
|
|
294
|
+
|
|
295
|
+
async function _openFile(this: UAMethod, inputArguments: Variant[], context: ISessionContext): Promise<CallMethodResultOptions> {
|
|
296
|
+
const addressSpace = this.addressSpace;
|
|
297
|
+
const mode = inputArguments[0].value as Byte;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* mode (Byte) Indicates whether the file should be opened only for read operations
|
|
301
|
+
* or for read and write operations and where the initial position is set.
|
|
302
|
+
* The mode is an 8-bit unsigned integer used as bit mask with the structure
|
|
303
|
+
* defined in the following table:
|
|
304
|
+
* Field Bit Description
|
|
305
|
+
* Read 0 The file is opened for reading. If this bit is not
|
|
306
|
+
* set the Read Method cannot be executed.
|
|
307
|
+
* Write 1 The file is opened for writing. If this bit is not
|
|
308
|
+
* set the Write Method cannot be executed.
|
|
309
|
+
* EraseExisting 2 This bit can only be set if the file is opened for writing
|
|
310
|
+
* (Write bit is set). The existing content of the file is
|
|
311
|
+
* erased and an empty file is provided.
|
|
312
|
+
* Append 3 When the Append bit is set the file is opened at end
|
|
313
|
+
* of the file, otherwise at begin of the file.
|
|
314
|
+
* The SetPosition Method can be used to change the position.
|
|
315
|
+
* Reserved 4:7 Reserved for future use. Shall always be zero.
|
|
316
|
+
*/
|
|
317
|
+
|
|
318
|
+
// see https://nodejs.org/api/fs.html#fs_file_system_flags
|
|
319
|
+
|
|
320
|
+
const flags = toNodeJSMode(mode);
|
|
321
|
+
if (flags === "?") {
|
|
322
|
+
errorLog("Invalid mode " + OpenFileMode[mode] + " (" + mode + ")");
|
|
323
|
+
return { statusCode: StatusCodes.BadInvalidArgument };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* fileHandle (UInt32) A handle for the file used in other method calls indicating not the
|
|
328
|
+
* file (this is done by the Object of the Method call) but the access
|
|
329
|
+
* request and thus the position in the file. The fileHandle is generated
|
|
330
|
+
* by the server and is unique for the Session. Clients cannot transfer the
|
|
331
|
+
* fileHandle to another Session but need to get a new fileHandle by calling
|
|
332
|
+
* the Open Method.
|
|
333
|
+
*/
|
|
334
|
+
const fileHandle = _addFile(addressSpace, context, mode as OpenFileMode);
|
|
335
|
+
|
|
336
|
+
const _fileInfo = _getFileInfo(addressSpace, context, fileHandle);
|
|
337
|
+
if (!_fileInfo) {
|
|
338
|
+
return { statusCode: StatusCodes.BadInvalidArgument };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const fileData = (context.object as any).$fileData as FileTypeData;
|
|
342
|
+
|
|
343
|
+
const filename = fileData.filename;
|
|
344
|
+
|
|
345
|
+
const abstractFs = _getFileSystem(context);
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
_fileInfo.fd = await promisify(abstractFs.open)(filename, flags);
|
|
349
|
+
|
|
350
|
+
// update position
|
|
351
|
+
_fileInfo.position = [0, 0];
|
|
352
|
+
|
|
353
|
+
const fileLength = (await promisify(abstractFs.stat)(filename)).size;
|
|
354
|
+
_fileInfo.size = fileLength;
|
|
355
|
+
|
|
356
|
+
// tslint:disable-next-line:no-bitwise
|
|
357
|
+
if ((mode & OpenFileModeMask.AppendBit) === OpenFileModeMask.AppendBit) {
|
|
358
|
+
_fileInfo.position[1] = fileLength;
|
|
359
|
+
}
|
|
360
|
+
if ((mode & OpenFileModeMask.EraseExistingBit) === OpenFileModeMask.EraseExistingBit) {
|
|
361
|
+
_fileInfo.size = 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
fileData.openCount += 1;
|
|
365
|
+
} catch (err) {
|
|
366
|
+
if (err instanceof Error) {
|
|
367
|
+
errorLog(err.message);
|
|
368
|
+
errorLog(err.stack);
|
|
369
|
+
}
|
|
370
|
+
return { statusCode: StatusCodes.BadUnexpectedError };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
debugLog("Opening file handle ", fileHandle, "filename: ", fileData.filename, "openCount: ", fileData.openCount);
|
|
374
|
+
|
|
375
|
+
const callMethodResult = {
|
|
376
|
+
outputArguments: [
|
|
377
|
+
{
|
|
378
|
+
dataType: DataType.UInt32,
|
|
379
|
+
value: fileHandle
|
|
380
|
+
}
|
|
381
|
+
],
|
|
382
|
+
statusCode: StatusCodes.Good
|
|
383
|
+
};
|
|
384
|
+
return callMethodResult;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function _getFileSystem(context: ISessionContext) {
|
|
388
|
+
const fs: AbstractFs = (context.object as any).$fs;
|
|
389
|
+
return fs;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Close is used to close a file represented by a FileType.
|
|
394
|
+
* When a client closes a file the handle becomes invalid.
|
|
395
|
+
*
|
|
396
|
+
* @param inputArguments
|
|
397
|
+
* @param context
|
|
398
|
+
* @private
|
|
399
|
+
*/
|
|
400
|
+
async function _closeFile(this: UAMethod, inputArguments: Variant[], context: ISessionContext): Promise<CallMethodResultOptions> {
|
|
401
|
+
const abstractFs = _getFileSystem(context);
|
|
402
|
+
|
|
403
|
+
const addressSpace = this.addressSpace;
|
|
404
|
+
|
|
405
|
+
const fileHandle: UInt32 = inputArguments[0].value as UInt32;
|
|
406
|
+
|
|
407
|
+
const _fileInfo = _getFileInfo(addressSpace, context, fileHandle);
|
|
408
|
+
if (!_fileInfo) {
|
|
409
|
+
return { statusCode: StatusCodes.BadInvalidArgument };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const data = (context.object as any).$fileData as FileTypeData;
|
|
413
|
+
|
|
414
|
+
debugLog("Closing file handle ", fileHandle, "filename: ", data.filename, "openCount: ", data.openCount);
|
|
415
|
+
|
|
416
|
+
await promisify(abstractFs.close)(_fileInfo.fd);
|
|
417
|
+
_close(addressSpace, context, _fileInfo);
|
|
418
|
+
data.openCount -= 1;
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
statusCode: StatusCodes.Good
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Read is used to read a part of the file starting from the current file position.
|
|
427
|
+
* The file position is advanced by the number of bytes read.
|
|
428
|
+
*
|
|
429
|
+
* @param inputArguments
|
|
430
|
+
* @param context
|
|
431
|
+
* @private
|
|
432
|
+
*/
|
|
433
|
+
async function _readFile(this: UAMethod, inputArguments: Variant[], context: ISessionContext): Promise<CallMethodResultOptions> {
|
|
434
|
+
const addressSpace = this.addressSpace;
|
|
435
|
+
|
|
436
|
+
const abstractFs = _getFileSystem(context);
|
|
437
|
+
|
|
438
|
+
// fileHandle A handle indicating the access request and thus indirectly the
|
|
439
|
+
// position inside the file.
|
|
440
|
+
const fileHandle: UInt32 = inputArguments[0].value as UInt32;
|
|
441
|
+
|
|
442
|
+
// Length Defines the length in bytes that should be returned in data, starting from the current
|
|
443
|
+
// position of the file handle. If the end of file is reached all data until the end of the file is
|
|
444
|
+
// returned. The Server is allowed to return less data than specified length.
|
|
445
|
+
let length: Int32 = inputArguments[1].value as Int32;
|
|
446
|
+
|
|
447
|
+
// Only positive values are allowed.
|
|
448
|
+
if (length < 0) {
|
|
449
|
+
return { statusCode: StatusCodes.BadInvalidArgument };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const _fileInfo = _getFileInfo(addressSpace, context, fileHandle);
|
|
453
|
+
if (!_fileInfo) {
|
|
454
|
+
return { statusCode: StatusCodes.BadInvalidState };
|
|
455
|
+
}
|
|
456
|
+
// tslint:disable-next-line:no-bitwise
|
|
457
|
+
if ((_fileInfo.openMode & OpenFileModeMask.ReadBit) === 0x0) {
|
|
458
|
+
// open mode did not specify Read Flag
|
|
459
|
+
return { statusCode: StatusCodes.BadInvalidState };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
length = Math.min(_fileInfo.size - _fileInfo.position[1], length);
|
|
463
|
+
|
|
464
|
+
const data = Buffer.alloc(length);
|
|
465
|
+
|
|
466
|
+
let ret = { bytesRead: 0 };
|
|
467
|
+
try {
|
|
468
|
+
// note: we do not util.promise here as it has a wierd behavior...
|
|
469
|
+
ret = await new Promise((resolve, reject) =>
|
|
470
|
+
abstractFs.read(_fileInfo.fd, data, 0, length, _fileInfo.position[1], (err, bytesRead, buff) => {
|
|
471
|
+
if (err) {
|
|
472
|
+
return reject(err);
|
|
473
|
+
}
|
|
474
|
+
return resolve({ bytesRead });
|
|
475
|
+
})
|
|
476
|
+
);
|
|
477
|
+
_fileInfo.position[1] += ret.bytesRead;
|
|
478
|
+
} catch (err) {
|
|
479
|
+
if (err instanceof Error) {
|
|
480
|
+
errorLog("Read error : ", err.message);
|
|
481
|
+
}
|
|
482
|
+
return { statusCode: StatusCodes.BadUnexpectedError };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Data Contains the returned data of the file. If the ByteString is empty it indicates that the end
|
|
486
|
+
// of the file is reached.
|
|
487
|
+
return {
|
|
488
|
+
outputArguments: [{ dataType: DataType.ByteString, value: data.slice(0, ret.bytesRead) }],
|
|
489
|
+
statusCode: StatusCodes.Good
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function _writeFile(this: UAMethod, inputArguments: Variant[], context: ISessionContext): Promise<CallMethodResultOptions> {
|
|
494
|
+
const addressSpace = this.addressSpace;
|
|
495
|
+
|
|
496
|
+
const abstractFs = _getFileSystem(context);
|
|
497
|
+
|
|
498
|
+
const fileHandle: UInt32 = inputArguments[0].value as UInt32;
|
|
499
|
+
|
|
500
|
+
const _fileInfo = _getFileInfo(addressSpace, context, fileHandle);
|
|
501
|
+
if (!_fileInfo) {
|
|
502
|
+
return { statusCode: StatusCodes.BadInvalidArgument };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// tslint:disable-next-line:no-bitwise
|
|
506
|
+
if ((_fileInfo.openMode & OpenFileModeMask.WriteBit) === 0x00) {
|
|
507
|
+
// File has not been open with write mode
|
|
508
|
+
return { statusCode: StatusCodes.BadInvalidState };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const data: Buffer = inputArguments[1].value as Buffer;
|
|
512
|
+
|
|
513
|
+
let ret = { bytesWritten: 0 };
|
|
514
|
+
try {
|
|
515
|
+
// note: we do not util.promise here as it has a wierd behavior...
|
|
516
|
+
ret = await new Promise((resolve, reject) => {
|
|
517
|
+
abstractFs.write(_fileInfo.fd, data, 0, data.length, _fileInfo.position[1], (err, bytesWritten) => {
|
|
518
|
+
if (err) {
|
|
519
|
+
errorLog("Err", err);
|
|
520
|
+
return reject(err);
|
|
521
|
+
}
|
|
522
|
+
return resolve({ bytesWritten });
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
assert(typeof ret.bytesWritten === "number");
|
|
526
|
+
_fileInfo.position[1] += ret.bytesWritten;
|
|
527
|
+
_fileInfo.size = Math.max(_fileInfo.size, _fileInfo.position[1]);
|
|
528
|
+
|
|
529
|
+
const fileTypeData = (context.object as any).$fileData as FileTypeData;
|
|
530
|
+
debugLog(fileTypeData.fileSize);
|
|
531
|
+
fileTypeData.fileSize = Math.max(fileTypeData.fileSize, _fileInfo.position[1]);
|
|
532
|
+
debugLog(fileTypeData.fileSize);
|
|
533
|
+
} catch (err) {
|
|
534
|
+
if (err instanceof Error) {
|
|
535
|
+
errorLog("Write error : ", err.message);
|
|
536
|
+
}
|
|
537
|
+
return { statusCode: StatusCodes.BadUnexpectedError };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
outputArguments: [],
|
|
542
|
+
statusCode: StatusCodes.Good
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function _setPositionFile(
|
|
547
|
+
this: UAMethod,
|
|
548
|
+
inputArguments: Variant[],
|
|
549
|
+
context: ISessionContext
|
|
550
|
+
): Promise<CallMethodResultOptions> {
|
|
551
|
+
const addressSpace = this.addressSpace;
|
|
552
|
+
|
|
553
|
+
const fileHandle: UInt32 = inputArguments[0].value as UInt32;
|
|
554
|
+
const position: UInt64 = inputArguments[1].value as UInt64;
|
|
555
|
+
|
|
556
|
+
const _fileInfo = _getFileInfo(addressSpace, context, fileHandle);
|
|
557
|
+
if (!_fileInfo) {
|
|
558
|
+
return { statusCode: StatusCodes.BadInvalidArgument };
|
|
559
|
+
}
|
|
560
|
+
_fileInfo.position = position;
|
|
561
|
+
return { statusCode: StatusCodes.Good };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function _getPositionFile(
|
|
565
|
+
this: UAMethod,
|
|
566
|
+
inputArguments: Variant[],
|
|
567
|
+
context: ISessionContext
|
|
568
|
+
): Promise<CallMethodResultOptions> {
|
|
569
|
+
const addressSpace = this.addressSpace;
|
|
570
|
+
|
|
571
|
+
const fileHandle: UInt32 = inputArguments[0].value as UInt32;
|
|
572
|
+
|
|
573
|
+
const _fileInfo = _getFileInfo(addressSpace, context, fileHandle);
|
|
574
|
+
if (!_fileInfo) {
|
|
575
|
+
return { statusCode: StatusCodes.BadInvalidArgument };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
outputArguments: [
|
|
580
|
+
{
|
|
581
|
+
arrayType: VariantArrayType.Scalar,
|
|
582
|
+
dataType: DataType.UInt64,
|
|
583
|
+
value: _fileInfo.position
|
|
584
|
+
}
|
|
585
|
+
],
|
|
586
|
+
statusCode: StatusCodes.Good
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export const defaultMaxSize = 100000000;
|
|
591
|
+
|
|
592
|
+
function install_method_handle_on_type(addressSpace: IAddressSpace): void {
|
|
593
|
+
const fileType = addressSpace.findObjectType("FileType") as any;
|
|
594
|
+
if (fileType.open.isBound()) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
fileType.open.bindMethod(callbackify(_openFile));
|
|
598
|
+
fileType.close.bindMethod(callbackify(_closeFile));
|
|
599
|
+
fileType.read.bindMethod(callbackify(_readFile));
|
|
600
|
+
fileType.write.bindMethod(callbackify(_writeFile));
|
|
601
|
+
fileType.setPosition.bindMethod(callbackify(_setPositionFile));
|
|
602
|
+
fileType.getPosition.bindMethod(callbackify(_getPositionFile));
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* bind all methods of a UAFileType OPCUA node
|
|
607
|
+
* @param file the OPCUA Node that has a typeDefinition of FileType
|
|
608
|
+
* @param options the options
|
|
609
|
+
*/
|
|
610
|
+
export function installFileType(file: UAFile, options: FileOptions): void {
|
|
611
|
+
if ((file as any).$fileData) {
|
|
612
|
+
errorLog("File already installed ", file.nodeId.toString(), file.browseName.toString());
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
(file as any).$fs = options.fileSystem || fsOrig;
|
|
616
|
+
|
|
617
|
+
// make sure that FileType methods are also bound.
|
|
618
|
+
install_method_handle_on_type(file.addressSpace);
|
|
619
|
+
|
|
620
|
+
// to protect the server we setup a maximum limit in bytes on the file
|
|
621
|
+
// if the client try to access or set the position above this limit
|
|
622
|
+
// the server will return an error
|
|
623
|
+
options.maxSize = options.maxSize === undefined ? defaultMaxSize : options.maxSize;
|
|
624
|
+
|
|
625
|
+
const $fileData = new FileTypeData(options, file);
|
|
626
|
+
(file as any).$fileData = $fileData;
|
|
627
|
+
|
|
628
|
+
// ----- install mime type
|
|
629
|
+
if (options.mineType) {
|
|
630
|
+
if (file.mimeType) {
|
|
631
|
+
file.mimeType.bindVariable({
|
|
632
|
+
get: () => (file as any).$fileOptions.mineType
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
file.open.bindMethod(callbackify(_openFile));
|
|
638
|
+
file.close.bindMethod(callbackify(_closeFile));
|
|
639
|
+
file.read.bindMethod(callbackify(_readFile));
|
|
640
|
+
file.write.bindMethod(callbackify(_writeFile));
|
|
641
|
+
file.setPosition.bindMethod(callbackify(_setPositionFile));
|
|
642
|
+
file.getPosition.bindMethod(callbackify(_getPositionFile));
|
|
643
|
+
}
|