@jupyterlite/services 0.8.0-alpha.0 → 0.8.0-alpha.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/lib/contents/drive.d.ts +1 -1
- package/lib/contents/drive.js +184 -75
- package/lib/contents/drive.js.map +1 -1
- package/lib/contents/drivecontents.js +14 -9
- package/lib/contents/drivecontents.js.map +1 -1
- package/lib/contents/drivefs.js +2 -2
- package/lib/contents/drivefs.js.map +1 -1
- package/lib/contents/emscripten.d.ts +5 -1
- package/lib/contents/emscripten.js +11 -0
- package/lib/contents/emscripten.js.map +1 -1
- package/lib/kernel/base.js +8 -8
- package/lib/kernel/base.js.map +1 -1
- package/lib/nbconvert/exporters.js +24 -1
- package/lib/nbconvert/exporters.js.map +1 -1
- package/lib/session/client.js +23 -3
- package/lib/session/client.js.map +1 -1
- package/package.json +8 -19
- package/src/contents/drive.ts +190 -72
- package/src/contents/drivecontents.ts +15 -9
- package/src/contents/drivefs.ts +2 -2
- package/src/contents/emscripten.ts +15 -1
- package/src/kernel/base.ts +8 -16
- package/src/nbconvert/exporters.ts +24 -1
- package/src/session/client.ts +24 -0
package/src/contents/drive.ts
CHANGED
|
@@ -36,6 +36,104 @@ const N_CHECKPOINTS = 5;
|
|
|
36
36
|
const encoder = new TextEncoder();
|
|
37
37
|
const decoder = new TextDecoder('utf-8');
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Converts a contents model into JSON
|
|
41
|
+
*
|
|
42
|
+
* @param model the model to convert
|
|
43
|
+
* @returns the converted model
|
|
44
|
+
*/
|
|
45
|
+
function convertToJSON(model: Contents.IModel): Contents.IModel {
|
|
46
|
+
switch (model.format) {
|
|
47
|
+
case 'json': {
|
|
48
|
+
return model;
|
|
49
|
+
}
|
|
50
|
+
case 'text': {
|
|
51
|
+
return {
|
|
52
|
+
...model,
|
|
53
|
+
content: JSON.parse(model.content),
|
|
54
|
+
format: 'json',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
case 'base64': {
|
|
58
|
+
const binary = atob(model.content);
|
|
59
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
60
|
+
const decoded = new TextDecoder('utf-8').decode(bytes);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
...model,
|
|
64
|
+
content: JSON.parse(decoded),
|
|
65
|
+
format: 'json',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
throw new Error(`Invalid format ${model.format}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Converts a contents model into Text
|
|
75
|
+
*
|
|
76
|
+
* @param model the model to convert
|
|
77
|
+
* @returns the converted model
|
|
78
|
+
*/
|
|
79
|
+
function convertToText(model: Contents.IModel): Contents.IModel {
|
|
80
|
+
switch (model.format) {
|
|
81
|
+
case 'json': {
|
|
82
|
+
return {
|
|
83
|
+
...model,
|
|
84
|
+
content: JSON.stringify(model.content),
|
|
85
|
+
format: 'text',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
case 'text': {
|
|
89
|
+
return model;
|
|
90
|
+
}
|
|
91
|
+
case 'base64': {
|
|
92
|
+
const binary = atob(model.content);
|
|
93
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
94
|
+
const decoded = new TextDecoder('utf-8').decode(bytes);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
...model,
|
|
98
|
+
content: decoded,
|
|
99
|
+
format: 'text',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
throw new Error(`Invalid format ${model.format}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Converts a contents model into Base64
|
|
109
|
+
*
|
|
110
|
+
* @param model the model to convert
|
|
111
|
+
* @returns the converted model
|
|
112
|
+
*/
|
|
113
|
+
function convertToBase64(model: Contents.IModel): Contents.IModel {
|
|
114
|
+
switch (model.format) {
|
|
115
|
+
case 'json': {
|
|
116
|
+
return {
|
|
117
|
+
...model,
|
|
118
|
+
content: btoa(JSON.stringify(model.content)),
|
|
119
|
+
format: 'base64',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
case 'text': {
|
|
123
|
+
return {
|
|
124
|
+
...model,
|
|
125
|
+
content: btoa(model.content),
|
|
126
|
+
format: 'base64',
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
case 'base64': {
|
|
130
|
+
return model;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
throw new Error(`Invalid format ${model.format}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
39
137
|
/**
|
|
40
138
|
* A custom drive to store files in the browser storage.
|
|
41
139
|
*/
|
|
@@ -258,27 +356,7 @@ export class BrowserStorageDrive implements Contents.IDrive {
|
|
|
258
356
|
const type = options?.type ?? 'notebook';
|
|
259
357
|
const created = new Date().toISOString();
|
|
260
358
|
|
|
261
|
-
let
|
|
262
|
-
const basename = PathExt.basename(path);
|
|
263
|
-
const extname = PathExt.extname(path);
|
|
264
|
-
const item = await this.get(dirname).catch(() => null);
|
|
265
|
-
|
|
266
|
-
// handle the case of "Save As", where the path points to the new file
|
|
267
|
-
// to create, e.g. subfolder/example-copy.ipynb
|
|
268
|
-
let name = '';
|
|
269
|
-
if (path && !extname && item) {
|
|
270
|
-
// directory
|
|
271
|
-
dirname = `${path}/`;
|
|
272
|
-
name = '';
|
|
273
|
-
} else if (dirname && basename) {
|
|
274
|
-
// file in a subfolder
|
|
275
|
-
dirname = `${dirname}/`;
|
|
276
|
-
name = basename;
|
|
277
|
-
} else {
|
|
278
|
-
// file at the top level
|
|
279
|
-
dirname = '';
|
|
280
|
-
name = path;
|
|
281
|
-
}
|
|
359
|
+
let name: string | undefined = undefined;
|
|
282
360
|
|
|
283
361
|
let file: IModel;
|
|
284
362
|
switch (type) {
|
|
@@ -287,7 +365,7 @@ export class BrowserStorageDrive implements Contents.IDrive {
|
|
|
287
365
|
name = `Untitled Folder${counter || ''}`;
|
|
288
366
|
file = {
|
|
289
367
|
name,
|
|
290
|
-
path:
|
|
368
|
+
path: PathExt.join(path, name),
|
|
291
369
|
last_modified: created,
|
|
292
370
|
created,
|
|
293
371
|
format: 'json',
|
|
@@ -304,7 +382,7 @@ export class BrowserStorageDrive implements Contents.IDrive {
|
|
|
304
382
|
name = name || `Untitled${counter || ''}.ipynb`;
|
|
305
383
|
file = {
|
|
306
384
|
name,
|
|
307
|
-
path:
|
|
385
|
+
path: PathExt.join(path, name),
|
|
308
386
|
last_modified: created,
|
|
309
387
|
created,
|
|
310
388
|
format: 'json',
|
|
@@ -317,15 +395,20 @@ export class BrowserStorageDrive implements Contents.IDrive {
|
|
|
317
395
|
break;
|
|
318
396
|
}
|
|
319
397
|
default: {
|
|
320
|
-
let ext = options?.ext
|
|
321
|
-
if (!ext.startsWith('.')) {
|
|
398
|
+
let ext = options?.ext;
|
|
399
|
+
if (ext && !ext.startsWith('.')) {
|
|
322
400
|
ext = `.${ext}`;
|
|
323
401
|
}
|
|
324
402
|
const counter = await this._incrementCounter('file');
|
|
325
|
-
const mimetype =
|
|
403
|
+
const mimetype = ext
|
|
404
|
+
? FILE.getType(ext) || MIME.OCTET_STREAM
|
|
405
|
+
: MIME.OCTET_STREAM;
|
|
326
406
|
|
|
327
407
|
let format: Contents.FileFormat;
|
|
328
|
-
if (
|
|
408
|
+
if (!ext) {
|
|
409
|
+
format = 'base64';
|
|
410
|
+
ext = '';
|
|
411
|
+
} else if (FILE.hasFormat(ext, 'text') || mimetype.indexOf('text') !== -1) {
|
|
329
412
|
format = 'text';
|
|
330
413
|
} else if (ext.indexOf('json') !== -1 || ext.indexOf('ipynb') !== -1) {
|
|
331
414
|
format = 'json';
|
|
@@ -336,7 +419,7 @@ export class BrowserStorageDrive implements Contents.IDrive {
|
|
|
336
419
|
name = name || `untitled${counter || ''}${ext}`;
|
|
337
420
|
file = {
|
|
338
421
|
name,
|
|
339
|
-
path:
|
|
422
|
+
path: PathExt.join(path, name),
|
|
340
423
|
last_modified: created,
|
|
341
424
|
created,
|
|
342
425
|
format,
|
|
@@ -440,16 +523,36 @@ export class BrowserStorageDrive implements Contents.IDrive {
|
|
|
440
523
|
const item = await storage.getItem(path);
|
|
441
524
|
const serverItem = await this._getServerContents(path, options);
|
|
442
525
|
|
|
443
|
-
|
|
526
|
+
let model = (item || serverItem) as IModel | null;
|
|
444
527
|
|
|
445
528
|
if (!model) {
|
|
446
529
|
throw Error(`Could not find content with path ${path}`);
|
|
447
530
|
}
|
|
448
531
|
|
|
449
|
-
if (
|
|
532
|
+
if (options?.content) {
|
|
533
|
+
// Fix model format if the requested format does not match
|
|
534
|
+
const requestedFormat = options?.format;
|
|
535
|
+
if (requestedFormat && model.format !== requestedFormat) {
|
|
536
|
+
switch (requestedFormat) {
|
|
537
|
+
case 'json': {
|
|
538
|
+
model = convertToJSON(model);
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
case 'text': {
|
|
542
|
+
model = convertToText(model);
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
case 'base64': {
|
|
546
|
+
model = convertToBase64(model);
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
450
552
|
return {
|
|
451
|
-
size: 0,
|
|
452
553
|
...model,
|
|
554
|
+
size: 0,
|
|
555
|
+
format: options?.format ?? model.format,
|
|
453
556
|
content: null,
|
|
454
557
|
};
|
|
455
558
|
}
|
|
@@ -574,7 +677,9 @@ export class BrowserStorageDrive implements Contents.IDrive {
|
|
|
574
677
|
path = decodeURIComponent(path);
|
|
575
678
|
|
|
576
679
|
// process the file if coming from an upload
|
|
577
|
-
const
|
|
680
|
+
const name = options.name ? options.name : PathExt.basename(path) ?? undefined;
|
|
681
|
+
const ext = name ? PathExt.extname(name) ?? undefined : undefined;
|
|
682
|
+
const mimetype = ext ? FILE.getType(ext) || MIME.OCTET_STREAM : MIME.OCTET_STREAM;
|
|
578
683
|
const chunk = options.chunk;
|
|
579
684
|
|
|
580
685
|
// retrieve the content if it is a later chunk or the last one
|
|
@@ -582,74 +687,79 @@ export class BrowserStorageDrive implements Contents.IDrive {
|
|
|
582
687
|
const appendChunk = chunk ? chunk > 1 || chunk === -1 : false;
|
|
583
688
|
item = await this.get(path, { content: appendChunk }).catch(() => null);
|
|
584
689
|
|
|
585
|
-
|
|
586
|
-
item = await this.newUntitled({ path, ext, type: 'file' });
|
|
587
|
-
}
|
|
690
|
+
const now = new Date().toISOString();
|
|
588
691
|
|
|
589
|
-
|
|
590
|
-
|
|
692
|
+
let type = options.type || 'file';
|
|
693
|
+
|
|
694
|
+
// The Contents API treats Notebooks as a special case.
|
|
695
|
+
if (ext && ext.toLowerCase() === '.ipynb') {
|
|
696
|
+
type = 'notebook';
|
|
591
697
|
}
|
|
592
698
|
|
|
593
|
-
|
|
594
|
-
const
|
|
699
|
+
const format = options?.format || 'base64';
|
|
700
|
+
const content = options?.content || '';
|
|
595
701
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
702
|
+
// keep a reference to the original content
|
|
703
|
+
const originalContent = item?.content;
|
|
704
|
+
|
|
705
|
+
if (item) {
|
|
706
|
+
item = {
|
|
707
|
+
...item,
|
|
708
|
+
last_modified: now,
|
|
709
|
+
format,
|
|
710
|
+
mimetype,
|
|
711
|
+
content,
|
|
712
|
+
writable: true,
|
|
713
|
+
type,
|
|
714
|
+
};
|
|
715
|
+
} else {
|
|
716
|
+
item = {
|
|
717
|
+
name,
|
|
718
|
+
path,
|
|
719
|
+
last_modified: now,
|
|
720
|
+
created: now,
|
|
721
|
+
format,
|
|
722
|
+
mimetype,
|
|
723
|
+
content,
|
|
724
|
+
writable: true,
|
|
725
|
+
type,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
603
728
|
|
|
604
|
-
|
|
605
|
-
|
|
729
|
+
// Handle multichunks uploads
|
|
730
|
+
if (chunk) {
|
|
731
|
+
const lastChunk = chunk === -1;
|
|
606
732
|
|
|
607
733
|
const contentBinaryString = this._handleUploadChunk(
|
|
608
|
-
|
|
734
|
+
content,
|
|
609
735
|
originalContent,
|
|
610
736
|
appendChunk,
|
|
611
737
|
);
|
|
612
738
|
|
|
613
|
-
if (
|
|
614
|
-
const content = lastChunk
|
|
615
|
-
? JSON.parse(decoder.decode(this._binaryStringToBytes(contentBinaryString)))
|
|
616
|
-
: contentBinaryString;
|
|
617
|
-
item = {
|
|
618
|
-
...item,
|
|
619
|
-
content,
|
|
620
|
-
format: 'json',
|
|
621
|
-
type: 'notebook',
|
|
622
|
-
size: contentBinaryString.length,
|
|
623
|
-
};
|
|
624
|
-
} else if (FILE.hasFormat(ext, 'json')) {
|
|
739
|
+
if (item.format === 'json') {
|
|
625
740
|
const content = lastChunk
|
|
626
741
|
? JSON.parse(decoder.decode(this._binaryStringToBytes(contentBinaryString)))
|
|
627
742
|
: contentBinaryString;
|
|
628
743
|
item = {
|
|
629
744
|
...item,
|
|
630
745
|
content,
|
|
631
|
-
format: 'json',
|
|
632
|
-
type: 'file',
|
|
633
746
|
size: contentBinaryString.length,
|
|
634
747
|
};
|
|
635
|
-
} else if (
|
|
748
|
+
} else if (item.format === 'text') {
|
|
636
749
|
const content = lastChunk
|
|
637
750
|
? decoder.decode(this._binaryStringToBytes(contentBinaryString))
|
|
638
751
|
: contentBinaryString;
|
|
639
752
|
item = {
|
|
640
753
|
...item,
|
|
641
754
|
content,
|
|
642
|
-
format: 'text',
|
|
643
|
-
type: 'file',
|
|
644
755
|
size: contentBinaryString.length,
|
|
645
756
|
};
|
|
646
757
|
} else {
|
|
758
|
+
// item.format is base64
|
|
647
759
|
const content = lastChunk ? btoa(contentBinaryString) : contentBinaryString;
|
|
648
760
|
item = {
|
|
649
761
|
...item,
|
|
650
762
|
content,
|
|
651
|
-
format: 'base64',
|
|
652
|
-
type: 'file',
|
|
653
763
|
size: contentBinaryString.length,
|
|
654
764
|
};
|
|
655
765
|
}
|
|
@@ -657,7 +767,7 @@ export class BrowserStorageDrive implements Contents.IDrive {
|
|
|
657
767
|
|
|
658
768
|
// fixup content sizes if necessary
|
|
659
769
|
if (item.content) {
|
|
660
|
-
switch (
|
|
770
|
+
switch (item.format) {
|
|
661
771
|
case 'json': {
|
|
662
772
|
item = { ...item, size: encoder.encode(JSON.stringify(item.content)).length };
|
|
663
773
|
break;
|
|
@@ -666,8 +776,9 @@ export class BrowserStorageDrive implements Contents.IDrive {
|
|
|
666
776
|
item = { ...item, size: encoder.encode(item.content).length };
|
|
667
777
|
break;
|
|
668
778
|
}
|
|
669
|
-
// base64 save was already handled above
|
|
670
779
|
case 'base64': {
|
|
780
|
+
const padding = (item.content.match(/=+$/) || [''])[0].length;
|
|
781
|
+
item = { ...item, size: (item.content.length * 3) / 4 - padding };
|
|
671
782
|
break;
|
|
672
783
|
}
|
|
673
784
|
default: {
|
|
@@ -970,7 +1081,7 @@ export class BrowserStorageDrive implements Contents.IDrive {
|
|
|
970
1081
|
};
|
|
971
1082
|
|
|
972
1083
|
/**
|
|
973
|
-
* retrieve the contents for this path from
|
|
1084
|
+
* retrieve the contents for this path from the contents index file in the appropriate
|
|
974
1085
|
* folder.
|
|
975
1086
|
*
|
|
976
1087
|
* @param newLocalPath - The new file path.
|
|
@@ -981,11 +1092,18 @@ export class BrowserStorageDrive implements Contents.IDrive {
|
|
|
981
1092
|
const content = this._serverContents.get(path) || new Map();
|
|
982
1093
|
|
|
983
1094
|
if (!this._serverContents.has(path)) {
|
|
1095
|
+
// Check if contents are indexed by looking for the filename in PageConfig
|
|
1096
|
+
const contentsAllJsonFile = PageConfig.getOption('contentsAllJsonFile');
|
|
1097
|
+
if (!contentsAllJsonFile) {
|
|
1098
|
+
this._serverContents.set(path, content);
|
|
1099
|
+
return content;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
984
1102
|
const apiURL = URLExt.join(
|
|
985
1103
|
PageConfig.getBaseUrl(),
|
|
986
1104
|
'api/contents',
|
|
987
1105
|
path,
|
|
988
|
-
|
|
1106
|
+
contentsAllJsonFile,
|
|
989
1107
|
);
|
|
990
1108
|
|
|
991
1109
|
try {
|
|
@@ -2,7 +2,7 @@ import { PathExt } from '@jupyterlab/coreutils';
|
|
|
2
2
|
import type { Contents } from '@jupyterlab/services';
|
|
3
3
|
import type { TDriveMethod, TDriveRequest, TDriveResponse } from './drivefs';
|
|
4
4
|
import { BLOCK_SIZE } from './drivefs';
|
|
5
|
-
import { DIR_MODE, FILE_MODE } from './emscripten';
|
|
5
|
+
import { isDirMode, DIR_MODE, FILE_MODE } from './emscripten';
|
|
6
6
|
|
|
7
7
|
export interface IDriveContentsProcessor {
|
|
8
8
|
/**
|
|
@@ -168,7 +168,7 @@ export class DriveContentsProcessor implements IDriveContentsProcessor {
|
|
|
168
168
|
ok: true,
|
|
169
169
|
mode: model.type === 'directory' ? DIR_MODE : FILE_MODE,
|
|
170
170
|
};
|
|
171
|
-
} catch
|
|
171
|
+
} catch {
|
|
172
172
|
response = { ok: false };
|
|
173
173
|
}
|
|
174
174
|
|
|
@@ -176,12 +176,18 @@ export class DriveContentsProcessor implements IDriveContentsProcessor {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
async mknod(request: TDriveRequest<'mknod'>): Promise<TDriveResponse<'mknod'>> {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
179
|
+
// Contents API does not permit creating folders with given name. We can only create untitled folder then rename.
|
|
180
|
+
if (isDirMode(request.data.mode)) {
|
|
181
|
+
const model = await this.contentsManager.newUntitled({
|
|
182
|
+
path: PathExt.dirname(request.path),
|
|
183
|
+
type: 'directory',
|
|
184
|
+
ext: PathExt.extname(request.path),
|
|
185
|
+
});
|
|
186
|
+
await this.contentsManager.rename(model.path, request.path);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
await this.contentsManager.save(request.path, { type: 'file' });
|
|
185
191
|
return null;
|
|
186
192
|
}
|
|
187
193
|
|
|
@@ -211,7 +217,7 @@ export class DriveContentsProcessor implements IDriveContentsProcessor {
|
|
|
211
217
|
let model: Contents.IModel;
|
|
212
218
|
try {
|
|
213
219
|
model = await this.contentsManager.get(request.path, { content: true });
|
|
214
|
-
} catch
|
|
220
|
+
} catch {
|
|
215
221
|
return null;
|
|
216
222
|
}
|
|
217
223
|
|
package/src/contents/drivefs.ts
CHANGED
|
@@ -168,7 +168,7 @@ export class DriveFSEmscriptenStreamOps implements IEmscriptenStreamOps {
|
|
|
168
168
|
try {
|
|
169
169
|
const file = this.fs.API.get(path);
|
|
170
170
|
stream.file = file;
|
|
171
|
-
} catch
|
|
171
|
+
} catch {
|
|
172
172
|
// If we're opening a file for writing and the file does not exist, create it! Otherwise, throw the proper error
|
|
173
173
|
// We need to do this because the current thread is thinking a file exist (isFile returns true)
|
|
174
174
|
// whilst it was actually deleted in the main thread
|
|
@@ -342,7 +342,7 @@ export class DriveFSEmscriptenNodeOps implements IEmscriptenNodeOps {
|
|
|
342
342
|
let file;
|
|
343
343
|
try {
|
|
344
344
|
file = this.fs.API.get(path);
|
|
345
|
-
} catch
|
|
345
|
+
} catch {
|
|
346
346
|
// TODO: Should do anything here? Should we create the file?
|
|
347
347
|
break;
|
|
348
348
|
}
|
|
@@ -13,13 +13,27 @@
|
|
|
13
13
|
* Ideally, much more of these would be taken from `@types/emscripten`.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
type EmscriptenFS = typeof FS
|
|
16
|
+
type EmscriptenFS = typeof globalThis extends { FS: infer T } ? T : Record<string, any>;
|
|
17
|
+
|
|
18
|
+
// Those constants are standard that is very unlikely to change
|
|
19
|
+
// https://github.com/bminor/glibc/blob/04e750e75b73957cf1c791535a3f4319534a52fc/bits/stat.h#L66
|
|
20
|
+
const S_IFMT = 0o170000;
|
|
21
|
+
const S_IFDIR = 0o040000;
|
|
22
|
+
const S_IFREG = 0o100000;
|
|
17
23
|
|
|
18
24
|
export const DIR_MODE = 16895; // 040777
|
|
19
25
|
export const FILE_MODE = 33206; // 100666
|
|
20
26
|
export const SEEK_CUR = 1;
|
|
21
27
|
export const SEEK_END = 2;
|
|
22
28
|
|
|
29
|
+
export function isDirMode(mode: number): boolean {
|
|
30
|
+
return (mode & S_IFMT) === S_IFDIR;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isFileMode(mode: number): boolean {
|
|
34
|
+
return (mode & S_IFMT) === S_IFREG;
|
|
35
|
+
}
|
|
36
|
+
|
|
23
37
|
export interface IStats {
|
|
24
38
|
dev: number;
|
|
25
39
|
ino?: number;
|
package/src/kernel/base.ts
CHANGED
|
@@ -243,8 +243,7 @@ export abstract class BaseKernel implements IKernel {
|
|
|
243
243
|
| KernelMessage.IHeader<KernelMessage.MessageType>
|
|
244
244
|
| undefined = undefined,
|
|
245
245
|
): void {
|
|
246
|
-
const parentHeaderValue =
|
|
247
|
-
typeof parentHeader !== 'undefined' ? parentHeader : this._parentHeader;
|
|
246
|
+
const parentHeaderValue = parentHeader ?? this._parentHeader;
|
|
248
247
|
const message = KernelMessage.createMessage<KernelMessage.IStreamMsg>({
|
|
249
248
|
channel: 'iopub',
|
|
250
249
|
msgType: 'stream',
|
|
@@ -269,8 +268,7 @@ export abstract class BaseKernel implements IKernel {
|
|
|
269
268
|
| undefined = undefined,
|
|
270
269
|
): void {
|
|
271
270
|
// Make sure metadata is always set
|
|
272
|
-
const parentHeaderValue =
|
|
273
|
-
typeof parentHeader !== 'undefined' ? parentHeader : this._parentHeader;
|
|
271
|
+
const parentHeaderValue = parentHeader ?? this._parentHeader;
|
|
274
272
|
content.metadata = content.metadata ?? {};
|
|
275
273
|
|
|
276
274
|
const message = KernelMessage.createMessage<KernelMessage.IDisplayDataMsg>({
|
|
@@ -296,8 +294,7 @@ export abstract class BaseKernel implements IKernel {
|
|
|
296
294
|
| KernelMessage.IHeader<KernelMessage.MessageType>
|
|
297
295
|
| undefined = undefined,
|
|
298
296
|
): void {
|
|
299
|
-
const parentHeaderValue =
|
|
300
|
-
typeof parentHeader !== 'undefined' ? parentHeader : this._parentHeader;
|
|
297
|
+
const parentHeaderValue = parentHeader ?? this._parentHeader;
|
|
301
298
|
const message = KernelMessage.createMessage<KernelMessage.IInputRequestMsg>({
|
|
302
299
|
channel: 'stdin',
|
|
303
300
|
msgType: 'input_request',
|
|
@@ -321,8 +318,7 @@ export abstract class BaseKernel implements IKernel {
|
|
|
321
318
|
| KernelMessage.IHeader<KernelMessage.MessageType>
|
|
322
319
|
| undefined = undefined,
|
|
323
320
|
): void {
|
|
324
|
-
const parentHeaderValue =
|
|
325
|
-
typeof parentHeader !== 'undefined' ? parentHeader : this._parentHeader;
|
|
321
|
+
const parentHeaderValue = parentHeader ?? this._parentHeader;
|
|
326
322
|
const message = KernelMessage.createMessage<KernelMessage.IExecuteResultMsg>({
|
|
327
323
|
channel: 'iopub',
|
|
328
324
|
msgType: 'execute_result',
|
|
@@ -346,8 +342,7 @@ export abstract class BaseKernel implements IKernel {
|
|
|
346
342
|
| KernelMessage.IHeader<KernelMessage.MessageType>
|
|
347
343
|
| undefined = undefined,
|
|
348
344
|
): void {
|
|
349
|
-
const parentHeaderValue =
|
|
350
|
-
typeof parentHeader !== 'undefined' ? parentHeader : this._parentHeader;
|
|
345
|
+
const parentHeaderValue = parentHeader ?? this._parentHeader;
|
|
351
346
|
const message = KernelMessage.createMessage<KernelMessage.IErrorMsg>({
|
|
352
347
|
channel: 'iopub',
|
|
353
348
|
msgType: 'error',
|
|
@@ -371,8 +366,7 @@ export abstract class BaseKernel implements IKernel {
|
|
|
371
366
|
| KernelMessage.IHeader<KernelMessage.MessageType>
|
|
372
367
|
| undefined = undefined,
|
|
373
368
|
): void {
|
|
374
|
-
const parentHeaderValue =
|
|
375
|
-
typeof parentHeader !== 'undefined' ? parentHeader : this._parentHeader;
|
|
369
|
+
const parentHeaderValue = parentHeader ?? this._parentHeader;
|
|
376
370
|
const message = KernelMessage.createMessage<KernelMessage.IUpdateDisplayDataMsg>({
|
|
377
371
|
channel: 'iopub',
|
|
378
372
|
msgType: 'update_display_data',
|
|
@@ -396,8 +390,7 @@ export abstract class BaseKernel implements IKernel {
|
|
|
396
390
|
| KernelMessage.IHeader<KernelMessage.MessageType>
|
|
397
391
|
| undefined = undefined,
|
|
398
392
|
): void {
|
|
399
|
-
const parentHeaderValue =
|
|
400
|
-
typeof parentHeader !== 'undefined' ? parentHeader : this._parentHeader;
|
|
393
|
+
const parentHeaderValue = parentHeader ?? this._parentHeader;
|
|
401
394
|
const message = KernelMessage.createMessage<KernelMessage.IClearOutputMsg>({
|
|
402
395
|
channel: 'iopub',
|
|
403
396
|
msgType: 'clear_output',
|
|
@@ -423,8 +416,7 @@ export abstract class BaseKernel implements IKernel {
|
|
|
423
416
|
| KernelMessage.IHeader<KernelMessage.MessageType>
|
|
424
417
|
| undefined = undefined,
|
|
425
418
|
): void {
|
|
426
|
-
const parentHeaderValue =
|
|
427
|
-
typeof parentHeader !== 'undefined' ? parentHeader : this._parentHeader;
|
|
419
|
+
const parentHeaderValue = parentHeader ?? this._parentHeader;
|
|
428
420
|
const message = KernelMessage.createMessage<any>({
|
|
429
421
|
channel: 'iopub',
|
|
430
422
|
msgType: type,
|
|
@@ -55,8 +55,31 @@ export class NotebookExporter extends BaseExporter {
|
|
|
55
55
|
* @param path The path to the notebook
|
|
56
56
|
*/
|
|
57
57
|
async export(model: Contents.IModel, path: string): Promise<void> {
|
|
58
|
+
let content = '';
|
|
59
|
+
switch (model.format) {
|
|
60
|
+
case 'base64': {
|
|
61
|
+
const decoded = atob(model.content);
|
|
62
|
+
try {
|
|
63
|
+
// If it contains JSON, format it nicely
|
|
64
|
+
const parsed = JSON.parse(decoded);
|
|
65
|
+
content = JSON.stringify(parsed, null, 2);
|
|
66
|
+
} catch {
|
|
67
|
+
// If it's not JSON, just use decoded text
|
|
68
|
+
content = decoded;
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case 'json': {
|
|
73
|
+
content = JSON.stringify(model.content, null, 2);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case 'text': {
|
|
77
|
+
content = model.content;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
58
82
|
const mime = model.mimetype ?? 'application/json';
|
|
59
|
-
const content = JSON.stringify(model.content, null, 2);
|
|
60
83
|
this.triggerDownload(content, mime, path);
|
|
61
84
|
}
|
|
62
85
|
}
|
package/src/session/client.ts
CHANGED
|
@@ -157,6 +157,30 @@ export class LiteSessionClient implements ISessionAPIClient {
|
|
|
157
157
|
if (running) {
|
|
158
158
|
return running;
|
|
159
159
|
}
|
|
160
|
+
|
|
161
|
+
// Check if we should reuse an existing kernel (kernel.id takes precedence over name).
|
|
162
|
+
// Note: The Session.ISessionOptions type doesn't include kernel.id, but at runtime
|
|
163
|
+
// it may be passed (e.g., from SessionContext._changeKernel when sharing a kernel).
|
|
164
|
+
const requestedKernelId = (options.kernel as { id?: string } | undefined)?.id;
|
|
165
|
+
if (requestedKernelId) {
|
|
166
|
+
const existingSession = this._sessions.find(
|
|
167
|
+
(session) => session.kernel?.id === requestedKernelId,
|
|
168
|
+
);
|
|
169
|
+
if (existingSession) {
|
|
170
|
+
// Create a new session that shares the existing kernel
|
|
171
|
+
const id = UUID.uuid4();
|
|
172
|
+
const session: Session.IModel = {
|
|
173
|
+
id,
|
|
174
|
+
path,
|
|
175
|
+
name: name ?? path,
|
|
176
|
+
type: 'notebook',
|
|
177
|
+
kernel: existingSession.kernel,
|
|
178
|
+
};
|
|
179
|
+
this._sessions.push(session);
|
|
180
|
+
return session;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
160
184
|
const kernelName = options.kernel?.name ?? '';
|
|
161
185
|
const id = UUID.uuid4();
|
|
162
186
|
const nameOrPath = options.name ?? options.path;
|