Skip to content

Feature/webdav sync#189

Open
jdkruzr wants to merge 51 commits intoEthran:mainfrom
jdkruzr:feature/webdav-sync
Open

Feature/webdav sync#189
jdkruzr wants to merge 51 commits intoEthran:mainfrom
jdkruzr:feature/webdav-sync

Conversation

@jdkruzr
Copy link
Contributor

@jdkruzr jdkruzr commented Dec 25, 2025

Outstanding Issues:

  • Needs image/other attachment hashing solution to avoid duplication (requires messing with database, tabling for now)
  • Needs folder deletion/cascade solution (also requires messing with database)

jdkruzr added 30 commits December 11, 2025 15:59
Implements Phase 1 of WebDAV synchronization feature:
- Add dependencies: WorkManager, OkHttp, security-crypto
- Add network permissions (INTERNET, ACCESS_NETWORK_STATE)
- Create SyncSettings data class with sync configuration
- Implement CredentialManager for encrypted credential storage
- Implement WebDAVClient with full WebDAV operations
  - Basic authentication support
  - PROPFIND, PUT, GET, DELETE, MKCOL methods
  - Directory creation and file streaming support
Implements Phase 2 of WebDAV synchronization:
- FolderSerializer: Convert folder hierarchy to/from folders.json
- NotebookSerializer: Convert notebooks/pages/strokes/images to/from JSON
  - Handles manifest.json for notebook metadata
  - Handles per-page JSON with all strokes and images
  - Converts absolute URIs to relative paths for WebDAV storage
  - Supports ISO 8601 timestamps for conflict resolution

Phase 2 complete. Next: SyncEngine for orchestrating sync operations.
Creates skeleton implementations for remaining sync components:

Core Sync Components:
- SyncEngine: Core orchestrator with stub methods for sync operations
- ConnectivityChecker: Network state monitoring (complete)
- SyncWorker: Background periodic sync via WorkManager
- SyncScheduler: Helper to enable/disable periodic sync

UI Integration:
- Add "Sync" tab to Settings UI
- Stub SyncSettings composable with basic toggle

All components compile and have proper structure. Ready to fill in
implementation details incrementally. TODOs mark where logic needs
to be added.
- Fix KvProxy import path (com.ethran.notable.data.db.KvProxy)
- Replace HTTP_METHOD_NOT_ALLOWED with constant 405
- Correct package imports in SyncEngine
Add full-featured sync settings interface with:
- Server URL, username, password input fields
- Test Connection button with success/failure feedback
- Enable/disable sync toggle
- Auto-sync toggle (enables/disables WorkManager)
- Sync on note close toggle
- Manual "Sync Now" button
- Last sync timestamp display
- Encrypted credential storage via CredentialManager
- Proper styling matching app's design patterns

All settings are functional and persist correctly. UI is ready
for actual sync implementation.
showHint takes (text, scope) not (context, text)
Log URL and credentials being used, response codes, and errors
to help diagnose connection issues
Use Dispatchers.IO for network calls (Test Connection, Sync Now).
Switch back to Dispatchers.Main for UI updates using withContext.

Fixes: NetworkOnMainThreadException when testing WebDAV connection
Core sync implementation:
- syncAllNotebooks(): Orchestrates full sync of folders + notebooks
- syncFolders(): Bidirectional folder hierarchy sync with merge
- syncNotebook(): Per-notebook sync with last-write-wins conflict resolution
- uploadNotebook/uploadPage(): Upload notebook data and files to WebDAV
- downloadNotebook/downloadPage(): Download notebook data and files from WebDAV
- Image and background file handling (upload/download)

Database enhancements:
- Add getAll() to FolderDao/FolderRepository
- Add getAll() to NotebookDao/BookRepository

Sync features:
- Timestamp-based conflict resolution (last-write-wins)
- Full page overwrite on conflict (no partial merge)
- Image file sync with local path resolution
- Custom background sync (skips native templates)
- Comprehensive error handling and logging
- Resilient to partial failures (continues if one notebook fails)

Quick Pages sync still TODO (marked in code).
- Remove context parameter from ensureBackgroundsFolder/ensureImagesFolder
- Fix image URI updating (create new Image objects instead of reassigning val)
- Use updatedImages when saving to database
- Handle nullable URI checks properly
Safety Features for Initial Sync Setup:
- forceUploadAll(): Delete server data, upload all local notebooks/folders
- forceDownloadAll(): Delete local data, download all from server

UI:
- "Replace Server with Local Data" button (orange warning)
- "Replace Local with Server Data" button (red warning)
- Confirmation dialogs with clear warnings
- Prevents accidental data loss on fresh device sync

Use cases:
- Setting up sync for first time
- Adding new device to existing sync
- Recovering from sync conflicts
- Resetting sync environment
Log notebook discovery, download attempts, and directory listings
to diagnose sync issues
Features:
- SyncLogger: Maintains last 50 sync log entries in memory
- Live log display in Settings UI (last 20 entries)
- Color-coded: green (info), orange (warning), red (error)
- Auto-scrolls to bottom as new logs arrive
- Clear button to reset logs
- Monospace font for readability

Makes debugging sync issues much easier for end users without
needing to check Logcat.
Fixes:
- forceUploadAll: Delete only notebook directories, not entire /Notable folder
- Add detailed SyncLogger calls throughout force operations
- Add logging to upload/download operations with notebook titles

Log viewer now shows:
- Exactly which notebooks are being uploaded/downloaded
- Success/failure for each notebook
- Number of pages per notebook
- Any errors encountered

This makes debugging sync issues much easier and prevents
accidentally wiping the entire sync directory.
Add auto-sync trigger when switching pages in editor:
- Hook into EditorControlTower.switchPage()
- Pass context to EditorControlTower constructor
- Trigger SyncEngine.syncNotebook() when leaving a page
- Only syncs if enabled in settings
- Runs in background on IO dispatcher
- Logs to SyncLogger for visibility

Now sync-on-close setting is functional.
Show clearly in sync log:
- ↑ Uploading (local newer or doesn't exist on server)
- ↓ Downloading (remote newer)
- Timestamp comparison for each decision
- Which path is taken for each notebook

This will help diagnose why sync only goes up and never down.
Create AppRepository instance to properly access PageRepository
in triggerSyncForPage method.
Previous logic: equal timestamps → upload
New logic: equal timestamps → skip (no changes needed)

Now properly handles three cases:
- Remote newer → download
- Local newer → upload
- Equal → skip

This prevents unnecessary re-uploads when nothing has changed.
Move sync trigger to EditorView's DisposableEffect.onDispose
which fires when navigating away from the editor.

Now sync-on-close actually works when you:
- Navigate back to library
- Switch to a different notebook
- Exit the app

Will show "Auto-syncing on editor close" in sync log.
Use new CoroutineScope instead of composition scope in onDispose.
The composition scope gets cancelled during disposal, causing
"rememberCoroutineScope left the composition" error.

Now sync-on-close will actually complete.
Log when credentials are loaded or missing to help diagnose
AUTH_ERROR issues.
Show full Date.time millisecond values and difference to diagnose
why timestamps that appear equal are being treated as different.

This should reveal if there are sub-second differences causing
unnecessary uploads.
Add null-safe access to remoteUpdatedAt.time
Problem: ISO 8601 serialization loses milliseconds, causing
local timestamps to always be slightly newer (100-500ms).

Solution: Ignore differences < 1 second (1000ms)
- Difference < -1000ms: remote newer → download
- Difference > 1000ms: local newer → upload
- Within ±1 second: no significant change → skip

This prevents unnecessary re-uploads of unchanged notebooks while
still detecting real changes.
After syncing local notebooks, now scans server for notebooks
that don't exist locally and downloads them.

Flow:
1. Sync folders
2. Sync all local notebooks (upload/download/skip)
3. Discover new notebooks on server
4. Download any that don't exist locally

This enables proper bidirectional sync - devices can now discover
and download notebooks created on other devices.
Copy link
Owner

@Ethran Ethran left a comment

Choose a reason for hiding this comment

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

Initial review, I only glanced at the code. Letter I'll actually look at code in folder sync.

Please clean up the stuff left behind by AI.
In setting syncing setting should be implemented in separate file. Probably other setting also should be done this way, but don't change it in this PR.

@Ethran
Copy link
Owner

Ethran commented Dec 25, 2025

Also, as I understand it should work with nextcloud?

If so, please give a short instruction how to setup it, etc. I will test it on nextcloud probably (if I have still access to university instance).

Removing username from logger as suggested.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@jdkruzr
Copy link
Contributor Author

jdkruzr commented Dec 25, 2025

Also, as I understand it should work with nextcloud?

If so, please give a short instruction how to setup it, etc. I will test it on nextcloud probably (if I have still access to university instance).

Do you mean how to set up Nextcloud? Or something else? And, do you want me to just create a "how to use sync" .md in /docs? That's probably easiest.

@Ethran
Copy link
Owner

Ethran commented Dec 25, 2025

It would be best to have two files:

  1. How to use sync, with Nextcloud as the example, to be linked in the README. If user needs to change default settings of nextcloud include it. Just how to connect app to the server, what to have in mind etc.

  2. overview of how sync works for developers.
    If AI is used to generate it, ensure the information is accurate and as compact as possible, without unnecessary details. Something similar to what already is written in docs.

Copy link
Owner

@Ethran Ethran left a comment

Choose a reason for hiding this comment

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

Also, how do you handle conflicts in stroke data?

I feel like there should be some function that decide how to resolve conflicts, and it should be reusable for imports, if possible.

https://github.com/Ethran/notable/blob/main/app/src/main/java/com/ethran/notable/io/ImportEngine.kt

Whenever possible reuse functions, it makes code easier to maintain.

Copy link
Owner

@Ethran Ethran left a comment

Choose a reason for hiding this comment

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

I would say that first we should settle on the format for notable pages. Make proper documentation explaining how its handle etc. Add ability for app to import export it, handle marge conflicts. Then syncing should reuse this format.

It should be internal to notable, but it has to be well thought out. It also should be easily modifiable as db schema changes. Now you putted all the data in json, its really bad approach for space, speed, and possibly future modifications. The strokes points should be encoded in SB1 -- see docs.

I think it would be best to implement the basic sync format in a separate PR, then update the WebDAV sync to use that format. The format doesn't need to be perfect initially, but it should be flexible and easy to modify. Labeling it "experimental" for the first few releases is a sensible approach: it lets us make breaking changes without requiring migrations. That said, it would be good to start from a solid foundation.

I will be going on vacation for couple of days soon, so I won't be answering probably.

jdkruzr and others added 3 commits January 17, 2026 16:26
@jdkruzr
Copy link
Contributor Author

jdkruzr commented Jan 17, 2026

OK just for clarification: what do you mean by conflicts in stroke data? Do you mean two different devices making changes? Because since this isn't a multiuser system, my inclination is just to go with "most recent page wins."

@Ethran
Copy link
Owner

Ethran commented Jan 18, 2026

Multiple devices making changes, changes done on server while device was offline, etc. A lot of things might get wrong, there might be a lot of edge cases:
Example: One device removes page, other changes something on this page. What should happen when both devices go online and try to sync with server?

I was thinking that there should be some strategies for merging predefined:

enum class ImportConflictStrategy {

So in questionable situations we know what to do (ie always prioritise device, ask case by case, etc) I didn't implemented it yet, so you can change it a little bit, if you have better idea. But I would expect some functions that will take data from device and database, and merging settings, and return merged data. Try to make it as much generic as possible without too much hassle, so it can be latter reused.

Also, for now one strategy will be enough, ie take newest changes, but leave space for other to be added later.

And, I really think that debugging will be much easier if you add first possibility to import new format, then to merge changes from the linked file, and then to reuse those for syncing. Just testing one thing at the time.

Also, I'm will have quite busy two weeks, so I might not look into code, but I will try to answer questions.

@jdkruzr
Copy link
Contributor Author

jdkruzr commented Jan 19, 2026

Multiple devices making changes, changes done on server while device was offline, etc. A lot of things might get wrong, there might be a lot of edge cases: Example: One device removes page, other changes something on this page. What should happen when both devices go online and try to sync with server?

I was thinking that there should be some strategies for merging predefined:

enum class ImportConflictStrategy {

So in questionable situations we know what to do (ie always prioritise device, ask case by case, etc) I didn't implemented it yet, so you can change it a little bit, if you have better idea. But I would expect some functions that will take data from device and database, and merging settings, and return merged data. Try to make it as much generic as possible without too much hassle, so it can be latter reused.
Also, for now one strategy will be enough, ie take newest changes, but leave space for other to be added later.

And, I really think that debugging will be much easier if you add first possibility to import new format, then to merge changes from the linked file, and then to reuse those for syncing. Just testing one thing at the time.

Also, I'm will have quite busy two weeks, so I might not look into code, but I will try to answer questions.

OK, got you I think. I don't think we need a new format at all; I've gone ahead and switched to SB1 and it works quite well imo. For now at least I've defaulted to "last writer wins" including preventing changes to notes saved after deletion from being lost. I don't get the sense there's going to be huge demand for multiple conflict resolution strategies as long as it works consistently.

@Ethran
Copy link
Owner

Ethran commented Jan 19, 2026

I think that it would be best to get the some rough sketch on how the sync is working, I saw that you added documentation for users, that's great.

When you add it I will review this, so you you will know if concept is alright. It will also be needed for latter development. The purpose for this file should be to get an idea how the sync works, how its structured, without looking at the code itself.

Proper review of the code will have to wait a little bit, in February I should be able to do it.

Copy link
Owner

@Ethran Ethran left a comment

Choose a reason for hiding this comment

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

Please make sure that all the information on different providers are correct, or mark [UNTESTED] if you cant.

Try to get more data from refused connection, give user more information what want wrong.

Give more troubleshooting information, I spend a lot of time trying to find what is wrong. URL: https://your-nextcloud.com/remote.php/dav/files/ it claimed that is correct, but on trying to create folder it failed, with 403.

For now I tested only with my phone, after reviewing code I will check on my main device.

### 2. Configure Notable

1. Open Notable
2. Go to **Settings** (three-line menu icon)
Copy link
Owner

Choose a reason for hiding this comment

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

it's a gear wheel

1. **Nextcloud** (Recommended for self-hosting)
- Free and open source
- Full control over your data
- URL format: `https://your-nextcloud.com/remote.php/dav/files/username/`
Copy link
Owner

@Ethran Ethran Jan 23, 2026

Choose a reason for hiding this comment

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

there should not be a username in the link, at list in my case.
in my case this worked:
https://your-nextcloud.com/remote.php/webdav/

You should probably add instructions "how to know that my url is correct?" in troubleshooting section.
ie for next cloud going to the url should result in blank page or
"This is the WebDAV interface. It can only be accessed by WebDAV clients such as the Nextcloud desktop sync client."


### Connection Failed

**Problem**: Test connection fails with "✗ Connection failed"
Copy link
Owner

Choose a reason for hiding this comment

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

The error message should be more descriptive, try showing:
something like:
"url does not exists"
"wrong password/username"

@Ethran
Copy link
Owner

Ethran commented Jan 25, 2026

I'm not sure how you check which file was modified, I suppose that you aren't using ETag, which would require db changes, and hashes I think are not available over WebDav. I think later (NOT in this PR) we should save ETag to db.

Either way, I think that it is very important to make sure that server time, and device time are the same. If there would be two devices with different times set, the merging base on modification time will break. I even think that we should not allow syncing if time is messed up (or at list require explicite action to sync with broken time).

@Ethran
Copy link
Owner

Ethran commented Feb 3, 2026

please add switch to explicitly allow syncing on mobile data. It shouldn't be syncing by default when it's not connected to wifi.

@Ethran
Copy link
Owner

Ethran commented Feb 3, 2026

Also, please provide instructions how to setup syncing if user have 2fa, ie:
Log in via browser
Go to Settings → Security
Under Devices & sessions
Click Create new app password
Give it a name (e.g. notable),
You will then see a username and password created for this app. Use them to setup nextcloud.

@Ethran
Copy link
Owner

Ethran commented Feb 3, 2026

Also, the ui doesn't look great with dark mode:
Screenshot_20260203-105433_Notable
And there should be button to show password that was entered, but only if user just entered it, not the saved password. For saved password I don't see a point in showing anything else then "change password".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants