148 ํ
์คํธ ํต๊ณผ. typecheck ํต๊ณผ. npm run build ์ฑ๊ณต.
- package.json, tsconfig.json, vitest.config.ts, esbuild.config.mjs
- manifest.json, .gitignore
- CLAUDE.md, PROGRESS.md
- src/types.ts, src/hash.ts, src/adapters/interfaces.ts
- npm install + typecheck ํต๊ณผ
- src/adapters/memory.ts (MemoryFileSystem, MemoryRemoteStorage, MemoryStateStore)
- test/hash.test.ts (8 ํ ์คํธ โ ๋นํ์ผ, ๋จ์ผ๋ธ๋ก, ๋ฉํฐ๋ธ๋ก, ๊ณต์๋ฒกํฐ)
- test/memory-adapters.test.ts (28 ํ ์คํธ โ CRUD, cursor, rev ์ถฉ๋)
- src/sync/planner.ts (classifyChange, createPlan ์์ ํจ์)
- test/planner.test.ts (21 ํ ์คํธ โ 15 classifyChange + 6 createPlan)
- src/sync/executor.ts (upload/download/delete/conflict + rev ์ถฉ๋ โ conflict ์ ํ)
- test/executor.test.ts (14 ํ ์คํธ โ ๊ฐ action + partial failure + conflict path)
- src/sync/engine.ts (base+delta ๋ณํฉ, cursor ์กฐ๊ฑด๋ถ ๊ฐฑ์ )
- test/support/sync-simulator.ts (Device, SyncSimulator)
- test/support/failing-remote.ts (FailingRemoteStorage)
- test/simulation/two-device.test.ts (9 ํ ์คํธ)
- test/simulation/network-failure.test.ts (5 ํ ์คํธ)
- test/simulation/bulk.test.ts (6 ํ ์คํธ โ 100ํ์ผ, 3๊ธฐ๊ธฐ)
- 91 ํ ์คํธ ์ ๋ถ ํต๊ณผ โ ๋๊ธฐํ ๋ก์ง ์ ํ์ฑ 100% ์ฆ๋ช
- engine์์ base+delta๋ฅผ ๋ณํฉํ์ฌ ์ ์ฒด ์๊ฒฉ ์ํ๋ฅผ ๊ตฌ์ฑํ ํ planner์ ์ ๋ฌ
- delta๋ง ์ ๋ฌํ๋ฉด "๋ณ๊ฒฝ ์๋ ์๊ฒฉ ํ์ผ"์ด "์ญ์ ๋จ"์ผ๋ก ์คํ๋จ
- cursor๋ ๋ชจ๋ ์์
์ฑ๊ณต ์์๋ง ๊ฐฑ์
- ์คํจ ์ ๊ฐ์ delta๋ฅผ ๋ค์ cycle์์ ์ฌ์์ ํ์ฌ ์๋ ์ฌ์๋
- src/hash.browser.ts (crypto.subtle ๊ธฐ๋ฐ, async)
- src/adapters/dropbox-types.ts (Dropbox API ์๋ต ํ์ )
- src/adapters/dropbox-auth.ts (OAuth PKCE)
- src/adapters/dropbox-adapter.ts (RemoteStorage ๊ตฌํ, requestUrl)
- src/adapters/vault-adapter.ts (FileSystem ๊ตฌํ, Vault API)
- src/adapters/indexeddb-store.ts (SyncStateStore ๊ตฌํ, localforage)
- test/hash-browser.test.ts (6 ํ ์คํธ โ node hash์ ๊ฒฐ๊ณผ ์ผ์น ๊ฒ์ฆ)
- 97 ํ ์คํธ ํต๊ณผ + typecheck ํต๊ณผ
- src/settings.ts (PluginSettings + DEFAULT_SETTINGS)
- src/ui/status-bar.ts (idle/syncing/success/error ์ํ)
- src/ui/conflict-modal.ts (์ถฉ๋ ์๋ฆผ)
- src/ui/settings-tab.ts (OAuth 2๋จ๊ณ + ๋๊ธฐํ ์ค์ )
- src/main.ts (Plugin ์ง์ ์ , adapter DI)
- styles.css
- executor.ts: hash.ts(node:crypto) โ hash.browser.ts(crypto.subtle) ์ ํ
- npm run build โ main.js (113KB) ์์ฑ ์ฑ๊ณต
- 97 ํ ์คํธ: ์์ ๋ก์ง + ์๋ฎฌ๋ ์ด์ ์ผ๋ก ๋๊ธฐํ ์ ํ์ฑ ์ฆ๋ช
- ํ๋ก๋์ ๋น๋: 113KB (obsidian ์ธ๋ถ ์์กด์ฑ + localforage ๋ฒ๋ค)
- ๋ค์: Obsidian์ ๋ก๋ํ์ฌ ์ค์ ๋๊ธฐํ ํ ์คํธ (์๋)
- planner.ts:
classifyChange์localDeleteIntended์ต์ ์ถ๊ฐ - planner.ts:
createPlan์localDeletedPaths?: Set<string>์ต์ ์ถ๊ฐ - ํต์ฌ ๋ณ๊ฒฝ: ์ญ์ ์๋ ์๋ ๋ถ์ฌ โ
download(missing_local_restored)(์์ ๋ฐฉํฅ) - test/planner.test.ts: ์ญ์ ์๋ ์ /๋ฌด ๋ถ๊ธฐ ํ ์คํธ (+4 ์ผ์ด์ค)
- src/sync/guards.ts:
checkDeleteGuard()์์ ํจ์ (๊ฐ์ ๊ธฐ๋ฐ ์ฐจ๋จ) - src/ui/delete-confirm-modal.ts: ๋๋ ์ญ์ ํ์ธ ๋ชจ๋ฌ
- test/guards.test.ts: 7 ํ ์คํธ (ํต๊ณผ/์ฐจ๋จ/๋นํ์ฑํ/threshold ๊ฒฝ๊ณ)
- executor.ts: conflict ์ ๋ต 3๋ถ๊ธฐ โ keep_both / newest(mtime) / manual(์ฝ๋ฐฑ)
- src/ui/conflict-modal.ts: "Keep local" / "Keep remote" ๋ ๋ฒํผ
- test/executor.test.ts: newest/manual ์ ๋ต ํ ์คํธ (+6 ์ผ์ด์ค)
- engine.ts:
deletedPaths๊ด๋ฆฌ + planner ์ ๋ฌ + ๊ฐ๋ ์ ์ฉ + ์ญ์ ๋ก๊ทธ API - main.ts:
vault.on('delete/rename')์ด๋ฒคํธ ์ถ์ +store.setMeta("deleteLog")์์ํ - settings.ts:
deleteProtection(true),deleteThreshold(5)์ถ๊ฐ - test/support/sync-simulator.ts: Device.deleteFile()์ ์ญ์ ๋ก๊ทธ ์๋ ๊ธฐ๋ก
- test/simulation/delete-protection.test.ts: 9 ์๋ฎฌ๋ ์ด์ ํ ์คํธ
- dropbox-auth.ts:
generateState(),buildAuthUrl()๊ฐ์ฒด ํ๋ผ๋ฏธํฐ,redirectUri์ง์ - settings.ts:
__DROPBOX_APP_KEY__๋น๋ ํ์ ๋ณ์,useCustomAppKey,getEffectiveAppKey() - main.ts: ๋ฐ์คํฌํฑ
obsidian://ํ๋กํ ์ฝ ํธ๋ค๋ฌ + state ๊ฒ์ฆ (CSRF ๋ฐฉ์ง) - settings-tab.ts: ๋ฐ์คํฌํฑ ์ํด๋ฆญ / ๋ชจ๋ฐ์ผ 2๋จ๊ณ ๋ถ๊ธฐ, Advanced ์น์
- test/dropbox-auth.test.ts: 10 ํ ์คํธ
- 133 ํ ์คํธ: ๊ธฐ์กด 97 + ์ ๊ท 36 (์ญ์ ๋ณดํธ 20 + ์ถฉ๋ ์ ๋ต 6 + auth 10)
- 3๊ณ์ธต ๋ฐฉ์ด: ์ญ์ ์ด๋ฒคํธ ์ถ์ โ ๋๋ ์ญ์ ๊ฐ๋ โ Dropbox ์์ฒด ํด์งํต
- ์ถฉ๋ ์ ๋ต: keep_both(๊ธฐ๋ณธ) / newest(mtime) / manual(๋ชจ๋ฌ)
- ์ธ์ฆ: ๋ฐ์คํฌํฑ ์ํด๋ฆญ (obsidian:// redirect) + ๋ชจ๋ฐ์ผ 2๋จ๊ณ ์๋
-
obsidian-dropbox-syncโdropbox-sync์ ์ญ ๊ต์ฒด (manifest, main.ts, indexeddb-store.ts)
- dropbox-adapter.ts: rpcCall/download/upload์ 429 retry (์ต๋ 3ํ, retryAfter ๊ธฐ๋ฐ)
- test/dropbox-adapter-retry.test.ts: 5 ํ ์คํธ
- engine.ts:
DropboxCursorResetErrorcatch โ cursor ์ด๊ธฐํ + ์ ์ฒด ์ฌ์ค์บ
- src/adapters/vault-file-store.ts: vault ๋ด JSON ํ์ผ ๊ธฐ๋ฐ SyncStateStore
- test/vault-file-store.test.ts: 10 ํ ์คํธ
- main.ts:
Platform.isIosAppโ VaultFileStore, ๊ทธ ์ธ โ IndexedDBStore - test/mocks/obsidian.ts:
Platform.isIosApp์ถ๊ฐ
- README.md, LICENSE (MIT), versions.json
- .github/workflows/release.yml (tag push โ test โ build โ release)
- 148 ํ ์คํธ: ๊ธฐ์กด 133 + ์ ๊ท 15 (retry 5 + VaultFileStore 10)
- Plugin ID:
dropbox-sync(Community Plugin ๋ฑ๋ก ์๊ตฌ์ฌํญ ์ถฉ์กฑ) - API ์์ ์ฑ: rate limit ์๋ ์ฌ์๋ + cursor ๋ง๋ฃ ์๋ ๋ณต๊ตฌ
- iOS ์ง์: VaultFileStore fallback (IndexedDB ๋ถ์์ ๋๋น)
- ๋ฐฐํฌ: README, LICENSE, versions.json, GitHub Release workflow
| ํญ๋ชฉ | ๊ตฌํ | ์ฝ๋ ์์น |
|---|---|---|
| Dropbox ๋ฐ์คํฌํฑ ํด๋ผ์ด์ธํธ ๊ฐ์ญ | content_hash ๋น๊ต๋ก mtime ์คํ ์์ฒ ์ฐจ๋จ. ๋ฐ์คํฌํฑ ํด๋ผ์ด์ธํธ๊ฐ ๋ก์ปฌ์ ๋จผ์ ์ ๋ฐ์ดํธํด๋ hash ๋์ผ โ noop | planner.ts:50 |
| ์ญ์ ์ ํ ๋ฐฉํฅ | base state + localDeleteIntended ํ๋๊ทธ. ์ญ์ ์๋ ์๋ ๋ถ์ฌ โ download(๋ณต๊ตฌ). ์ญ์ +์์ ๊ต์ฐจ โ ๋ณ๊ฒฝ ์ฐ์ |
planner.ts:90-105 |
| ๊ฒฝ๋ก ๋์๋ฌธ์ | pathLower ํค ์ ๊ทํ. Dropbox case-insensitive ๋์๊ณผ ์ผ๊ด |
planner.ts:128-142 |
| ๋ฐ์ฐ๊ธฐ ๋ฐฉ์ง | download ํ hash ๊ฒ์ฆ + Vault API atomic write ์์ | executor.ts:86-107 |
| ํ ํฐ ์ ์ฅ/๊ฐฑ์ | data.json์ ์ ์ฅ, refresh ์๋ ๊ฐฑ์ . .obsidian ๋๊ธฐํ์ ๋ถ๋ฆฌ | main.ts:369-373 |
| Rate limit | 429 โ ์ต๋ 3ํ retry, retryAfter ์ค์, exponential backoff + jitter | dropbox-adapter.ts |
| Cursor ๋ง๋ฃ | DropboxCursorResetError catch โ ์ ์ฒด ์ฌ์ค์บ ์๋ ๋ณต๊ตฌ | engine.ts:77-87 |
| ํ๋ฌ๊ทธ์ธ ๋ฏธ์คํ ์ค ์ญ์ | ์ด๋ฒคํธ ๋ฏธ์์ง โ ์ญ์ ์๋ ์์ โ download(๋ณต๊ตฌ). ์์ ๋ฐฉํฅ ์คํ | planner.ts:100-101 |
| # | ํญ๋ชฉ | ์ํ๋ | ํ์ฌ ์ํ |
|---|---|---|---|
| 1 | ์ฑํฌ ์ค ํ์ฑ ํ์ผ ํธ์ง | ๋์ | executor๊ฐ write ์ getActiveFile() ๋ฏธ์ฒดํฌ. ํธ์ง ์ค ๋ฎ์ด์ฐ๊ธฐ โ ๋ฐ์ดํฐ ์ ์ค ๊ฐ๋ฅ |
| 2 | ๋๋ ๋ณ๊ฒฝ ์์ฐจ ์คํ | ์ค๊ฐ | for...of ์ง๋ ฌ ์ฒ๋ฆฌ. 500๊ฐ ํ์ผ ์ ๋๋ฆผ + UI ๋ธ๋กํน ๊ฐ๋ฅ |
| 3 | cursor all-or-nothing | ์ค๊ฐ | 200/500 ์ฑ๊ณตํด๋ cursor ๋ฏธ๊ฐฑ์ โ ์ ๋ถ ์ฌ์ฒ๋ฆฌ. base ๊ฐฑ์ ์ ๋จ |
| 4 | ์งํ ํ์ ์์ | ์ค๊ฐ | ๋๋ ์ฑํฌ ์ ์ฌ์ฉ์ ํผ๋๋ฐฑ ์์. statusBar๋ syncing/success๋ง |
| 5 | ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋จ (๋ชจ๋ฐ์ผ) | ์ค๊ฐ | AbortController ์์. ์ค๋จ ์ ์งํ ์ค ์์ ์ํ ๋ถ๋ช ํ |
| 6 | ํน์๋ฌธ์ ๊ฒ์ฆ | ์ค๊ฐ | Dropbox ๊ธ์ง ๋ฌธ์ ํํฐ๋ง ์์ |
| 7 | ๋คํธ์ํฌ ์จ/์คํ ๊ฐ์ง | ๋ฎ์ | navigator.onLine ๋ฏธ์ฌ์ฉ. ์คํ๋ผ์ธ ์์๋ ์ฑํฌ ์๋ |
| 8 | ํ ํฐ revoke ์๋ฆผ | ๋ฎ์ | refresh_token revoke ์ ์กฐ์ฉํ ์คํจ. ์ฌ์ฉ์ ๋ฏธํต์ง |
| 9 | ์ฑ ์์ ๋ ์ด์ค | ๋ฎ์ | onLayoutReady ๊ตฌํ๋จ. editLock์ ์์ผ๋ content_hash๋ก ์ต์ข ๋ณดํธ |
๋ํ์ ์ปค๋ฎค๋ํฐ ํ๋ฌ๊ทธ์ธ remotely-save์์ ๊ตฌ์กฐ์ ์ฐจ์ด. ๋ฒ์ฉ์ฑ(9๊ฐ ๋ฐฑ์๋)์ ํํ ๋๊ฐ๋ก Dropbox ๊ณ ์ ๊ธฐ๋ฅ์ ํ์ฉํ์ง ๋ชปํจ.
์ฐ๋ฆฌ๊ฐ ์์๋ ๋ถ๋ถ:
| ์์ญ | remotely-save | dropbox-sync |
|---|---|---|
| ๋ณ๊ฒฝ ๊ฐ์ง | mtime + size (๊ฐ์ ํฌ๊ธฐ๋ฉด ๋ณ๊ฒฝ ๋ชป ๊ฐ์ง) | content_hash (๋ด์ฉ ๊ธฐ๋ฐ) |
| ์ฆ๋ถ ๋๊ธฐํ | ๋งค๋ฒ ์ ์ฒด ์ค์บ | cursor ๊ธฐ๋ฐ delta |
| ์ ๋ก๋ ์์ | mode: "overwrite" ๋ฌด์กฐ๊ฑด ๋ฎ์ด์ฐ๊ธฐ |
rev ๋๊ด์ ์ ๊ธ (์๋ฒ ์ถฉ๋ ๊ฐ์ง) |
| ์ถฉ๋ ์ฒ๋ฆฌ | keep_newer ๊ธฐ๋ณธ โ ์ง ์ชฝ ํต๋ณด ์์ด ์ญ์ | conflict ํ์ผ ๋ณด์กด + ์ฌ์ฉ์ ํ์ธ |
| ์ญ์ ํ๋จ | prevSync ๊ธฐ๋ฐ์ด๋ ๋ถํ ๋ฒ๊ทธ ๋ค์ | deleteIntended + ์์ ๋ฐฉํฅ ๊ธฐ๋ณธ๊ฐ |
| ์ฝ๋ ๊ตฌ์กฐ | 40+ ๋ธ๋์น ๊ฑฐ๋ ํจ์ | planner/executor ์์ ํจ์ ๋ถ๋ฆฌ |
| ํ ์คํธ | ~112๊ฐ (ํต์ฌ ๋๊ธฐํ ๋ก์ง ๋ฏธํ ์คํธ) | 148๊ฐ (์๋ฎฌ๋ ์ด์ ํฌํจ) |
remotely-save์ ์๋ ค์ง ๋ฌธ์ :
- mtime ๊ธฐ๋ฐ ์คํ โ macOS/iOS ๊ฐ mtime ๋ถ์ผ์น๋ก ๋งค๋ฒ ์ ์ฒด ์ฌ๋๊ธฐํ (#575)
- ์ญ์ ํ์ผ ๋ถํ (#611, #985)
- smart_conflict ๋ฐ์ดํฐ ์ ์ค (#697)
- rate limit ํญ๋ฐ โ ๋๊ท๋ชจ vault์์ 429 ๋ฐ๋ณต (#1026)
- README์ "ALWAYS backup your vault before using" ๊ฒฝ๊ณ
remotely-save๊ฐ ์์๋ ๋ถ๋ถ:
- ์ปค๋ฎค๋ํฐ ํ๋ฌ๊ทธ์ธ ๋ฑ๋ก ์๋ฃ, ์ค์ฌ์ฉ์ ๋ค์
- ์คํ๊ฒฝ ์ค๋ ์ด์ โ ์ฃ์ง์ผ์ด์ค ๋ฐ๊ฒฌ ์ถ์
- .obsidian ๊ธฐ๊ธฐ๋ณ ์ค์ ๋ถ๋ฆฌ ๋ฑ ์ธ๋ฐํ ์ต์
๊ณตํต ๋ฏธ๊ตฌํ: ํ์ฑ ํ์ผ ๋ณดํธ, ๋ณ๋ ฌ ์คํ, ๋ฐฑ๊ทธ๋ผ์ด๋ ๋๊ธฐํ (Obsidian ํ๋ซํผ ์ ์ฝ)
ํ์ฌ executor๊ฐ fs.write() ์ ํ์ฑ ํธ์ง ์ฌ๋ถ๋ฅผ ํ์ธํ์ง ์์.
์๊ฒฉ ๋ฒ์ ์ ๋ค์ด๋ก๋ํ์ฌ ๋ก์ปฌ์ ๋ฎ์ด์ธ ๋, ์ฌ์ฉ์๊ฐ ์๋ํฐ์์ ํด๋น ํ์ผ์ ํธ์ง ์ค์ด๋ฉด ๋ด์ฉ์ด ์ ์ค๋ ์ ์์.
- executor.ts download/conflict: write ์
app.workspace.getActiveFile()์ฒดํฌ - ํ์ฑ ํ์ผ์ด๋ฉด conflict๋ก ๋ถ๋ฅํ๊ฑฐ๋ ์ฑํฌ ์ง์ฐ
- Obsidian์
vault.modifyBinary()์ ์๋ํฐ in-memory ์ํ์ ์ํธ์์ฉ ๊ฒ์ฆ ํ์
ํ์ฌ executor๊ฐ plan.items๋ฅผ for...of๋ก ์์ฐจ ์คํ.
500๊ฐ ํ์ผ ๋ณ๊ฒฝ ์ ์ง๋ ฌ ์ฒ๋ฆฌ๋ก ๋๋ฆฌ๊ณ , ๋ฉ์ธ ์ค๋ ๋ ๋ธ๋กํน ๊ฐ๋ฅ.
- executor์ p-queue ๋์ (concurrency ์ ํ ๋ณ๋ ฌ ์คํ)
- ์งํ ๋ฐ + ์์ธ ์ํ ํ์ (ํ์ฌ N/M ํ์ผ)
- cursor all-or-nothing ์ํ ๊ฒํ : ์ฑ๊ณต ํญ๋ชฉ๋ง base ๊ฐฑ์ ์ ์ด๋ฏธ ๊ตฌํ๋จ. cursor ๋ถ๋ถ ๊ฐฑ์ ์ Dropbox API ํน์ฑ์ ๋ถ๊ฐ โ ํ์ฌ ๋ฐฉ์ ์ ์งํ๋ ์ฑ๋ฅ์ผ๋ก ๋ณด์
๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋จ: ๋ชจ๋ฐ์ผ์์ ์ฑ ์ ํ ์ ์ฑํฌ ์ค๋จ ๊ฐ๋ฅ.
- AbortController/signal ๋์ โ ์ค๋จ ์ ์งํ ์ค ์์ ๋ง ์คํจ ์ฒ๋ฆฌ
- ์ด๋ฏธ ์ฑ๊ณตํ ํญ๋ชฉ์ base ์ ๋ฐ์ดํธ๋ ์ ์ง๋จ (ํ์ฌ ์ค๊ณ๋ก ์์ )
- cursor ๋ฏธ๊ฐฑ์ โ ๋ค์ ์คํ ์ ์์ฐ ์ฌ์๋
๋คํธ์ํฌ ์ํ ๊ฐ์ง:
navigator.onLine๋๋ Obsidian ๋คํธ์ํฌ ์ด๋ฒคํธ๋ก ์จ/์คํ ๊ฐ์ง- ์คํ๋ผ์ธ ์ ์ฑํฌ ์คํต โ ์จ๋ผ์ธ ๋ณต๊ท ์ ์ฆ์ ์ฑํฌ
์ฑ ์์ ๋ ์ด์ค:
- onLayoutReady ์ดํ์๋ง ์ด๋ฒคํธ ๋ฑ๋ก (๊ตฌํ๋จ)
- ์ด๊ธฐ ์ฑํฌ ์ค ๋ฐ์ํ vault ์ด๋ฒคํธ๊ฐ planner์ ์ ํํ ๋ฐ์๋๋์ง ๊ฒ์ฆ
ํน์๋ฌธ์ ํํฐ๋ง: Dropbox๊ฐ ํ์ฉํ์ง ์๋ ํ์ผ๋ช ๋ฌธ์ ๊ฒ์ฆ.
- upload ์ ํ์ผ๋ช sanitize ๋๋ ์ฌ์ฉ์ ์๋ฆผ
- Dropbox ๊ธ์ง ๋ฌธ์: NUL, /, ์ ์ด๋ฌธ์ ๋ฑ
ํ ํฐ revoke ๊ฐ์ง:
- refresh_token์ด revoke๋ ๊ฒฝ์ฐ (์ฌ์ฉ์๊ฐ Dropbox ์ฑ ๊ถํ ํด์ ) ํ์ฌ ์กฐ์ฉํ ์คํจ
- ์ธ์ฆ ์คํจ ์ Notice๋ก "์ฌ์ธ์ฆ ํ์" ์๋ฆผ + ์ค์ ํญ ์ ๋
- Dropbox
/2/files/list_folder/longpoll๋ก ๋ณ๊ฒฝ ์ฆ์ ๊ฐ์ง - ๋ฐ์คํฌํฑ ์ ์ฉ (๋ชจ๋ฐ์ผ์ ํด๋ง ์ ์ง)
- excludePatterns ์ค์ ์ ์ด๋ฏธ ์กด์ฌ (settings.ts)
- planner์์ ์ ์ธ ํจํด ์ ์ฉ ๋ก์ง ๊ตฌํ ํ์
- ์ ์์ ์ฑ ํญ๋ชฉ ํด๊ฒฐ ํ ์งํ
- obsidianmd/obsidian-releases PR ์ ์ถ