From 0a3e839da06052fa8b48db6388c248abcc6a0499 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:58:47 +0000 Subject: [PATCH] feat: clean up the 1.x app's leftover data on upgrade The React Native 1.x app shipped as com.subtracks and left an AsyncStorage database (RKStorage), an older `servers` dump and a downloaded/cached artwork+audio tree in app-specific storage. v3 reads none of those paths, so an update would otherwise leave the data behind, reachable only by wiping the whole app. Remove them once at startup, gated on one of those 1.x paths existing, so a fresh install does nothing and an upgrade is cleaned exactly once. The cached trees can be large, so the delete runs off the main thread. Kept in a single file so it can be deleted once no installs can come from 1.x. --- .../main/kotlin/com/subtracks/SubtracksApp.kt | 2 + .../com/subtracks/data/LegacyDataCleanup.kt | 41 ++++++++++++ .../subtracks/data/LegacyDataCleanupTest.kt | 67 +++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 app/src/main/kotlin/com/subtracks/data/LegacyDataCleanup.kt create mode 100644 app/src/test/kotlin/com/subtracks/data/LegacyDataCleanupTest.kt diff --git a/app/src/main/kotlin/com/subtracks/SubtracksApp.kt b/app/src/main/kotlin/com/subtracks/SubtracksApp.kt index cd2ed7dc..44c95eb3 100644 --- a/app/src/main/kotlin/com/subtracks/SubtracksApp.kt +++ b/app/src/main/kotlin/com/subtracks/SubtracksApp.kt @@ -5,6 +5,7 @@ import coil3.ImageLoader import coil3.SingletonImageLoader import coil3.annotation.DelicateCoilApi import coil3.network.okhttp.OkHttpNetworkFetcherFactory +import com.subtracks.data.LegacyDataCleanup import com.subtracks.data.prefs.UserPreferences import com.subtracks.data.repo.ArtworkSeedStore import com.subtracks.di.appModule @@ -25,6 +26,7 @@ class SubtracksApp : Application() { override fun onCreate() { super.onCreate() Log.init(this) + LegacyDataCleanup.run(this) stopKoin() SingletonImageLoader.reset() val koin = diff --git a/app/src/main/kotlin/com/subtracks/data/LegacyDataCleanup.kt b/app/src/main/kotlin/com/subtracks/data/LegacyDataCleanup.kt new file mode 100644 index 00000000..157db816 --- /dev/null +++ b/app/src/main/kotlin/com/subtracks/data/LegacyDataCleanup.kt @@ -0,0 +1,41 @@ +package com.subtracks.data + +import android.content.Context +import com.subtracks.log.Log +import java.io.File + +// One-shot cleanup of the 1.x React Native app, which shipped as com.subtracks before this rewrite. +// It left an AsyncStorage database (RKStorage), an older `servers` dump and a downloaded/cached +// artwork+audio tree (under the app's external files dir) that v3 never reads. Only a real 1.x upgrade +// has those paths, so a fresh install does nothing and an upgrade is cleaned exactly once. +// +// Delete this file and its call in SubtracksApp once installs can no longer be coming from 1.x. +internal object LegacyDataCleanup { + fun run(context: Context) { + // The cached trees can be large, so keep the walk off the main thread. + Thread { run(context.dataDir, context.getExternalFilesDir(null)) }.start() + } + + internal fun run( + dataDir: File, + externalFilesDir: File?, + ) { + val database = File(dataDir, "databases/RKStorage") + val servers = File(dataDir, "files/servers") + val cache = externalFilesDir?.let { File(it, "s") } + if (!database.exists() && !servers.exists() && cache?.exists() != true) return + deleteDatabase(database) + deleteTree(servers) + cache?.let(::deleteTree) + } + + private fun deleteDatabase(database: File) { + listOf("", "-wal", "-shm", "-journal").forEach { deleteTree(File(database.path + it)) } + } + + private fun deleteTree(target: File) { + if (!target.exists()) return + val removed = target.deleteRecursively() + Log.i("legacy", "removed ${target.path} ($removed)") + } +} diff --git a/app/src/test/kotlin/com/subtracks/data/LegacyDataCleanupTest.kt b/app/src/test/kotlin/com/subtracks/data/LegacyDataCleanupTest.kt new file mode 100644 index 00000000..18b1ad2b --- /dev/null +++ b/app/src/test/kotlin/com/subtracks/data/LegacyDataCleanupTest.kt @@ -0,0 +1,67 @@ +package com.subtracks.data + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File + +class LegacyDataCleanupTest { + @get:Rule + val tmp = TemporaryFolder() + + @Test + fun removesThe1xLeftoversAndLeavesTheNativeData() { + val dataDir = tmp.newFolder("data") + val external = tmp.newFolder("external") + + val legacyDatabase = file(dataDir, "databases/RKStorage") + val legacyDatabaseWal = file(dataDir, "databases/RKStorage-wal") + val legacyServers = file(dataDir, "files/servers/0.json") + val legacyCache = file(external, "s/0/coverArt/cover") + val nativeDatabase = file(dataDir, "databases/subtracks.db") + val nativeDownload = file(external, "Music/downloads/song.mp3") + + LegacyDataCleanup.run(dataDir, external) + + assertFalse(legacyDatabase.exists()) + assertFalse(legacyDatabaseWal.exists()) + assertFalse(legacyServers.exists()) + assertFalse(legacyCache.exists()) + assertTrue(nativeDatabase.exists()) + assertTrue(nativeDownload.exists()) + } + + @Test + fun leavesTheNativeDataAloneOnAFreshInstall() { + val dataDir = tmp.newFolder("data2") + val external = tmp.newFolder("external2") + val nativeDatabase = file(dataDir, "databases/subtracks.db") + val nativeDownload = file(external, "Music/downloads/song.mp3") + + LegacyDataCleanup.run(dataDir, external) + + assertTrue(nativeDatabase.exists()) + assertTrue(nativeDownload.exists()) + } + + @Test + fun cleansUpEvenWhenExternalStorageIsUnavailable() { + val dataDir = tmp.newFolder("data3") + val legacyDatabase = file(dataDir, "databases/RKStorage") + + LegacyDataCleanup.run(dataDir, null) + + assertFalse(legacyDatabase.exists()) + } + + private fun file( + dir: File, + path: String, + ): File = + File(dir, path).apply { + parentFile?.mkdirs() + createNewFile() + } +}