Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
pip uninstall -y "jupyterlab_open_url_parameter" jupyterlab

- name: Upload extension packages
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v6
with:
name: extension-artifacts
path: dist/jupyterlab_open_url_parameter*
Expand All @@ -60,7 +60,7 @@ jobs:
with:
python-version: '3.9'
architecture: 'x64'
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v6
with:
name: extension-artifacts
- name: Install and Test
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/check-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Upload Distributions
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v6
with:
name: jupyterlab_open_url_parameter-releaser-dist-${{ github.run_number }}
path: .jupyter_releaser_checkout/dist
4 changes: 2 additions & 2 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
version: 2

build:
os: "ubuntu-20.04"
os: "ubuntu-lts-latest"
tools:
python: "mambaforge-4.10"
python: "mambaforge-latest"

conda:
environment: docs/environment.yml
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ Which will result in the following URL when JupyterLab is running locally:

http://localhost:8888/lab?fromURL=https://raw.githubusercontent.com/jupyterlab/jupyterlab-demo/master/data/iris.csv&fromURL=https://raw.githubusercontent.com/jupyterlab/jupyterlab-demo/master/notebooks/Lorenz.ipynb

To place downloaded files in a specific destination, use `fromURLToFolder` in the URL.
Missing directories are created automatically.

For example:

http://localhost:8888/lab?fromURL=https://raw.githubusercontent.com/jupyterlab/jupyterlab_apod/cffae6a4049af97436f0c19a0dae65a574f74390/src/index.ts&fromURLToFolder=step-05

With multiple `fromURL` values, the same `fromURLToFolder` destination is used for all files.

https://user-images.githubusercontent.com/591645/230422671-c12761e9-9b9f-4d23-ab66-344568c6b0a5.mp4

ℹ️ This extension uses the command `filebrowser:open-url` available in JupyterLab by default.
Expand Down
12 changes: 4 additions & 8 deletions docs/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@ name: jupyterlab-open-url-parameter
channels:
- conda-forge
dependencies:
- build
- python=3.10
- mamba
- pydata-sphinx-theme
- myst-parser
- jupyterlab>=3.5.0,<3.6
- nodejs=18
- jupyterlite>=0.1.0,<0.2
- pip:
- jupyterlite-sphinx
- ..
- jupyterlab>=4.0.0,<5
- nodejs=22
- jupyterlite-core>=0.7.0,<0.8
- jupyterlite-sphinx
86 changes: 79 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,70 @@ const plugin: JupyterFrontEndPlugin<void> = {

const urlParams = new URLSearchParams(search);
const paramName = 'fromURL';
const folderParamName = 'fromURLToFolder';
const paths = urlParams.getAll(paramName);
if (!paths || paths.length === 0) {
if (paths.length === 0) {
return;
}
const urls = paths.map(path => decodeURIComponent(path));
const urls = paths;
const folder = urlParams.get(folderParamName) ?? '';
Comment thread
krassowski marked this conversation as resolved.
Outdated
const normalizedFolder = folder ? PathExt.normalize(folder) : '';
const trimmedFolder =
normalizedFolder !== '/' && normalizedFolder.endsWith('/')
? normalizedFolder.slice(0, -1)
: normalizedFolder;
const folderPath = PathExt.removeSlash(trimmedFolder);
const uploadDirectory = normalizedFolder
? folderPath === '.'
? ''
: folderPath
: '';
Comment thread
MUFFANUJ marked this conversation as resolved.

// handle the route and remove the fromURL parameter
const handleRoute = () => {
const url = new URL(URLExt.join(PageConfig.getBaseUrl(), request));
// only remove the fromURL parameter
// only remove parameters handled by the extension
url.searchParams.delete(paramName);
url.searchParams.delete(folderParamName);
const { pathname, search } = url;
router.navigate(`${pathname}${search}`, { skipRouting: true });
};

const ensureDirectory = async (directory: string): Promise<void> => {
if (!directory) {
return;
}

const contents = app.serviceManager.contents;
let currentPath = '';
for (const part of directory.split('/').filter(Boolean)) {
currentPath = currentPath ? PathExt.join(currentPath, part) : part;
try {
const model = await contents.get(currentPath, { content: false });
if (model.type !== 'directory') {
throw new Error(
trans.__('Path is not a directory: %1', currentPath)
);
}
} catch (reason) {
const error = reason as any;
if (error?.response?.status !== 404) {
throw reason;
}
try {
await contents.save(currentPath, {
type: 'directory'
});
} catch (saveReason) {
const saveError = saveReason as any;
if (saveError?.response?.status !== 409) {
throw saveReason;
}
}
}
}
};

// fetch the file from the URL and open it with the docmanager
const fetchAndOpen = async (url: string): Promise<void> => {
let type = '';
Expand All @@ -84,11 +133,14 @@ const plugin: JupyterFrontEndPlugin<void> = {
try {
// FIXME: handle Content-Disposition: https://github.com/jupyterlab/jupyterlab/issues/11531
const name = PathExt.basename(url);
const file = new File([blob], name, { type });
const model = await browser?.model.upload(file);
const model = await browser?.model.upload(
new File([blob], name, { type })
);

if (!model) {
return;
}

return commands.execute('docmanager:open', {
path: model.path,
options: {
Expand All @@ -103,17 +155,37 @@ const plugin: JupyterFrontEndPlugin<void> = {
}
};

const openUrls = async (targets: string[]): Promise<void> => {
const currentDirectory = browser?.model.path ?? '';

if (uploadDirectory && browser) {
await ensureDirectory(uploadDirectory);
await browser.model.cd(uploadDirectory);
}

try {
for (const url of targets) {
await fetchAndOpen(url);
}
} finally {
if (uploadDirectory && browser) {
await browser.model.cd(currentDirectory);
void browser.model.refresh();
}
}
Comment thread
MUFFANUJ marked this conversation as resolved.
Outdated
};

const [match] = matches;
// handle opening the URL with the Notebook 7 separately
if (match?.includes('/notebooks') || match?.includes('/edit')) {
const [first] = urls;
await fetchAndOpen(first);
await openUrls([first]);
handleRoute();
return;
}

app.restored.then(async () => {
await Promise.all(urls.map(url => fetchAndOpen(url)));
await openUrls(urls);
handleRoute();
});
}
Expand Down
Loading