Release v1.2.2: Backward compatibility for v1.2.0 users
- Added dual-mode download for server restore - Scans both /notes/ (new) and Root (old v1.2.0) folders - Normal sync only uses /notes/ for performance - Fixed URL construction bugs - Updated F-Droid changelogs
This commit is contained in:
17
CHANGELOG.md
17
CHANGELOG.md
@@ -6,6 +6,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
---
|
||||
|
||||
## [1.2.2] - TBD
|
||||
|
||||
### Fixed
|
||||
- **Backward Compatibility for v1.2.0 Users (Critical)**
|
||||
- App now reads BOTH old (Root) AND new (`/notes/`) folder structures
|
||||
- Users upgrading from v1.2.0 no longer lose their existing notes
|
||||
- Server-Restore now finds notes from v1.2.0 stored in Root folder
|
||||
- Automatic deduplication prevents loading the same note twice
|
||||
- Graceful error handling if Root folder is not accessible
|
||||
|
||||
### Technical
|
||||
- `WebDavSyncService.downloadRemoteNotes()` - Dual-mode download (Root + /notes/)
|
||||
- `WebDavSyncService.restoreFromServer()` - Now uses dual-mode download
|
||||
- Migration happens naturally: new uploads go to `/notes/`, old notes stay readable
|
||||
|
||||
---
|
||||
|
||||
## [1.2.1] - 2026-01-05
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -17,8 +17,8 @@ android {
|
||||
applicationId = "dev.dettmer.simplenotes"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 6 // 🐛 v1.2.1: Markdown Initial Export Bugfix
|
||||
versionName = "1.2.1" // 🐛 v1.2.1: Markdown Initial Export Bugfix
|
||||
versionCode = 7 // 🔧 v1.2.2: Backward compatibility for v1.2.0 migration
|
||||
versionName = "1.2.2" // 🔧 v1.2.2: Dual-mode download (Root + /notes/)
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
||||
@@ -675,12 +675,22 @@ class WebDavSyncService(private val context: Context) {
|
||||
val conflictCount: Int
|
||||
)
|
||||
|
||||
private fun downloadRemoteNotes(sardine: Sardine, serverUrl: String): DownloadResult {
|
||||
private fun downloadRemoteNotes(
|
||||
sardine: Sardine,
|
||||
serverUrl: String,
|
||||
includeRootFallback: Boolean = false // 🆕 v1.2.2: Only for restore from server
|
||||
): DownloadResult {
|
||||
var downloadedCount = 0
|
||||
var conflictCount = 0
|
||||
val processedIds = mutableSetOf<String>() // 🆕 v1.2.2: Track already loaded notes
|
||||
|
||||
try {
|
||||
// 🆕 PHASE 1: Download from /notes/ (new structure v1.2.1+)
|
||||
val notesUrl = getNotesUrl(serverUrl)
|
||||
Logger.d(TAG, "🔍 Phase 1: Checking /notes/ at: $notesUrl")
|
||||
|
||||
if (sardine.exists(notesUrl)) {
|
||||
Logger.d(TAG, " ✅ /notes/ exists, scanning...")
|
||||
val resources = sardine.list(notesUrl)
|
||||
|
||||
for (resource in resources) {
|
||||
@@ -688,10 +698,13 @@ class WebDavSyncService(private val context: Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
val noteUrl = resource.href.toString()
|
||||
// 🔧 Fix: Build full URL instead of using href directly
|
||||
val noteUrl = notesUrl.trimEnd('/') + "/" + resource.name
|
||||
val jsonContent = sardine.get(noteUrl).bufferedReader().use { it.readText() }
|
||||
val remoteNote = Note.fromJson(jsonContent) ?: continue
|
||||
|
||||
processedIds.add(remoteNote.id) // 🆕 Mark as processed
|
||||
|
||||
val localNote = storage.loadNote(remoteNote.id)
|
||||
|
||||
when {
|
||||
@@ -699,6 +712,7 @@ class WebDavSyncService(private val context: Context) {
|
||||
// New note from server
|
||||
storage.saveNote(remoteNote.copy(syncStatus = SyncStatus.SYNCED))
|
||||
downloadedCount++
|
||||
Logger.d(TAG, " ✅ Downloaded from /notes/: ${remoteNote.id}")
|
||||
}
|
||||
localNote.updatedAt < remoteNote.updatedAt -> {
|
||||
// Remote is newer
|
||||
@@ -710,14 +724,95 @@ class WebDavSyncService(private val context: Context) {
|
||||
// Safe to overwrite
|
||||
storage.saveNote(remoteNote.copy(syncStatus = SyncStatus.SYNCED))
|
||||
downloadedCount++
|
||||
Logger.d(TAG, " ✅ Updated from /notes/: ${remoteNote.id}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Log error but don't fail entire sync
|
||||
Logger.d(TAG, " 📊 Phase 1 complete: $downloadedCount notes from /notes/")
|
||||
} else {
|
||||
Logger.w(TAG, " ⚠️ /notes/ does not exist, skipping Phase 1")
|
||||
}
|
||||
|
||||
// 🆕 PHASE 2: BACKWARD-COMPATIBILITY - Download from Root (old structure v1.2.0)
|
||||
// ⚠️ ONLY for restore from server! Normal sync should NOT scan Root
|
||||
if (includeRootFallback) {
|
||||
val rootUrl = serverUrl.trimEnd('/')
|
||||
Logger.d(TAG, "🔍 Phase 2: Checking ROOT at: $rootUrl (Restore mode)")
|
||||
|
||||
try {
|
||||
val rootResources = sardine.list(rootUrl)
|
||||
Logger.d(TAG, " 📂 Found ${rootResources.size} resources in ROOT")
|
||||
|
||||
val oldNotes = rootResources.filter { resource ->
|
||||
!resource.isDirectory &&
|
||||
resource.name.endsWith(".json") &&
|
||||
!resource.path.contains("/notes/") && // Not from /notes/ subdirectory
|
||||
!resource.path.contains("/notes-md/") // Not from /notes-md/
|
||||
}
|
||||
|
||||
Logger.d(TAG, " 🔎 Filtered to ${oldNotes.size} .json files (excluding /notes/ and /notes-md/)")
|
||||
|
||||
if (oldNotes.isNotEmpty()) {
|
||||
Logger.w(TAG, "⚠️ Found ${oldNotes.size} notes in ROOT (old v1.2.0 structure)")
|
||||
|
||||
for (resource in oldNotes) {
|
||||
// 🔧 Fix: Build full URL instead of using href directly
|
||||
val noteUrl = rootUrl.trimEnd('/') + "/" + resource.name
|
||||
Logger.d(TAG, " 📄 Processing: ${resource.name} from ${resource.path}")
|
||||
|
||||
val jsonContent = sardine.get(noteUrl).bufferedReader().use { it.readText() }
|
||||
val remoteNote = Note.fromJson(jsonContent) ?: continue
|
||||
|
||||
// Skip if already loaded from /notes/
|
||||
if (processedIds.contains(remoteNote.id)) {
|
||||
Logger.d(TAG, " ⏭️ Skipping ${remoteNote.id} (already loaded from /notes/)")
|
||||
continue
|
||||
}
|
||||
|
||||
processedIds.add(remoteNote.id)
|
||||
val localNote = storage.loadNote(remoteNote.id)
|
||||
|
||||
when {
|
||||
localNote == null -> {
|
||||
storage.saveNote(remoteNote.copy(syncStatus = SyncStatus.SYNCED))
|
||||
downloadedCount++
|
||||
Logger.d(TAG, " ✅ Downloaded from ROOT: ${remoteNote.id}")
|
||||
}
|
||||
localNote.updatedAt < remoteNote.updatedAt -> {
|
||||
if (localNote.syncStatus == SyncStatus.PENDING) {
|
||||
storage.saveNote(localNote.copy(syncStatus = SyncStatus.CONFLICT))
|
||||
conflictCount++
|
||||
} else {
|
||||
storage.saveNote(remoteNote.copy(syncStatus = SyncStatus.SYNCED))
|
||||
downloadedCount++
|
||||
Logger.d(TAG, " ✅ Updated from ROOT: ${remoteNote.id}")
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Local is newer - do nothing
|
||||
Logger.d(TAG, " ⏭️ Local is newer: ${remoteNote.id}")
|
||||
}
|
||||
}
|
||||
}
|
||||
Logger.d(TAG, " 📊 Phase 2 complete: downloaded ${oldNotes.size} notes from ROOT")
|
||||
} else {
|
||||
Logger.d(TAG, " ℹ️ No old notes found in ROOT")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "⚠️ Failed to scan ROOT directory: ${e.message}", e)
|
||||
Logger.e(TAG, " Stack trace: ${e.stackTraceToString()}")
|
||||
// Not fatal - new users may not have root access
|
||||
}
|
||||
} else {
|
||||
Logger.d(TAG, "⏭️ Skipping Phase 2 (Root scan) - only enabled for restore from server")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "❌ downloadRemoteNotes failed", e)
|
||||
}
|
||||
|
||||
Logger.d(TAG, "📊 Total download result: $downloadedCount notes, $conflictCount conflicts")
|
||||
return DownloadResult(downloadedCount, conflictCount)
|
||||
}
|
||||
|
||||
@@ -757,38 +852,18 @@ class WebDavSyncService(private val context: Context) {
|
||||
|
||||
Logger.d(TAG, "🔄 Starting restore from server...")
|
||||
|
||||
val notesUrl = getNotesUrl(serverUrl)
|
||||
// Clear local storage FIRST
|
||||
Logger.d(TAG, "🗑️ Clearing local storage...")
|
||||
storage.deleteAllNotes()
|
||||
|
||||
// List all files on server
|
||||
val resources = sardine.list(notesUrl)
|
||||
val jsonFiles = resources.filter {
|
||||
!it.isDirectory && it.name.endsWith(".json")
|
||||
}
|
||||
// 🆕 v1.2.2: Use downloadRemoteNotes() with Root fallback enabled
|
||||
val result = downloadRemoteNotes(
|
||||
sardine = sardine,
|
||||
serverUrl = serverUrl,
|
||||
includeRootFallback = true // ✅ Enable backward compatibility for restore
|
||||
)
|
||||
|
||||
Logger.d(TAG, "📂 Found ${jsonFiles.size} files on server")
|
||||
|
||||
val restoredNotes = mutableListOf<Note>()
|
||||
|
||||
// Download and parse each file
|
||||
for (resource in jsonFiles) {
|
||||
try {
|
||||
val fileUrl = notesUrl.trimEnd('/') + "/" + resource.name
|
||||
val content = sardine.get(fileUrl).bufferedReader().use { it.readText() }
|
||||
|
||||
val note = Note.fromJson(content)
|
||||
if (note != null) {
|
||||
restoredNotes.add(note)
|
||||
Logger.d(TAG, "✅ Downloaded: ${note.title}")
|
||||
} else {
|
||||
Logger.e(TAG, "❌ Failed to parse ${resource.name}: Note.fromJson returned null")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "❌ Failed to download ${resource.name}", e)
|
||||
// Continue with other files
|
||||
}
|
||||
}
|
||||
|
||||
if (restoredNotes.isEmpty()) {
|
||||
if (result.downloadedCount == 0) {
|
||||
return@withContext RestoreResult(
|
||||
isSuccess = false,
|
||||
errorMessage = "Keine Notizen auf Server gefunden",
|
||||
@@ -796,22 +871,14 @@ class WebDavSyncService(private val context: Context) {
|
||||
)
|
||||
}
|
||||
|
||||
// Clear local storage
|
||||
Logger.d(TAG, "🗑️ Clearing local storage...")
|
||||
storage.deleteAllNotes()
|
||||
saveLastSyncTimestamp()
|
||||
|
||||
// Save all restored notes
|
||||
Logger.d(TAG, "💾 Saving ${restoredNotes.size} notes...")
|
||||
restoredNotes.forEach { note ->
|
||||
storage.saveNote(note.copy(syncStatus = SyncStatus.SYNCED))
|
||||
}
|
||||
|
||||
Logger.d(TAG, "✅ Restore completed: ${restoredNotes.size} notes")
|
||||
Logger.d(TAG, "✅ Restore completed: ${result.downloadedCount} notes")
|
||||
|
||||
RestoreResult(
|
||||
isSuccess = true,
|
||||
errorMessage = null,
|
||||
restoredCount = restoredNotes.size
|
||||
restoredCount = result.downloadedCount
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
|
||||
12
fastlane/metadata/android/de-DE/changelogs/7.txt
Normal file
12
fastlane/metadata/android/de-DE/changelogs/7.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
v1.2.2 - Rückwärtskompatibilität für v1.2.0 User
|
||||
|
||||
Kritische Fehlerbehebung
|
||||
• Server-Wiederherstellung findet jetzt ALLE Notizen (Root + /notes/)
|
||||
• User die von v1.2.0 upgraden verlieren keine Daten mehr
|
||||
• Alte Notizen aus Root-Ordner werden beim Restore gefunden
|
||||
|
||||
Technische Details
|
||||
• Dual-Mode Download nur bei Server-Restore aktiv
|
||||
• Normale Syncs bleiben schnell (scannen nur /notes/)
|
||||
• Automatische Deduplication verhindert Duplikate
|
||||
• Sanfte Migration: Neue Uploads gehen in /notes/, alte bleiben lesbar
|
||||
12
fastlane/metadata/android/en-US/changelogs/7.txt
Normal file
12
fastlane/metadata/android/en-US/changelogs/7.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
v1.2.2 - Backward Compatibility for v1.2.0 Users
|
||||
|
||||
Critical Bugfix
|
||||
• Server restore now finds ALL notes (Root + /notes/)
|
||||
• Users upgrading from v1.2.0 no longer lose data
|
||||
• Old notes from Root folder are found during restore
|
||||
|
||||
Technical Details
|
||||
• Dual-mode download only active for server restore
|
||||
• Normal syncs remain fast (scan only /notes/)
|
||||
• Automatic deduplication prevents duplicates
|
||||
• Smooth migration: New uploads go to /notes/, old ones remain readable
|
||||
Reference in New Issue
Block a user