insite-ws-transfers 1.0.465
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/LICENSE +21 -0
- package/README.md +13 -0
- package/lib/IncomingTransfer.js +272 -0
- package/lib/IncomingTransport.js +92 -0
- package/lib/OutgoingTransfer.js +296 -0
- package/lib/OutgoingTransport.js +59 -0
- package/lib/StringStreamer.js +52 -0
- package/lib/browser/FileStreamer.js +63 -0
- package/lib/browser/arrayBufferToBase64.js +11 -0
- package/lib/browser/incoming.js +41 -0
- package/lib/browser/index.js +2 -0
- package/lib/browser/outgoing.js +61 -0
- package/lib/common.js +18 -0
- package/lib/node/incoming.js +108 -0
- package/lib/node/index.js +2 -0
- package/lib/node/outgoing.js +91 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Eugene Nesvetaev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { callArgsSymbol, headers, progressInterval } from "./common";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export class IncomingTransfer {
|
|
5
|
+
constructor(ws, kind, id, { type, collect, encoding, size, metadata }) {
|
|
6
|
+
this.ws = ws;
|
|
7
|
+
this[callArgsSymbol] = ws.wss ? [ ws.wss, ws ] : [ ws ];
|
|
8
|
+
this.incomingTransport = (ws.wss ?? ws).incomingTransport;
|
|
9
|
+
|
|
10
|
+
this.kind = kind;
|
|
11
|
+
this.id = id;
|
|
12
|
+
this.type = type;
|
|
13
|
+
this.collect = !!collect;
|
|
14
|
+
this.encoding = encoding;
|
|
15
|
+
this.size = size;
|
|
16
|
+
this.metadata = metadata;
|
|
17
|
+
|
|
18
|
+
this.listeners = this.incomingTransport.listeners.get(this.kind);
|
|
19
|
+
|
|
20
|
+
this.incomingTransport.transfers.set(this.id, this);
|
|
21
|
+
|
|
22
|
+
return (async () => {
|
|
23
|
+
await this.#setup();
|
|
24
|
+
|
|
25
|
+
this.confirm();
|
|
26
|
+
|
|
27
|
+
return this;
|
|
28
|
+
})();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#methods;
|
|
32
|
+
isAborted = false;
|
|
33
|
+
isAbortedBySender = false;
|
|
34
|
+
isAbortedByReceiver = false;
|
|
35
|
+
isTransfered = false;
|
|
36
|
+
confirmResponse;
|
|
37
|
+
beginAt = null;
|
|
38
|
+
#prevChunkAt = null;
|
|
39
|
+
#chunksQueue = [];
|
|
40
|
+
#isProcessing = false;
|
|
41
|
+
duration = null;
|
|
42
|
+
transferedSize = 0;
|
|
43
|
+
processedSize = 0;
|
|
44
|
+
bytesPerMs = null;
|
|
45
|
+
progress = 0;
|
|
46
|
+
endAt = null;
|
|
47
|
+
error = null;
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
#progressInterval;
|
|
51
|
+
#lastProgress = 0;
|
|
52
|
+
|
|
53
|
+
async #setup() {
|
|
54
|
+
|
|
55
|
+
this.#methods = this.constructor.methods[this.type];
|
|
56
|
+
|
|
57
|
+
if (this.#methods)
|
|
58
|
+
await this.#methods.setup.call(this);
|
|
59
|
+
else
|
|
60
|
+
throw new Error(`Unknown type of transfer "${this.type}"`);
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async confirm() {
|
|
65
|
+
|
|
66
|
+
for (const { begin } of this.listeners)
|
|
67
|
+
if (await begin?.call(...this[callArgsSymbol], this) === false)
|
|
68
|
+
return this.throw("Transfer was rejected by receiver");
|
|
69
|
+
|
|
70
|
+
this.beginAt =
|
|
71
|
+
this.#prevChunkAt =
|
|
72
|
+
Date.now();
|
|
73
|
+
|
|
74
|
+
this.ws.sendMessage(headers.confirm, this.id, this.confirmResponse);
|
|
75
|
+
|
|
76
|
+
this.#progressInterval = setInterval(() => {
|
|
77
|
+
|
|
78
|
+
if (this.#lastProgress !== this.progress) {
|
|
79
|
+
this.#lastProgress = this.progress;
|
|
80
|
+
this.ws.sendMessage(headers.progress, this.id, this.progress);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
}, progressInterval);
|
|
84
|
+
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
handleChunk(chunk, length = chunk.length) {
|
|
88
|
+
|
|
89
|
+
const ts = Date.now();
|
|
90
|
+
this.bytesPerMs = length / (ts - this.#prevChunkAt);
|
|
91
|
+
this.#prevChunkAt = ts;
|
|
92
|
+
this.transferedSize += length;
|
|
93
|
+
|
|
94
|
+
this.#chunksQueue.push([ chunk, length ]);
|
|
95
|
+
|
|
96
|
+
if (!this.#isProcessing) {
|
|
97
|
+
this.#isProcessing = true;
|
|
98
|
+
this.#process();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async #process() {
|
|
104
|
+
|
|
105
|
+
let [ chunk, length ] = this.#chunksQueue.shift();
|
|
106
|
+
|
|
107
|
+
if (this.#methods.transformChunk)
|
|
108
|
+
chunk = await this.#methods.transformChunk.call(this, chunk);
|
|
109
|
+
|
|
110
|
+
if (this.collect)
|
|
111
|
+
await this.#methods.collect.call(this, chunk);
|
|
112
|
+
|
|
113
|
+
for (const { chunk: chunkListener, progress: progressListener } of this.listeners)
|
|
114
|
+
await (chunkListener ?? progressListener)?.call(...this[callArgsSymbol], this, chunk);
|
|
115
|
+
|
|
116
|
+
this.processedSize += length;
|
|
117
|
+
if (this.size)
|
|
118
|
+
this.progress = this.processedSize / this.size;
|
|
119
|
+
|
|
120
|
+
if (this.#chunksQueue.length)
|
|
121
|
+
this.#process();
|
|
122
|
+
else {
|
|
123
|
+
this.#isProcessing = false;
|
|
124
|
+
|
|
125
|
+
if (this.isTransfered)
|
|
126
|
+
this.#complete();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
handleSent() {
|
|
132
|
+
|
|
133
|
+
this.isTransfered = true;
|
|
134
|
+
|
|
135
|
+
if (!this.size)
|
|
136
|
+
this.size = this.transferedSize;
|
|
137
|
+
|
|
138
|
+
this.endAt = Date.now();
|
|
139
|
+
this.duration = this.endAt - this.beginAt;
|
|
140
|
+
this.bytesPerMs = this.size / this.duration;
|
|
141
|
+
|
|
142
|
+
if (!this.#isProcessing)
|
|
143
|
+
this.#complete();
|
|
144
|
+
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async #complete() {
|
|
148
|
+
|
|
149
|
+
this.progress = 1;
|
|
150
|
+
|
|
151
|
+
await this.#methods.done?.call(this);
|
|
152
|
+
|
|
153
|
+
for (const { end } of this.listeners)
|
|
154
|
+
await end?.call(...this[callArgsSymbol], this);
|
|
155
|
+
|
|
156
|
+
clearInterval(this.#progressInterval);
|
|
157
|
+
|
|
158
|
+
this.incomingTransport.transfers.delete(this.id);
|
|
159
|
+
|
|
160
|
+
this.ws.sendMessage(headers.completed, this.id);
|
|
161
|
+
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
abort(bySender = false) {
|
|
165
|
+
|
|
166
|
+
this.isAborted = true;
|
|
167
|
+
this.isAbortedBySender = bySender;
|
|
168
|
+
this.isAbortedByReceiver = !bySender;
|
|
169
|
+
|
|
170
|
+
return this.throw(`Transfer is aborted by ${bySender ? "sender" : "receiver"}`, !bySender);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
throw(errorMessage, sendToSender = true) {
|
|
174
|
+
|
|
175
|
+
this.incomingTransport.transfers.delete(this.id);
|
|
176
|
+
|
|
177
|
+
clearInterval(this.#progressInterval);
|
|
178
|
+
|
|
179
|
+
this.error = new Error(errorMessage);
|
|
180
|
+
|
|
181
|
+
for (const { error } of this.listeners)
|
|
182
|
+
error?.call(...this[callArgsSymbol], this, this.error);
|
|
183
|
+
|
|
184
|
+
if (sendToSender)
|
|
185
|
+
this.ws.sendMessage(headers.error, this.id, errorMessage);
|
|
186
|
+
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
serialize() {
|
|
190
|
+
return {
|
|
191
|
+
id: this.id,
|
|
192
|
+
kind: this.kind,
|
|
193
|
+
type: this.type,
|
|
194
|
+
collect: this.collect,
|
|
195
|
+
encoding: this.encoding,
|
|
196
|
+
size: this.size,
|
|
197
|
+
metadata: this.metadata,
|
|
198
|
+
isAborted: this.isAborted,
|
|
199
|
+
isAbortedBySender: this.isAbortedBySender,
|
|
200
|
+
isAbortedByReceiver: this.isAbortedByReceiver,
|
|
201
|
+
isTransfered: this.isTransfered,
|
|
202
|
+
confirmResponse: this.confirmResponse,
|
|
203
|
+
beginAt: this.beginAt,
|
|
204
|
+
duration: this.duration,
|
|
205
|
+
transferedSize: this.transferedSize,
|
|
206
|
+
processedSize: this.processedSize,
|
|
207
|
+
bytesPerMs: this.bytesPerMs,
|
|
208
|
+
progress: this.progress,
|
|
209
|
+
endAt: this.endAt,
|
|
210
|
+
error: this.error
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
static methods = {
|
|
216
|
+
|
|
217
|
+
object: {
|
|
218
|
+
setup() {
|
|
219
|
+
|
|
220
|
+
this.data = "";
|
|
221
|
+
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
collect(chunk) {
|
|
225
|
+
this.data += chunk;
|
|
226
|
+
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
done() {
|
|
230
|
+
this.data = JSON.parse(this.data);
|
|
231
|
+
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
datauri: {
|
|
236
|
+
setup() {
|
|
237
|
+
|
|
238
|
+
if (this.collect)
|
|
239
|
+
this.data = "";
|
|
240
|
+
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
collect(chunk) {
|
|
244
|
+
this.data += chunk;
|
|
245
|
+
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
done() {
|
|
249
|
+
|
|
250
|
+
if (this.collect)
|
|
251
|
+
this.data = `data:${this.metadata.type};base64,${this.data}`;
|
|
252
|
+
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
string: {
|
|
257
|
+
setup() {
|
|
258
|
+
|
|
259
|
+
if (this.collect)
|
|
260
|
+
this.data = "";
|
|
261
|
+
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
collect(chunk) {
|
|
265
|
+
this.data += chunk;
|
|
266
|
+
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { headers, sizeLimit as defaultSizeLimit } from "./common";
|
|
2
|
+
import { IncomingTransfer } from "./IncomingTransfer";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export class IncomingTransport {
|
|
6
|
+
constructor(ws, options = {}) {
|
|
7
|
+
ws.incomingTransport = this;
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
sizeLimit = defaultSizeLimit
|
|
11
|
+
} = options;
|
|
12
|
+
|
|
13
|
+
this.sizeLimit = sizeLimit;
|
|
14
|
+
|
|
15
|
+
ws.listenerHandlers.add(this.listenerHandler);
|
|
16
|
+
|
|
17
|
+
if (ws.isWebSocketServer) {
|
|
18
|
+
this.listenerHandlerRegExp = /^client-transfer:(.*)$/;
|
|
19
|
+
|
|
20
|
+
ws.on(`client-message:${headers.request}`, (...args) => IncomingTransport.handleRequest.call(...args));
|
|
21
|
+
ws.on(`client-message:${headers.chunk}`, (...args) => IncomingTransport.handleChunk.call(...args));
|
|
22
|
+
ws.on(`client-message:${headers.sent}`, (...args) => IncomingTransport.handleSent.call(...args));
|
|
23
|
+
ws.on(`client-message:${headers.abort}`, (...args) => IncomingTransport.handleAbort.call(...args));
|
|
24
|
+
} else {
|
|
25
|
+
this.listenerHandlerRegExp = /^transfer:(.*)$/;
|
|
26
|
+
|
|
27
|
+
ws.on(`message:${headers.request}`, IncomingTransport.handleRequest);
|
|
28
|
+
ws.on(`message:${headers.chunk}`, IncomingTransport.handleChunk);
|
|
29
|
+
ws.on(`message:${headers.sent}`, IncomingTransport.handleSent);
|
|
30
|
+
ws.on(`message:${headers.abort}`, IncomingTransport.handleAbort);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
listeners = new Map();
|
|
36
|
+
|
|
37
|
+
listenerHandler = (add, eventName, listener) => {
|
|
38
|
+
const kind = eventName.match(this.listenerHandlerRegExp)?.[1];
|
|
39
|
+
if (kind) {
|
|
40
|
+
if (add)
|
|
41
|
+
this.listeners.get(kind)?.add(listener) ??
|
|
42
|
+
this.listeners.set(kind, new Set([ listener ]));
|
|
43
|
+
else
|
|
44
|
+
if (listener)
|
|
45
|
+
this.listeners.get(kind)?.delete(listener);
|
|
46
|
+
else
|
|
47
|
+
this.listeners.delete(kind);
|
|
48
|
+
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
transfers = new Map();
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
static IncomingTransfer = IncomingTransfer;
|
|
58
|
+
|
|
59
|
+
static handleRequest(kind, id, { type, size, metadata, ...restProps }) {
|
|
60
|
+
|
|
61
|
+
const { incomingTransport } = this.wss ?? this;
|
|
62
|
+
const { IncomingTransportTransfer } = incomingTransport.constructor;
|
|
63
|
+
|
|
64
|
+
if (!incomingTransport.listeners.has(kind))
|
|
65
|
+
this.sendMessage(headers.error, id, `Unknown kind of file "${kind}"`);
|
|
66
|
+
else if (incomingTransport.transfers.has(id))
|
|
67
|
+
this.sendMessage(headers.error, id, "Transfer already exists");
|
|
68
|
+
else if (!IncomingTransportTransfer.methods[type])
|
|
69
|
+
this.sendMessage(headers.error, id, "Unknown type of transfer");
|
|
70
|
+
else if (size > this.sizeLimit)
|
|
71
|
+
this.sendMessage(headers.error, id, `Transfer size (${size} bytes) exeeds limit of ${this.sizeLimit} bytes`);
|
|
72
|
+
else
|
|
73
|
+
new IncomingTransportTransfer(this, kind, id, { type, size, metadata, ...restProps });
|
|
74
|
+
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static handleChunk(id, ...restArgs) {
|
|
78
|
+
(this.wss ?? this).incomingTransport.transfers.get(id)?.handleChunk(...restArgs);
|
|
79
|
+
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static handleSent(id) {
|
|
83
|
+
(this.wss ?? this).incomingTransport.transfers.get(id)?.handleSent();
|
|
84
|
+
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static handleAbort(id) {
|
|
88
|
+
(this.wss ?? this).incomingTransport.transfers.get(id)?.abort(true);
|
|
89
|
+
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { uid } from "@nesvet/n";
|
|
2
|
+
import { callArgsSymbol, chunkSize, headers } from "./common";
|
|
3
|
+
import { StringStreamer } from "./StringStreamer";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// FIXME
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export class OutgoingTransfer {
|
|
10
|
+
constructor(ws, kind, {
|
|
11
|
+
data,
|
|
12
|
+
type,
|
|
13
|
+
incomingType,
|
|
14
|
+
collect,
|
|
15
|
+
metadata,
|
|
16
|
+
size,
|
|
17
|
+
chunkSize,
|
|
18
|
+
encoding,
|
|
19
|
+
incomingEncoding,
|
|
20
|
+
onBegin,
|
|
21
|
+
onSenderProgress,
|
|
22
|
+
onProgress,
|
|
23
|
+
onEnd,
|
|
24
|
+
onError
|
|
25
|
+
}) {
|
|
26
|
+
|
|
27
|
+
this.ws = ws;
|
|
28
|
+
this[callArgsSymbol] = ws.wss ? [ ws.wss, ws ] : [ ws ];
|
|
29
|
+
this.outgoingTransport = (ws.wss ?? ws).outgoingTransport;
|
|
30
|
+
|
|
31
|
+
this.kind = kind;
|
|
32
|
+
|
|
33
|
+
this.data = data;
|
|
34
|
+
this.collect = collect ?? false;
|
|
35
|
+
this.metadata = metadata;
|
|
36
|
+
this.size = size ?? null;
|
|
37
|
+
this.encoding = encoding;
|
|
38
|
+
this.chunkSize = chunkSize ?? this.constructor.chunkSize;
|
|
39
|
+
|
|
40
|
+
if (onBegin)
|
|
41
|
+
this.onBegin = onBegin;
|
|
42
|
+
if (onSenderProgress)
|
|
43
|
+
this.onSenderProgress = onSenderProgress;
|
|
44
|
+
if (onProgress)
|
|
45
|
+
this.onProgress = onProgress;
|
|
46
|
+
if (onEnd)
|
|
47
|
+
this.onEnd = onEnd;
|
|
48
|
+
if (onError)
|
|
49
|
+
this.onError = onError;
|
|
50
|
+
|
|
51
|
+
this.outgoingTransport.transfers.set(this.id, this);
|
|
52
|
+
|
|
53
|
+
return (async () => {
|
|
54
|
+
|
|
55
|
+
await this.#setup(type);
|
|
56
|
+
|
|
57
|
+
this.ws.sendMessage(headers.request, this.kind, this.id, {
|
|
58
|
+
type: incomingType ?? this.type,
|
|
59
|
+
collect: this.collect,
|
|
60
|
+
encoding: incomingEncoding ?? this.encoding,
|
|
61
|
+
size: this.size,
|
|
62
|
+
metadata: this.metadata
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return this;
|
|
66
|
+
})();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
id = uid();
|
|
70
|
+
#methods = null;
|
|
71
|
+
isAborted = false;
|
|
72
|
+
isTransfered = false;
|
|
73
|
+
confirmResponse;
|
|
74
|
+
beginAt = null;
|
|
75
|
+
#prevChunkAt = null;
|
|
76
|
+
#chunksQueue = [];
|
|
77
|
+
#isProcessing = false;
|
|
78
|
+
duration = null;
|
|
79
|
+
transferedSize = 0;
|
|
80
|
+
bytesPerMs = null;
|
|
81
|
+
senderProgress = 0;
|
|
82
|
+
progress = 0;
|
|
83
|
+
endAt = null;
|
|
84
|
+
error = null;
|
|
85
|
+
|
|
86
|
+
async #setup(type) {
|
|
87
|
+
|
|
88
|
+
for (const [ availableType, test, methods ] of this.constructor.methods)
|
|
89
|
+
if (type === availableType || test(this.data)) {
|
|
90
|
+
this.type = availableType;
|
|
91
|
+
this.#methods = methods;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (this.type) {
|
|
96
|
+
await this.#methods.setup.call(this);
|
|
97
|
+
delete this.data;
|
|
98
|
+
} else
|
|
99
|
+
throw new Error("Unknown type of transfer");
|
|
100
|
+
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async handleConfirm(confirmResponse) {
|
|
104
|
+
this.confirmResponse = confirmResponse;
|
|
105
|
+
|
|
106
|
+
this.beginAt =
|
|
107
|
+
this.#prevChunkAt =
|
|
108
|
+
Date.now();
|
|
109
|
+
|
|
110
|
+
await this.onBegin?.call(...this[callArgsSymbol], this);
|
|
111
|
+
|
|
112
|
+
this.#methods.confirm.call(this);
|
|
113
|
+
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
handleChunk = chunk => {
|
|
117
|
+
this.#chunksQueue.push(chunk);
|
|
118
|
+
|
|
119
|
+
if (!this.#isProcessing) {
|
|
120
|
+
this.#isProcessing = true;
|
|
121
|
+
this.#process();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
async #process() {
|
|
127
|
+
|
|
128
|
+
let chunk = this.#chunksQueue.shift();
|
|
129
|
+
|
|
130
|
+
const chunkLength = chunk.length;
|
|
131
|
+
|
|
132
|
+
if (this.#methods.transformChunk)
|
|
133
|
+
chunk = await this.#methods.transformChunk.call(this, chunk);
|
|
134
|
+
|
|
135
|
+
this.ws.sendMessage(headers.chunk, this.id, chunk, chunkLength);
|
|
136
|
+
|
|
137
|
+
const ts = Date.now();
|
|
138
|
+
this.bytesPerMs = chunkLength / (ts - this.#prevChunkAt);
|
|
139
|
+
this.#prevChunkAt = ts;
|
|
140
|
+
this.transferedSize += chunkLength;
|
|
141
|
+
|
|
142
|
+
if (this.size)
|
|
143
|
+
this.senderProgress = this.transferedSize / this.size;
|
|
144
|
+
|
|
145
|
+
await this.onSenderProgress?.call(...this[callArgsSymbol], this);
|
|
146
|
+
|
|
147
|
+
if (this.#chunksQueue.length)
|
|
148
|
+
this.#process();
|
|
149
|
+
else {
|
|
150
|
+
this.#isProcessing = false;
|
|
151
|
+
|
|
152
|
+
if (this.senderProgress === 1)
|
|
153
|
+
this.sent();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
sent = () => this.ws.sendMessage(headers.sent, this.id);
|
|
159
|
+
|
|
160
|
+
handleProgress(progress) {
|
|
161
|
+
this.progress = progress;
|
|
162
|
+
|
|
163
|
+
this.onProgress?.call(...this[callArgsSymbol], this);
|
|
164
|
+
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async handleCompleted() {
|
|
168
|
+
|
|
169
|
+
this.isTransfered = true;
|
|
170
|
+
|
|
171
|
+
if (!this.size) {
|
|
172
|
+
this.size = this.transferedSize;
|
|
173
|
+
this.senderProgress = 1;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.endAt = Date.now();
|
|
177
|
+
this.duration = this.endAt - this.beginAt;
|
|
178
|
+
this.bytesPerMs = this.size / this.duration;
|
|
179
|
+
|
|
180
|
+
this.outgoingTransport.transfers.delete(this.id);
|
|
181
|
+
|
|
182
|
+
if (this.progress !== 1) {
|
|
183
|
+
this.progress = 1;
|
|
184
|
+
await this.onProgress?.call(...this[callArgsSymbol], this);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.onEnd?.call(...this[callArgsSymbol], this);
|
|
188
|
+
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
throw(errorMessage) {
|
|
192
|
+
|
|
193
|
+
this.outgoingTransport.transfers.delete(this.id);
|
|
194
|
+
|
|
195
|
+
this.error = new Error(errorMessage);
|
|
196
|
+
|
|
197
|
+
this.onError?.call(...this[callArgsSymbol], this, this.error);
|
|
198
|
+
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
abort() {
|
|
202
|
+
|
|
203
|
+
this.isAborted = true;
|
|
204
|
+
|
|
205
|
+
this.ws.sendMessage(headers.abort, this.id);
|
|
206
|
+
|
|
207
|
+
return this.throw("Transfer is aborted by sender");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
serialize() {
|
|
211
|
+
return {
|
|
212
|
+
id: this.id,
|
|
213
|
+
kind: this.kind,
|
|
214
|
+
type: this.type,
|
|
215
|
+
collect: this.collect,
|
|
216
|
+
encoding: this.encoding,
|
|
217
|
+
size: this.size,
|
|
218
|
+
metadata: this.metadata,
|
|
219
|
+
isAborted: this.isAborted,
|
|
220
|
+
isTransfered: this.isTransfered,
|
|
221
|
+
confirmResponse: this.confirmResponse,
|
|
222
|
+
beginAt: this.beginAt,
|
|
223
|
+
duration: this.duration,
|
|
224
|
+
transferedSize: this.transferedSize,
|
|
225
|
+
bytesPerMs: this.bytesPerMs,
|
|
226
|
+
senderProgress: this.senderProgress,
|
|
227
|
+
progress: this.progress,
|
|
228
|
+
endAt: this.endAt,
|
|
229
|
+
error: this.error
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
static methods = [
|
|
235
|
+
|
|
236
|
+
[ "object", data => typeof data == "object", {
|
|
237
|
+
|
|
238
|
+
setup() {
|
|
239
|
+
|
|
240
|
+
this.stringStreamer = new StringStreamer(JSON.stringify(this.data), { chunkSize: this.chunkSize });
|
|
241
|
+
this.collect = true;
|
|
242
|
+
this.size = this.stringStreamer.size;
|
|
243
|
+
this.encoding = "utf8";
|
|
244
|
+
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
confirm() {
|
|
248
|
+
|
|
249
|
+
this.stringStreamer.start(this.handleChunk);
|
|
250
|
+
|
|
251
|
+
}
|
|
252
|
+
} ],
|
|
253
|
+
|
|
254
|
+
[ "datauri", data => typeof data == "string" && /^data:[\w-.]+\/[\w-.+,]+(?:;base64)?,/.test(data), {
|
|
255
|
+
|
|
256
|
+
setup() {
|
|
257
|
+
|
|
258
|
+
const [ type, data ] = this.data.split(/^data:|;base64,|,/).slice(1);
|
|
259
|
+
|
|
260
|
+
this.stringStreamer = new StringStreamer(data, { chunkSize: this.chunkSize });
|
|
261
|
+
this.size = this.stringStreamer.size;
|
|
262
|
+
this.encoding = "base64";
|
|
263
|
+
this.metadata = { ...this.metadata, type };
|
|
264
|
+
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
confirm() {
|
|
268
|
+
|
|
269
|
+
this.stringStreamer.start(this.handleChunk);
|
|
270
|
+
|
|
271
|
+
}
|
|
272
|
+
} ],
|
|
273
|
+
|
|
274
|
+
[ "string", data => typeof data == "string", {
|
|
275
|
+
|
|
276
|
+
setup() {
|
|
277
|
+
|
|
278
|
+
this.stringStreamer = new StringStreamer(this.data, { chunkSize: this.chunkSize });
|
|
279
|
+
this.size = this.stringStreamer.size;
|
|
280
|
+
if (!this.encoding)
|
|
281
|
+
this.encoding = "utf8";
|
|
282
|
+
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
confirm() {
|
|
286
|
+
|
|
287
|
+
this.stringStreamer.start(this.handleChunk);
|
|
288
|
+
|
|
289
|
+
}
|
|
290
|
+
} ]
|
|
291
|
+
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
static chunkSize = chunkSize;
|
|
295
|
+
|
|
296
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { headers } from "./common";
|
|
2
|
+
import { OutgoingTransfer } from "./OutgoingTransfer";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export class OutgoingTransport {
|
|
6
|
+
constructor(ws) {
|
|
7
|
+
ws.outgoingTransport = this;
|
|
8
|
+
|
|
9
|
+
if (ws.isWebSocketServer) {
|
|
10
|
+
ws.options.WebSocket.prototype.transfer = OutgoingTransport.transfer;
|
|
11
|
+
|
|
12
|
+
ws.on(`client-message:${headers.confirm}`, (...args) => OutgoingTransport.handleConfirm.call(...args));
|
|
13
|
+
ws.on(`client-message:${headers.progress}`, (...args) => OutgoingTransport.handleProgress.call(...args));
|
|
14
|
+
ws.on(`client-message:${headers.completed}`, (...args) => OutgoingTransport.handleCompleted.call(...args));
|
|
15
|
+
ws.on(`client-message:${headers.error}`, (...args) => OutgoingTransport.handleError.call(...args));
|
|
16
|
+
} else {
|
|
17
|
+
ws.transfer = OutgoingTransport.transfer;
|
|
18
|
+
|
|
19
|
+
ws.on(`message:${headers.confirm}`, OutgoingTransport.handleConfirm);
|
|
20
|
+
ws.on(`message:${headers.progress}`, OutgoingTransport.handleProgress);
|
|
21
|
+
ws.on(`message:${headers.completed}`, OutgoingTransport.handleCompleted);
|
|
22
|
+
ws.on(`message:${headers.error}`, OutgoingTransport.handleError);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
transfers = new Map();
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
static OutgoingTransfer = OutgoingTransfer;
|
|
31
|
+
|
|
32
|
+
static async transfer(...args) {
|
|
33
|
+
|
|
34
|
+
const { OutgoingTransportTransfer } = (this.wss ?? this).outgoingTransport.constructor;
|
|
35
|
+
|
|
36
|
+
return new OutgoingTransportTransfer(this, ...args);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static handleConfirm(id, ...restArgs) {
|
|
40
|
+
(this.wss ?? this).outgoingTransport.transfers.get(id)?.handleConfirm(...restArgs);
|
|
41
|
+
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static handleProgress(id, ...restArgs) {
|
|
45
|
+
(this.wss ?? this).outgoingTransport.transfers.get(id)?.handleProgress(...restArgs);
|
|
46
|
+
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static handleCompleted(id, ...restArgs) {
|
|
50
|
+
(this.wss ?? this).outgoingTransport.transfers.get(id)?.handleCompleted(...restArgs);
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static handleError(id, errorMessage) {
|
|
55
|
+
(this.wss ?? this).outgoingTransport.transfers.get(id)?.throw(errorMessage);
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export class StringStreamer {
|
|
2
|
+
constructor(string, options = {}) {
|
|
3
|
+
this.string = string;
|
|
4
|
+
this.size = this.string.length;
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
chunkSize = 1024 * 256
|
|
8
|
+
} = options;
|
|
9
|
+
|
|
10
|
+
this.chunkSize = chunkSize;
|
|
11
|
+
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#start = 0;
|
|
15
|
+
#end = 0;
|
|
16
|
+
|
|
17
|
+
isAborted = false;
|
|
18
|
+
|
|
19
|
+
start(listener) {
|
|
20
|
+
this.listener = listener;
|
|
21
|
+
|
|
22
|
+
this.#next();
|
|
23
|
+
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async #next() {
|
|
27
|
+
|
|
28
|
+
if (!this.isAborted) {
|
|
29
|
+
this.#end = Math.min(this.#start + this.chunkSize, this.size);
|
|
30
|
+
|
|
31
|
+
const chunk = await new Promise(resolve => resolve(this.string.slice(this.#start, this.#end)));
|
|
32
|
+
|
|
33
|
+
if (!this.isAborted) {
|
|
34
|
+
await this.listener(chunk);
|
|
35
|
+
|
|
36
|
+
if (this.#end < this.size) {
|
|
37
|
+
this.#start = this.#end;
|
|
38
|
+
|
|
39
|
+
this.#next();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
abort() {
|
|
47
|
+
|
|
48
|
+
this.isAborted = true;
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export class FileStreamer {
|
|
2
|
+
constructor(file, options = {}) {
|
|
3
|
+
this.file = file;
|
|
4
|
+
this.size = this.file.size;
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
chunkSize = 1024 * 256
|
|
8
|
+
} = options;
|
|
9
|
+
|
|
10
|
+
this.chunkSize = chunkSize;
|
|
11
|
+
|
|
12
|
+
this.fileReader = new FileReader();
|
|
13
|
+
|
|
14
|
+
this.fileReader.onload = this.#handleFileReaderLoad;
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#start = 0;
|
|
19
|
+
#end = 0;
|
|
20
|
+
|
|
21
|
+
isAborted = false;
|
|
22
|
+
|
|
23
|
+
start(listener) {
|
|
24
|
+
this.listener = listener;
|
|
25
|
+
|
|
26
|
+
this.#next();
|
|
27
|
+
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#next() {
|
|
31
|
+
|
|
32
|
+
if (!this.isAborted) {
|
|
33
|
+
this.#end = Math.min(this.#start + this.chunkSize, this.size);
|
|
34
|
+
|
|
35
|
+
this.fileReader.readAsArrayBuffer(this.file.slice(this.#start, this.#end));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#handleFileReaderLoad = async () => {
|
|
41
|
+
|
|
42
|
+
if (!this.isAborted) {
|
|
43
|
+
const arrayBuffer = this.fileReader.result;
|
|
44
|
+
arrayBuffer.length = arrayBuffer.byteLength;
|
|
45
|
+
|
|
46
|
+
await this.listener(arrayBuffer);
|
|
47
|
+
|
|
48
|
+
if (this.#end < this.size) {
|
|
49
|
+
this.#start = this.#end;
|
|
50
|
+
|
|
51
|
+
this.#next();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
abort() {
|
|
58
|
+
|
|
59
|
+
this.isAborted = true;
|
|
60
|
+
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export async function arrayBufferToBase64(arrayBuffer) {
|
|
2
|
+
return new Promise(resolve => {
|
|
3
|
+
|
|
4
|
+
const fileReader = new FileReader();
|
|
5
|
+
|
|
6
|
+
fileReader.onload = ({ target: { result } }) => resolve(result.substring(result.indexOf(",") + 1));
|
|
7
|
+
|
|
8
|
+
fileReader.readAsDataURL(new Blob([ arrayBuffer ]));
|
|
9
|
+
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { IncomingTransfer } from "../IncomingTransfer";
|
|
2
|
+
import { IncomingTransport } from "../IncomingTransport";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BrowserIncomingTransfer extends IncomingTransfer {
|
|
6
|
+
|
|
7
|
+
static methods = {
|
|
8
|
+
|
|
9
|
+
file: {
|
|
10
|
+
setup() {
|
|
11
|
+
|
|
12
|
+
if (this.collect)
|
|
13
|
+
this.data = "";
|
|
14
|
+
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
collect(chunk) {
|
|
18
|
+
this.data += chunk;
|
|
19
|
+
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
end() {
|
|
23
|
+
|
|
24
|
+
if (this.collect && this.encoding === "utf8")
|
|
25
|
+
this.data = atob(this.data);
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class BrowserIncomingTransport extends IncomingTransport {
|
|
35
|
+
|
|
36
|
+
static IncomingTransfer = BrowserIncomingTransfer;
|
|
37
|
+
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
export { BrowserIncomingTransfer as IncomingTransfer, BrowserIncomingTransport as IncomingTransport };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { OutgoingTransfer } from "../OutgoingTransfer";
|
|
2
|
+
import { OutgoingTransport } from "../OutgoingTransport";
|
|
3
|
+
import { arrayBufferToBase64 } from "./arrayBufferToBase64";
|
|
4
|
+
import { FileStreamer } from "./FileStreamer";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BrowserOutgoingTransfer extends OutgoingTransfer {
|
|
8
|
+
|
|
9
|
+
static methods = [
|
|
10
|
+
|
|
11
|
+
[ "file", data => typeof data == "object" && data instanceof File, {
|
|
12
|
+
|
|
13
|
+
setup() {
|
|
14
|
+
|
|
15
|
+
const file = this.data;
|
|
16
|
+
|
|
17
|
+
this.fileStreamer = new FileStreamer(file, { chunkSize: this.chunkSize });
|
|
18
|
+
|
|
19
|
+
const metadata = {
|
|
20
|
+
name: file.name,
|
|
21
|
+
type: file.type,
|
|
22
|
+
size: file.size,
|
|
23
|
+
modifiedAt: file.lastModified
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (this.metadata)
|
|
27
|
+
Object.assign(this.metadata, metadata);
|
|
28
|
+
else
|
|
29
|
+
this.metadata = metadata;
|
|
30
|
+
|
|
31
|
+
this.size = this.fileStreamer.size;
|
|
32
|
+
this.encoding = "buffer";
|
|
33
|
+
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
confirm() {
|
|
37
|
+
|
|
38
|
+
this.fileStreamer.start(this.handleChunk);
|
|
39
|
+
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
transformChunk(chunk) {
|
|
43
|
+
return arrayBufferToBase64(chunk);
|
|
44
|
+
}
|
|
45
|
+
} ],
|
|
46
|
+
|
|
47
|
+
...OutgoingTransfer.methods
|
|
48
|
+
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
class BrowserOutgoingTransport extends OutgoingTransport {
|
|
54
|
+
|
|
55
|
+
static OutgoingTransfer = BrowserOutgoingTransfer;
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
export { BrowserOutgoingTransfer as OutgoingTransfer,
|
|
61
|
+
BrowserOutgoingTransport as OutgoingTransport };
|
package/lib/common.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const headers = {
|
|
2
|
+
request: "~t-request",
|
|
3
|
+
confirm: "~t-confirm",
|
|
4
|
+
chunk: "~t-c",
|
|
5
|
+
progress: "~t-p",
|
|
6
|
+
sent: "~t-sent",
|
|
7
|
+
completed: "~t-completed",
|
|
8
|
+
abort: "~t-abort",
|
|
9
|
+
error: "~t-error"
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const sizeLimit = 10 * 1024 * 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
export const chunkSize = 1024 * 256;
|
|
15
|
+
|
|
16
|
+
export const callArgsSymbol = Symbol("callArgs");
|
|
17
|
+
|
|
18
|
+
export const progressInterval = 250;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import { IncomingTransfer } from "../IncomingTransfer";
|
|
3
|
+
import { IncomingTransport } from "../IncomingTransport";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TransferStream extends Readable {
|
|
7
|
+
constructor(writableStream) {
|
|
8
|
+
super();
|
|
9
|
+
|
|
10
|
+
this.writable = writableStream;
|
|
11
|
+
|
|
12
|
+
this.promise = new Promise((resolve, reject) => {
|
|
13
|
+
writableStream.on("close", resolve);
|
|
14
|
+
writableStream.on("error", reject);
|
|
15
|
+
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
this.pipe(writableStream);
|
|
19
|
+
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_read() {}
|
|
23
|
+
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function pipeTo(writableStream) {
|
|
27
|
+
const stream = new TransferStream(writableStream);
|
|
28
|
+
|
|
29
|
+
this.streams?.push(stream) ??
|
|
30
|
+
(this.streams = [ stream ]);
|
|
31
|
+
|
|
32
|
+
return writableStream;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const streamMethod = {
|
|
36
|
+
setup() {
|
|
37
|
+
|
|
38
|
+
if (!this.encoding)
|
|
39
|
+
this.encoding = "buffer";
|
|
40
|
+
|
|
41
|
+
if (this.encoding === "buffer")
|
|
42
|
+
this.isBuffer = true;
|
|
43
|
+
|
|
44
|
+
if (this.collect)
|
|
45
|
+
this.data = this.isBuffer ? Buffer.from("") : "";
|
|
46
|
+
|
|
47
|
+
this.pipeTo = pipeTo;
|
|
48
|
+
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
transformChunk(chunk) {
|
|
52
|
+
chunk = this.isBuffer ? Buffer.from(chunk, "base64") : Buffer.from(chunk, "base64").toString(this.encoding);
|
|
53
|
+
|
|
54
|
+
if (this.streams)
|
|
55
|
+
for (const stream of this.streams)
|
|
56
|
+
stream.push(chunk);
|
|
57
|
+
|
|
58
|
+
return chunk;
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
collect(chunk) {
|
|
62
|
+
if (this.isBuffer)
|
|
63
|
+
this.data = Buffer.concat([ this.data, chunk ]);
|
|
64
|
+
else
|
|
65
|
+
this.data += chunk;
|
|
66
|
+
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async done() {
|
|
70
|
+
|
|
71
|
+
if (this.streams) {
|
|
72
|
+
const promises = [];
|
|
73
|
+
|
|
74
|
+
for (const stream of this.streams) {
|
|
75
|
+
stream.push(null);
|
|
76
|
+
promises.push(stream.promise);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await Promise.all(promises);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class NodeIncomingTransfer extends IncomingTransfer {
|
|
87
|
+
|
|
88
|
+
static methods = {
|
|
89
|
+
|
|
90
|
+
stream: streamMethod,
|
|
91
|
+
|
|
92
|
+
file: streamMethod,
|
|
93
|
+
|
|
94
|
+
...IncomingTransfer.methods
|
|
95
|
+
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
class NodeIncomingTransport extends IncomingTransport {
|
|
101
|
+
|
|
102
|
+
static IncomingTransfer = NodeIncomingTransfer;
|
|
103
|
+
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
export { NodeIncomingTransfer as IncomingTransfer,
|
|
108
|
+
NodeIncomingTransport as IncomingTransport };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import mime from "mime";
|
|
4
|
+
import { OutgoingTransfer } from "../OutgoingTransfer";
|
|
5
|
+
import { OutgoingTransport } from "../OutgoingTransport";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NodeOutgoingTransfer extends OutgoingTransfer {
|
|
9
|
+
|
|
10
|
+
static methods = [
|
|
11
|
+
|
|
12
|
+
[ "stream", data => typeof data == "object" && typeof data.on == "function" && typeof data.read == "function", {
|
|
13
|
+
|
|
14
|
+
setup() {
|
|
15
|
+
|
|
16
|
+
this.stream = this.data;
|
|
17
|
+
|
|
18
|
+
if (!this.encoding)
|
|
19
|
+
this.encoding = "buffer";
|
|
20
|
+
|
|
21
|
+
if (this.encoding === "buffer")
|
|
22
|
+
this.isBuffer = true;
|
|
23
|
+
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
confirm() {
|
|
27
|
+
|
|
28
|
+
this.stream.on("data", this.handleChunk);
|
|
29
|
+
if (!this.size)
|
|
30
|
+
this.stream.on("end", this.sent);
|
|
31
|
+
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
transformChunk(chunk) {
|
|
35
|
+
return (this.isBuffer ? chunk : Buffer.from(chunk, this.encoding)).toString("base64");
|
|
36
|
+
}
|
|
37
|
+
} ],
|
|
38
|
+
|
|
39
|
+
[ "file", data => typeof data == "string" && /^(\/|(\/[^/]+)+)$/.test(data), {
|
|
40
|
+
|
|
41
|
+
async setup() {
|
|
42
|
+
|
|
43
|
+
const fileName = this.data;
|
|
44
|
+
const name = path.basename(fileName);
|
|
45
|
+
const { size, mtimeMs: modifiedAt } = await fs.promises.stat(fileName);
|
|
46
|
+
|
|
47
|
+
const metadata = {
|
|
48
|
+
name,
|
|
49
|
+
type: mime.getType(path.extname(name).slice(1)),
|
|
50
|
+
size,
|
|
51
|
+
modifiedAt
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
this.stream = fs.createReadStream(fileName);
|
|
55
|
+
|
|
56
|
+
this.size = size;
|
|
57
|
+
this.encoding = "buffer";
|
|
58
|
+
|
|
59
|
+
if (this.metadata)
|
|
60
|
+
Object.assign(this.metadata, metadata);
|
|
61
|
+
else
|
|
62
|
+
this.metadata = metadata;
|
|
63
|
+
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
confirm() {
|
|
67
|
+
|
|
68
|
+
this.stream.on("data", this.handleChunk);
|
|
69
|
+
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
transformChunk(chunk) {
|
|
73
|
+
return chunk.toString("base64");
|
|
74
|
+
}
|
|
75
|
+
} ],
|
|
76
|
+
|
|
77
|
+
...OutgoingTransfer.methods
|
|
78
|
+
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class NodeOutgoingTransport extends OutgoingTransport {
|
|
84
|
+
|
|
85
|
+
static OutgoingTransfer = NodeOutgoingTransfer;
|
|
86
|
+
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
export { NodeOutgoingTransfer as OutgoingTransfer,
|
|
91
|
+
NodeOutgoingTransport as OutgoingTransport };
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "insite-ws-transfers",
|
|
3
|
+
"version": "1.0.465",
|
|
4
|
+
"description": "inSite file transfers over WebSockets",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/node/index.js",
|
|
7
|
+
"module": "./lib/node/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/node/index.js",
|
|
10
|
+
"./node": "./lib/node/index.js",
|
|
11
|
+
"./node/incoming": "./lib/node/incoming.js",
|
|
12
|
+
"./node/outgoing": "./lib/node/outgoing.js",
|
|
13
|
+
"./browser": "./lib/browser/index.js",
|
|
14
|
+
"./browser/incoming": "./lib/browser/incoming.js",
|
|
15
|
+
"./browser/outgoing": "./lib/browser/outgoing.js"
|
|
16
|
+
},
|
|
17
|
+
"packageManager": "yarn@1.22.22",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/nesvet/insite-ws-transfers.git"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"insite",
|
|
27
|
+
"ws",
|
|
28
|
+
"websockets",
|
|
29
|
+
"files",
|
|
30
|
+
"transfers"
|
|
31
|
+
],
|
|
32
|
+
"author": "Eugene Nesvetaev <dev@nesvet.dev> (https://nesvet.dev/)",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@nesvet/n": "^1",
|
|
36
|
+
"mime": "^2"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@nesvet/eslint-config": "^1"
|
|
40
|
+
}
|
|
41
|
+
}
|