mirror of
https://github.com/sotashimozono/obsidian-remote-ssh.git
synced 2026-07-22 06:52:07 +00:00
5 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
40ef493c9c |
fix(config): persist third-party plugin settings across restarts (#342, #429)
Plugin settings were lost on every restart of the shadow vault — and the
real settings on the remote were destroyed in the process.
Root cause is an ordering problem the connect-time pull cannot fix.
Obsidian loads community plugins during startup, and each plugin's
`onload()` calls `Plugin.loadData()` -> reads
`<configDir>/plugins/<id>/data.json` — BEFORE remote-ssh has connected
over SSH and patched the adapter. That read therefore always hits the
real FileSystemAdapter, i.e. the LOCAL shadow disk. We cannot get in
front of it: the SSH connect is async and lands at layout-ready.
Nothing ever wrote that local copy. `saveData()` went through the
patched adapter straight to the remote, so the local file stayed empty
and every restart booted the plugin on DEFAULTS — which the plugin then
saved back, overwriting the good settings on the remote too. The plugin
CODE survived (main.js/manifest.json/styles.css are round-tripped by
PLUGIN_BINARY_FILES), which is exactly the reported asymmetry: "the
plugin is still installed, but its settings are gone".
Two changes:
1. SftpDataAdapter: write-through. A successful `<configDir>/**` write is
now mirrored onto the local shadow disk with the same bytes that
landed on the remote. The local disk becomes a warm cache, so the
startup read sees the settings this device last saved no matter how
the shadow window was launched (Connect from the source window, or
reopening the vault directly — the latter never runs preSpawnPull).
Best-effort: a mirror failure never fails the remote write. The NOTE
tree stays virtual — only the configDir is mirrored.
2. PathMapper: `plugins/*/data.json` is now per-device, redirected into
`<configDir>/user/<clientId>/`. `saveData()` fires on every settings
change, so a shared remote path would reintroduce the perpetual
write-conflict that the four core config files were redirected to
escape (
|
||
|
|
1d75cf911b |
fix(review): address /review-pr findings on #450 — preSpawnPull bypass + dotfile leak + misleading Notice
6-dimension multi-agent review surfaced one CRITICAL and several correctness/
clarity issues in the per-device-config + hide-dotfiles change:
CRITICAL — preSpawnPull bypassed PathMapper (main.ts). The pre-spawn config pull
built `toRemote` by joining the remote base only, so it read the OLD shared
identity path `<configDir>/app.json` and clobbered the freshly-redirected
per-device config on EVERY shadow-window spawn — silently undoing the fix. Now it
applies the same `PathMapper.toRemote` the shadow's adapter uses (from
`resolveClientId(settings)` + configDir), so the four config files pull from this
client's `user/<id>/` subtree; identity for non-private paths keeps
community-plugins.json / plugin binaries shared, unchanged.
MEDIUM — cross-client leak + configDir hardcode (BulkWalker). The dotfile filter
excepted a hard-coded `.obsidian`, which (a) drifted from the configurable
`app.vault.configDir` and (b) let every other client's `.obsidian/user/<id>/*`
private state (incl the 4 newly-redirected files) materialize as editable files
in the File Explorer. Dropped the exception — config is loaded off the local
shadow disk, never from the walked model, so hiding the whole `.obsidian` subtree
costs nothing and isolates foreign clients. `isHiddenPath` simplifies to the
codebase's `startsWith('.')` / `.some()` idiom.
HIGH — misleading "0 files" Notice (main.ts + BulkWalker). An all-dotfile remote
filtered to zero entries and fired "0 files found — check remotePath", the wrong
remediation. BulkWalkResult now carries `hiddenCount`; populate distinguishes
"0 visible, N hidden" with an accurate message and logs the count.
LOW — user-facing Notices + a comment said "shared-config" for files that are now
per-device; corrected. (Constant/method rename of SHARED_OBSIDIAN_CONFIG_FILES,
and a legacy→per-client one-time settings migration, are deferred — see PR notes.)
Tests: BulkWalker dotfile test updated (config dir now dropped) + fallback-path
filter test + hiddenCount assertions; PathMapper cross-machine isolation test;
stale restart-roundtrip comment corrected. tsc/lint clean; vitest 1206 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
b0e2773f1a |
fix(config+vault): per-device Obsidian config (kills the perpetual write-conflict) + hide dotfiles
Dogfooding follow-up to the lazy loading in this PR. Two real-machine problems:
1. `.obsidian` config kept raising write-conflicts, blocking work. Root cause
(read from the code, not guessed): a conflict fires when the read cache's
mtime ≠ the remote file's mtime (SftpDataAdapter.writeBuffer). The daemon
compares mtime as UnixMilli on both sides, so a single device's sequential
note edits can't perpetually conflict. What CAN: app.json / appearance.json /
core-plugins.json / hotkeys.json were the only config files NOT redirected
per-client — they sat at the shared identity path, so two sessions (or one
device across reconnects) both wrote the SAME remote file and every settings
save tripped PreconditionFailed on the other's mtime.
Fix = make them per-device: add the four to DEFAULT_PRIVATE_PATTERN_BASENAMES
so PathMapper redirects each into this client's `<configDir>/user/<id>/`
subtree (exactly what workspace.json/graph/cache already do). No shared path
→ the conflict is impossible by construction. The existing shared-config
round-trip keeps working — now on the per-client path, so each machine gets
its own remote backup + cross-session persistence. This is the user's own
ask: "config should be settable per accessing device".
2. `.julia` (and other dot-dirs) cluttered + slowed the tree. Now:
- BulkWalker drops dot-prefixed entries from every walk result (full walk AND
each lazy per-folder deepen), keeping only the vault config dir — matching
Obsidian's own default of hiding dot-names. A dot-DIR hides its whole
subtree, not just its row.
- DEFAULT_WALK_IGNORE_DIRS gains `.julia .cargo .rustup .npm .conda .gem` so
the daemon prunes these huge caches server-side (never walked/transferred),
the perf half of "hide `.julia` by default".
tsc / lint clean; vitest 1204 passed (+ per-device PathMapper assertions and a
BulkWalker dotfile-filter test; config round-trip integration tests still green
because seed-write and pull-read go through the same PathMapper). Ships beta.17.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
ceb3356242 |
feat(plugin): expose clientId + userName in settings (F19/F20)
PathMapper currently derives clientId from os.hostname(), which is fine for the single-machine case but unreadable when multiple clients share a remote vault. Surface two settings so users can pick a friendly identity per device: - clientId — overrides the per-client subtree name on the remote (.obsidian/user/<id>/...). Empty falls back to defaultClientId(). Inputs are sanitised before save so a typo can't produce an invalid path. Changing leaves the old subtree behind; a one-line description in the UI flags that. - userName — display-only for now; surfaces in connect notices and is reserved for the future multi-client presence file. Empty falls back to the OS username. Both fields live under a new "This device" section in the SettingsTab. main.ts reads them through a new resolveClientId() that's used at adapter-patch time. defaultUserName() helper added next to defaultClientId() in PathMapper.ts. Tests: 2 new sanity cases (defaultClientId is round-trip-safe under sanitize, defaultUserName is non-empty); 195 total green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2827823879 |
feat(plugin): per-client path mapping (PathMapper)
Vault-relative paths matching `.obsidian/workspace.json`, `.obsidian/
cache`, `.obsidian/types.json`, etc. are redirected into a
per-client subtree on the remote (`.obsidian/user/<client-id>/...`)
so two machines on the same vault don't trample each other's UI
state. The client id defaults to a sanitized OS hostname; multi-
instance setups can override it later when a settings field lands.
src/path/PathMapper.ts
- DEFAULT_PRIVATE_PATTERNS: workspace.json, workspace-mobile.json,
cache, cache.zlib, types.json, file-recovery.json, graph.json,
canvas.json. Errors on the side of "private" — the cost of an
unnecessary per-machine copy is low; the cost of a shared
workspace.json being clobbered is loud.
- isPrivate(path): exact match or directory-prefix match against
the patterns. Tolerates a leading slash on the input.
- isCrossingPoint(path): true when `path` is the parent of one or
more private patterns but not itself private. Today this matches
exactly `.obsidian` — listing it requires merging the shared
remote `.obsidian/` with the per-client subtree.
- toRemote / toVault: round-trip path translation. Foreign clients'
subtrees are preserved unchanged on the way back so the caller
can decide whether to filter them out.
- resolveListing(path): planning helper for `list()` — returns
{ primary, mergeFromUser, userSubtree?, hideUserDirName? }.
- defaultClientId() / sanitizeClientId(): hostname-based id with
unsafe characters replaced by hyphens; falls back to "unknown"
when sanitization yields an empty string.
src/adapter/SftpDataAdapter.ts
- Optional 6th constructor param `pathMapper`. When supplied,
`toRemote()` first runs the vault path through the mapper, then
joins with `remoteBasePath` as before. A new private `joinRemote`
exposes the join-only step for the listing planner.
- `list()` consults the mapper's `resolveListing(...)` plan: it
always lists the primary path (and prunes the `user/` directory
on a crossing-point listing), then optionally lists the user
subtree and merges the entries. User-subtree entries take
precedence on name conflicts so private files always show up
under their nominal `.obsidian/...` name.
- A small `planList(path)` shim exists so adapter tests can see
what the mapper decided without going through a real
RemoteFsClient.
src/main.ts
- `debugPatchAdapter` constructs a PathMapper with `defaultClientId`
and passes it to the adapter on every patch. The clientId is
logged so diagnostics in console.log show which subtree the
current session writes into.
Tests
- tests/PathMapper.test.ts (22 cases): isPrivate matching including
prefix/sibling distinction, isCrossingPoint, toRemote/toVault
round-trip, foreign-client subtree pass-through, resolveListing
for `.obsidian` (merge with hidden user/), private-dir
redirection, ordinary path pass-through, custom-pattern
override, and the sanitizeClientId helper for the canonical
cases (alphanumerics + dot/hyphen/underscore preserved, unsafe
characters folded, empty-after-sanitization → "unknown").
- tests/SftpDataAdapter.test.ts grew a "with PathMapper" describe
block (5 cases): writes to private paths land in the per-client
subtree, reads come back from there, non-private vault content is
not redirected, `.obsidian` listing merges shared + per-client
while hiding the user/ dir, and a private-directory listing
walks the per-client subtree.
Verification
- cd plugin && npx tsc --noEmit clean
- cd plugin && npm test 151 / 151 pass (28 new)
- cd plugin && npm run build:full all green; bundle 386 KB
(+1 KB over Phase 5-D.5).
Follow-ups
- Settings field for explicit clientId overrides (multi-instance
setups, anonymisation).
- Bootstrap: copy local `.obsidian/` skeleton into the per-client
subtree on first connect so Obsidian's first read of
workspace.json doesn't fail.
- fs.watch (Phase 5-E) and getResourcePath (Phase 5-F) are still
the next big features.
|