jupyterlab_share_files_extension
Peer-to-peer file sharing for JupyterLab. Create a share (file drop) or request (inbox) from a side panel, copy the link - recipients open it in their own JupyterLab panel or any plain browser.
Screenshots
The Share Files panel and the create-share dialog with optional password:
| Side panel | New share with password |
|---|---|
The standalone page recipients see in any browser - download view, upload view (dark theme), password gate. System theme by default, Light / Dark / Auto switch:
Features
- Shares - read-only drops of files and folders; recipients download
- Requests - inboxes; recipients upload, organised per uploader
- Per-uploader identity - request uploaders get a server-issued short hash in a browser cookie; the page shows only their own uploads with add/remove control; the owner panel shows
name (hash)so several "anonymous" uploaders stay distinct - Connections - paste someone's link to subscribe to their share or upload to their request
- Drag-and-drop from the file browser - drop zone (new share), share row (add files), request row (upload)
- Browse inside a share - double-click a folder to drill in; the
..row goes back up - Open files directly - double-click a file in the panel, JupyterLab opens it with the right viewer
- Hover for details - hovering a file or folder row shows a tooltip with its full name, path, size and modified date
- Copy/paste between the panel and the file browser
- Right-click context menu - file browser ("Share Files...") and panel rows ("Copy to Current Folder", "Show in File Browser"); Tab reaches a share, request or connection row, its Expand/Collapse button and a connected peer's entries, Enter on an entry opens it (a folder drills in, the
..row goes up), and Shift+F10 or the ContextMenu key opens its menu - Optional password protection - set at creation or later (right-click → Set Password); recipients unlock before any access; one-click xkcdpass passphrase generation; link dialog shows the password with a copy button; attempts rate limited server-side
- Hidden files visible by default - dotfiles like
.env,.gitignoreare shareable; toggle in Settings - Standalone HTML page - link works in any browser, no JupyterLab needed; Light / Dark / Auto theme
- QR code in the share-link dialog for scanning from a phone (right-click copies the image), plus a copy icon embedded in the link field to grab the link again on demand
- Live upload notifications when someone uploads to your request
- Large files stay off the heap - every transfer is written or read as it moves: a share is served chunk by chunk and a folder's zip is built into the response file by file; an upload from the recipient page streams to a spool file on disk (raw body, name in an
X-Filenameheader - the same shape the galaxahub fileshare service takes) with no size limit but the disk's free space; an upload from the panel to a connected request streams from the workspace file; a download from a connected share is relayed to the browser while it is still arriving, and the browser saves it straight to disk - Self-connect guard - pasting your own link shows a "you already own this" dialog
- Symlink-friendly - sharing
@shared/...and similar works - Delete to trash - panel deletes go to the OS trash by default (
c.ShareFilesConfig.use_trash) - HTTPS-aware links - share URLs follow the scheme the browser is on
- Cloudflare tunnel sharing - optional public links beyond your network; cloud icon in the panel header shows state, toggles public/private, opens setup when unconfigured (docs/cloudflare_setup.md)
- Hub mode - on a lab spawned by galaxahub the panel works only through the hub's fileshare API and the lab mounts no unauthenticated route; the hub stages and serves the files (docs/design-hub-public-zone.md)
- Settings toggles - shares, requests, hidden-file visibility, poll interval, download limit
Requirements
- JupyterLab >= 4.0.0
- Python >= 3.9
Install
Developers (project Makefile):
make install
End-users (PyPI):
pip install jupyterlab_share_files_extension
Configuration
Optional, in jupyter_server_config.py:
c.ShareFilesConfig.shares_dir = "uploads" # default - relative to the notebook root
c.ShareFilesConfig.use_trash = True # default: True
c.ShareFilesConfig.verify_peer_tls = True # default: True
c.ShareFilesConfig.password_max_attempts_per_minute = 30 # default: 30
c.ShareFilesConfig.password_attempt_cooldown_seconds = 1 # default: 1
shares_dir- storage for shares/requests/connections; relative paths resolve against the notebook root; created only when you first create a share or request or connect to a link, so the folder never appears in a workspace that has not shared anything; must resolve inside the notebook root; otherwise the server extension logs an error and registers no routes - JupyterLab still starts, and every panel call answers 404use_trash-Falsedeletes permanently instead of moving to the OS trashverify_peer_tls- setFalsefor peers with self-signed certificates; otherwise server-side saves/uploads to them fail with a 502password_max_attempts_per_minute/password_attempt_cooldown_seconds- per-resource rate limiting of password attempts (limitslibrary); generous defaults (30/minute, 1s); lower the cap or raise the cooldown to hardenpollIntervalSeconds- panel refresh interval, Settings Editor → Share Files (default 15, minimum 2)tunnelAutostart- Settings Editor → Share Files (default off); bring the Cloudflare tunnel up at server startup - off, the server starts with private links and the cloud icon switches the tunnel on demandpeerDownloadMaxGb- Settings Editor → Share Files (1, 2, 5, 10, 20, 50 or 100 GB; default 10); the largest download from a connected share - a save into the workspace, unpacked, or one item handed to the browser; the server refuses a larger one with a 502 naming the limit and leaves nothing behind; the download lands on the workspace disk as it arrives (a spool file under the store'stmpfolder), never in the server's memory, and may take 300 s for every GB of the limit; an item handed to the browser is relayed as it arrives and saved by the browser itself, so no memory bounds that path; the CLI'spick-upsends no limit and gets the default
CLI
jupyterlab_share_files - the panel's operations as subcommands; a thin client over the same authenticated HTTP API, for scripts and AI agents. Human-readable output by default, --json for machine-readable.
SHARE_FILES_BASE_URL- base URL of the Jupyter server; on JupyterHub this must be the public user URL (e.g.https://hub.example.com/user/<name>/) so links carry the public host; falls back toJUPYTER_SERVER_URLSHARE_FILES_TOKEN- Jupyter/JupyterHub API token; falls back toJUPYTERHUB_API_TOKEN/JUPYTER_TOKENSHARE_FILES_INSECURE-1skips TLS verification (self-signed certificates); off by default
jupyterlab_share_files list-items
jupyterlab_share_files create-share <name> [paths...] [--password PW | --generate-password]
jupyterlab_share_files create-request <name> [--password PW | --generate-password]
jupyterlab_share_files add-files <share-id> <paths...>
jupyterlab_share_files remove-files <share-id> <names...>
jupyterlab_share_files remove-upload <request-id> <uploader-hash> <name>
jupyterlab_share_files set-password <share|request> <id> [PW] [--generate] [--clear]
jupyterlab_share_files generate-password
jupyterlab_share_files connect <link>
jupyterlab_share_files disconnect <key>
jupyterlab_share_files close-share <id>
jupyterlab_share_files close-request <id>
jupyterlab_share_files pick-up <key> [names...] [--target-dir DIR]
jupyterlab_share_files send-to-request <key> <paths...> [--uploader NAME]
jupyterlab_share_files list-request-uploads <id>
jupyterlab_share_files install-claude-skill
install-claude-skill installs the bundled Claude skill (a usage guide for this CLI) into ~/.claude/skills/jupyterlab_share_files/, asking for confirmation before writing.
Cloudflare tunnel sharing
The cloudflare command exposes share/request links beyond the hub or local network through a Cloudflare tunnel. Chosen for security: outbound-only connector (no inbound port), HTTPS enforced at the edge, and path-restricted ingress - only the extension's /public/... endpoints are routable; everything else answers 404 at the edge. Full guide: docs/cloudflare_setup.md.
setup --token <T> --account-id <A> --hostname <H> --private-base-url <URL>- save credentials (chmod-600 config) and provision end to end: create/reuse the tunnel (deterministic nameshare-files-<sluggified private base URL>), route the hostname, add a proxied CNAME, enforce HTTPS, savepublic_base_url, start the connector;--private-base-urlis required and must behttpsvalidate- verify every component of the saved config: config completeness, URL sanity, token validity, tunnel existence/status/name on Cloudflare, proxied CNAME, ingress rule,cloudflaredbinary on PATH, daemon/toggle stateinfo- current configuration; tokens masked to last 4 characters,tunnel_active,daemon_running, Cloudflare-sidetunnel_statusstart/stop- switch between public links (daemon running) and private links; credentials, tunnel and DNS kept; effective on the next request, no restartreset- clear the saved token and derived state; links revert to the local/hub address; Cloudflare-side resources untouched- Connector supervision - the extension keeps
cloudflared tunnel runalive, retrying up toc.ShareFilesConfig.cloudflared_retriestimes (default 3); autostart is a user setting (default off) - Cloud icon - panel header, always visible: green filled = tunnel on (public links), dim dashed = off/unconfigured (private links), blinking blue = switching on or off; click, Enter or Space toggles, or opens the setup popup when unconfigured; a switch on whose connector does not come up is refused with
cloudflared did not start - see <connector log>and links stay private - Reachability check - when the tunnel is active, the link dialog probes the public link server-side (
api/link-check; a frontend fetch would be blocked by CORS) and shows reachable/not reachable; configured-but-off shows "Cloudflare sharing is not running" instead - Link rewrite - the server reads
public_base_urland the toggle per request and rewrites only scheme+host; the path stays auto-detected; without config, links keep the browser's host - Token policies required -
Account → Cloudflare Tunnel → Editplus zone-scopedDNS → Editfor the hostname's domain
jupyterlab_share_files cloudflare setup --token <api-token> --account-id <account-id> \
--hostname share.example.com --private-base-url "https://hub.example.com/user/<name>/"
jupyterlab_share_files cloudflare validate
jupyterlab_share_files cloudflare info
jupyterlab_share_files cloudflare start
jupyterlab_share_files cloudflare stop
jupyterlab_share_files cloudflare reset
Hub mode
A lab spawned by galaxahub carries SHARE_FILES_PUBLIC_ZONE=hub, the path of the hub's fileshare API in SHARE_FILES_HUB_API and its own JUPYTERHUB_API_TOKEN. The extension then registers only its authenticated api/* routes - no public/*, no static/* - and every panel action is a call to the hub. Design: docs/design-hub-public-zone.md.
- Shares are snapshots - a share names workspace paths; the hub copies the bytes with its own transfer job and the row shows
staginguntil the copy lands, orrefusedwith the hub's reason - Recipients reach the hub - the link is the hub's page; the lab answers 404 on its recipient paths
- Uploads are fetched in - each upload under a request offers "Fetch to current folder"; the hub copies it into a fresh folder under the file browser's current directory
- Cloud icon - every share and request carries its own Cloudflare switch on the hub; the header icon flips them all and sets the default for the next one, a row's context menu flips one. On, the link carries the hub's Cloudflare address; off, the hub's own address, reachable on the hub's network only. No tunnel runs on the lab, and the hub runs its tunnel only while some record is switched on. The header icon alone shows the state - rows carry no cloud mark
- Switch on waits for the hub - the hub's tunnel registers some seconds after the first record is switched on and the hub sends no change for it, so the lab reads the hub's items at most every 5s until each record switched on carries the tunnel address, then tells every open panel to fetch; the header icon blinks meanwhile, and a click on it ends the wait and switches off. Not confirmed within 120s, the lab switches those records and the default back off and the panel warns that links stay on the hub network; a hub that stops answering during the wait and comes back (a redeployment) gets the 120s again from its first answer, once. A switch back off that fails leaves the default as it is, and the panel says the hub could not be reached, or, when the hub answered with an error, that the hub did not switch Cloudflare off. After a timeout the header icon shows off, with the reason in its tooltip, until the next switch on.
api/infoandapi/tunnelreport the wait astunnel_waitingand the timeout astunnel_reason: cloud_not_confirmed, orhub_unavailableorcloud_not_switched_offwhen the switch back off failed - Link check - for a link on the Cloudflare hostname the link dialog asks the lab server to open it and shows what answered; a link on the hub's own address is not checked and the dialog says it works on the hub's network only, or, for a record switched on while the lab waits for the hub, that it moves to the Cloudflare hostname once the hub confirms. The answer is reachable on HTTP 200, otherwise the HTTP status,
no answer within 10 s,connection refused,no address for the host name,a TLS error,a closed connectionora network error, and the address opened only when it differs from the link shown; the hub's cached serving verdict plays no part - Live panel - the panel holds one change stream to the lab, which holds one to the hub: a new upload, a share turning ready or refused, a close or an expiry shows without polling; an older hub without the stream puts the panel back on its timer
- Grants - what the panel may create comes from the hub; a refused kind is greyed out with the reason; a group that requires a password gets a create dialog with the password field required and pre-filled
- Not available - peer connections, adding files to an existing share, removing a single upload, per-user Cloudflare setup
- Fail closed - a lab in hub mode with an incomplete contract reports the hub unavailable; it never falls back to serving recipients itself
Security
- The link is the credential - 40 bits of entropy, no expiry; share over trusted channels
- Optional password as a second factor - unlock token bound to the password, so changing it instantly locks out everyone holding the old one
- Brute-force protection - per-resource rate limiting (
limitslibrary, in-memory): per-minute cap plus mandatory cooldown, both tunable (defaults 30/minute, 1s) - HTTPS inherited from your JupyterHub/Jupyter proxy
- Cloudflare exposure is HTTPS-only and limited to the
/public/...capability endpoints; the hub login, authenticated APIs and the private network stay unreachable - Connector token passed via the
TUNNEL_TOKENenvironment variable, never on the command line - cannot leak throughps//proc
Availability of a link
A link is served by the JupyterLab server that created it, so it works only while that server is running.
- Owner's server stopped - the link stops answering; on JupyterHub the idle culler stops unused servers, so a link can go dead without anyone touching it
- In the panel a connected peer then shows
offline; hover the badge for the reason - the panel's own server reads the peer, so the badge names the peer's answer (stopped server, removed record, rejected password), and a page policy such asdefault-src 'self'cannot stop the read - For recipients the page cannot load until the owner's server is back
Releases
Versioned releases ship to npm and PyPI together, tagged RELEASE_v<version> on the GitHub releases page. Full delivered feature list: RELEASE.md; per-version changes: CHANGELOG.md.
Uninstall
pip uninstall jupyterlab_share_files_extension
Release files for jupyterlab-share-files-extension 1.2.47
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jupyterlab_share_files_extension-1.2.47.tar.gz | 755.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jupyterlab_share_files_extension-1.2.47-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.0 MB