Compare commits

...

1 Commits

Author SHA1 Message Date
claude[bot] d34c1cb2fa fix: Add X-Tigris-Consistent headers and fix Windows rclone config path
- Add X-Tigris-Consistent: true headers to rclone commands for global strong consistency
- Fixes stale content reads for non-US users hitting Tigris edge cache
- Update get_rclone_config_path() to use runtime detection via rclone config file
- Supports Scoop, Chocolatey, and manual rclone installations on Windows
- Add tests for new headers and runtime config detection

Fixes #548

Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2026-02-07 18:12:16 +00:00
4 changed files with 93 additions and 2 deletions
@@ -212,6 +212,10 @@ def project_sync(
remote_path,
"--filter-from",
str(filter_path),
"--header-download",
"X-Tigris-Consistent: true",
"--header-upload",
"X-Tigris-Consistent: true",
]
if verbose:
@@ -287,6 +291,10 @@ def project_bisync(
str(filter_path),
"--workdir",
str(state_path),
"--header-download",
"X-Tigris-Consistent: true",
"--header-upload",
"X-Tigris-Consistent: true",
]
# Add --create-empty-src-dirs if rclone version supports it (v1.64+)
@@ -356,6 +364,10 @@ def project_check(
remote_path,
"--filter-from",
str(filter_path),
"--header-download",
"X-Tigris-Consistent: true",
"--header-upload",
"X-Tigris-Consistent: true",
]
if one_way:
@@ -393,6 +405,12 @@ def project_ls(
if path:
remote_path = f"{remote_path}/{path}"
cmd = ["rclone", "ls", remote_path]
cmd = [
"rclone",
"ls",
remote_path,
"--header-download",
"X-Tigris-Consistent: true",
]
result = run(cmd, capture_output=True, text=True, check=True)
return result.stdout.splitlines()
@@ -7,6 +7,7 @@ Uses a single "basic-memory-cloud" remote for all operations.
import configparser
import os
import shutil
import subprocess
from pathlib import Path
from typing import Optional
@@ -22,7 +23,32 @@ class RcloneConfigError(Exception):
def get_rclone_config_path() -> Path:
"""Get the path to rclone configuration file."""
"""Get the path to rclone configuration file.
Uses runtime detection via `rclone config file` to support different
installation methods (Scoop, Chocolatey, manual) across platforms.
Falls back to default location if rclone command fails.
"""
try:
# Use rclone to determine its actual config location
result = subprocess.run(
["rclone", "config", "file"],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode == 0:
# Output format: "Configuration file is stored at:\n/path/to/rclone.conf"
lines = result.stdout.strip().split("\n")
if lines:
config_path = Path(lines[-1].strip())
config_path.parent.mkdir(parents=True, exist_ok=True)
return config_path
except (subprocess.SubprocessError, FileNotFoundError):
# If rclone is not available or command fails, fall back to default
pass
# Fallback to default location
config_dir = Path.home() / ".config" / "rclone"
config_dir.mkdir(parents=True, exist_ok=True)
return config_dir / "rclone.conf"
@@ -1,4 +1,7 @@
import subprocess
import time
from pathlib import Path
from unittest.mock import Mock
from basic_memory.cli.commands.cloud.bisync_commands import convert_bmignore_to_rclone_filters
from basic_memory.cli.commands.cloud.rclone_config import (
@@ -57,6 +60,38 @@ def test_convert_bmignore_to_rclone_filters_is_cached_when_up_to_date(config_hom
assert second.stat().st_mtime >= first_mtime
def test_get_rclone_config_path_uses_runtime_detection(monkeypatch, tmp_path):
"""Test that get_rclone_config_path uses rclone config file for runtime detection."""
config_path = tmp_path / "custom" / "rclone.conf"
def mock_run(cmd, **kwargs):
if cmd == ["rclone", "config", "file"]:
result = Mock()
result.returncode = 0
result.stdout = f"Configuration file is stored at:\n{config_path}"
return result
raise NotImplementedError(f"Unexpected command: {cmd}")
monkeypatch.setattr("subprocess.run", mock_run)
detected_path = get_rclone_config_path()
assert detected_path == config_path
assert detected_path.parent.exists()
def test_get_rclone_config_path_falls_back_on_error(monkeypatch):
"""Test that get_rclone_config_path falls back to default if rclone fails."""
def mock_run(cmd, **kwargs):
raise subprocess.SubprocessError("rclone not found")
monkeypatch.setattr("subprocess.run", mock_run)
fallback_path = get_rclone_config_path()
expected = Path.home() / ".config" / "rclone" / "rclone.conf"
assert fallback_path == expected
def test_configure_rclone_remote_writes_config_and_backs_up_existing(config_home):
cfg_path = get_rclone_config_path()
cfg_path.parent.mkdir(parents=True, exist_ok=True)
+12
View File
@@ -130,6 +130,9 @@ def test_project_sync_success(tmp_path):
assert "--filter-from" in cmd
assert str(filter_path) in cmd
assert "--dry-run" in cmd
assert "--header-download" in cmd
assert "X-Tigris-Consistent: true" in cmd
assert "--header-upload" in cmd
assert kwargs["text"] is True
@@ -214,6 +217,9 @@ def test_project_bisync_success(tmp_path):
assert "--compare=modtime" in cmd
assert "--workdir" in cmd
assert str(state_path) in cmd
assert "--header-download" in cmd
assert "X-Tigris-Consistent: true" in cmd
assert "--header-upload" in cmd
def test_project_bisync_requires_resync_first_time(tmp_path):
@@ -369,6 +375,9 @@ def test_project_check_success(tmp_path):
assert result is True
cmd, kwargs = runner.calls[0]
assert cmd[:2] == ["rclone", "check"]
assert "--header-download" in cmd
assert "X-Tigris-Consistent: true" in cmd
assert "--header-upload" in cmd
assert kwargs["capture_output"] is True
assert kwargs["text"] is True
@@ -407,6 +416,9 @@ def test_project_ls_success():
project = SyncProject(name="research", path="app/data/research")
files = project_ls(project, "my-bucket", run=runner, is_installed=lambda: True)
assert files == ["file1.md", "file2.md", "subdir/file3.md"]
cmd, _ = runner.calls[0]
assert "--header-download" in cmd
assert "X-Tigris-Consistent: true" in cmd
def test_project_ls_with_subpath():