Skip to content

Commit 81d5da0

Browse files
Purge sharing-specific metadata before downloading notebooks as IPyNB files (#238)
1 parent 3fa176e commit 81d5da0

File tree

2 files changed

+60
-3
lines changed

2 files changed

+60
-3
lines changed

src/index.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,37 @@ const plugin: JupyterFrontEndPlugin<void> = {
192192
commands.addCommand(Commands.downloadNotebookCommand, {
193193
label: 'Download as a notebook',
194194
execute: args => {
195-
// Execute the built-in download command
195+
// Clear all sharing-specific metadata before download
196+
const panel = readonlyTracker.currentWidget ?? tracker.currentWidget;
197+
198+
if (!panel) {
199+
console.warn('No active notebook to download');
200+
return;
201+
}
202+
203+
const content = panel.context.model.toJSON() as INotebookContent;
204+
205+
// Remove sharing-specific metadata
206+
const purgedMetadata = { ...content.metadata };
207+
delete purgedMetadata.isSharedNotebook;
208+
delete purgedMetadata.sharedId;
209+
delete purgedMetadata.readableId;
210+
delete purgedMetadata.sharedName;
211+
delete purgedMetadata.lastShared;
212+
213+
// Ensure that we preserve kernelspec metadata if present
214+
const kernelSpec = content.metadata?.kernelspec;
215+
if (kernelSpec) {
216+
purgedMetadata.kernelspec = kernelSpec;
217+
}
218+
219+
const cleanedContent: INotebookContent = {
220+
...content,
221+
metadata: purgedMetadata
222+
};
223+
panel.context.model.fromJSON(cleanedContent);
224+
225+
// Execute the built-in download command with the cleaned model
196226
return commands.execute('docmanager:download');
197227
}
198228
});
@@ -427,7 +457,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
427457
delete purgedMetadata.sharedName;
428458
delete purgedMetadata.lastShared;
429459

430-
// Ensure that we preserve kernelspec metadata
460+
// Ensure that we preserve kernelspec metadata if present
431461
const kernelSpec = originalContent.metadata?.kernelspec;
432462

433463
// Remove cell-level editable=false; as the notebook has

ui-tests/tests/jupytereverywhere.spec.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ const PYTHON_TEST_NOTEBOOK: JSONObject = {
3333
execution_count: null,
3434
id: '55eb9a2d-401d-4abd-b0eb-373ded5b408d',
3535
outputs: [],
36-
metadata: {},
36+
metadata: {
37+
sharedId: 'some-random-alphanumeric-id',
38+
readableId: 'python-test-notebook',
39+
sharedName: 'Notebook_1980-10-30_00-10-20',
40+
lastShared: '2024-06-20T00:10:20.123Z'
41+
},
3742
source: [`# This is a test notebook`]
3843
}
3944
],
@@ -316,6 +321,28 @@ test.describe('Download', () => {
316321
const pdfPath = await (await pdfDownload).path();
317322
expect(pdfPath).not.toBeNull();
318323
});
324+
325+
test('Notebook downloaded as IPyNB should not have sharing-specific metadata', async ({
326+
page,
327+
context
328+
}) => {
329+
await mockTokenRoute(page);
330+
await mockShareNotebookResponse(page, 'test-download-metadata-notebook');
331+
332+
const ipynbDownload = page.waitForEvent('download');
333+
await runCommand(page, 'jupytereverywhere:download-notebook');
334+
const ipynbPath = await (await ipynbDownload).path();
335+
expect(ipynbPath).not.toBeNull();
336+
337+
const content = await fs.promises.readFile(ipynbPath!, { encoding: 'utf-8' });
338+
const notebook = JSON.parse(content) as JSONObject;
339+
const metadata = notebook['metadata'] as JSONObject | undefined;
340+
expect(metadata).toBeDefined();
341+
expect(metadata).not.toHaveProperty('sharedId');
342+
expect(metadata).not.toHaveProperty('readableId');
343+
expect(metadata).not.toHaveProperty('sharedName');
344+
expect(metadata).not.toHaveProperty('lastShared');
345+
});
319346
});
320347

321348
// We take a screenshot of the full files page, to

0 commit comments

Comments
 (0)