Skip to content

Commit e2f762b

Browse files
committed
fix: harden desktop cloud provider and autostart regressions
1 parent 9029fae commit e2f762b

12 files changed

Lines changed: 273 additions & 17 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@ Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/).
66
## [Unreleased]
77

88
### Hinzugefügt / Added
9+
- **Box provider (Windows):** root auto-discovery via `~/Box` plus optional `CustomBoxLocation` registry path, process detection via `Box.exe`, and pause/resume support.
10+
- **Nextcloud provider (Windows):** root auto-discovery via `%APPDATA%\\Nextcloud\\nextcloud.cfg` plus default `~/Nextcloud`, process detection via `nextcloud.exe`, and pause/resume support.
911
- README.md, README.de.md and `llms.txt`: added discovery/search context for
1012
OneDrive access-denied, `cldflt.sys`, Windows Cloud Files filter, error
1113
`0x8007016A`, and cloud-sync locked-folder retry workflows.
14+
15+
### Behoben / Fixed
16+
- **Autostart in packaged builds:** PyInstaller/Frozen builds now register the
17+
packaged executable instead of the source-tree `clf_launcher.pyw`.
1218
- `tests/source_platform_smoke.py`: headless Smoke-Tests für Linux und macOS — prüft Modul-Import, Version, `ops`-Operationen (rename/move/delete), `models.Queue`-Persistenz, `paths.data_dir()` und `worker.run_once()` ohne Cloud-Client oder GUI.
1319
- `.github/workflows/source-platform-smoke.yml`: CI-Matrix für `ubuntu-latest` und `macos-latest`, die die Smoke-Tests bei jedem Push/PR auf `main` ausführt.
1420

README.de.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ des Sync-Clients.
3838
**Tray-Dialog** und **Explorer-Rechtsklick**-Kontextmenü
3939
- Auto-Retry in einstellbarem Intervall (Default 2 h) und auf Abruf
4040
- Optionales Pausieren/Neustarten des Sync-Clients während einer Operation
41-
(OneDrive-Provider)
41+
für unterstützte ordnerbasierte Provider
4242
- Optionaler Präventiv-Wächter, der den Sync-Client je nach Ordner-Aktivität
4343
pausiert/fortsetzt
44+
- Unterstützte Windows-Provider aktuell: OneDrive, Google Drive, Dropbox, Box,
45+
iCloud und Nextcloud
4446
- Autostart mit Windows; Single-Instance-Tray-App
4547

4648
## Installation
@@ -111,12 +113,12 @@ stehen, aber temporär durch einen Cloud-Sync-Provider blockiert werden.
111113
- **P3 (fertig):** Präventiv-Wächter (Änderungsrate *konfigurierter* Ordner
112114
beobachten → Sync-Client automatisch pausieren/fortsetzen; bounded + stat-only,
113115
hydratisiert keine Online-only-Placeholder; opt-in).
114-
- **Tests:** `pytest`, **92 grün** (Core + P2/P3 + i18n + Multicloud + Leerordner-Eigen-Handle-Lock-Regressionen).
116+
- **Tests:** `pytest`, **110 grün** (Core + P2/P3 + i18n + Multicloud + Box + Nextcloud + Leerordner-Eigen-Handle-Lock-Regressionen).
115117
- **Im Lifetest gehärtet (2026-05-29):** `is_running()/pause()` robust gegen
116118
nicht-UTF-8-`tasklist`-Ausgabe; `delete` entfernt read-only-Attribute statt an
117119
WinError 5 zu scheitern. Erster echter Einsatz: ein Ordner-Rename, den manuelle
118120
Versuche/`cldflt` zuvor blockierten, gelang per copy+delete.
119-
- **Offen/künftig:** weitere Provider-Adapter (Dropbox/Google Drive/iCloud);
121+
- **Offen/künftig:** weitere Provider-Adapter (pCloud/Synology Drive);
120122
optional Relaunch-Unterdrückung des Sync-Clients während langer Operationen.
121123

122124
Windows-only (`cldflt` ist Windows-spezifisch); der Kern ist plattformneutral

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,12 @@ sync client.
3838
- Multiple input paths: **CLI** (for LLMs/scripts), human-readable
3939
**`queue.txt`**, **tray dialog**, and an **Explorer right-click** context menu
4040
- Auto-retry on a configurable interval (default 2 h) and on demand
41-
- Optional sync-client pause/restart during an operation (OneDrive provider)
41+
- Optional sync-client pause/restart during an operation for supported
42+
folder-based providers
4243
- Optional preventive watcher that pauses/resumes the sync client based on
4344
folder activity
45+
- Supported Windows providers today: OneDrive, Google Drive, Dropbox, Box,
46+
iCloud, and Nextcloud
4447
- Autostart with Windows; single-instance tray app
4548

4649
## Installation
@@ -112,8 +115,8 @@ cloud-sync provider.
112115
- **P3 (done):** Preventive watcher (observes the change rate of *configured*
113116
folders → pauses/resumes the sync client; bounded, stat-only, does not
114117
hydrate online-only placeholders; opt-in).
115-
- **Tests:** `pytest`, 92 passing (core + P2/P3 + i18n + multicloud + empty-dir own-handle-lock regressions).
116-
- **Open/future:** more provider adapters (Dropbox / Google Drive / iCloud);
118+
- **Tests:** `pytest`, 110 passing (core + P2/P3 + i18n + multicloud + Box + Nextcloud + empty-dir own-handle-lock regressions).
119+
- **Open/future:** more provider adapters (pCloud / Synology Drive);
117120
optional suppression of sync-client relaunch during long operations.
118121

119122
Windows-only; the core is platform-neutral for later ports.

ROADMAP.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@
3939

4040
## Langfristig
4141

42-
- [ ] Weitere Cloud-Provider (Box, Nextcloud, pCloud, Synology Drive)
42+
- [~] Weitere Cloud-Provider (Box, Nextcloud, pCloud, Synology Drive)
43+
- Box erledigt 2026-06-17
44+
- Nextcloud erledigt 2026-06-16
45+
- Offen: pCloud, Synology Drive
4346
- [ ] Konfigurierbares Retry-Verhalten (Exponential Backoff, max Retries)
4447
- [ ] Benachrichtigungen (System-Toast bei Dauerfehler)
4548
- [ ] Web-Dashboard / Remote-Status

TODO.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,7 @@ Beim Port der copy+delete-Logik in den ellmos-filecommander-mcp-Server (TypeScri
5555
- [ ] Test-CI beobachten und bei Bedarf Windows-spezifische Runtime-Abhängigkeiten ergänzen
5656
- [ ] Cross-Platform: Linux-Support (siehe PORTIERUNGSPLAN.md)
5757
- [ ] Cross-Platform: macOS-Support (siehe PORTIERUNGSPLAN.md)
58-
- [ ] Weitere Provider: Box, Nextcloud, pCloud, Synology Drive
58+
- [~] Weitere Provider: Box, Nextcloud, pCloud, Synology Drive
59+
Box erledigt 2026-06-17 (`~/Box` plus `CustomBoxLocation`-Registry-Pfad, `Box.exe`-Prozesssteuerung).
60+
Nextcloud erledigt 2026-06-16 (`nextcloud.cfg`-Root-Erkennung + Prozesssteuerung).
61+
Offen bleiben pCloud und Synology Drive.

llms.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
CloudLockFixer is a Windows tray and CLI tool for delayed rename, move and delete
44
operations in cloud-sync folders. It is designed for OneDrive, Dropbox, Google
5-
Drive and iCloud folders where Windows Cloud Files locks can make normal file
5+
Drive, Box, iCloud and Nextcloud folders where Windows Cloud Files locks can make normal file
66
operations fail.
77

88
Version: 0.2.2

src/cloudlockfixer/autostart.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
def _launch_command() -> str:
1111
"""Befehl, der die Tray-App startet (pythonw, kein Konsolenfenster)."""
12+
if getattr(sys, "frozen", False):
13+
return f'"{sys.executable}"'
1214
from .paths import launcher, pythonw
1315
return f'"{pythonw()}" "{launcher()}"'
1416

src/cloudlockfixer/providers.py

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,30 @@ def _get_volume_label(drive_letter: str) -> str:
111111
return ""
112112

113113

114+
def _read_box_custom_location() -> Path | None:
115+
"""Read Box Drive's optional custom root parent from the Windows registry."""
116+
if sys.platform != "win32":
117+
return None
118+
try:
119+
import winreg
120+
except ImportError:
121+
return None
122+
123+
try:
124+
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Box\Box") as key:
125+
value, _ = winreg.QueryValueEx(key, "CustomBoxLocation")
126+
except (OSError, ValueError):
127+
return None
128+
129+
if not isinstance(value, str):
130+
return None
131+
normalized = value.strip().strip('"')
132+
if not normalized:
133+
return None
134+
base = Path(normalized)
135+
return base if base.name.lower() == "box" else base / "Box"
136+
137+
114138
# ── OneDrive ───────────────────────────────────────────────────────
115139

116140

@@ -245,6 +269,99 @@ def resume(self) -> bool:
245269
return False
246270

247271

272+
# ── Box ────────────────────────────────────────────────────────────
273+
274+
275+
class BoxProvider(SyncProvider):
276+
name = "Box"
277+
mount_type = "folder"
278+
279+
def _detect_roots(self) -> list[Path]:
280+
roots: list[Path] = []
281+
default_root = Path.home() / "Box"
282+
if default_root.exists():
283+
roots.append(default_root)
284+
285+
custom_root = _read_box_custom_location()
286+
if custom_root and custom_root.exists():
287+
roots.append(custom_root)
288+
return _dedup_paths(roots)
289+
290+
def is_running(self) -> bool:
291+
return _check_process("Box.exe")
292+
293+
def pause(self) -> bool:
294+
return _kill_process("Box.exe")
295+
296+
def resume(self) -> bool:
297+
if sys.platform != "win32":
298+
return False
299+
candidates = [
300+
Path(r"C:\Program Files\Box\Box\Box.exe"),
301+
Path(r"C:\Program Files (x86)\Box\Box\Box.exe"),
302+
]
303+
for exe in candidates:
304+
if exe.exists():
305+
try:
306+
subprocess.Popen([str(exe)])
307+
return True
308+
except OSError:
309+
continue
310+
return False
311+
312+
313+
# ── Nextcloud ───────────────────────────────────────────────────────
314+
315+
316+
class NextcloudProvider(SyncProvider):
317+
name = "Nextcloud"
318+
mount_type = "folder"
319+
320+
def _detect_roots(self) -> list[Path]:
321+
roots: list[Path] = []
322+
default_root = Path.home() / "Nextcloud"
323+
if default_root.exists():
324+
roots.append(default_root)
325+
326+
cfg_path = Path(os.environ.get("APPDATA", "")) / "Nextcloud" / "nextcloud.cfg"
327+
if cfg_path.exists():
328+
try:
329+
for raw_line in cfg_path.read_text(encoding="utf-8", errors="ignore").splitlines():
330+
line = raw_line.strip()
331+
if not line or line.startswith(("#", ";")) or "localPath=" not in line:
332+
continue
333+
_, raw_path = line.split("localPath=", 1)
334+
normalized = raw_path.strip().strip('"').replace("/", os.sep)
335+
if normalized:
336+
roots.append(Path(normalized))
337+
except OSError:
338+
pass
339+
return _dedup_paths(roots)
340+
341+
def is_running(self) -> bool:
342+
return _check_process("nextcloud.exe")
343+
344+
def pause(self) -> bool:
345+
return _kill_process("nextcloud.exe")
346+
347+
def resume(self) -> bool:
348+
if sys.platform != "win32":
349+
return False
350+
candidates = [
351+
Path(r"C:\Program Files\Nextcloud\nextcloud.exe"),
352+
Path(r"C:\Program Files (x86)\Nextcloud\nextcloud.exe"),
353+
Path(os.environ.get("LOCALAPPDATA", "")) / "Programs" / "Nextcloud" / "nextcloud.exe",
354+
]
355+
for exe in candidates:
356+
if exe.exists():
357+
try:
358+
subprocess.Popen([str(exe)])
359+
return True
360+
except OSError:
361+
continue
362+
return False
363+
364+
248365
# ── iCloud ─────────────────────────────────────────────────────────
249366

250367

@@ -292,7 +409,7 @@ def resume(self) -> bool:
292409

293410
def _discover_providers() -> list[SyncProvider]:
294411
candidates = [OneDriveProvider(), GoogleDriveProvider(),
295-
DropboxProvider(), ICloudProvider()]
412+
DropboxProvider(), BoxProvider(), NextcloudProvider(), ICloudProvider()]
296413
active: list[SyncProvider] = []
297414
for prov in candidates:
298415
try:

src/cloudlockfixer/tray.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ def _make_icon(color: str = "#2e9e44") -> QIcon:
4444
if not ic.isNull():
4545
return ic
4646
pm = QPixmap(64, 64)
47-
pm.fill(Qt.transparent)
47+
pm.fill(Qt.GlobalColor.transparent)
4848
p = QPainter(pm)
49-
p.setRenderHint(QPainter.Antialiasing)
49+
p.setRenderHint(QPainter.RenderHint.Antialiasing)
5050
p.setBrush(QColor(color))
51-
p.setPen(Qt.NoPen)
51+
p.setPen(Qt.PenStyle.NoPen)
5252
p.drawEllipse(8, 26, 28, 26)
5353
p.drawEllipse(26, 18, 30, 28)
5454
p.drawRoundedRect(10, 38, 46, 16, 8, 8)
@@ -57,7 +57,7 @@ def _make_icon(color: str = "#2e9e44") -> QIcon:
5757
f.setBold(True)
5858
f.setPointSize(13)
5959
p.setFont(f)
60-
p.drawText(pm.rect(), Qt.AlignCenter, "↻")
60+
p.drawText(pm.rect(), Qt.AlignmentFlag.AlignCenter, "↻")
6161
p.end()
6262
return QIcon(pm)
6363

tests/test_bug_regressions.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Regressionstests — bugfix-library-transfer 2026-06-21."""
2+
import unittest
3+
from pathlib import Path
4+
5+
SRC = Path(__file__).resolve().parents[1] / "src" / "cloudlockfixer"
6+
TRAY = SRC / "tray.py"
7+
8+
9+
class TestD2NoDeprecatedEnums(unittest.TestCase):
10+
"""BUG-D2: Deprecated Qt/QPainter-Enums in PySide6 6.4+."""
11+
12+
def _src(self):
13+
return TRAY.read_text(encoding="utf-8")
14+
15+
def test_no_qt_transparent_bare(self):
16+
self.assertNotIn(
17+
"Qt.transparent",
18+
self._src(),
19+
"Qt.transparent (deprecated bare) in tray.py — BUG-D2",
20+
)
21+
22+
def test_no_qpainter_antialiasing_bare(self):
23+
self.assertNotIn(
24+
"QPainter.Antialiasing",
25+
self._src(),
26+
"QPainter.Antialiasing (deprecated bare) in tray.py — BUG-D2",
27+
)
28+
29+
def test_no_qt_nopen_bare(self):
30+
self.assertNotIn(
31+
"Qt.NoPen",
32+
self._src(),
33+
"Qt.NoPen (deprecated bare) in tray.py — BUG-D2",
34+
)
35+
36+
def test_no_qt_aligncenter_bare(self):
37+
self.assertNotIn(
38+
"Qt.AlignCenter",
39+
self._src(),
40+
"Qt.AlignCenter (deprecated bare) in tray.py — BUG-D2",
41+
)
42+
43+
44+
if __name__ == "__main__":
45+
unittest.main()

0 commit comments

Comments
 (0)