-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add initial crossplatform backup serialization [WPB-10575] #3121
Changes from all commits
0690300
b824fb5
3a1dc1d
bc8ddeb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
/* | ||
* Wire | ||
* Copyright (C) 2024 Wire Swiss GmbH | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with this program. If not, see http://www.gnu.org/licenses/. | ||
*/ | ||
package com.wire.backup | ||
|
||
import kotlin.js.JsExport | ||
|
||
@JsExport | ||
object MPBackup { | ||
const val ZIP_ENTRY_DATA = "data.wmbu" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/* | ||
* Wire | ||
* Copyright (C) 2024 Wire Swiss GmbH | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with this program. If not, see http://www.gnu.org/licenses/. | ||
*/ | ||
@file:OptIn(ExperimentalObjCRefinement::class, ExperimentalObjCName::class) | ||
|
||
package com.wire.backup.data | ||
|
||
import kotlinx.serialization.SerialName | ||
import kotlinx.serialization.Serializable | ||
import kotlin.experimental.ExperimentalObjCName | ||
import kotlin.experimental.ExperimentalObjCRefinement | ||
import kotlin.js.JsExport | ||
import kotlin.native.ObjCName | ||
import kotlin.native.ShouldRefineInSwift | ||
|
||
@JsExport | ||
class BackupData( | ||
val metadata: BackupMetadata, | ||
@ShouldRefineInSwift | ||
val users: Array<BackupUser>, | ||
@ShouldRefineInSwift | ||
val conversations: Array<BackupConversation>, | ||
@ShouldRefineInSwift | ||
val messages: Array<BackupMessage> | ||
) { | ||
@ObjCName("users") | ||
val userList: List<BackupUser> get() = users.toList() | ||
|
||
@ObjCName("conversations") | ||
val conversationList: List<BackupConversation> get() = conversations.toList() | ||
|
||
@ObjCName("messages") | ||
val messageList: List<BackupMessage> get() = messages.toList() | ||
} | ||
|
||
@JsExport | ||
@Serializable | ||
data class BackupQualifiedId( | ||
@SerialName("id") | ||
val id: String, | ||
@SerialName("domain") | ||
val domain: String, | ||
) { | ||
override fun toString() = "$id@$domain" | ||
|
||
companion object { | ||
private const val QUALIFIED_ID_COMPONENT_COUNT = 2 | ||
|
||
fun fromEncodedString(id: String): BackupQualifiedId? { | ||
val components = id.split("@") | ||
if (components.size != QUALIFIED_ID_COMPONENT_COUNT) return null | ||
return BackupQualifiedId(components[0], components[1]) | ||
} | ||
} | ||
} | ||
|
||
@JsExport | ||
data class BackupUser( | ||
val id: BackupQualifiedId, | ||
val name: String, | ||
val handle: String, | ||
) | ||
|
||
@JsExport | ||
data class BackupConversation( | ||
val id: BackupQualifiedId, | ||
val name: String, | ||
) | ||
|
||
@JsExport | ||
data class BackupMessage( | ||
val id: String, | ||
val conversationId: BackupQualifiedId, | ||
val senderUserId: BackupQualifiedId, | ||
val senderClientId: String, | ||
val creationDate: BackupDateTime, | ||
val content: BackupMessageContent | ||
) | ||
|
||
expect class BackupDateTime | ||
|
||
expect fun BackupDateTime(timestampMillis: Long): BackupDateTime | ||
expect fun BackupDateTime.toLongMilliseconds(): Long | ||
Comment on lines
+94
to
+97
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is because JS doesn't have 64-bit Integers (Long). |
||
|
||
@JsExport | ||
sealed class BackupMessageContent { | ||
data class Text(val text: String) : BackupMessageContent() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Text messages also contain link previews, mentions, and are possibly a reply to another message. Are we planning to include these in the first version? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can expand later, like what's done in: |
||
|
||
// TODO: Not _yet_ implemented | ||
data class Asset(val todo: String) : BackupMessageContent() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
/* | ||
* Wire | ||
* Copyright (C) 2024 Wire Swiss GmbH | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with this program. If not, see http://www.gnu.org/licenses/. | ||
*/ | ||
|
||
package com.wire.backup.data | ||
|
||
import kotlin.js.JsExport | ||
|
||
@JsExport | ||
data class BackupMetadata( | ||
val version: String, | ||
val userId: BackupQualifiedId, | ||
val creationTime: BackupDateTime, | ||
val clientId: String? | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
/* | ||
* Wire | ||
* Copyright (C) 2024 Wire Swiss GmbH | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with this program. If not, see http://www.gnu.org/licenses/. | ||
*/ | ||
package com.wire.backup.data | ||
|
||
import com.wire.kalium.protobuf.backup.ExportedQualifiedId | ||
|
||
internal fun BackupQualifiedId.toProtoModel() = ExportedQualifiedId(id, domain) | ||
internal fun ExportedQualifiedId.toModel() = BackupQualifiedId(value, domain) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
/* | ||
* Wire | ||
* Copyright (C) 2024 Wire Swiss GmbH | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with this program. If not, see http://www.gnu.org/licenses/. | ||
*/ | ||
package com.wire.backup.dump | ||
|
||
import com.wire.backup.data.BackupConversation | ||
import com.wire.backup.data.BackupMessage | ||
import com.wire.backup.data.BackupMessageContent | ||
import com.wire.backup.data.BackupQualifiedId | ||
import com.wire.backup.data.BackupUser | ||
import com.wire.backup.data.toLongMilliseconds | ||
import com.wire.backup.data.toProtoModel | ||
import com.wire.kalium.protobuf.backup.BackupData | ||
import com.wire.kalium.protobuf.backup.BackupInfo | ||
import com.wire.kalium.protobuf.backup.ExportUser | ||
import com.wire.kalium.protobuf.backup.ExportedConversation | ||
import com.wire.kalium.protobuf.backup.ExportedMessage | ||
import com.wire.kalium.protobuf.backup.ExportedText | ||
import kotlinx.datetime.Clock | ||
import pbandk.encodeToByteArray | ||
import kotlin.experimental.ExperimentalObjCName | ||
import kotlin.experimental.ExperimentalObjCRefinement | ||
import kotlin.js.JsExport | ||
import kotlin.native.ObjCName | ||
import kotlin.native.ShouldRefineInSwift | ||
|
||
/** | ||
* Entity able to serialize [BackupData] entities, like [BackupMessage], [BackupConversation], [BackupUser] | ||
* into a cross-platform [BackupData] format. | ||
*/ | ||
@OptIn(ExperimentalObjCName::class, ExperimentalObjCRefinement::class) | ||
@JsExport | ||
abstract class CommonMPBackupExporter( | ||
private val selfUserId: BackupQualifiedId | ||
) { | ||
private val allUsers = mutableListOf<BackupUser>() | ||
private val allConversations = mutableListOf<BackupConversation>() | ||
private val allMessages = mutableListOf<BackupMessage>() | ||
|
||
// TODO: Replace `ObjCName` with `JsName` in the future and flip it around. | ||
// Unfortunately the IDE doesn't understand this right now and | ||
// keeps complaining if making the other way around | ||
@ObjCName("add") | ||
fun addUser(user: BackupUser) { | ||
allUsers.add(user) | ||
} | ||
|
||
@ObjCName("add") | ||
fun addConversation(conversation: BackupConversation) { | ||
allConversations.add(conversation) | ||
} | ||
|
||
@ObjCName("add") | ||
fun addMessage(message: BackupMessage) { | ||
allMessages.add(message) | ||
} | ||
Comment on lines
+57
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was planning on having Unfortunately it doesn't work on JS, as it is not a typed language... So at least when calling from Swift it will look like this: backupExporter.add(user: BackupUser(...))
backupExporter.add(conversation: BackupConversation(...))
backupExporter.add(message: BackupMesage(...)) |
||
|
||
@OptIn(ExperimentalStdlibApi::class) | ||
@ShouldRefineInSwift // Hidden in Swift | ||
fun serialize(): ByteArray { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this the method we call to produce the backup? The comment suggests it's hidden but it the POC it is visible but returns a I'm wondering if it would be possible for the client to pass a URL to which the library could write the backup file to. This could allow for more memory efficienct handling of the backup data. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed :) JS will only have this bytearray alternative. Android and iOS will be able to pass paths that will stream and take paginated chunks of data in, without having to load the whole file into memory This PR only handles initial serialization. There are more PRs incoming with:
|
||
val backupData = BackupData( | ||
BackupInfo( | ||
platform = "Common", | ||
version = "1.0", | ||
userId = selfUserId.toProtoModel(), | ||
creationTime = Clock.System.now().toEpochMilliseconds(), | ||
clientId = "lol" | ||
), | ||
allConversations.map { ExportedConversation(it.id.toProtoModel(), it.name) }, | ||
allMessages.map { | ||
ExportedMessage( | ||
id = it.id, | ||
timeIso = it.creationDate.toLongMilliseconds(), | ||
senderUserId = it.senderUserId.toProtoModel(), | ||
senderClientId = it.senderClientId, | ||
conversationId = it.conversationId.toProtoModel(), | ||
content = when (val content = it.content) { | ||
is BackupMessageContent.Asset -> | ||
ExportedMessage.Content.Text(ExportedText("FAKE ASSET")) // TODO: Support assets | ||
is BackupMessageContent.Text -> | ||
ExportedMessage.Content.Text(ExportedText(content.text)) | ||
} | ||
) | ||
}, | ||
allUsers.map { | ||
ExportUser( | ||
id = it.id.toProtoModel(), | ||
name = it.name, | ||
handle = it.handle | ||
) | ||
}, | ||
) | ||
return backupData.encodeToByteArray().also { | ||
println("XPlatform Backup POC. Exported data bytes: ${it.toHexString()}") | ||
} | ||
} | ||
} | ||
|
||
expect class MPBackupExporter : CommonMPBackupExporter |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/* | ||
* Wire | ||
* Copyright (C) 2024 Wire Swiss GmbH | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with this program. If not, see http://www.gnu.org/licenses/. | ||
*/ | ||
package com.wire.backup.ingest | ||
|
||
import com.wire.backup.data.BackupData | ||
import kotlin.js.JsExport | ||
|
||
@JsExport | ||
sealed class BackupImportResult { | ||
data object ParsingFailure : BackupImportResult() | ||
data class Success(val backupData: BackupData) : BackupImportResult() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JsExport
is needed so this class can be called from JS later.