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.
- {mountlet-0.2.1 → mountlet-0.2.2}/CHANGELOG.md +26 -0
- {mountlet-0.2.1/src/mountlet.egg-info → mountlet-0.2.2}/PKG-INFO +41 -4
- {mountlet-0.2.1 → mountlet-0.2.2}/README.md +40 -3
- {mountlet-0.2.1 → mountlet-0.2.2}/SECURITY.md +2 -1
- {mountlet-0.2.1 → mountlet-0.2.2}/docs/README.md +24 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/docs/RELEASE.md +2 -1
- {mountlet-0.2.1 → mountlet-0.2.2}/examples/rclone.conf.sample +3 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/pyproject.toml +1 -1
- {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/__init__.py +1 -1
- mountlet-0.2.2/src/mountlet/assets/icon.png +0 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/core.py +164 -11
- mountlet-0.2.2/src/mountlet/rclone_wizard.py +294 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/settings.py +31 -1
- mountlet-0.2.2/src/mountlet/tray.py +4771 -0
- {mountlet-0.2.1 → mountlet-0.2.2/src/mountlet.egg-info}/PKG-INFO +41 -4
- {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet.egg-info/SOURCES.txt +2 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/tests/test_cli.py +1 -1
- {mountlet-0.2.1 → mountlet-0.2.2}/tests/test_core.py +266 -2
- mountlet-0.2.2/tests/test_rclone_wizard.py +213 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/tests/test_settings.py +29 -0
- mountlet-0.2.2/tests/test_tray.py +2092 -0
- mountlet-0.2.1/src/mountlet/assets/icon.png +0 -0
- mountlet-0.2.1/src/mountlet/tray.py +0 -2044
- mountlet-0.2.1/tests/test_tray.py +0 -818
- {mountlet-0.2.1 → mountlet-0.2.2}/LICENSE +0 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/MANIFEST.in +0 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/setup.cfg +0 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/cli.py +0 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/config_tools/export_config.py +0 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/config_tools/import_config.py +0 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/config_tools/path_config.py +0 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/config_tools/reconnect_config.py +0 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/config_tools/setup_wizard.py +0 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/config_tools/shared.py +0 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/config_tools/verify_config.py +0 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet/tui.py +0 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet.egg-info/dependency_links.txt +0 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet.egg-info/entry_points.txt +0 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet.egg-info/requires.txt +0 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/src/mountlet.egg-info/top_level.txt +0 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/tests/test_config_tools.py +0 -0
- {mountlet-0.2.1 → mountlet-0.2.2}/tests/test_setup_wizard.py +0 -0
- {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.
|
|
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
|
|
134
|
-
|
|
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
|
|
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
|
|
103
|
-
|
|
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
|
|
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
|
|
|
@@ -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.
|
|
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.
|
|
Binary file
|
|
@@ -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
|
|
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(
|
|
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=
|
|
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
|
-
|
|
328
|
-
|
|
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",
|
|
574
|
+
args = [rclone_bin, "mount", remote_source(remote), remote.mount_path]
|
|
459
575
|
args.extend(remote.flags)
|
|
460
576
|
|
|
461
|
-
|
|
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",
|
|
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",
|
|
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",
|