Initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,63 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.androidMultiplatformLibrary)
|
||||
alias(libs.plugins.kotlinxSerialization)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
android {
|
||||
namespace = "ru.tanexc.hemoanalysis"
|
||||
compileSdk {
|
||||
version = release(36)
|
||||
}
|
||||
minSdk = 26
|
||||
}
|
||||
|
||||
listOf(
|
||||
iosArm64(),
|
||||
iosSimulatorArm64()
|
||||
).forEach { iosTarget ->
|
||||
iosTarget.binaries.framework {
|
||||
baseName = "datakit"
|
||||
isStatic = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api(projects.domain)
|
||||
api(projects.core.database)
|
||||
api(projects.core.network)
|
||||
|
||||
// settings store
|
||||
implementation(libs.multiplatform.settings)
|
||||
implementation(libs.multiplatform.settings.serialization)
|
||||
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
|
||||
// koin
|
||||
implementation(libs.koin.core)
|
||||
|
||||
implementation(libs.kotlinx.datetime)
|
||||
}
|
||||
}
|
||||
|
||||
androidMain {
|
||||
dependencies {
|
||||
// datastore
|
||||
implementation(libs.androidx.datastore)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
}
|
||||
}
|
||||
|
||||
iosMain {
|
||||
dependencies {
|
||||
// datastore
|
||||
implementation(libs.androidx.datastore)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package ru.tanexc.hemoanalysis.usecase
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import ru.tanexc.hemoanalysis.Constants.INTERNAL_MODELS_DIR
|
||||
import ru.tanexc.hemoanalysis.domain.repository.ModelRepository
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.GetLatestModelFileUseCase
|
||||
import java.io.File
|
||||
|
||||
internal actual class GetLatestModelFileUseCaseImpl : GetLatestModelFileUseCase, KoinComponent {
|
||||
private val context: Context by inject()
|
||||
private val modelRepository: ModelRepository by inject()
|
||||
|
||||
actual override operator fun invoke(): ByteArray? {
|
||||
val filename = modelRepository.getCurrentModelInfo()?.filename?: return null
|
||||
|
||||
val modelsDir = File(context.filesDir, INTERNAL_MODELS_DIR)
|
||||
|
||||
if (!modelsDir.exists()) return null
|
||||
|
||||
val modelFile = File(modelsDir, filename)
|
||||
|
||||
if (!modelFile.exists()) return null
|
||||
|
||||
Log.i("GetLatestModelFileUseCase", "get model bytearray ${modelFile.absolutePath}")
|
||||
|
||||
return modelFile.inputStream().buffered().use { inputStream ->
|
||||
inputStream.readBytes()
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package ru.tanexc.hemoanalysis.usecase
|
||||
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import ru.tanexc.hemoanalysis.domain.repository.ModelRepository
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.GetModelPathUseCase
|
||||
import kotlin.getValue
|
||||
|
||||
actual class GetModelPathUseCaseImpl actual constructor() : GetModelPathUseCase, KoinComponent {
|
||||
private val modelRepository: ModelRepository by inject()
|
||||
|
||||
actual override fun invoke(): String? {
|
||||
return modelRepository.getCurrentModelInfo()?.filename
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package ru.tanexc.hemoanalysis.usecase
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import ru.tanexc.hemoanalysis.Constants.INTERNAL_MODELS_DIR
|
||||
import ru.tanexc.hemoanalysis.domain.model.Size
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.BytesReader
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.SaveModelFileUseCase
|
||||
import java.io.File
|
||||
|
||||
internal actual class SaveModelFileUseCaseImpl : SaveModelFileUseCase, KoinComponent {
|
||||
val context: Context by inject()
|
||||
|
||||
actual override suspend operator fun invoke(
|
||||
filename: String,
|
||||
bytesReader: BytesReader
|
||||
): Size {
|
||||
val modelsDir = File(context.filesDir, INTERNAL_MODELS_DIR)
|
||||
modelsDir.mkdirs()
|
||||
|
||||
Log.i("SaveModelFileUseCase", "saving model into ${modelsDir.absolutePath}")
|
||||
|
||||
val modelFile = File(modelsDir, filename)
|
||||
|
||||
modelFile.outputStream().buffered().use { output ->
|
||||
var bytes = bytesReader.readNext(BUFFER_SIZE)
|
||||
|
||||
while (bytes.isNotEmpty()) {
|
||||
output.write(bytes)
|
||||
bytes = bytesReader.readNext(BUFFER_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("SaveModelFileUseCase", "model saved into file $modelFile ${modelFile.length()}")
|
||||
|
||||
return Size(modelFile.length().toInt())
|
||||
}
|
||||
|
||||
private val BUFFER_SIZE = 1024 * 1024
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.tanexc.hemoanalysis
|
||||
|
||||
internal object Constants {
|
||||
const val INTERNAL_MODELS_DIR = "models"
|
||||
const val DEFAULT_MODEL_NAME = "model.ort"
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package ru.tanexc.hemoanalysis.di
|
||||
|
||||
import org.koin.core.context.loadKoinModules
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
import ru.tanexc.hemoanalysis.repository.AnalysisParamsRepositoryImpl
|
||||
import ru.tanexc.hemoanalysis.repository.ModelRepositoryImpl
|
||||
import ru.tanexc.hemoanalysis.core.database.databaseModule
|
||||
import ru.tanexc.hemoanalysis.core.network.di.networkModule
|
||||
import ru.tanexc.hemoanalysis.domain.repository.AnalysisParamsRepository
|
||||
import ru.tanexc.hemoanalysis.domain.repository.ModelRepository
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.GetLatestModelFileUseCase
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.GetModelPathUseCase
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.SaveModelFileUseCase
|
||||
import ru.tanexc.hemoanalysis.usecase.GetLatestModelFileUseCaseImpl
|
||||
import ru.tanexc.hemoanalysis.usecase.GetModelPathUseCaseImpl
|
||||
import ru.tanexc.hemoanalysis.usecase.SaveModelFileUseCaseImpl
|
||||
|
||||
val dataModule = module {
|
||||
loadKoinModules(listOf(databaseModule, networkModule))
|
||||
singleOf(::GetModelPathUseCaseImpl) bind GetModelPathUseCase::class
|
||||
singleOf(::SaveModelFileUseCaseImpl) bind SaveModelFileUseCase::class
|
||||
singleOf(::GetLatestModelFileUseCaseImpl) bind GetLatestModelFileUseCase::class
|
||||
|
||||
single<AnalysisParamsRepository> {
|
||||
AnalysisParamsRepositoryImpl(
|
||||
settings = get(named("AnalysisParamsSettings"))
|
||||
)
|
||||
}
|
||||
|
||||
single<ModelRepository> {
|
||||
ModelRepositoryImpl(
|
||||
settings = get(named("ModelInfoSettings")),
|
||||
api = get(),
|
||||
saveModelFileUseCase = get()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package ru.tanexc.hemoanalysis.repository
|
||||
|
||||
import com.russhwolf.settings.Settings
|
||||
import com.russhwolf.settings.float
|
||||
import com.russhwolf.settings.int
|
||||
import ru.tanexc.hemoanalysis.domain.repository.AnalysisParamsRepository
|
||||
|
||||
internal class AnalysisParamsRepositoryImpl(
|
||||
settings: Settings
|
||||
): AnalysisParamsRepository {
|
||||
override var confThreshold by settings.float("conf_threshold", 0.5f)
|
||||
override var iouThreshold by settings.float("iou_threshold", 0.5f)
|
||||
override var preprocessVariant by settings.int("preprocess_variant", 0)
|
||||
override var roiVariant by settings.int("roi_variant", 0)
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
package ru.tanexc.hemoanalysis.repository
|
||||
|
||||
import com.russhwolf.settings.Settings
|
||||
import com.russhwolf.settings.string
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.format
|
||||
import kotlinx.datetime.format.DateTimeFormat
|
||||
import kotlinx.datetime.format.char
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.koin.core.logger.Logger
|
||||
import ru.tanexc.hemoanalysis.core.network.ModelApi
|
||||
import ru.tanexc.hemoanalysis.core.network.RequestState
|
||||
import ru.tanexc.hemoanalysis.domain.State
|
||||
import ru.tanexc.hemoanalysis.domain.model.ModelInfo
|
||||
import ru.tanexc.hemoanalysis.domain.model.Size
|
||||
import ru.tanexc.hemoanalysis.domain.repository.ModelRepository
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.SaveModelFileUseCase
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
internal class ModelRepositoryImpl(
|
||||
settings: Settings,
|
||||
private val api: ModelApi,
|
||||
private val saveModelFileUseCase: SaveModelFileUseCase,
|
||||
) : ModelRepository {
|
||||
private var currentModelInfo: String by settings.string("current_model_info", "")
|
||||
private var latestModelInfo: String by settings.string("latest_model_info", "")
|
||||
private var apiKey: String by settings.string("api_key", "")
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
override fun getLatestModelInfo(): ModelInfo? = runCatching {
|
||||
json.decodeFromString<ModelInfo>(latestModelInfo)
|
||||
}.getOrNull()
|
||||
|
||||
override fun getCurrentModelInfo(): ModelInfo? = runCatching {
|
||||
json.decodeFromString<ModelInfo>(currentModelInfo)
|
||||
}.getOrNull()
|
||||
|
||||
override fun updateModelInfo(): Flow<State<ModelInfo?>> = flow {
|
||||
emit(State.Loading)
|
||||
when (val state = api.getLatestModelInfo(apiKey)) {
|
||||
is RequestState.Error -> emit(State.Error)
|
||||
is RequestState.Success -> {
|
||||
val timeNow = nowFormatted()
|
||||
val meta = state.data
|
||||
val currentInfo = getCurrentModelInfo()?.copy(
|
||||
lastUpdateTime = timeNow
|
||||
)
|
||||
currentModelInfo = currentInfo.asString()
|
||||
|
||||
if (meta.version.isBiggerVersion(currentInfo?.version ?: "")) {
|
||||
val modelInfo = ModelInfo(
|
||||
version = meta.version,
|
||||
lastUpdateTime = timeNow,
|
||||
createdAt = meta.createdAt.toDisplayDateTime(),
|
||||
filename = meta.filename,
|
||||
isLatest = true,
|
||||
size = Size.ZERO
|
||||
)
|
||||
|
||||
currentModelInfo = currentInfo?.copy(
|
||||
isLatest = false
|
||||
).asString()
|
||||
|
||||
latestModelInfo = modelInfo.asString()
|
||||
emit(State.Success(modelInfo))
|
||||
} else {
|
||||
emit(State.Success(currentInfo))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadLatestModel(): Flow<State<ModelInfo?>> = flow {
|
||||
emit(State.Loading)
|
||||
val latestInfo = getLatestModelInfo()
|
||||
if (latestInfo == null) {
|
||||
emit(State.Error)
|
||||
return@flow
|
||||
}
|
||||
|
||||
val state = api.downloadLatestModel(
|
||||
version = latestInfo.version,
|
||||
filename = latestInfo.filename,
|
||||
apiKey = apiKey
|
||||
)
|
||||
|
||||
when (state) {
|
||||
is RequestState.Error -> emit(State.Error)
|
||||
is RequestState.Success -> {
|
||||
val savedSize = saveModelFileUseCase(latestInfo.filename, state.data.bytesReader)
|
||||
|
||||
val currentInfo = latestInfo.copy(
|
||||
lastUpdateTime = nowFormatted(),
|
||||
isLatest = true,
|
||||
size = savedSize
|
||||
)
|
||||
|
||||
currentModelInfo = currentInfo.asString()
|
||||
emit(State.Success(currentInfo))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getKeyApi(): String = apiKey
|
||||
|
||||
override fun updateApiKey(apiKey: String) {
|
||||
this.apiKey = apiKey
|
||||
}
|
||||
|
||||
private fun String.toVersionParts(): List<Int> {
|
||||
return split(".").map { part ->
|
||||
part.toIntOrNull() ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.isBiggerVersion(
|
||||
second: String
|
||||
): Boolean {
|
||||
val firstParts = this.toVersionParts()
|
||||
val secondParts = second.toVersionParts()
|
||||
|
||||
val maxSize = maxOf(firstParts.size, secondParts.size)
|
||||
|
||||
for (index in 0 until maxSize) {
|
||||
val firstValue = firstParts.getOrElse(index) { 0 }
|
||||
val secondValue = secondParts.getOrElse(index) { 0 }
|
||||
|
||||
if (firstValue > secondValue) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun ModelInfo?.asString() = this?.let(json::encodeToString) ?: ""
|
||||
|
||||
private fun nowFormatted(): String {
|
||||
return Clock.System.now().toDisplayDateTime()
|
||||
}
|
||||
|
||||
private fun String.toDisplayDateTime(): String {
|
||||
if (runCatching { displayDateTimeFormat.parse(this) }.isSuccess) return this
|
||||
|
||||
return runCatching {
|
||||
Instant.parse(this).toDisplayDateTime()
|
||||
}.getOrElse {
|
||||
runCatching {
|
||||
LocalDateTime.parse(this).toDisplayDateTime()
|
||||
}.getOrDefault(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Instant.toDisplayDateTime(): String {
|
||||
return toLocalDateTime(TimeZone.currentSystemDefault()).toDisplayDateTime()
|
||||
}
|
||||
|
||||
private fun LocalDateTime.toDisplayDateTime(): String {
|
||||
return format(displayDateTimeFormat)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val displayDateTimeFormat: DateTimeFormat<LocalDateTime> = LocalDateTime.Format {
|
||||
day()
|
||||
char('.')
|
||||
monthNumber()
|
||||
char('.')
|
||||
year()
|
||||
char(' ')
|
||||
hour()
|
||||
char(':')
|
||||
minute()
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package ru.tanexc.hemoanalysis.usecase
|
||||
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.GetLatestModelFileUseCase
|
||||
|
||||
internal expect class GetLatestModelFileUseCaseImpl(): GetLatestModelFileUseCase {
|
||||
override operator fun invoke(): ByteArray?
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package ru.tanexc.hemoanalysis.usecase
|
||||
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.GetModelPathUseCase
|
||||
|
||||
expect class GetModelPathUseCaseImpl() : GetModelPathUseCase {
|
||||
override fun invoke(): String?
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package ru.tanexc.hemoanalysis.usecase
|
||||
|
||||
import ru.tanexc.hemoanalysis.domain.model.Size
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.BytesReader
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.SaveModelFileUseCase
|
||||
|
||||
internal expect class SaveModelFileUseCaseImpl(): SaveModelFileUseCase {
|
||||
override suspend fun invoke(filename: String, bytesReader: BytesReader): Size
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package ru.tanexc.hemoanalysis.usecase
|
||||
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.readBytes
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import platform.Foundation.NSApplicationSupportDirectory
|
||||
import platform.Foundation.NSData
|
||||
import platform.Foundation.NSFileManager
|
||||
import platform.Foundation.NSSearchPathForDirectoriesInDomains
|
||||
import platform.Foundation.NSUserDomainMask
|
||||
import platform.Foundation.dataWithContentsOfFile
|
||||
import ru.tanexc.hemoanalysis.Constants.INTERNAL_MODELS_DIR
|
||||
import ru.tanexc.hemoanalysis.domain.repository.ModelRepository
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.GetLatestModelFileUseCase
|
||||
import kotlin.getValue
|
||||
|
||||
internal actual class GetLatestModelFileUseCaseImpl : GetLatestModelFileUseCase, KoinComponent {
|
||||
private val modelRepository: ModelRepository by inject()
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual override operator fun invoke(): ByteArray? {
|
||||
val filename = modelRepository.getCurrentModelInfo()?.filename?: return null
|
||||
|
||||
val appSupportDir = getApplicationSupportDir()
|
||||
val modelFilePath = "$appSupportDir/$INTERNAL_MODELS_DIR/$filename"
|
||||
|
||||
if (!NSFileManager.defaultManager.fileExistsAtPath(modelFilePath)) return null
|
||||
|
||||
val data = NSData.dataWithContentsOfFile(modelFilePath) ?: return null
|
||||
|
||||
return data.bytes?.readBytes(data.length.toInt())
|
||||
}
|
||||
|
||||
private fun getApplicationSupportDir(): String {
|
||||
val paths = NSSearchPathForDirectoriesInDomains(
|
||||
directory = NSApplicationSupportDirectory,
|
||||
domainMask = NSUserDomainMask,
|
||||
expandTilde = true
|
||||
)
|
||||
|
||||
return paths.first() as? String ?: error("Application Support directory not found")
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package ru.tanexc.hemoanalysis.usecase
|
||||
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import platform.Foundation.NSApplicationSupportDirectory
|
||||
import platform.Foundation.NSFileManager
|
||||
import platform.Foundation.NSSearchPathForDirectoriesInDomains
|
||||
import platform.Foundation.NSUserDomainMask
|
||||
import ru.tanexc.hemoanalysis.Constants.INTERNAL_MODELS_DIR
|
||||
import ru.tanexc.hemoanalysis.domain.repository.ModelRepository
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.GetModelPathUseCase
|
||||
import kotlin.getValue
|
||||
|
||||
actual class GetModelPathUseCaseImpl actual constructor() : GetModelPathUseCase, KoinComponent {
|
||||
private val modelRepository: ModelRepository by inject()
|
||||
|
||||
actual override fun invoke(): String? {
|
||||
val filename = modelRepository.getCurrentModelInfo()?.filename ?: return null
|
||||
val modelPath = "${getApplicationSupportDir()}/$INTERNAL_MODELS_DIR/$filename"
|
||||
return modelPath.takeIf { NSFileManager.defaultManager.fileExistsAtPath(it) }
|
||||
}
|
||||
|
||||
private fun getApplicationSupportDir(): String {
|
||||
val paths = NSSearchPathForDirectoriesInDomains(
|
||||
directory = NSApplicationSupportDirectory,
|
||||
domainMask = NSUserDomainMask,
|
||||
expandTilde = true
|
||||
)
|
||||
|
||||
return paths.first() as? String ?: error("Application Support directory not found")
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package ru.tanexc.hemoanalysis.usecase
|
||||
|
||||
import kotlinx.cinterop.BetaInteropApi
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.allocArrayOf
|
||||
import kotlinx.cinterop.memScoped
|
||||
import platform.Foundation.NSApplicationSupportDirectory
|
||||
import platform.Foundation.NSData
|
||||
import platform.Foundation.NSFileHandle
|
||||
import platform.Foundation.NSFileManager
|
||||
import platform.Foundation.NSSearchPathForDirectoriesInDomains
|
||||
import platform.Foundation.NSUserDomainMask
|
||||
import platform.Foundation.closeFile
|
||||
import platform.Foundation.create
|
||||
import platform.Foundation.fileHandleForWritingAtPath
|
||||
import platform.Foundation.seekToEndOfFile
|
||||
import platform.Foundation.writeData
|
||||
import ru.tanexc.hemoanalysis.Constants.INTERNAL_MODELS_DIR
|
||||
import ru.tanexc.hemoanalysis.domain.model.Size
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.BytesReader
|
||||
import ru.tanexc.hemoanalysis.domain.usecase.SaveModelFileUseCase
|
||||
|
||||
internal actual class SaveModelFileUseCaseImpl actual constructor() :
|
||||
SaveModelFileUseCase {
|
||||
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
|
||||
actual override suspend operator fun invoke(
|
||||
filename: String,
|
||||
bytesReader: BytesReader
|
||||
): Size {
|
||||
val appSupportDir = getApplicationSupportDir()
|
||||
val modelDir = "$appSupportDir/$INTERNAL_MODELS_DIR"
|
||||
NSFileManager.defaultManager.createDirectoryAtPath(
|
||||
path = modelDir,
|
||||
withIntermediateDirectories = true,
|
||||
attributes = null,
|
||||
error = null
|
||||
)
|
||||
|
||||
val filePath = "$modelDir/$filename"
|
||||
if (NSFileManager.defaultManager.fileExistsAtPath(filePath)) {
|
||||
NSFileManager.defaultManager.removeItemAtPath(filePath, error = null)
|
||||
}
|
||||
check(
|
||||
NSFileManager.defaultManager.createFileAtPath(
|
||||
path = filePath,
|
||||
contents = null,
|
||||
attributes = null
|
||||
)
|
||||
) { "Cannot create file at $filePath" }
|
||||
|
||||
val handle = NSFileHandle.fileHandleForWritingAtPath(filePath)
|
||||
?: error("Cannot open file for writing at $filePath")
|
||||
var totalBytes = 0
|
||||
try {
|
||||
handle.seekToEndOfFile()
|
||||
|
||||
var bytes = bytesReader.readNext(BUFFER_SIZE)
|
||||
while (bytes.isNotEmpty()) {
|
||||
handle.writeData(bytes.toNSData())
|
||||
totalBytes += bytes.size
|
||||
bytes = bytesReader.readNext(BUFFER_SIZE)
|
||||
}
|
||||
} finally {
|
||||
handle.closeFile()
|
||||
}
|
||||
|
||||
return Size(totalBytes)
|
||||
}
|
||||
|
||||
private fun getApplicationSupportDir(): String {
|
||||
val paths = NSSearchPathForDirectoriesInDomains(
|
||||
directory = NSApplicationSupportDirectory,
|
||||
domainMask = NSUserDomainMask,
|
||||
expandTilde = true
|
||||
)
|
||||
|
||||
return paths.first() as? String ?: error("Application Support directory not found")
|
||||
}
|
||||
|
||||
private val BUFFER_SIZE = 1024 * 1024
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
|
||||
private fun ByteArray.toNSData(): NSData = memScoped {
|
||||
NSData.create(
|
||||
bytes = allocArrayOf(this@toNSData),
|
||||
length = this@toNSData.size.toULong()
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user