Skip to content

Commit 1756af4

Browse files
committed
fix(sync): Markdown auto-sync not firing on save (v2.2.1)
Root cause: SharedPreferences for KEY_MARKDOWN_EXPORT and KEY_MARKDOWN_AUTO_IMPORT were only persisted AFTER a successful initial export. If the initial export failed (HTTP 405 on bewCloud, timeout, network error, or ViewModel cancellation), the prefs were never set — so on-save markdown export never fired, even though the toggle appeared ON. Fixes: 1. MarkdownSyncManager: Add HTTP 405 fallback (list() after failed exists()) in ensureMarkdownDirExists(), matching the existing pattern in WebDavSyncService 2. SettingsViewModel: Persist markdown prefs immediately after server config validation, not after initial export. Initial export is now best-effort — only auth/config errors revert the prefs 3. NoteUploader: Add debug log for markdown export enabled state 4. WebDavSyncService: Include pref values in auto-import skip log Thanks to @minosimo for the detailed bug report and logs that made the root cause immediately clear. Closes #50
1 parent d24d2f2 commit 1756af4

5 files changed

Lines changed: 92 additions & 44 deletions

File tree

android/app/src/main/java/dev/dettmer/simplenotes/sync/MarkdownSyncManager.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -637,9 +637,25 @@ internal class MarkdownSyncManager(
637637

638638
try {
639639
val mdUrl = urlBuilder.getMarkdownUrl(serverUrl)
640-
if (!sardine.exists(mdUrl)) {
640+
641+
// 🔧 v2.2.1 (Issue #50): exists() may return HTTP 405 on bewCloud/some servers,
642+
// which Sardine throws as an IOException. Fallback: try list() — if it succeeds,
643+
// the directory exists. Identical pattern to WebDavSyncService.ensureMarkdownDirectoryExists().
644+
val dirExists = try {
645+
sardine.exists(mdUrl)
646+
} catch (e: java.io.IOException) {
647+
Logger.w(TAG, "⚠️ notes-md/ exists() check failed: ${e.message}, trying list()")
648+
try {
649+
sardine.list(mdUrl)
650+
true
651+
} catch (_: java.io.IOException) {
652+
false
653+
}
654+
}
655+
656+
if (!dirExists) {
641657
sardine.createDirectory(mdUrl)
642-
Logger.d(TAG, "📁 Created notes-md/ directory (for future use)")
658+
Logger.d(TAG, "📁 Created notes-md/ directory")
643659
}
644660
connectionManager.markdownDirEnsured = true
645661
} catch (e: Exception) {

android/app/src/main/java/dev/dettmer/simplenotes/sync/NoteUploader.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ internal class NoteUploader(
5555
): UploadBatchResult {
5656
val localNotes = storage.loadAllNotes()
5757
val markdownExportEnabled = prefs.getBoolean(Constants.KEY_MARKDOWN_EXPORT, false)
58+
Logger.d(TAG, "📋 Markdown export enabled: $markdownExportEnabled") // 🔧 v2.2.1 (Issue #50)
5859

5960
// 🆕 v1.9.0 (Opt 1): Einmalige Prüfung statt N × exists(notes-md/)
6061
val markdownDirExists: Boolean = if (markdownExportEnabled) {

android/app/src/main/java/dev/dettmer/simplenotes/sync/SafeSardineWrapper.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class SafeSardineWrapper private constructor(
7272
* - 2xx → true (existiert)
7373
* - 403 → true (existiert, HEAD auf Collection nicht erlaubt — Jianguoyun-Verhalten)
7474
* - 404 → false (existiert nicht)
75+
* - 405 → list() fallback (bewCloud erlaubt kein HEAD — PROPFIND als Alternative)
7576
* - 410 → false (wurde gelöscht)
7677
* - 401 → IOException (Auth-Fehler, soll propagiert werden)
7778
* - Sonstiges → IOException (unerwarteter Fehler)
@@ -99,6 +100,27 @@ class SafeSardineWrapper private constructor(
99100
code == HTTP_UNAUTHORIZED -> throw java.io.IOException(
100101
"Authentication failed ($code) for $url"
101102
)
103+
// 🔧 v2.2.1 (Issue #50): bewCloud returns 405 for HEAD requests.
104+
// Fallback: use PROPFIND (list) to check existence.
105+
code == HTTP_METHOD_NOT_ALLOWED -> {
106+
Logger.d(
107+
TAG,
108+
"exists($url) → false ($code), trying list() fallback"
109+
)
110+
try {
111+
val resources = delegate.list(url)
112+
val exists = resources.isNotEmpty()
113+
Logger.d(
114+
TAG,
115+
"list() fallback → exists=$exists" +
116+
" (found ${resources.size} items)"
117+
)
118+
exists
119+
} catch (e: Exception) {
120+
Logger.d(TAG, "list() fallback failed: ${e.message}")
121+
false
122+
}
123+
}
102124
else -> throw java.io.IOException(
103125
"Unexpected HTTP $code for exists($url): ${response.message}"
104126
)

android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,11 @@ class WebDavSyncService(private val context: Context, private val ioDispatcher:
659659
// Vorher: syncedCount += reUploadedCount → führte zu Doppelzählung.
660660
}
661661
} else {
662-
Logger.d(TAG, "⏭️ Markdown auto-import disabled")
662+
// 🔧 v2.2.1 (Issue #50): Include pref values for easier diagnosis
663+
val mdExport = prefs.getBoolean(Constants.KEY_MARKDOWN_EXPORT, false)
664+
val mdImport = prefs.getBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, false)
665+
Logger.d(TAG, "⏭️ Markdown auto-import disabled " +
666+
"(KEY_MARKDOWN_EXPORT=$mdExport, KEY_MARKDOWN_AUTO_IMPORT=$mdImport)")
663667
}
664668
} catch (e: Exception) {
665669
Logger.e(TAG, "⚠️ Markdown auto-import failed (non-fatal)", e)

android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsViewModel.kt

Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,17 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
828828
return@launch
829829
}
830830

831+
// 🔧 v2.2.1 (Issue #50): Prefs SOFORT setzen nach Server-Config-Validierung.
832+
// KEY_MARKDOWN_EXPORT wird in NoteUploader beim on-save Sync gelesen.
833+
// Wenn der Initial-Export fehlschlägt (z.B. bewCloud 405, Timeout,
834+
// Netzwerkfehler oder ViewModel-Cancellation), muss der reguläre Sync
835+
// trotzdem bei jedem Save die .md-Datei schreiben können.
836+
// Der Initial-Export ist "best effort" für Bestandsnotizen.
837+
prefs.edit {
838+
putBoolean(Constants.KEY_MARKDOWN_EXPORT, true)
839+
putBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, true)
840+
}
841+
831842
// Check if there are notes to export
832843
val noteStorage = dev.dettmer.simplenotes.storage.NotesStorage(getApplication())
833844
val noteCount = withContext(ioDispatcher) { noteStorage.loadAllNotes().size }
@@ -840,7 +851,8 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
840851
// erreichbar ist. Ein einzelner Socket-Check reicht (1×timeout).
841852
val reachable = withContext(ioDispatcher) { syncService.isServerReachable() }
842853
if (!reachable) {
843-
_markdownAutoSync.value = false
854+
// 🔧 v2.2.1: Prefs bleiben auf true — on-save Export funktioniert
855+
// beim nächsten erfolgreichen Sync. Nur Toast als Warnung.
844856
_markdownExportProgress.value = null
845857
emitToast(getString(R.string.snackbar_server_unreachable))
846858
return@launch
@@ -861,62 +873,55 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
861873
) * 2 * 1000L
862874
val totalTimeoutMs = (noteCount * perNoteTimeoutMs) + EXPORT_OVERHEAD_TIMEOUT_MS
863875

864-
val exportedCount = withTimeout(totalTimeoutMs) {
865-
withContext(ioDispatcher) {
866-
syncService.exportAllNotesToMarkdown(
867-
serverUrl = serverUrl,
868-
username = username,
869-
password = password,
870-
onProgress = { current, total ->
871-
_markdownExportProgress.value = MarkdownExportProgress(current, total)
872-
}
873-
)
876+
try {
877+
val exportedCount = withTimeout(totalTimeoutMs) {
878+
withContext(ioDispatcher) {
879+
syncService.exportAllNotesToMarkdown(
880+
serverUrl = serverUrl,
881+
username = username,
882+
password = password,
883+
onProgress = { current, total ->
884+
_markdownExportProgress.value = MarkdownExportProgress(current, total)
885+
}
886+
)
887+
}
874888
}
875-
}
876889

877-
// 🔧 v1.10.0 Fix: Safety-Net — wenn KEIN einziger Export erfolgreich war,
878-
// ist das ein Fehler (z.B. Server per Socket erreichbar aber HTTP schlägt fehl).
879-
// Toggle wird zurückgesetzt.
880-
if (exportedCount == 0) {
881-
_markdownAutoSync.value = false
890+
_markdownExportProgress.value = MarkdownExportProgress(noteCount, noteCount, isComplete = true)
891+
if (exportedCount > 0) {
892+
emitToast(getString(R.string.toast_markdown_exported, exportedCount))
893+
} else {
894+
// 🔧 v2.2.1: exportedCount==0 → Warnung, Feature bleibt aktiv.
895+
// Bestandsnotizen werden beim nächsten Save/Sync exportiert.
896+
emitToast(getString(R.string.toast_markdown_enabled))
897+
}
898+
} catch (e: TimeoutCancellationException) {
899+
// 🔧 v2.2.1: Timeout → Feature bleibt aktiv (Prefs bereits gesetzt).
900+
Logger.w(TAG, "Markdown initial export timed out: ${e.message}")
882901
_markdownExportProgress.value = null
883-
emitToast(
884-
getString(R.string.toast_export_failed, getString(R.string.snackbar_server_unreachable))
885-
)
886-
return@launch
887-
}
888-
889-
// Export successful — prefs persistieren (_markdownAutoSync ist bereits true)
890-
prefs.edit {
891-
putBoolean(Constants.KEY_MARKDOWN_EXPORT, true)
892-
putBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, true)
902+
emitToast(getString(R.string.toast_export_timeout))
893903
}
894904

895-
_markdownExportProgress.value = MarkdownExportProgress(noteCount, noteCount, isComplete = true)
896-
emitToast(getString(R.string.toast_markdown_exported, exportedCount))
897-
898905
// Clear progress after short delay
899906
kotlinx.coroutines.delay(PROGRESS_CLEAR_DELAY_MS)
900907
_markdownExportProgress.value = null
901908
} else {
902909
// No notes — feature sofort aktivieren, kein Export nötig
903910
_markdownExportProgress.value = null
904-
prefs.edit {
905-
putBoolean(Constants.KEY_MARKDOWN_EXPORT, true)
906-
putBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, true)
907-
}
908911
emitToast(getString(R.string.toast_markdown_enabled))
909912
}
910-
} catch (e: TimeoutCancellationException) {
911-
// 🆕 v1.10.0: Gesamt-Export-Timeout überschritten — Toggle zurücksetzen
912-
Logger.w(TAG, "Markdown export timed out: ${e.message}")
913-
_markdownAutoSync.value = false
914-
_markdownExportProgress.value = null
915-
emitToast(getString(R.string.toast_export_timeout))
913+
} catch (e: kotlinx.coroutines.CancellationException) {
914+
// 🔧 v2.2.1 (Issue #50): viewModelScope-Cancellation (Activity destroyed).
915+
// Prefs sind bereits gesetzt — Feature funktioniert unabhängig vom Initial-Export.
916+
throw e // CancellationException muss weiter propagiert werden
916917
} catch (e: Exception) {
917-
// 🔧 v1.10.0: Toggle zurücksetzen + konsistente Fehlermeldung
918+
// 🔧 v2.2.1: Bei echtem Fehler (z.B. Auth-Problem) → Feature deaktivieren + Prefs zurücksetzen.
918919
_markdownAutoSync.value = false
919920
_markdownExportProgress.value = null
921+
prefs.edit {
922+
putBoolean(Constants.KEY_MARKDOWN_EXPORT, false)
923+
putBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, false)
924+
}
920925
val syncService = WebDavSyncService(getApplication())
921926
val userMessage = syncService.mapSyncExceptionToMessage(e)
922927
emitToast(getString(R.string.toast_export_failed, userMessage))

0 commit comments

Comments
 (0)