Commit graph

49 commits

Author SHA1 Message Date
ml2310
b90fcf458a chore: sync package-lock.json to current version/license
Lockfile was stale at 1.0.6/ISC across several version bumps.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 17:22:16 +08:00
ml2310
2522789bdb
v1.0.10: mask refresh token field in settings (#6)
The refresh token grants full read/write/delete access to the user's Drive.
The settings-tab field displayed it in plaintext, so anyone screenshotting
or screen-sharing the NeoGDSync settings tab would leak it. Masked as a
password input with a show/hide toggle.


Claude-Session: https://claude.ai/code/session_011DimwV9zDr1ViCXRCZPxC9

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-13 15:10:16 +08:00
ml2310
b0de078e15
v1.0.9: fix data-loss and correctness bugs across the sync engine (rebased onto v1.0.8) (#5)
Correctness:
- Edits made while a sync runs are no longer dropped. Vault event handlers
  now ignore only paths the sync itself wrote (Syncer.syncWrites) instead of
  suppressing all events behind a global flag, and pending ops are cleared
  per-op right after success instead of in a bulk sweep that also wiped
  ops re-queued mid-sync.
- Renames now propagate to Drive: handlePush compares the Drive-side name
  and calls files.rename, so a local a.md -> b.md no longer leaves the old
  name on Drive (which later caused duplicate uploads after a rebuild).
- A 'modify' op with no index entry (index lost/reset) looks the file up on
  Drive by path before uploading, preventing duplicate files on Drive.
- Excluded paths queued in pendingOps are cleared instead of sticking in
  the "N pending" counter forever; exclusion logic is now shared between
  event handlers and the sync engine (src/exclude.ts), so user glob
  patterns apply consistently. computeDiff no longer reports
  newly-excluded snapshot entries as deletions.
- Files first seen via unknown Drive changes (syncedAt=0) now go through
  conflict handling instead of silently overwriting the remote copy.
- index.db is written via tmp-file + rename with recovery on load, so a
  crash mid-save can no longer silently reset the index.
- Multipart upload boundary is randomized; a file containing the fixed
  boundary string no longer corrupts the upload.

Auth / settings:
- authProxyUrl is actually used now (it was silently ignored) and is
  editable in settings; DriveApi credentials update in place so PathIndex
  never holds a stale instance; token cache is keyed by token+proxy.
- Exclude patterns are editable in settings (README advertised this but
  there was no UI).

Performance / UX:
- Force Pull and pull-new-from-Drive download with a small concurrency
  pool (settings.concurrency, previously dead config).
- Unknown-change resolution fetches each file's metadata once instead of
  twice.
- First run with a non-empty vault shows a notice explaining that an
  initial Force Push/Pull is required.

Security / hygiene:
- OAuth proxy escapes HTML in the callback pages (reflected XSS via
  ?error=...), and README now accurately describes that token refresh goes
  through the proxy and how to self-host it.
- Removed stale compiled artifacts (src/*.js) and dead code
  (listRevisions); fixed package.json metadata (MIT, author, scripts).
- Added vitest with unit tests for exclusion, snapshot diffing, and index
  persistence/recovery (17 tests).


Claude-Session: https://claude.ai/code/session_011DimwV9zDr1ViCXRCZPxC9

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-13 11:56:55 +08:00
ml2310
79b3fd0758 feat(v1.0.8): bound revision growth with active pruning (keepForever can't be unpinned)
2026-07-13: a one-time cleanup script found 3.46GB/15GB quota pinned by
keepRevisionForever=true revisions accumulated from repeated re-uploads of
unchanged binaries. Attempting to reclaim it via revisions.update
(keepForever: false) failed on 695/700 attempts with a 400 "Cannot update a
revision to false that is marked as keepForever" — Drive's API silently
does not support unpinning, contrary to its own docs. The only way to
reclaim space is revisions.delete, which is immediate and permanent.

This release prevents the backlog from re-forming:

- driveApi.ts: add deleteRevision() and pruneRevisions(driveId, keepCount),
  which lists a file's revisions and deletes the oldest keepForever ones
  beyond keepCount. Revisions without keepForever are left alone since
  Drive already reclaims those on its own ~30-day schedule.
- syncer.ts: after each push that updates an existing file, prune its
  revisions in the background (failure doesn't fail the sync).
- mime.ts: add isBinaryMime() — attachments (images/PDFs/office docs/etc.)
  are usually replaced wholesale rather than edited, so they default to a
  much lower keep count than notes.
- New settings: "Revisions to keep — notes" (default 5) and "Revisions to
  keep — attachments" (default 1), both only shown when Keep revisions is
  on. Existing installs pick up these defaults automatically via the
  settings merge in loadSettings.
2026-07-13 09:10:56 +08:00
ml2310
e5cf6f9826 chore: ignore data.json backups (contain refresh token)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 12:34:08 +08:00
ml2310
963071f833 feat(v1.0.7): persist wholesale sync failures to .neogdsync/sync-errors.log
- runSync: log per-file errors and fatal aborts to .neogdsync/sync-errors.log
  (bounded ~500 lines, excluded from sync); raise a persistent Notice when a
  sync fails wholesale (>=10 errors, 0 succeeded) — 2026-07-12 incident: the
  scheduled 5am sync errored on all 714 ops and the 4s notice was never seen.
- auth: send browser User-Agent to the token proxy (Cloudflare banned the
  default UA on 2026-06-30) — was live in data but uncommitted.
- driveApi: cast Uint8Array.buffer for newer TS lib (ArrayBufferLike).
- tsconfig: ignoreDeprecations 6.0 (baseUrl deprecation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 12:33:48 +08:00
ml2310
ad0fe5e48a Add NeoGDSync logo 2026-06-04 16:40:08 +08:00
ml2310
41a89b2cb4 chore: ignore local agent and generated artifacts 2026-05-26 17:16:06 +08:00
ml2310
58a8db02a8 fix(manifest): use portal-readable funding URLs 2026-05-26 17:06:44 +08:00
ml2310
e9ccbd5655 Add privacy policy page (docs/privacy.html) 2026-05-24 20:00:47 +08:00
ml2310
29ac958658 v1.0.5: switch to neogdsync-worker (Web Application OAuth client)
- wrangler.toml: name → neogdsync-worker
- auth.ts / types.ts: DEFAULT_PROXY_URL → neogdsync-worker.neogdsync.workers.dev
- main.ts: migration covers both legacy OGD and neogdsync-oauth URLs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:51:27 +08:00
ml2310
6dd16def1a v1.0.4: migrate legacy OGD proxy URL to self-hosted Worker on load
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:35:03 +08:00
ml2310
03243972b1 v1.0.3: add Google Drive OAuth connect flow via Cloudflare Worker
- Worker: add GET /authorize (redirect to Google consent) and GET /callback
  (exchange auth code → show refresh_token to user)
- Plugin settings: add "Connect Google Drive" button + connected/disconnect UI
- types.ts: update default authProxyUrl to neogdsync-oauth.neogdsync.workers.dev

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:20:38 +08:00
ml2310
82f63abed7 v1.0.2: fix manifest description (remove word Obsidian); add funding URLs 2026-05-24 17:09:00 +08:00
ml2310
bd5ed1ac62 Bump manifest and versions.json to v1.0.1 2026-05-24 15:50:24 +08:00
ml2310
14c5439906 Bump to v1.0.1; replace third-party OAuth proxy with self-hosted Cloudflare Worker
- Remove dependency on ogd.richardxiong.com (third-party, closed-source)
- Add oauth-proxy/worker.js: zero-dependency CF Worker, pure Web APIs, auditable
- Add oauth-proxy/wrangler.toml: deploy config, secrets via wrangler CLI
- Update DEFAULT_PROXY_URL to self-hosted workers.dev endpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 15:13:58 +08:00
ml2310
8577ce5ae5 Bump to v1.0.0; fix author fields for community plugin submission
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 18:42:27 +08:00
ml2310
7cb5986684
Fix folder renames stuck in pendingOps forever (#2)
handleRename registered TFolder moves as 'create' ops, but handlePush
early-returns on non-TFile paths without clearing the op — causing any
renamed/moved folder (e.g. Runestones/Context-Hooks) to appear as
"pending create" permanently and survive force-push.

Fix: handleRename skips pendingOps entirely for folders (they're
created on-demand by resolveParentFolder). Defense-in-depth: handlePush
now clears any stranded folder/missing-file op by adding to result.pushed.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 18:41:05 +08:00
ml2310
7d68037555 Build main.js for v0.2.2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 00:01:56 +08:00
ml2310
4a2e75d759 Bump to v0.2.2; fix new-subfolder files not syncing to other devices
When a new folder and files inside it are created on device A and synced
to Drive, device B's smart sync silently skipped all files in that folder.
Root cause: the unknownChanges loop in smartSync() skipped FOLDER_MIME
entries before files were processed, so when a file's parent folder was
also new (not yet in the local index), folderIdToPath.get(parentId)
returned undefined and the file was dropped with a console.warn.

Fix: split the loop into two passes — Pass 1 resolves new folders and
registers them in folderIdToPath, Pass 2 resolves files using the now-
complete folder map. This ensures files in newly-created subfolders
(e.g. 2026/Attachments/Nala Project/nala.pdf) are downloaded correctly
on all other devices without requiring a manual Rebuild Index.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 19:58:59 +08:00
ml2310
0e2ffe2e79 Bump to v0.2.1; fix cross-folder rename data loss + surface delete errors
1. Cross-folder rename: handlePush now calls drive.moveFile when op='create'
   and a cached driveId exists (index.rename already ran). Previously,
   updateFile only updated content — the Drive file stayed in the old parent
   folder and was deleted when that folder was trashed.
   New DriveApi.moveFile uses addParents/removeParents to atomically relocate
   the file on Drive.

2. handleDelete no longer swallows Drive API errors silently. Failures are
   now surfaced in result.errors and the index entry is preserved so the
   next sync can retry, instead of orphaning the Drive file forever.

3. getChanges fields now include 'size' and confirm 'trashed' is requested
   (aligns compiled JS with existing TS source).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 18:09:51 +08:00
ml2310
4104a479da Fix review bot: replace element.style.* with CSS class
Replace inline style assignments on folderInput with
.neogdsync-folder-input CSS class in styles.css.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 17:59:31 +08:00
ml2310
c46ece8f31 Bump to v0.2.0; fix Drive trash not propagating to other devices
smartSync was checking only c.removed (permanent delete) to trigger
local deletion, but Drive changes API returns removed=false with
file.trashed=true for soft-trashed files. Other devices never received
the deletion signal when a folder/file was trashed.

Fix: treat c.file.trashed===true as removed in driveChanged map.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 17:57:39 +08:00
ml2310
5b0415f42b Add versions.json for BRAT compatibility
BRAT requires this file to discover available plugin versions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 12:53:55 +08:00
ml2310
b12ad07460 Bump to 0.1.20; fix manifest description case
- Fix: manifest description now lowercase (smart/push/pull) to match
  community-plugins.json and satisfy ObsidianReviewBot check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 12:32:10 +08:00
ml2310
3ee7ba8886 feat: per-folder force push/pull
Add optional folder filter to Force Push and Force Pull so large vaults
can sync a single subtree without crawling or downloading everything.

- SyncModal: folder path input (vault-relative, e.g. 2026/2026-04)
- Syncer.forcePush(folder?): runs computeDiff first then filters pendingOps to prefix
- Syncer.forcePull(folder?): partial Drive index rebuild + scoped download
- PathIndex.rebuildFolder(folder): re-crawls one Drive subtree, removes stale
  entries under that prefix, leaves rest of index intact
- PathIndex.lookupFolderOnDrive: read-only Drive folder navigation (no creation)
- normFolder / matchesFolder helpers guard prefix collisions and slash variants

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 12:29:33 +08:00
ml2310
508db5e9ae Fix two trash-handling bugs that caused permanent loss + ghost resurrection
1. deleteFile now PATCHes trashed=true instead of HTTP DELETE.
   The previous DELETE was a permanent removal that bypassed Drive trash, so any
   accidental local deletion (or buggy delete propagation) became unrecoverable.
   Switching to trashed=true gives users the standard ~30-day recovery window.

2. unknownChanges in smartSync now skips changes whose file is currently trashed.
   When a file is trashed on Drive, the changes batch typically contains both an
   earlier "modified" event (removed=false) and the later trashed event. If the
   fileId was no longer in the local pathIndex (e.g. just rebuilt or a sibling
   device cleaned up), the modified event walked the unknownChanges path, fetched
   metadata, and re-pulled the file locally — silently undoing the trash. The fix
   skips such changes when file.trashed is set on the change event, and double-
   checks meta.trashed after the live metadata fetch as defence-in-depth.

DriveFileInfo and DriveChange.file gain optional `trashed?: boolean`, and the
getFileMeta + getChanges fields lists request the trashed property explicitly.

Bumps version to 0.1.19.
2026-04-28 18:23:46 +08:00
ml2310
a073ec2a13 Fix: stale pendingOps permanently blocking Drive pulls in pullNewFromDrive
Previously, pullNewFromDrive used `if (this.pendingOps[path]) continue` to skip
already-handled paths. If handlePush/handleConflict returned early (missing local
file or index entry) without adding the path to result.pushed/pulled, the stale
pendingOp was never cleared and the file was permanently blocked from downloading
on the receiving device.

Fix: build a `handled` Set from result.pushed+pulled+deleted after the main
pendingOps loop, and use `handled.has(path)` instead. Only successfully processed
paths are skipped; stale pendingOps fall through to the normal pull.

Bumps version to 0.1.18.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 01:47:49 +08:00
ml2310
02d513200a Bump version to 0.1.7 2026-04-25 14:56:29 +08:00
ml2310
b2b5272c63 Fix: silent fileId drop in smartSync and reversed conflict direction
Bug 1 (smartSync — unknown fileId silently dropped):
Drive Changes whose fileId is not in the local index were silently
discarded. This causes files created/modified on another device to
never appear on a stale-index device (e.g. after reinstall). Fix:
resolve unknown fileIds via getFileMeta + parent folder reverse-lookup,
pre-populate the index entry, and let pullNewFromDrive download them.
Logs a warn if the parent folder itself is also missing from the index.

Bug 2 (handleConflict — reversed push direction):
On conflict, local offline edits were pushed to Drive, overwriting the
newer remote version. Fix: keep Drive version as canonical (pull it
locally), save local offline edits as .conflict copy for manual merge.
No push to Drive; index updated to reflect pulled Drive mtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:11:39 +08:00
ml2310
fa4f3b5691 feat: add Clear all conflicts button to ConflictModal
Allow users to dismiss resolved or deleted conflict records without
manually editing data.json. ConflictModal now passes plugin reference
(not snapshot array) so the button can mutate live state and persist
via saveSettings().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 21:39:16 +08:00
ml2310
2906817f9e Fix: sentence case violations in settings UI text (L364, L378)
- setDesc 'Google OAuth2 refresh token' → 'Refresh token for Google Drive'
- setPlaceholder '1xGNFQGB…' → 'Root folder ID'

The linter (enforceCamelCaseLower mode) flags 'OAuth2' as camelCase and
'1xGNFQGB' because the leading digit '1' matches \p{Emoji} making the
first alpha ('x') get sentence-cased to 'X'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:07:47 +08:00
ml2310
05682785f8 Bump version to 0.1.6
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 11:55:52 +08:00
ml2310
66d33218ad Fix: revert ID→id overcorrection; eslint-plugin confirms ID is correct (acronym)
- 'Vault root folder ID' and 'Google Drive folder ID' → keep uppercase ID
- Add eslint-plugin-obsidianmd config for local pre-commit checks
- All obsidianmd lint rules now pass: 0 errors on src/*.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 11:55:45 +08:00
ml2310
22fd51f1e9 Fix: all remaining Obsidian review bot required issues (v0.1.5)
Required fixes from ObsidianReviewBot second scan:
- Sentence case: lowercase standalone 'Drive' in all UI text (commands, settings, notices)
- Remove plugin name (NeoGDSync) from all Notice messages and modal headings
- Remove setName('NeoGDSync') from settings heading (.setHeading() only)
- Sentence case: lowercase 'ID' → 'id' in setting names/descs
- Sentence case: remove emoji prefix from modal button text
- Regex: replace \x00 control character placeholder in matchGlob with
  character-by-character parser (no control characters in regex literals)
- Bump version to 0.1.5

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 11:03:34 +08:00
ml2310
ec5867b10b Fix: all Obsidian review bot required issues (v0.1.4)
Required fixes from ObsidianReviewBot scan:
- fetch → requestUrl (Obsidian API) in auth.ts and driveApi.ts
- any types → proper types (DriveChange, DriveRevision interfaces added)
- console.log → console.debug (only warn/error/debug allowed)
- .obsidian hardcoded → vault.configDir (configurable by user)
- Sentence case for all UI text (commands, settings, modals)
- createEl h2/h3 → new Setting(...).setHeading()
- Inline styles removed → styles.css with CSS classes
- Unhandled promises → void operator
- async save() with no await → removed async keyword
- Unnecessary type assertion removed
- Unused imports removed (normalizePath, Snapshot, TFile, Notice, FOLDER_MIME)
- styles.css added for theming-friendly styles

Rebuilt main.js from TypeScript source.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 22:17:46 +08:00
ml2310
a1e165969c Fix: pulled files still appear in pendingOps after sync (race condition)
Root cause: vault.modifyBinary() fires modify events asynchronously, AFTER
this.syncing is set to false in the finally block. By then handleModify()
sees syncing=false, and the snapshot still has the old mtime (vault.getFiles()
returns stale TFile stats until events are processed).

Fix: delay clearing this.syncing by 600ms. During this window, vault events
from writeLocal are still suppressed. After the delay, re-save snapshot
with fresh TFile stats (now updated by Obsidian's event processing), then
clear the flag.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 09:53:34 +08:00
ml2310
ef2016edab Fix: vault events during sync cause pulled files to re-appear in pendingOps
writeLocal()/modifyBinary() fires Obsidian vault modify/create events
asynchronously. These events fire AFTER the pendingOps cleanup, so
result.pulled files get re-added to pendingOps on the next tick.

Fix: skip handleModify/handleCreate entirely while this.syncing is true.
Events during sync are caused by the sync itself (writeLocal), not by
real user edits. Real offline edits are caught by mergeOfflineDiff on
next startup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 09:45:47 +08:00
ml2310
12df4f84a5 Bump version to 0.1.3 2026-04-14 09:30:04 +08:00
ml2310
3fbe0dc525 Fix: pulled files re-added to pendingOps after sync (v0.1.3)
writeLocal() calls vault.modifyBinary() which fires Obsidian vault modify
events. handleModify() sees the file as changed (snapshot not yet updated
at event time) and re-adds it to pendingOps. Fix: clear result.pulled from
pendingOps at the end of smartSync, forcePush, and forcePull — same as
result.pushed/deleted are already cleared.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 09:30:04 +08:00
ml2310
d6fc188152 Add MIT LICENSE 2026-04-14 00:09:51 +08:00
ml2310
f799e6cc26 Fix: pendingOps flooding on every Obsidian restart (v0.1.2)
Root causes found and fixed:
1. mergeOfflineDiff() was called before onLayoutReady — vault.getFiles()
   returns stale/zero stats before layout ready, causing all 6000+ files
   to be flagged as modified on every startup. Moved to onLayoutReady.
2. onunload() called saveSettings() before snapshot.save(), so the fresh
   snapshot was never persisted to data.json. Fixed order.
3. Snapshot contained 261 .obsidian entries (from earlier versions).
   vault.getFiles() never returns .obsidian files, so all 261 were
   flagged as 'delete' on every restart. Fixed: added .obsidian to
   exclude(), purge in setRaw(), cleaned existing data.json.
4. Snapshot now stored in data.json (loadData/saveData) for reliability.
   Removed dependency on .neogdsync/snapshot.json file write.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 00:05:44 +08:00
ml2310
e0f7b422ee Fix: snapshot saves on unload, empty snapshot treated as baseline not full diff
- onunload() now saves snapshot so restarts don't lose vault state
- mergeOfflineDiff skips flood when snapshot is empty (first run / missing file)
  instead saves current vault as baseline — prevents 6000+ fake pendingOps
- snapshot.getAll() helper method added
- These 3 fixes together prevent the recurring 'all files re-queued' problem
2026-04-13 22:00:23 +08:00
ml2310
5b1c3bfddb Fix: graceful getChanges failure — push local changes even if Drive changes fetch fails
Smart Sync now catches errors from getChanges/getStartPageToken and
continues with local push only, instead of crashing the whole sync.
2026-04-13 21:52:19 +08:00
ml2310
16b6b31f7c Fix: prevent false conflicts and duplicate uploads
- snapshot.save() now creates .neogdsync/ dir via adapter.mkdir()
- ensureDir switched from vault.createFolder to adapter.mkdir (works for hidden dirs)
- Conflict detection now compares driveChange.mtime vs index.driveMtime
  instead of relying on changesToken presence (avoids false conflicts from stale tokens)
- forcePush initializes changesToken after first push
- smartSync initializes changesToken if missing before fetching changes
2026-04-13 21:33:58 +08:00
ml2310
7d3c12908a Add MIT license 2026-04-13 02:04:58 +08:00
ml2310
4784d6f221 Fix: align description with community-plugins.json 2026-04-13 02:04:51 +08:00
ml2310
2fadb8d72e Add README 2026-04-13 02:00:24 +08:00
LM
830c2e8edb Initial release of NeoGDSync v0.1.0
Lightweight Google Drive sync plugin for Obsidian.
- Path-based Drive index, conflict detection, versioning
- Smart/push/pull sync modes
- URI handler: obsidian://neogdsync?mode=smart|push|pull
- isDesktopOnly: false (iOS/Android compatible)
2026-04-13 01:44:39 +08:00