Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,7 @@ Current Proto API surface:
| Category | APIs |
|----------|------|
| Lifecycle | `close()` |
| Byte backup | `exportAsByteArray`, `importFromByteArray` |
| Whole message | `data()` |
| Raw fields | `field()` |
| Enum fields | `enumField`, `nullableEnumField`, `enumSetField` |
Expand All @@ -875,6 +876,22 @@ val dataPref: ProtoPreference<MyProtoMessage> = protoDatastore.data()
// Then use get(), set(), asFlow(), etc. just like Preferences DataStore
```

#### Byte Backup and Restore

Factory-created proto datastores can export and import the raw datastore file bytes:

```kotlin
val backupBytes: ByteArray = protoDatastore.exportAsByteArray()

protoDatastore.importFromByteArray(backupBytes)
```

`exportAsByteArray()` returns an empty `ByteArray` if the backing file has not been created yet.
`importFromByteArray()` decodes the bytes with the datastore's `OkioSerializer` and replaces the
whole proto message. Low-level `GenericProtoDatastore` wrappers only support these APIs when they
are constructed with the serializer and resolved path; otherwise they throw
`UnsupportedOperationException`.

#### Per-Field Access

Use `field()` to create a preference for an individual field of the proto message. The `getter`
Expand Down
4 changes: 4 additions & 0 deletions generic-datastore-proto/api/generic-datastore-proto.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ abstract interface <#A: kotlin/Any?> io.github.arthurkun.generic.datastore.proto
abstract fun <#A1: kotlin/Enum<#A1>> nullableEnumField(kotlin/Array<#A1>, kotlin/Function1<#A, kotlin/String?>, kotlin/Function2<#A, kotlin/String?, #A>): io.github.arthurkun.generic.datastore.proto/ProtoPreference<#A1?> // io.github.arthurkun.generic.datastore.proto/ProtoDatastore.nullableEnumField|nullableEnumField(kotlin.Array<0:0>;kotlin.Function1<1:0,kotlin.String?>;kotlin.Function2<1:0,kotlin.String?,1:0>){0§<kotlin.Enum<0:0>>}[0]
abstract fun data(): io.github.arthurkun.generic.datastore.proto/ProtoPreference<#A> // io.github.arthurkun.generic.datastore.proto/ProtoDatastore.data|data(){}[0]
open fun close() // io.github.arthurkun.generic.datastore.proto/ProtoDatastore.close|close(){}[0]
open suspend fun exportAsByteArray(): kotlin/ByteArray // io.github.arthurkun.generic.datastore.proto/ProtoDatastore.exportAsByteArray|exportAsByteArray(){}[0]
open suspend fun importFromByteArray(kotlin/ByteArray) // io.github.arthurkun.generic.datastore.proto/ProtoDatastore.importFromByteArray|importFromByteArray(kotlin.ByteArray){}[0]
}

abstract interface <#A: kotlin/Any?> io.github.arthurkun.generic.datastore.proto/ProtoPreference : io.github.arthurkun.generic.datastore.core/DelegatedPreference<#A> // io.github.arthurkun.generic.datastore.proto/ProtoPreference|null[0]
Expand All @@ -44,6 +46,8 @@ final class <#A: kotlin/Any?> io.github.arthurkun.generic.datastore.proto/Generi
final fun <#A1: kotlin/Enum<#A1>> nullableEnumField(kotlin/Array<#A1>, kotlin/Function1<#A, kotlin/String?>, kotlin/Function2<#A, kotlin/String?, #A>): io.github.arthurkun.generic.datastore.proto/ProtoPreference<#A1?> // io.github.arthurkun.generic.datastore.proto/GenericProtoDatastore.nullableEnumField|nullableEnumField(kotlin.Array<0:0>;kotlin.Function1<1:0,kotlin.String?>;kotlin.Function2<1:0,kotlin.String?,1:0>){0§<kotlin.Enum<0:0>>}[0]
final fun close() // io.github.arthurkun.generic.datastore.proto/GenericProtoDatastore.close|close(){}[0]
final fun data(): io.github.arthurkun.generic.datastore.proto/ProtoPreference<#A> // io.github.arthurkun.generic.datastore.proto/GenericProtoDatastore.data|data(){}[0]
final suspend fun exportAsByteArray(): kotlin/ByteArray // io.github.arthurkun.generic.datastore.proto/GenericProtoDatastore.exportAsByteArray|exportAsByteArray(){}[0]
final suspend fun importFromByteArray(kotlin/ByteArray) // io.github.arthurkun.generic.datastore.proto/GenericProtoDatastore.importFromByteArray|importFromByteArray(kotlin.ByteArray){}[0]
}

final fun <#A: kotlin/Any?> io.github.arthurkun.generic.datastore.proto/createProtoDatastore(androidx.datastore.core.okio/OkioSerializer<#A>, #A, kotlin/String, kotlin/String = ..., androidx.datastore.core.handlers/ReplaceFileCorruptionHandler<#A>? = ..., kotlin.collections/List<androidx.datastore.core/DataMigration<#A>> = ..., kotlinx.coroutines/CoroutineScope? = ..., kotlinx.serialization.json/Json = ..., kotlin/Function0<kotlin/String>): io.github.arthurkun.generic.datastore.proto/GenericProtoDatastore<#A> // io.github.arthurkun.generic.datastore.proto/createProtoDatastore|createProtoDatastore(androidx.datastore.core.okio.OkioSerializer<0:0>;0:0;kotlin.String;kotlin.String;androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<0:0>?;kotlin.collections.List<androidx.datastore.core.DataMigration<0:0>>;kotlinx.coroutines.CoroutineScope?;kotlinx.serialization.json.Json;kotlin.Function0<kotlin.String>){0§<kotlin.Any?>}[0]
Expand Down
6 changes: 6 additions & 0 deletions generic-datastore-proto/api/jvm/generic-datastore-proto.api
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ public final class io/github/arthurkun/generic/datastore/proto/GenericProtoDatas
public fun data ()Lio/github/arthurkun/generic/datastore/proto/ProtoPreference;
public fun enumField (Ljava/lang/Enum;[Ljava/lang/Enum;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lio/github/arthurkun/generic/datastore/proto/ProtoPreference;
public fun enumSetField (Ljava/util/Set;[Ljava/lang/Enum;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lio/github/arthurkun/generic/datastore/proto/ProtoPreference;
public fun exportAsByteArray (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun field (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lio/github/arthurkun/generic/datastore/proto/ProtoPreference;
public fun importFromByteArray ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun kserializedField (Ljava/lang/Object;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/json/Json;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lio/github/arthurkun/generic/datastore/proto/ProtoPreference;
public fun kserializedListField (Ljava/util/List;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/json/Json;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lio/github/arthurkun/generic/datastore/proto/ProtoPreference;
public fun kserializedSetField (Ljava/util/Set;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/json/Json;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lio/github/arthurkun/generic/datastore/proto/ProtoPreference;
Expand All @@ -34,7 +36,9 @@ public abstract interface class io/github/arthurkun/generic/datastore/proto/Prot
public abstract fun enumField (Ljava/lang/Enum;[Ljava/lang/Enum;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lio/github/arthurkun/generic/datastore/proto/ProtoPreference;
public abstract fun enumSetField (Ljava/util/Set;[Ljava/lang/Enum;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lio/github/arthurkun/generic/datastore/proto/ProtoPreference;
public static synthetic fun enumSetField$default (Lio/github/arthurkun/generic/datastore/proto/ProtoDatastore;Ljava/util/Set;[Ljava/lang/Enum;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/github/arthurkun/generic/datastore/proto/ProtoPreference;
public fun exportAsByteArray (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun field (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lio/github/arthurkun/generic/datastore/proto/ProtoPreference;
public fun importFromByteArray ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun kserializedField (Ljava/lang/Object;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/json/Json;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lio/github/arthurkun/generic/datastore/proto/ProtoPreference;
public static synthetic fun kserializedField$default (Lio/github/arthurkun/generic/datastore/proto/ProtoDatastore;Ljava/lang/Object;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/json/Json;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/github/arthurkun/generic/datastore/proto/ProtoPreference;
public abstract fun kserializedListField (Ljava/util/List;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/json/Json;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lio/github/arthurkun/generic/datastore/proto/ProtoPreference;
Expand All @@ -58,6 +62,8 @@ public abstract interface class io/github/arthurkun/generic/datastore/proto/Prot
public final class io/github/arthurkun/generic/datastore/proto/ProtoDatastore$DefaultImpls {
public static fun close (Lio/github/arthurkun/generic/datastore/proto/ProtoDatastore;)V
public static synthetic fun enumSetField$default (Lio/github/arthurkun/generic/datastore/proto/ProtoDatastore;Ljava/util/Set;[Ljava/lang/Enum;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/github/arthurkun/generic/datastore/proto/ProtoPreference;
public static fun exportAsByteArray (Lio/github/arthurkun/generic/datastore/proto/ProtoDatastore;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static fun importFromByteArray (Lio/github/arthurkun/generic/datastore/proto/ProtoDatastore;[BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun kserializedField$default (Lio/github/arthurkun/generic/datastore/proto/ProtoDatastore;Ljava/lang/Object;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/json/Json;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/github/arthurkun/generic/datastore/proto/ProtoPreference;
public static synthetic fun kserializedListField$default (Lio/github/arthurkun/generic/datastore/proto/ProtoDatastore;Ljava/util/List;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/json/Json;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/github/arthurkun/generic/datastore/proto/ProtoPreference;
public static synthetic fun kserializedSetField$default (Lio/github/arthurkun/generic/datastore/proto/ProtoDatastore;Ljava/util/Set;Lkotlinx/serialization/KSerializer;Lkotlinx/serialization/json/Json;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/github/arthurkun/generic/datastore/proto/ProtoPreference;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.github.arthurkun.generic.datastore.proto.backup

import android.content.Context
import androidx.datastore.core.DataMigration
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.github.arthurkun.generic.datastore.proto.GenericProtoDatastore
import io.github.arthurkun.generic.datastore.proto.core.AndroidProtoTestHelper
import io.github.arthurkun.generic.datastore.proto.core.TestProtoData
import io.github.arthurkun.generic.datastore.proto.core.TestProtoDataSerializer
import kotlinx.coroutines.test.TestDispatcher
import org.junit.After
import org.junit.Before
import org.junit.runner.RunWith
import java.io.File
import io.github.arthurkun.generic.datastore.proto.createProtoDatastore as createProtoDatastoreFactory

@RunWith(AndroidJUnit4::class)
class AndroidProtoBackupTest : AbstractProtoBackupTest() {

private val helper = AndroidProtoTestHelper.standard("test_proto_backup")

override val protoDatastore get() = helper.protoDatastore
override val testDispatcher: TestDispatcher get() = helper.testDispatcher

override fun createProtoDatastore(
datastoreName: String,
migrations: List<DataMigration<TestProtoData>>,
): GenericProtoDatastore<TestProtoData> = createProtoDatastoreFactory(
serializer = TestProtoDataSerializer,
defaultValue = TestProtoData(),
migrations = migrations,
producePath = {
val context = ApplicationProvider.getApplicationContext<Context>()
context.filesDir.resolve("datastore/$datastoreName.pb").absolutePath
},
)

override fun deleteProtoDatastore(datastoreName: String) {
val context = ApplicationProvider.getApplicationContext<Context>()
val dataStoreFile = File(context.filesDir, "datastore/$datastoreName.pb")
if (dataStoreFile.exists()) {
dataStoreFile.delete()
}
}

@Before
fun setup() = helper.setup()

@After
fun tearDown() = helper.tearDown()
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ public fun <T> createProtoDatastore(
key = nameKey,
defaultJson = defaultJson,
ownedScope = datastoreScope,
serializer = serializer,
path = path,
)
}

Expand Down Expand Up @@ -107,23 +109,26 @@ public fun <T> createProtoDatastore(
produceOkioPath: () -> okio.Path,
): GenericProtoDatastore<T> {
val datastoreScope = createDatastoreScope(scope)
val path = produceOkioPath()
val datastore = createDataStore(
storage = OkioStorage(
fileSystem = systemFileSystem,
serializer = serializer,
producePath = produceOkioPath,
producePath = { path },
),
corruptionHandler = corruptionHandler,
migrations = migrations,
scope = datastoreScope,
)
val nameKey = key ?: produceOkioPath().name
val nameKey = key ?: path.name
return GenericProtoDatastore(
datastore = datastore,
defaultValue = defaultValue,
key = nameKey,
defaultJson = defaultJson,
ownedScope = datastoreScope,
serializer = serializer,
path = path,
)
}

Expand Down Expand Up @@ -180,6 +185,8 @@ public fun <T> createProtoDatastore(
key = nameKey,
defaultJson = defaultJson,
ownedScope = datastoreScope,
serializer = serializer,
path = path,
)
}

Expand Down Expand Up @@ -221,11 +228,12 @@ public fun <T> createProtoDatastore(
producePath: () -> String,
): GenericProtoDatastore<T> {
val datastoreScope = createDatastoreScope(scope)
val path = producePath().toPath() / fileName
val datastore = createDataStore(
storage = OkioStorage(
fileSystem = systemFileSystem,
serializer = serializer,
producePath = { producePath().toPath() / fileName },
producePath = { path },
),
corruptionHandler = corruptionHandler,
migrations = migrations,
Expand All @@ -237,6 +245,8 @@ public fun <T> createProtoDatastore(
key = key,
defaultJson = defaultJson,
ownedScope = datastoreScope,
serializer = serializer,
path = path,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
package io.github.arthurkun.generic.datastore.proto

import androidx.datastore.core.DataStore
import androidx.datastore.core.okio.OkioSerializer
import io.github.arthurkun.generic.datastore.core.InternalGenericDatastoreApi
import io.github.arthurkun.generic.datastore.core.PreferenceDefaults
import io.github.arthurkun.generic.datastore.proto.backup.ProtoBackupCreator
import io.github.arthurkun.generic.datastore.proto.backup.ProtoBackupRestorer
import io.github.arthurkun.generic.datastore.proto.core.GenericProtoPreferenceItem
import io.github.arthurkun.generic.datastore.proto.custom.ProtoSerialFieldPreference
import io.github.arthurkun.generic.datastore.proto.custom.core.enumFieldInternal
Expand All @@ -23,9 +26,11 @@ import io.github.arthurkun.generic.datastore.proto.custom.set.serializedSetField
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.Json
import okio.Path

/**
* A DataStore implementation for Proto DataStore.
Expand All @@ -39,13 +44,17 @@ import kotlinx.serialization.json.Json
* @param datastore The underlying [DataStore<T>] instance.
* @param defaultValue The default value for the proto message.
* @param ownedScope The scope owned by this wrapper when it creates the underlying [DataStore].
* @param serializer The serializer used by byte-array restore for factory-created datastores.
* @param path The resolved datastore file path used by byte-array backup for factory-created datastores.
*/
public class GenericProtoDatastore<T> @InternalGenericDatastoreApi constructor(
internal val datastore: DataStore<T>,
private val defaultValue: T,
private val key: String = "proto_datastore",
private val defaultJson: Json = PreferenceDefaults.defaultJson,
private val ownedScope: CoroutineScope? = null,
private val serializer: OkioSerializer<T>? = null,
private val path: Path? = null,
) : ProtoDatastore<T> {

private val cachedData: ProtoPreference<T> by lazy {
Expand All @@ -64,6 +73,22 @@ public class GenericProtoDatastore<T> @InternalGenericDatastoreApi constructor(
}
}

override suspend fun exportAsByteArray(): ByteArray {
val resolvedPath = path
?: throw UnsupportedOperationException("Byte-array backup requires a resolved datastore path.")
datastore.data.first()
return ProtoBackupCreator(resolvedPath).exportAsByteArray()
}
Comment thread
ArthurKun21 marked this conversation as resolved.

override suspend fun importFromByteArray(data: ByteArray) {
val resolvedSerializer = serializer
?: throw UnsupportedOperationException("Byte-array restore requires an OkioSerializer.")
ProtoBackupRestorer(
datastore = datastore,
serializer = resolvedSerializer,
).importFromByteArray(data)
}

override fun <F> field(
defaultValue: F,
getter: (T) -> F,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,30 @@ import kotlinx.serialization.json.Json
public interface ProtoDatastore<T> : AutoCloseable {
override fun close() {}

/**
* Exports the raw proto datastore file contents as bytes.
*
* Factory-created datastores support this API. Implementations that do not own a resolvable
* datastore file may throw [UnsupportedOperationException].
*
* @return The current datastore file bytes, or an empty [ByteArray] when the file does not exist.
*/
public suspend fun exportAsByteArray(): ByteArray {
throw UnsupportedOperationException("Byte-array backup is not supported by this ProtoDatastore.")
}

/**
* Imports raw proto bytes and replaces the whole proto message.
*
* Factory-created datastores support this API by decoding [data] with their [androidx.datastore.core.okio.OkioSerializer].
* Implementations that cannot decode raw proto bytes may throw [UnsupportedOperationException].
*
* @param data The serialized proto bytes to import.
*/
public suspend fun importFromByteArray(data: ByteArray) {
throw UnsupportedOperationException("Byte-array restore is not supported by this ProtoDatastore.")
}

/**
* Returns the proto message wrapped as a [DelegatedPreference] instance.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.github.arthurkun.generic.datastore.proto.backup

import io.github.arthurkun.generic.datastore.core.systemFileSystem
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.withContext
import okio.Path

internal class ProtoBackupCreator(
private val path: Path,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) {

suspend fun exportAsByteArray(): ByteArray = withContext(ioDispatcher) {
if (!systemFileSystem.exists(path)) {
return@withContext ByteArray(0)
}

systemFileSystem.read(path) {
readByteArray()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.github.arthurkun.generic.datastore.proto.backup

import androidx.datastore.core.DataStore
import androidx.datastore.core.okio.OkioSerializer
import okio.Buffer

internal class ProtoBackupRestorer<T>(
private val datastore: DataStore<T>,
private val serializer: OkioSerializer<T>,
) {

suspend fun importFromByteArray(data: ByteArray) {
val imported = serializer.readFrom(Buffer().write(data))
datastore.updateData { imported }
}
}
Loading
Loading