Skip to content
Closed
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
15 changes: 10 additions & 5 deletions config.yaml
Original file line number Diff line number Diff line change
@@ -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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (jwt): Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

Source: betterleaks

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/**"
Expand All @@ -21,4 +26,4 @@ client:

logging:
level: "INFO"
file: ""
file: "/home/ubuntu/space/notes/fns-cli.log"
6 changes: 6 additions & 0 deletions fns_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 4 additions & 2 deletions fns_cli/file_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,9 +501,11 @@ 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:
# 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 first.startswith(".") and not self.config.sync.sync_files:
if not self.config.sync.sync_files:
continue
try:
stat = fp.stat()
Expand Down
19 changes: 19 additions & 0 deletions fns_cli/folder_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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():
Expand All @@ -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:
Expand Down
13 changes: 11 additions & 2 deletions fns_cli/setting_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
16 changes: 11 additions & 5 deletions fns_cli/sync_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -230,7 +236,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)

Expand Down Expand Up @@ -284,16 +290,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)
Expand Down
Loading