Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cloudhop/templates/wizard.html
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@
<div class="mode-card mode-bisync" role="radio" aria-checked="false" onclick="selectMode(this, 'bisync')">
<input type="radio" name="mode" value="bisync" style="display:none;">
<div class="mode-icon" style="color:var(--blue);">&#x21C4;</div>
<div class="mode-label">Two-Way Mirror <span class="mode-info-badge">&#x24D8;</span></div>
<div class="mode-label">Two-Way Sync <span class="mode-info-badge">&#x24D8;</span></div>
<div class="mode-desc">Mirror both directions. Changes on either side are propagated to the other.</div>
</div>
</div>
Expand Down
3 changes: 3 additions & 0 deletions cloudhop/tests/test_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ def test_ac_power_no_pause(self, tmp_path):
"""On AC power, transfer should not be paused."""
mgr = TransferManager(cm_dir=str(tmp_path))
mgr.state["pause_on_battery"] = True
mgr._has_battery = True # Force battery capability for CI (Linux/Windows)
with patch.object(mgr, "_is_on_battery", return_value=False):
with patch.object(mgr, "is_rclone_running", return_value=True):
with patch.object(mgr, "pause") as mock_pause:
Expand All @@ -200,6 +201,7 @@ def test_battery_below_threshold_pauses(self, tmp_path):
"""On battery power, running transfer should be paused."""
mgr = TransferManager(cm_dir=str(tmp_path))
mgr.state["pause_on_battery"] = True
mgr._has_battery = True # Force battery capability for CI (Linux/Windows)
with patch.object(mgr, "_is_on_battery", return_value=True):
with patch.object(mgr, "is_rclone_running", return_value=True):
with patch.object(mgr, "pause") as mock_pause:
Expand All @@ -212,6 +214,7 @@ def test_recovery_battery_to_ac_resumes(self, tmp_path):
mgr = TransferManager(cm_dir=str(tmp_path))
mgr.state["pause_on_battery"] = True
mgr.state["_battery_paused"] = True
mgr._has_battery = True # Force battery capability for CI (Linux/Windows)
mgr.rclone_cmd = ["rclone", "copy", "a:", "b:"]
with patch.object(mgr, "_is_on_battery", return_value=False):
with patch.object(mgr, "is_rclone_running", return_value=False):
Expand Down
2 changes: 1 addition & 1 deletion cloudhop/tests/test_transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ def test_pause_kills_process_windows(self, mock_sleep, mock_run, mock_sys, manag
assert result["ok"] is True
assert "5555" in result["msg"]
mock_run.assert_called_once_with(
["taskkill", "/F", "/T", "/PID", "5555"], capture_output=True
["taskkill", "/F", "/T", "/PID", "5555"], capture_output=True, timeout=10
)
assert manager.rclone_pid is None

Expand Down
15 changes: 13 additions & 2 deletions cloudhop/transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2123,7 +2123,15 @@ def _start_transfer_locked(self, body: Dict[str, Any]) -> Dict[str, Any]:
transfers = 8
except (ValueError, TypeError):
transfers = 8
excludes: List[str] = body.get("excludes", [])
# [V901] Accept both "excludes" (list) and "exclude" (string/list)
# so API callers using either key name get the correct behaviour.
excludes_raw = body.get("excludes", body.get("exclude", []))
if isinstance(excludes_raw, str):
excludes = [e.strip() for e in excludes_raw.split(",") if e.strip()]
elif isinstance(excludes_raw, list):
excludes = [str(e).strip() for e in excludes_raw if str(e).strip()]
else:
excludes = []
bw_limit: str = body.get("bw_limit", "")
source_type: str = body.get("source_type", "")
dest_type: str = body.get("dest_type", "")
Expand Down Expand Up @@ -2202,7 +2210,10 @@ def _start_transfer_locked(self, body: Dict[str, Any]) -> Dict[str, Any]:
if not _dest_path:
# Dest is cloud root — determine source folder name
_src_folder = ""
if source_type == "local":
# [V906] Infer source_type when not provided (API callers
# may omit it). A path without ":" is assumed local.
_effective_src_type = source_type or ("local" if ":" not in source else "")
if _effective_src_type == "local":
if os.path.isdir(source):
_src_folder = os.path.basename(source.rstrip("/\\"))
else:
Expand Down
Loading