diff --git a/Cargo.lock b/Cargo.lock index 8c04871..a0f3355 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1497,6 +1497,7 @@ checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.13.2", "block2 0.6.2", + "libc", "objc2 0.6.5", ] @@ -3743,14 +3744,23 @@ dependencies = [ "anyhow", "apple-native-keyring-store", "async-channel", + "dbus", "dbus-secret-service-keyring-store", "directories", + "dispatch2", "embed-resource", "gpui-kit", "gpui-tray", "interprocess", "keyring-core", + "libc", "mock-immich", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-location", + "objc2-core-wlan", + "objc2-foundation 0.3.2", + "objc2-system-configuration", "rusqlite", "serde", "smappservice-rs", @@ -3762,6 +3772,8 @@ dependencies = [ "tracing", "tracing-appender", "tracing-subscriber", + "windows 0.62.2", + "windows-core 0.62.2", "windows-native-keyring-store", "windows-registry 0.6.1", "zip", @@ -4900,6 +4912,16 @@ dependencies = [ "objc2-foundation 0.3.2", ] +[[package]] +name = "objc2-contacts" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b034b578389f89a85c055eacc8d8b368be5f04a6c1b07f672bf3aec21d0ef621" +dependencies = [ + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-core-data" version = "0.2.2" @@ -4971,6 +4993,19 @@ dependencies = [ "objc2-foundation 0.3.2", ] +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "block2 0.6.2", + "dispatch2", + "objc2 0.6.5", + "objc2-contacts", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-core-text" version = "0.3.2" @@ -4997,6 +5032,20 @@ dependencies = [ "objc2-metal 0.3.2", ] +[[package]] +name = "objc2-core-wlan" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e34919aba0d701380d911702455038a8a3587467fe0141d6a71501e7ffe48" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-security", + "objc2-security-foundation", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -5101,6 +5150,16 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-security-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef76382e9cedd18123099f17638715cc3d81dba3637d4c0d39ab69df2ef345a5" +dependencies = [ + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-service-management" version = "0.3.2" @@ -5114,6 +5173,20 @@ dependencies = [ "objc2-security", ] +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "libc", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-security", +] + [[package]] name = "objc2-ui-kit" version = "0.3.2" diff --git a/GLOSSARY.md b/GLOSSARY.md index 9f0b7cd..2144aae 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -151,6 +151,10 @@ _Avoid_: settings dialog, preferences The status icon in the notification area. It shows sync state and it opens the config window. _Avoid_: system tray, notification icon +**Pause**: +The user's hold on new uploads, from the dashboard control or the tray menu. It is separate from the known-network hold, so resuming never overrides the list. +_Avoid_: stop, disable sync + **Folder status**: The sync state of one source folder. The dashboard shows one card per source folder, and the folder detail renders the same status in full. _Avoid_: folder state, folder progress @@ -163,6 +167,22 @@ _Avoid_: add dialog, onboarding The enlarged view of one source folder: its full path, live state and counts, its own upload activity, settings, timestamps, and the list of the assets it has uploaded, with links into the Failed and Review views filtered to it. Its Remove needs a confirmation, because the folder's uploaded assets stay on the server. _Avoid_: folder modal, folder popup +**Known networks**: +The user's list of network names for the **Sync only on known networks** setting, compared against the name the OS reports for the current wireless network. A wired link always syncs, because it has no name to match. The list is off by default. +_Avoid_: allowed networks, trusted networks, whitelist + +**Network check**: +The client's one read of the OS's current network, through a **network source** per platform. It decides whether uploads are held, and its answer is what the Settings page shows as the current network. +_Avoid_: SSID lookup, network probe, reachability check + +**Network source**: +The platform backend that answers the network check: NetworkManager over D-Bus on Linux, the Network List Manager on Windows, and CoreWLAN with CaptiveNetwork on macOS. It also reports the OS's network-change events, which the client uses to re-check before the 30-second fallback poll. +_Avoid_: provider, adapter, detector + +**Pause reason**: +Why uploads are held: the user's own **pause**, or the known-network list. Both read as the paused activity state; the dashboard and the tray tooltip phrase the reason. +_Avoid_: pause kind, pause cause, hold type + ### Diagnostics **Client version**: diff --git a/README.md b/README.md index 1b0fcec..66b4e72 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,14 @@ copy and its shortcuts. Drive, or on a removable or network volume, asks for access to files and folders. Allow it; without the grant the folder shows as unreadable, with the reason (see [Troubleshooting](#troubleshooting)). +4. Turning **Sync only on known networks** on asks for Location permission, + because macOS reveals the Wi-Fi network name only to an app that has it. The + unsigned build cannot register for the permission the way a signed app does, + so if macOS refuses it the Settings page says the name is unknown, offers + **Open Location Settings**, and the list holds uploads; turning the list off + restores syncing on any network. The prompt only works from the installed + **Immich Sync** app: a build run out of `target/` is not an app bundle, so + macOS cannot show it and the page stays unknown. To uninstall, first turn **Autostart at login** off if macOS refused the unsigned login item: that removes the LaunchAgent at @@ -254,8 +262,10 @@ its card instead of producing one error per file. **Trash** for it. A rename is recognized by content and is not a deletion. - **Settings.** Changes save immediately. General holds **Autostart at login** (off by default); Limits holds the bandwidth limit (0 means unlimited) and - concurrent uploads; Sync holds the reconciliation scan interval and the retry - attempt cap; Tagging holds **Tag uploaded assets** (on by default), which adds + concurrent uploads; Sync holds the reconciliation scan interval, the retry + attempt cap, and **Sync only on known networks** (off by default) with the + list of network names beside it; Tagging holds **Tag uploaded assets** (on by + default), which adds the fixed tag `immich-sync` to every asset the client uploads. With an API key, tagging needs the `tag.create`, `tag.asset`, and `asset.update` permissions. The same section shows the backfill's progress and **Tag existing @@ -272,6 +282,12 @@ its card instead of producing one error per file. newest file while the view is open; scrolling up pauses it and shows the new line count with **Jump to latest**, and **Refresh** re-reads the tail once. The log never contains the password, API key, or session token. +- **Known networks.** With **Sync only on known networks** on, the client + pauses unless the OS names the current Wi-Fi network in your list, and shows + the name the OS reports beside the list. Nothing fails and the queue is kept; + syncing resumes by itself when a listed network comes back. Ethernet always + syncs, and with the list off the client syncs on any network. The name is read + from the OS on this computer and is never sent anywhere. - The theme follows the system theme. ## Where everything lives @@ -369,6 +385,20 @@ identity. After an update, the login-Keychain entry and the **Files & Folders** grant can ask again. Allow them again and syncing continues; nothing is lost. +### Sync is paused and the reason names the network + +**Sync only on known networks** is on and the client cannot prove the current +network is on your list. The Settings page shows the name the OS reports +("unknown" with the reason when it cannot name it), and the dashboard and tray +say why. Add the network to the list, connect to a listed network, or turn the +setting off. Ethernet always syncs. On Linux the name comes from NetworkManager, +so a machine using systemd-networkd or `iwd` reports no name and stays paused +while the setting is on; on macOS the name needs the Location grant, and the +Settings page has **Open Location Settings** when it is missing (the prompt +appears only in the installed app bundle, not in a `cargo run` build). Full +checks per platform are in +[docs/network-hand-checks.md](docs/network-hand-checks.md). + ### Sync does not pick up a new file Hidden files, symlinks, temporary files, and system files are skipped by diff --git a/SPEC.md b/SPEC.md index 7c29f57..3e22c45 100644 --- a/SPEC.md +++ b/SPEC.md @@ -117,8 +117,9 @@ Acceptance: a sync at a 1 MB/s limit produces near-limit observed throughput. Ra - R8.5 Autostart at login is a setting. The default is off. Windows uses its Run key, Linux an XDG autostart entry, and macOS a Login Item (`SMAppService.mainApp`), falling back to a LaunchAgent plist when an unsigned bundle is refused. - R8.6 The theme follows the system theme. - R8.7 Pause stops new uploads. In-flight uploads finish or stop at a safe point. +- R8.8 **Sync only on known networks** is a setting, off by default, with a list of names the user types. While it is on, uploads and scans wait unless the OS names the current wireless network in the list: no new attempts, the queue is kept, nothing fails, and syncing resumes on its own when a listed network comes back. A wired connection always syncs, and the list's help text says so. It fails closed: an unknown name, a missing or empty OS answer, an OS call that errors or times out, and no network at all hold uploads, and the dashboard and tray say why ("Paused — not on a known network", "Paused — network unknown"). It checks at startup, on the OS's network-change events, on a 30-second fallback poll, and after any upload network error. The Settings page shows the name the OS reports beside the list. -Acceptance: every tray action works from a cold start. A second launch does not start a second process. Pause stops new uploads within 5 seconds. +Acceptance: every tray action works from a cold start. A second launch does not start a second process. Pause stops new uploads within 5 seconds. Leaving a listed network holds uploads within 5 seconds, the queue keeps every waiting file, and returning to a listed network resumes without a click. ### R9 — Settings and paths diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index d785740..651b8d2 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -148,6 +148,10 @@ embed-resource = "3" # (ADR 0006 research §3.2). `crypto-rust` keeps the D-Bus transport encryption # in Rust, so the build needs no OpenSSL. dbus-secret-service-keyring-store = { version = "=1.0.1", features = ["crypto-rust"] } +# The known-network check (R8.8): NetworkManager's primary connection. The +# crate is already in the tree through the keyring store, so this adds no +# dependency, only a direct edge to it. +dbus = "0.9" [target."cfg(target_os = \"macos\")".dependencies] # SMAppService (macOS 13+), the login-item API behind the autostart setting. @@ -162,6 +166,39 @@ smappservice-rs = "=0.1.3" # (ADR 0008). apple-native-keyring-store = { version = "=1.0.2", features = ["keychain"] } +# The known-network check (R8.8): the SSID through CaptiveNetwork, the Wi-Fi +# interface through CoreWLAN, the Location grant that gates the SSID, and a +# System Configuration store for change events. The objc2 family is already in +# the tree through the login-item wrapper; every crate here is a safe binding, +# so the backend's unsafe blocks are only the calls themselves. +dispatch2 = "0.3" +# `getifaddrs`, to tell "Wi-Fi off with Ethernet" from "no link at all". +libc = "0.2" +objc2 = "0.6" +objc2-core-foundation = "0.3" +objc2-core-location = "0.3" +objc2-core-wlan = "0.3" +objc2-foundation = "0.3" +objc2-system-configuration = "0.3" + [target."cfg(windows)".dependencies] windows-native-keyring-store = "=1.1.0" windows-registry = "0.6" +# The known-network check (R8.8): the Network List Manager for the connected +# profile's name and its change events, the COM plumbing for both, and the IP +# helper's adapter list for the media type. The module paths changed in 0.62 +# from `NetworkManagement::NetworkConnector` to +# `Networking::NetworkListManager`. +# `#[implement]` expands to `windows_core::` paths, so the crate that defines +# the COM sink must name it directly, not only through `windows::core`. +windows-core = "0.62" +windows = { version = "0.62", features = [ + "Win32_Foundation", + "Win32_NetworkManagement_IpHelper", + "Win32_NetworkManagement_Ndis", + "Win32_Networking_NetworkListManager", + "Win32_Networking_WinSock", + "Win32_System_Com", + "Win32_System_Ole", + "Win32_System_Variant", +] } diff --git a/crates/app/macos/Info.plist b/crates/app/macos/Info.plist index 9a2c554..f48588e 100644 --- a/crates/app/macos/Info.plist +++ b/crates/app/macos/Info.plist @@ -2,8 +2,9 @@