Skip to content

Commit

Permalink
Migrate database location on iOS to app group
Browse files Browse the repository at this point in the history
  • Loading branch information
prof18 committed Feb 23, 2025
1 parent 462c317 commit dc22ccc
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 33 deletions.
1 change: 1 addition & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ kotlin {
implementation(compose.runtime)
implementation(libs.immutable.collections)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.touchlab.kermit)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.prof18.feedflow.core.utils

import kotlinx.cinterop.ExperimentalForeignApi
import platform.Foundation.NSFileManager

@OptIn(ExperimentalForeignApi::class)
fun getAppGroupDatabasePath(): String {
val fileManager = NSFileManager.defaultManager()
val containerURL = fileManager.containerURLForSecurityApplicationGroupIdentifier(IOS_APP_GROUP)
check(containerURL != null) { "Could not access App Group container" }

val containerPath = containerURL.path
val directoryPath = "$containerPath/databases"

if (!fileManager.fileExistsAtPath(directoryPath)) {
fileManager.createDirectoryAtPath(
directoryPath,
withIntermediateDirectories = true,
attributes = null,
error = null,
)
}
return directoryPath
}

internal const val IOS_APP_GROUP = "group.com.prof18.feedflow"
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.prof18.feedflow.core.utils

import co.touchlab.kermit.Logger
import kotlinx.cinterop.ExperimentalForeignApi
import platform.Foundation.NSApplicationSupportDirectory
import platform.Foundation.NSFileManager
import platform.Foundation.NSSearchPathForDirectoriesInDomains
import platform.Foundation.NSURL
import platform.Foundation.NSUserDomainMask
import kotlin.experimental.ExperimentalObjCRefinement

class DatabaseFileMigration(
private val databaseName: String,
) {

@OptIn(ExperimentalForeignApi::class, ExperimentalObjCRefinement::class)
@HiddenFromObjC
fun migrate() {
if (!isDatabaseAvailable()) {
Logger.d { "No need to migrate the $databaseName database" }
return
}

val databasePathString = getAppDatabasePathAsString()
val appDatabasePath = NSURL.fileURLWithPath(databasePathString)

val appWalPath = NSURL.fileURLWithPath("$databasePathString-wal")
val appShmPath = NSURL.fileURLWithPath("$databasePathString-shm")

val groupDatabasePath = NSURL.fileURLWithPath(getAppGroupDatabasePath())
.URLByAppendingPathComponent(databaseName) ?: return

groupDatabasePath.path?.let { path ->
if (NSFileManager.defaultManager.fileExistsAtPath(path)) {
Logger.d { "Database file already exists in the new location, skipping" }
return
}
}

val groupWalPath = NSURL.fileURLWithPath(getAppGroupDatabasePath())
.URLByAppendingPathComponent("$databaseName-wal") ?: return

val groupShmPath = NSURL.fileURLWithPath(getAppGroupDatabasePath())
.URLByAppendingPathComponent("$databaseName-shm") ?: return

// Move files
NSFileManager.defaultManager.copyItemAtURL(
srcURL = appDatabasePath,
toURL = groupDatabasePath,
error = null,
)
NSFileManager.defaultManager.copyItemAtURL(
srcURL = appWalPath,
toURL = groupWalPath,
error = null,
)
NSFileManager.defaultManager.copyItemAtURL(
srcURL = appShmPath,
toURL = groupShmPath,
error = null,
)

// Delete stuff
NSFileManager.defaultManager.removeItemAtURL(appDatabasePath, null)
NSFileManager.defaultManager.removeItemAtURL(appWalPath, null)
NSFileManager.defaultManager.removeItemAtURL(appShmPath, null)

Logger.d { "$databaseName Database file Migration done" }
}

private fun isDatabaseAvailable(): Boolean {
val databaseDirectory = getAppDatabasePathAsString()
val fileManager = NSFileManager.defaultManager()
return fileManager.fileExistsAtPath(databaseDirectory)
}

private fun getAppDatabasePathAsString(): String {
val paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, true)
val documentsDirectory = paths[0] as String

return "$documentsDirectory/databases/$databaseName"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ fun createDatabaseDriver(context: Context, appEnvironment: AppEnvironment): SqlD
schema = FeedFlowDB.Schema,
context = context,
name = if (appEnvironment.isDebug()) {
DatabaseHelper.DATABASE_NAME_DEBUG
DatabaseHelper.APP_DATABASE_NAME_DEBUG
} else {
DatabaseHelper.DATABASE_NAME_PROD
DatabaseHelper.APP_DATABASE_NAME_PROD
},
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -565,9 +565,9 @@ class DatabaseHelper(
)
}

internal companion object {
const val DB_FILE_NAME_WITH_EXTENSION = "FeedFlow.db"
const val DATABASE_NAME_PROD = "FeedFlowDB"
const val DATABASE_NAME_DEBUG = "FeedFlowDB-debug"
companion object {
internal const val DB_FILE_NAME_WITH_EXTENSION = "FeedFlow.db"
const val APP_DATABASE_NAME_PROD = "FeedFlowDB"
const val APP_DATABASE_NAME_DEBUG = "FeedFlowDB-debug"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@ package com.prof18.feedflow.database
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.native.NativeSqliteDriver
import com.prof18.feedflow.core.utils.AppEnvironment
import com.prof18.feedflow.core.utils.getAppGroupDatabasePath
import com.prof18.feedflow.db.FeedFlowDB

fun createDatabaseDriver(appEnvironment: AppEnvironment): SqlDriver {
return NativeSqliteDriver(
schema = FeedFlowDB.Schema,
onConfiguration = { conf ->
conf.copy(
extendedConfig = conf.extendedConfig.copy(
basePath = getAppGroupDatabasePath(),
),
)
},
name = if (appEnvironment.isDebug()) {
DatabaseHelper.DATABASE_NAME_DEBUG
DatabaseHelper.APP_DATABASE_NAME_DEBUG
} else {
DatabaseHelper.DATABASE_NAME_PROD
DatabaseHelper.APP_DATABASE_NAME_PROD
},
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.prof18.feedflow.feedsync.database.di
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.native.NativeSqliteDriver
import com.prof18.feedflow.core.utils.AppEnvironment
import com.prof18.feedflow.core.utils.DatabaseFileMigration
import com.prof18.feedflow.core.utils.getAppGroupDatabasePath
import com.prof18.feedflow.feedsync.database.data.SyncedDatabaseHelper
import com.prof18.feedflow.feedsync.database.db.FeedFlowFeedSyncDB
import org.koin.core.module.Module
Expand All @@ -12,13 +14,26 @@ import org.koin.dsl.module
internal actual fun getPlatformModule(appEnvironment: AppEnvironment): Module = module {
scope(named(FEED_SYNC_SCOPE_NAME)) {
scoped<SqlDriver>(named(SYNC_DB_DRIVER)) {
val databaseName = if (appEnvironment.isDebug()) {
SyncedDatabaseHelper.SYNC_DATABASE_NAME_DEBUG
} else {
SyncedDatabaseHelper.SYNC_DATABASE_NAME_PROD
}

DatabaseFileMigration(
databaseName = databaseName,
).migrate()

NativeSqliteDriver(
schema = FeedFlowFeedSyncDB.Schema,
name = if (appEnvironment.isDebug()) {
SyncedDatabaseHelper.SYNC_DATABASE_NAME_DEBUG
} else {
SyncedDatabaseHelper.SYNC_DATABASE_NAME_PROD
onConfiguration = { conf ->
conf.copy(
extendedConfig = conf.extendedConfig.copy(
basePath = getAppGroupDatabasePath(),
),
)
},
name = databaseName,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import co.touchlab.kermit.crashlytics.CrashlyticsLogWriter
import com.prof18.feedflow.core.domain.HtmlParser
import com.prof18.feedflow.core.utils.AppConfig
import com.prof18.feedflow.core.utils.AppEnvironment
import com.prof18.feedflow.core.utils.DatabaseFileMigration
import com.prof18.feedflow.core.utils.DispatcherProvider
import com.prof18.feedflow.database.DatabaseHelper
import com.prof18.feedflow.database.createDatabaseDriver
import com.prof18.feedflow.feedsync.dropbox.DropboxDataSource
import com.prof18.feedflow.i18n.EnFeedFlowStrings
Expand Down Expand Up @@ -95,6 +97,13 @@ internal actual fun getPlatformModule(appEnvironment: AppEnvironment): Module =
}

single<SqlDriver> {
DatabaseFileMigration(
databaseName = if (appEnvironment.isDebug()) {
DatabaseHelper.APP_DATABASE_NAME_DEBUG
} else {
DatabaseHelper.APP_DATABASE_NAME_PROD
},
).migrate()
createDatabaseDriver(appEnvironment)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.prof18.feedflow.core.model.SyncResult
import com.prof18.feedflow.core.utils.AppEnvironment
import com.prof18.feedflow.core.utils.DispatcherProvider
import com.prof18.feedflow.core.utils.FeedSyncMessageQueue
import com.prof18.feedflow.core.utils.getAppGroupDatabasePath
import com.prof18.feedflow.feedsync.database.data.SyncedDatabaseHelper.Companion.SYNC_DATABASE_NAME_DEBUG
import com.prof18.feedflow.feedsync.database.data.SyncedDatabaseHelper.Companion.SYNC_DATABASE_NAME_PROD
import com.prof18.feedflow.feedsync.dropbox.DropboxDataSource
Expand All @@ -29,7 +30,6 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
import platform.Foundation.NSApplicationSupportDirectory
import platform.Foundation.NSDocumentDirectory
import platform.Foundation.NSError
import platform.Foundation.NSFileManager
Expand Down Expand Up @@ -140,26 +140,9 @@ internal class FeedSyncIosWorker(
}
}

@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
private fun getDatabaseUrl(): NSURL? {
memScoped {
val errorPtr: ObjCObjectVar<NSError?> = alloc()
val documentsDirectory = NSFileManager.defaultManager.URLForDirectory(
directory = NSApplicationSupportDirectory,
inDomain = NSUserDomainMask,
appropriateForURL = null,
create = true,
error = errorPtr.ptr,
)
val databaseUrl = documentsDirectory?.URLByAppendingPathComponent("databases/${getDatabaseName()}")

if (errorPtr.value != null) {
logger.e { "Error getting database URL: ${errorPtr.value}" }
return null
}
return databaseUrl
}
}
private fun getDatabaseUrl(): NSURL? =
NSURL.fileURLWithPath(getAppGroupDatabasePath())
.URLByAppendingPathComponent(getDatabaseName())

@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
private fun replaceDatabase(url: NSURL): Boolean {
Expand Down

0 comments on commit dc22ccc

Please sign in to comment.