mountlet 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {mountlet-0.2.1 → mountlet-0.2.2}/CHANGELOG.md +26 -0
  2. {mountlet-0.2.1/src/mountlet.egg-info → mountlet-0.2.2}/PKG-INFO +41 -4
  3. {mountlet-0.2.1 → mountlet-0.2.2}/README.md +40 -3
  4. {mountlet-0.2.1 → mountlet-0.2.2}/SECURITY.md +2 -1
  5. {mountlet-0.2.1 → mountlet-0.2.2}/docs/README.md +24 -0
  6. {mountlet-0.2.1 → mountlet-0.2.2}/docs/RELEASE.md +2 -1
  7. {mountlet-0.2.1 → mountlet-0.2.2}/examples/rclone.conf.sample +3 -0
  8. {mountlet-0.2.1 → mountlet-0.2.2}/pyproject.toml +1 -1
  9. {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/__init__.py +1 -1
  10. mountlet-0.2.2/src/mountlet/assets/icon.png +0 -0
  11. {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/core.py +164 -11
  12. mountlet-0.2.2/src/mountlet/rclone_wizard.py +294 -0
  13. {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/settings.py +31 -1
  14. mountlet-0.2.2/src/mountlet/tray.py +4771 -0
  15. {mountlet-0.2.1 → mountlet-0.2.2/src/mountlet.egg-info}/PKG-INFO +41 -4
  16. {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet.egg-info/SOURCES.txt +2 -0
  17. {mountlet-0.2.1 → mountlet-0.2.2}/tests/test_cli.py +1 -1
  18. {mountlet-0.2.1 → mountlet-0.2.2}/tests/test_core.py +266 -2
  19. mountlet-0.2.2/tests/test_rclone_wizard.py +213 -0
  20. {mountlet-0.2.1 → mountlet-0.2.2}/tests/test_settings.py +29 -0
  21. mountlet-0.2.2/tests/test_tray.py +2092 -0
  22. mountlet-0.2.1/src/mountlet/assets/icon.png +0 -0
  23. mountlet-0.2.1/src/mountlet/tray.py +0 -2044
  24. mountlet-0.2.1/tests/test_tray.py +0 -818
  25. {mountlet-0.2.1 → mountlet-0.2.2}/LICENSE +0 -0
  26. {mountlet-0.2.1 → mountlet-0.2.2}/MANIFEST.in +0 -0
  27. {mountlet-0.2.1 → mountlet-0.2.2}/setup.cfg +0 -0
  28. {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/cli.py +0 -0
  29. {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/config_tools/export_config.py +0 -0
  30. {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/config_tools/import_config.py +0 -0
  31. {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/config_tools/path_config.py +0 -0
  32. {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/config_tools/reconnect_config.py +0 -0
  33. {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/config_tools/setup_wizard.py +0 -0
  34. {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/config_tools/shared.py +0 -0
  35. {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/config_tools/verify_config.py +0 -0
  36. {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/tui.py +0 -0
  37. {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet.egg-info/dependency_links.txt +0 -0
  38. {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet.egg-info/entry_points.txt +0 -0
  39. {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet.egg-info/requires.txt +0 -0
  40. {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet.egg-info/top_level.txt +0 -0
  41. {mountlet-0.2.1 → mountlet-0.2.2}/tests/test_config_tools.py +0 -0
  42. {mountlet-0.2.1 → mountlet-0.2.2}/tests/test_setup_wizard.py +0 -0
  43. {mountlet-0.2.1 → mountlet-0.2.2}/tests/test_tui.py +0 -0
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2 - 2026-06-19
4
+
5
+ - Added a guided new-remote wizard for major cloud providers, with browser and
6
+ token-based rclone authentication flows, provider-specific fields, official
7
+ setup links, connection validation, and cleanup of incomplete remotes.
8
+ - Added remote ordering controls, saved one-time sorting by registration time,
9
+ name, provider, and storage usage, and per-remote move buttons.
10
+ - Added provider-colored labels and browser shortcuts, compact mount controls,
11
+ dynamic window sizing, and responsive background mount, unmount, usage, and
12
+ folder-opening operations.
13
+ - Added frameless main and configuration windows, an in-window keep-above
14
+ control, and reliable current-desktop tray behavior on Plasma X11. Pinning and
15
+ desktop movement now use EWMH requests without remapping the Qt window.
16
+ - Added provider status colors in the new-remote wizard to distinguish locally
17
+ tested providers from untested setup paths without adding extra label text.
18
+ - Added a dedicated Koofr setup path using rclone's Koofr backend instead of
19
+ routing Koofr through WebDAV.
20
+ - Added provider-specific S3 setup hints and links for Cloudflare R2, MinIO,
21
+ Amazon S3, Wasabi, and other S3-compatible providers.
22
+ - Added post-registration and post-mount connection checks so failed setup does
23
+ not quietly leave unusable remotes in the app list.
24
+ - Improved remote naming, provider suffixes, credential reuse, OAuth port
25
+ handling, wizard cancellation, application shutdown, and child-window
26
+ lifecycle behavior.
27
+ - Documented locally tested providers and untested provider paths.
28
+
3
29
  ## 0.2.1 - 2026-06-02
4
30
 
5
31
  - Added a cropped transparent Mountlet icon for the tray and window.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mountlet
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: CLI and tray tools for mounting rclone remotes
5
5
  Author: Eric Holt
6
6
  License-Expression: MIT
@@ -130,15 +130,52 @@ pipx inject mountlet PySide6
130
130
  The tray app uses the tray icon this way:
131
131
 
132
132
  - Hover shows a short mounted/unmounted summary.
133
- - Left-click opens the Mountlet window with compact remote strips, storage
134
- usage, mount-state toggles, click-to-open behavior, and config actions.
133
+ - Left-click opens or closes the Mountlet window. If it is behind another
134
+ window, the first click brings it forward. On Plasma X11, opening it from a
135
+ different desktop moves it to the current desktop.
135
136
  - Right-click shows app-level actions such as mount all, unmount all, update
136
137
  status, app settings, raw app, mount, rclone, and FUSE config files, and
137
138
  quit.
138
139
 
140
+ The Mountlet window provides:
141
+
142
+ - Compact remote strips with storage usage and mount-state toggles.
143
+ - Click-to-open folders, provider website shortcuts, and per-remote settings.
144
+ - A guided `+` flow for adding supported cloud remotes through rclone.
145
+ - Sorting by registration time, name, provider, total size, used space, or
146
+ remaining space, with manual move controls for final adjustments.
147
+ - A pin control that keeps the window above other windows without tying it to
148
+ one desktop.
149
+
139
150
  If your desktop session does not expose a system tray, use the terminal menu
140
151
  instead.
141
152
 
153
+ ## Provider Support
154
+
155
+ Mountlet uses `rclone` under the hood, so provider support depends on both
156
+ Mountlet's setup UI and rclone's backend behavior.
157
+
158
+ Locally tested with the current GUI flow and/or active local remotes:
159
+
160
+ - Google Drive
161
+ - Dropbox
162
+ - Microsoft OneDrive
163
+ - Box
164
+ - pCloud
165
+ - Cloudflare R2 through the S3-compatible wizard
166
+ - Koofr through rclone's dedicated Koofr backend
167
+
168
+ Available but not yet locally tested:
169
+
170
+ - Amazon S3
171
+ - MinIO and other S3-compatible providers
172
+ - Wasabi
173
+ - WebDAV providers such as Nextcloud, ownCloud, SharePoint, and Fastmail Files
174
+
175
+ In the setup window, tested options are shown in white and untested options in
176
+ yellow. Untested providers may work through rclone, but expect rough edges until
177
+ the wizard path is tested with a real account.
178
+
142
179
  ## Extra Commands
143
180
 
144
181
  These are useful for backup, troubleshooting, or moving to another computer:
@@ -190,7 +227,7 @@ export MOUNTLET_MOUNT_BASE=/path/to/mounts
190
227
  ### App Settings
191
228
 
192
229
  In the tray app, use `Config` > `App settings` to edit app-wide behavior. Use
193
- the `Config` button on a remote strip to edit only that mount. The settings
230
+ the gear button on a remote strip to edit only that mount. The settings
194
231
  windows show the available fields with text boxes, checkboxes, and dropdowns,
195
232
  then write `config.toml` and `mounts.toml` for you.
196
233
 
@@ -99,15 +99,52 @@ pipx inject mountlet PySide6
99
99
  The tray app uses the tray icon this way:
100
100
 
101
101
  - Hover shows a short mounted/unmounted summary.
102
- - Left-click opens the Mountlet window with compact remote strips, storage
103
- usage, mount-state toggles, click-to-open behavior, and config actions.
102
+ - Left-click opens or closes the Mountlet window. If it is behind another
103
+ window, the first click brings it forward. On Plasma X11, opening it from a
104
+ different desktop moves it to the current desktop.
104
105
  - Right-click shows app-level actions such as mount all, unmount all, update
105
106
  status, app settings, raw app, mount, rclone, and FUSE config files, and
106
107
  quit.
107
108
 
109
+ The Mountlet window provides:
110
+
111
+ - Compact remote strips with storage usage and mount-state toggles.
112
+ - Click-to-open folders, provider website shortcuts, and per-remote settings.
113
+ - A guided `+` flow for adding supported cloud remotes through rclone.
114
+ - Sorting by registration time, name, provider, total size, used space, or
115
+ remaining space, with manual move controls for final adjustments.
116
+ - A pin control that keeps the window above other windows without tying it to
117
+ one desktop.
118
+
108
119
  If your desktop session does not expose a system tray, use the terminal menu
109
120
  instead.
110
121
 
122
+ ## Provider Support
123
+
124
+ Mountlet uses `rclone` under the hood, so provider support depends on both
125
+ Mountlet's setup UI and rclone's backend behavior.
126
+
127
+ Locally tested with the current GUI flow and/or active local remotes:
128
+
129
+ - Google Drive
130
+ - Dropbox
131
+ - Microsoft OneDrive
132
+ - Box
133
+ - pCloud
134
+ - Cloudflare R2 through the S3-compatible wizard
135
+ - Koofr through rclone's dedicated Koofr backend
136
+
137
+ Available but not yet locally tested:
138
+
139
+ - Amazon S3
140
+ - MinIO and other S3-compatible providers
141
+ - Wasabi
142
+ - WebDAV providers such as Nextcloud, ownCloud, SharePoint, and Fastmail Files
143
+
144
+ In the setup window, tested options are shown in white and untested options in
145
+ yellow. Untested providers may work through rclone, but expect rough edges until
146
+ the wizard path is tested with a real account.
147
+
111
148
  ## Extra Commands
112
149
 
113
150
  These are useful for backup, troubleshooting, or moving to another computer:
@@ -159,7 +196,7 @@ export MOUNTLET_MOUNT_BASE=/path/to/mounts
159
196
  ### App Settings
160
197
 
161
198
  In the tray app, use `Config` > `App settings` to edit app-wide behavior. Use
162
- the `Config` button on a remote strip to edit only that mount. The settings
199
+ the gear button on a remote strip to edit only that mount. The settings
163
200
  windows show the available fields with text boxes, checkboxes, and dropdowns,
164
201
  then write `config.toml` and `mounts.toml` for you.
165
202
 
@@ -23,4 +23,5 @@ can be reviewed before public disclosure.
23
23
 
24
24
  ## Supported Versions
25
25
 
26
- No public stable version is supported yet.
26
+ Mountlet is still pre-1.0. Security fixes are intended for the latest public
27
+ 0.2.x release line.
@@ -51,9 +51,33 @@ ignored by git and must not be part of the installed-user workflow.
51
51
  - Build a wheel and install it in a clean virtual environment.
52
52
  - Test on a fresh Ubuntu installation with `rclone` and `fuse3`.
53
53
  - Verify import/export flows with non-sensitive sample configs.
54
+ - Update the provider support table in the root README after checking real
55
+ setup paths.
54
56
  - Confirm the built wheel and source distribution do not include local secrets.
55
57
  - Follow [RELEASE.md](RELEASE.md) when merging `wip` to `main`, tagging, and publishing.
56
58
 
59
+ ## Provider Test Status
60
+
61
+ The 0.2.2 release documents provider status based on local remotes in
62
+ `~/.config/rclone/rclone.conf` and recent GUI setup work.
63
+
64
+ Locally tested:
65
+
66
+ - Google Drive
67
+ - Dropbox
68
+ - Microsoft OneDrive
69
+ - Box
70
+ - pCloud
71
+ - Cloudflare R2
72
+ - Koofr
73
+
74
+ Available but untested:
75
+
76
+ - Amazon S3
77
+ - MinIO and other S3-compatible storage
78
+ - Wasabi
79
+ - WebDAV providers including Nextcloud, ownCloud, SharePoint, and Fastmail Files
80
+
57
81
  ## Release Strategy
58
82
 
59
83
  - Keep the CLI/TUI core MIT licensed.
@@ -17,7 +17,7 @@ Create release branches only if a maintained older line needs fixes while
17
17
  Run these from `wip` first:
18
18
 
19
19
  ```bash
20
- VERSION=0.2.1
20
+ VERSION=0.2.2
21
21
  python -m unittest discover -s tests
22
22
  python -m compileall -q src tests
23
23
  python -m pip wheel . -w /tmp/mountlet-release --no-deps --no-build-isolation
@@ -26,6 +26,7 @@ python -m pip wheel . -w /tmp/mountlet-release --no-deps --no-build-isolation
26
26
  Confirm:
27
27
 
28
28
  - `README.md` describes the user flow.
29
+ - `README.md` documents the tested and untested provider setup paths.
29
30
  - `CHANGELOG.md` has a section for the version being released.
30
31
  - `pyproject.toml` and `src/mountlet/__init__.py` have the same version.
31
32
  - `SECURITY.md` has an active security reporting path or GitHub private vulnerability reporting is enabled.
@@ -5,3 +5,6 @@
5
5
  # client_id = your-client-id.apps.googleusercontent.com
6
6
  # client_secret = your-client-secret
7
7
  # token = {"access_token":"..."}
8
+ #
9
+ # Provider setup paths marked tested in the Mountlet UI are based on local
10
+ # maintainer testing. Do not use this file for real credentials.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mountlet"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "CLI and tray tools for mounting rclone remotes"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,5 +1,5 @@
1
1
  """Mountlet package."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.2.2"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -114,6 +114,7 @@ class RemoteInfo:
114
114
  flags: List[str] = field(default_factory=list)
115
115
  extra_info: Dict[str, str] = field(default_factory=dict)
116
116
  auto_mount: bool = False
117
+ remote_path: str = ""
117
118
 
118
119
  @property
119
120
  def display_name(self) -> str:
@@ -136,7 +137,18 @@ class StorageUsage:
136
137
  return min(round((used / self.total) * 100), 100)
137
138
 
138
139
 
140
+ @dataclass(frozen=True)
141
+ class DriveOAuthCredentials:
142
+ remote_name: str
143
+ client_id: str
144
+ client_secret: str
145
+ remote_names: Tuple[str, ...] = ()
146
+
147
+
139
148
  PIDS: Dict[str, int] = {}
149
+ OAUTH_BACKEND_TYPES = {"drive", "dropbox", "onedrive", "box", "pcloud"}
150
+ RCLONE_STATUS_TIMEOUT_SECONDS = 20
151
+ RCLONE_CONNECT_TIMEOUT_SECONDS = 20
140
152
 
141
153
 
142
154
  TYPE_FLAG_PRESETS: Dict[str, List[str]] = {
@@ -164,6 +176,12 @@ TYPE_FLAG_PRESETS: Dict[str, List[str]] = {
164
176
  "--buffer-size",
165
177
  "16M",
166
178
  ],
179
+ "koofr": [
180
+ "--vfs-cache-mode",
181
+ "full",
182
+ "--buffer-size",
183
+ "16M",
184
+ ],
167
185
  "s3": [
168
186
  "--vfs-cache-mode",
169
187
  "full",
@@ -187,6 +205,14 @@ SAFE_RCLONE_CONFIG_KEYS: Dict[str, Tuple[str, ...]] = {
187
205
  "onedrive": ("drive_type", "region", "drive_id"),
188
206
  "webdav": ("url", "vendor"),
189
207
  "s3": ("provider", "region", "endpoint", "env_auth", "storage_class", "acl"),
208
+ "koofr": ("provider", "user", "mountid"),
209
+ }
210
+ S3_PROVIDER_DISPLAY_NAMES = {
211
+ "cloudflare": "Cloudflare R2",
212
+ "minio": "MinIO",
213
+ "aws": "Amazon S3",
214
+ "wasabi": "Wasabi",
215
+ "other": "S3",
190
216
  }
191
217
 
192
218
 
@@ -280,6 +306,47 @@ def save_rclone_fields(remote_name: str, updates: Dict[str, str]) -> None:
280
306
  _save_config(config)
281
307
 
282
308
 
309
+ def drive_oauth_credentials() -> List[DriveOAuthCredentials]:
310
+ config = _load_config()
311
+ groups: Dict[Tuple[str, str], List[str]] = {}
312
+ for remote_name in config.sections():
313
+ section = config[remote_name]
314
+ if section.get("type", "").lower() != "drive":
315
+ continue
316
+ client_id = section.get("client_id", "").strip()
317
+ client_secret = section.get("client_secret", "").strip()
318
+ if client_id and client_secret:
319
+ groups.setdefault((client_id, client_secret), []).append(remote_name)
320
+ credentials: List[DriveOAuthCredentials] = []
321
+ for (client_id, client_secret), remote_names in groups.items():
322
+ names = tuple(remote_names)
323
+ credentials.append(
324
+ DriveOAuthCredentials(
325
+ remote_name=_drive_credential_group_label(names),
326
+ client_id=client_id,
327
+ client_secret=client_secret,
328
+ remote_names=names,
329
+ )
330
+ )
331
+ return credentials
332
+
333
+
334
+ def _drive_credential_group_label(remote_names: Tuple[str, ...]) -> str:
335
+ if not remote_names:
336
+ return "existing remote"
337
+ if len(remote_names) == 1:
338
+ return remote_names[0]
339
+ return f"{remote_names[0]}, +{len(remote_names) - 1}"
340
+
341
+
342
+ def delete_rclone_remote(remote_name: str) -> bool:
343
+ config = _load_config()
344
+ if not config.remove_section(remote_name):
345
+ return False
346
+ _save_config(config)
347
+ return True
348
+
349
+
283
350
  def _build_flags(backend_type: str, extra_flags: List[str]) -> List[str]:
284
351
  flags = list(TYPE_FLAG_PRESETS.get(backend_type, DEFAULT_FLAGS))
285
352
  if backend_type == "drive" and "--links" not in flags:
@@ -288,18 +355,31 @@ def _build_flags(backend_type: str, extra_flags: List[str]) -> List[str]:
288
355
  return flags
289
356
 
290
357
 
291
- def load_remotes() -> List[RemoteInfo]:
358
+ def _s3_provider_display_name(provider: str, fallback: str = "S3") -> str:
359
+ normalized = provider.strip().lower()
360
+ return S3_PROVIDER_DISPLAY_NAMES.get(normalized, provider.strip() or fallback)
361
+
362
+
363
+ def load_remotes(*, include_incomplete: bool = True) -> List[RemoteInfo]:
292
364
  config = _load_config()
293
365
  app_settings = load_app_settings()
294
366
  mount_settings = load_mount_settings()
295
- remotes: List[RemoteInfo] = []
296
- for name in config.sections():
367
+ remotes: List[Tuple[int | None, int, RemoteInfo]] = []
368
+ for config_index, name in enumerate(config.sections()):
297
369
  section = config[name]
298
370
  remote_settings = mount_settings.get(name)
299
371
  if remote_settings and not remote_settings.enabled:
300
372
  continue
301
373
  backend_type = section.get("type", "").lower()
374
+ if not include_incomplete and not _remote_section_is_configured(backend_type, dict(section.items())):
375
+ continue
302
376
  alias, provider = _parse_remote_name(name, backend_type)
377
+ mount_provider = provider
378
+ display_provider = (
379
+ _s3_provider_display_name(section.get("provider", ""), provider)
380
+ if backend_type == "s3"
381
+ else provider
382
+ )
303
383
  extra_flags_str = section.get("mount_flags", "").strip()
304
384
  extra_flags = shlex.split(extra_flags_str) if extra_flags_str else []
305
385
  if remote_settings:
@@ -307,7 +387,7 @@ def load_remotes() -> List[RemoteInfo]:
307
387
  mount_path = (
308
388
  _resolve_configured_mount_path(remote_settings.mount_path)
309
389
  if remote_settings and remote_settings.mount_path
310
- else _build_mount_path(provider, alias)
390
+ else _build_mount_path(mount_provider, alias)
311
391
  )
312
392
  auto_mount = (
313
393
  remote_settings.auto_mount
@@ -317,15 +397,46 @@ def load_remotes() -> List[RemoteInfo]:
317
397
  info = RemoteInfo(
318
398
  name=name,
319
399
  alias=alias,
320
- provider=provider,
400
+ provider=display_provider,
321
401
  backend_type=backend_type,
322
402
  mount_path=mount_path,
403
+ remote_path=remote_settings.remote_path if remote_settings and remote_settings.remote_path else "",
323
404
  flags=_build_flags(backend_type, extra_flags),
324
405
  extra_info=dict(section.items()),
325
406
  auto_mount=auto_mount,
326
407
  )
327
- remotes.append(info)
328
- return remotes
408
+ order = remote_settings.order if remote_settings else None
409
+ remotes.append((order, config_index, info))
410
+ if any(order is not None for order, _config_index, _info in remotes):
411
+ remotes.sort(
412
+ key=lambda item: (
413
+ item[0] is None,
414
+ item[0] if item[0] is not None else item[1],
415
+ item[1],
416
+ )
417
+ )
418
+ return [info for _order, _config_index, info in remotes]
419
+
420
+
421
+ def _remote_section_is_configured(backend_type: str, values: Dict[str, str]) -> bool:
422
+ if backend_type not in OAUTH_BACKEND_TYPES:
423
+ if backend_type == "s3":
424
+ provider = values.get("provider", "").strip()
425
+ env_auth = values.get("env_auth", "").strip().lower() in {"true", "1", "yes", "on"}
426
+ has_keys = bool(values.get("access_key_id", "").strip() and values.get("secret_access_key", "").strip())
427
+ if not provider or not (env_auth or has_keys):
428
+ return False
429
+ if provider.lower() != "aws" and not values.get("endpoint", "").strip():
430
+ return False
431
+ return True
432
+ if backend_type == "webdav":
433
+ return values.get("url", "").strip().startswith(("http://", "https://"))
434
+ if backend_type == "koofr":
435
+ return bool(values.get("provider", "").strip() and values.get("user", "").strip() and values.get("password", "").strip())
436
+ return bool(backend_type)
437
+ if backend_type == "onedrive":
438
+ return bool(values.get("token") and values.get("drive_id") and values.get("drive_type"))
439
+ return bool(values.get("token") or values.get("service_account_file"))
329
440
 
330
441
 
331
442
  def find_rclone() -> str | None:
@@ -346,6 +457,11 @@ def mount_path(remote: RemoteInfo) -> str:
346
457
  return remote.mount_path
347
458
 
348
459
 
460
+ def remote_source(remote: RemoteInfo) -> str:
461
+ path = remote.remote_path.strip().lstrip("/")
462
+ return f"{remote.name}:{path}" if path else f"{remote.name}:"
463
+
464
+
349
465
  def is_mounted_windows(path: str) -> bool:
350
466
  if not os.path.exists(path):
351
467
  return False
@@ -455,10 +571,45 @@ def mount_remote(remote: RemoteInfo) -> Tuple[bool, str]:
455
571
  if not ok:
456
572
  return False, err or "[!] Unable to prepare mount directory."
457
573
 
458
- args = [rclone_bin, "mount", f"{remote.name}:", remote.mount_path]
574
+ args = [rclone_bin, "mount", remote_source(remote), remote.mount_path]
459
575
  args.extend(remote.flags)
460
576
 
461
- return _launch_mount_process(remote, args)
577
+ success, message = _launch_mount_process(remote, args)
578
+ if not success:
579
+ return success, message
580
+
581
+ connected, connection_message = check_remote_connection(remote, rclone_bin)
582
+ if connected:
583
+ return success, message
584
+
585
+ unmounted, unmount_message = unmount_remote(remote)
586
+ if not unmounted:
587
+ return False, f"{connection_message}\n{unmount_message}"
588
+ return False, connection_message
589
+
590
+
591
+ def check_remote_connection(remote: RemoteInfo, rclone_bin: str | None = None) -> Tuple[bool, str]:
592
+ binary = rclone_bin or find_rclone()
593
+ if not binary:
594
+ return False, "[!] rclone not found. Set RCLONE_PATH or add rclone to PATH."
595
+ source = remote_source(remote)
596
+ try:
597
+ result = subprocess.run(
598
+ [binary, "lsf", source, "--max-depth", "1"],
599
+ stdout=subprocess.DEVNULL,
600
+ stderr=subprocess.PIPE,
601
+ text=True,
602
+ timeout=RCLONE_CONNECT_TIMEOUT_SECONDS,
603
+ )
604
+ except subprocess.TimeoutExpired:
605
+ return False, f"[!] {remote.display_name} did not respond while checking {source}."
606
+ except Exception as exc:
607
+ return False, f"[!] Failed to check {remote.display_name}: {exc}"
608
+ if result.returncode == 0:
609
+ return True, f"[*] connected {remote.display_name}."
610
+ detail = result.stderr.strip()
611
+ summary = detail.splitlines()[0] if detail else f"exit code {result.returncode}"
612
+ return False, f"[!] {remote.display_name} is not connected to {source}: {summary}"
462
613
 
463
614
 
464
615
  def unmount_remote(remote: RemoteInfo) -> Tuple[bool, str]:
@@ -548,9 +699,10 @@ def get_storage_usage_details(remote: RemoteInfo) -> StorageUsage:
548
699
  return StorageUsage("?")
549
700
  try:
550
701
  output = subprocess.check_output(
551
- [rclone_bin, "about", f"{remote.name}:", "--json"],
702
+ [rclone_bin, "about", remote_source(remote), "--json"],
552
703
  stderr=subprocess.DEVNULL,
553
704
  text=True,
705
+ timeout=RCLONE_STATUS_TIMEOUT_SECONDS,
554
706
  )
555
707
  data = json.loads(output)
556
708
  used = int(data.get("used", 0))
@@ -574,7 +726,7 @@ def verify_remote(remote: RemoteInfo) -> Tuple[bool, str]:
574
726
  return False, "[!] rclone not found."
575
727
  try:
576
728
  result = subprocess.run(
577
- [rclone_bin, "about", f"{remote.name}:"],
729
+ [rclone_bin, "about", remote_source(remote)],
578
730
  stdout=subprocess.PIPE,
579
731
  stderr=subprocess.PIPE,
580
732
  text=True,
@@ -611,6 +763,7 @@ __all__ = [
611
763
  "editable_rclone_fields",
612
764
  "save_rclone_fields",
613
765
  "mount_remote",
766
+ "check_remote_connection",
614
767
  "unmount_remote",
615
768
  "refresh_remote",
616
769
  "mount_all",