From bb215ffd8b363635d826f7cd368f59b82c108295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Dvo=C5=99=C3=A1k?= <150935816+Dvorinka@users.noreply.github.com> Date: Mon, 21 Sep 2026 09:10:47 +0200 Subject: [PATCH] feat(desktop): share dialog on Linux/macOS via cloudreve://share deeplink (#240) * feat(desktop): share dialog on Linux/macOS via cloudreve://share deeplink The Explorer context menu is Windows-only; the share dialog itself is cross-platform but had no trigger elsewhere. cloudreve://share/ now routes through the same ManagerCommand::ShareLink pipeline, and desktop/linux/ ships a cloudreve-share helper plus Nautilus script and Dolphin service-menu glue so file managers can reach it. OAuth callback deeplinks are unaffected. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(desktop): don't drop share deep links at startup Queue cloudreve://share/ links arriving before APP_STATE init and drain after the sync service is up; also scan argv for a cloudreve:// URL so a cold start launched by the file manager still opens the share dialog. Nautilus script now reads NAUTILUS_SCRIPT_SELECTED_FILE_PATHS per line so paths with spaces survive. * feat(desktop): macOS share helper + file-manager install docs cloudreve-share for macOS (open cloudreve://share/...) plus Shortcuts Quick Action recipe for Finder. READMEs document Nautilus, Dolphin, and Finder installation. --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- ROADMAP.md | 2 +- desktop/linux/README.md | 35 ++++++++++ desktop/linux/cloudreve-share | 16 +++++ desktop/linux/kde/cloudreve-share.desktop | 15 ++++ .../linux/nautilus/scripts/Cloudreve Share | 10 +++ desktop/macos/README.md | 25 +++++++ desktop/macos/cloudreve-share | 15 ++++ desktop/src-tauri/src/lib.rs | 70 +++++++++++++++++-- 8 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 desktop/linux/README.md create mode 100755 desktop/linux/cloudreve-share create mode 100644 desktop/linux/kde/cloudreve-share.desktop create mode 100755 desktop/linux/nautilus/scripts/Cloudreve Share create mode 100644 desktop/macos/README.md create mode 100755 desktop/macos/cloudreve-share diff --git a/ROADMAP.md b/ROADMAP.md index 3355f0b8..46fb6321 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -252,7 +252,7 @@ Goal: Windows + macOS + Linux from the `desktop/` tree in this repo. - [x] Release pipelines for all four artifacts, all verified locally — server: `release.yml` + goreleaser on `v*` tags (12 binaries linux/win/darwin/freebsd + ghcr docker amd64/arm64/slim + manifests; snapshot build verified); desktop: `desktop-release.yml` on `desktop-v*` tags (.msi/.exe, .dmg, .deb/.AppImage — local `cargo tauri build` produced both Linux bundles, `.deb` declares `fuse3` for on-demand mode, `libfuse2`+`fuse3` added to runner deps for the AppImage toolchain); android: new `android-release.yml` on `android-v*` tags ships signed release APK + AAB (env-keystore via `ANDROID_KEYSTORE_*` secrets, debug-cert fallback so APKs sideload; `lintVitalRelease` caught a real missing `dataSync` foregroundServiceType on `SystemForegroundService` — fixed in manifest, would have crashed API 34+); `tauri-plugin-http` pinned `~2.6` + npm `@tauri-apps/plugin-http@^2.6.1` to clear the version-mismatch guard that blocked every desktop release build - [x] #167 (upstream desktop#49) — online-only thumbnails missing in Explorer: root cause was a client/server contract mismatch — the CE `/file/thumb` response carries only `url`/`expires` while the `cloudreve-api` model required `obfuscated`, failing deserialization on every thumbnail request (`E_FAIL` to Explorer; hydrated files were unaffected since Windows thumbs them locally). `obfuscated` is now `#[serde(default)]`; the decode path still runs when a server emits the flag - [x] Linux on-demand hydration via FUSE (`fuser`) — new "On-demand" sync mode in Add Drive (Linux-only picker): remote tree projects virtually into a FUSE mount at the configured path backed by inventory; files hydrate into a private store (`~/.cloudreve/fuse-store/`) on open with per-path serialization, sibling tmp dir + atomic rename, zero kernel cache TTLs; the watcher/sync engine operate on the store so hydrated edits sync normally; remote refreshes evict stale hydrated copies (event-blocked so no remote delete); engine writes gated by `local_updated_at`/`local_size` snapshots so downloads/hydrations never boomerang into uploads; `unlink`/`rename` on virtual files route through synthesized events + `MountCommand::Rename`; full-sync mode unchanged on Linux/macOS; covered by a real kernel-mount integration test (`tests/fuse_mount.rs`) -- [x] #160 (upstream desktop#34) — FileCloud-style quick share: Explorer "Share link…" submenu item opens a share dialog window (new `ManagerCommand::ShareLink` → `OpenShareDialog` event → dedicated `share` webview window + `/share` route) with permission mode (view+download / preview-only / edit / folder upload-only), expiry, password, and download limit mapped to the full `PUT /share` surface (`ShareCreateService` gained the option fields); "Copy share link" stays as the one-click public-link action alongside it +- [x] #160 (upstream desktop#34) — FileCloud-style quick share: Explorer "Share link…" submenu item opens a share dialog window (new `ManagerCommand::ShareLink` → `OpenShareDialog` event → dedicated `share` webview window + `/share` route) with permission mode (view+download / preview-only / edit / folder upload-only), expiry, password, and download limit mapped to the full `PUT /share` surface (`ShareCreateService` gained the option fields); "Copy share link" stays as the one-click public-link action alongside it; on Linux/macOS the same dialog is reachable via the `cloudreve://share/` deep link — `desktop/linux/` ships a `cloudreve-share` helper plus Nautilus script and Dolphin service-menu definitions ## 7. Phase E — Android app (native, no iOS) diff --git a/desktop/linux/README.md b/desktop/linux/README.md new file mode 100644 index 00000000..c6557ebd --- /dev/null +++ b/desktop/linux/README.md @@ -0,0 +1,35 @@ +# Linux file-manager integration + +`cloudreve-share` opens the share dialog for a local path via the +`cloudreve://share/` deep link — the same dialog the Windows Explorer +context menu opens. Works whether the app is running or not. + +## Helper + +```sh +cp cloudreve-share ~/.local/bin/ +chmod +x ~/.local/bin/cloudreve-share +cloudreve-share ~/Cloudreve/report.pdf +``` + +## GNOME / Nautilus + +```sh +mkdir -p ~/.local/share/nautilus/scripts +cp "nautilus/scripts/Cloudreve Share" ~/.local/share/nautilus/scripts/ +chmod +x ~/.local/share/nautilus/scripts/"Cloudreve Share" +``` + +Right-click a file → **Scripts** → **Cloudreve Share**. + +## KDE / Dolphin + +```sh +mkdir -p ~/.local/share/kio/servicemenus # Dolphin ≥ 22 +cp kde/cloudreve-share.desktop ~/.local/share/kio/servicemenus/ +``` + +Right-click a file → **Cloudreve** → **Share link…**. Restart Dolphin if the +entry does not appear. + +Requires `python3` and `xdg-open` (both standard on desktop Linux). diff --git a/desktop/linux/cloudreve-share b/desktop/linux/cloudreve-share new file mode 100755 index 00000000..6e88acb0 --- /dev/null +++ b/desktop/linux/cloudreve-share @@ -0,0 +1,16 @@ +#!/bin/sh +# cloudreve-share — open the Cloudreve share dialog for local file(s). +# Invoked by the Nautilus script and Dolphin service menu; also usable +# directly: `cloudreve-share ~/CloudDrive/report.pdf`. +# +# Works whether the app is running or not — the packaged .desktop file +# registers x-scheme-handler/cloudreve, and the app processes share deep +# links passed on its own command line at cold start. +# Requires python3 for URL encoding. + +set -eu + +for f in "$@"; do + encoded=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1]))' "$f") + xdg-open "cloudreve://share/$encoded" & +done diff --git a/desktop/linux/kde/cloudreve-share.desktop b/desktop/linux/kde/cloudreve-share.desktop new file mode 100644 index 00000000..48f374c8 --- /dev/null +++ b/desktop/linux/kde/cloudreve-share.desktop @@ -0,0 +1,15 @@ +# KDE Dolphin service menu — right-click > Cloudreve > Share link… +# Install: copy to ~/.local/share/kio/servicemenus/ (Dolphin ≥ 22) or +# ~/.local/share/kservices5/ServiceMenus/ (older), then restart Dolphin. +# Requires `cloudreve-share` on PATH (see desktop/linux/cloudreve-share). + +[Desktop Entry] +Type=Service +MimeType=all/all +Actions=cloudreveShare +X-KDE-Submenu=Cloudreve + +[Desktop Action cloudreveShare] +Name=Share link… +Icon=emblem-shared +Exec=cloudreve-share %f diff --git a/desktop/linux/nautilus/scripts/Cloudreve Share b/desktop/linux/nautilus/scripts/Cloudreve Share new file mode 100755 index 00000000..59029049 --- /dev/null +++ b/desktop/linux/nautilus/scripts/Cloudreve Share @@ -0,0 +1,10 @@ +#!/bin/sh +# Nautilus script — adds "Scripts > Cloudreve Share" to the right-click menu. +# Install: copy to ~/.local/share/nautilus/scripts/ and chmod +x. +# Requires `cloudreve-share` on PATH (see desktop/linux/cloudreve-share). + +# NAUTILUS_SCRIPT_SELECTED_FILE_PATHS is newline-separated; read per line so +# paths containing spaces survive. +printf '%s\n' "$NAUTILUS_SCRIPT_SELECTED_FILE_PATHS" | while IFS= read -r f; do + [ -n "$f" ] && cloudreve-share "$f" +done diff --git a/desktop/macos/README.md b/desktop/macos/README.md new file mode 100644 index 00000000..bd73f9ea --- /dev/null +++ b/desktop/macos/README.md @@ -0,0 +1,25 @@ +# macOS file-manager integration + +`cloudreve-share` opens the share dialog for a local path via the +`cloudreve://share/` deep link — the same dialog the Windows Explorer +context menu opens. + +## Terminal + +```sh +cp cloudreve-share ~/.local/bin/ # or anywhere on PATH +chmod +x ~/.local/bin/cloudreve-share +cloudreve-share ~/Cloudreve/report.pdf +``` + +## Finder Quick Action + +1. Open **Shortcuts** → new shortcut. +2. Add action **"Run Shell Script"**: + - Shell: `zsh` (or `sh`), Input: **Shortcut Input** + - Script: `cloudreve-share "$1"` +3. Enable **"Use as Quick Action"** → **Finder**, name it `Cloudreve Share`. +4. In Finder: right-click a file → **Quick Actions** → **Cloudreve Share**. + +(Requires `cloudreve-share` on PATH and `python3`, which ships with macOS +Command Line Tools.) diff --git a/desktop/macos/cloudreve-share b/desktop/macos/cloudreve-share new file mode 100755 index 00000000..24ea401a --- /dev/null +++ b/desktop/macos/cloudreve-share @@ -0,0 +1,15 @@ +#!/bin/sh +# cloudreve-share — open the Cloudreve share dialog for local file(s). +# macOS counterpart of desktop/linux/cloudreve-share; usable from Terminal: +# cloudreve-share ~/Cloudreve/report.pdf +# or from a Finder Quick Action (see README.md in this directory). +# +# Works whether the app is running or not — the app bundle registers the +# cloudreve:// scheme and processes share deep links passed at cold start. + +set -eu + +for f in "$@"; do + encoded=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1]))' "$f") + open "cloudreve://share/$encoded" +done diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 4986e353..5da95079 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -176,6 +176,16 @@ async fn init_sync_service(app: AppHandle) -> anyhow::Result<()> { // Store in Tauri's managed state as well for commands app.manage(AppStateHandle); + // Dispatch share deep links that arrived while the sync service was still + // initializing, then handle a `cloudreve://` URL passed on our own argv — + // the cold-start case where the app was launched by the file manager. + drain_pending_share_links(); + for arg in std::env::args().skip(1) { + if arg.starts_with("cloudreve://") { + handle_deeplink_url(&app, &arg); + } + } + if has_no_drives { event_broadcaster.no_drive(); } @@ -232,6 +242,60 @@ impl AppStateHandle { } } +/// Share deep links that arrived before `APP_STATE` was ready. Drained once +/// after init completes — the link is a user action and must not be dropped. +static PENDING_SHARE_LINKS: Mutex> = Mutex::new(Vec::new()); + +fn dispatch_share_link(path: std::path::PathBuf) { + match APP_STATE.get() { + Some(state) => { + if let Err(e) = state + .drive_manager + .get_command_sender() + .send(cloudreve_sync::drive::commands::ManagerCommand::ShareLink { path }) + { + tracing::error!(target: "main", error = %e, "Failed to dispatch share deep link"); + } + } + None => { + tracing::info!(target: "main", path = %path.display(), "Share deep link queued until sync service init"); + PENDING_SHARE_LINKS + .lock() + .unwrap_or_else(|e| e.into_inner()) + .push(path); + } + } +} + +fn drain_pending_share_links() { + let pending: Vec = PENDING_SHARE_LINKS + .lock() + .map(|mut q| std::mem::take(&mut *q)) + .unwrap_or_default(); + for path in pending { + dispatch_share_link(path); + } +} + +/// Route an incoming `cloudreve://` deep link. `cloudreve://share/` opens the share dialog via the same pipeline as the Explorer +/// context menu — this is the Linux/macOS file-manager entry point (Nautilus +/// scripts, Dolphin service menus, `xdg-open`). Everything else keeps the +/// existing behavior: forward to the frontend and show the add-drive window. +fn handle_deeplink_url(app: &AppHandle, url: &str) { + if let Some(encoded_path) = url.strip_prefix("cloudreve://share/") { + let Ok(decoded) = urlencoding::decode(encoded_path) else { + tracing::warn!(target: "main", url = %url, "Malformed share deep link"); + return; + }; + dispatch_share_link(std::path::PathBuf::from(decoded.as_ref())); + return; + } + + let _ = app.emit("deeplink", url.to_string()); + show_add_drive_window_impl(app); +} + /// Spawn a task that bridges EventBroadcaster to Tauri events fn spawn_event_bridge(app_handle: AppHandle, event_broadcaster: &EventBroadcaster) { let mut receiver = event_broadcaster.subscribe(); @@ -394,8 +458,7 @@ pub fn run() { .plugin(tauri_plugin_single_instance::init(|app, argv, _cwd| { tracing::info!("a new app instance was opened with {argv:?} and the deep link event was already triggered"); if argv.len() > 1 { - let _ = app.emit("deeplink", argv[1].clone()); - show_add_drive_window_impl(app); + handle_deeplink_url(app, &argv[1]); } // when defining deep link schemes at runtime, you must also check `argv` here })) @@ -428,8 +491,7 @@ pub fn run() { if let Ok(urls) = serde_json::from_str::>(event.payload()) { if let Some(url) = urls.first() { tracing::info!(target: "main", "Received deep-link URL: {}", url); - let _ = app_handle.emit("deeplink", url.clone()); - show_add_drive_window_impl(&app_handle); + handle_deeplink_url(&app_handle, url); } } });