Skip to content

Commit 0d3b427

Browse files
committed
fix: Various rclone fixes for cloud sync on Windows
1 parent a7d7cc5 commit 0d3b427

3 files changed

Lines changed: 92 additions & 15 deletions

File tree

src/basic_memory/cli/commands/cloud/bisync_commands.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Cloud bisync commands for Basic Memory CLI."""
22

33
import asyncio
4+
import platform
45
import subprocess
56
import time
67
from datetime import datetime
@@ -167,16 +168,40 @@ def validate_bisync_directory(bisync_dir: Path) -> None:
167168
f" 2. Specify different directory: --dir ~/my-sync-folder"
168169
)
169170

170-
# Check if mount is active at this location
171-
result = subprocess.run(["mount"], capture_output=True, text=True)
172-
if str(bisync_dir) in result.stdout and "rclone" in result.stdout:
173-
raise BisyncError(
174-
f"{bisync_dir} is currently mounted via 'bm cloud mount'\n"
175-
f"Cannot use mounted directory for bisync.\n\n"
176-
f"Either:\n"
177-
f" 1. Unmount first: bm cloud unmount\n"
178-
f" 2. Use different directory for bisync"
179-
)
171+
system = platform.system()
172+
if system.lower() != "windows":
173+
# Unix: check with mount command
174+
result = subprocess.run(["mount"], capture_output=True, text=True)
175+
if str(bisync_dir) in result.stdout and "rclone" in result.stdout:
176+
raise BisyncError(
177+
f"{bisync_dir} is currently mounted via 'bm cloud mount'\n"
178+
f"Cannot use mounted directory for bisync.\n\n"
179+
f"Either:\n"
180+
f" 1. Unmount first: bm cloud unmount\n"
181+
f" 2. Use different directory for bisync"
182+
)
183+
else:
184+
# Windows: check for rclone mount process using this directory
185+
try:
186+
# Use 'wmic' to get running processes and their command lines
187+
result = subprocess.run(
188+
["wmic", "process", "where", "name='rclone.exe'", "get", "CommandLine"],
189+
capture_output=True,
190+
text=True,
191+
)
192+
if result.returncode == 0:
193+
for line in result.stdout.splitlines():
194+
if "mount" in line.lower() and str(bisync_dir) in line:
195+
raise BisyncError(
196+
f"{bisync_dir} appears to be mounted via 'bm cloud mount' (rclone process detected)\n"
197+
f"Cannot use mounted directory for bisync.\n\n"
198+
f"Either:\n"
199+
f" 1. Unmount first: bm cloud unmount\n"
200+
f" 2. Use different directory for bisync"
201+
)
202+
except Exception:
203+
# If wmic is not available, skip the check (or optionally warn)
204+
pass
180205

181206

182207
def convert_bmignore_to_rclone_filters() -> Path:

src/basic_memory/cli/commands/cloud/rclone_installer.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Cross-platform rclone installation utilities."""
22

3+
import os
34
import platform
45
import shutil
56
import subprocess
@@ -116,7 +117,15 @@ def install_rclone_windows() -> None:
116117
if shutil.which("winget"):
117118
try:
118119
console.print("[blue]Installing rclone via winget...[/blue]")
119-
run_command(["winget", "install", "Rclone.Rclone"])
120+
run_command(
121+
[
122+
"winget",
123+
"install",
124+
"Rclone.Rclone",
125+
"--accept-source-agreements",
126+
"--accept-package-agreements",
127+
]
128+
)
120129
console.print("[green]✓ rclone installed via winget[/green]")
121130
return
122131
except RcloneInstallError:
@@ -165,6 +174,7 @@ def install_rclone(platform_override: Optional[str] = None) -> None:
165174
install_rclone_linux()
166175
elif platform_name == "windows":
167176
install_rclone_windows()
177+
refresh_windows_path()
168178
else:
169179
raise RcloneInstallError(f"Unsupported platform: {platform_name}")
170180

@@ -180,6 +190,47 @@ def install_rclone(platform_override: Optional[str] = None) -> None:
180190
raise RcloneInstallError(f"Unexpected error during installation: {e}") from e
181191

182192

193+
def refresh_windows_path() -> None:
194+
"""Refresh the Windows PATH environment variable for the current session."""
195+
if platform.system().lower() != "windows":
196+
return
197+
198+
# Importing here after performing platform detection. Also note that we have to ignore pylance/pyright
199+
# warnings about winreg attributes so that "errors" don't appear on non-Windows platforms.
200+
import winreg
201+
202+
user_key_path = r"Environment"
203+
system_key_path = r"System\CurrentControlSet\Control\Session Manager\Environment"
204+
new_path = ""
205+
206+
# Read user PATH
207+
try:
208+
reg_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, user_key_path, 0, winreg.KEY_READ) # type: ignore[reportAttributeAccessIssue]
209+
user_path, _ = winreg.QueryValueEx(reg_key, "PATH") # type: ignore[reportAttributeAccessIssue]
210+
winreg.CloseKey(reg_key) # type: ignore[reportAttributeAccessIssue]
211+
except Exception:
212+
user_path = ""
213+
214+
# Read system PATH
215+
try:
216+
reg_key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, system_key_path, 0, winreg.KEY_READ) # type: ignore[reportAttributeAccessIssue]
217+
system_path, _ = winreg.QueryValueEx(reg_key, "PATH") # type: ignore[reportAttributeAccessIssue]
218+
winreg.CloseKey(reg_key) # type: ignore[reportAttributeAccessIssue]
219+
except Exception:
220+
system_path = ""
221+
222+
# Merge user and system PATHs (system first, then user)
223+
if system_path and user_path:
224+
new_path = system_path + ";" + user_path
225+
elif system_path:
226+
new_path = system_path
227+
elif user_path:
228+
new_path = user_path
229+
230+
if new_path:
231+
os.environ["PATH"] = new_path
232+
233+
183234
def get_rclone_version() -> Optional[str]:
184235
"""Get the installed rclone version."""
185236
if not is_rclone_installed():

tests/markdown/test_date_frontmatter_parsing.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"""
77

88
import pytest
9-
from pathlib import Path
109
from basic_memory.markdown.entity_parser import EntityParser
1110

1211

@@ -225,11 +224,13 @@ async def test_parse_file_with_datetime_objects(tmp_path):
225224
created_at = entity_markdown.frontmatter.metadata.get("created_at")
226225
assert isinstance(created_at, str), "Datetime should be converted to string"
227226
# PyYAML parses "2025-10-24 14:30:00" as datetime, which we normalize to ISO
228-
assert "2025-10-24" in created_at and "14:30:00" in created_at, \
227+
assert "2025-10-24" in created_at and "14:30:00" in created_at, (
229228
f"Datetime with time should be normalized to ISO format, got: {created_at}"
229+
)
230230

231231
updated_at = entity_markdown.frontmatter.metadata.get("updated_at")
232232
assert isinstance(updated_at, str), "Datetime should be converted to string"
233233
# PyYAML parses "2025-10-24T00:00:00" as datetime, which we normalize to ISO
234-
assert "2025-10-24" in updated_at and "00:00:00" in updated_at, \
235-
f"Datetime at midnight should be normalized to ISO format, got: {updated_at}"
234+
assert "2025-10-24" in updated_at and "00:00:00" in updated_at, (
235+
f"Datetime at midnight should be normalized to ISO format, got: {updated_at}"
236+
)

0 commit comments

Comments
 (0)