Adds flat share items (Copy path, Upload to <name>, …) to Nautilus'
right-click menu for selected files. The registered share methods live in
methods/ and are auto-discovered — drop a new *.py file in
there, define a ShareMethod subclass, and a new entry appears in the menu.
(Items are deliberately flat, not a submenu: Nautilus 43+ uses
GtkPopoverMenu, where submenus push a new page and hide the parent menu.)
Built-in methods:
| Method | Description | Requires |
|---|---|---|
| Copy path | Copy absolute path(s) to clipboard | — |
| Copy file URI | Copy file://… URI(s) |
— |
| Upload to <name> | Upload to an HTTP endpoint via curl; multi-file selections are zipped first | curl + config |
Note on KDE Connect: KDE Connect ships its own Nautilus extension (
kdeconnect-share.py) that adds a "Send to device" entry, so we don't duplicate it here. Installkdeconnectseparately if you want that.
Targets GNOME Files (Nautilus) 43+ using the Nautilus 4.1 extension API and the GTK 4 stack.
Hard (extension must load at all):
| Distro | Package |
|---|---|
| Arch | sudo pacman -S nautilus-python |
| Debian/Ubuntu | sudo apt install python3-nautilus gir1.2-nautilus-4.0 libgtk-4-dev |
| Fedora | sudo dnf install python-nautilus gtk4-devel |
Optional (gates individual methods, the rest still works):
| Binary | Affects |
|---|---|
curl |
Web upload method |
dms |
Routes clipboard + notifications through the DMS shell (preferred); falls back to Gdk/notify-send if absent |
notify-send |
Notifications when dms is also absent (Gtk.MessageDialog as last resort) |
Without dms, the extension still works: clipboard uses Gdk.Clipboard and
notifications use notify-send (or a Gtk.MessageDialog if that's missing
too). When dms is on PATH, clipboard copies go through dms cl copy
(into DMS clipboard history) and notifications go through
dms ipc call toast <info|warn|error> <message> (DMS in-shell toast).
./install.shThe installer:
- Verifies
nautilus-pythonimports (refuses to install if not). Prints per-distro install hints on failure. - Warns (but continues) if any optional binary is missing, naming the affected method.
- Symlinks
share_menu.py,registry.py,methods/into~/.local/share/nautilus-python/extensions/. - Runs
nautilus -qso the running instance restarts with the new extension.
Reopen a Nautilus window, select a file, right-click → Copy path /
Upload to <name> / … Symlinking means source edits take effect the next
time Nautilus loads the extension (run nautilus -q to restart).
Web upload profiles live in ~/.config/nautilus-share/web-upload.conf
(TOML). A commented template is written there on first activation. After
editing, run nautilus -q and reopen a window.
[[profile]]
name = "Personal transfer" # menu label
url_template = "https://transfer.example.com/{filename}" # one curl per file
method = "put" # post (multipart) or put
headers = { Authorization = "Bearer YOURTOKEN" }
follow_redirects = true
insecure = false
timeout = 120
max_days = 5 # sends "Max-Days: 5" (expiry; omit to skip)
max_downloads = 1 # sends "Max-Downloads: 1" (omit to skip)
zip_multi = true # zip multi-file selections into one archive
copy = true # copy extracted result to clipboard
notify = true # desktop notification on success
# Keep plain keys ABOVE this header — in TOML, keys written after a table
# header belong to that table, so a `copy = false` placed below
# [profile.response] would be silently ignored.
[profile.response]
mode = "json_key" # body | line | json_key
json_key = "url" # nested keys OK: "data.url"Instead of url_template you can set a static url
(e.g. url = "https://bin.example.com/upload" with method = "post" and
field = "filedata"): all files then go out in a single multipart POST.
Set exactly one of the two. Each [[profile]] becomes its own flat
Upload to <name> menu entry.
All options:
| Key | Default | Meaning |
|---|---|---|
name |
required | Menu label (Upload to <name>) |
url |
— | Static endpoint; all files in one request |
url_template |
— | Per-file endpoint; {filename} is replaced with each file's URL-encoded basename (transfer.sh style) |
method |
"post" |
post (multipart form) or put |
field |
"file" |
Multipart field name (POST only) |
headers |
{} |
Extra headers table; fed to curl on stdin so tokens never appear in /proc/*/cmdline |
follow_redirects |
true |
curl -L |
insecure |
false |
curl -k |
timeout |
120 |
curl --max-time, seconds |
max_days |
unset | Sends Max-Days: <n> (transfer.sh-style expiry); omitted when unset |
max_downloads |
unset | Sends Max-Downloads: <n>; omitted when unset |
zip_multi |
true |
Zip multi-file selections into one archive first |
copy |
true |
Copy the extracted result to the clipboard |
notify |
true |
Desktop notifications for progress/success/failure |
response.mode |
"body" |
What to extract from the response (see below) |
response.json_key |
— | Key to pull when mode = "json_key" |
Multi-file selections are zipped into one archive before uploading
(named after the files' parent directory, e.g. selecting three files in
~/Pictures uploads Pictures.zip), so you share a single link. Zipping
happens off the UI thread and the temp archive is deleted after the upload.
Set zip_multi = false to upload files individually instead — note that
method = "put" with a static url only supports single files.
response.mode:
body— entire curl stdout → clipboardline— first non-empty stdout line → clipboardjson_key— parse response as JSON; pulljson_key(dotted path OK) → clipboard
Results and failures. On success the extracted result (usually the share link) is copied to the clipboard and a notification confirms it. On failure you get an error notification with the first curl error. If some of several per-file uploads fail, the URLs that did succeed are still copied and the error notice says how many made it.
Create methods/my_thing.py:
"""Share via Foo."""
from registry import ShareMethod, register
@register
class FooMethod(ShareMethod):
name = "Send via Foo"
icon = "document-send"
def available(self) -> bool:
# Return False when your backend isn't online and Nautilus will
# quietly hide you from the menu.
return True
def share(self, files, ctx) -> None:
for f in files:
path = f.get_location().get_path()
# ...do stuff...
if ctx.notify:
ctx.notify("Sent via Foo", "info")
if ctx.copy_to_clipboard:
ctx.copy_to_clipboard("…")Restart Nautilus (nautilus -q) and it shows up. Methods whose
available() returns False are silently skipped so a missing backend
doesn't clutter the menu.
files is a list of Nautilus.FileInfo. Each has .get_location()
returning a Gio.File (.get_path() → native string).
To expose several menu entries from one method (like Web upload's one
entry per profile), override menu_entries(files) to return
(label, icon, payload) tuples — the payload of whichever entry the user
clicks is what share(payload, ctx) receives. The default implementation
returns a single entry whose payload is the file list itself.
Long-running work must avoid blocking the GTK main loop — and must not
fork() from a Python worker thread (subprocess in a thread can corrupt
the multi-threaded Nautilus process). Use Gio.Subprocess with async
callbacks for external commands (see methods/web_upload.py); pure-Python
work is fine in a threading.Thread with GTK calls routed back through
GLib.idle_add (the built-in ctx.notify / ctx.copy_to_clipboard
already do this).
rm -f ~/.local/share/nautilus-python/extensions/{share_menu.py,registry.py}
rm -rf ~/.local/share/nautilus-python/extensions/methods
nautilus -qextensions/
├── share_menu.py # Nautilus.MenuProvider entry point (loaded by Nautilus)
├── registry.py # ShareMethod ABC + auto-discovery of methods/
├── methods/
│ ├── copy_path.py # copy absolute path / file:// URI
│ └── web_upload.py # async curl to configured HTTP endpoints (TOML
│ # config; zips multi-file selections, Max-Days/
│ # Max-Downloads expiry headers)
├── install.sh
└── README.md
- No share items in the menu. Run
nautilus -qfrom a terminal and reopen a window — Nautilus only re-scans extensions on startup. Tail~/.xsession-errorsorjournalctl --user -efor Python tracebacks at load time. - A method is missing. Its
available()returned False (curl not installed, no[[profile]]inweb-upload.conf). Edit config / install the binary and restart Nautilus. - No "Upload to …" entries.
~/.config/nautilus-share/web-upload.confis still the commented template — uncomment a[[profile]]block with a realurlorurl_template.