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/<encoded
local path> 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>
pull/3593/head
Tomáš Dvořák 2 weeks ago committed by GitHub
parent de40dde4bd
commit bb215ffd8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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/<id>`) 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/<path>` 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)

@ -0,0 +1,35 @@
# Linux file-manager integration
`cloudreve-share` opens the share dialog for a local path via the
`cloudreve://share/<path>` 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).

@ -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

@ -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

@ -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

@ -0,0 +1,25 @@
# macOS file-manager integration
`cloudreve-share` opens the share dialog for a local path via the
`cloudreve://share/<path>` 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.)

@ -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

@ -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<Vec<std::path::PathBuf>> = 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<std::path::PathBuf> = 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/<url-encoded
/// local path>` 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::<Vec<String>>(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);
}
}
});

Loading…
Cancel
Save