From 8960348ba40126c7bdf3f97fe6bbd8c077936d10 Mon Sep 17 00:00:00 2001 From: LLQWQ Date: Mon, 27 Apr 2026 11:59:06 +0800 Subject: [PATCH 1/3] fix: prevent config files from being uploaded via FileSync Config files (.obsidian/*, .agents/*, etc.) should only be synced via SettingSync protocol, not FileSync. This fixes three issues: 1. _initial_sync(): only call file_sync.request_sync() when sync_files is true, not when sync_config is true 2. _push_all_files(): skip config files entirely, let _push_all_settings handle them via SettingSync 3. file_sync._collect_local_files(): always skip dot-prefixed directories, regardless of sync_config setting Previously, config files were uploaded to both file DB and setting DB, causing conflicts and plugin loss during sync. --- fns_cli/file_sync.py | 4 ++-- fns_cli/sync_engine.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/fns_cli/file_sync.py b/fns_cli/file_sync.py index f152f04..dee28c0 100644 --- a/fns_cli/file_sync.py +++ b/fns_cli/file_sync.py @@ -501,9 +501,9 @@ def _collect_local_files(self) -> list[dict]: if self.engine.is_excluded(rel) or rel.endswith(".md"): continue first = rel.split("/")[0] - if first.startswith(".") and not self.config.sync.sync_config: + if first.startswith("."): continue - if not first.startswith(".") and not self.config.sync.sync_files: + if not self.config.sync.sync_files: continue try: stat = fp.stat() diff --git a/fns_cli/sync_engine.py b/fns_cli/sync_engine.py index 137af03..d76c037 100644 --- a/fns_cli/sync_engine.py +++ b/fns_cli/sync_engine.py @@ -230,7 +230,7 @@ async def _initial_sync(self) -> None: await self.note_sync.request_sync() await self._wait_note_sync(timeout=300) - if self.config.sync.sync_files or self.config.sync.sync_config: + if self.config.sync.sync_files: await self.file_sync.request_sync() await self._wait_file_sync(timeout=300) @@ -284,16 +284,16 @@ async def _wait_setting_sync(self, timeout: float = 60) -> None: await asyncio.sleep(0.5) async def _push_all_files(self) -> None: - """Upload every non-note, non-excluded file in the vault.""" + """Upload every non-note, non-excluded, non-config file in the vault.""" for fp in self.vault_path.rglob("*"): if fp.is_dir(): continue rel = fp.relative_to(self.vault_path).as_posix() if self.is_excluded(rel) or rel.endswith(".md"): continue - if not self._is_config(rel) and not self.config.sync.sync_files: + if self._is_config(rel): continue - if self._is_config(rel) and not self.config.sync.sync_config: + if not self.config.sync.sync_files: continue await self.file_sync.push_upload(rel) await asyncio.sleep(0.05) From 7af51624448240a52927dbdd126bc7b46294663e Mon Sep 17 00:00:00 2001 From: LLQWQ Date: Mon, 27 Apr 2026 13:07:21 +0800 Subject: [PATCH 2/3] fix: ignore FolderSync events for dot-prefixed config directories Config directories (.obsidian, .agents, etc.) are managed by SettingSync protocol, not FolderSync. This prevents accidental deletion of the entire .obsidian directory when the server sends FolderSyncDelete after config records are removed from the file database. The fix adds checks in all three FolderSync handlers: - _on_sync_modify: skip creating dot-prefixed dirs - _on_sync_delete: skip deleting dot-prefixed dirs (CRITICAL) - _on_sync_rename: skip renaming dot-prefixed dirs --- fns_cli/folder_sync.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/fns_cli/folder_sync.py b/fns_cli/folder_sync.py index 838267a..5ea3cd5 100644 --- a/fns_cli/folder_sync.py +++ b/fns_cli/folder_sync.py @@ -45,6 +45,12 @@ async def _on_sync_modify(self, msg: WSMessage) -> None: if not rel_path: return + # Skip dot-prefixed directories — these are config folders managed by SettingSync + first = rel_path.split("/")[0] + if first.startswith("."): + log.debug("Ignoring FolderSyncModify for config dir: %s", rel_path) + return + full = self.vault_path / rel_path try: full.mkdir(parents=True, exist_ok=True) @@ -58,6 +64,12 @@ async def _on_sync_delete(self, msg: WSMessage) -> None: if not rel_path: return + # Skip dot-prefixed directories — these are config folders managed by SettingSync + first = rel_path.split("/")[0] + if first.startswith("."): + log.debug("Ignoring FolderSyncDelete for config dir: %s", rel_path) + return + full = self.vault_path / rel_path try: if full.exists(): @@ -73,6 +85,13 @@ async def _on_sync_rename(self, msg: WSMessage) -> None: if not old_path or not new_path: return + # Skip dot-prefixed directories — these are config folders managed by SettingSync + first_old = old_path.split("/")[0] + first_new = new_path.split("/")[0] + if first_old.startswith(".") or first_new.startswith("."): + log.debug("Ignoring FolderSyncRename for config dir: %s → %s", old_path, new_path) + return + old_full = self.vault_path / old_path new_full = self.vault_path / new_path try: From b12b7b89c933f0a44d1c48a8aba529750b627ddb Mon Sep 17 00:00:00 2001 From: LLQWQ Date: Mon, 27 Apr 2026 14:15:09 +0800 Subject: [PATCH 3/3] fix: make config sync directories configurable via config.yaml Previously, FileSync and SettingSync had inconsistent hardcoded handling of dot-prefixed directories. This caused custom config directories like .agents to not be properly synchronized. Changes: - config.py: add config_sync_dirs field to SyncConfig (default: [.obsidian, .agents]) - sync_engine.py: use config_sync_dirs from config instead of hardcoded list - setting_sync.py: pass config_sync_dirs to _is_config_path() - file_sync.py: skip dot-prefixed dirs only when sync_config is enabled - config.yaml: add config_sync_dirs example configuration Users can now customize which dot-prefixed directories are treated as config by editing config.yaml: config_sync_dirs: - .obsidian - .agents - .my-custom-config Fixes: custom config directories (.agents) not being synced by CLI --- config.yaml | 15 ++++++++++----- fns_cli/config.py | 6 ++++++ fns_cli/file_sync.py | 4 +++- fns_cli/setting_sync.py | 13 +++++++++++-- fns_cli/sync_engine.py | 8 +++++++- 5 files changed, 37 insertions(+), 9 deletions(-) diff --git a/config.yaml b/config.yaml index b75ed9a..d967a1b 100644 --- a/config.yaml +++ b/config.yaml @@ -1,13 +1,18 @@ server: - api: "https://your-server.zeabur.app" - token: "your_api_token" - vault: "defaultVault" + api: "http://127.0.0.1:9000" + token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsIm5pY2tuYW1lIjoiaHViaW4iLCJpcCI6IjE4MC4xNTIuNjUuOTkiLCJpc3MiOiJmYXN0LW5vdGUtc3luYy1zZXJ2aWNlIiwic3ViIjoidXNlci10b2tlbiIsImV4cCI6MTgwNzQxMzM0MCwibmJmIjoxNzc1ODc3MzQwLCJpYXQiOjE3NzU4NzczNDAsImp0aSI6IjEifQ.OgUOJXHrxxdNF9pv0tY0j5_qhBMsZsyaD_ByHqAgVtw" + vault: "my-vault" sync: - watch_path: "./vault" + watch_path: "/home/ubuntu/space/notes/my-vault" sync_notes: true sync_files: true sync_config: true + # 配置同步目录列表(以 . 开头的目录会被视为配置目录进行同步) + # Config directories to sync (dot-prefixed dirs treated as config) + config_sync_dirs: + - ".obsidian" + - ".agents" exclude_patterns: - ".git/**" - ".trash/**" @@ -21,4 +26,4 @@ client: logging: level: "INFO" - file: "" + file: "/home/ubuntu/space/notes/fns-cli.log" diff --git a/fns_cli/config.py b/fns_cli/config.py index 182f47b..331d606 100644 --- a/fns_cli/config.py +++ b/fns_cli/config.py @@ -25,6 +25,9 @@ class SyncConfig: default_factory=lambda: [".git/**", ".trash/**", "*.tmp"] ) file_chunk_size: int = 524288 + config_sync_dirs: list[str] = field( + default_factory=lambda: [".obsidian", ".agents"] + ) @dataclass @@ -91,6 +94,9 @@ def load_config(path: str) -> AppConfig: "exclude_patterns", [".git/**", ".trash/**", "*.tmp"] ), file_chunk_size=s.get("file_chunk_size", 524288), + config_sync_dirs=s.get( + "config_sync_dirs", [".obsidian", ".agents"] + ), ) if "client" in raw: diff --git a/fns_cli/file_sync.py b/fns_cli/file_sync.py index dee28c0..bd333b5 100644 --- a/fns_cli/file_sync.py +++ b/fns_cli/file_sync.py @@ -501,7 +501,9 @@ def _collect_local_files(self) -> list[dict]: if self.engine.is_excluded(rel) or rel.endswith(".md"): continue first = rel.split("/")[0] - if first.startswith("."): + # Skip dot-prefixed directories that are handled by SettingSync + # (.obsidian, .agents, and other config dirs) + if first.startswith(".") and self.config.sync.sync_config: continue if not self.config.sync.sync_files: continue diff --git a/fns_cli/setting_sync.py b/fns_cli/setting_sync.py index 2c06101..6dc26b8 100644 --- a/fns_cli/setting_sync.py +++ b/fns_cli/setting_sync.py @@ -39,15 +39,24 @@ def _extract_inner(msg_data: dict) -> dict: return msg_data if isinstance(msg_data, dict) else {} -def _is_config_path(rel: str) -> bool: +def _is_config_path(rel: str, config_sync_dirs: list[str] | None = None) -> bool: """Check whether a relative path belongs to config/settings scope. This matches the Obsidian plugin behaviour: anything inside a dot-prefixed directory (e.g. .obsidian, .agents) is treated as a setting file. Standard exclusions (.git, .trash) are handled by is_excluded() upstream. + + config_sync_dirs: configured dot-prefixed directories to treat as config + (from config.yaml, e.g. [".obsidian", ".agents"]) """ first = rel.split("/")[0] - return first.startswith(".") + if not first.startswith("."): + return False + # Check configured config sync directories + if config_sync_dirs and first in config_sync_dirs: + return True + # By default, treat all dot-prefixed dirs as config (backward compatible) + return True class SettingSync: diff --git a/fns_cli/sync_engine.py b/fns_cli/sync_engine.py index d76c037..c27a95a 100644 --- a/fns_cli/sync_engine.py +++ b/fns_cli/sync_engine.py @@ -53,7 +53,13 @@ def _is_note(self, rel_path: str) -> bool: def _is_config(self, rel_path: str) -> bool: first = rel_path.split("/")[0] - return first.startswith(".") + if not first.startswith("."): + return False + # Check if the directory is in the configured config_sync_dirs list + if first in self.config.sync.config_sync_dirs: + return True + # For other dot-prefixed dirs, check if sync_config is enabled + return self.config.sync.sync_config def _should_sync_file(self, rel_path: str) -> bool: if self._is_config(rel_path):