xrootd 0.2.3 → 1.0.0-beta.2
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 +24 -0
- package/LICENSE +189 -0
- package/README.md +334 -101
- package/dist/index.d.mts +620 -821
- package/dist/index.mjs +1554 -723
- package/package.json +64 -83
- package/LICENSE-GPLv3 +0 -674
- package/LICENSE-MIT +0 -7
- package/dist/index.cjs +0 -817
- package/dist/index.d.cts +0 -864
- package/libs/darwin-arm64/libXrdCl.6.dylib +0 -0
- package/libs/darwin-arm64/libXrdCrypto.6.dylib +0 -0
- package/libs/darwin-arm64/libXrdCryptossl-6.so +0 -0
- package/libs/darwin-arm64/libXrdSec-6.so +0 -0
- package/libs/darwin-arm64/libXrdSecProt-6.so +0 -0
- package/libs/darwin-arm64/libXrdSeckrb5-6.so +0 -0
- package/libs/darwin-arm64/libXrdSecpwd-6.so +0 -0
- package/libs/darwin-arm64/libXrdSecsss-6.so +0 -0
- package/libs/darwin-arm64/libXrdSecunix-6.so +0 -0
- package/libs/darwin-arm64/libXrdSecztn-6.so +0 -0
- package/libs/darwin-arm64/libXrdUtils.6.dylib +0 -0
- package/libs/darwin-arm64/libXrdXml.6.dylib +0 -0
- package/libs/darwin-x64/libXrdCl.6.dylib +0 -0
- package/libs/darwin-x64/libXrdCrypto.6.dylib +0 -0
- package/libs/darwin-x64/libXrdCryptossl-6.so +0 -0
- package/libs/darwin-x64/libXrdSec-6.so +0 -0
- package/libs/darwin-x64/libXrdSecProt-6.so +0 -0
- package/libs/darwin-x64/libXrdSeckrb5-6.so +0 -0
- package/libs/darwin-x64/libXrdSecpwd-6.so +0 -0
- package/libs/darwin-x64/libXrdSecsss-6.so +0 -0
- package/libs/darwin-x64/libXrdSecunix-6.so +0 -0
- package/libs/darwin-x64/libXrdSecztn-6.so +0 -0
- package/libs/darwin-x64/libXrdUtils.6.dylib +0 -0
- package/libs/darwin-x64/libXrdXml.6.dylib +0 -0
- package/libs/linux-arm64/libXrdCl.so.6 +0 -0
- package/libs/linux-arm64/libXrdCrypto.so.6 +0 -0
- package/libs/linux-arm64/libXrdCryptossl-6.so +0 -0
- package/libs/linux-arm64/libXrdSec-6.so +0 -0
- package/libs/linux-arm64/libXrdSecProt-6.so +0 -0
- package/libs/linux-arm64/libXrdSeckrb5-6.so +0 -0
- package/libs/linux-arm64/libXrdSecpwd-6.so +0 -0
- package/libs/linux-arm64/libXrdSecsss-6.so +0 -0
- package/libs/linux-arm64/libXrdSecunix-6.so +0 -0
- package/libs/linux-arm64/libXrdSecztn-6.so +0 -0
- package/libs/linux-arm64/libXrdUtils.so.6 +0 -0
- package/libs/linux-arm64/libXrdXml.so.6 +0 -0
- package/libs/linux-x64/libXrdCl.so.6 +0 -0
- package/libs/linux-x64/libXrdCrypto.so.6 +0 -0
- package/libs/linux-x64/libXrdCryptossl-6.so +0 -0
- package/libs/linux-x64/libXrdSec-6.so +0 -0
- package/libs/linux-x64/libXrdSecProt-6.so +0 -0
- package/libs/linux-x64/libXrdSeckrb5-6.so +0 -0
- package/libs/linux-x64/libXrdSecpwd-6.so +0 -0
- package/libs/linux-x64/libXrdSecsss-6.so +0 -0
- package/libs/linux-x64/libXrdSecunix-6.so +0 -0
- package/libs/linux-x64/libXrdSecztn-6.so +0 -0
- package/libs/linux-x64/libXrdUtils.so.6 +0 -0
- package/libs/linux-x64/libXrdXml.so.6 +0 -0
- package/prebuilds/darwin-arm64/xrootd.node +0 -0
- package/prebuilds/darwin-x64/xrootd.node +0 -0
- package/prebuilds/linux-arm64/xrootd.node +0 -0
- package/prebuilds/linux-x64/xrootd.node +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,785 +1,1616 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import tls from "node:tls";
|
|
3
|
+
//#region src/protocol/constants.ts
|
|
4
|
+
const RequestId = {
|
|
5
|
+
Auth: 3e3,
|
|
6
|
+
Query: 3001,
|
|
7
|
+
Chmod: 3002,
|
|
8
|
+
Close: 3003,
|
|
9
|
+
Dirlist: 3004,
|
|
10
|
+
Gpfile: 3005,
|
|
11
|
+
Protocol: 3006,
|
|
12
|
+
Login: 3007,
|
|
13
|
+
Mkdir: 3008,
|
|
14
|
+
Mv: 3009,
|
|
15
|
+
Open: 3010,
|
|
16
|
+
Ping: 3011,
|
|
17
|
+
Chkpoint: 3012,
|
|
18
|
+
Read: 3013,
|
|
19
|
+
Rm: 3014,
|
|
20
|
+
Rmdir: 3015,
|
|
21
|
+
Sync: 3016,
|
|
22
|
+
Stat: 3017,
|
|
23
|
+
Set: 3018,
|
|
24
|
+
Write: 3019,
|
|
25
|
+
Fattr: 3020,
|
|
26
|
+
Prepare: 3021,
|
|
27
|
+
Statx: 3022,
|
|
28
|
+
Endsess: 3023,
|
|
29
|
+
Bind: 3024,
|
|
30
|
+
ReadV: 3025,
|
|
31
|
+
PgWrite: 3026,
|
|
32
|
+
Locate: 3027,
|
|
33
|
+
Truncate: 3028,
|
|
34
|
+
Sigver: 3029,
|
|
35
|
+
PgRead: 3030,
|
|
36
|
+
WriteV: 3031,
|
|
37
|
+
Clone: 3032
|
|
38
|
+
};
|
|
39
|
+
const ResponseStatus = {
|
|
40
|
+
Ok: 0,
|
|
41
|
+
Oksofar: 4e3,
|
|
42
|
+
Attn: 4001,
|
|
43
|
+
Authmore: 4002,
|
|
44
|
+
Error: 4003,
|
|
45
|
+
Redirect: 4004,
|
|
46
|
+
Wait: 4005,
|
|
47
|
+
Waitresp: 4006,
|
|
48
|
+
Status: 4007
|
|
49
|
+
};
|
|
50
|
+
const ServerError = {
|
|
51
|
+
ArgInvalid: 3e3,
|
|
52
|
+
ArgMissing: 3001,
|
|
53
|
+
ArgTooLong: 3002,
|
|
54
|
+
FileLocked: 3003,
|
|
55
|
+
FileNotOpen: 3004,
|
|
56
|
+
FSError: 3005,
|
|
57
|
+
InvalidRequest: 3006,
|
|
58
|
+
IOError: 3007,
|
|
59
|
+
NoMemory: 3008,
|
|
60
|
+
NoSpace: 3009,
|
|
61
|
+
NotAuthorized: 3010,
|
|
62
|
+
NotFound: 3011,
|
|
63
|
+
ServerError: 3012,
|
|
64
|
+
Unsupported: 3013,
|
|
65
|
+
NoServer: 3014,
|
|
66
|
+
NotFile: 3015,
|
|
67
|
+
IsDirectory: 3016,
|
|
68
|
+
Cancelled: 3017,
|
|
69
|
+
ItExists: 3018,
|
|
70
|
+
CheckSumErr: 3019,
|
|
71
|
+
InProgress: 3020,
|
|
72
|
+
OverQuota: 3021,
|
|
73
|
+
SigVerErr: 3022,
|
|
74
|
+
DecryptErr: 3023,
|
|
75
|
+
Overloaded: 3024,
|
|
76
|
+
FsReadOnly: 3025,
|
|
77
|
+
BadPayload: 3026,
|
|
78
|
+
AttrNotFound: 3027,
|
|
79
|
+
TLSRequired: 3028,
|
|
80
|
+
NoReplicas: 3029,
|
|
81
|
+
AuthFailed: 3030,
|
|
82
|
+
Impossible: 3031,
|
|
83
|
+
Conflict: 3032,
|
|
84
|
+
TooManyErrs: 3033,
|
|
85
|
+
ReqTimedOut: 3034,
|
|
86
|
+
TimerExpired: 3035
|
|
87
|
+
};
|
|
88
|
+
const ClientError = {
|
|
89
|
+
Ok: 0,
|
|
90
|
+
InvalidArgs: 300,
|
|
91
|
+
NotFound: 301,
|
|
92
|
+
Permission: 302,
|
|
93
|
+
Serialization: 303,
|
|
94
|
+
CommandNotFound: 304,
|
|
95
|
+
HostNotFound: 305,
|
|
96
|
+
ServiceUnavail: 306,
|
|
97
|
+
InternalError: 307,
|
|
98
|
+
BadRequest: 308,
|
|
99
|
+
Timeout: 309,
|
|
100
|
+
InsufficientData: 310,
|
|
101
|
+
Uninitialized: 311,
|
|
102
|
+
Disconnected: 312,
|
|
103
|
+
Redirect: 313,
|
|
104
|
+
LossyRetry: 314,
|
|
105
|
+
TooManyRedirs: 315,
|
|
106
|
+
ChunkChecksumErr: 316,
|
|
107
|
+
UnexpectedResp: 317,
|
|
108
|
+
ClientSkipped: 318,
|
|
109
|
+
Failed: 501,
|
|
110
|
+
WinNetworkError: 601
|
|
111
|
+
};
|
|
14
112
|
const OpenFlags = {
|
|
15
|
-
|
|
16
|
-
|
|
113
|
+
Read: 16,
|
|
114
|
+
Write: 32,
|
|
115
|
+
Append: 512,
|
|
116
|
+
New: 8,
|
|
17
117
|
Delete: 2,
|
|
18
118
|
Force: 4,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Update: 32,
|
|
119
|
+
Compress: 1,
|
|
120
|
+
Async: 64,
|
|
22
121
|
Refresh: 128,
|
|
23
|
-
|
|
24
|
-
|
|
122
|
+
Mkpath: 256,
|
|
123
|
+
Retstat: 1024,
|
|
25
124
|
Replica: 2048,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
PrefName: 256,
|
|
31
|
-
Dup: 65536,
|
|
32
|
-
Samefs: 131072
|
|
125
|
+
Posc: 4096,
|
|
126
|
+
Nowait: 8192,
|
|
127
|
+
Seqio: 16384,
|
|
128
|
+
Wrto: 32768
|
|
33
129
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
130
|
+
const PROTOCOL_VERSION = 1312;
|
|
131
|
+
const REQUEST_HDR_SIZE = 24;
|
|
132
|
+
const RESPONSE_HDR_SIZE = 8;
|
|
133
|
+
const BODY_SIZE = 16;
|
|
134
|
+
const SESS_ID_SIZE = 16;
|
|
135
|
+
const FHANDLE_SIZE = 4;
|
|
136
|
+
const REQUEST_OFFSET_STREAM_ID = 0;
|
|
137
|
+
const REQUEST_OFFSET_REQUEST_ID = 2;
|
|
138
|
+
const REQUEST_OFFSET_BODY = 4;
|
|
139
|
+
const REQUEST_OFFSET_DLEN = 20;
|
|
140
|
+
const RESPONSE_OFFSET_STREAM_ID = 0;
|
|
141
|
+
const RESPONSE_OFFSET_STATUS = 2;
|
|
142
|
+
const RESPONSE_OFFSET_DLEN = 4;
|
|
143
|
+
const RESPONSE_OFFSET_BODY = 8;
|
|
144
|
+
const HANDSHAKE_FIRST = 0;
|
|
145
|
+
const HANDSHAKE_SECOND = 0;
|
|
146
|
+
const HANDSHAKE_THIRD = 0;
|
|
147
|
+
const HANDSHAKE_FOURTH = 4;
|
|
148
|
+
const HANDSHAKE_FIFTH = 2012;
|
|
149
|
+
const kXR_secreqs = 1;
|
|
150
|
+
const kXR_ableTLS = 2;
|
|
151
|
+
const kXR_wantTLS = 4;
|
|
152
|
+
const kXR_bifreqs = 8;
|
|
153
|
+
const kXR_ExpLogin = 1;
|
|
154
|
+
const kXR_ExpBind = 2;
|
|
155
|
+
const DEFAULT_PORT = 1094;
|
|
156
|
+
const S_IFDIR = 16384;
|
|
157
|
+
const S_IFLNK = 40960;
|
|
158
|
+
const DirlistOptions = {
|
|
159
|
+
Online: 1,
|
|
160
|
+
Dstat: 2,
|
|
161
|
+
Dcksm: 4,
|
|
162
|
+
Dstatx: 8
|
|
50
163
|
};
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
MakePath: 1
|
|
164
|
+
const CRED_TYPE = {
|
|
165
|
+
host: 0,
|
|
166
|
+
sss: 1,
|
|
167
|
+
unix: 2,
|
|
168
|
+
krb5: 3,
|
|
169
|
+
gsi: 4
|
|
58
170
|
};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/url/url.ts
|
|
173
|
+
var XRootDUrl = class XRootDUrl {
|
|
174
|
+
protocol;
|
|
175
|
+
user;
|
|
176
|
+
password;
|
|
177
|
+
host;
|
|
178
|
+
port;
|
|
179
|
+
path;
|
|
180
|
+
constructor(url) {
|
|
181
|
+
const normalized = url.startsWith("root://") || url.startsWith("roots://") ? url : `root://${url}`;
|
|
182
|
+
const parsed = new URL(normalized);
|
|
183
|
+
const protocol = parsed.protocol.replace(/:$/, "");
|
|
184
|
+
if (protocol !== "root" && protocol !== "roots") throw new Error(`Invalid XRootD URL protocol: ${protocol}`);
|
|
185
|
+
this.protocol = protocol;
|
|
186
|
+
this.host = parsed.hostname;
|
|
187
|
+
this.port = parsed.port ? parseInt(parsed.port, 10) : DEFAULT_PORT;
|
|
188
|
+
this.path = parsed.pathname || "/";
|
|
189
|
+
this.user = parsed.username || void 0;
|
|
190
|
+
this.password = parsed.password || void 0;
|
|
191
|
+
}
|
|
192
|
+
static parse(url) {
|
|
193
|
+
return new XRootDUrl(url);
|
|
194
|
+
}
|
|
195
|
+
toString() {
|
|
196
|
+
const auth = this.getAuthString();
|
|
197
|
+
const portStr = this.port === 1094 ? "" : `:${this.port}`;
|
|
198
|
+
return `${this.protocol}://${auth}${this.host}${portStr}${this.path}`;
|
|
199
|
+
}
|
|
200
|
+
isValid() {
|
|
201
|
+
return this.protocol === "root" || this.protocol === "roots";
|
|
202
|
+
}
|
|
203
|
+
isSecure() {
|
|
204
|
+
return this.protocol === "roots";
|
|
205
|
+
}
|
|
206
|
+
getHostId() {
|
|
207
|
+
return `${this.getAuthString()}${this.host}:${this.port}`;
|
|
208
|
+
}
|
|
209
|
+
getChannelId() {
|
|
210
|
+
return `${this.host}:${this.port}`;
|
|
211
|
+
}
|
|
212
|
+
getLocation() {
|
|
213
|
+
return `${this.protocol}://${this.host}:${this.port}${this.path}`;
|
|
214
|
+
}
|
|
215
|
+
getAuthString() {
|
|
216
|
+
if (!this.user) return "";
|
|
217
|
+
let auth = this.user;
|
|
218
|
+
if (this.password) auth += ":" + this.password;
|
|
219
|
+
return auth + "@";
|
|
220
|
+
}
|
|
75
221
|
};
|
|
76
|
-
//!< Back up copy exists
|
|
77
222
|
//#endregion
|
|
78
|
-
//#region
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
223
|
+
//#region src/transport/transport.ts
|
|
224
|
+
var Transport = class {
|
|
225
|
+
socket = null;
|
|
226
|
+
closeCallback = null;
|
|
227
|
+
errorCallback = null;
|
|
228
|
+
dataHandlers = [];
|
|
229
|
+
dataListenerInstalled = false;
|
|
230
|
+
async connect(host, port, useTls = false) {
|
|
231
|
+
this.socket = useTls ? await this.tlsConnect(host, port) : await this.tcpConnect(host, port);
|
|
232
|
+
this.socket.on("close", () => {
|
|
233
|
+
this.closeCallback?.();
|
|
234
|
+
});
|
|
235
|
+
this.socket.on("error", (err) => {
|
|
236
|
+
this.errorCallback?.(err);
|
|
237
|
+
});
|
|
238
|
+
this.socket.on("data", (chunk) => {
|
|
239
|
+
for (const handler of this.dataHandlers) handler(chunk);
|
|
240
|
+
});
|
|
241
|
+
this.dataListenerInstalled = true;
|
|
242
|
+
}
|
|
243
|
+
send(data) {
|
|
244
|
+
return new Promise((resolve, reject) => {
|
|
245
|
+
this.socket.write(data, (err) => err ? reject(err) : resolve());
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
onData(callback) {
|
|
249
|
+
this.dataHandlers.push(callback);
|
|
250
|
+
}
|
|
251
|
+
removeDataHandler(callback) {
|
|
252
|
+
const idx = this.dataHandlers.indexOf(callback);
|
|
253
|
+
if (idx >= 0) this.dataHandlers.splice(idx, 1);
|
|
254
|
+
}
|
|
255
|
+
onClose(callback) {
|
|
256
|
+
this.closeCallback = callback;
|
|
257
|
+
}
|
|
258
|
+
onError(callback) {
|
|
259
|
+
this.errorCallback = callback;
|
|
91
260
|
}
|
|
92
|
-
/**
|
|
93
|
-
* 打开远程文件
|
|
94
|
-
* @param url 目标地址 (如 root://server//path/to/file)
|
|
95
|
-
* @param flags 打开标志位
|
|
96
|
-
* @param mode 访问权限模式
|
|
97
|
-
*/
|
|
98
|
-
async open(url, flags = OpenFlags.None, mode = AccessMode.None) {
|
|
99
|
-
return this._internal.Open(url, flags, mode);
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* 关闭文件
|
|
103
|
-
*/
|
|
104
261
|
async close() {
|
|
105
|
-
|
|
262
|
+
this.destroy();
|
|
106
263
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
async stat() {
|
|
111
|
-
return this._internal.Stat();
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* 读取文件块 (Zero-Copy from C++)
|
|
115
|
-
* @param offset 偏移量 (支持 >2GB)
|
|
116
|
-
* @param size 读取字节数
|
|
117
|
-
* @returns 包含数据的 Node.js Buffer
|
|
118
|
-
*/
|
|
119
|
-
async read(offset, size) {
|
|
120
|
-
return this._internal.Read(BigInt(offset), size);
|
|
121
|
-
}
|
|
122
|
-
async write(offset, arg1, arg2, arg3) {
|
|
123
|
-
if (Buffer.isBuffer(arg1)) return this._internal.Write(BigInt(offset), arg1);
|
|
124
|
-
else if (typeof arg1 === "number" && typeof arg2 === "number") return this._internal.WriteFd(BigInt(offset), arg1, arg2, arg3 !== void 0 ? BigInt(arg3) : void 0);
|
|
125
|
-
else throw new TypeError("Invalid arguments for write");
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* 从本地 fd 写入文件块 (直接映射)
|
|
129
|
-
* @param offset 写入的起始字节偏移量。
|
|
130
|
-
* @param size 写入大小。
|
|
131
|
-
* @param fd 本地文件描述符 (fd)。
|
|
132
|
-
* @param fdoff 可选,从本地 fd 中读取的起始偏移。
|
|
133
|
-
*/
|
|
134
|
-
async writeFd(offset, size, fd, fdoff) {
|
|
135
|
-
return this._internal.WriteFd(BigInt(offset), size, fd, fdoff !== void 0 ? BigInt(fdoff) : void 0);
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* 同步文件缓冲区到磁盘
|
|
139
|
-
*/
|
|
140
|
-
async sync() {
|
|
141
|
-
return this._internal.Sync();
|
|
264
|
+
destroy() {
|
|
265
|
+
this.socket?.destroy();
|
|
266
|
+
this.socket = null;
|
|
142
267
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
async truncate(size) {
|
|
148
|
-
return this._internal.Truncate(BigInt(size));
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* 检查本地实例状态 (同步操作)
|
|
152
|
-
*/
|
|
153
|
-
isOpen() {
|
|
154
|
-
return this._internal.IsOpen();
|
|
155
|
-
}
|
|
156
|
-
async getProperty(name) {
|
|
157
|
-
const ret = this._internal.GetProperty(name);
|
|
158
|
-
if (ret.success) return ret.value;
|
|
159
|
-
throw new Error("TODO");
|
|
160
|
-
}
|
|
161
|
-
async setProperty(name, value) {
|
|
162
|
-
return this._internal.SetProperty(name, value);
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* 向量化读取 (Vector Read)
|
|
166
|
-
* 在单个请求中从文件的多个非连续区域读取数据,极大地减少网络往返开销。
|
|
167
|
-
* @param chunks 包含 offset 和 size 的读取请求数组
|
|
168
|
-
* @returns 与请求数组顺序对应的 Buffer 数组
|
|
169
|
-
*/
|
|
170
|
-
async vectorRead(chunks) {
|
|
171
|
-
const normalizedChunks = chunks.map((c) => ({
|
|
172
|
-
offset: BigInt(c.offset),
|
|
173
|
-
size: c.size
|
|
174
|
-
}));
|
|
175
|
-
return this._internal.VectorRead(normalizedChunks);
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* 读取块 (Read Chunks)
|
|
179
|
-
* 类似于 VectorRead,但底层实现可能利用更高级的预读或多路复用策略。
|
|
180
|
-
*/
|
|
181
|
-
async readChunks(chunks) {
|
|
182
|
-
const normalizedChunks = chunks.map((c) => ({
|
|
183
|
-
offset: BigInt(c.offset),
|
|
184
|
-
size: c.size
|
|
185
|
-
}));
|
|
186
|
-
return this._internal.ReadChunks(normalizedChunks);
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* 设置文件的扩展属性
|
|
190
|
-
*/
|
|
191
|
-
async setXAttrs(attrs) {
|
|
192
|
-
return this._internal.SetXAttr(attrs);
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* 设置文件的扩展属性
|
|
196
|
-
*/
|
|
197
|
-
async setXAttr(key, value) {
|
|
198
|
-
return (await this._internal.SetXAttr({ [key]: value }))[0].ok;
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* 获取文件的扩展属性
|
|
202
|
-
*/
|
|
203
|
-
async getXAttrs(keys) {
|
|
204
|
-
return this._internal.GetXAttr(keys);
|
|
205
|
-
}
|
|
206
|
-
async getXAttr(key) {
|
|
207
|
-
return (await this._internal.GetXAttr([key]))[key];
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* 删除文件的扩展属性
|
|
211
|
-
*/
|
|
212
|
-
async delXAttrs(keys) {
|
|
213
|
-
return this._internal.DelXAttr(keys);
|
|
214
|
-
}
|
|
215
|
-
async delXAttr(key) {
|
|
216
|
-
return (await this._internal.DelXAttr([key]))[0].ok;
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* 列出文件所有的扩展属性和内容
|
|
220
|
-
*/
|
|
221
|
-
async listXAttrs() {
|
|
222
|
-
return this._internal.ListXAttr();
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* 将其他文件的指定区间在服务器端克隆到当前文件
|
|
226
|
-
* 当前文件必须以写入/更新模式打开
|
|
227
|
-
*/
|
|
228
|
-
clone(locations) {
|
|
229
|
-
return this._internal.Clone(locations);
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* 创建一个可读流 (Readable Stream)
|
|
233
|
-
* 使得 XRootD 文件可以无缝 pipe 到其他 Node.js 流 (如本地 fs, HTTP response)
|
|
234
|
-
*/
|
|
235
|
-
createReadStream(options = {}) {
|
|
236
|
-
let currentOffset = options.start ?? 0n;
|
|
237
|
-
const endOffset = options.end;
|
|
238
|
-
const chunkSize = options.highWaterMark ?? 64 * 1024;
|
|
239
|
-
const self = this;
|
|
240
|
-
return new Readable({
|
|
241
|
-
highWaterMark: chunkSize,
|
|
242
|
-
async read(size) {
|
|
243
|
-
try {
|
|
244
|
-
let bytesToRead = size;
|
|
245
|
-
if (endOffset !== void 0) {
|
|
246
|
-
const remaining = endOffset - currentOffset + 1n;
|
|
247
|
-
if (remaining <= 0n) {
|
|
248
|
-
this.push(null);
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
if (remaining < BigInt(bytesToRead)) bytesToRead = Number(remaining);
|
|
252
|
-
}
|
|
253
|
-
const buffer = await self.read(currentOffset, bytesToRead);
|
|
254
|
-
if (buffer.length === 0) this.push(null);
|
|
255
|
-
else {
|
|
256
|
-
currentOffset += BigInt(buffer.length);
|
|
257
|
-
this.push(buffer);
|
|
258
|
-
}
|
|
259
|
-
} catch (err) {
|
|
260
|
-
this.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
261
|
-
}
|
|
262
|
-
}
|
|
268
|
+
tcpConnect(host, port) {
|
|
269
|
+
return new Promise((resolve, reject) => {
|
|
270
|
+
const socket = net.connect(port, host, () => resolve(socket));
|
|
271
|
+
socket.once("error", reject);
|
|
263
272
|
});
|
|
264
273
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
async write(chunk, encoding, callback) {
|
|
274
|
-
try {
|
|
275
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
|
|
276
|
-
await self.write(currentOffset, buffer);
|
|
277
|
-
currentOffset += BigInt(buffer.length);
|
|
278
|
-
callback();
|
|
279
|
-
} catch (err) {
|
|
280
|
-
callback(err instanceof Error ? err : new Error(String(err)));
|
|
281
|
-
}
|
|
282
|
-
},
|
|
283
|
-
async writev(chunks, callback) {
|
|
284
|
-
try {
|
|
285
|
-
const buffers = chunks.map((c) => Buffer.isBuffer(c.chunk) ? c.chunk : Buffer.from(c.chunk, c.encoding));
|
|
286
|
-
const masterBuffer = Buffer.concat(buffers);
|
|
287
|
-
await self.write(currentOffset, masterBuffer);
|
|
288
|
-
currentOffset += BigInt(masterBuffer.length);
|
|
289
|
-
callback();
|
|
290
|
-
} catch (err) {
|
|
291
|
-
callback(err instanceof Error ? err : new Error(String(err)));
|
|
292
|
-
}
|
|
293
|
-
}
|
|
274
|
+
tlsConnect(host, port) {
|
|
275
|
+
return new Promise((resolve, reject) => {
|
|
276
|
+
const socket = tls.connect({
|
|
277
|
+
host,
|
|
278
|
+
port,
|
|
279
|
+
rejectUnauthorized: false
|
|
280
|
+
}, () => resolve(socket));
|
|
281
|
+
socket.once("error", reject);
|
|
294
282
|
});
|
|
295
283
|
}
|
|
296
284
|
};
|
|
297
285
|
//#endregion
|
|
298
|
-
//#region
|
|
299
|
-
function reverseStr(str) {
|
|
300
|
-
let reversed = "";
|
|
301
|
-
for (let i = str.length - 1; i >= 0; i--) reversed += str.charAt(i);
|
|
302
|
-
return reversed;
|
|
303
|
-
}
|
|
304
|
-
function isXrdError(e) {
|
|
305
|
-
return e instanceof Error && "xrdStatus" in e && typeof e.xrdStatus === "number" && e.xrdStatus > 0;
|
|
306
|
-
}
|
|
307
|
-
//#endregion
|
|
308
|
-
//#region lib/filesystem.ts
|
|
286
|
+
//#region src/transport/framer.ts
|
|
309
287
|
/**
|
|
310
|
-
* XRootD
|
|
311
|
-
*
|
|
288
|
+
* Frame parser: handles TCP fragmentation, splits byte stream into complete XRootD response frames.
|
|
289
|
+
*
|
|
290
|
+
* XRootD response format:
|
|
291
|
+
* streamid[2] + status[2] + dlen[4] + body[dlen]
|
|
292
|
+
* Fixed header 8 bytes + variable body
|
|
312
293
|
*/
|
|
313
|
-
var
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const posixPath = posix.normalize(targetPath);
|
|
330
|
-
return posixPath.startsWith("/") ? posixPath : "/" + posixPath;
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
|
-
* 定位文件在集群中的具体数据节点
|
|
334
|
-
* @param filePath 目标文件路径
|
|
335
|
-
* @param flags 定位标志位 (默认 0)
|
|
336
|
-
* @returns 包含主机、端口等信息的数组
|
|
337
|
-
*/
|
|
338
|
-
async locate(filePath, flags = 0) {
|
|
339
|
-
return this._internal.Locate(this._normalize(filePath), flags);
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* 获取文件或目录的状态信息
|
|
343
|
-
* @param targetPath 目标路径
|
|
344
|
-
*/
|
|
345
|
-
async stat(targetPath) {
|
|
346
|
-
const rawStat = await this._internal.Stat(this._normalize(targetPath));
|
|
347
|
-
return {
|
|
348
|
-
...rawStat,
|
|
349
|
-
get modeOctal() {
|
|
350
|
-
return reverseStr(rawStat.modeAsOctString);
|
|
351
|
-
},
|
|
352
|
-
get modeString() {
|
|
353
|
-
return reverseStr(rawStat.modeAsString);
|
|
354
|
-
},
|
|
355
|
-
get isFile() {
|
|
356
|
-
return (rawStat.flags & StatFlags.IsDir) === 0 && (rawStat.flags & StatFlags.Other) === 0;
|
|
357
|
-
},
|
|
358
|
-
get isDir() {
|
|
359
|
-
return (rawStat.flags & StatFlags.IsDir) !== 0;
|
|
360
|
-
},
|
|
361
|
-
get isOther() {
|
|
362
|
-
return (rawStat.flags & StatFlags.Other) !== 0;
|
|
363
|
-
},
|
|
364
|
-
get isOffline() {
|
|
365
|
-
return (rawStat.flags & StatFlags.Offline) !== 0;
|
|
366
|
-
},
|
|
367
|
-
get isPOSCPending() {
|
|
368
|
-
return (rawStat.flags & StatFlags.POSCPending) !== 0;
|
|
369
|
-
},
|
|
370
|
-
get isReadable() {
|
|
371
|
-
return (rawStat.flags & StatFlags.IsReadable) !== 0;
|
|
372
|
-
},
|
|
373
|
-
get isWritable() {
|
|
374
|
-
return (rawStat.flags & StatFlags.IsWritable) !== 0;
|
|
375
|
-
},
|
|
376
|
-
get isBackUpExists() {
|
|
377
|
-
return (rawStat.flags & StatFlags.BackUpExists) !== 0;
|
|
378
|
-
}
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* 删除远程文件
|
|
383
|
-
* @param filePath 要删除的文件路径
|
|
384
|
-
*/
|
|
385
|
-
async rm(filePath) {
|
|
386
|
-
return this._internal.Rm(this._normalize(filePath));
|
|
387
|
-
}
|
|
388
|
-
/**
|
|
389
|
-
* 创建远程目录
|
|
390
|
-
* @param dirPath 目录路径
|
|
391
|
-
* @param flags 标志位 (例如 MakePath,允许创建多级父目录)
|
|
392
|
-
* @param mode 访问权限模式 (默认 0755 对应的 AccessMode)
|
|
393
|
-
*/
|
|
394
|
-
async mkdir(dirPath, flags = MkDirFlags.None, mode = AccessMode.UR | AccessMode.UW | AccessMode.UX | AccessMode.GR | AccessMode.GX | AccessMode.OR | AccessMode.OX) {
|
|
395
|
-
return this._internal.MkDir(this._normalize(dirPath), flags, mode);
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* 删除远程目录 (目录必须为空)
|
|
399
|
-
* @param dirPath 要删除的目录路径
|
|
400
|
-
*/
|
|
401
|
-
async rmdir(dirPath) {
|
|
402
|
-
return this._internal.RmDir(this._normalize(dirPath));
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* 移动或重命名文件/目录
|
|
406
|
-
* @param source 源路径
|
|
407
|
-
* @param dest 目标路径
|
|
408
|
-
*/
|
|
409
|
-
async mv(source, dest) {
|
|
410
|
-
return this._internal.Mv(this._normalize(source), this._normalize(dest));
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* 列出目录下的所有文件和子目录名
|
|
414
|
-
* @param dirPath 目录路径
|
|
415
|
-
* @param flags 标志位 (例如是否显示隐藏文件)
|
|
416
|
-
*/
|
|
417
|
-
async dirList(dirPath, flags = 0) {
|
|
418
|
-
return this._internal.DirList(this._normalize(dirPath), flags);
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* 检查文件或目录是否存在
|
|
422
|
-
* (通过捕获 stat 的错误来实现,类似于老版本 Node 的 fs.exists)
|
|
423
|
-
* @param targetPath 目标路径
|
|
424
|
-
*/
|
|
425
|
-
async exists(targetPath) {
|
|
426
|
-
try {
|
|
427
|
-
await this.stat(targetPath);
|
|
428
|
-
return true;
|
|
429
|
-
} catch (err) {
|
|
430
|
-
debugger;
|
|
431
|
-
if (isXrdError(err)) {
|
|
432
|
-
if (err.xrdErrNo === 3011) return false;
|
|
433
|
-
}
|
|
434
|
-
throw err;
|
|
294
|
+
var Framer = class {
|
|
295
|
+
pending = Buffer.alloc(0);
|
|
296
|
+
/** Feed raw bytes, return parsed complete frames (0 or more) */
|
|
297
|
+
feed(chunk) {
|
|
298
|
+
this.pending = Buffer.concat([this.pending, chunk]);
|
|
299
|
+
const frames = [];
|
|
300
|
+
while (this.pending.length >= 8) {
|
|
301
|
+
const dlen = this.pending.readUInt32BE(4);
|
|
302
|
+
if (this.pending.length < 8 + dlen) break;
|
|
303
|
+
frames.push({
|
|
304
|
+
streamId: this.pending.subarray(0, 2),
|
|
305
|
+
status: this.pending.readUInt16BE(2),
|
|
306
|
+
dlen,
|
|
307
|
+
body: this.pending.subarray(8, 8 + dlen)
|
|
308
|
+
});
|
|
309
|
+
this.pending = this.pending.subarray(8 + dlen);
|
|
435
310
|
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* 确保目录存在。如果目录不存在,则会自动创建它及其所有父目录。
|
|
439
|
-
* (类似于 fs-extra 的 ensureDir 或 mkdir -p)
|
|
440
|
-
* @param dirPath 目标目录
|
|
441
|
-
*/
|
|
442
|
-
async ensureDir(dirPath) {
|
|
443
|
-
if (!await this.exists(dirPath)) await this.mkdir(dirPath, MkDirFlags.MakePath);
|
|
444
|
-
}
|
|
445
|
-
/**
|
|
446
|
-
* 深度定位:返回包含所有数据节点副本的详细物理位置信息
|
|
447
|
-
*/
|
|
448
|
-
async deepLocate(filePath, flags = 0) {
|
|
449
|
-
return this._internal.DeepLocate(this._normalize(filePath), flags);
|
|
450
|
-
}
|
|
451
|
-
/**
|
|
452
|
-
* 在不打开文件的情况下,直接截断目标文件
|
|
453
|
-
*/
|
|
454
|
-
async truncate(filePath, size) {
|
|
455
|
-
return this._internal.Truncate(this._normalize(filePath), size);
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* 更改远程文件或目录的访问权限
|
|
459
|
-
*/
|
|
460
|
-
async chmod(targetPath, mode) {
|
|
461
|
-
return this._internal.ChMod(this._normalize(targetPath), mode);
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
|
-
* 探活:检查远端文件系统服务是否响应
|
|
465
|
-
*/
|
|
466
|
-
async ping() {
|
|
467
|
-
return this._internal.Ping();
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* 获取虚拟文件系统(VFS)的状态(如磁盘总容量、剩余可用空间)
|
|
471
|
-
*/
|
|
472
|
-
async statVFS(targetPath) {
|
|
473
|
-
return this._internal.StatVFS(this._normalize(targetPath));
|
|
474
|
-
}
|
|
475
|
-
/**
|
|
476
|
-
* 获取当前连接协议的详细属性
|
|
477
|
-
*/
|
|
478
|
-
async protocol() {
|
|
479
|
-
return this._internal.Protocol();
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* 发送带外查询指令到数据节点 (通常用于 XRootD 的高级自定义插件)
|
|
483
|
-
*/
|
|
484
|
-
async query(queryCode, args) {
|
|
485
|
-
return this._internal.Query(queryCode, args);
|
|
486
|
-
}
|
|
487
|
-
/**
|
|
488
|
-
* 发送通用信息到服务器
|
|
489
|
-
*/
|
|
490
|
-
async sendInfo(info) {
|
|
491
|
-
return this._internal.SendInfo(info);
|
|
492
|
-
}
|
|
493
|
-
/**
|
|
494
|
-
* 发送缓存操作信息给集群
|
|
495
|
-
*/
|
|
496
|
-
async sendCache(info) {
|
|
497
|
-
return this._internal.SendCache(info);
|
|
498
|
-
}
|
|
499
|
-
/**
|
|
500
|
-
* 数据预热/暂存 (Staging):
|
|
501
|
-
* 在处理海量物理数据时,通知存储集群将特定的冷数据(如磁带上的文件)提前拉取到磁盘缓存。
|
|
502
|
-
* @param targetPaths 需要预热的路径数组
|
|
503
|
-
* @param flags 预热策略标志
|
|
504
|
-
* @param priority 优先级
|
|
505
|
-
*/
|
|
506
|
-
async prepare(targetPaths, flags = 0, priority = 0) {
|
|
507
|
-
const normalizedPaths = targetPaths.map((p) => this._normalize(p));
|
|
508
|
-
return this._internal.Prepare(normalizedPaths, flags, priority);
|
|
509
|
-
}
|
|
510
|
-
/**
|
|
511
|
-
* 获取文件系统属性
|
|
512
|
-
*/
|
|
513
|
-
getProperty(name) {
|
|
514
|
-
return this._internal.GetProperty(name);
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* 设置文件系统属性
|
|
518
|
-
*/
|
|
519
|
-
setProperty(name, value) {
|
|
520
|
-
return this._internal.SetProperty(name, value);
|
|
521
|
-
}
|
|
522
|
-
/**
|
|
523
|
-
* 设置扩展属性
|
|
524
|
-
* @param targetPath 目标路径
|
|
525
|
-
* @param attrs 扩展属性键值对记录
|
|
526
|
-
*/
|
|
527
|
-
async setXAttrs(targetPath, attrs) {
|
|
528
|
-
return this._internal.SetXAttr(this._normalize(targetPath), attrs);
|
|
529
|
-
}
|
|
530
|
-
async setXAttr(targetPath, key, value) {
|
|
531
|
-
return (await this.setXAttrs(targetPath, { [key]: value }))[0].ok;
|
|
532
|
-
}
|
|
533
|
-
/**
|
|
534
|
-
* 获取扩展属性
|
|
535
|
-
* @param targetPath 目标路径
|
|
536
|
-
* @param keys 需要获取的属性名数组
|
|
537
|
-
*/
|
|
538
|
-
async getXAttrs(targetPath, keys) {
|
|
539
|
-
return this._internal.GetXAttr(this._normalize(targetPath), keys);
|
|
540
|
-
}
|
|
541
|
-
async getXAttr(targetPath, key) {
|
|
542
|
-
return (await this.getXAttrs(targetPath, [key]))[key];
|
|
543
|
-
}
|
|
544
|
-
/**
|
|
545
|
-
* 删除指定的扩展属性
|
|
546
|
-
* @param targetPath 目标路径
|
|
547
|
-
* @param keys 需要删除的属性名数组
|
|
548
|
-
*/
|
|
549
|
-
async delXAttrs(targetPath, keys) {
|
|
550
|
-
return this._internal.DelXAttr(this._normalize(targetPath), keys);
|
|
551
|
-
}
|
|
552
|
-
async delXAttr(targetPath, key) {
|
|
553
|
-
return (await this.delXAttrs(targetPath, [key]))[0].ok;
|
|
554
|
-
}
|
|
555
|
-
/**
|
|
556
|
-
* 列出目标文件或目录的所有扩展属性
|
|
557
|
-
*/
|
|
558
|
-
async listXAttr(targetPath) {
|
|
559
|
-
return this._internal.ListXAttr(this._normalize(targetPath));
|
|
311
|
+
return frames;
|
|
560
312
|
}
|
|
561
313
|
};
|
|
562
314
|
//#endregion
|
|
563
|
-
//#region
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
315
|
+
//#region src/protocol/message-class.ts
|
|
316
|
+
/**
|
|
317
|
+
* Message builder for constructing binary protocol messages.
|
|
318
|
+
* Tracks offset automatically during writes.
|
|
319
|
+
*/
|
|
320
|
+
var Message = class {
|
|
321
|
+
buffer;
|
|
322
|
+
offset = 0;
|
|
323
|
+
constructor(size) {
|
|
324
|
+
this.buffer = Buffer.alloc(size);
|
|
325
|
+
}
|
|
326
|
+
writeInt32BE(value) {
|
|
327
|
+
this.buffer.writeInt32BE(value, this.offset);
|
|
328
|
+
this.offset += 4;
|
|
329
|
+
}
|
|
330
|
+
writeInt16BE(value) {
|
|
331
|
+
this.buffer.writeInt16BE(value, this.offset);
|
|
332
|
+
this.offset += 2;
|
|
333
|
+
}
|
|
334
|
+
writeUInt8(value) {
|
|
335
|
+
this.buffer.writeUInt8(value, this.offset);
|
|
336
|
+
this.offset += 1;
|
|
337
|
+
}
|
|
338
|
+
writeBytes(data) {
|
|
339
|
+
Buffer.from(data).copy(this.buffer, this.offset);
|
|
340
|
+
this.offset += data.length;
|
|
341
|
+
}
|
|
342
|
+
readInt32BE() {
|
|
343
|
+
const value = this.buffer.readInt32BE(this.offset);
|
|
344
|
+
this.offset += 4;
|
|
345
|
+
return value;
|
|
346
|
+
}
|
|
347
|
+
readInt16BE() {
|
|
348
|
+
const value = this.buffer.readInt16BE(this.offset);
|
|
349
|
+
this.offset += 2;
|
|
350
|
+
return value;
|
|
351
|
+
}
|
|
352
|
+
readBytes(length) {
|
|
353
|
+
const data = this.buffer.subarray(this.offset, this.offset + length);
|
|
354
|
+
this.offset += length;
|
|
355
|
+
return data;
|
|
356
|
+
}
|
|
357
|
+
getBuffer() {
|
|
358
|
+
return this.buffer.subarray(0, this.offset);
|
|
601
359
|
}
|
|
602
360
|
};
|
|
603
361
|
//#endregion
|
|
604
|
-
//#region
|
|
362
|
+
//#region src/utils/bytes.ts
|
|
605
363
|
/**
|
|
606
|
-
*
|
|
607
|
-
* 安全、轻量,避免跨越 C++ N-API 边界
|
|
364
|
+
* Byte conversion utilities for the XRootD protocol.
|
|
608
365
|
*/
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
366
|
+
/** Convert a stream ID (uint16) to a 2-byte big-endian Uint8Array. */
|
|
367
|
+
function streamIdToBytes(sid) {
|
|
368
|
+
return new Uint8Array([sid >> 8 & 255, sid & 255]);
|
|
369
|
+
}
|
|
370
|
+
/** Convert a string to UTF-8 bytes. */
|
|
371
|
+
function strToBytes(str) {
|
|
372
|
+
return Buffer.from(str, "utf8");
|
|
373
|
+
}
|
|
374
|
+
/** Parse a stream ID from 2 big-endian bytes. */
|
|
375
|
+
function bytesToStreamId(bytes) {
|
|
376
|
+
return bytes[0] << 8 | bytes[1];
|
|
377
|
+
}
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region src/protocol/builders.ts
|
|
380
|
+
/**
|
|
381
|
+
* Request builders for XRootD protocol messages.
|
|
382
|
+
* Each function creates a Buffer ready to send on the wire.
|
|
383
|
+
*/
|
|
384
|
+
/**
|
|
385
|
+
* Initial handshake (20 B) + kXR_protocol (24 B) merged into one 44-byte
|
|
386
|
+
* buffer ready to send immediately after TCP connect.
|
|
387
|
+
*
|
|
388
|
+
* Layout:
|
|
389
|
+
* [0..19] ClientInitHandShake
|
|
390
|
+
* [20..43] kXR_protocol request (streamid=0, body, dlen)
|
|
391
|
+
*/
|
|
392
|
+
function buildHandshakeAndProtocol(streamId, flags = 1, expect = 1) {
|
|
393
|
+
const msg = new Message(44);
|
|
394
|
+
msg.writeInt32BE(0);
|
|
395
|
+
msg.writeInt32BE(0);
|
|
396
|
+
msg.writeInt32BE(0);
|
|
397
|
+
msg.writeInt32BE(4);
|
|
398
|
+
msg.writeInt32BE(HANDSHAKE_FIFTH);
|
|
399
|
+
msg.writeBytes(streamIdToBytes(streamId));
|
|
400
|
+
msg.writeInt16BE(RequestId.Protocol);
|
|
401
|
+
msg.writeInt32BE(PROTOCOL_VERSION);
|
|
402
|
+
msg.writeUInt8(flags & 255);
|
|
403
|
+
msg.writeUInt8(expect & 255);
|
|
404
|
+
msg.writeBytes(/* @__PURE__ */ new Uint8Array(10));
|
|
405
|
+
msg.writeInt32BE(0);
|
|
406
|
+
return msg.getBuffer();
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* kXR_login request (24 B header only, or 24 B + CGI string).
|
|
410
|
+
*
|
|
411
|
+
* Body layout (16 B):
|
|
412
|
+
* pid[4] + username[8] + ability2[1] + ability[1] + capver[1] + reserved[1]
|
|
413
|
+
*/
|
|
414
|
+
function buildLoginRequest(streamId, pid, username, ability = 0, cgi) {
|
|
415
|
+
const cgiBytes = cgi ? strToBytes(cgi) : void 0;
|
|
416
|
+
const msg = new Message(24 + (cgiBytes?.length ?? 0));
|
|
417
|
+
msg.writeBytes(streamIdToBytes(streamId));
|
|
418
|
+
msg.writeInt16BE(RequestId.Login);
|
|
419
|
+
msg.writeInt32BE(pid);
|
|
420
|
+
const u8 = strToBytes(username);
|
|
421
|
+
msg.writeBytes(u8.length > 8 ? u8.subarray(0, 8) : u8);
|
|
422
|
+
if (u8.length < 8) msg.writeBytes(new Uint8Array(8 - u8.length));
|
|
423
|
+
msg.writeUInt8(0);
|
|
424
|
+
msg.writeUInt8(ability & 255);
|
|
425
|
+
msg.writeUInt8(4);
|
|
426
|
+
msg.writeUInt8(0);
|
|
427
|
+
msg.writeInt32BE(cgiBytes?.length ?? 0);
|
|
428
|
+
if (cgiBytes && cgiBytes.length > 0) msg.writeBytes(cgiBytes);
|
|
429
|
+
return msg.getBuffer();
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* kXR_open request (24 B header + path string).
|
|
433
|
+
*
|
|
434
|
+
* Body layout (16 B):
|
|
435
|
+
* mode[2] + options[2] + optiont[2] + reserved[6] + fhtemplt[4]
|
|
436
|
+
*/
|
|
437
|
+
function buildOpenRequest(streamId, path, options, mode = 0) {
|
|
438
|
+
const pathBytes = strToBytes(path);
|
|
439
|
+
const msg = new Message(24 + pathBytes.length);
|
|
440
|
+
msg.writeBytes(streamIdToBytes(streamId));
|
|
441
|
+
msg.writeInt16BE(RequestId.Open);
|
|
442
|
+
msg.writeInt16BE(mode & 65535);
|
|
443
|
+
msg.writeInt16BE(options & 65535);
|
|
444
|
+
msg.writeInt16BE(0);
|
|
445
|
+
msg.writeBytes(/* @__PURE__ */ new Uint8Array(6));
|
|
446
|
+
msg.writeBytes(/* @__PURE__ */ new Uint8Array(4));
|
|
447
|
+
msg.writeInt32BE(pathBytes.length);
|
|
448
|
+
msg.writeBytes(pathBytes);
|
|
449
|
+
return msg.getBuffer();
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* kXR_read request (24 B header, no extra data).
|
|
453
|
+
*
|
|
454
|
+
* Body layout (16 B):
|
|
455
|
+
* fhandle[4] + offset[8] + rlen[4]
|
|
456
|
+
*/
|
|
457
|
+
function buildReadRequest(streamId, fhandle, offset, rlen) {
|
|
458
|
+
const msg = new Message(24);
|
|
459
|
+
msg.writeBytes(streamIdToBytes(streamId));
|
|
460
|
+
msg.writeInt16BE(RequestId.Read);
|
|
461
|
+
msg.writeBytes(fhandle);
|
|
462
|
+
msg.writeInt32BE(Math.floor(offset / 4294967296));
|
|
463
|
+
msg.writeInt32BE(offset >>> 0);
|
|
464
|
+
msg.writeInt32BE(rlen);
|
|
465
|
+
msg.writeInt32BE(0);
|
|
466
|
+
return msg.getBuffer();
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* kXR_write request (24 B header + data bytes).
|
|
470
|
+
*
|
|
471
|
+
* Body layout (16 B):
|
|
472
|
+
* fhandle[4] + offset[8] + pathid[1] + reserved[3]
|
|
473
|
+
*/
|
|
474
|
+
function buildWriteRequest(streamId, fhandle, offset, data) {
|
|
475
|
+
const msg = new Message(24 + data.length);
|
|
476
|
+
msg.writeBytes(streamIdToBytes(streamId));
|
|
477
|
+
msg.writeInt16BE(RequestId.Write);
|
|
478
|
+
msg.writeBytes(fhandle);
|
|
479
|
+
msg.writeInt32BE(Math.floor(offset / 4294967296));
|
|
480
|
+
msg.writeInt32BE(offset >>> 0);
|
|
481
|
+
msg.writeUInt8(0);
|
|
482
|
+
msg.writeBytes(/* @__PURE__ */ new Uint8Array(3));
|
|
483
|
+
msg.writeInt32BE(data.length);
|
|
484
|
+
msg.writeBytes(data);
|
|
485
|
+
return msg.getBuffer();
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* kXR_close request (24 B header, no extra data).
|
|
489
|
+
*
|
|
490
|
+
* Body layout (16 B):
|
|
491
|
+
* fhandle[4] + reserved[12]
|
|
492
|
+
*/
|
|
493
|
+
function buildCloseRequest(streamId, fhandle) {
|
|
494
|
+
const msg = new Message(24);
|
|
495
|
+
msg.writeBytes(streamIdToBytes(streamId));
|
|
496
|
+
msg.writeInt16BE(RequestId.Close);
|
|
497
|
+
msg.writeBytes(fhandle);
|
|
498
|
+
msg.writeBytes(/* @__PURE__ */ new Uint8Array(12));
|
|
499
|
+
msg.writeInt32BE(0);
|
|
500
|
+
return msg.getBuffer();
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* kXR_stat request (24 B header + optional path string).
|
|
504
|
+
*
|
|
505
|
+
* Body layout (16 B):
|
|
506
|
+
* options[1] + reserved[7] + wants[4] + fhandle[4]
|
|
507
|
+
*/
|
|
508
|
+
function buildStatRequest(streamId, path, fhandle) {
|
|
509
|
+
const pathBytes = strToBytes(path);
|
|
510
|
+
const msg = new Message(24 + pathBytes.length);
|
|
511
|
+
msg.writeBytes(streamIdToBytes(streamId));
|
|
512
|
+
msg.writeInt16BE(RequestId.Stat);
|
|
513
|
+
if (fhandle) {
|
|
514
|
+
msg.writeUInt8(0);
|
|
515
|
+
msg.writeBytes(/* @__PURE__ */ new Uint8Array(7));
|
|
516
|
+
msg.writeBytes(/* @__PURE__ */ new Uint8Array(4));
|
|
517
|
+
msg.writeBytes(fhandle);
|
|
518
|
+
msg.writeInt32BE(0);
|
|
519
|
+
} else {
|
|
520
|
+
msg.writeUInt8(0);
|
|
521
|
+
msg.writeBytes(/* @__PURE__ */ new Uint8Array(7));
|
|
522
|
+
msg.writeBytes(/* @__PURE__ */ new Uint8Array(4));
|
|
523
|
+
msg.writeBytes(/* @__PURE__ */ new Uint8Array(4));
|
|
524
|
+
msg.writeInt32BE(pathBytes.length);
|
|
525
|
+
msg.writeBytes(pathBytes);
|
|
526
|
+
}
|
|
527
|
+
return msg.getBuffer();
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* kXR_sync request (24 B header, no extra data).
|
|
531
|
+
*
|
|
532
|
+
* Body layout (16 B):
|
|
533
|
+
* fhandle[4] + reserved[12]
|
|
534
|
+
*/
|
|
535
|
+
function buildSyncRequest(streamId, fhandle) {
|
|
536
|
+
const msg = new Message(24);
|
|
537
|
+
msg.writeBytes(streamIdToBytes(streamId));
|
|
538
|
+
msg.writeInt16BE(RequestId.Sync);
|
|
539
|
+
msg.writeBytes(fhandle);
|
|
540
|
+
msg.writeBytes(/* @__PURE__ */ new Uint8Array(12));
|
|
541
|
+
msg.writeInt32BE(0);
|
|
542
|
+
return msg.getBuffer();
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* kXR_truncate request (24 B header + 8 B size).
|
|
546
|
+
*
|
|
547
|
+
* Body layout (16 B):
|
|
548
|
+
* fhandle[4] + reserved[12]
|
|
549
|
+
* Extra data:
|
|
550
|
+
* size[8] (int64 BE)
|
|
551
|
+
*/
|
|
552
|
+
function buildTruncateRequest(streamId, fhandle, size) {
|
|
553
|
+
const msg = new Message(32);
|
|
554
|
+
msg.writeBytes(streamIdToBytes(streamId));
|
|
555
|
+
msg.writeInt16BE(RequestId.Truncate);
|
|
556
|
+
msg.writeBytes(fhandle);
|
|
557
|
+
msg.writeBytes(/* @__PURE__ */ new Uint8Array(12));
|
|
558
|
+
msg.writeInt32BE(8);
|
|
559
|
+
msg.writeInt32BE(Math.floor(size / 4294967296));
|
|
560
|
+
msg.writeInt32BE(size >>> 0);
|
|
561
|
+
return msg.getBuffer();
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* kXR_dirlist request (24 B header + path string).
|
|
565
|
+
*
|
|
566
|
+
* Body layout (16 B):
|
|
567
|
+
* reserved[15] + options[1]
|
|
568
|
+
*/
|
|
569
|
+
function buildDirlistRequest(streamId, path, options = 0) {
|
|
570
|
+
const pathBytes = strToBytes(path);
|
|
571
|
+
const msg = new Message(24 + pathBytes.length);
|
|
572
|
+
msg.writeBytes(streamIdToBytes(streamId));
|
|
573
|
+
msg.writeInt16BE(RequestId.Dirlist);
|
|
574
|
+
msg.writeBytes(/* @__PURE__ */ new Uint8Array(15));
|
|
575
|
+
msg.writeUInt8(options & 255);
|
|
576
|
+
msg.writeInt32BE(pathBytes.length);
|
|
577
|
+
msg.writeBytes(pathBytes);
|
|
578
|
+
return msg.getBuffer();
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* kXR_mkdir request (24 B header + path string).
|
|
582
|
+
*
|
|
583
|
+
* Body layout (16 B):
|
|
584
|
+
* mode[2] + reserved[14]
|
|
585
|
+
*/
|
|
586
|
+
function buildMkdirRequest(streamId, path, mode = 493) {
|
|
587
|
+
const pathBytes = strToBytes(path);
|
|
588
|
+
const msg = new Message(24 + pathBytes.length);
|
|
589
|
+
msg.writeBytes(streamIdToBytes(streamId));
|
|
590
|
+
msg.writeInt16BE(RequestId.Mkdir);
|
|
591
|
+
msg.writeInt16BE(mode & 65535);
|
|
592
|
+
msg.writeBytes(/* @__PURE__ */ new Uint8Array(14));
|
|
593
|
+
msg.writeInt32BE(pathBytes.length);
|
|
594
|
+
msg.writeBytes(pathBytes);
|
|
595
|
+
return msg.getBuffer();
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* kXR_rmdir request (24 B header + path string).
|
|
599
|
+
*
|
|
600
|
+
* Body layout (16 B):
|
|
601
|
+
* reserved[16]
|
|
602
|
+
*/
|
|
603
|
+
function buildRmdirRequest(streamId, path) {
|
|
604
|
+
const pathBytes = strToBytes(path);
|
|
605
|
+
const msg = new Message(24 + pathBytes.length);
|
|
606
|
+
msg.writeBytes(streamIdToBytes(streamId));
|
|
607
|
+
msg.writeInt16BE(RequestId.Rmdir);
|
|
608
|
+
msg.writeBytes(/* @__PURE__ */ new Uint8Array(16));
|
|
609
|
+
msg.writeInt32BE(pathBytes.length);
|
|
610
|
+
msg.writeBytes(pathBytes);
|
|
611
|
+
return msg.getBuffer();
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* kXR_rm request (24 B header + path string).
|
|
615
|
+
*
|
|
616
|
+
* Body layout (16 B):
|
|
617
|
+
* reserved[16]
|
|
618
|
+
*/
|
|
619
|
+
function buildRmRequest(streamId, path) {
|
|
620
|
+
const pathBytes = strToBytes(path);
|
|
621
|
+
const msg = new Message(24 + pathBytes.length);
|
|
622
|
+
msg.writeBytes(streamIdToBytes(streamId));
|
|
623
|
+
msg.writeInt16BE(RequestId.Rm);
|
|
624
|
+
msg.writeBytes(/* @__PURE__ */ new Uint8Array(16));
|
|
625
|
+
msg.writeInt32BE(pathBytes.length);
|
|
626
|
+
msg.writeBytes(pathBytes);
|
|
627
|
+
return msg.getBuffer();
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* kXR_mv request (24 B header + source + target).
|
|
631
|
+
*
|
|
632
|
+
* Body layout (16 B):
|
|
633
|
+
* reserved[14] + arg1len[2]
|
|
634
|
+
* Extra data:
|
|
635
|
+
* source[arg1len] + target[dlen - arg1len]
|
|
636
|
+
*/
|
|
637
|
+
function buildMvRequest(streamId, source, target) {
|
|
638
|
+
const srcBytes = strToBytes(source);
|
|
639
|
+
const tgtBytes = strToBytes(target);
|
|
640
|
+
const msg = new Message(24 + srcBytes.length + tgtBytes.length);
|
|
641
|
+
msg.writeBytes(streamIdToBytes(streamId));
|
|
642
|
+
msg.writeInt16BE(RequestId.Mv);
|
|
643
|
+
msg.writeBytes(/* @__PURE__ */ new Uint8Array(14));
|
|
644
|
+
msg.writeInt16BE(srcBytes.length & 65535);
|
|
645
|
+
msg.writeInt32BE(srcBytes.length + tgtBytes.length);
|
|
646
|
+
msg.writeBytes(srcBytes);
|
|
647
|
+
msg.writeBytes(tgtBytes);
|
|
648
|
+
return msg.getBuffer();
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* kXR_endsess request (24 B header, no extra data).
|
|
652
|
+
*
|
|
653
|
+
* Body layout (16 B):
|
|
654
|
+
* sessid[16]
|
|
655
|
+
*/
|
|
656
|
+
function buildEndsessRequest(streamId, sessid) {
|
|
657
|
+
const msg = new Message(24);
|
|
658
|
+
msg.writeBytes(streamIdToBytes(streamId));
|
|
659
|
+
msg.writeInt16BE(RequestId.Endsess);
|
|
660
|
+
msg.writeBytes(sessid);
|
|
661
|
+
msg.writeInt32BE(0);
|
|
662
|
+
return msg.getBuffer();
|
|
663
|
+
}
|
|
664
|
+
//#endregion
|
|
665
|
+
//#region src/protocol/codec.ts
|
|
666
|
+
/**
|
|
667
|
+
* Big-endian encoding / decoding utilities for the XRootD protocol.
|
|
668
|
+
*
|
|
669
|
+
* Every multi-byte field in XRootD is transmitted in network byte order
|
|
670
|
+
* (big-endian). The helpers below keep offset tracking explicit so
|
|
671
|
+
* callers never forget to advance.
|
|
672
|
+
*/
|
|
673
|
+
/** Write `value` as uint16 BE at `offset`, return new offset. */
|
|
674
|
+
function put16(buf, offset, value) {
|
|
675
|
+
buf.writeUInt16BE(value, offset);
|
|
676
|
+
return offset + 2;
|
|
677
|
+
}
|
|
678
|
+
/** Read uint16 BE at `offset`, return `[value, newOffset]`. */
|
|
679
|
+
function get16(buf, offset) {
|
|
680
|
+
return [buf.readUInt16BE(offset), offset + 2];
|
|
681
|
+
}
|
|
682
|
+
/** Write `value` as uint32 BE at `offset`, return new offset. */
|
|
683
|
+
function put32(buf, offset, value) {
|
|
684
|
+
buf.writeUInt32BE(value, offset);
|
|
685
|
+
return offset + 4;
|
|
686
|
+
}
|
|
687
|
+
/** Read uint32 BE at `offset`, return `[value, newOffset]`. */
|
|
688
|
+
function get32(buf, offset) {
|
|
689
|
+
return [buf.readUInt32BE(offset), offset + 4];
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Write `str` bytes into `buf` starting at `offset`, padded with zeros
|
|
693
|
+
* up to `maxLen`. Returns the new offset (always `offset + maxLen`).
|
|
694
|
+
*/
|
|
695
|
+
function putString(buf, offset, str, maxLen) {
|
|
696
|
+
const bytes = Buffer.from(str, "utf8");
|
|
697
|
+
const len = Math.min(bytes.length, maxLen);
|
|
698
|
+
bytes.copy(buf, offset, 0, len);
|
|
699
|
+
buf.fill(0, offset + len, offset + maxLen);
|
|
700
|
+
return offset + maxLen;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Read a fixed-length string from `buf` at `offset` for `length` bytes.
|
|
704
|
+
* Trailing NUL bytes are stripped.
|
|
705
|
+
*/
|
|
706
|
+
function getString(buf, offset, length) {
|
|
707
|
+
return [buf.toString("utf8", offset, offset + length).replace(/\0+$/, ""), offset + length];
|
|
708
|
+
}
|
|
709
|
+
/** Copy `data` into `buf` at `offset`, return new offset. */
|
|
710
|
+
function putBytes(buf, offset, data) {
|
|
711
|
+
Buffer.from(data).copy(buf, offset);
|
|
712
|
+
return offset + data.length;
|
|
713
|
+
}
|
|
714
|
+
/** Slice `length` bytes from `buf` at `offset`, return `[slice, newOffset]`. */
|
|
715
|
+
function getBytes(buf, offset, length) {
|
|
716
|
+
return [buf.subarray(offset, offset + length), offset + length];
|
|
717
|
+
}
|
|
718
|
+
//#endregion
|
|
719
|
+
//#region src/protocol/parsers.ts
|
|
720
|
+
/**
|
|
721
|
+
* Response parsers for XRootD protocol messages.
|
|
722
|
+
* Each function parses a response body Buffer into a typed object.
|
|
723
|
+
*/
|
|
724
|
+
/**
|
|
725
|
+
* Parse kXR_protocol OK response body.
|
|
726
|
+
*
|
|
727
|
+
* Body layout:
|
|
728
|
+
* pval[4] + flags[4] + secReqs (remaining bytes, NUL-terminated) +
|
|
729
|
+
* bifReqs (remaining bytes, NUL-terminated)
|
|
730
|
+
*/
|
|
731
|
+
function parseProtocolResponse(body) {
|
|
732
|
+
let off = 0;
|
|
733
|
+
const [pval, o1] = get32(body, off);
|
|
734
|
+
off = o1;
|
|
735
|
+
const [flags, o2] = get32(body, off);
|
|
736
|
+
off = o2;
|
|
737
|
+
let secReqs;
|
|
738
|
+
let bifReqs;
|
|
739
|
+
if (off < body.length) {
|
|
740
|
+
const [s, o3] = getString(body, off, body.length - off);
|
|
741
|
+
off = o3;
|
|
742
|
+
if (s) secReqs = s;
|
|
743
|
+
}
|
|
744
|
+
if (off < body.length) {
|
|
745
|
+
const [b] = getString(body, off, body.length - off);
|
|
746
|
+
if (b) bifReqs = b;
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
pval,
|
|
750
|
+
flags,
|
|
751
|
+
secReqs,
|
|
752
|
+
bifReqs
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Parse kXR_login OK response body.
|
|
757
|
+
*
|
|
758
|
+
* Body layout:
|
|
759
|
+
* sessid[16] + optional secToken (remaining bytes)
|
|
760
|
+
*/
|
|
761
|
+
function parseLoginResponse(body) {
|
|
762
|
+
const [sessid] = getBytes(body, 0, 16);
|
|
763
|
+
let secToken;
|
|
764
|
+
if (body.length > 16) {
|
|
765
|
+
const [tok] = getBytes(body, 16, body.length - 16);
|
|
766
|
+
secToken = tok;
|
|
767
|
+
}
|
|
768
|
+
return {
|
|
769
|
+
sessid: new Uint8Array(sessid),
|
|
770
|
+
secToken,
|
|
771
|
+
needsAuth: body.length > 16
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Parse kXR_open OK response body.
|
|
776
|
+
*
|
|
777
|
+
* Body layout:
|
|
778
|
+
* fhandle[4] + cpsize[4] + cptype[4] + optional stat
|
|
779
|
+
*/
|
|
780
|
+
function parseOpenResponse(body) {
|
|
781
|
+
const [fhandle] = getBytes(body, 0, 4);
|
|
782
|
+
let cpsize = 0;
|
|
783
|
+
let cptype = "";
|
|
784
|
+
if (body.length >= 8) {
|
|
785
|
+
const [v] = get32(body, 4);
|
|
786
|
+
cpsize = v;
|
|
787
|
+
}
|
|
788
|
+
if (body.length >= 12) {
|
|
789
|
+
const [raw] = getBytes(body, 8, 4);
|
|
790
|
+
cptype = Buffer.from(raw).toString("utf8").replace(/\0+$/, "");
|
|
791
|
+
}
|
|
792
|
+
return {
|
|
793
|
+
fhandle: new Uint8Array(fhandle),
|
|
794
|
+
cpsize,
|
|
795
|
+
cptype
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Parse kXR_error response body.
|
|
800
|
+
*
|
|
801
|
+
* Body layout:
|
|
802
|
+
* errnum[4] + errmsg[variable, NUL-terminated]
|
|
803
|
+
*/
|
|
804
|
+
function parseErrorResponse(body) {
|
|
805
|
+
const [errnum, off] = get32(body, 0);
|
|
806
|
+
const [errmsg] = getString(body, off, body.length - off);
|
|
807
|
+
return {
|
|
808
|
+
errnum,
|
|
809
|
+
errmsg
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Parse kXR_redirect response body.
|
|
814
|
+
*
|
|
815
|
+
* Body layout:
|
|
816
|
+
* port[4] + host[variable, NUL-terminated]
|
|
817
|
+
*/
|
|
818
|
+
function parseRedirectResponse(body) {
|
|
819
|
+
const [port, off] = get32(body, 0);
|
|
820
|
+
const [host] = getString(body, off, body.length - off);
|
|
821
|
+
return {
|
|
822
|
+
port,
|
|
823
|
+
host
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Parse kXR_wait / kXR_waitresp response body.
|
|
828
|
+
*
|
|
829
|
+
* Body layout:
|
|
830
|
+
* seconds[4] + infomsg[variable, NUL-terminated]
|
|
831
|
+
*/
|
|
832
|
+
function parseWaitResponse(body) {
|
|
833
|
+
const [seconds, off] = get32(body, 0);
|
|
834
|
+
const [infomsg] = getString(body, off, body.length - off);
|
|
835
|
+
return {
|
|
836
|
+
seconds,
|
|
837
|
+
infomsg
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
const DSTAT_PREFIX = ".\n0 0 0 0";
|
|
841
|
+
/**
|
|
842
|
+
* Parse kXR_dirlist response body.
|
|
843
|
+
*
|
|
844
|
+
* Two possible formats depending on options:
|
|
845
|
+
* 1. Names-only (kXR_online, default): `name1\nname2\nname3\0` — newline-separated, last entry null-terminated
|
|
846
|
+
* 2. With stat info (kXR_dstat): `.\n0 0 0 0\nname1\n<statinfo1>\nname2\n<statinfo2>\n...\0`
|
|
847
|
+
* statinfo format: `<devid> <size> <flags> <mtime> <ctime> <atime> <mode> <owner> <group>`
|
|
848
|
+
*/
|
|
849
|
+
function parseDirlistResponse(body) {
|
|
850
|
+
const entries = [];
|
|
851
|
+
if (body.length === 0) return { entries };
|
|
852
|
+
const text = body.toString("utf8").replace(/\0$/, "");
|
|
853
|
+
if (text.startsWith(DSTAT_PREFIX)) {
|
|
854
|
+
const lines = text.slice(10).split("\n").filter((l) => l.length > 0);
|
|
855
|
+
for (let i = 0; i < lines.length - 1; i += 2) {
|
|
856
|
+
const name = lines[i];
|
|
857
|
+
const statFields = lines[i + 1]?.split(/\s+/);
|
|
858
|
+
if (statFields && statFields.length >= 4) entries.push({
|
|
859
|
+
name,
|
|
860
|
+
size: parseInt(statFields[1], 10) || 0,
|
|
861
|
+
flags: parseInt(statFields[2], 10) || 0,
|
|
862
|
+
mtime: parseInt(statFields[3], 10) || 0
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
} else {
|
|
866
|
+
const names = text.split("\n");
|
|
867
|
+
for (const name of names) {
|
|
868
|
+
const trimmed = name.trim();
|
|
869
|
+
if (trimmed.length > 0) entries.push({
|
|
870
|
+
name: trimmed,
|
|
871
|
+
size: 0,
|
|
872
|
+
flags: 0,
|
|
873
|
+
mtime: 0
|
|
874
|
+
});
|
|
875
|
+
}
|
|
627
876
|
}
|
|
628
|
-
|
|
629
|
-
|
|
877
|
+
return { entries };
|
|
878
|
+
}
|
|
879
|
+
//#endregion
|
|
880
|
+
//#region src/api/errors.ts
|
|
881
|
+
const codeMessages = {
|
|
882
|
+
[ServerError.ArgInvalid]: "Invalid argument",
|
|
883
|
+
[ServerError.ArgMissing]: "Missing argument",
|
|
884
|
+
[ServerError.ArgTooLong]: "Argument too long",
|
|
885
|
+
[ServerError.FileLocked]: "File locked",
|
|
886
|
+
[ServerError.FileNotOpen]: "File not open",
|
|
887
|
+
[ServerError.FSError]: "File system error",
|
|
888
|
+
[ServerError.InvalidRequest]: "Invalid request",
|
|
889
|
+
[ServerError.IOError]: "I/O error",
|
|
890
|
+
[ServerError.NoMemory]: "No memory",
|
|
891
|
+
[ServerError.NoSpace]: "No space",
|
|
892
|
+
[ServerError.NotAuthorized]: "Not authorized",
|
|
893
|
+
[ServerError.NotFound]: "File not found",
|
|
894
|
+
[ServerError.ServerError]: "Server error",
|
|
895
|
+
[ServerError.Unsupported]: "Unsupported",
|
|
896
|
+
[ServerError.NoServer]: "No server",
|
|
897
|
+
[ServerError.NotFile]: "Not a file",
|
|
898
|
+
[ServerError.IsDirectory]: "Is a directory",
|
|
899
|
+
[ServerError.Cancelled]: "Operation cancelled",
|
|
900
|
+
[ServerError.ItExists]: "File already exists",
|
|
901
|
+
[ServerError.CheckSumErr]: "Checksum error",
|
|
902
|
+
[ServerError.InProgress]: "Operation in progress",
|
|
903
|
+
[ServerError.OverQuota]: "Over quota",
|
|
904
|
+
[ServerError.SigVerErr]: "Signature verification error",
|
|
905
|
+
[ServerError.DecryptErr]: "Decryption error",
|
|
906
|
+
[ServerError.Overloaded]: "Server overloaded",
|
|
907
|
+
[ServerError.FsReadOnly]: "File system read-only",
|
|
908
|
+
[ServerError.BadPayload]: "Bad payload",
|
|
909
|
+
[ServerError.AttrNotFound]: "Attribute not found",
|
|
910
|
+
[ServerError.TLSRequired]: "TLS required",
|
|
911
|
+
[ServerError.NoReplicas]: "No replicas",
|
|
912
|
+
[ServerError.AuthFailed]: "Authentication failed",
|
|
913
|
+
[ServerError.Impossible]: "Impossible",
|
|
914
|
+
[ServerError.Conflict]: "Conflict",
|
|
915
|
+
[ServerError.TooManyErrs]: "Too many errors",
|
|
916
|
+
[ServerError.ReqTimedOut]: "Request timed out",
|
|
917
|
+
[ServerError.TimerExpired]: "Timer expired",
|
|
918
|
+
[ClientError.Ok]: "OK",
|
|
919
|
+
[ClientError.InvalidArgs]: "Invalid arguments",
|
|
920
|
+
[ClientError.NotFound]: "Not found",
|
|
921
|
+
[ClientError.Permission]: "Permission denied",
|
|
922
|
+
[ClientError.Serialization]: "Serialization error",
|
|
923
|
+
[ClientError.CommandNotFound]: "Command not found",
|
|
924
|
+
[ClientError.HostNotFound]: "Host not found",
|
|
925
|
+
[ClientError.ServiceUnavail]: "Service unavailable",
|
|
926
|
+
[ClientError.InternalError]: "Internal error",
|
|
927
|
+
[ClientError.BadRequest]: "Bad request",
|
|
928
|
+
[ClientError.Timeout]: "Timeout",
|
|
929
|
+
[ClientError.InsufficientData]: "Insufficient data",
|
|
930
|
+
[ClientError.Uninitialized]: "Client not connected",
|
|
931
|
+
[ClientError.Disconnected]: "Disconnected",
|
|
932
|
+
[ClientError.Redirect]: "Redirect",
|
|
933
|
+
[ClientError.LossyRetry]: "Lossy retry",
|
|
934
|
+
[ClientError.TooManyRedirs]: "Too many redirects",
|
|
935
|
+
[ClientError.ChunkChecksumErr]: "Chunk checksum error",
|
|
936
|
+
[ClientError.UnexpectedResp]: "Unexpected response",
|
|
937
|
+
[ClientError.ClientSkipped]: "Client skipped",
|
|
938
|
+
[ClientError.Failed]: "Failed",
|
|
939
|
+
[ClientError.WinNetworkError]: "Windows network error"
|
|
940
|
+
};
|
|
941
|
+
var XRootDError = class XRootDError extends Error {
|
|
942
|
+
code;
|
|
943
|
+
errno;
|
|
944
|
+
constructor(code, message, errno) {
|
|
945
|
+
super(message ?? XRootDError.codeToMessage(code));
|
|
946
|
+
this.name = "XRootDError";
|
|
947
|
+
this.code = code;
|
|
948
|
+
this.errno = errno;
|
|
949
|
+
}
|
|
950
|
+
static codeToMessage(code) {
|
|
951
|
+
return codeMessages[code] ?? `Unknown error (${code})`;
|
|
630
952
|
}
|
|
631
|
-
|
|
632
|
-
|
|
953
|
+
};
|
|
954
|
+
//#endregion
|
|
955
|
+
//#region src/transport/multiplexer.ts
|
|
956
|
+
/**
|
|
957
|
+
* Layer 3 Multiplexer
|
|
958
|
+
*
|
|
959
|
+
* Maintains streamId → Promise mapping.
|
|
960
|
+
* Generates incremental stream IDs, stores pending requests in Map,
|
|
961
|
+
* resolves when Framer delivers matching response frames.
|
|
962
|
+
*/
|
|
963
|
+
var Multiplexer = class {
|
|
964
|
+
transport;
|
|
965
|
+
framer;
|
|
966
|
+
pending = /* @__PURE__ */ new Map();
|
|
967
|
+
nextStreamId = 0;
|
|
968
|
+
timeout = 3e4;
|
|
969
|
+
sweepTimer = null;
|
|
970
|
+
closed = false;
|
|
971
|
+
redirectCount = 0;
|
|
972
|
+
maxRedirects;
|
|
973
|
+
onRedirect;
|
|
974
|
+
constructor(transport, options) {
|
|
975
|
+
this.transport = transport;
|
|
976
|
+
this.framer = new Framer();
|
|
977
|
+
this.maxRedirects = options?.maxRedirects ?? 16;
|
|
978
|
+
this.onRedirect = options?.onRedirect;
|
|
979
|
+
this.sweepTimer = globalThis.setInterval(() => this.sweepTimeouts(), 1e3);
|
|
980
|
+
this.sweepTimer.unref();
|
|
981
|
+
this.transport.onData((chunk) => {
|
|
982
|
+
const frames = this.framer.feed(chunk);
|
|
983
|
+
for (const frame of frames) this.handleFrame(frame);
|
|
984
|
+
});
|
|
985
|
+
this.transport.onClose(() => {
|
|
986
|
+
this.rejectAll(/* @__PURE__ */ new Error("Connection closed"));
|
|
987
|
+
});
|
|
988
|
+
this.transport.onError((err) => {
|
|
989
|
+
this.rejectAll(err);
|
|
990
|
+
});
|
|
633
991
|
}
|
|
634
|
-
|
|
635
|
-
|
|
992
|
+
allocateStreamId() {
|
|
993
|
+
let sid = this.nextStreamId;
|
|
994
|
+
while (this.pending.has(sid)) {
|
|
995
|
+
sid = sid + 1 & 65535;
|
|
996
|
+
if (sid === this.nextStreamId) throw new XRootDError(ClientError.InternalError, "Max concurrent requests (65535) reached");
|
|
997
|
+
}
|
|
998
|
+
this.nextStreamId = sid + 1 & 65535;
|
|
999
|
+
return sid;
|
|
1000
|
+
}
|
|
1001
|
+
async request(requestId, body, data) {
|
|
1002
|
+
if (this.closed) throw new XRootDError(ClientError.InternalError, "Multiplexer is closed");
|
|
1003
|
+
const sid = this.allocateStreamId();
|
|
1004
|
+
const bodyBuf = Buffer.alloc(16);
|
|
1005
|
+
Buffer.from(body).copy(bodyBuf);
|
|
1006
|
+
const msg = new Message(24 + (data?.length ?? 0));
|
|
1007
|
+
msg.writeBytes(streamIdToBytes(sid));
|
|
1008
|
+
msg.writeInt16BE(requestId);
|
|
1009
|
+
msg.writeBytes(bodyBuf);
|
|
1010
|
+
msg.writeInt32BE(data?.length ?? 0);
|
|
1011
|
+
if (data && data.length > 0) msg.writeBytes(data);
|
|
1012
|
+
return new Promise((resolve, reject) => {
|
|
1013
|
+
this.pending.set(sid, {
|
|
1014
|
+
resolve,
|
|
1015
|
+
reject,
|
|
1016
|
+
expiresAt: Date.now() + this.timeout,
|
|
1017
|
+
requestId,
|
|
1018
|
+
body,
|
|
1019
|
+
data
|
|
1020
|
+
});
|
|
1021
|
+
this.transport.send(msg.getBuffer()).catch(reject);
|
|
1022
|
+
});
|
|
636
1023
|
}
|
|
637
|
-
|
|
638
|
-
|
|
1024
|
+
handleFrame(frame) {
|
|
1025
|
+
const sid = bytesToStreamId(frame.streamId);
|
|
1026
|
+
if (frame.status === ResponseStatus.Wait || frame.status === ResponseStatus.Waitresp) {
|
|
1027
|
+
this.handleWaitResponse(sid, frame);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (frame.status === ResponseStatus.Redirect) {
|
|
1031
|
+
this.handleRedirectResponse(sid, frame);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
const pending = this.pending.get(sid);
|
|
1035
|
+
if (!pending) return;
|
|
1036
|
+
this.pending.delete(sid);
|
|
1037
|
+
pending.resolve(frame);
|
|
1038
|
+
}
|
|
1039
|
+
handleWaitResponse(sid, frame) {
|
|
1040
|
+
const seconds = frame.body.readInt32BE(0);
|
|
1041
|
+
const pending = this.pending.get(sid);
|
|
1042
|
+
if (pending) {
|
|
1043
|
+
pending.expiresAt = Date.now() + seconds * 1e3 + this.timeout;
|
|
1044
|
+
globalThis.setTimeout(() => this.retryRequest(sid), seconds * 1e3);
|
|
1045
|
+
}
|
|
639
1046
|
}
|
|
640
|
-
|
|
641
|
-
|
|
1047
|
+
handleRedirectResponse(sid, frame) {
|
|
1048
|
+
const pending = this.pending.get(sid);
|
|
1049
|
+
if (!pending) return;
|
|
1050
|
+
if (this.redirectCount >= this.maxRedirects) {
|
|
1051
|
+
this.pending.delete(sid);
|
|
1052
|
+
pending.reject(/* @__PURE__ */ new Error(`Too many redirects (max ${this.maxRedirects})`));
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
this.redirectCount++;
|
|
1056
|
+
const { host, port } = parseRedirectResponse(frame.body);
|
|
1057
|
+
if (this.onRedirect) this.onRedirect(host, port).then(() => {
|
|
1058
|
+
this.retryRequest(sid);
|
|
1059
|
+
}).catch((err) => {
|
|
1060
|
+
this.pending.delete(sid);
|
|
1061
|
+
pending.reject(err);
|
|
1062
|
+
});
|
|
1063
|
+
else {
|
|
1064
|
+
this.pending.delete(sid);
|
|
1065
|
+
pending.reject(/* @__PURE__ */ new Error(`Redirect to ${host}:${port} but no onRedirect handler configured`));
|
|
1066
|
+
}
|
|
642
1067
|
}
|
|
643
|
-
|
|
644
|
-
|
|
1068
|
+
retryRequest(sid) {
|
|
1069
|
+
const pending = this.pending.get(sid);
|
|
1070
|
+
if (!pending) return;
|
|
1071
|
+
this.pending.delete(sid);
|
|
1072
|
+
this.request(pending.requestId, pending.body, pending.data).then(pending.resolve).catch(pending.reject);
|
|
1073
|
+
}
|
|
1074
|
+
sweepTimeouts() {
|
|
1075
|
+
const now = Date.now();
|
|
1076
|
+
for (const [sid, req] of this.pending.entries()) if (now > req.expiresAt) {
|
|
1077
|
+
this.pending.delete(sid);
|
|
1078
|
+
req.reject(/* @__PURE__ */ new Error(`Request timeout: streamid=${sid}`));
|
|
1079
|
+
}
|
|
645
1080
|
}
|
|
646
|
-
|
|
647
|
-
this.
|
|
1081
|
+
setTimeout(ms) {
|
|
1082
|
+
this.timeout = ms;
|
|
648
1083
|
}
|
|
649
|
-
|
|
650
|
-
|
|
1084
|
+
resetRedirectCount() {
|
|
1085
|
+
this.redirectCount = 0;
|
|
651
1086
|
}
|
|
652
|
-
|
|
653
|
-
this.
|
|
1087
|
+
getTransport() {
|
|
1088
|
+
return this.transport;
|
|
654
1089
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
1090
|
+
close() {
|
|
1091
|
+
if (this.closed) return;
|
|
1092
|
+
this.closed = true;
|
|
1093
|
+
if (this.sweepTimer) {
|
|
1094
|
+
globalThis.clearInterval(this.sweepTimer);
|
|
1095
|
+
this.sweepTimer = null;
|
|
1096
|
+
}
|
|
1097
|
+
this.rejectAll(/* @__PURE__ */ new Error("Multiplexer closed"));
|
|
660
1098
|
}
|
|
661
|
-
|
|
662
|
-
const [
|
|
663
|
-
this.
|
|
664
|
-
this._url.search = s ?? "";
|
|
1099
|
+
rejectAll(err) {
|
|
1100
|
+
for (const [, req] of this.pending.entries()) req.reject(err);
|
|
1101
|
+
this.pending.clear();
|
|
665
1102
|
}
|
|
666
|
-
|
|
667
|
-
|
|
1103
|
+
};
|
|
1104
|
+
//#endregion
|
|
1105
|
+
//#region src/utils/frame-reader.ts
|
|
1106
|
+
/**
|
|
1107
|
+
* Creates a persistent frame reader that registers ONE onData handler
|
|
1108
|
+
* before any data is sent, avoiding the race condition where the
|
|
1109
|
+
* Multiplexer's handler consumes frames before the handshake can read them.
|
|
1110
|
+
*
|
|
1111
|
+
* Uses a queue pattern: incoming frames are queued, and nextFrame()
|
|
1112
|
+
* resolves the next available frame (or waits for one to arrive).
|
|
1113
|
+
*/
|
|
1114
|
+
function createFrameReader(transport) {
|
|
1115
|
+
const framer = new Framer();
|
|
1116
|
+
const frameQueue = [];
|
|
1117
|
+
const waiters = [];
|
|
1118
|
+
const handler = (chunk) => {
|
|
1119
|
+
const frames = framer.feed(chunk);
|
|
1120
|
+
for (const frame of frames) if (waiters.length > 0) waiters.shift()(frame);
|
|
1121
|
+
else frameQueue.push(frame);
|
|
1122
|
+
};
|
|
1123
|
+
transport.onData(handler);
|
|
1124
|
+
return {
|
|
1125
|
+
nextFrame() {
|
|
1126
|
+
if (frameQueue.length > 0) return Promise.resolve(frameQueue.shift());
|
|
1127
|
+
return new Promise((resolve) => {
|
|
1128
|
+
waiters.push(resolve);
|
|
1129
|
+
});
|
|
1130
|
+
},
|
|
1131
|
+
close() {
|
|
1132
|
+
transport.removeDataHandler(handler);
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
//#endregion
|
|
1137
|
+
//#region src/session/handshake.ts
|
|
1138
|
+
/**
|
|
1139
|
+
* Perform XRootD connection handshake:
|
|
1140
|
+
* 1. Send ClientInitHandShake(20B) + kXR_protocol(24B) merged = 44 bytes
|
|
1141
|
+
* 2. Receive handshake response frame (16B: ServerResponseHeader 8B with
|
|
1142
|
+
* dlen/msglen shared with ServerInitHandShake)
|
|
1143
|
+
* 3. Receive kXR_ok + Protocol Response
|
|
1144
|
+
* 4. Send kXR_login request
|
|
1145
|
+
* 5. Receive kXR_ok + Login Response (sessid[16] + optional secToken)
|
|
1146
|
+
* 6. [Optional] kXR_auth multi-round authentication
|
|
1147
|
+
*/
|
|
1148
|
+
async function handshake(mux, url, options) {
|
|
1149
|
+
const username = options?.username ?? "";
|
|
1150
|
+
const pid = options?.pid ?? process.pid;
|
|
1151
|
+
const handshakeBuf = buildHandshakeAndProtocol(0, 9, 1);
|
|
1152
|
+
const transport = mux.getTransport();
|
|
1153
|
+
const reader = createFrameReader(transport);
|
|
1154
|
+
try {
|
|
1155
|
+
await transport.send(handshakeBuf);
|
|
1156
|
+
await reader.nextFrame();
|
|
1157
|
+
const protoFrame = await reader.nextFrame();
|
|
1158
|
+
if (protoFrame.status === ResponseStatus.Error) {
|
|
1159
|
+
const err = parseErrorResponse(protoFrame.body);
|
|
1160
|
+
throw new XRootDError(ClientError.InternalError, `Protocol handshake error: ${err.errmsg} (${err.errnum})`);
|
|
1161
|
+
}
|
|
1162
|
+
if (protoFrame.status !== ResponseStatus.Ok) throw new XRootDError(ClientError.InternalError, `Unexpected protocol response status: ${protoFrame.status}`);
|
|
1163
|
+
const protoResp = parseProtocolResponse(protoFrame.body);
|
|
1164
|
+
const loginBuf = buildLoginRequest(0, pid, username);
|
|
1165
|
+
await transport.send(loginBuf);
|
|
1166
|
+
const loginFrame = await reader.nextFrame();
|
|
1167
|
+
if (loginFrame.status === ResponseStatus.Error) {
|
|
1168
|
+
const err = parseErrorResponse(loginFrame.body);
|
|
1169
|
+
throw new XRootDError(ClientError.InternalError, `Login error: ${err.errmsg} (${err.errnum})`);
|
|
1170
|
+
}
|
|
1171
|
+
if (loginFrame.status === ResponseStatus.Redirect) {
|
|
1172
|
+
const redir = parseRedirectResponse(loginFrame.body);
|
|
1173
|
+
throw new XRootDError(ClientError.Redirect, `Login redirect to ${redir.host}:${redir.port}`);
|
|
1174
|
+
}
|
|
1175
|
+
if (loginFrame.status !== ResponseStatus.Ok) throw new XRootDError(ClientError.InternalError, `Unexpected login response status: ${loginFrame.status}`);
|
|
1176
|
+
return {
|
|
1177
|
+
sessid: parseLoginResponse(loginFrame.body).sessid,
|
|
1178
|
+
protocolVersion: protoResp.pval,
|
|
1179
|
+
secReqs: protoResp.secReqs,
|
|
1180
|
+
bifReqs: protoResp.bifReqs
|
|
1181
|
+
};
|
|
1182
|
+
} finally {
|
|
1183
|
+
reader.close();
|
|
668
1184
|
}
|
|
669
|
-
|
|
670
|
-
|
|
1185
|
+
}
|
|
1186
|
+
//#endregion
|
|
1187
|
+
//#region src/session/auth.ts
|
|
1188
|
+
const authProtocols = /* @__PURE__ */ new Map();
|
|
1189
|
+
function registerAuthProtocol(name, factory) {
|
|
1190
|
+
authProtocols.set(name, factory);
|
|
1191
|
+
}
|
|
1192
|
+
async function doAuthentication(mux, secReqs, params) {
|
|
1193
|
+
if (!secReqs || secReqs.trim().length === 0) return {
|
|
1194
|
+
prot: "",
|
|
1195
|
+
uid: 0,
|
|
1196
|
+
gid: 0
|
|
1197
|
+
};
|
|
1198
|
+
const supportedProtocols = secReqs.split(",").map((s) => s.trim());
|
|
1199
|
+
for (const protoName of supportedProtocols) {
|
|
1200
|
+
const factory = authProtocols.get(protoName);
|
|
1201
|
+
if (!factory) continue;
|
|
1202
|
+
return await executeAuth(mux, factory(), params);
|
|
1203
|
+
}
|
|
1204
|
+
throw new XRootDError(ServerError.AuthFailed, `No supported authentication protocol. Server requires: ${secReqs}`);
|
|
1205
|
+
}
|
|
1206
|
+
async function executeAuth(mux, protocol, params) {
|
|
1207
|
+
const creds = await protocol.getCredentials(params);
|
|
1208
|
+
const credType = getCredType(protocol.name);
|
|
1209
|
+
const body = /* @__PURE__ */ new Uint8Array(16);
|
|
1210
|
+
body[12] = credType >> 24 & 255;
|
|
1211
|
+
body[13] = credType >> 16 & 255;
|
|
1212
|
+
body[14] = credType >> 8 & 255;
|
|
1213
|
+
body[15] = credType & 255;
|
|
1214
|
+
let frame = await mux.request(RequestId.Auth, body, creds);
|
|
1215
|
+
while (frame.status === ResponseStatus.Authmore) {
|
|
1216
|
+
const challenge = frame.body;
|
|
1217
|
+
const response = await protocol.processChallenge(challenge);
|
|
1218
|
+
frame = await mux.request(RequestId.Auth, body, response);
|
|
1219
|
+
}
|
|
1220
|
+
if (frame.status !== ResponseStatus.Ok) {
|
|
1221
|
+
const { errnum, errmsg } = parseErrorResponse(frame.body);
|
|
1222
|
+
throw new XRootDError(errnum || ServerError.AuthFailed, errmsg || `Authentication failed with protocol: ${protocol.name}`);
|
|
1223
|
+
}
|
|
1224
|
+
return protocol.getEntity();
|
|
1225
|
+
}
|
|
1226
|
+
function getCredType(name) {
|
|
1227
|
+
const credType = CRED_TYPE[name];
|
|
1228
|
+
if (credType === void 0) throw new XRootDError(ClientError.BadRequest, `Unknown auth protocol: ${name}`);
|
|
1229
|
+
return credType;
|
|
1230
|
+
}
|
|
1231
|
+
//#endregion
|
|
1232
|
+
//#region src/security/host.ts
|
|
1233
|
+
var HostAuth = class {
|
|
1234
|
+
name = "host";
|
|
1235
|
+
entity = {
|
|
1236
|
+
prot: "host",
|
|
1237
|
+
uid: 0,
|
|
1238
|
+
gid: 0
|
|
1239
|
+
};
|
|
1240
|
+
complete = false;
|
|
1241
|
+
async getCredentials(params) {
|
|
1242
|
+
const hostname = params.host || "unknown";
|
|
1243
|
+
return new TextEncoder().encode(hostname);
|
|
1244
|
+
}
|
|
1245
|
+
async processChallenge(_challenge) {
|
|
1246
|
+
this.complete = true;
|
|
1247
|
+
return /* @__PURE__ */ new Uint8Array(0);
|
|
1248
|
+
}
|
|
1249
|
+
isComplete() {
|
|
1250
|
+
return this.complete;
|
|
1251
|
+
}
|
|
1252
|
+
getEntity() {
|
|
1253
|
+
return this.entity;
|
|
671
1254
|
}
|
|
672
|
-
|
|
673
|
-
|
|
1255
|
+
};
|
|
1256
|
+
//#endregion
|
|
1257
|
+
//#region src/api/types.ts
|
|
1258
|
+
const StatFlags = {
|
|
1259
|
+
XBitSet: 1,
|
|
1260
|
+
IsDir: 2,
|
|
1261
|
+
Other: 4,
|
|
1262
|
+
Offline: 8,
|
|
1263
|
+
Readable: 16,
|
|
1264
|
+
Writable: 32,
|
|
1265
|
+
POSCPending: 64,
|
|
1266
|
+
BackUpExists: 128,
|
|
1267
|
+
CacheResp: 512
|
|
1268
|
+
};
|
|
1269
|
+
/**
|
|
1270
|
+
* Parse XRootD stat response string.
|
|
1271
|
+
*
|
|
1272
|
+
* Format: "<id> <size> <flags> <mtime> <ctime> <atime> <mode> <owner> <group>"
|
|
1273
|
+
* - id: opaque 64-bit device id (string to avoid precision loss)
|
|
1274
|
+
* - size: uint64 file size (bigint)
|
|
1275
|
+
* - flags: XRootD flags bitmask (StatFlags)
|
|
1276
|
+
* - mtime: modification time (epoch seconds)
|
|
1277
|
+
* - ctime: change time (epoch seconds)
|
|
1278
|
+
* - atime: access time (epoch seconds)
|
|
1279
|
+
* - mode: POSIX mode (octal string, e.g. "100644")
|
|
1280
|
+
* - owner: file owner
|
|
1281
|
+
* - group: file group
|
|
1282
|
+
*/
|
|
1283
|
+
function createStatInfo(data) {
|
|
1284
|
+
const parts = data.trim().split(/\s+/);
|
|
1285
|
+
const id = parts[0] ?? "0";
|
|
1286
|
+
const size = BigInt(parts[1] ?? "0");
|
|
1287
|
+
const serverFlags = parseInt(parts[2] ?? "0", 10) || 0;
|
|
1288
|
+
const mtime = parseInt(parts[3] ?? "0", 10) || 0;
|
|
1289
|
+
const ctime = parseInt(parts[4] ?? "0", 10) || 0;
|
|
1290
|
+
const atime = parseInt(parts[5] ?? "0", 10) || 0;
|
|
1291
|
+
const modeStr = parts[6] ?? "0";
|
|
1292
|
+
const mode = parseInt(modeStr, 8) || 0;
|
|
1293
|
+
return {
|
|
1294
|
+
id,
|
|
1295
|
+
size,
|
|
1296
|
+
flags: serverFlags,
|
|
1297
|
+
mtime,
|
|
1298
|
+
ctime,
|
|
1299
|
+
atime,
|
|
1300
|
+
mode,
|
|
1301
|
+
owner: parts[7] ?? "",
|
|
1302
|
+
group: parts[8] ?? "",
|
|
1303
|
+
get isDirectory() {
|
|
1304
|
+
return (mode & S_IFDIR) !== 0;
|
|
1305
|
+
},
|
|
1306
|
+
get isLink() {
|
|
1307
|
+
return (mode & S_IFLNK) === S_IFLNK;
|
|
1308
|
+
},
|
|
1309
|
+
get isOffline() {
|
|
1310
|
+
return (serverFlags & StatFlags.Offline) !== 0;
|
|
1311
|
+
},
|
|
1312
|
+
get isCached() {
|
|
1313
|
+
return (serverFlags & StatFlags.CacheResp) !== 0;
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
//#endregion
|
|
1318
|
+
//#region src/utils/request.ts
|
|
1319
|
+
/** Extract the 16-byte request body. */
|
|
1320
|
+
function extractBody(buf) {
|
|
1321
|
+
return new Uint8Array(buf.subarray(4, 20));
|
|
1322
|
+
}
|
|
1323
|
+
/** Extract extra data based on dlen. */
|
|
1324
|
+
function extractExtraData(buf) {
|
|
1325
|
+
const dlen = extractDataLength(buf);
|
|
1326
|
+
if (dlen === 0) return void 0;
|
|
1327
|
+
return new Uint8Array(buf.subarray(24, 24 + dlen));
|
|
1328
|
+
}
|
|
1329
|
+
/** Extract the request ID (uint16 BE). */
|
|
1330
|
+
function extractRequestId(buf) {
|
|
1331
|
+
return buf.readUInt16BE(2);
|
|
1332
|
+
}
|
|
1333
|
+
/** Extract the data length (uint32 BE). */
|
|
1334
|
+
function extractDataLength(buf) {
|
|
1335
|
+
return buf.readUInt32BE(20);
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Send a request through the multiplexer, extracting fields from the buffer.
|
|
1339
|
+
* This is a convenience wrapper used by File and FileSystem classes.
|
|
1340
|
+
*/
|
|
1341
|
+
async function sendRequest(mux, buf, data) {
|
|
1342
|
+
const requestId = extractRequestId(buf);
|
|
1343
|
+
const body = extractBody(buf);
|
|
1344
|
+
const dlen = extractDataLength(buf);
|
|
1345
|
+
const extraData = data ?? (dlen > 0 ? new Uint8Array(buf.subarray(24, 24 + dlen)) : void 0);
|
|
1346
|
+
return mux.request(requestId, body, extraData);
|
|
1347
|
+
}
|
|
1348
|
+
//#endregion
|
|
1349
|
+
//#region src/api/file.ts
|
|
1350
|
+
var File = class {
|
|
1351
|
+
mux;
|
|
1352
|
+
session;
|
|
1353
|
+
fhandle = null;
|
|
1354
|
+
_isOpen = false;
|
|
1355
|
+
constructor(mux, session) {
|
|
1356
|
+
this.mux = mux;
|
|
1357
|
+
this.session = session;
|
|
1358
|
+
}
|
|
1359
|
+
get isOpen() {
|
|
1360
|
+
return this._isOpen;
|
|
1361
|
+
}
|
|
1362
|
+
async open(path, options) {
|
|
1363
|
+
if (this._isOpen) throw new XRootDError(ServerError.FileNotOpen, "File is already open");
|
|
1364
|
+
const buf = buildOpenRequest(0, path, options?.flags ?? OpenFlags.Read, options?.mode ?? 0);
|
|
1365
|
+
const frame = await sendRequest(this.mux, buf, Buffer.from(path));
|
|
1366
|
+
if (frame.status === ResponseStatus.Error) {
|
|
1367
|
+
const { errnum, errmsg } = parseErrorResponse(frame.body);
|
|
1368
|
+
throw new XRootDError(errnum, errmsg);
|
|
1369
|
+
}
|
|
1370
|
+
if (frame.status === ResponseStatus.Ok) {
|
|
1371
|
+
const resp = parseOpenResponse(frame.body);
|
|
1372
|
+
this.fhandle = resp.fhandle;
|
|
1373
|
+
this._isOpen = true;
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
throw new XRootDError(ServerError.ServerError, `Unexpected open response status: ${frame.status}`);
|
|
674
1377
|
}
|
|
675
|
-
|
|
676
|
-
this.
|
|
1378
|
+
async read(offset, size) {
|
|
1379
|
+
if (!this._isOpen || !this.fhandle) throw new XRootDError(ServerError.FileNotOpen, "File is not open");
|
|
1380
|
+
const buf = buildReadRequest(0, this.fhandle, offset, size);
|
|
1381
|
+
const frame = await sendRequest(this.mux, buf);
|
|
1382
|
+
if (frame.status === ResponseStatus.Error) {
|
|
1383
|
+
const { errnum, errmsg } = parseErrorResponse(frame.body);
|
|
1384
|
+
throw new XRootDError(errnum, errmsg);
|
|
1385
|
+
}
|
|
1386
|
+
if (frame.status === ResponseStatus.Ok) return new Uint8Array(frame.body);
|
|
1387
|
+
throw new XRootDError(ServerError.ServerError, `Unexpected read response status: ${frame.status}`);
|
|
1388
|
+
}
|
|
1389
|
+
async write(offset, data) {
|
|
1390
|
+
if (!this._isOpen || !this.fhandle) throw new XRootDError(ServerError.FileNotOpen, "File is not open");
|
|
1391
|
+
const buf = buildWriteRequest(0, this.fhandle, offset, data);
|
|
1392
|
+
const frame = await sendRequest(this.mux, buf, data);
|
|
1393
|
+
if (frame.status === ResponseStatus.Error) {
|
|
1394
|
+
const { errnum, errmsg } = parseErrorResponse(frame.body);
|
|
1395
|
+
throw new XRootDError(errnum, errmsg);
|
|
1396
|
+
}
|
|
1397
|
+
if (frame.status === ResponseStatus.Ok) return frame.dlen;
|
|
1398
|
+
throw new XRootDError(ServerError.ServerError, `Unexpected write response status: ${frame.status}`);
|
|
677
1399
|
}
|
|
678
|
-
|
|
679
|
-
|
|
1400
|
+
async close() {
|
|
1401
|
+
if (!this._isOpen || !this.fhandle) return;
|
|
1402
|
+
const buf = buildCloseRequest(0, this.fhandle);
|
|
1403
|
+
const frame = await sendRequest(this.mux, buf);
|
|
1404
|
+
this.fhandle = null;
|
|
1405
|
+
this._isOpen = false;
|
|
1406
|
+
if (frame.status === ResponseStatus.Error) {
|
|
1407
|
+
const { errnum, errmsg } = parseErrorResponse(frame.body);
|
|
1408
|
+
throw new XRootDError(errnum, errmsg);
|
|
1409
|
+
}
|
|
680
1410
|
}
|
|
681
|
-
|
|
682
|
-
this.
|
|
1411
|
+
async stat() {
|
|
1412
|
+
if (!this._isOpen || !this.fhandle) throw new XRootDError(ServerError.FileNotOpen, "File is not open");
|
|
1413
|
+
const buf = buildStatRequest(0, "", this.fhandle);
|
|
1414
|
+
const frame = await sendRequest(this.mux, buf);
|
|
1415
|
+
if (frame.status === ResponseStatus.Error) {
|
|
1416
|
+
const { errnum, errmsg } = parseErrorResponse(frame.body);
|
|
1417
|
+
throw new XRootDError(errnum, errmsg);
|
|
1418
|
+
}
|
|
1419
|
+
if (frame.status === ResponseStatus.Ok) return parseStatInfo(frame.body);
|
|
1420
|
+
throw new XRootDError(ServerError.ServerError, `Unexpected stat response status: ${frame.status}`);
|
|
683
1421
|
}
|
|
684
|
-
|
|
685
|
-
|
|
1422
|
+
async sync() {
|
|
1423
|
+
if (!this._isOpen || !this.fhandle) throw new XRootDError(ServerError.FileNotOpen, "File is not open");
|
|
1424
|
+
const buf = buildSyncRequest(0, this.fhandle);
|
|
1425
|
+
const frame = await sendRequest(this.mux, buf);
|
|
1426
|
+
if (frame.status === ResponseStatus.Error) {
|
|
1427
|
+
const { errnum, errmsg } = parseErrorResponse(frame.body);
|
|
1428
|
+
throw new XRootDError(errnum, errmsg);
|
|
1429
|
+
}
|
|
686
1430
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
1431
|
+
async truncate(size) {
|
|
1432
|
+
if (!this._isOpen || !this.fhandle) throw new XRootDError(ServerError.FileNotOpen, "File is not open");
|
|
1433
|
+
const buf = buildTruncateRequest(0, this.fhandle, size);
|
|
1434
|
+
const frame = await sendRequest(this.mux, buf);
|
|
1435
|
+
if (frame.status === ResponseStatus.Error) {
|
|
1436
|
+
const { errnum, errmsg } = parseErrorResponse(frame.body);
|
|
1437
|
+
throw new XRootDError(errnum, errmsg);
|
|
1438
|
+
}
|
|
691
1439
|
}
|
|
692
|
-
|
|
693
|
-
|
|
1440
|
+
};
|
|
1441
|
+
function parseStatInfo(body) {
|
|
1442
|
+
return createStatInfo(body.toString("utf-8"));
|
|
1443
|
+
}
|
|
1444
|
+
//#endregion
|
|
1445
|
+
//#region src/api/filesystem.ts
|
|
1446
|
+
var FileSystem = class {
|
|
1447
|
+
mux;
|
|
1448
|
+
constructor(mux) {
|
|
1449
|
+
this.mux = mux;
|
|
1450
|
+
}
|
|
1451
|
+
async stat(path) {
|
|
1452
|
+
const req = buildStatRequest(0, path);
|
|
1453
|
+
const frame = await this.mux.request(RequestId.Stat, extractBody(req), extractExtraData(req));
|
|
1454
|
+
this.handleError(frame);
|
|
1455
|
+
return createStatInfo(frame.body.toString("utf8"));
|
|
1456
|
+
}
|
|
1457
|
+
async readdir(path) {
|
|
1458
|
+
const req = buildDirlistRequest(0, path);
|
|
1459
|
+
const frame = await this.mux.request(RequestId.Dirlist, extractBody(req), extractExtraData(req));
|
|
1460
|
+
this.handleError(frame);
|
|
1461
|
+
const { entries } = parseDirlistResponse(frame.body);
|
|
1462
|
+
return {
|
|
1463
|
+
name: path,
|
|
1464
|
+
entries
|
|
1465
|
+
};
|
|
694
1466
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
1467
|
+
async mkdir(path, mode = 493) {
|
|
1468
|
+
const req = buildMkdirRequest(0, path, mode);
|
|
1469
|
+
const frame = await this.mux.request(RequestId.Mkdir, extractBody(req), extractExtraData(req));
|
|
1470
|
+
this.handleError(frame);
|
|
1471
|
+
}
|
|
1472
|
+
async rmdir(path) {
|
|
1473
|
+
const req = buildRmdirRequest(0, path);
|
|
1474
|
+
const frame = await this.mux.request(RequestId.Rmdir, extractBody(req), extractExtraData(req));
|
|
1475
|
+
this.handleError(frame);
|
|
1476
|
+
}
|
|
1477
|
+
async rm(path) {
|
|
1478
|
+
const req = buildRmRequest(0, path);
|
|
1479
|
+
const frame = await this.mux.request(RequestId.Rm, extractBody(req), extractExtraData(req));
|
|
1480
|
+
this.handleError(frame);
|
|
1481
|
+
}
|
|
1482
|
+
async mv(source, target) {
|
|
1483
|
+
const req = buildMvRequest(0, source, target);
|
|
1484
|
+
const frame = await this.mux.request(RequestId.Mv, extractBody(req), extractExtraData(req));
|
|
1485
|
+
this.handleError(frame);
|
|
1486
|
+
}
|
|
1487
|
+
handleError(frame) {
|
|
1488
|
+
if (frame.status === ResponseStatus.Error) {
|
|
1489
|
+
const { errnum, errmsg } = parseErrorResponse(frame.body);
|
|
1490
|
+
throw new XRootDError(errnum, errmsg);
|
|
700
1491
|
}
|
|
701
1492
|
}
|
|
702
1493
|
};
|
|
703
1494
|
//#endregion
|
|
704
|
-
//#region
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1495
|
+
//#region src/client.ts
|
|
1496
|
+
var XRootDClient = class {
|
|
1497
|
+
url;
|
|
1498
|
+
options;
|
|
1499
|
+
transport = null;
|
|
1500
|
+
mux = null;
|
|
1501
|
+
session = null;
|
|
1502
|
+
fs = null;
|
|
1503
|
+
constructor(url, options = {}) {
|
|
1504
|
+
this.url = XRootDUrl.parse(url);
|
|
1505
|
+
this.options = options;
|
|
1506
|
+
}
|
|
1507
|
+
async connect() {
|
|
1508
|
+
await this.doConnect(this.url);
|
|
1509
|
+
}
|
|
1510
|
+
async doConnect(url) {
|
|
1511
|
+
this.transport = new Transport();
|
|
1512
|
+
await this.transport.connect(url.host, url.port);
|
|
1513
|
+
this.mux = new Multiplexer(this.transport, {
|
|
1514
|
+
maxRedirects: this.options.maxRedirects ?? 16,
|
|
1515
|
+
onRedirect: (host, port) => this.handleRedirect(host, port)
|
|
1516
|
+
});
|
|
1517
|
+
if (this.options.timeout) this.mux.setTimeout(this.options.timeout);
|
|
1518
|
+
this.session = await handshake(this.mux, url, { username: this.options.credentials?.username });
|
|
1519
|
+
registerAuthProtocol("host", () => new HostAuth());
|
|
1520
|
+
if (this.session.secReqs && this.options.credentials) {
|
|
1521
|
+
const secEntity = await doAuthentication(this.mux, this.session.secReqs, {
|
|
1522
|
+
host: url.host,
|
|
1523
|
+
port: url.port,
|
|
1524
|
+
username: this.options.credentials.username,
|
|
1525
|
+
password: this.options.credentials.password,
|
|
1526
|
+
sessid: this.session.sessid
|
|
1527
|
+
});
|
|
1528
|
+
this.session.secEntity = secEntity;
|
|
725
1529
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}
|
|
737
|
-
/**
|
|
738
|
-
* 获取整数配置项。
|
|
739
|
-
*/
|
|
740
|
-
getInt(key) {
|
|
741
|
-
return nativeAddon.Env.GetInt(key) ?? void 0;
|
|
742
|
-
}
|
|
743
|
-
/**
|
|
744
|
-
* 获取字符串配置项。
|
|
745
|
-
*/
|
|
746
|
-
getString(key) {
|
|
747
|
-
if (key === "SecProtocol") return process.env.XrdSecPROTOCOL ?? process.env.XRD_SECPROTOCOL ?? void 0;
|
|
748
|
-
return nativeAddon.Env.GetString(key) ?? void 0;
|
|
749
|
-
}
|
|
750
|
-
/**
|
|
751
|
-
* 批量安全地设置 XRootD 底层参数
|
|
752
|
-
*/
|
|
753
|
-
configure(config) {
|
|
754
|
-
for (const [key, value] of Object.entries(config)) {
|
|
755
|
-
if (value === void 0 || value === null) continue;
|
|
756
|
-
let success = true;
|
|
757
|
-
if (key === "SecProtocol") {
|
|
758
|
-
process.env.XrdSecPROTOCOL = String(value);
|
|
759
|
-
process.env.XRD_SECPROTOCOL = String(value);
|
|
760
|
-
continue;
|
|
761
|
-
}
|
|
762
|
-
if (typeof value === "number" || typeof value === "bigint") {
|
|
763
|
-
checkIntRange(key, value);
|
|
764
|
-
success = nativeAddon.Env.PutInt(key, Number(value));
|
|
765
|
-
} else if (typeof value === "string") success = nativeAddon.Env.PutString(key, value);
|
|
766
|
-
else if (typeof value === "boolean") success = nativeAddon.Env.PutString(key, value ? "true" : "false");
|
|
767
|
-
else {
|
|
768
|
-
console.warn(`[xrootd] Unhandled type ${typeof value} for key ${key}.`);
|
|
769
|
-
success = nativeAddon.Env.PutString(key, value.toString());
|
|
770
|
-
}
|
|
771
|
-
if (!success) console.warn(`[xrootd] Warning: Failed to set configuration "${key}"=${value}. It might have been overridden by a system environment variable.`);
|
|
1530
|
+
this.fs = new FileSystem(this.mux);
|
|
1531
|
+
}
|
|
1532
|
+
async handleRedirect(host, port) {
|
|
1533
|
+
if (this.session && this.mux) try {
|
|
1534
|
+
const endsessBody = buildEndsessRequest(0, this.session.sessid);
|
|
1535
|
+
await this.mux.request(RequestId.Endsess, new Uint8Array(endsessBody));
|
|
1536
|
+
} catch {}
|
|
1537
|
+
if (this.mux) {
|
|
1538
|
+
this.mux.close();
|
|
1539
|
+
this.mux = null;
|
|
772
1540
|
}
|
|
1541
|
+
if (this.transport) {
|
|
1542
|
+
await this.transport.close();
|
|
1543
|
+
this.transport = null;
|
|
1544
|
+
}
|
|
1545
|
+
const newUrl = XRootDUrl.parse(`root://${host}:${port}`);
|
|
1546
|
+
await this.doConnect(newUrl);
|
|
1547
|
+
}
|
|
1548
|
+
async open(path, options) {
|
|
1549
|
+
this.ensureConnected();
|
|
1550
|
+
const file = new File(this.mux, this.session);
|
|
1551
|
+
await file.open(path, options);
|
|
1552
|
+
return file;
|
|
1553
|
+
}
|
|
1554
|
+
async stat(path) {
|
|
1555
|
+
this.ensureConnected();
|
|
1556
|
+
const file = new File(this.mux, this.session);
|
|
1557
|
+
await file.open(path, { flags: OpenFlags.Read });
|
|
1558
|
+
try {
|
|
1559
|
+
return await file.stat();
|
|
1560
|
+
} finally {
|
|
1561
|
+
await file.close();
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
async statFilesystem(path) {
|
|
1565
|
+
this.ensureFileSystem();
|
|
1566
|
+
return this.fs.stat(path);
|
|
1567
|
+
}
|
|
1568
|
+
async readdir(path) {
|
|
1569
|
+
this.ensureFileSystem();
|
|
1570
|
+
return this.fs.readdir(path);
|
|
1571
|
+
}
|
|
1572
|
+
async mkdir(path, mode) {
|
|
1573
|
+
this.ensureFileSystem();
|
|
1574
|
+
return this.fs.mkdir(path, mode);
|
|
1575
|
+
}
|
|
1576
|
+
async rmdir(path) {
|
|
1577
|
+
this.ensureFileSystem();
|
|
1578
|
+
return this.fs.rmdir(path);
|
|
1579
|
+
}
|
|
1580
|
+
async rm(path) {
|
|
1581
|
+
this.ensureFileSystem();
|
|
1582
|
+
return this.fs.rm(path);
|
|
1583
|
+
}
|
|
1584
|
+
async mv(source, target) {
|
|
1585
|
+
this.ensureFileSystem();
|
|
1586
|
+
return this.fs.mv(source, target);
|
|
1587
|
+
}
|
|
1588
|
+
async close() {
|
|
1589
|
+
if (this.mux) {
|
|
1590
|
+
this.mux.close();
|
|
1591
|
+
this.mux = null;
|
|
1592
|
+
}
|
|
1593
|
+
if (this.transport) {
|
|
1594
|
+
await this.transport.close();
|
|
1595
|
+
this.transport = null;
|
|
1596
|
+
}
|
|
1597
|
+
this.session = null;
|
|
1598
|
+
this.fs = null;
|
|
1599
|
+
}
|
|
1600
|
+
get isConnected() {
|
|
1601
|
+
return this.session !== null;
|
|
1602
|
+
}
|
|
1603
|
+
get location() {
|
|
1604
|
+
return this.url.getLocation();
|
|
1605
|
+
}
|
|
1606
|
+
ensureConnected() {
|
|
1607
|
+
if (!this.mux || !this.session) throw new XRootDError(ClientError.Uninitialized, "Client not connected");
|
|
773
1608
|
}
|
|
774
|
-
|
|
775
|
-
if (
|
|
776
|
-
return nativeAddon.Env.GetInt(key) ?? nativeAddon.Env.GetString(key) ?? void 0;
|
|
1609
|
+
ensureFileSystem() {
|
|
1610
|
+
if (!this.fs) throw new XRootDError(ClientError.Uninitialized, "Client not connected");
|
|
777
1611
|
}
|
|
778
1612
|
};
|
|
779
|
-
const Env = new XRootDEnvironment();
|
|
780
|
-
Env.configure({
|
|
781
|
-
RequestTimeout: 30,
|
|
782
|
-
WorkerThreads: 4
|
|
783
|
-
});
|
|
784
1613
|
//#endregion
|
|
785
|
-
export {
|
|
1614
|
+
export { BODY_SIZE, CRED_TYPE, ClientError, DEFAULT_PORT, DirlistOptions, FHANDLE_SIZE, File, FileSystem, Framer, HANDSHAKE_FIFTH, HANDSHAKE_FIRST, HANDSHAKE_FOURTH, HANDSHAKE_SECOND, HANDSHAKE_THIRD, Message, Multiplexer, OpenFlags, PROTOCOL_VERSION, REQUEST_HDR_SIZE, REQUEST_OFFSET_BODY, REQUEST_OFFSET_DLEN, REQUEST_OFFSET_REQUEST_ID, REQUEST_OFFSET_STREAM_ID, RESPONSE_HDR_SIZE, RESPONSE_OFFSET_BODY, RESPONSE_OFFSET_DLEN, RESPONSE_OFFSET_STATUS, RESPONSE_OFFSET_STREAM_ID, RequestId, ResponseStatus, SESS_ID_SIZE, S_IFDIR, S_IFLNK, ServerError, StatFlags, Transport, XRootDClient, XRootDError, XRootDUrl, buildCloseRequest, buildHandshakeAndProtocol, buildLoginRequest, buildOpenRequest, buildReadRequest, buildStatRequest, buildWriteRequest, createStatInfo, get16, get32, getBytes, getString, handshake, kXR_ExpBind, kXR_ExpLogin, kXR_ableTLS, kXR_bifreqs, kXR_secreqs, kXR_wantTLS, parseErrorResponse, parseLoginResponse, parseOpenResponse, parseProtocolResponse, parseRedirectResponse, parseWaitResponse, put16, put32, putBytes, putString };
|
|
1615
|
+
|
|
1616
|
+
//# sourceMappingURL=index.mjs.map
|