tabby-rdp 0.5.0 → 0.5.1
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/CHANGELOG.md +490 -0
- package/README.md +177 -48
- package/dist/accountForm.js +0 -1
- package/dist/accounts.js +0 -1
- package/dist/audio.js +0 -1
- package/dist/authorities.js +331 -0
- package/dist/byteQueue.js +102 -0
- package/dist/clipboard.js +106 -0
- package/dist/connectionStatus.js +0 -1
- package/dist/desk.js +16 -47
- package/dist/deskScript.js +196 -6
- package/dist/desktop.service.js +1442 -244
- package/dist/desktopForm.js +51 -17
- package/dist/desktops.js +232 -24
- package/dist/drives.js +271 -30
- package/dist/fileTransfer.js +958 -83
- package/dist/gateway.js +216 -57
- package/dist/header.js +76 -5
- package/dist/help.js +34 -13
- package/dist/hyperv.js +65 -15
- package/dist/index.js +2 -2
- package/dist/keyboard.js +0 -1
- package/dist/microphone.js +57 -13
- package/dist/ntlm.js +0 -1
- package/dist/osd.js +56 -15
- package/dist/probe.js +15 -3
- package/dist/rdcleanpath.js +319 -44
- package/dist/rdpFile.js +22 -7
- package/dist/rdpProfile.js +39 -21
- package/dist/remoteSetup.js +103 -23
- package/dist/sessionLog.js +53 -0
- package/dist/settingsPage.js +65 -29
- package/dist/signin.js +407 -40
- package/dist/ssh.js +99 -18
- package/dist/sshChannelStream.js +40 -6
- package/dist/targets.js +185 -39
- package/dist/ui.js +88 -20
- package/dist/unshowable.js +41 -0
- package/dist/updates.js +37 -2
- package/dist/vms.js +17 -3
- package/dist/wake.js +15 -8
- package/package.json +4 -3
- package/remote/trd-pty.py +74 -13
- package/vendor/SHA256SUMS +26 -0
- package/vendor/ironrdp_web_bg.wasm +0 -0
- package/dist/accountForm.js.map +0 -1
- package/dist/accounts.js.map +0 -1
- package/dist/audio.js.map +0 -1
- package/dist/connectionStatus.js.map +0 -1
- package/dist/desk.js.map +0 -1
- package/dist/deskScript.js.map +0 -1
- package/dist/desktop.service.js.map +0 -1
- package/dist/desktopForm.js.map +0 -1
- package/dist/desktops.js.map +0 -1
- package/dist/drives.js.map +0 -1
- package/dist/fileTransfer.js.map +0 -1
- package/dist/gateway.js.map +0 -1
- package/dist/header.js.map +0 -1
- package/dist/help.js.map +0 -1
- package/dist/hyperv.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/keyboard.js.map +0 -1
- package/dist/microphone.js.map +0 -1
- package/dist/ntlm.js.map +0 -1
- package/dist/osd.js.map +0 -1
- package/dist/probe.js.map +0 -1
- package/dist/rdcleanpath.js.map +0 -1
- package/dist/rdpFile.js.map +0 -1
- package/dist/rdpProfile.js.map +0 -1
- package/dist/remoteSetup.js.map +0 -1
- package/dist/settingsPage.js.map +0 -1
- package/dist/signin.js.map +0 -1
- package/dist/ssh.js.map +0 -1
- package/dist/sshChannelStream.js.map +0 -1
- package/dist/targets.js.map +0 -1
- package/dist/ui.js.map +0 -1
- package/dist/updates.js.map +0 -1
- package/dist/vms.js.map +0 -1
- package/dist/wake.js.map +0 -1
package/dist/drives.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SharedDrives = void 0;
|
|
3
|
+
exports.SharedDrives = exports.WINDOW_OPEN_FILES = exports.MAX_OPEN_FILES = void 0;
|
|
4
4
|
exports.sharedFolders = sharedFolders;
|
|
5
5
|
exports.driveName = driveName;
|
|
6
|
+
exports.openFilesInWindow = openFilesInWindow;
|
|
6
7
|
const fs = require("fs");
|
|
7
8
|
const path = require("path");
|
|
8
9
|
const fsp = fs.promises;
|
|
@@ -43,6 +44,69 @@ function fail(code, message) {
|
|
|
43
44
|
}
|
|
44
45
|
/** One read's worth: the server asks for 64 KiB or so; the proxy takes messages up to 32 MiB. */
|
|
45
46
|
const READ_LIMIT = 4 * 1024 * 1024;
|
|
47
|
+
/**
|
|
48
|
+
* Files a desktop may have open at once. Each one holds a file descriptor of Tabby's window, which all its tabs and
|
|
49
|
+
* connections share, and the server decides how many it opens and when it closes them: with no limit, a hostile one
|
|
50
|
+
* could take them all, and the window could then open no file or connection, or crash. Programs on the remote keep
|
|
51
|
+
* far fewer open. More are refused (EMFILE) until the server closes some.
|
|
52
|
+
*/
|
|
53
|
+
exports.MAX_OPEN_FILES = 1024;
|
|
54
|
+
/**
|
|
55
|
+
* Files all the desktops in a window may have open at once. The window's descriptors are one process's, so a limit per
|
|
56
|
+
* desktop alone would let several desktops together hold that many times more. The window may have 8192 open on
|
|
57
|
+
* Windows (the C runtime's most); on macOS, Node raises its soft limit to the hard one as it starts, which
|
|
58
|
+
* kern.maxfilesperproc caps (some 138,000 on a Mac measured with Electron 40). This many is a quarter of the smaller of
|
|
59
|
+
* those, which leaves most of them to everything else whatever the desktops do, and one desktop can't take it all
|
|
60
|
+
* (MAX_OPEN_FILES is half of it). On Linux the window's limit wasn't measured (the system's hard limit, often half a
|
|
61
|
+
* million or more, if Node raises it there as on macOS): where it is 4,096 or less, this many is half of it or more.
|
|
62
|
+
*/
|
|
63
|
+
exports.WINDOW_OPEN_FILES = 2 * exports.MAX_OPEN_FILES;
|
|
64
|
+
/**
|
|
65
|
+
* Files open, being opened, or being closed, through every SharedDrives of this window (the plugin is loaded once per
|
|
66
|
+
* window). A file counts until its close has finished: closing waits for what is under way on the file (a slow disk or
|
|
67
|
+
* network folder), and the descriptor is the window's until then.
|
|
68
|
+
*/
|
|
69
|
+
let openInWindow = 0;
|
|
70
|
+
/** Files open, being opened or being closed, on the desktops of this window (see WINDOW_OPEN_FILES). */
|
|
71
|
+
function openFilesInWindow() {
|
|
72
|
+
return openInWindow;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Space a desktop leaves free on the disk a shared file is on when it makes the file larger than the data it sends:
|
|
76
|
+
* a write past the end of a file, or a larger end of file. One request can ask for any size, and where files can't
|
|
77
|
+
* have holes (HFS+, NTFS, exFAT) the space is taken at once, on the whole disk; elsewhere the file looks petabytes
|
|
78
|
+
* long, and grows again with every request. Explorer sets a copy's size before writing it, so sizes aren't capped as
|
|
79
|
+
* such: the part of a file the server sent no data for (its size beyond what it has on disk, see room) may be no
|
|
80
|
+
* larger than the disk's free space less this much (1 GB, or a twentieth of a disk smaller than 20 GB). Data the
|
|
81
|
+
* server does send isn't checked: that takes as long as any copy.
|
|
82
|
+
*/
|
|
83
|
+
function reserve(total) {
|
|
84
|
+
return Math.min(1024 ** 3, Math.floor(total / 20));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Growth on each disk (by device number) in this window, one at a time across its desktops: from the reading of the
|
|
88
|
+
* disk's free space (see room) until the file has grown. A check then never goes by free space another desktop's growth
|
|
89
|
+
* has taken since it was read, so two desktops can't each be told the same free space is theirs. The latest growth's
|
|
90
|
+
* end, which the next one on that disk waits for.
|
|
91
|
+
*/
|
|
92
|
+
const growth = new Map();
|
|
93
|
+
/** Runs `grow` once the growth before it on disk `dev`, in any desktop of this window, has ended (see growth). */
|
|
94
|
+
async function inTurn(dev, grow) {
|
|
95
|
+
const before = growth.get(dev);
|
|
96
|
+
let done = () => { };
|
|
97
|
+
const mine = new Promise(resolve => { done = resolve; });
|
|
98
|
+
growth.set(dev, mine);
|
|
99
|
+
try {
|
|
100
|
+
await before;
|
|
101
|
+
return await grow();
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
done();
|
|
105
|
+
if (growth.get(dev) === mine) {
|
|
106
|
+
growth.delete(dev);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
46
110
|
/**
|
|
47
111
|
* The shared folders as the remote desktop's file system (MS-RDPEFS drive redirection), on Tabby's Node side.
|
|
48
112
|
* IronRDP asks one thing at a time and waits for the answer, so everything here is asynchronous (Node's thread
|
|
@@ -51,28 +115,59 @@ const READ_LIMIT = 4 * 1024 * 1024;
|
|
|
51
115
|
* The server's paths are relative to the drive, with backslashes. Each resolves inside its folder, also once
|
|
52
116
|
* symbolic links are followed: a path that leaves the folder, by `..` or through a link, doesn't exist as far as
|
|
53
117
|
* the remote is concerned. Only regular files and folders are served (no devices, pipes or sockets, which could
|
|
54
|
-
* block or do things on being opened). Read-only folders refuse anything that writes
|
|
118
|
+
* block or do things on being opened). Read-only folders refuse anything that writes, and so does a file with more
|
|
119
|
+
* than one name (a hard link), which may be outside the folder too. What the server opens and how much space it
|
|
120
|
+
* takes without sending data are limited (MAX_OPEN_FILES and WINDOW_OPEN_FILES, reserve).
|
|
55
121
|
*/
|
|
56
122
|
class SharedDrives {
|
|
57
|
-
|
|
123
|
+
/** `flow`: the room for what is read for the server (the desktop's proxy, see ServerFlow and read). */
|
|
124
|
+
constructor(folders, log = () => null, flow) {
|
|
58
125
|
this.log = log;
|
|
126
|
+
this.flow = flow;
|
|
59
127
|
this.byId = new Map();
|
|
60
128
|
this.roots = new Map();
|
|
61
129
|
this.handles = new Map();
|
|
62
130
|
this.nextHandle = 1;
|
|
131
|
+
/** Opens under way, which count towards MAX_OPEN_FILES too. */
|
|
132
|
+
this.opening = 0;
|
|
133
|
+
/** Closes under way, which count too: the descriptor is still open until the close has finished. */
|
|
134
|
+
this.closing = 0;
|
|
135
|
+
/** A limit on open files was reached (and logged, once). */
|
|
136
|
+
this.full = false;
|
|
137
|
+
/** The connection ended (see dispose): nothing more is opened, and what was still opening is closed again. */
|
|
138
|
+
this.disposed = false;
|
|
63
139
|
// RDPDR device ids are the session's: the audio device, when there is one, takes 0 in IronRDP.
|
|
64
140
|
this.drives = folders.map((folder, i) => {
|
|
65
141
|
this.byId.set(i + 1, folder);
|
|
66
142
|
return { id: i + 1, name: folder.name };
|
|
67
143
|
});
|
|
68
144
|
}
|
|
69
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Closes whatever the server left open (the connection ended), and what it was still opening once that is open.
|
|
147
|
+
* Each counts towards the limits until its close has finished (see closeFile), not before.
|
|
148
|
+
*/
|
|
70
149
|
dispose() {
|
|
150
|
+
this.disposed = true;
|
|
71
151
|
for (const handle of this.handles.values()) {
|
|
72
|
-
handle.file
|
|
152
|
+
this.closeFile(handle.file).catch(() => null);
|
|
73
153
|
}
|
|
74
154
|
this.handles.clear();
|
|
75
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Closes a file that was counted as open (or being opened), and stops counting it once the close has finished,
|
|
158
|
+
* whether it worked or not: closing waits for what is under way on the file, and a slot handed on before then would
|
|
159
|
+
* let more descriptors be open than the limits allow.
|
|
160
|
+
*/
|
|
161
|
+
async closeFile(file) {
|
|
162
|
+
this.closing++;
|
|
163
|
+
try {
|
|
164
|
+
await file.close();
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
this.closing--;
|
|
168
|
+
openInWindow--;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
76
171
|
/** The folder's own real path (it may be reached through a link itself, as /tmp is on macOS). */
|
|
77
172
|
rootOf(folder) {
|
|
78
173
|
let root = this.roots.get(folder);
|
|
@@ -139,6 +234,54 @@ class SharedDrives {
|
|
|
139
234
|
handle(id) {
|
|
140
235
|
return this.handles.get(id) ?? fail('EBADF', `no open file ${id}`);
|
|
141
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Makes sure the disk of an open file (`now`: its stat) can take the file being `end` bytes long without the server
|
|
239
|
+
* sending data for what that adds (see reserve). What counts is the part of the file that isn't on disk, from what
|
|
240
|
+
* the file has there, not from its size: on file systems with holes, growing a file takes no space, so growing it
|
|
241
|
+
* again and again would pass a check of each growth alone. Called in the disk's turn (see grow).
|
|
242
|
+
*/
|
|
243
|
+
async room(handle, now, end) {
|
|
244
|
+
const space = await this.spaceOf(handle, now);
|
|
245
|
+
// 512-byte blocks, as POSIX counts them (and libuv on Windows); a file system that doesn't say has its size.
|
|
246
|
+
const onDisk = Number.isFinite(now.blocks) ? now.blocks * 512 : now.size;
|
|
247
|
+
const unsent = Math.max(0, end - onDisk);
|
|
248
|
+
if (unsent > space.bsize * space.bavail - reserve(space.bsize * space.blocks)) {
|
|
249
|
+
fail('ENOSPC', `${handle.folder.name}: not enough free space for a file ${end} bytes long`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Makes an open file larger than the data the server sends for it, with `change`: `end` bytes long, with no data
|
|
254
|
+
* from `from` (past the end of the file now) on. In its disk's turn (see growth), once room() has checked the space
|
|
255
|
+
* against the file as it is then: another request may have changed it while this one waited its turn.
|
|
256
|
+
*/
|
|
257
|
+
grow(handle, dev, from, end, change) {
|
|
258
|
+
return inTurn(dev, async () => {
|
|
259
|
+
const now = await handle.file.stat();
|
|
260
|
+
if (from > now.size) {
|
|
261
|
+
await this.room(handle, now, end);
|
|
262
|
+
}
|
|
263
|
+
return change();
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The free space of the disk an open file (`now`) is on: that of the folder it was opened in, or else of the shared
|
|
268
|
+
* folder, whichever is on that disk (the same device). A disk mounted somewhere in a shared folder (a USB stick in
|
|
269
|
+
* a shared /Volumes, a disk image in a shared home) has its own free space, which the shared folder's doesn't say.
|
|
270
|
+
* When neither is on it (the file's folder was moved away meanwhile), the space isn't known, and the file isn't
|
|
271
|
+
* grown (ENOSPC).
|
|
272
|
+
*/
|
|
273
|
+
async spaceOf(handle, now) {
|
|
274
|
+
for (const dir of [path.dirname(handle.local), await this.rootOf(handle.folder).catch(() => null)]) {
|
|
275
|
+
if (dir === null) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const [at, space] = await Promise.all([fsp.stat(dir), fsp.statfs(dir)]).catch(() => [null, null]);
|
|
279
|
+
if (at && space && at.dev === now.dev) {
|
|
280
|
+
return space;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return fail('ENOSPC', `${handle.folder.name}: the free space of this file's disk isn't known`);
|
|
284
|
+
}
|
|
142
285
|
async stat(deviceId, remote) {
|
|
143
286
|
let s;
|
|
144
287
|
let folder;
|
|
@@ -163,7 +306,8 @@ class SharedDrives {
|
|
|
163
306
|
created: s.birthtimeMs || s.ctimeMs,
|
|
164
307
|
accessed: s.atimeMs,
|
|
165
308
|
modified: s.mtimeMs,
|
|
166
|
-
|
|
309
|
+
// A file with other names (hard links) isn't changed through the share (see open), so it shows read-only.
|
|
310
|
+
readonly: folder.readOnly || (s.mode & 0o200) === 0 || (s.isFile() && s.nlink > 1),
|
|
167
311
|
};
|
|
168
312
|
}
|
|
169
313
|
async list(deviceId, remote) {
|
|
@@ -171,6 +315,43 @@ class SharedDrives {
|
|
|
171
315
|
return fsp.readdir(local);
|
|
172
316
|
}
|
|
173
317
|
async open(deviceId, remote, mode) {
|
|
318
|
+
if (this.disposed) {
|
|
319
|
+
fail('EBADF', `${remote}: the connection has ended`);
|
|
320
|
+
}
|
|
321
|
+
const mine = this.handles.size + this.opening + this.closing >= exports.MAX_OPEN_FILES;
|
|
322
|
+
if (mine || openInWindow >= exports.WINDOW_OPEN_FILES) {
|
|
323
|
+
if (!this.full) {
|
|
324
|
+
this.full = true;
|
|
325
|
+
this.log(mine
|
|
326
|
+
? `drives: the remote has ${exports.MAX_OPEN_FILES} files open, the most a desktop may; more are refused until it closes some`
|
|
327
|
+
: `drives: the desktops in this window have ${exports.WINDOW_OPEN_FILES} files open, the most they may together; more are refused until some are closed`);
|
|
328
|
+
}
|
|
329
|
+
fail('EMFILE', `${remote}: ${mine ? exports.MAX_OPEN_FILES : exports.WINDOW_OPEN_FILES} files are open already`);
|
|
330
|
+
}
|
|
331
|
+
this.opening++;
|
|
332
|
+
openInWindow++;
|
|
333
|
+
let handle;
|
|
334
|
+
try {
|
|
335
|
+
handle = await this.openFile(deviceId, remote, mode);
|
|
336
|
+
}
|
|
337
|
+
catch (e) {
|
|
338
|
+
openInWindow--;
|
|
339
|
+
throw e;
|
|
340
|
+
}
|
|
341
|
+
finally {
|
|
342
|
+
this.opening--;
|
|
343
|
+
}
|
|
344
|
+
// The connection ended while the file was being opened: dispose() has closed the others, and nothing would
|
|
345
|
+
// close this one.
|
|
346
|
+
if (this.disposed) {
|
|
347
|
+
await this.closeFile(handle.file).catch(() => null);
|
|
348
|
+
fail('EBADF', `${remote}: the connection has ended`);
|
|
349
|
+
}
|
|
350
|
+
const id = this.nextHandle++;
|
|
351
|
+
this.handles.set(id, handle);
|
|
352
|
+
return id;
|
|
353
|
+
}
|
|
354
|
+
async openFile(deviceId, remote, mode) {
|
|
174
355
|
const { folder, local } = await this.locate(deviceId, remote, true);
|
|
175
356
|
if (mode.write || mode.create || mode.truncate) {
|
|
176
357
|
this.writable(folder, `can't write ${remote}`);
|
|
@@ -180,51 +361,109 @@ class SharedDrives {
|
|
|
180
361
|
fail(existing.isDirectory() ? 'EISDIR' : 'EACCES', `${remote}: not a file`);
|
|
181
362
|
}
|
|
182
363
|
// The path was resolved just now: a link (or a pipe) put in its place meanwhile isn't followed (or waited on).
|
|
183
|
-
|
|
364
|
+
// Truncating waits until the file is known to be one the remote may change (below).
|
|
365
|
+
const { O_RDONLY, O_RDWR, O_CREAT, O_EXCL } = fs.constants;
|
|
184
366
|
const guard = (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0);
|
|
185
|
-
const flags = (mode.write ? O_RDWR : O_RDONLY) | (mode.create ? O_CREAT : 0) | (mode.exclusive ? O_EXCL : 0) |
|
|
367
|
+
const flags = (mode.write || mode.truncate ? O_RDWR : O_RDONLY) | (mode.create ? O_CREAT : 0) | (mode.exclusive ? O_EXCL : 0) | guard;
|
|
186
368
|
const file = await fsp.open(local, flags, 0o644);
|
|
187
|
-
|
|
188
|
-
await file.
|
|
189
|
-
|
|
369
|
+
try {
|
|
370
|
+
const s = await file.stat();
|
|
371
|
+
if (!s.isFile()) {
|
|
372
|
+
fail('EACCES', `${remote}: not a file`);
|
|
373
|
+
}
|
|
374
|
+
// A file with another name (a hard link) is the same file there, which may be outside the shared folder:
|
|
375
|
+
// what the remote writes would change it there too. Reading it is reading what is in the folder.
|
|
376
|
+
if ((mode.write || mode.truncate) && s.nlink > 1) {
|
|
377
|
+
fail('EACCES', `${remote}: has more than one name (a hard link), so it isn't changed from the remote`);
|
|
378
|
+
}
|
|
379
|
+
if (mode.truncate) {
|
|
380
|
+
await file.truncate(0);
|
|
381
|
+
}
|
|
190
382
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
383
|
+
catch (e) {
|
|
384
|
+
await file.close().catch(() => null);
|
|
385
|
+
throw e;
|
|
386
|
+
}
|
|
387
|
+
return { file, folder, local };
|
|
194
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* A read of the server's: up to READ_LIMIT, as much as it asks for. Its answer is handed to the connection, where
|
|
391
|
+
* it stays, in the window's memory, until the proxy has read it, as the server takes what is sent: a server that
|
|
392
|
+
* keeps asking and reads slowly, or not at all, would otherwise have every answer kept there. So it takes room for
|
|
393
|
+
* it first (see ServerFlow), waiting while there is none, and gives back what it didn't read.
|
|
394
|
+
*
|
|
395
|
+
* How far the room holds the answers back: see ServerFlow (the proxy reading the connection's other messages makes
|
|
396
|
+
* room too).
|
|
397
|
+
*/
|
|
195
398
|
async read(handle, offset, length) {
|
|
399
|
+
const size = Math.min(length, READ_LIMIT);
|
|
400
|
+
while (this.flow && !this.flow.room()) {
|
|
401
|
+
await this.flow.whenRoom();
|
|
402
|
+
}
|
|
196
403
|
const { file } = this.handle(handle);
|
|
197
|
-
|
|
404
|
+
this.flow?.spend(size);
|
|
405
|
+
const buffer = Buffer.allocUnsafe(size);
|
|
198
406
|
let read = 0;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
407
|
+
try {
|
|
408
|
+
// A read can come back short before the end of the file: fill what was asked.
|
|
409
|
+
while (read < buffer.length) {
|
|
410
|
+
const { bytesRead } = await file.read(buffer, read, buffer.length - read, offset + read);
|
|
411
|
+
if (bytesRead === 0) {
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
read += bytesRead;
|
|
204
415
|
}
|
|
205
|
-
read += bytesRead;
|
|
206
416
|
}
|
|
417
|
+
catch (e) {
|
|
418
|
+
this.flow?.refund(size);
|
|
419
|
+
throw e;
|
|
420
|
+
}
|
|
421
|
+
this.flow?.refund(size - read);
|
|
207
422
|
return new Uint8Array(buffer.buffer, buffer.byteOffset, read);
|
|
208
423
|
}
|
|
209
424
|
async write(handle, offset, data) {
|
|
210
|
-
const
|
|
425
|
+
const entry = this.handle(handle);
|
|
426
|
+
const { file, folder } = entry;
|
|
211
427
|
this.writable(folder, 'can\'t write');
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
428
|
+
const end = offset + data.length;
|
|
429
|
+
if (!(offset >= 0 && Number.isSafeInteger(offset) && Number.isSafeInteger(end))) {
|
|
430
|
+
fail('ENOSPC', `can't write at ${offset}: past what a file can hold`);
|
|
215
431
|
}
|
|
216
|
-
|
|
432
|
+
const writeAll = async () => {
|
|
433
|
+
let written = 0;
|
|
434
|
+
while (written < data.length) {
|
|
435
|
+
written += (await file.write(data, written, data.length - written, offset + written)).bytesWritten;
|
|
436
|
+
}
|
|
437
|
+
return written;
|
|
438
|
+
};
|
|
439
|
+
// Past the end of the file, the gap up to the data is made as well, with nothing sent for it (see room).
|
|
440
|
+
if (offset > 0) {
|
|
441
|
+
const now = await file.stat();
|
|
442
|
+
if (offset > now.size) {
|
|
443
|
+
return this.grow(entry, now.dev, offset, end, writeAll);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return writeAll();
|
|
217
447
|
}
|
|
218
448
|
async truncate(handle, size) {
|
|
219
|
-
const
|
|
449
|
+
const entry = this.handle(handle);
|
|
450
|
+
const { file, folder } = entry;
|
|
220
451
|
this.writable(folder, 'can\'t resize');
|
|
452
|
+
if (!(size >= 0 && Number.isSafeInteger(size))) {
|
|
453
|
+
fail('ENOSPC', `can't make a file ${size} bytes long`);
|
|
454
|
+
}
|
|
455
|
+
// A larger end of file: what it adds has no data sent for it (see room).
|
|
456
|
+
const now = await file.stat();
|
|
457
|
+
if (size > now.size) {
|
|
458
|
+
return this.grow(entry, now.dev, size, size, () => file.truncate(size));
|
|
459
|
+
}
|
|
221
460
|
await file.truncate(size);
|
|
222
461
|
}
|
|
223
462
|
async close(handle) {
|
|
224
463
|
const entry = this.handles.get(handle);
|
|
225
464
|
if (entry) {
|
|
226
465
|
this.handles.delete(handle);
|
|
227
|
-
await entry.file
|
|
466
|
+
await this.closeFile(entry.file);
|
|
228
467
|
}
|
|
229
468
|
}
|
|
230
469
|
async mkdir(deviceId, remote) {
|
|
@@ -272,7 +511,10 @@ class SharedDrives {
|
|
|
272
511
|
const folder = this.byId.get(deviceId) ?? fail('ENOENT', `no drive ${deviceId}`);
|
|
273
512
|
try {
|
|
274
513
|
const s = await fsp.statfs(await this.rootOf(folder));
|
|
275
|
-
|
|
514
|
+
const total = s.bsize * s.blocks;
|
|
515
|
+
// Free space less the reserve, which the remote can't take by making files larger (see room): Explorer
|
|
516
|
+
// checks a copy fits before starting it.
|
|
517
|
+
return { total, free: Math.max(0, s.bsize * s.bavail - reserve(total)), label: folder.name };
|
|
276
518
|
}
|
|
277
519
|
catch (e) {
|
|
278
520
|
this.log(`drives: ${folder.name}: no volume information: ${e?.message ?? e}`);
|
|
@@ -281,4 +523,3 @@ class SharedDrives {
|
|
|
281
523
|
}
|
|
282
524
|
}
|
|
283
525
|
exports.SharedDrives = SharedDrives;
|
|
284
|
-
//# sourceMappingURL=drives.js.map
|