@jupyterlite/services 0.1.0 → 0.7.0-rc.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.
Files changed (87) hide show
  1. package/lib/contents/drive.d.ts +278 -0
  2. package/lib/contents/drive.js +888 -0
  3. package/lib/contents/drive.js.map +1 -0
  4. package/lib/contents/drivecontents.d.ts +92 -0
  5. package/lib/contents/drivecontents.js +132 -0
  6. package/lib/contents/drivecontents.js.map +1 -0
  7. package/lib/contents/drivefs.d.ts +245 -0
  8. package/lib/contents/drivefs.js +481 -0
  9. package/lib/contents/drivefs.js.map +1 -0
  10. package/lib/contents/emscripten.d.ts +96 -0
  11. package/lib/contents/emscripten.js +10 -0
  12. package/lib/contents/emscripten.js.map +1 -0
  13. package/lib/contents/index.d.ts +5 -0
  14. package/lib/contents/index.js +8 -0
  15. package/lib/contents/index.js.map +1 -0
  16. package/lib/contents/tokens.d.ts +21 -0
  17. package/lib/contents/tokens.js +55 -0
  18. package/lib/contents/tokens.js.map +1 -0
  19. package/lib/index.d.ts +9 -0
  20. package/lib/index.js +12 -0
  21. package/lib/index.js.map +1 -0
  22. package/lib/kernel/base.d.ts +245 -0
  23. package/lib/kernel/base.js +457 -0
  24. package/lib/kernel/base.js.map +1 -0
  25. package/lib/kernel/client.d.ts +115 -0
  26. package/lib/kernel/client.js +375 -0
  27. package/lib/kernel/client.js.map +1 -0
  28. package/lib/kernel/index.d.ts +5 -0
  29. package/lib/kernel/index.js +8 -0
  30. package/lib/kernel/index.js.map +1 -0
  31. package/lib/kernel/kernelspecclient.d.ts +40 -0
  32. package/lib/kernel/kernelspecclient.js +37 -0
  33. package/lib/kernel/kernelspecclient.js.map +1 -0
  34. package/lib/kernel/kernelspecs.d.ts +59 -0
  35. package/lib/kernel/kernelspecs.js +62 -0
  36. package/lib/kernel/kernelspecs.js.map +1 -0
  37. package/lib/kernel/tokens.d.ts +163 -0
  38. package/lib/kernel/tokens.js +20 -0
  39. package/lib/kernel/tokens.js.map +1 -0
  40. package/lib/nbconvert/exporters.d.ts +80 -0
  41. package/lib/nbconvert/exporters.js +154 -0
  42. package/lib/nbconvert/exporters.js.map +1 -0
  43. package/lib/nbconvert/index.d.ts +3 -0
  44. package/lib/nbconvert/index.js +6 -0
  45. package/lib/nbconvert/index.js.map +1 -0
  46. package/lib/nbconvert/manager.d.ts +67 -0
  47. package/lib/nbconvert/manager.js +77 -0
  48. package/lib/nbconvert/manager.js.map +1 -0
  49. package/lib/nbconvert/tokens.d.ts +49 -0
  50. package/lib/nbconvert/tokens.js +8 -0
  51. package/lib/nbconvert/tokens.js.map +1 -0
  52. package/lib/session/client.d.ts +86 -0
  53. package/lib/session/client.js +200 -0
  54. package/lib/session/client.js.map +1 -0
  55. package/lib/session/index.d.ts +1 -0
  56. package/lib/session/index.js +4 -0
  57. package/lib/session/index.js.map +1 -0
  58. package/lib/settings/index.d.ts +1 -0
  59. package/lib/settings/index.js +4 -0
  60. package/lib/settings/index.js.map +1 -0
  61. package/lib/settings/settings.d.ts +92 -0
  62. package/lib/settings/settings.js +185 -0
  63. package/lib/settings/settings.js.map +1 -0
  64. package/package.json +67 -8
  65. package/src/contents/drive.ts +1032 -0
  66. package/src/contents/drivecontents.ts +254 -0
  67. package/src/contents/drivefs.ts +821 -0
  68. package/src/contents/emscripten.ts +148 -0
  69. package/src/contents/index.ts +8 -0
  70. package/src/contents/tokens.ts +61 -0
  71. package/src/index.ts +13 -0
  72. package/src/kernel/base.ts +638 -0
  73. package/src/kernel/client.ts +483 -0
  74. package/src/kernel/index.ts +8 -0
  75. package/src/kernel/kernelspecclient.ts +65 -0
  76. package/src/kernel/kernelspecs.ts +105 -0
  77. package/src/kernel/tokens.ts +222 -0
  78. package/src/nbconvert/exporters.ts +177 -0
  79. package/src/nbconvert/index.ts +6 -0
  80. package/src/nbconvert/manager.ts +106 -0
  81. package/src/nbconvert/tokens.ts +60 -0
  82. package/src/session/client.ts +252 -0
  83. package/src/session/index.ts +4 -0
  84. package/src/settings/index.ts +4 -0
  85. package/src/settings/settings.ts +237 -0
  86. package/style/index.css +6 -0
  87. package/style/index.js +6 -0
@@ -0,0 +1,821 @@
1
+ // Copyright (c) Jupyter Development Team.
2
+ // Distributed under the terms of the Modified BSD License.
3
+
4
+ // Types and implementation inspired from https://github.com/jvilk/BrowserFS
5
+ // LICENSE: https://github.com/jvilk/BrowserFS/blob/8977a704ea469d05daf857e4818bef1f4f498326/LICENSE
6
+ // And from https://github.com/gzuidhof/starboard-notebook
7
+
8
+ // LICENSE: https://github.com/gzuidhof/starboard-notebook/blob/cd8d3fc30af4bd29cdd8f6b8c207df8138f5d5dd/LICENSE
9
+ import type { Contents } from '@jupyterlab/services';
10
+
11
+ import { UUID } from '@lumino/coreutils';
12
+
13
+ import type {
14
+ FS,
15
+ ERRNO_CODES,
16
+ PATH,
17
+ IEmscriptenStream,
18
+ IEmscriptenStreamOps,
19
+ IEmscriptenNodeOps,
20
+ IEmscriptenFSNode,
21
+ IStats,
22
+ } from './emscripten';
23
+ import { DIR_MODE, SEEK_CUR, SEEK_END, instanceOfStream } from './emscripten';
24
+
25
+ export const DRIVE_SEPARATOR = ':';
26
+
27
+ export const BLOCK_SIZE = 4096;
28
+
29
+ const encoder = new TextEncoder();
30
+ const decoder = new TextDecoder('utf-8');
31
+
32
+ export type TDriveMethod =
33
+ | 'readdir'
34
+ | 'rmdir'
35
+ | 'rename'
36
+ | 'getmode'
37
+ | 'lookup'
38
+ | 'mknod'
39
+ | 'getattr'
40
+ | 'get'
41
+ | 'put';
42
+
43
+ /**
44
+ * Type of the data argument for the drive request, based on the request name
45
+ */
46
+ export type TDriveData = {
47
+ rename: {
48
+ /**
49
+ * The new path for the file
50
+ */
51
+ newPath: string;
52
+ };
53
+ mknod: {
54
+ /**
55
+ * The mode of the file to create
56
+ */
57
+ mode: number;
58
+ };
59
+ put: {
60
+ /**
61
+ * The file content to write
62
+ */
63
+ data: any;
64
+
65
+ /**
66
+ * The file content format
67
+ */
68
+ format: Contents.FileFormat;
69
+ };
70
+ };
71
+
72
+ /**
73
+ * Drive request
74
+ */
75
+ export type TDriveRequest<T extends TDriveMethod> = {
76
+ /**
77
+ * The method of the request (rmdir, readdir etc)
78
+ */
79
+ method: T;
80
+
81
+ /**
82
+ * A unique ID to identify the origin of this request
83
+ */
84
+ browsingContextId?: string;
85
+
86
+ /**
87
+ * A unique ID to correlate this specific request with its response
88
+ */
89
+ requestId?: string;
90
+
91
+ /**
92
+ * The path to the file/directory for which the request was sent
93
+ */
94
+ path: string;
95
+ } & (T extends keyof TDriveData ? { data: TDriveData[T] } : object);
96
+
97
+ type TDriveResponses = {
98
+ readdir: string[];
99
+ rmdir: null;
100
+ rename: null;
101
+ getmode: number;
102
+ lookup: DriveFS.ILookup;
103
+ mknod: null;
104
+ getattr: IStats;
105
+ get: {
106
+ /**
107
+ * The returned file content
108
+ */
109
+ content: any;
110
+
111
+ /**
112
+ * The content format
113
+ */
114
+ format: Contents.FileFormat;
115
+ } | null;
116
+ put: null;
117
+ };
118
+
119
+ /**
120
+ * Drive response
121
+ */
122
+ export type TDriveResponse<T extends TDriveMethod> = TDriveResponses[T];
123
+
124
+ // Mapping flag -> do we need to overwrite the file upon closing it
125
+ const flagNeedsWrite: { [flag: number]: boolean } = {
126
+ 0 /*O_RDONLY*/: false,
127
+ 1 /*O_WRONLY*/: true,
128
+ 2 /*O_RDWR*/: true,
129
+ 64 /*O_CREAT*/: true,
130
+ 65 /*O_WRONLY|O_CREAT*/: true,
131
+ 66 /*O_RDWR|O_CREAT*/: true,
132
+ 129 /*O_WRONLY|O_EXCL*/: true,
133
+ 193 /*O_WRONLY|O_CREAT|O_EXCL*/: true,
134
+ 514 /*O_RDWR|O_TRUNC*/: true,
135
+ 577 /*O_WRONLY|O_CREAT|O_TRUNC*/: true,
136
+ 578 /*O_CREAT|O_RDWR|O_TRUNC*/: true,
137
+ 705 /*O_WRONLY|O_CREAT|O_EXCL|O_TRUNC*/: true,
138
+ 706 /*O_RDWR|O_CREAT|O_EXCL|O_TRUNC*/: true,
139
+ 1024 /*O_APPEND*/: true,
140
+ 1025 /*O_WRONLY|O_APPEND*/: true,
141
+ 1026 /*O_RDWR|O_APPEND*/: true,
142
+ 1089 /*O_WRONLY|O_CREAT|O_APPEND*/: true,
143
+ 1090 /*O_RDWR|O_CREAT|O_APPEND*/: true,
144
+ 1153 /*O_WRONLY|O_EXCL|O_APPEND*/: true,
145
+ 1154 /*O_RDWR|O_EXCL|O_APPEND*/: true,
146
+ 1217 /*O_WRONLY|O_CREAT|O_EXCL|O_APPEND*/: true,
147
+ 1218 /*O_RDWR|O_CREAT|O_EXCL|O_APPEND*/: true,
148
+ 4096 /*O_RDONLY|O_DSYNC*/: true,
149
+ 4098 /*O_RDWR|O_DSYNC*/: true,
150
+ };
151
+
152
+ /** Implementation-specifc extension of an open stream, adding the file. */
153
+ export interface IDriveStream extends IEmscriptenStream {
154
+ file?: DriveFS.IFile;
155
+ }
156
+
157
+ export class DriveFSEmscriptenStreamOps implements IEmscriptenStreamOps {
158
+ private fs: DriveFS;
159
+
160
+ constructor(fs: DriveFS) {
161
+ this.fs = fs;
162
+ }
163
+
164
+ open(stream: IDriveStream): void {
165
+ const path = this.fs.realPath(stream.node);
166
+
167
+ if (this.fs.FS.isFile(stream.node.mode)) {
168
+ try {
169
+ const file = this.fs.API.get(path);
170
+ stream.file = file;
171
+ } catch (e) {
172
+ // If we're opening a file for writing and the file does not exist, create it! Otherwise, throw the proper error
173
+ // We need to do this because the current thread is thinking a file exist (isFile returns true)
174
+ // whilst it was actually deleted in the main thread
175
+
176
+ // if writing
177
+ const flags = stream.flags ?? stream.shared.flags;
178
+ let parsedFlags = typeof flags === 'string' ? parseInt(flags, 10) : flags;
179
+ parsedFlags &= 0x1fff;
180
+
181
+ let needsWrite = true;
182
+ if (parsedFlags in flagNeedsWrite) {
183
+ needsWrite = flagNeedsWrite[parsedFlags];
184
+ }
185
+ if (needsWrite) {
186
+ stream.node = this.fs.node_ops.mknod(
187
+ stream.node.parent,
188
+ stream.node.name,
189
+ stream.node.mode,
190
+ 0, // dev should be 0 for regular files
191
+ );
192
+ const file = this.fs.API.get(path);
193
+ stream.file = file;
194
+ } else {
195
+ throw new this.fs.FS.ErrnoError(this.fs.ERRNO_CODES['ENOENT']);
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ close(stream: IDriveStream): void {
202
+ if (!this.fs.FS.isFile(stream.node.mode) || !stream.file) {
203
+ return;
204
+ }
205
+
206
+ const path = this.fs.realPath(stream.node);
207
+
208
+ const flags = stream.flags ?? stream.shared.flags;
209
+ let parsedFlags = typeof flags === 'string' ? parseInt(flags, 10) : flags;
210
+ parsedFlags &= 0x1fff;
211
+
212
+ let needsWrite = true;
213
+ if (parsedFlags in flagNeedsWrite) {
214
+ needsWrite = flagNeedsWrite[parsedFlags];
215
+ }
216
+
217
+ if (needsWrite) {
218
+ this.fs.API.put(path, stream.file);
219
+ }
220
+
221
+ stream.file = undefined;
222
+ }
223
+
224
+ read(
225
+ stream: IDriveStream,
226
+ buffer: Uint8Array,
227
+ offset: number,
228
+ length: number,
229
+ position: number,
230
+ ): number {
231
+ if (
232
+ length <= 0 ||
233
+ stream.file === undefined ||
234
+ position >= (stream.file.data.length || 0)
235
+ ) {
236
+ return 0;
237
+ }
238
+
239
+ const size = Math.min(stream.file.data.length - position, length);
240
+ buffer.set(stream.file.data.subarray(position, position + size), offset);
241
+ return size;
242
+ }
243
+
244
+ write(
245
+ stream: IDriveStream,
246
+ buffer: Uint8Array,
247
+ offset: number,
248
+ length: number,
249
+ position: number,
250
+ ): number {
251
+ if (length <= 0 || stream.file === undefined) {
252
+ return 0;
253
+ }
254
+
255
+ const now = Date.now();
256
+ stream.node.timestamp = now;
257
+ stream.node.atime = now;
258
+ stream.node.mtime = now;
259
+ stream.node.ctime = now;
260
+
261
+ if (position + length > (stream.file?.data.length || 0)) {
262
+ const oldData = stream.file.data ? stream.file.data : new Uint8Array();
263
+ stream.file.data = new Uint8Array(position + length);
264
+ stream.file.data.set(oldData);
265
+ }
266
+
267
+ stream.file.data.set(buffer.subarray(offset, offset + length), position);
268
+
269
+ return length;
270
+ }
271
+
272
+ llseek(stream: IDriveStream, offset: number, whence: number): number {
273
+ let position = offset;
274
+ if (whence === SEEK_CUR) {
275
+ position += stream.position ?? stream.shared.position;
276
+ } else if (whence === SEEK_END) {
277
+ if (this.fs.FS.isFile(stream.node.mode)) {
278
+ if (stream.file !== undefined) {
279
+ position += stream.file.data.length;
280
+ } else {
281
+ throw new this.fs.FS.ErrnoError(this.fs.ERRNO_CODES.EPERM);
282
+ }
283
+ }
284
+ }
285
+
286
+ if (position < 0) {
287
+ throw new this.fs.FS.ErrnoError(this.fs.ERRNO_CODES.EINVAL);
288
+ }
289
+
290
+ return position;
291
+ }
292
+ }
293
+
294
+ export class DriveFSEmscriptenNodeOps implements IEmscriptenNodeOps {
295
+ private fs: DriveFS;
296
+
297
+ constructor(fs: DriveFS) {
298
+ this.fs = fs;
299
+ }
300
+
301
+ protected node = (
302
+ nodeOrStream: IEmscriptenFSNode | IEmscriptenStream,
303
+ ): IEmscriptenFSNode => {
304
+ if (instanceOfStream(nodeOrStream)) {
305
+ return nodeOrStream.node;
306
+ }
307
+ return nodeOrStream;
308
+ };
309
+
310
+ getattr = (value: IEmscriptenFSNode | IEmscriptenStream): IStats => {
311
+ const node = this.node(value);
312
+ return {
313
+ ...this.fs.API.getattr(this.fs.realPath(node)),
314
+ mode: node.mode,
315
+ ino: node.id,
316
+ };
317
+ };
318
+
319
+ setattr = (value: IEmscriptenFSNode | IEmscriptenStream, attr: IStats): void => {
320
+ const node = this.node(value);
321
+ for (const [key, value] of Object.entries(attr)) {
322
+ switch (key) {
323
+ case 'mode':
324
+ node.mode = value;
325
+ break;
326
+ case 'timestamp':
327
+ node.timestamp = value;
328
+ break;
329
+ case 'atime':
330
+ node.atime = value;
331
+ break;
332
+ case 'mtime':
333
+ node.mtime = value;
334
+ break;
335
+ case 'ctime':
336
+ node.ctime = value;
337
+ break;
338
+ case 'size': {
339
+ const size = value;
340
+ const path = this.fs.realPath(node);
341
+ if (this.fs.FS.isFile(node.mode) && size >= 0) {
342
+ let file;
343
+ try {
344
+ file = this.fs.API.get(path);
345
+ } catch (e) {
346
+ // TODO: Should do anything here? Should we create the file?
347
+ break;
348
+ }
349
+
350
+ const oldData = file.data ? file.data : new Uint8Array();
351
+ if (size !== oldData.length) {
352
+ if (size < oldData.length) {
353
+ file.data = file.data.slice(0, size);
354
+ } else {
355
+ file.data = new Uint8Array(size);
356
+ file.data.set(oldData);
357
+ }
358
+ this.fs.API.put(path, file);
359
+ }
360
+ } else {
361
+ console.warn('setattr size of', size, 'on', node, 'not yet implemented');
362
+ }
363
+ break;
364
+ }
365
+ case 'dontFollow':
366
+ // Ignore for now
367
+ break;
368
+ default:
369
+ console.warn('setattr', key, 'of', value, 'on', node, 'not yet implemented');
370
+ break;
371
+ }
372
+ }
373
+ };
374
+
375
+ lookup = (
376
+ parent: IEmscriptenFSNode | IEmscriptenStream,
377
+ name: string,
378
+ ): IEmscriptenFSNode => {
379
+ const node = this.node(parent);
380
+ const path = this.fs.PATH.join2(this.fs.realPath(node), name);
381
+ const result = this.fs.API.lookup(path);
382
+ if (!result.ok) {
383
+ throw new this.fs.FS.ErrnoError(this.fs.ERRNO_CODES['ENOENT']);
384
+ }
385
+ return this.fs.createNode(node, name, result.mode!, 0);
386
+ };
387
+
388
+ mknod = (
389
+ parent: IEmscriptenFSNode | IEmscriptenStream,
390
+ name: string,
391
+ mode: number,
392
+ dev: number,
393
+ ): IEmscriptenFSNode => {
394
+ const node = this.node(parent);
395
+ const path = this.fs.PATH.join2(this.fs.realPath(node), name);
396
+ this.fs.API.mknod(path, mode);
397
+ return this.fs.createNode(node, name, mode, dev);
398
+ };
399
+
400
+ rename = (
401
+ value: IEmscriptenFSNode | IEmscriptenStream,
402
+ newDir: IEmscriptenFSNode | IEmscriptenStream,
403
+ newName: string,
404
+ ): void => {
405
+ const oldNode = this.node(value);
406
+ const newDirNode = this.node(newDir);
407
+ this.fs.API.rename(
408
+ oldNode.parent
409
+ ? this.fs.PATH.join2(this.fs.realPath(oldNode.parent), oldNode.name)
410
+ : oldNode.name,
411
+ this.fs.PATH.join2(this.fs.realPath(newDirNode), newName),
412
+ );
413
+
414
+ // Updating the in-memory node
415
+ oldNode.name = newName;
416
+ oldNode.parent = newDirNode;
417
+ };
418
+
419
+ unlink = (parent: IEmscriptenFSNode | IEmscriptenStream, name: string): null => {
420
+ return this.fs.API.rmdir(
421
+ this.fs.PATH.join2(this.fs.realPath(this.node(parent)), name),
422
+ );
423
+ };
424
+
425
+ rmdir = (parent: IEmscriptenFSNode | IEmscriptenStream, name: string): null => {
426
+ return this.fs.API.rmdir(
427
+ this.fs.PATH.join2(this.fs.realPath(this.node(parent)), name),
428
+ );
429
+ };
430
+
431
+ readdir = (value: IEmscriptenFSNode | IEmscriptenStream): string[] => {
432
+ return this.fs.API.readdir(this.fs.realPath(this.node(value)));
433
+ };
434
+
435
+ symlink = (
436
+ parent: IEmscriptenFSNode | IEmscriptenStream,
437
+ newName: string,
438
+ oldPath: string,
439
+ ): void => {
440
+ throw new this.fs.FS.ErrnoError(this.fs.ERRNO_CODES['EPERM']);
441
+ };
442
+
443
+ readlink = (node: IEmscriptenFSNode | IEmscriptenStream): string => {
444
+ throw new this.fs.FS.ErrnoError(this.fs.ERRNO_CODES['EINVAL']);
445
+ };
446
+ }
447
+
448
+ /**
449
+ * ContentsAPI base class
450
+ */
451
+ export abstract class ContentsAPI {
452
+ constructor(options: ContentsAPI.IOptions) {
453
+ this._driveName = options.driveName;
454
+ this._mountpoint = options.mountpoint;
455
+
456
+ this.FS = options.FS;
457
+ this.ERRNO_CODES = options.ERRNO_CODES;
458
+ }
459
+
460
+ lookup(path: string): DriveFS.ILookup {
461
+ return this.request({ method: 'lookup', path: this.normalizePath(path) });
462
+ }
463
+
464
+ getmode(path: string): number {
465
+ return this.request({ method: 'getmode', path: this.normalizePath(path) });
466
+ }
467
+
468
+ mknod(path: string, mode: number): null {
469
+ return this.request({
470
+ method: 'mknod',
471
+ path: this.normalizePath(path),
472
+ data: { mode },
473
+ });
474
+ }
475
+
476
+ rename(oldPath: string, newPath: string): null {
477
+ return this.request({
478
+ method: 'rename',
479
+ path: this.normalizePath(oldPath),
480
+ data: { newPath: this.normalizePath(newPath) },
481
+ });
482
+ }
483
+
484
+ readdir(path: string): string[] {
485
+ const dirlist = this.request({
486
+ method: 'readdir',
487
+ path: this.normalizePath(path),
488
+ });
489
+ dirlist.push('.');
490
+ dirlist.push('..');
491
+ return dirlist;
492
+ }
493
+
494
+ rmdir(path: string): null {
495
+ return this.request({ method: 'rmdir', path: this.normalizePath(path) });
496
+ }
497
+
498
+ get(path: string): DriveFS.IFile {
499
+ const response = this.request({
500
+ method: 'get',
501
+ path: this.normalizePath(path),
502
+ });
503
+
504
+ if (!response) {
505
+ throw new this.FS.ErrnoError(this.ERRNO_CODES['ENOENT']);
506
+ }
507
+
508
+ const serializedContent = response.content;
509
+ const format: 'json' | 'text' | 'base64' | null = response.format;
510
+
511
+ switch (format) {
512
+ case 'json':
513
+ case 'text':
514
+ return {
515
+ data: encoder.encode(serializedContent),
516
+ format,
517
+ };
518
+ case 'base64': {
519
+ const binString = atob(serializedContent);
520
+ const len = binString.length;
521
+ const data = new Uint8Array(len);
522
+ for (let i = 0; i < len; i++) {
523
+ data[i] = binString.charCodeAt(i);
524
+ }
525
+ return {
526
+ data,
527
+ format,
528
+ };
529
+ }
530
+ default:
531
+ throw new this.FS.ErrnoError(this.ERRNO_CODES['ENOENT']);
532
+ }
533
+ }
534
+
535
+ put(path: string, value: DriveFS.IFile): null {
536
+ switch (value.format) {
537
+ case 'json':
538
+ case 'text':
539
+ return this.request({
540
+ method: 'put',
541
+ path: this.normalizePath(path),
542
+ data: {
543
+ format: value.format,
544
+ data: decoder.decode(value.data),
545
+ },
546
+ });
547
+ case 'base64': {
548
+ let binary = '';
549
+ for (let i = 0; i < value.data.byteLength; i++) {
550
+ binary += String.fromCharCode(value.data[i]);
551
+ }
552
+ return this.request({
553
+ method: 'put',
554
+ path: this.normalizePath(path),
555
+ data: {
556
+ format: value.format,
557
+ data: btoa(binary),
558
+ },
559
+ });
560
+ }
561
+ }
562
+ }
563
+
564
+ getattr(path: string): IStats {
565
+ const stats = this.request({
566
+ method: 'getattr',
567
+ path: this.normalizePath(path),
568
+ });
569
+
570
+ // Emscripten 4.0.9+ (used by Pyodide 0.28+) requires all three timestamps
571
+ // to be valid Date objects with .getTime() method (see https://github.com/emscripten-core/emscripten/pull/22998).
572
+ // Fallback to epoch if any timestamp is missing/null/undefined.
573
+ const defaultDate = new Date(0);
574
+ stats.atime = stats.atime ? new Date(stats.atime) : defaultDate;
575
+ stats.mtime = stats.mtime ? new Date(stats.mtime) : defaultDate;
576
+ stats.ctime = stats.ctime ? new Date(stats.ctime) : defaultDate;
577
+
578
+ // ensure a non-undefined size (0 isn't great, though)
579
+ stats.size = stats.size || 0;
580
+ return stats;
581
+ }
582
+
583
+ /**
584
+ * Normalize a Path by making it compliant for the content manager
585
+ *
586
+ * @param path: the path relatively to the Emscripten drive
587
+ */
588
+ normalizePath(path: string): string {
589
+ // Remove mountpoint prefix
590
+ if (path.startsWith(this._mountpoint)) {
591
+ path = path.slice(this._mountpoint.length);
592
+ }
593
+
594
+ // Add JupyterLab drive name
595
+ if (this._driveName) {
596
+ path = `${this._driveName}${DRIVE_SEPARATOR}${path}`;
597
+ }
598
+
599
+ return path;
600
+ }
601
+
602
+ abstract request<T extends TDriveMethod>(data: TDriveRequest<T>): TDriveResponse<T>;
603
+
604
+ private _driveName: string;
605
+ private _mountpoint: string;
606
+
607
+ protected FS: FS;
608
+ protected ERRNO_CODES: ERRNO_CODES;
609
+ }
610
+
611
+ /**
612
+ * An Emscripten-compatible synchronous Contents API using the service worker.
613
+ */
614
+ export class ServiceWorkerContentsAPI extends ContentsAPI {
615
+ /**
616
+ * Construct a new ServiceWorkerContentsAPI.
617
+ */
618
+ constructor(options: ServiceWorkerContentsAPI.IOptions) {
619
+ super(options);
620
+
621
+ this._baseUrl = options.baseUrl;
622
+ this._browsingContextId = options.browsingContextId || '';
623
+ }
624
+
625
+ request<T extends TDriveMethod>(data: TDriveRequest<T>): TDriveResponse<T> {
626
+ const xhr = new XMLHttpRequest();
627
+ xhr.open('POST', encodeURI(this.endpoint), false);
628
+
629
+ // Generate unique request ID for correlation
630
+ const requestId = UUID.uuid4();
631
+
632
+ // Add the origin browsing context ID and request ID to the request
633
+ const requestWithMetadata = {
634
+ data: { ...data, requestId },
635
+ browsingContextId: this._browsingContextId,
636
+ requestId,
637
+ };
638
+
639
+ try {
640
+ xhr.send(JSON.stringify(requestWithMetadata));
641
+ } catch (e) {
642
+ console.error(e);
643
+ }
644
+
645
+ if (xhr.status >= 400) {
646
+ throw new this.FS.ErrnoError(this.ERRNO_CODES['EINVAL']);
647
+ }
648
+
649
+ return JSON.parse(xhr.responseText);
650
+ }
651
+
652
+ /**
653
+ * Get the api/drive endpoint
654
+ */
655
+ get endpoint(): string {
656
+ return `${this._baseUrl}api/drive`;
657
+ }
658
+
659
+ private _baseUrl: string;
660
+ private _browsingContextId: string;
661
+ }
662
+
663
+ export class DriveFS {
664
+ FS: FS;
665
+ API: ContentsAPI;
666
+ PATH: PATH;
667
+ ERRNO_CODES: ERRNO_CODES;
668
+ driveName: string;
669
+
670
+ constructor(options: DriveFS.IOptions) {
671
+ this.FS = options.FS;
672
+ this.PATH = options.PATH;
673
+ this.ERRNO_CODES = options.ERRNO_CODES;
674
+ this.API = this.createAPI(options);
675
+
676
+ this.driveName = options.driveName;
677
+
678
+ this.node_ops = new DriveFSEmscriptenNodeOps(this);
679
+ this.stream_ops = new DriveFSEmscriptenStreamOps(this);
680
+ }
681
+
682
+ node_ops: IEmscriptenNodeOps;
683
+ stream_ops: IEmscriptenStreamOps;
684
+
685
+ /**
686
+ * Create the ContentsAPI.
687
+ *
688
+ * This is supposed to be overwritten if needed.
689
+ */
690
+ createAPI(options: DriveFS.IOptions): ContentsAPI {
691
+ if (!options.browsingContextId || !options.baseUrl) {
692
+ throw new Error(
693
+ 'Cannot create service-worker API without current browsingContextId',
694
+ );
695
+ }
696
+
697
+ return new ServiceWorkerContentsAPI(options as ServiceWorkerContentsAPI.IOptions);
698
+ }
699
+
700
+ mount(mount: any): IEmscriptenFSNode {
701
+ return this.createNode(null, mount.mountpoint, DIR_MODE | 511, 0);
702
+ }
703
+
704
+ createNode(
705
+ parent: IEmscriptenFSNode | null,
706
+ name: string,
707
+ mode: number,
708
+ dev: number,
709
+ ): IEmscriptenFSNode {
710
+ const FS = this.FS;
711
+ if (!FS.isDir(mode) && !FS.isFile(mode)) {
712
+ throw new FS.ErrnoError(this.ERRNO_CODES['EINVAL']);
713
+ }
714
+ const node = FS.createNode(parent, name, mode, dev);
715
+ node.node_ops = this.node_ops;
716
+ node.stream_ops = this.stream_ops;
717
+ return node;
718
+ }
719
+
720
+ getMode(path: string): number {
721
+ return this.API.getmode(path);
722
+ }
723
+
724
+ realPath(node: IEmscriptenFSNode): string {
725
+ const parts: string[] = [];
726
+ let currentNode: IEmscriptenFSNode = node;
727
+
728
+ parts.push(currentNode.name);
729
+ while (currentNode.parent !== currentNode) {
730
+ currentNode = currentNode.parent;
731
+ parts.push(currentNode.name);
732
+ }
733
+ parts.reverse();
734
+
735
+ return this.PATH.join.apply(null, parts);
736
+ }
737
+ }
738
+
739
+ /**
740
+ * A namespace for ContentsAPI configurations, etc.
741
+ */
742
+ export namespace ContentsAPI {
743
+ /**
744
+ * Initialization options for a contents API;
745
+ */
746
+ export interface IOptions {
747
+ /**
748
+ * The name of the drive to use for the contents API request.
749
+ */
750
+ driveName: string;
751
+
752
+ /**
753
+ * Where to mount files in the kernel.
754
+ */
755
+ mountpoint: string;
756
+
757
+ /**
758
+ * The filesystem module API.
759
+ */
760
+ FS: FS;
761
+
762
+ /**
763
+ * The filesystem error codes.
764
+ */
765
+ ERRNO_CODES: ERRNO_CODES;
766
+ }
767
+ }
768
+
769
+ /**
770
+ * A namespace for ServiceWorkerContentsAPI configurations, etc.
771
+ */
772
+ export namespace ServiceWorkerContentsAPI {
773
+ /**
774
+ * Initialization options for a service worker contents API
775
+ */
776
+ export interface IOptions extends ContentsAPI.IOptions {
777
+ /**
778
+ * The base URL.
779
+ */
780
+ baseUrl: string;
781
+
782
+ /**
783
+ * The ID of the browsing context where the request originated.
784
+ */
785
+ browsingContextId: string;
786
+ }
787
+ }
788
+
789
+ /**
790
+ * A namespace for DriveFS configurations, etc.
791
+ */
792
+ export namespace DriveFS {
793
+ /**
794
+ * A file representation;
795
+ */
796
+ export interface IFile {
797
+ data: Uint8Array;
798
+ format: 'json' | 'text' | 'base64';
799
+ }
800
+
801
+ /**
802
+ * The response to a lookup request;
803
+ */
804
+ export interface ILookup {
805
+ ok: boolean;
806
+ mode?: number;
807
+ }
808
+
809
+ /**
810
+ * Initialization options for a drive;
811
+ */
812
+ export interface IOptions {
813
+ FS: FS;
814
+ PATH: PATH;
815
+ ERRNO_CODES: ERRNO_CODES;
816
+ baseUrl: string;
817
+ driveName: string;
818
+ mountpoint: string;
819
+ browsingContextId?: string;
820
+ }
821
+ }