|
1 | | -# CloudMount — Mount cloud storage as native macOS drives. |
| 1 | +# CloudMount -- Mount cloud storage as native macOS volumes. |
2 | 2 |
|
3 | | -macOS 12+ menu bar app that mounts Backblaze B2 buckets as local FUSE volumes in Finder. Browse, read, write, and delete files as if they were on a local disk. Native SwiftUI interface, Rust FUSE daemon, no Electron. Free and open source alternative to Mountain Duck. |
| 3 | +Pure Swift macOS 26+ menu bar app that mounts Backblaze B2 buckets as local Finder volumes using Apple's FSKit framework. Browse, read, write, and delete files as if they were on a local disk. No FUSE, no kernel extensions, no Electron. |
4 | 4 |
|
5 | 5 | <!-- <img src="screenshot.png" alt="CloudMount menu bar screenshot" width="520" /> --> |
6 | 6 |
|
7 | 7 | ## Install |
8 | 8 |
|
9 | 9 | ### Requirements |
10 | | -- macOS 12+ (Monterey) |
11 | | -- [macFUSE](https://osxfuse.github.io/) (CloudMount will guide you through installation on first launch) |
| 10 | +- macOS 26 (Tahoe) or later |
| 11 | +- Enable the FSKit extension after install: System Settings -> General -> Login Items & Extensions -> CloudMount |
12 | 12 |
|
13 | | -### Build from source |
14 | | - |
15 | | -**Swift UI app:** |
| 13 | +### Homebrew |
16 | 14 | ```bash |
17 | | -swift build |
| 15 | +brew install ebreen/cloudmount/cloudmount |
18 | 16 | ``` |
19 | 17 |
|
20 | | -**Rust FUSE daemon:** |
| 18 | +### GitHub Releases |
| 19 | +Download the latest DMG: <https://github.com/ebreen/cloudmount/releases> |
| 20 | + |
| 21 | +Open the DMG and drag CloudMount to Applications. |
| 22 | + |
| 23 | +### Build from source |
21 | 24 | ```bash |
22 | | -cd Daemon/CloudMountDaemon |
23 | | -cargo build --release |
| 25 | +brew install xcodegen create-dmg |
| 26 | +xcodegen generate |
| 27 | +xcodebuild archive \ |
| 28 | + -project CloudMount.xcodeproj \ |
| 29 | + -scheme CloudMount \ |
| 30 | + -configuration Release \ |
| 31 | + -archivePath build/CloudMount.xcarchive |
24 | 32 | ``` |
25 | 33 |
|
26 | 34 | ### First run |
27 | | -- Launch CloudMount — it appears in your menu bar (no Dock icon). |
28 | | -- If macFUSE isn't installed, you'll see a guided installation dialog. |
29 | | -- Open Settings → Credentials and add your Backblaze B2 application key ID + key. |
30 | | -- Add a bucket in Settings → Buckets with name and mount point. |
31 | | -- Click Mount from the menu bar. Your bucket appears in Finder under `/Volumes/`. |
| 35 | +- Launch CloudMount -- it appears in your menu bar (no Dock icon). |
| 36 | +- If the FSKit extension isn't enabled, an onboarding screen guides you to System Settings. |
| 37 | +- Open Settings -> Credentials and add your Backblaze B2 application key ID + key. |
| 38 | +- Add a bucket in Settings -> Buckets, choose a mount point (default: `/Volumes/<bucket>`). |
| 39 | +- Click Mount. Your bucket appears in Finder. |
| 40 | + |
| 41 | +### CLI |
| 42 | +```bash |
| 43 | +mount -t b2 b2://my-bucket /Volumes/my-bucket |
| 44 | +diskutil unmount /Volumes/my-bucket |
| 45 | +``` |
32 | 46 |
|
33 | 47 | ## Features |
34 | | -- Mount B2 buckets as native macOS volumes visible in Finder. |
35 | | -- Browse directories, open files, drag-and-drop — it's just a folder. |
36 | | -- Write files locally, upload to B2 on close (write-on-close strategy). |
| 48 | +- Mount B2 buckets as native macOS volumes visible in Finder and `diskutil`. |
| 49 | +- Browse directories, open files, drag-and-drop -- it's a regular folder. |
| 50 | +- Read files on open, write back to B2 on close (download-on-open / write-on-close). |
37 | 51 | - Delete files and create directories through Finder. |
38 | | -- Metadata caching with Moka reduces B2 API calls by 80%+. |
39 | | -- Secure credential storage in macOS Keychain — never in config files. |
40 | | -- Bucket configuration persists between restarts (`~/Library/Application Support/CloudMount/`). |
41 | | -- Retry with exponential backoff for transient network failures. |
42 | | -- macOS metadata file suppression (`.DS_Store`, `._*`) to minimize API calls. |
| 52 | +- On-disk file cache with LRU eviction (default 1 GB, `~/Library/Caches/CloudMount/`). |
| 53 | +- In-memory metadata cache reduces B2 API calls (default 5 min TTL). |
| 54 | +- macOS metadata suppression (`.DS_Store`, `._*`, `.Spotlight-V100`, etc.) to minimize API noise. |
| 55 | +- Secure credential storage in macOS Keychain -- never in config files. |
| 56 | +- Automatic B2 token refresh with retry on auth expiry. |
| 57 | +- Upload retry with fresh upload URL on transient failures. |
| 58 | +- Sparkle auto-updates -- checks for new versions automatically. |
| 59 | +- Launch at Login option in Settings. |
| 60 | +- Signed, notarized, and stapled for Gatekeeper. |
43 | 61 | - No Dock icon, minimal UI, lives entirely in the menu bar. |
44 | 62 |
|
45 | 63 | ## Architecture |
46 | 64 |
|
47 | | -CloudMount is a dual-process architecture: |
| 65 | +CloudMount is a three-target Xcode project generated by XcodeGen: |
48 | 66 |
|
49 | 67 | ``` |
50 | | -┌─────────────────────┐ Unix Socket ┌──────────────────────┐ |
51 | | -│ Swift/SwiftUI │ ◄──── JSON Protocol ────► │ Rust Daemon │ |
52 | | -│ │ /tmp/cloudmount.sock │ │ |
53 | | -│ • Menu bar UI │ │ • FUSE filesystem │ |
54 | | -│ • Settings window │ │ • B2 API client │ |
55 | | -│ • Credential mgmt │ │ • Metadata cache │ |
56 | | -│ • Daemon client │ │ • Mount manager │ |
57 | | -└─────────────────────┘ └──────────────────────┘ |
| 68 | +CloudMount.app |
| 69 | +├── CloudMount Menu bar app (SwiftUI) |
| 70 | +│ Manages accounts, settings, mount/unmount via Process, |
| 71 | +│ monitors volume events via NSWorkspace notifications. |
| 72 | +│ |
| 73 | +├── CloudMountExtension FSKit filesystem extension (XPC) |
| 74 | +│ Implements FSUnaryFileSystem for the b2:// URL scheme. |
| 75 | +│ Handles all file operations: lookup, enumerate, read, |
| 76 | +│ write, create, delete, rename. Runs sandboxed. |
| 77 | +│ |
| 78 | +└── CloudMountKit Shared framework |
| 79 | + B2 API client, auth manager, credential store, |
| 80 | + metadata cache, file cache, shared config. |
58 | 81 | ``` |
59 | 82 |
|
60 | | -- **Swift app** (`Sources/CloudMount/`) — SwiftUI MenuBarExtra, settings, Keychain access, daemon communication via actor-based `DaemonClient`. |
61 | | -- **Rust daemon** (`Daemon/CloudMountDaemon/`) — fuser-based FUSE filesystem, B2 API client with reqwest, Moka metadata cache, Unix socket IPC server. |
62 | | -- **IPC** — JSON protocol over Unix domain socket. Commands: `Mount`, `Unmount`, `GetStatus`. 2-second polling for status updates. |
63 | | - |
64 | | -## How it works |
65 | | - |
66 | | -1. Swift app sends `Mount` command via Unix socket with bucket name, credentials, and mount point. |
67 | | -2. Rust daemon authenticates with B2 API (`b2_authorize_account`). |
68 | | -3. Daemon creates a FUSE filesystem and mounts it at the requested path. |
69 | | -4. Finder sees the mount as a native volume. |
70 | | -5. File operations (read/write/delete) are handled by FUSE callbacks that proxy to B2 API calls. |
71 | | -6. Metadata is cached (10min for attrs, 5min for directories) to keep Finder responsive. |
| 83 | +The extension runs as an XPC service managed by macOS. When you mount a `b2://` URL, the kernel routes filesystem operations to the extension. No daemon process, no socket IPC, no polling -- it's native. |
72 | 84 |
|
73 | 85 | ## File operations |
74 | 86 |
|
75 | | -| Operation | Strategy | Notes | |
76 | | -|-----------|----------|-------| |
77 | | -| Read | Download + local cache | Cached for performance | |
78 | | -| Write | Buffer locally, upload on close | Avoids partial uploads | |
79 | | -| Delete | Permanent delete | B2 versioning ignored for MVP | |
80 | | -| Mkdir | Create empty `.bzEmpty` marker | B2 has no real directories | |
81 | | -| Rename | Server-side copy + delete | B2 has no rename API | |
82 | | -| Dir rename | Not supported | Returns `ENOSYS` (MVP limitation) | |
| 87 | +| Operation | Strategy | |
| 88 | +|-----------|----------| |
| 89 | +| Read | Download to local staging on open, read from disk | |
| 90 | +| Write | Write to local staging, upload to B2 on close | |
| 91 | +| Delete | `b2_delete_file_version` (permanent) | |
| 92 | +| Mkdir | Zero-byte marker with `application/x-directory` content type | |
| 93 | +| Rename | Server-side copy + delete original | |
| 94 | +| Dir rename | Not supported (B2 limitation) | |
83 | 95 |
|
84 | | -## Project structure |
85 | | - |
86 | | -``` |
87 | | -Sources/CloudMount/ |
88 | | -├── CloudMountApp.swift # Main app, BucketConfigStore, persistence |
89 | | -├── DaemonClient.swift # Actor-based IPC client |
90 | | -├── CredentialStore.swift # Keychain storage |
91 | | -├── MacFUSEDetector.swift # macFUSE installation detection |
92 | | -├── MenuContentView.swift # Menu bar with mount controls |
93 | | -└── SettingsView.swift # Credentials + Buckets tabs |
94 | | -
|
95 | | -Daemon/CloudMountDaemon/src/ |
96 | | -├── main.rs # Daemon entry point |
97 | | -├── b2/ |
98 | | -│ ├── client.rs # B2 API client (auth, list, upload, delete) |
99 | | -│ └── types.rs # B2 API types with serde |
100 | | -├── cache/ |
101 | | -│ └── metadata.rs # Moka-based metadata cache |
102 | | -├── fs/ |
103 | | -│ ├── b2fs.rs # FUSE filesystem implementation |
104 | | -│ └── inode.rs # Stable path-to-inode mapping |
105 | | -├── ipc/ |
106 | | -│ ├── protocol.rs # JSON protocol definitions |
107 | | -│ └── server.rs # Unix socket IPC server |
108 | | -└── mount/ |
109 | | - └── manager.rs # Mount lifecycle management |
110 | | -``` |
| 96 | +## Known limitations |
| 97 | +- **Backblaze B2 only** -- S3-compatible providers planned. |
| 98 | +- **Directory rename not supported** -- B2 has no rename primitive; file rename uses copy+delete. |
| 99 | +- **No symlinks or hard links** -- B2 is a flat object store. |
111 | 100 |
|
112 | | -## Known limitations (v1.0) |
113 | | -- **Backblaze B2 only** — generic S3 support planned for v2. |
114 | | -- **Disk usage shows N/A** — B2 has no bucket size API; protocol is wired for when computation is added. |
115 | | -- **Directory rename not supported** — complex operation deferred. |
116 | | -- **Single-bucket mount** — multi-bucket simultaneous mount planned for v2. |
117 | | -- **No auto-mount** — manual mount from menu bar required each session. |
118 | | - |
119 | | -## Roadmap |
120 | | -- **v1.1** — Multi-bucket support, auto-mount on startup, connection status indicators. |
121 | | -- **v2.0** — Generic S3 provider support (AWS S3, Wasabi, DigitalOcean Spaces), custom endpoints. |
122 | | - |
123 | | -## Tech stack |
124 | | -- **Swift/SwiftUI** — Native menu bar app, Keychain integration |
125 | | -- **Rust** — FUSE daemon, async I/O with Tokio |
126 | | -- **fuser 0.16** — FUSE filesystem trait implementation |
127 | | -- **reqwest** — HTTP client for B2 API |
128 | | -- **moka** — High-performance concurrent cache (sync mode for FUSE compatibility) |
129 | | -- **serde** — JSON serialization for IPC protocol and B2 API types |
130 | | -- **tracing** — Structured logging |
| 101 | +## Privacy |
| 102 | +CloudMount reads/writes only to the Keychain (credentials), App Group UserDefaults (mount configs), and your chosen mount points. No telemetry, no analytics, no network calls except to the B2 API and Sparkle update feed. |
131 | 103 |
|
132 | 104 | ## Related |
133 | | -- [Mountain Duck](https://mountainduck.io/) — Commercial alternative ($39) that inspired this project. |
134 | | -- [macFUSE](https://osxfuse.github.io/) — Required kernel extension for userspace filesystems on macOS. |
135 | | -- [rclone](https://rclone.org/) — CLI tool for cloud storage (different approach — sync, not mount). |
| 105 | +- [Mountain Duck](https://mountainduck.io/) -- Commercial alternative that inspired this project. |
| 106 | +- [rclone](https://rclone.org/) -- CLI tool for cloud storage (sync approach, not native mount). |
136 | 107 |
|
137 | 108 | ## License |
138 | | -MIT — Eirik Breen ([ebreen](https://github.com/ebreen)) |
| 109 | +MIT -- Eirik Breen ([ebreen](https://github.com/ebreen)) |
0 commit comments