@jupyterlite/services 0.1.0 → 0.7.0-rc.0

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