diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7a24a679..eaeb9920 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,5 +1,7 @@
default:
image: ${CI_REGISTRY}/android/shared/docker-android:v1.1.1
+ tags:
+ - shared-small
variables:
# Use fastzip to improve cache times
@@ -54,7 +56,7 @@ prepare-build:
expire_in: 1 week
cache: []
tags:
- - large
+ - shared-large
script:
- export ARCHIVES_BASE_NAME=$(./gradlew -Dorg.gradle.jvmargs=-Xmx1024m -q getArchivesName | grep "\[ARCHIVES_NAME\]" | sed 's/\[ARCHIVES_NAME\]//')
- export ARCHIVES_VERSION=$(./gradlew -Dorg.gradle.jvmargs=-Xmx1024m -q getArchivesVersion | grep "\[ARCHIVES_VERSION\]" | sed 's/\[ARCHIVES_VERSION\]//')
@@ -65,6 +67,8 @@ prepare-build:
prepare-gradle-build-scan:
stage: prepare
+ tags:
+ - shared-large
script:
- echo "BUILD_SCAN_PUBLISH=true" >> build_scan.env
artifacts:
@@ -77,7 +81,7 @@ prepare-gradle-build-scan:
detekt analysis:
stage: analyze
tags:
- - large
+ - shared-large
script:
- ./gradlew multiModuleDetekt --configuration-cache-problems=warn
allow_failure: true
@@ -179,6 +183,8 @@ dev debug unit test:
upload to firebase:
stage: startReview
+ tags:
+ - shared-medium
variables:
PRODUCT_FLAVOR: "dynamic"
script:
@@ -202,7 +208,7 @@ upload to firebase:
- job: "build dynamic debug"
stage: test
tags:
- - medium
+ - shared-medium
variables:
RESULTS_DIR: "$FIREBASE_RESULT_ROOT/$CI_JOB_NAME"
PRODUCT_FLAVOR: "dynamic"
@@ -357,6 +363,8 @@ startReview:
needs:
- job: "prepare-build"
- job: "build dev debug"
+ tags:
+ - shared-medium
variables:
PRODUCT_FLAVOR: "dev"
before_script:
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 89ff52c5..936d37d3 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -51,6 +51,7 @@ driveModule(
implementation(project(":app-lock"))
implementation(project(":app-ui-settings"))
implementation(project(":drive"))
+ implementation(project(":verifier"))
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.compose.foundationLayout)
diff --git a/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt b/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt
index 7c3218a7..e7c25eed 100644
--- a/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt
@@ -30,8 +30,6 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import me.proton.android.drive.BuildConfig
-import me.proton.android.drive.lock.data.usecase.BuildAppKeyImpl
-import me.proton.android.drive.lock.domain.usecase.BuildAppKey
import me.proton.android.drive.log.DriveLogger
import me.proton.android.drive.notification.AppNotificationBuilderProvider
import me.proton.android.drive.notification.AppNotificationEventHandler
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/upload/UploadFlowTest.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/upload/UploadFlowTest.kt
index d676b4c1..9303b2e8 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/upload/UploadFlowTest.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/upload/UploadFlowTest.kt
@@ -118,8 +118,8 @@ class UploadFlowTest : BaseTest() {
}
@Test
- fun upload4MBFile() {
- val file = externalFilesRule.createFile("4MB.txt", 4 * 1024 * 1024)
+ fun upload6MBFile() {
+ val file = externalFilesRule.createFile("6MB.txt", 6 * 1024 * 1024)
Intents.intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWithFunction {
Instrumentation.ActivityResult(Activity.RESULT_OK, Intent().setData(Uri.fromFile(file)))
@@ -135,7 +135,7 @@ class UploadFlowTest : BaseTest() {
assertStageUploading()
assertStageUploadedProgress(0)
assertStageUploadedProgress(100)
- itemIsDisplayed("4MB.txt")
+ itemIsDisplayed("6MB.txt")
}
}
diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt
index c27b8580..9f454d0d 100644
--- a/buildSrc/src/main/kotlin/Config.kt
+++ b/buildSrc/src/main/kotlin/Config.kt
@@ -22,7 +22,7 @@ object Config {
const val minSdk = 23
const val targetSdk = 33
const val testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- const val versionName = "1.2.3"
+ const val versionName = "1.3.1"
const val archivesBaseName = "ProtonDrive-$versionName"
val resourceConfigurations = listOf("en")
}
diff --git a/drive/base/data-test/build.gradle.kts b/drive/base/data-test/build.gradle.kts
index ab13ea8f..1730d0f5 100644
--- a/drive/base/data-test/build.gradle.kts
+++ b/drive/base/data-test/build.gradle.kts
@@ -22,6 +22,7 @@ plugins {
driveModule(
hilt = true,
) {
+ api(project(":drive:base:domain"))
api(libs.core.domain)
}
diff --git a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/Dto.kt b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/Dto.kt
index a6a00d1a..5e15ff05 100644
--- a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/Dto.kt
+++ b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/Dto.kt
@@ -127,6 +127,8 @@ object Dto {
const val URL_PASSWORD_SALT = "UrlPasswordSalt"
const val URLS_EXPIRED = "UrlsExpired"
const val USED_SPACE = "UsedSpace"
+ const val VERIFICATION_CODE = "VerificationCode"
+ const val VERIFIER = "Verifier"
const val VOLUME = "Volume"
const val VOLUMES = "Volumes"
const val VOLUME_ID = "VolumeID"
diff --git a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/ProtonApiCode.kt b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/ProtonApiCode.kt
index e5a703c6..e9ee4628 100644
--- a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/ProtonApiCode.kt
+++ b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/ProtonApiCode.kt
@@ -26,6 +26,7 @@ object ProtonApiCode {
const val ALREADY_EXISTS = 2500
const val NOT_EXISTS = 2501
const val INSUFFICIENT_QUOTA = 200001
+ const val ENCRYPTION_VERIFICATION_FAILED = 200501
val Long.isSuccessful: Boolean get() = this == SUCCESS.toLong()
}
diff --git a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/Column.kt b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/Column.kt
index d3afdac2..8bc569ce 100644
--- a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/Column.kt
+++ b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/Column.kt
@@ -109,6 +109,7 @@ object Column {
const val URL_PASSWORD_SALT = "url_password_salt"
const val USED_SPACE = "used_space"
const val USER_ID = "user_id"
+ const val VERIFIER_TOKEN = "verifier_token"
const val VOLUME_ID = "volume_id"
const val WORK_ID = "work_id"
const val WORKER_ID = "worker_id"
diff --git a/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/api/entity/UploadBlockDto.kt b/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/api/entity/UploadBlockDto.kt
index a7172a41..61a314e6 100644
--- a/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/api/entity/UploadBlockDto.kt
+++ b/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/api/entity/UploadBlockDto.kt
@@ -23,6 +23,7 @@ import me.proton.core.drive.base.data.api.Dto.ENC_SIGNATURE
import me.proton.core.drive.base.data.api.Dto.HASH
import me.proton.core.drive.base.data.api.Dto.INDEX
import me.proton.core.drive.base.data.api.Dto.SIZE
+import me.proton.core.drive.base.data.api.Dto.VERIFIER
@Serializable
data class UploadBlockDto(
@@ -33,5 +34,7 @@ data class UploadBlockDto(
@SerialName(ENC_SIGNATURE)
val encSignature: String,
@SerialName(HASH)
- val hash: String
+ val hash: String,
+ @SerialName(VERIFIER)
+ val verifier: VerifierDto? = null,
)
diff --git a/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/api/entity/VerifierDto.kt b/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/api/entity/VerifierDto.kt
new file mode 100644
index 00000000..b24544bb
--- /dev/null
+++ b/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/api/entity/VerifierDto.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core 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.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+package me.proton.core.drive.block.data.api.entity
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import me.proton.core.drive.base.data.api.Dto.TOKEN
+
+@Serializable
+data class VerifierDto(
+ @SerialName(TOKEN)
+ val token: String,
+)
diff --git a/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/extension/UploadBlock.kt b/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/extension/UploadBlock.kt
index a3de861c..c14934b2 100644
--- a/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/extension/UploadBlock.kt
+++ b/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/extension/UploadBlock.kt
@@ -18,6 +18,7 @@
package me.proton.core.drive.block.data.extension
import me.proton.core.drive.block.data.api.entity.UploadBlockDto
+import me.proton.core.drive.block.data.api.entity.VerifierDto
import me.proton.core.drive.linkupload.domain.entity.UploadBlock
fun UploadBlock.toUploadBlockDto() =
@@ -26,4 +27,5 @@ fun UploadBlock.toUploadBlockDto() =
size = size.value,
hash = hashSha256,
encSignature = encSignature,
+ verifier = verifierToken?.let { token -> VerifierDto(token) },
)
diff --git a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/base/UseSessionKey.kt b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/base/UseSessionKey.kt
index 43f0078a..af3cd1c5 100644
--- a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/base/UseSessionKey.kt
+++ b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/base/UseSessionKey.kt
@@ -35,7 +35,7 @@ class UseSessionKey @Inject constructor(
) {
suspend operator fun invoke(
contentKey: ContentKey,
- checkSignature: Boolean = false,
+ checkSignature: Boolean,
coroutineContext: CoroutineContext = CryptoScope.EncryptAndDecryptWithIO.coroutineContext,
block: suspend (SessionKey) -> T
) =
@@ -54,4 +54,17 @@ class UseSessionKey @Inject constructor(
}
block(sessionKey)
}
+
+ suspend operator fun invoke(
+ contentKey: ContentKey,
+ coroutineContext: CoroutineContext = CryptoScope.EncryptAndDecryptWithIO.coroutineContext,
+ block: suspend (SessionKey) -> T
+ ) =
+ useSessionKey(
+ decryptKey = contentKey.decryptKey.keyHolder,
+ encryptedKeyPacket = contentKey.encryptedKeyPacket,
+ coroutineContext = coroutineContext,
+ ) { sessionKey ->
+ block(sessionKey)
+ }
}
diff --git a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/file/DecryptFiles.kt b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/file/DecryptFiles.kt
index 93d326fb..65a50d47 100644
--- a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/file/DecryptFiles.kt
+++ b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/file/DecryptFiles.kt
@@ -30,6 +30,24 @@ class DecryptFiles @Inject constructor(
private val useSessionKey: UseSessionKey,
private val decryptFile: DecryptFile
) {
+ suspend operator fun invoke(
+ contentKey: ContentKey,
+ checkSignature: Boolean,
+ input: List,
+ output: List,
+ ): Result> = coRunCatching {
+ useSessionKey(contentKey = contentKey, checkSignature = checkSignature) { sessionKey ->
+ input.mapIndexed { index, file ->
+ DecryptedFile(
+ file = decryptFile(sessionKey, file, output[index]).getOrThrow(),
+ status = VerificationStatus.Unknown,
+ filename = "",
+ lastModifiedEpochSeconds = -1,
+ )
+ }
+ }.getOrThrow()
+ }
+
suspend operator fun invoke(
contentKey: ContentKey,
input: List,
diff --git a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/file/EncryptFiles.kt b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/file/EncryptFiles.kt
index edb4cf8a..0e9ffde5 100644
--- a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/file/EncryptFiles.kt
+++ b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/file/EncryptFiles.kt
@@ -32,11 +32,12 @@ class EncryptFiles @Inject constructor(
) {
suspend operator fun invoke(
contentKey: ContentKey,
+ checkSignature: Boolean = false,
input: List,
output: List,
coroutineContext: CoroutineContext = CryptoScope.EncryptAndDecryptWithIO.coroutineContext,
): Result> = coRunCatching {
- useSessionKey(contentKey = contentKey) { sessionKey ->
+ useSessionKey(contentKey = contentKey, checkSignature = checkSignature) { sessionKey ->
input.mapIndexed { index, file ->
encryptFile(sessionKey, file, output[index], coroutineContext).getOrThrow()
}
diff --git a/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/27.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/27.json
new file mode 100644
index 00000000..dd138ce1
--- /dev/null
+++ b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/27.json
@@ -0,0 +1,4460 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 27,
+ "identityHash": "443da6c93db22831596a4ae60bb0a948",
+ "entities": [
+ {
+ "tableName": "AccountEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `username` TEXT NOT NULL, `email` TEXT, `state` TEXT NOT NULL, `sessionId` TEXT, `sessionState` TEXT, PRIMARY KEY(`userId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "username",
+ "columnName": "username",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sessionId",
+ "columnName": "sessionId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "sessionState",
+ "columnName": "sessionState",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_AccountEntity_sessionId",
+ "unique": false,
+ "columnNames": [
+ "sessionId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)"
+ },
+ {
+ "name": "index_AccountEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "SessionEntity",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "sessionId"
+ ],
+ "referencedColumns": [
+ "sessionId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "AccountMetadataEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `product` TEXT NOT NULL, `primaryAtUtc` INTEGER NOT NULL, `migrations` TEXT, PRIMARY KEY(`userId`, `product`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "product",
+ "columnName": "product",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "primaryAtUtc",
+ "columnName": "primaryAtUtc",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "migrations",
+ "columnName": "migrations",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId",
+ "product"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_AccountMetadataEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ },
+ {
+ "name": "index_AccountMetadataEntity_product",
+ "unique": false,
+ "columnNames": [
+ "product"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_product` ON `${TABLE_NAME}` (`product`)"
+ },
+ {
+ "name": "index_AccountMetadataEntity_primaryAtUtc",
+ "unique": false,
+ "columnNames": [
+ "primaryAtUtc"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_primaryAtUtc` ON `${TABLE_NAME}` (`primaryAtUtc`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "SessionEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT, `sessionId` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT NOT NULL, `scopes` TEXT NOT NULL, `product` TEXT NOT NULL, PRIMARY KEY(`sessionId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "sessionId",
+ "columnName": "sessionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accessToken",
+ "columnName": "accessToken",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "refreshToken",
+ "columnName": "refreshToken",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scopes",
+ "columnName": "scopes",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "product",
+ "columnName": "product",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "sessionId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_SessionEntity_sessionId",
+ "unique": false,
+ "columnNames": [
+ "sessionId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)"
+ },
+ {
+ "name": "index_SessionEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "SessionDetailsEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sessionId` TEXT NOT NULL, `initialEventId` TEXT NOT NULL, `requiredAccountType` TEXT NOT NULL, `secondFactorEnabled` INTEGER NOT NULL, `twoPassModeEnabled` INTEGER NOT NULL, `password` TEXT, PRIMARY KEY(`sessionId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "sessionId",
+ "columnName": "sessionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "initialEventId",
+ "columnName": "initialEventId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "requiredAccountType",
+ "columnName": "requiredAccountType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "secondFactorEnabled",
+ "columnName": "secondFactorEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "twoPassModeEnabled",
+ "columnName": "twoPassModeEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "password",
+ "columnName": "password",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "sessionId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_SessionDetailsEntity_sessionId",
+ "unique": false,
+ "columnNames": [
+ "sessionId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionDetailsEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "SessionEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "sessionId"
+ ],
+ "referencedColumns": [
+ "sessionId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UserEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `email` TEXT, `name` TEXT, `displayName` TEXT, `currency` TEXT NOT NULL, `credit` INTEGER NOT NULL, `usedSpace` INTEGER NOT NULL, `maxSpace` INTEGER NOT NULL, `maxUpload` INTEGER NOT NULL, `role` INTEGER, `private` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `services` INTEGER NOT NULL, `delinquent` INTEGER, `passphrase` BLOB, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "credit",
+ "columnName": "credit",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "usedSpace",
+ "columnName": "usedSpace",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "maxSpace",
+ "columnName": "maxSpace",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "maxUpload",
+ "columnName": "maxUpload",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "role",
+ "columnName": "role",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isPrivate",
+ "columnName": "private",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subscribed",
+ "columnName": "subscribed",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "services",
+ "columnName": "services",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "delinquent",
+ "columnName": "delinquent",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "passphrase",
+ "columnName": "passphrase",
+ "affinity": "BLOB",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_UserEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UserEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UserKeyEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `fingerprint` TEXT, `activation` TEXT, `active` INTEGER, PRIMARY KEY(`keyId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keyId",
+ "columnName": "keyId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "privateKey",
+ "columnName": "privateKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isPrimary",
+ "columnName": "isPrimary",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isUnlockable",
+ "columnName": "isUnlockable",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "fingerprint",
+ "columnName": "fingerprint",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "activation",
+ "columnName": "activation",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "active",
+ "columnName": "active",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "keyId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_UserKeyEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ },
+ {
+ "name": "index_UserKeyEntity_keyId",
+ "unique": false,
+ "columnNames": [
+ "keyId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "AddressEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `addressId` TEXT NOT NULL, `email` TEXT NOT NULL, `displayName` TEXT, `signature` TEXT, `domainId` TEXT, `canSend` INTEGER NOT NULL, `canReceive` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `type` INTEGER, `order` INTEGER NOT NULL, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, `signedKeyList_minEpochId` INTEGER, `signedKeyList_maxEpochId` INTEGER, `signedKeyList_expectedMinEpochId` INTEGER, PRIMARY KEY(`addressId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addressId",
+ "columnName": "addressId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signature",
+ "columnName": "signature",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "domainId",
+ "columnName": "domainId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "canSend",
+ "columnName": "canSend",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "canReceive",
+ "columnName": "canReceive",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enabled",
+ "columnName": "enabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "signedKeyList.data",
+ "columnName": "signedKeyList_data",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signedKeyList.signature",
+ "columnName": "signedKeyList_signature",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signedKeyList.minEpochId",
+ "columnName": "signedKeyList_minEpochId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signedKeyList.maxEpochId",
+ "columnName": "signedKeyList_maxEpochId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signedKeyList.expectedMinEpochId",
+ "columnName": "signedKeyList_expectedMinEpochId",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "addressId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_AddressEntity_addressId",
+ "unique": false,
+ "columnNames": [
+ "addressId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_addressId` ON `${TABLE_NAME}` (`addressId`)"
+ },
+ {
+ "name": "index_AddressEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "AddressKeyEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`addressId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `passphrase` BLOB, `token` TEXT, `signature` TEXT, `fingerprint` TEXT, `fingerprints` TEXT, `activation` TEXT, `active` INTEGER NOT NULL, PRIMARY KEY(`keyId`), FOREIGN KEY(`addressId`) REFERENCES `AddressEntity`(`addressId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "addressId",
+ "columnName": "addressId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keyId",
+ "columnName": "keyId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "privateKey",
+ "columnName": "privateKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isPrimary",
+ "columnName": "isPrimary",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isUnlockable",
+ "columnName": "isUnlockable",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "passphrase",
+ "columnName": "passphrase",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signature",
+ "columnName": "signature",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "fingerprint",
+ "columnName": "fingerprint",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "fingerprints",
+ "columnName": "fingerprints",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "activation",
+ "columnName": "activation",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "active",
+ "columnName": "active",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "keyId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_AddressKeyEntity_addressId",
+ "unique": false,
+ "columnNames": [
+ "addressId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_addressId` ON `${TABLE_NAME}` (`addressId`)"
+ },
+ {
+ "name": "index_AddressKeyEntity_keyId",
+ "unique": false,
+ "columnNames": [
+ "keyId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AddressEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "addressId"
+ ],
+ "referencedColumns": [
+ "addressId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "KeySaltEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `keySalt` TEXT, PRIMARY KEY(`userId`, `keyId`))",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keyId",
+ "columnName": "keyId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keySalt",
+ "columnName": "keySalt",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId",
+ "keyId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_KeySaltEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ },
+ {
+ "name": "index_KeySaltEntity_keyId",
+ "unique": false,
+ "columnNames": [
+ "keyId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_keyId` ON `${TABLE_NAME}` (`keyId`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "PublicAddressEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `recipientType` INTEGER NOT NULL, `mimeType` TEXT, `ignoreKT` INTEGER, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, `signedKeyList_minEpochId` INTEGER, `signedKeyList_maxEpochId` INTEGER, `signedKeyList_expectedMinEpochId` INTEGER, PRIMARY KEY(`email`))",
+ "fields": [
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "recipientType",
+ "columnName": "recipientType",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mimeType",
+ "columnName": "mimeType",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "ignoreKT",
+ "columnName": "ignoreKT",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signedKeyListEntity.data",
+ "columnName": "signedKeyList_data",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signedKeyListEntity.signature",
+ "columnName": "signedKeyList_signature",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signedKeyListEntity.minEpochId",
+ "columnName": "signedKeyList_minEpochId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signedKeyListEntity.maxEpochId",
+ "columnName": "signedKeyList_maxEpochId",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signedKeyListEntity.expectedMinEpochId",
+ "columnName": "signedKeyList_expectedMinEpochId",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "email"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_PublicAddressEntity_email",
+ "unique": false,
+ "columnNames": [
+ "email"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressEntity_email` ON `${TABLE_NAME}` (`email`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "PublicAddressKeyEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `flags` INTEGER NOT NULL, `publicKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, PRIMARY KEY(`email`, `publicKey`), FOREIGN KEY(`email`) REFERENCES `PublicAddressEntity`(`email`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publicKey",
+ "columnName": "publicKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isPrimary",
+ "columnName": "isPrimary",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "email",
+ "publicKey"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_PublicAddressKeyEntity_email",
+ "unique": false,
+ "columnNames": [
+ "email"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressKeyEntity_email` ON `${TABLE_NAME}` (`email`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "PublicAddressEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "email"
+ ],
+ "referencedColumns": [
+ "email"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "HumanVerificationEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`clientId` TEXT NOT NULL, `clientIdType` TEXT NOT NULL, `verificationMethods` TEXT NOT NULL, `verificationToken` TEXT, `state` TEXT NOT NULL, `humanHeaderTokenType` TEXT, `humanHeaderTokenCode` TEXT, PRIMARY KEY(`clientId`))",
+ "fields": [
+ {
+ "fieldPath": "clientId",
+ "columnName": "clientId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "clientIdType",
+ "columnName": "clientIdType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "verificationMethods",
+ "columnName": "verificationMethods",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "verificationToken",
+ "columnName": "verificationToken",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "humanHeaderTokenType",
+ "columnName": "humanHeaderTokenType",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "humanHeaderTokenCode",
+ "columnName": "humanHeaderTokenCode",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "clientId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "UserSettingsEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `news` INTEGER, `locale` TEXT, `logAuth` INTEGER, `invoiceText` TEXT, `density` INTEGER, `theme` TEXT, `themeType` INTEGER, `weekStart` INTEGER, `dateFormat` INTEGER, `timeFormat` INTEGER, `welcome` INTEGER, `earlyAccess` INTEGER, `email_value` TEXT, `email_status` INTEGER, `email_notify` INTEGER, `email_reset` INTEGER, `phone_value` TEXT, `phone_status` INTEGER, `phone_notify` INTEGER, `phone_reset` INTEGER, `password_mode` INTEGER, `password_expirationTime` INTEGER, `twoFA_enabled` INTEGER, `twoFA_allowed` INTEGER, `twoFA_expirationTime` INTEGER, `flags_welcomed` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "news",
+ "columnName": "news",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "locale",
+ "columnName": "locale",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "logAuth",
+ "columnName": "logAuth",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "invoiceText",
+ "columnName": "invoiceText",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "density",
+ "columnName": "density",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "theme",
+ "columnName": "theme",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "themeType",
+ "columnName": "themeType",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "weekStart",
+ "columnName": "weekStart",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateFormat",
+ "columnName": "dateFormat",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "timeFormat",
+ "columnName": "timeFormat",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "welcome",
+ "columnName": "welcome",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "earlyAccess",
+ "columnName": "earlyAccess",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email.value",
+ "columnName": "email_value",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email.status",
+ "columnName": "email_status",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email.notify",
+ "columnName": "email_notify",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email.reset",
+ "columnName": "email_reset",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "phone.value",
+ "columnName": "phone_value",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "phone.status",
+ "columnName": "phone_status",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "phone.notify",
+ "columnName": "phone_notify",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "phone.reset",
+ "columnName": "phone_reset",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "password.mode",
+ "columnName": "password_mode",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "password.expirationTime",
+ "columnName": "password_expirationTime",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "twoFA.enabled",
+ "columnName": "twoFA_enabled",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "twoFA.allowed",
+ "columnName": "twoFA_allowed",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "twoFA.expirationTime",
+ "columnName": "twoFA_expirationTime",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "flags.welcomed",
+ "columnName": "flags_welcomed",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "OrganizationEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `name` TEXT NOT NULL, `displayName` TEXT, `planName` TEXT, `twoFactorGracePeriod` INTEGER, `theme` TEXT, `email` TEXT, `maxDomains` INTEGER, `maxAddresses` INTEGER, `maxSpace` INTEGER, `maxMembers` INTEGER, `maxVPN` INTEGER, `maxCalendars` INTEGER, `features` INTEGER, `flags` INTEGER, `usedDomains` INTEGER, `usedAddresses` INTEGER, `usedSpace` INTEGER, `assignedSpace` INTEGER, `usedMembers` INTEGER, `usedVPN` INTEGER, `usedCalendars` INTEGER, `hasKeys` INTEGER, `toMigrate` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "planName",
+ "columnName": "planName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "twoFactorGracePeriod",
+ "columnName": "twoFactorGracePeriod",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "theme",
+ "columnName": "theme",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxDomains",
+ "columnName": "maxDomains",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxAddresses",
+ "columnName": "maxAddresses",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxSpace",
+ "columnName": "maxSpace",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxMembers",
+ "columnName": "maxMembers",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxVPN",
+ "columnName": "maxVPN",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxCalendars",
+ "columnName": "maxCalendars",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "features",
+ "columnName": "features",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedDomains",
+ "columnName": "usedDomains",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedAddresses",
+ "columnName": "usedAddresses",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedSpace",
+ "columnName": "usedSpace",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "assignedSpace",
+ "columnName": "assignedSpace",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedMembers",
+ "columnName": "usedMembers",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedVPN",
+ "columnName": "usedVPN",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedCalendars",
+ "columnName": "usedCalendars",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "hasKeys",
+ "columnName": "hasKeys",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "toMigrate",
+ "columnName": "toMigrate",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "OrganizationKeysEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `privateKey` TEXT NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publicKey",
+ "columnName": "publicKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "privateKey",
+ "columnName": "privateKey",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "EventMetadataEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `config` TEXT NOT NULL, `eventId` TEXT, `nextEventId` TEXT, `refresh` TEXT, `more` INTEGER, `response` TEXT, `retry` INTEGER NOT NULL, `state` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER, PRIMARY KEY(`userId`, `config`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "config",
+ "columnName": "config",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "eventId",
+ "columnName": "eventId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "nextEventId",
+ "columnName": "nextEventId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "refresh",
+ "columnName": "refresh",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "more",
+ "columnName": "more",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "response",
+ "columnName": "response",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "retry",
+ "columnName": "retry",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "createdAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updatedAt",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId",
+ "config"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_EventMetadataEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ },
+ {
+ "name": "index_EventMetadataEntity_config",
+ "unique": false,
+ "columnNames": [
+ "config"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_config` ON `${TABLE_NAME}` (`config`)"
+ },
+ {
+ "name": "index_EventMetadataEntity_createdAt",
+ "unique": false,
+ "columnNames": [
+ "createdAt"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_createdAt` ON `${TABLE_NAME}` (`createdAt`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "FeatureFlagEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `featureId` TEXT NOT NULL, `scope` TEXT NOT NULL, `defaultValue` INTEGER NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`userId`, `featureId`))",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "featureId",
+ "columnName": "featureId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scope",
+ "columnName": "scope",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "defaultValue",
+ "columnName": "defaultValue",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId",
+ "featureId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_FeatureFlagEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_FeatureFlagEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ },
+ {
+ "name": "index_FeatureFlagEntity_featureId",
+ "unique": false,
+ "columnNames": [
+ "featureId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_FeatureFlagEntity_featureId` ON `${TABLE_NAME}` (`featureId`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "ChallengeFrameEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`challengeFrame` TEXT NOT NULL, `flow` TEXT NOT NULL, `focusTime` TEXT NOT NULL, `clicks` INTEGER NOT NULL, `copy` TEXT NOT NULL, `paste` TEXT NOT NULL, `keys` TEXT NOT NULL, PRIMARY KEY(`challengeFrame`))",
+ "fields": [
+ {
+ "fieldPath": "challengeFrame",
+ "columnName": "challengeFrame",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flow",
+ "columnName": "flow",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "focusTime",
+ "columnName": "focusTime",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "clicks",
+ "columnName": "clicks",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "copy",
+ "columnName": "copy",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "paste",
+ "columnName": "paste",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keys",
+ "columnName": "keys",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "challengeFrame"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "GooglePurchaseEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`googlePurchaseToken` TEXT NOT NULL, `paymentToken` TEXT NOT NULL, PRIMARY KEY(`googlePurchaseToken`))",
+ "fields": [
+ {
+ "fieldPath": "googlePurchaseToken",
+ "columnName": "googlePurchaseToken",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "paymentToken",
+ "columnName": "paymentToken",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "googlePurchaseToken"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_GooglePurchaseEntity_paymentToken",
+ "unique": true,
+ "columnNames": [
+ "paymentToken"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_GooglePurchaseEntity_paymentToken` ON `${TABLE_NAME}` (`paymentToken`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "ObservabilityEventEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `version` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `data` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "data",
+ "columnName": "data",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "AddressChangeEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `changeId` TEXT NOT NULL, `counterEncrypted` TEXT NOT NULL, `emailEncrypted` TEXT NOT NULL, `epochIdEncrypted` TEXT NOT NULL, `creationTimestampEncrypted` TEXT NOT NULL, `publicKeysEncrypted` TEXT NOT NULL, `isObsolete` TEXT NOT NULL, PRIMARY KEY(`userId`, `changeId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "changeId",
+ "columnName": "changeId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "counterEncrypted",
+ "columnName": "counterEncrypted",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "emailEncrypted",
+ "columnName": "emailEncrypted",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "epochIdEncrypted",
+ "columnName": "epochIdEncrypted",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creationTimestampEncrypted",
+ "columnName": "creationTimestampEncrypted",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publicKeysEncrypted",
+ "columnName": "publicKeysEncrypted",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isObsolete",
+ "columnName": "isObsolete",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId",
+ "changeId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "SelfAuditResultEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "VolumeEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `max_space` INTEGER, `used_space` INTEGER NOT NULL, `state` INTEGER NOT NULL, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creationTime",
+ "columnName": "creation_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "maxSpace",
+ "columnName": "max_space",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedSpace",
+ "columnName": "used_space",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_VolumeEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_VolumeEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_VolumeEntity_id",
+ "unique": false,
+ "columnNames": [
+ "id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_id` ON `${TABLE_NAME}` (`id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "ShareEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `address_id` TEXT, `flags` INTEGER NOT NULL, `link_id` TEXT NOT NULL, `locked` INTEGER NOT NULL, `key` TEXT NOT NULL, `passphrase` TEXT NOT NULL, `passphrase_signature` TEXT NOT NULL, `creation_time` INTEGER, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "volumeId",
+ "columnName": "volume_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addressId",
+ "columnName": "address_id",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isLocked",
+ "columnName": "locked",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "passphrase",
+ "columnName": "passphrase",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "passphraseSignature",
+ "columnName": "passphrase_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creationTime",
+ "columnName": "creation_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_ShareEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_ShareEntity_volume_id",
+ "unique": false,
+ "columnNames": [
+ "volume_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)"
+ },
+ {
+ "name": "index_ShareEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_ShareEntity_id",
+ "unique": true,
+ "columnNames": [
+ "id"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ShareEntity_id` ON `${TABLE_NAME}` (`id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "ShareUrlEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `flags` INTEGER NOT NULL, `name` TEXT, `token` TEXT NOT NULL, `creatior_email` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `creation_time` INTEGER NOT NULL, `expiration_time` INTEGER, `last_access_time` INTEGER, `max_accesses` INTEGER, `number_of_accesses` INTEGER NOT NULL, `url_password_salt` TEXT NOT NULL, `share_password_salt` TEXT NOT NULL, `srp_verifier` TEXT NOT NULL, `srp_modulus_id` TEXT NOT NULL, `password` TEXT NOT NULL, `share_passphrase_key_packet` TEXT NOT NULL, `public_url` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creatorEmail",
+ "columnName": "creatior_email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permissions",
+ "columnName": "permissions",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creationTime",
+ "columnName": "creation_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "expirationTime",
+ "columnName": "expiration_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lastAccessTime",
+ "columnName": "last_access_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxAccesses",
+ "columnName": "max_accesses",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "numberOfAccesses",
+ "columnName": "number_of_accesses",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "urlPasswordSalt",
+ "columnName": "url_password_salt",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sharePasswordSalt",
+ "columnName": "share_password_salt",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "srpVerifier",
+ "columnName": "srp_verifier",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "srpModulusId",
+ "columnName": "srp_modulus_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "encryptedUrlPassword",
+ "columnName": "password",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sharePassphraseKeyPacket",
+ "columnName": "share_passphrase_key_packet",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publicUrl",
+ "columnName": "public_url",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_ShareUrlEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_ShareUrlEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_ShareUrlEntity_user_id_share_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "ShareEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `parent_id` TEXT, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_signature_email` TEXT, `hash` TEXT NOT NULL, `state` INTEGER NOT NULL, `expiration_time` INTEGER, `size` INTEGER NOT NULL, `mime_type` TEXT NOT NULL, `attributes` INTEGER NOT NULL, `permissions` INTEGER NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `signature_address` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `last_modified` INTEGER NOT NULL, `trashed_time` INTEGER, `is_shared` INTEGER NOT NULL, `number_of_accesses` INTEGER NOT NULL, `share_url_expiration_time` INTEGER, `x_attr` TEXT, `share_url_share_id` TEXT DEFAULT NULL, `share_url_id` TEXT DEFAULT NULL, PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `parent_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentId",
+ "columnName": "parent_id",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nameSignatureEmail",
+ "columnName": "name_signature_email",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "hash",
+ "columnName": "hash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "expirationTime",
+ "columnName": "expiration_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mimeType",
+ "columnName": "mime_type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "attributes",
+ "columnName": "attributes",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permissions",
+ "columnName": "permissions",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodeKey",
+ "columnName": "node_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodePassphrase",
+ "columnName": "node_passphrase",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodePassphraseSignature",
+ "columnName": "node_passphrase_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "signatureAddress",
+ "columnName": "signature_address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creationTime",
+ "columnName": "creation_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastModified",
+ "columnName": "last_modified",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "trashedTime",
+ "columnName": "trashed_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "shared",
+ "columnName": "is_shared",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "numberOfAccesses",
+ "columnName": "number_of_accesses",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareUrlExpirationTime",
+ "columnName": "share_url_expiration_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "xAttr",
+ "columnName": "x_attr",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "shareUrlShareId",
+ "columnName": "share_url_share_id",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "NULL"
+ },
+ {
+ "fieldPath": "shareUrlId",
+ "columnName": "share_url_id",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "NULL"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_LinkEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_LinkEntity_parent_id",
+ "unique": false,
+ "columnNames": [
+ "parent_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)"
+ },
+ {
+ "name": "index_LinkEntity_id",
+ "unique": false,
+ "columnNames": [
+ "id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_id` ON `${TABLE_NAME}` (`id`)"
+ },
+ {
+ "name": "index_LinkEntity_user_id_share_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)"
+ },
+ {
+ "name": "index_LinkEntity_user_id_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_id` ON `${TABLE_NAME}` (`user_id`, `id`)"
+ },
+ {
+ "name": "index_LinkEntity_user_id_share_id_parent_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "parent_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id_parent_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `parent_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ },
+ {
+ "table": "ShareEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "id"
+ ]
+ },
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "parent_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkFilePropertiesEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`file_user_id` TEXT NOT NULL, `file_share_id` TEXT NOT NULL, `file_link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `has_thumbnail` INTEGER NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT, `file_signature_address` TEXT, PRIMARY KEY(`file_user_id`, `file_share_id`, `file_link_id`), FOREIGN KEY(`file_user_id`, `file_share_id`, `file_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "file_user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "file_share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "file_link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "activeRevisionId",
+ "columnName": "revision_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasThumbnail",
+ "columnName": "has_thumbnail",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentKeyPacket",
+ "columnName": "content_key_packet",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentKeyPacketSignature",
+ "columnName": "content_key_packet_signature",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "activeRevisionSignatureAddress",
+ "columnName": "file_signature_address",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "file_user_id",
+ "file_share_id",
+ "file_link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkFilePropertiesEntity_file_share_id",
+ "unique": false,
+ "columnNames": [
+ "file_share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_share_id` ON `${TABLE_NAME}` (`file_share_id`)"
+ },
+ {
+ "name": "index_LinkFilePropertiesEntity_file_link_id",
+ "unique": false,
+ "columnNames": [
+ "file_link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_link_id` ON `${TABLE_NAME}` (`file_link_id`)"
+ },
+ {
+ "name": "index_LinkFilePropertiesEntity_revision_id",
+ "unique": false,
+ "columnNames": [
+ "revision_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)"
+ },
+ {
+ "name": "index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id",
+ "unique": false,
+ "columnNames": [
+ "file_user_id",
+ "file_share_id",
+ "file_link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id` ON `${TABLE_NAME}` (`file_user_id`, `file_share_id`, `file_link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "file_user_id",
+ "file_share_id",
+ "file_link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkFolderPropertiesEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`folder_user_id` TEXT NOT NULL, `folder_share_id` TEXT NOT NULL, `folder_link_id` TEXT NOT NULL, `node_hash_key` TEXT NOT NULL, PRIMARY KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`), FOREIGN KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "folder_user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "folder_share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "folder_link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodeHashKey",
+ "columnName": "node_hash_key",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "folder_user_id",
+ "folder_share_id",
+ "folder_link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkFolderPropertiesEntity_folder_share_id",
+ "unique": false,
+ "columnNames": [
+ "folder_share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_share_id` ON `${TABLE_NAME}` (`folder_share_id`)"
+ },
+ {
+ "name": "index_LinkFolderPropertiesEntity_folder_link_id",
+ "unique": false,
+ "columnNames": [
+ "folder_link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_link_id` ON `${TABLE_NAME}` (`folder_link_id`)"
+ },
+ {
+ "name": "index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id",
+ "unique": false,
+ "columnNames": [
+ "folder_user_id",
+ "folder_share_id",
+ "folder_link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id` ON `${TABLE_NAME}` (`folder_user_id`, `folder_share_id`, `folder_link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "folder_user_id",
+ "folder_share_id",
+ "folder_link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkOfflineEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkOfflineEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkOfflineEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_LinkOfflineEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkOfflineEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_LinkOfflineEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkOfflineEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_LinkOfflineEntity_user_id_share_id_link_id",
+ "unique": true,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LinkOfflineEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkDownloadStateEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `state` TEXT NOT NULL, `manifest_signature` TEXT DEFAULT NULL, `signature_address` TEXT DEFAULT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "revisionId",
+ "columnName": "revision_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "manifestSignature",
+ "columnName": "manifest_signature",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "NULL"
+ },
+ {
+ "fieldPath": "signatureAddress",
+ "columnName": "signature_address",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "NULL"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkDownloadStateEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_LinkDownloadStateEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_LinkDownloadStateEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_LinkDownloadStateEntity_revision_id",
+ "unique": false,
+ "columnNames": [
+ "revision_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)"
+ },
+ {
+ "name": "index_LinkDownloadStateEntity_state",
+ "unique": false,
+ "columnNames": [
+ "state"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_state` ON `${TABLE_NAME}` (`state`)"
+ },
+ {
+ "name": "index_LinkDownloadStateEntity_user_id_share_id_link_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "DownloadBlockEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `index` INTEGER NOT NULL, `uri` TEXT NOT NULL, `encrypted_signature` TEXT, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`, `index`), FOREIGN KEY(`user_id`, `share_id`, `link_id`, `revision_id`) REFERENCES `LinkDownloadStateEntity`(`user_id`, `share_id`, `link_id`, `revision_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "revisionId",
+ "columnName": "revision_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "index",
+ "columnName": "index",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uri",
+ "columnName": "uri",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "encryptedSignature",
+ "columnName": "encrypted_signature",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id",
+ "index"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_DownloadBlockEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_DownloadBlockEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_DownloadBlockEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_DownloadBlockEntity_revision_id",
+ "unique": false,
+ "columnNames": [
+ "revision_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)"
+ },
+ {
+ "name": "index_DownloadBlockEntity_user_id_share_id_link_id_revision_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_user_id_share_id_link_id_revision_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`, `revision_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkDownloadStateEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkTrashStateEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `state` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkTrashStateEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_LinkTrashStateEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_LinkTrashStateEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_LinkTrashStateEntity_state",
+ "unique": false,
+ "columnNames": [
+ "state"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_state` ON `${TABLE_NAME}` (`state`)"
+ },
+ {
+ "name": "index_LinkTrashStateEntity_user_id_share_id_link_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "TrashWorkEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `work_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "workId",
+ "columnName": "work_id",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_TrashWorkEntity_user_id_share_id_link_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ },
+ {
+ "name": "index_TrashWorkEntity_work_id",
+ "unique": false,
+ "columnNames": [
+ "work_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_work_id` ON `${TABLE_NAME}` (`work_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "MessageEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `content` TEXT NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_MessageEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_MessageEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UiSettingsEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `layout_type` TEXT NOT NULL, `theme_style` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "layoutType",
+ "columnName": "layout_type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "themeStyle",
+ "columnName": "theme_style",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "DriveLinkRemoteKeyEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `previous_key` INTEGER, `next_key` INTEGER, PRIMARY KEY(`key`, `user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "prevKey",
+ "columnName": "previous_key",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "nextKey",
+ "columnName": "next_key",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "key",
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id",
+ "unique": true,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ },
+ {
+ "name": "index_DriveLinkRemoteKeyEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_DriveLinkRemoteKeyEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_DriveLinkRemoteKeyEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_DriveLinkRemoteKeyEntity_key",
+ "unique": false,
+ "columnNames": [
+ "key"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_key` ON `${TABLE_NAME}` (`key`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ },
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "SortingEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `sorting_by` TEXT NOT NULL, `sorting_direction` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sortingBy",
+ "columnName": "sorting_by",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sortingDirection",
+ "columnName": "sorting_direction",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkUploadEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `parent_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `name` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT NOT NULL, `manifest_signature` TEXT NOT NULL, `state` TEXT NOT NULL, `size` INTEGER DEFAULT NULL, `last_modified` INTEGER, `uri` TEXT DEFAULT NULL, `should_delete_source_uri` INTEGER NOT NULL DEFAULT false, `media_resolution_width` INTEGER DEFAULT NULL, `media_resolution_height` INTEGER DEFAULT NULL, `digests` TEXT DEFAULT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "volumeId",
+ "columnName": "volume_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentId",
+ "columnName": "parent_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "revisionId",
+ "columnName": "revision_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mimeType",
+ "columnName": "mime_type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodeKey",
+ "columnName": "node_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodePassphrase",
+ "columnName": "node_passphrase",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodePassphraseSignature",
+ "columnName": "node_passphrase_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentKeyPacket",
+ "columnName": "content_key_packet",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentKeyPacketSignature",
+ "columnName": "content_key_packet_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "manifestSignature",
+ "columnName": "manifest_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "INTEGER",
+ "notNull": false,
+ "defaultValue": "NULL"
+ },
+ {
+ "fieldPath": "lastModified",
+ "columnName": "last_modified",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "uri",
+ "columnName": "uri",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "NULL"
+ },
+ {
+ "fieldPath": "shouldDeleteSourceUri",
+ "columnName": "should_delete_source_uri",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "mediaResolutionWidth",
+ "columnName": "media_resolution_width",
+ "affinity": "INTEGER",
+ "notNull": false,
+ "defaultValue": "NULL"
+ },
+ {
+ "fieldPath": "mediaResolutionHeight",
+ "columnName": "media_resolution_height",
+ "affinity": "INTEGER",
+ "notNull": false,
+ "defaultValue": "NULL"
+ },
+ {
+ "fieldPath": "digests",
+ "columnName": "digests",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "NULL"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_LinkUploadEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_volume_id",
+ "unique": false,
+ "columnNames": [
+ "volume_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_revision_id",
+ "unique": false,
+ "columnNames": [
+ "revision_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_parent_id",
+ "unique": false,
+ "columnNames": [
+ "parent_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UploadBlockEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`upload_link_id` INTEGER NOT NULL, `index` INTEGER NOT NULL, `size` INTEGER NOT NULL, `encrypted_signature` TEXT NOT NULL, `hash` TEXT NOT NULL, `token` TEXT NOT NULL, `url` TEXT NOT NULL, `raw_size` INTEGER NOT NULL DEFAULT 0, `verifier_token` TEXT DEFAULT NULL, PRIMARY KEY(`upload_link_id`, `index`), FOREIGN KEY(`upload_link_id`) REFERENCES `LinkUploadEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "uploadLinkId",
+ "columnName": "upload_link_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "index",
+ "columnName": "index",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "encryptedSignature",
+ "columnName": "encrypted_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hash",
+ "columnName": "hash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uploadToken",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "rawSize",
+ "columnName": "raw_size",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "verifierToken",
+ "columnName": "verifier_token",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "NULL"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "upload_link_id",
+ "index"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "LinkUploadEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "upload_link_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UploadBulkEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `parent_id` TEXT NOT NULL, `should_delete_source_uri` INTEGER NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "volumeId",
+ "columnName": "volume_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentId",
+ "columnName": "parent_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shouldDeleteSourceUri",
+ "columnName": "should_delete_source_uri",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_UploadBulkEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_UploadBulkEntity_volume_id",
+ "unique": false,
+ "columnNames": [
+ "volume_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)"
+ },
+ {
+ "name": "index_UploadBulkEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_UploadBulkEntity_parent_id",
+ "unique": false,
+ "columnNames": [
+ "parent_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UploadBulkUriStringEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`upload_bulk_id` INTEGER NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`upload_bulk_id`, `uri`), FOREIGN KEY(`upload_bulk_id`) REFERENCES `UploadBulkEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "upload_bulk_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uri",
+ "columnName": "uri",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "upload_bulk_id",
+ "uri"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_UploadBulkUriStringEntity_upload_bulk_id",
+ "unique": false,
+ "columnNames": [
+ "upload_bulk_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkUriStringEntity_upload_bulk_id` ON `${TABLE_NAME}` (`upload_bulk_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "UploadBulkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "upload_bulk_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "FolderMetadataEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `last_fetch_children_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastFetchChildrenTimestamp",
+ "columnName": "last_fetch_children_timestamp",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "TrashMetadataEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `last_fetch_trash_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastFetchTrashTimestamp",
+ "columnName": "last_fetch_trash_timestamp",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "ShareEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "NotificationChannelEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`user_id`, `type`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "type"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_NotificationChannelEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationChannelEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "NotificationEventEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `channel_type` TEXT NOT NULL, `notification_tag` TEXT NOT NULL, `notification_id` INTEGER NOT NULL, `notification_event_id` TEXT NOT NULL, `notification_event` TEXT NOT NULL, PRIMARY KEY(`user_id`, `channel_type`, `notification_tag`, `notification_id`, `notification_event_id`), FOREIGN KEY(`user_id`, `channel_type`) REFERENCES `NotificationChannelEntity`(`user_id`, `type`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "channelType",
+ "columnName": "channel_type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationTag",
+ "columnName": "notification_tag",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationId",
+ "columnName": "notification_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationEventId",
+ "columnName": "notification_event_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationEvent",
+ "columnName": "notification_event",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "channel_type",
+ "notification_tag",
+ "notification_id",
+ "notification_event_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_NotificationEventEntity_user_id_channel_type_notification_tag_notification_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "channel_type",
+ "notification_tag",
+ "notification_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationEventEntity_user_id_channel_type_notification_tag_notification_id` ON `${TABLE_NAME}` (`user_id`, `channel_type`, `notification_tag`, `notification_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "NotificationChannelEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "channel_type"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "type"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkSelectionEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `selection_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `selection_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "selectionId",
+ "columnName": "selection_id",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "selection_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkSelectionEntity_user_id_share_id_link_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ },
+ {
+ "name": "index_LinkSelectionEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_LinkSelectionEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_LinkSelectionEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_LinkSelectionEntity_selection_id",
+ "unique": false,
+ "columnNames": [
+ "selection_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_selection_id` ON `${TABLE_NAME}` (`selection_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "WorkerRunEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `worker_id` TEXT NOT NULL, `run_at` INTEGER NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "workerId",
+ "columnName": "worker_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "runAt",
+ "columnName": "run_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_WorkerRunEntity_worker_id",
+ "unique": false,
+ "columnNames": [
+ "worker_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_WorkerRunEntity_worker_id` ON `${TABLE_NAME}` (`worker_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '443da6c93db22831596a4ae60bb0a948')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabase.kt b/drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabase.kt
index bfc4d6da..c38c481c 100644
--- a/drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabase.kt
+++ b/drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabase.kt
@@ -192,6 +192,7 @@ import me.proton.drive.android.settings.data.db.entity.UiSettingsEntity
AutoMigration(from = 18, to = 19),
AutoMigration(from = 22, to = 23),
AutoMigration(from = 23, to = 24),
+ AutoMigration(from = 26, to = 27),
],
exportSchema = true,
)
@@ -250,7 +251,7 @@ abstract class DriveDatabase :
WorkerDatabase {
companion object {
- const val VERSION = 26
+ const val VERSION = 27
private val migrations = listOf(
DriveDatabaseMigrations.MIGRATION_1_2,
@@ -278,6 +279,7 @@ abstract class DriveDatabase :
//AutoMigration(from = 23, to = 24)
DriveDatabaseMigrations.MIGRATION_24_25,
DriveDatabaseMigrations.MIGRATION_25_26,
+ //AutoMigration(from = 26, to = 27)
)
fun buildDatabase(context: Context): DriveDatabase =
diff --git a/drive/i18n/src/main/res/values/upload.xml b/drive/i18n/src/main/res/values/upload.xml
index 949c608b..c9d6a8dd 100644
--- a/drive/i18n/src/main/res/values/upload.xml
+++ b/drive/i18n/src/main/res/values/upload.xml
@@ -53,4 +53,5 @@
- %1$d item
- %1$d items
+ Upload failed: Verification of data failed
diff --git a/drive/i18n/src/main/res/values/verifier.xml b/drive/i18n/src/main/res/values/verifier.xml
new file mode 100644
index 00000000..216b2ea3
--- /dev/null
+++ b/drive/i18n/src/main/res/values/verifier.xml
@@ -0,0 +1,22 @@
+
+
+
+ Verifier initialization failed
+ Verifier failed verifing blocks
+
diff --git a/drive/key/domain/src/main/kotlin/me/proton/core/drive/key/domain/usecase/BuildContentKey.kt b/drive/key/domain/src/main/kotlin/me/proton/core/drive/key/domain/usecase/BuildContentKey.kt
index 55063c2f..9f6d2a1f 100644
--- a/drive/key/domain/src/main/kotlin/me/proton/core/drive/key/domain/usecase/BuildContentKey.kt
+++ b/drive/key/domain/src/main/kotlin/me/proton/core/drive/key/domain/usecase/BuildContentKey.kt
@@ -54,12 +54,24 @@ class BuildContentKey @Inject constructor(
userId: UserId,
uploadFile: UploadFileLink,
fileKey: Key.Node,
+ ): Result = invoke(
+ userId = userId,
+ contentKeyPacket = uploadFile.contentKeyPacket,
+ contentKeyPacketSignature = uploadFile.contentKeyPacketSignature,
+ fileKey = fileKey,
+ )
+
+ suspend operator fun invoke(
+ userId: UserId,
+ contentKeyPacket: String,
+ contentKeyPacketSignature: String,
+ fileKey: Key.Node,
): Result = coRunCatching {
contentKeyFactory.createContentKey(
decryptKey = fileKey,
verifyKey = listOf(fileKey, getAddressKeys(userId, getSignatureAddress(userId))),
- contentKeyPacket = uploadFile.contentKeyPacket,
- contentKeyPacketSignature = uploadFile.contentKeyPacketSignature
+ contentKeyPacket = contentKeyPacket,
+ contentKeyPacketSignature = contentKeyPacketSignature
)
}
}
diff --git a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/dao/UploadBlockDao.kt b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/dao/UploadBlockDao.kt
index 811f1c21..74f5e2d9 100644
--- a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/dao/UploadBlockDao.kt
+++ b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/dao/UploadBlockDao.kt
@@ -56,4 +56,14 @@ abstract class UploadBlockDao : BaseDao() {
index: Long,
token: String,
)
+
+ @Query("""
+ UPDATE UploadBlockEntity SET verifier_token = :verifierToken WHERE
+ upload_link_id = :uploadLinkId AND `index` = :index
+ """)
+ abstract fun updateVerifierToken(
+ uploadLinkId: Long,
+ index: Long,
+ verifierToken: String,
+ )
}
diff --git a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/entity/UploadBlockEntity.kt b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/entity/UploadBlockEntity.kt
index ca3467a0..77288859 100644
--- a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/entity/UploadBlockEntity.kt
+++ b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/entity/UploadBlockEntity.kt
@@ -29,6 +29,7 @@ import me.proton.core.drive.base.data.db.Column.SIZE
import me.proton.core.drive.base.data.db.Column.TOKEN
import me.proton.core.drive.base.data.db.Column.UPLOAD_LINK_ID
import me.proton.core.drive.base.data.db.Column.URL
+import me.proton.core.drive.base.data.db.Column.VERIFIER_TOKEN
@Entity(
primaryKeys = [UPLOAD_LINK_ID, INDEX],
@@ -58,4 +59,6 @@ data class UploadBlockEntity(
val url: String,
@ColumnInfo(name = RAW_SIZE, defaultValue = "0")
val rawSize: Long,
+ @ColumnInfo(name = VERIFIER_TOKEN, defaultValue = "NULL")
+ val verifierToken: String? = null,
)
diff --git a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/extension/UploadBlockEntity.kt b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/extension/UploadBlockEntity.kt
index 64bc0793..54f82d55 100644
--- a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/extension/UploadBlockEntity.kt
+++ b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/extension/UploadBlockEntity.kt
@@ -30,4 +30,5 @@ fun UploadBlockEntity.toUploadBlock(uploadBlockFactory: UploadBlockFactory) =
rawSize = rawSize.bytes,
size = size.bytes,
token = uploadToken,
+ verifierToken = verifierToken,
)
diff --git a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/factory/UploadBlockFactoryImpl.kt b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/factory/UploadBlockFactoryImpl.kt
index cef4e510..83e90aec 100644
--- a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/factory/UploadBlockFactoryImpl.kt
+++ b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/factory/UploadBlockFactoryImpl.kt
@@ -33,7 +33,8 @@ class UploadBlockFactoryImpl @Inject constructor() : UploadBlockFactory {
encSignature: String,
rawSize: Bytes,
size: Bytes,
- token: String
+ token: String,
+ verifierToken: String?,
): UploadBlock =
UploadBlock(
index = index,
@@ -44,6 +45,7 @@ class UploadBlockFactoryImpl @Inject constructor() : UploadBlockFactory {
size = size,
token = token,
file = block,
+ verifierToken = verifierToken,
)
override fun create(
@@ -53,7 +55,8 @@ class UploadBlockFactoryImpl @Inject constructor() : UploadBlockFactory {
encSignature: String,
rawSize: Bytes,
size: Bytes,
- token: String
+ token: String,
+ verifierToken: String?,
): UploadBlock =
UploadBlock(
index = index,
@@ -64,5 +67,6 @@ class UploadBlockFactoryImpl @Inject constructor() : UploadBlockFactory {
size = size,
token = token,
file = File(requireNotNull(Uri.parse(url)?.path)),
+ verifierToken = verifierToken,
)
}
diff --git a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/repository/LinkUploadRepositoryImpl.kt b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/repository/LinkUploadRepositoryImpl.kt
index 98a6b620..4f7f5126 100644
--- a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/repository/LinkUploadRepositoryImpl.kt
+++ b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/repository/LinkUploadRepositoryImpl.kt
@@ -17,6 +17,7 @@
*/
package me.proton.core.drive.linkupload.data.repository
+import android.util.Base64
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import me.proton.core.domain.entity.UserId
@@ -227,6 +228,17 @@ class LinkUploadRepositoryImpl @Inject constructor(
token = token,
)
+ override suspend fun updateUploadBlockVerifierToken(
+ uploadFileLinkId: Long,
+ uploadBlockIndex: Long,
+ verifierToken: ByteArray
+ ) =
+ db.uploadBlockDao.updateVerifierToken(
+ uploadLinkId = uploadFileLinkId,
+ index = uploadBlockIndex,
+ verifierToken = Base64.encodeToString(verifierToken, Base64.NO_WRAP),
+ )
+
override suspend fun removeUploadBlocks(uploadFileLink: UploadFileLink) =
db.uploadBlockDao.delete(uploadFileLink.id)
diff --git a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadBlock.kt b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadBlock.kt
index 07ad3986..9b4c5e27 100644
--- a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadBlock.kt
+++ b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadBlock.kt
@@ -30,4 +30,5 @@ data class UploadBlock(
val size: Bytes,
val token: String,
val file: File,
+ val verifierToken: String?,
) : Block
diff --git a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/factory/UploadBlockFactory.kt b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/factory/UploadBlockFactory.kt
index 20de272c..c013ef9b 100644
--- a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/factory/UploadBlockFactory.kt
+++ b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/factory/UploadBlockFactory.kt
@@ -30,6 +30,7 @@ interface UploadBlockFactory {
rawSize: Bytes,
size: Bytes,
token: String,
+ verifierToken: String?,
): UploadBlock
fun create(
@@ -40,5 +41,6 @@ interface UploadBlockFactory {
rawSize: Bytes,
size: Bytes,
token: String,
+ verifierToken: String?,
): UploadBlock
}
diff --git a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/repository/LinkUploadRepository.kt b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/repository/LinkUploadRepository.kt
index eceb092b..f654c20f 100644
--- a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/repository/LinkUploadRepository.kt
+++ b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/repository/LinkUploadRepository.kt
@@ -110,6 +110,12 @@ interface LinkUploadRepository {
token: String,
)
+ suspend fun updateUploadBlockVerifierToken(
+ uploadFileLinkId: Long,
+ uploadBlockIndex: Long,
+ verifierToken: ByteArray,
+ )
+
suspend fun removeUploadBlocks(uploadFileLink: UploadFileLink)
suspend fun insertUploadBulk(uploadBulk: UploadBulk): UploadBulk
diff --git a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/usecase/UpdateVerifierToken.kt b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/usecase/UpdateVerifierToken.kt
new file mode 100644
index 00000000..029b9d41
--- /dev/null
+++ b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/usecase/UpdateVerifierToken.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core 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.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+package me.proton.core.drive.linkupload.domain.usecase
+
+import me.proton.core.drive.linkupload.domain.entity.UploadBlock
+import me.proton.core.drive.linkupload.domain.repository.LinkUploadRepository
+import javax.inject.Inject
+
+class UpdateVerifierToken @Inject constructor(
+ private val linkUploadRepository: LinkUploadRepository,
+ private val getUploadBlockAfterOperation: GetUploadBlockAfterOperation,
+) {
+
+ suspend operator fun invoke(
+ uploadFileLinkId: Long,
+ index: Long,
+ verifierToken: ByteArray,
+ ): Result = getUploadBlockAfterOperation(uploadFileLinkId, index) {
+ linkUploadRepository.updateUploadBlockVerifierToken(uploadFileLinkId, index, verifierToken)
+ }
+}
diff --git a/drive/upload/data/build.gradle.kts b/drive/upload/data/build.gradle.kts
index c3f2f517..0ac3186b 100644
--- a/drive/upload/data/build.gradle.kts
+++ b/drive/upload/data/build.gradle.kts
@@ -33,7 +33,9 @@ driveModule(
implementation(project(":drive:link:presentation"))
implementation(project(":drive:notification:domain"))
implementation(project(":drive:worker:data"))
+ implementation(project(":verifier:data"))
implementation(libs.androidx.lifecycle.livedata.ktx)
implementation(libs.core.crypto)
implementation(libs.core.network)
+ testImplementation(libs.bundles.test.jvm)
}
diff --git a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/extension/Throwable.kt b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/extension/Throwable.kt
index 6f656de3..80fabd4c 100644
--- a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/extension/Throwable.kt
+++ b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/extension/Throwable.kt
@@ -17,6 +17,9 @@
*/
package me.proton.core.drive.upload.data.extension
+import me.proton.android.drive.verifier.data.extension.log
+import me.proton.android.drive.verifier.domain.exception.VerifierException
+import me.proton.core.drive.base.presentation.extension.log
import me.proton.core.network.domain.ApiException
import me.proton.core.network.domain.ApiResult
import me.proton.core.network.domain.isRetryable
@@ -30,5 +33,13 @@ internal val Throwable.isRetryable: Boolean
else -> this.error.isRetryable()
}
}
+ is VerifierException -> this.cause.isRetryable
else -> false
}
+
+internal fun Throwable.log(tag: String, message: String? = null): Throwable = this.also {
+ when (this) {
+ is VerifierException -> this.log(tag, message.orEmpty())
+ else -> this.log(tag, message)
+ }
+}
diff --git a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/extension/UploadCleanupException.kt b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/extension/UploadCleanupException.kt
index c2c71aa0..00f3ba15 100644
--- a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/extension/UploadCleanupException.kt
+++ b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/extension/UploadCleanupException.kt
@@ -18,15 +18,24 @@
package me.proton.core.drive.upload.data.extension
import android.content.Context
+import me.proton.android.drive.verifier.data.extension.log
+import me.proton.android.drive.verifier.domain.exception.VerifierException
import me.proton.core.drive.base.presentation.extension.getDefaultMessage
import me.proton.core.drive.base.presentation.extension.log
import me.proton.core.drive.upload.data.exception.UploadCleanupException
+import me.proton.core.drive.i18n.R as I18N
internal fun UploadCleanupException.getDefaultMessage(
context: Context,
useExceptionMessage: Boolean,
-): String = error.getDefaultMessage(context, useExceptionMessage)
+): String = when (error) {
+ is VerifierException -> context.getString(I18N.string.files_upload_verification_failed)
+ else -> error.getDefaultMessage(context, useExceptionMessage)
+}
internal fun UploadCleanupException.log(tag: String, message: String? = null): UploadCleanupException = also {
- message?.let { error.log(tag, message) } ?: error.log(tag)
+ when (error) {
+ is VerifierException -> message?.let { error.log(tag, message) } ?: error.log(tag)
+ else -> message?.let { error.log(tag, message) } ?: error.log(tag)
+ }
}
diff --git a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/BlockUploadWorker.kt b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/BlockUploadWorker.kt
index 49d07218..711e2d0d 100644
--- a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/BlockUploadWorker.kt
+++ b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/BlockUploadWorker.kt
@@ -46,6 +46,7 @@ import me.proton.core.drive.linkupload.domain.usecase.GetUploadFileLink
import me.proton.core.drive.linkupload.domain.usecase.UpdateToken
import me.proton.core.drive.upload.data.extension.getSizeData
import me.proton.core.drive.upload.data.extension.isRetryable
+import me.proton.core.drive.upload.data.extension.retryOrAbort
import me.proton.core.drive.upload.data.extension.setSize
import me.proton.core.drive.upload.data.worker.WorkerKeys.KEY_BLOCK_INDEX
import me.proton.core.drive.upload.data.worker.WorkerKeys.KEY_BLOCK_TOKEN
@@ -132,11 +133,7 @@ class BlockUploadWorker @AssistedInject constructor(
max retries reached ${!canRetry}
""".trimIndent(),
)
- return@coroutineScope if (retryable && canRetry) {
- Result.retry()
- } else {
- Result.success(getSizeData(progress.value))
- }
+ return@coroutineScope retryOrAbort(retryable && canRetry, error, uploadFileLink.name)
}
} finally {
job.cancel()
diff --git a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/FileUploadFlow.kt b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/FileUploadFlow.kt
index 16c7d7df..50d34104 100644
--- a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/FileUploadFlow.kt
+++ b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/FileUploadFlow.kt
@@ -46,13 +46,19 @@ internal sealed class FileUploadFlow {
uploadFileLinkId = uploadFileLinkId,
uriString = uriString,
shouldDeleteSource = shouldDeleteSource,
- tags = uploadTags
+ tags = uploadTags,
+ )
+ ).then(
+ VerifyBlocksWorker.getWorkRequest(
+ userId = userId,
+ uploadFileLinkId = uploadFileLinkId,
+ tags = uploadTags,
)
).then(
GetBlocksUploadUrlWorker.getWorkRequest(
userId = userId,
uploadFileLinkId = uploadFileLinkId,
- tags = uploadTags
+ tags = uploadTags,
)
).enqueue()
}
@@ -73,6 +79,12 @@ internal sealed class FileUploadFlow {
shouldDeleteSource = shouldDeleteSource,
tags = uploadTags
)
+ ).then(
+ VerifyBlocksWorker.getWorkRequest(
+ userId = userId,
+ uploadFileLinkId = uploadFileLinkId,
+ tags = uploadTags,
+ )
).then(
GetBlocksUploadUrlWorker.getWorkRequest(
userId = userId,
diff --git a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/UploadCleanupWorker.kt b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/UploadCleanupWorker.kt
index 7f52726e..edb5e4f2 100644
--- a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/UploadCleanupWorker.kt
+++ b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/UploadCleanupWorker.kt
@@ -27,6 +27,7 @@ import androidx.work.WorkerParameters
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import me.proton.android.drive.verifier.domain.usecase.CleanupVerifier
import me.proton.core.domain.entity.UserId
import me.proton.core.drive.base.data.workmanager.addTags
import me.proton.core.drive.base.domain.entity.Percentage
@@ -60,6 +61,7 @@ class UploadCleanupWorker @AssistedInject constructor(
private val getBlockFolder: GetBlockFolder,
private val removeUploadFile: RemoveUploadFile,
private val announceEvent: AnnounceEvent,
+ private val cleanupVerifier: CleanupVerifier,
configurationProvider: ConfigurationProvider,
canRun: CanRun,
run: Run,
@@ -95,6 +97,14 @@ class UploadCleanupWorker @AssistedInject constructor(
percentage = Percentage(0)
)
)
+ uploadFileLink.linkId?.let { linkId ->
+ cleanupVerifier(
+ userId = userId,
+ shareId = uploadFileLink.shareId.id,
+ linkId = linkId,
+ revisionId = uploadFileLink.draftRevisionId,
+ )
+ }
} finally {
uploadFileLink.deleteOnServer()
}
diff --git a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/VerifyBlocksWorker.kt b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/VerifyBlocksWorker.kt
new file mode 100644
index 00000000..410a33c3
--- /dev/null
+++ b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/VerifyBlocksWorker.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core 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.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+package me.proton.core.drive.upload.data.worker
+
+import android.content.Context
+import androidx.hilt.work.HiltWorker
+import androidx.work.BackoffPolicy
+import androidx.work.Constraints
+import androidx.work.Data
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import me.proton.core.domain.entity.UserId
+import me.proton.core.drive.base.data.workmanager.addTags
+import me.proton.core.drive.base.domain.provider.ConfigurationProvider
+import me.proton.core.drive.base.domain.usecase.BroadcastMessages
+import me.proton.core.drive.linkupload.domain.entity.UploadFileLink
+import me.proton.core.drive.linkupload.domain.usecase.GetUploadFileLink
+import me.proton.core.drive.upload.data.extension.isRetryable
+import me.proton.core.drive.upload.data.extension.log
+import me.proton.core.drive.upload.data.extension.logTag
+import me.proton.core.drive.upload.data.extension.retryOrAbort
+import me.proton.core.drive.upload.domain.usecase.VerifyBlocks
+import me.proton.core.drive.worker.domain.usecase.CanRun
+import me.proton.core.drive.worker.domain.usecase.Done
+import me.proton.core.drive.worker.domain.usecase.Run
+import java.util.concurrent.TimeUnit
+
+@HiltWorker
+@OptIn(ExperimentalCoroutinesApi::class)
+@Suppress("LongParameterList")
+class VerifyBlocksWorker @AssistedInject constructor(
+ @Assisted appContext: Context,
+ @Assisted workerParams: WorkerParameters,
+ workManager: WorkManager,
+ broadcastMessages: BroadcastMessages,
+ getUploadFileLink: GetUploadFileLink,
+ private val verifyBlocks: VerifyBlocks,
+ configurationProvider: ConfigurationProvider,
+ canRun: CanRun,
+ run: Run,
+ done: Done,
+) : UploadCoroutineWorker(
+ appContext = appContext,
+ workerParams = workerParams,
+ workManager = workManager,
+ broadcastMessages = broadcastMessages,
+ getUploadFileLink = getUploadFileLink,
+ configurationProvider = configurationProvider,
+ canRun = canRun,
+ run = run,
+ done = done,
+) {
+
+ override suspend fun doLimitedRetryUploadWork(uploadFileLink: UploadFileLink): Result {
+ verifyBlocks(uploadFileLink)
+ .onFailure { error ->
+ val retryable = error.isRetryable
+ val canRetry = canRetry()
+ error.log(
+ tag = uploadFileLink.logTag(),
+ message = """
+ Verify blocks failed with "${error.message}" retryable $retryable,
+ max retries reached ${!canRetry}
+ """.trimIndent()
+ )
+ return retryOrAbort(retryable && canRetry, error, uploadFileLink.name)
+ }
+ return Result.success()
+ }
+
+ companion object {
+ fun getWorkRequest(
+ userId: UserId,
+ uploadFileLinkId: Long,
+ tags: List = emptyList(),
+ ): OneTimeWorkRequest =
+ OneTimeWorkRequest.Builder(VerifyBlocksWorker::class.java)
+ .setConstraints(
+ Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+ )
+ .setInputData(
+ Data.Builder()
+ .putString(WorkerKeys.KEY_USER_ID, userId.id)
+ .putLong(WorkerKeys.KEY_UPLOAD_FILE_LINK_ID, uploadFileLinkId)
+ .build()
+ )
+ .setBackoffCriteria(
+ BackoffPolicy.EXPONENTIAL,
+ OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
+ TimeUnit.MILLISECONDS
+ )
+ .addTags(listOf(userId.id) + tags)
+ .build()
+ }
+}
diff --git a/drive/upload/data/src/test/kotlin/me/proton/core/drive/upload/data/worker/UpdateRevisionWorkerTest.kt b/drive/upload/data/src/test/kotlin/me/proton/core/drive/upload/data/worker/UpdateRevisionWorkerTest.kt
new file mode 100644
index 00000000..8d63fb18
--- /dev/null
+++ b/drive/upload/data/src/test/kotlin/me/proton/core/drive/upload/data/worker/UpdateRevisionWorkerTest.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core 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.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+package me.proton.core.drive.upload.data.worker
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.work.Data
+import androidx.work.ListenableWorker
+import androidx.work.Operation
+import androidx.work.WorkManager
+import androidx.work.WorkRequest
+import androidx.work.WorkerFactory
+import androidx.work.WorkerParameters
+import androidx.work.testing.TestListenableWorkerBuilder
+import io.mockk.coEvery
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import me.proton.core.domain.arch.DataResult
+import me.proton.core.domain.arch.ResponseSource
+import me.proton.core.domain.entity.UserId
+import me.proton.core.drive.base.domain.provider.ConfigurationProvider
+import me.proton.core.drive.base.domain.usecase.BroadcastMessages
+import me.proton.core.drive.linkupload.domain.entity.UploadFileLink
+import me.proton.core.drive.linkupload.domain.usecase.GetUploadFileLink
+import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
+import me.proton.core.drive.upload.domain.usecase.UpdateRevision
+import me.proton.core.drive.worker.domain.usecase.CanRun
+import me.proton.core.drive.worker.domain.usecase.Done
+import me.proton.core.drive.worker.domain.usecase.Run
+import me.proton.core.network.domain.ApiException
+import me.proton.core.network.domain.ApiResult
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(RobolectricTestRunner::class)
+class UpdateRevisionWorkerTest {
+ private val userId: UserId = UserId("user-id")
+ private val workManager = mockk()
+ private val broadcastMessages = mockk()
+ private val getUploadFileLink = mockk()
+ private val updateRevision = mockk()
+ private val configurationProvider = mockk()
+ private val canRun = mockk()
+ private val run = mockk()
+ private val done = mockk()
+ private val uploadFileLink = mockk()
+ private val operation = mockk()
+
+ @Before
+ fun before() {
+ coEvery { canRun(any(), any()) } returns Result.success(true)
+ coEvery { getUploadFileLink(any() as Long) } returns DataResult.Success(ResponseSource.Local, uploadFileLink)
+ coEvery { workManager.enqueue(any() as WorkRequest) } returns operation
+ coEvery { configurationProvider.useExceptionMessage } returns false
+ coEvery { broadcastMessages(userId, any(), any(), any()) } returns Unit
+ }
+
+ @Test
+ fun `when commit a revision receives error with proton code 200501, upload fails and message is shown`() = runTest {
+ // Given
+ val uploadFile = "proton_drive.pdf"
+ coEvery { uploadFileLink.name } returns uploadFile
+ val errorFromServer = "Upload failed: Verification of data failed"
+ coEvery { updateRevision(any()) } returns Result.failure(
+ ApiException(
+ ApiResult.Error.Http(
+ httpCode = 422,
+ message = "Unprocessable Content",
+ proton = ApiResult.Error.ProtonData(
+ code = 200501,
+ error = errorFromServer,
+ )
+ )
+ )
+ )
+
+ // When
+ val result = updateRevisionWorker(userId).doLimitedRetryWork()
+
+ // Then
+ assertEquals(ListenableWorker.Result.failure(), result)
+ verify(exactly = 1) {
+ broadcastMessages(
+ userId = userId,
+ message = "Uploading file $uploadFile failed with reason: $errorFromServer",
+ type = BroadcastMessage.Type.ERROR,
+ extra = null,
+ )
+ }
+ }
+
+ private fun updateRevisionWorker(
+ userId: UserId,
+ ): UpdateRevisionWorker {
+ val context = ApplicationProvider.getApplicationContext()
+ return TestListenableWorkerBuilder(context)
+ .setWorkerFactory(
+ object : WorkerFactory() {
+ override fun createWorker(
+ appContext: Context,
+ workerClassName: String,
+ workerParameters: WorkerParameters
+ ) = UpdateRevisionWorker(
+ appContext = appContext,
+ workerParams = workerParameters,
+ workManager = workManager,
+ broadcastMessages = broadcastMessages,
+ getUploadFileLink = getUploadFileLink,
+ updateRevision = updateRevision,
+ configurationProvider = configurationProvider,
+ canRun = canRun,
+ run = run,
+ done = done,
+ )
+ }
+ )
+ .setInputData(
+ Data.Builder()
+ .putString(WorkerKeys.KEY_USER_ID, userId.id)
+ .build()
+ )
+ .build()
+ }
+}
diff --git a/drive/upload/data/src/test/kotlin/me/proton/core/drive/upload/data/worker/VerifyBlocksWorkerTest.kt b/drive/upload/data/src/test/kotlin/me/proton/core/drive/upload/data/worker/VerifyBlocksWorkerTest.kt
new file mode 100644
index 00000000..60283c38
--- /dev/null
+++ b/drive/upload/data/src/test/kotlin/me/proton/core/drive/upload/data/worker/VerifyBlocksWorkerTest.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core 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.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+package me.proton.core.drive.upload.data.worker
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.work.Data
+import androidx.work.ListenableWorker
+import androidx.work.Operation
+import androidx.work.WorkManager
+import androidx.work.WorkRequest
+import androidx.work.WorkerFactory
+import androidx.work.WorkerParameters
+import androidx.work.testing.TestListenableWorkerBuilder
+import io.mockk.coEvery
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import me.proton.android.drive.verifier.domain.exception.VerifierException
+import me.proton.core.crypto.common.pgp.exception.CryptoException
+import me.proton.core.domain.arch.DataResult
+import me.proton.core.domain.arch.ResponseSource
+import me.proton.core.domain.entity.UserId
+import me.proton.core.drive.base.domain.provider.ConfigurationProvider
+import me.proton.core.drive.base.domain.usecase.BroadcastMessages
+import me.proton.core.drive.linkupload.domain.entity.UploadFileLink
+import me.proton.core.drive.linkupload.domain.usecase.GetUploadFileLink
+import me.proton.core.drive.upload.domain.usecase.VerifyBlocks
+import me.proton.core.drive.worker.domain.usecase.CanRun
+import me.proton.core.drive.worker.domain.usecase.Done
+import me.proton.core.drive.worker.domain.usecase.Run
+import me.proton.core.util.kotlin.CoreLogger
+import me.proton.core.util.kotlin.Logger
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(RobolectricTestRunner::class)
+class VerifyBlocksWorkerTest {
+ private val userId: UserId = UserId("user-id")
+ private val workManager = mockk()
+ private val broadcastMessages = mockk()
+ private val getUploadFileLink = mockk()
+ private val verifyBlocks = mockk()
+ private val configurationProvider = mockk()
+ private val canRun = mockk()
+ private val run = mockk()
+ private val done = mockk()
+ private val uploadFileLink = mockk()
+ private val operation = mockk()
+ private val logger = mockk()
+
+ @Before
+ fun before() {
+ coEvery { canRun(any(), any()) } returns Result.success(true)
+ coEvery { getUploadFileLink(any() as Long) } returns DataResult.Success(ResponseSource.Local, uploadFileLink)
+ coEvery { uploadFileLink.id } returns 123L
+ coEvery { uploadFileLink.name } returns "secret.jpg"
+ coEvery { workManager.enqueue(any() as WorkRequest) } returns operation
+ coEvery { configurationProvider.useExceptionMessage } returns false
+ coEvery { broadcastMessages(userId, any(), any(), any()) } returns Unit
+ coEvery { logger.d(any(), any()) } returns Unit
+ coEvery { logger.e(any(), any(), any()) } returns Unit
+ }
+
+ @Test
+ fun `when verify blocks fails with VerifyBlock exception, upload fails and it is logged as error`() = runTest {
+ // Given
+ val error = VerifierException.VerifyBlock(CryptoException("Invalid key"))
+ coEvery { verifyBlocks(uploadFileLink) } returns Result.failure(error)
+ val uploadFileLinkId = uploadFileLink.id
+ CoreLogger.set(logger)
+
+ // When
+ val result = verifyBlocksWorker(userId).doLimitedRetryWork()
+
+ // Then
+ Assert.assertEquals(ListenableWorker.Result.failure(), result)
+ verify(exactly = 1) {
+ logger.e(
+ tag = "core.drive.upload.$uploadFileLinkId",
+ e = error,
+ message = """
+ Verify blocks failed with "${error.message}" retryable false,
+ max retries reached false
+ """.trimIndent(),
+ )
+ }
+ }
+
+ private fun verifyBlocksWorker(
+ userId: UserId,
+ ): VerifyBlocksWorker {
+ val context = ApplicationProvider.getApplicationContext()
+ return TestListenableWorkerBuilder(context)
+ .setWorkerFactory(
+ object : WorkerFactory() {
+ override fun createWorker(
+ appContext: Context,
+ workerClassName: String,
+ workerParameters: WorkerParameters
+ ) = VerifyBlocksWorker(
+ appContext = appContext,
+ workerParams = workerParameters,
+ workManager = workManager,
+ broadcastMessages = broadcastMessages,
+ getUploadFileLink = getUploadFileLink,
+ verifyBlocks = verifyBlocks,
+ configurationProvider = configurationProvider,
+ canRun = canRun,
+ run = run,
+ done = done,
+ )
+ }
+ )
+ .setInputData(
+ Data.Builder()
+ .putString(WorkerKeys.KEY_USER_ID, userId.id)
+ .build()
+ )
+ .build()
+ }
+}
diff --git a/drive/upload/domain/build.gradle.kts b/drive/upload/domain/build.gradle.kts
index fded5d9f..4bd01cc0 100644
--- a/drive/upload/domain/build.gradle.kts
+++ b/drive/upload/domain/build.gradle.kts
@@ -28,6 +28,7 @@ driveModule(
api(project(":drive:link-upload:domain"))
api(project(":drive:notification:domain"))
api(project(":drive:thumbnail:domain"))
+ api(project(":verifier:domain"))
implementation(project(":drive:crypto:domain"))
}
diff --git a/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/SplitFileToBlocksAndEncrypt.kt b/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/SplitFileToBlocksAndEncrypt.kt
index cd988fa9..fd9f267e 100644
--- a/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/SplitFileToBlocksAndEncrypt.kt
+++ b/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/SplitFileToBlocksAndEncrypt.kt
@@ -197,6 +197,7 @@ class SplitFileToBlocksAndEncrypt @Inject constructor(
rawSize = rawBlock.size,
size = encryptedBlock.size,
token = "",
+ verifierToken = null,
).also { rawBlock.delete() }
}.getOrThrow()
@@ -231,6 +232,7 @@ class SplitFileToBlocksAndEncrypt @Inject constructor(
rawSize = thumbnail.size.bytes,
size = encryptedUploadThumbnail.size,
token = "",
+ verifierToken = null,
)
}
diff --git a/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/VerifyBlocks.kt b/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/VerifyBlocks.kt
new file mode 100644
index 00000000..753f448f
--- /dev/null
+++ b/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/VerifyBlocks.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core 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.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+package me.proton.core.drive.upload.domain.usecase
+
+import me.proton.android.drive.verifier.domain.usecase.BuildVerifier
+import me.proton.android.drive.verifier.domain.usecase.CleanupVerifier
+import me.proton.core.drive.base.domain.usecase.GetSignatureAddress
+import me.proton.core.drive.base.domain.util.coRunCatching
+import me.proton.core.drive.key.domain.entity.Key
+import me.proton.core.drive.key.domain.usecase.BuildNodeKey
+import me.proton.core.drive.key.domain.usecase.GetNodeKey
+import me.proton.core.drive.linkupload.domain.entity.UploadFileLink
+import me.proton.core.drive.linkupload.domain.usecase.GetUploadBlocks
+import me.proton.core.drive.linkupload.domain.usecase.UpdateVerifierToken
+import javax.inject.Inject
+
+class VerifyBlocks @Inject constructor(
+ private val buildVerifier: BuildVerifier,
+ private val cleanupVerifier: CleanupVerifier,
+ private val getUploadBlocks: GetUploadBlocks,
+ private val getSignatureAddress: GetSignatureAddress,
+ private val getNodeKey: GetNodeKey,
+ private val buildNodeKey: BuildNodeKey,
+ private val updateVerifierToken: UpdateVerifierToken,
+) {
+ suspend operator fun invoke(uploadFileLink: UploadFileLink): Result = coRunCatching {
+ val verifier = buildVerifier(
+ userId = uploadFileLink.userId,
+ shareId = uploadFileLink.shareId.id,
+ linkId = requireNotNull(uploadFileLink.linkId),
+ revisionId = uploadFileLink.draftRevisionId,
+ fileKey = uploadFileLink.buildFileKey(),
+ ).getOrThrow()
+ val uploadBlocks = getUploadBlocks(uploadFileLink)
+ .getOrThrow()
+ .associateBy { uploadBlock -> uploadBlock.file }
+ verifier.verifyBlocks(uploadBlocks.keys.toList())
+ .getOrThrow()
+ .map { (file, verifierToken) ->
+ updateVerifierToken(
+ uploadFileLinkId = uploadFileLink.id,
+ index = requireNotNull(uploadBlocks[file]).index,
+ verifierToken = verifierToken,
+ )
+ }
+ cleanupVerifier(
+ userId = uploadFileLink.userId,
+ shareId = uploadFileLink.shareId.id,
+ linkId = requireNotNull(uploadFileLink.linkId),
+ revisionId = uploadFileLink.draftRevisionId,
+ )
+ }
+
+ private suspend fun UploadFileLink.buildFileKey(): Key.Node =
+ buildNodeKey(
+ userId = userId,
+ parentKey = getNodeKey(parentLinkId).getOrThrow(),
+ uploadFileLink = this,
+ signatureAddress = getSignatureAddress(userId),
+ ).getOrThrow()
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 82fa39d9..6fd49792 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -231,6 +231,7 @@ androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator",
androidx-test-espresso-contrib = { module = "androidx.test.espresso:espresso-contrib", version.ref = "espresso-contrib"}
androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test", version.ref = "androidx-compose" }
androidx-compose-ui-test-junit = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-compose" }
+androidx-work-testing = { module = "androidx.work:work-testing", version.ref = "androidx-work" }
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
junit = { module = "junit:junit", version.ref = "junit" }
mockk-jvm = { module = "io.mockk:mockk", version.ref = "mockk" }
@@ -243,7 +244,7 @@ accompanist = ["accompanist-insets", "accompanist-navigation-animation", "accomp
core = ["core-account", "core-accountManager", "core-auth", "core-challenge", "core-country", "core-crypto", "core-cryptoValidator", "core-data", "core-dataRoom", "core-domain", "core-eventManager", "core-featureFlag", "core-humanVerification", "core-key", "core-keyTransparency", "core-network", "core-observability", "core-payment", "core-payment-iap", "core-plan", "core-report", "core-presentation", "core-presentation-compose", "core-user", "core-userSettings", "core-utilAndroidDagger", "core-utilKotlin"]
core-test = ["core-auth-test", "core-humanVerification-test", "core-report-test"]
test-android = ["junit", "mockk-android", "coroutines-test", "androidx-test-core-ktx", "androidx-test-runner", "androidx-test-rules", "androidx-compose-ui-test", "androidx-compose-ui-test-junit", "androidx-test-uiautomator", "core-test-android-instrumented"]
-test-jvm = ["junit", "mockk-jvm", "coroutines-test", "core-test-kotlin", "core-test-quark", "robolectric"]
+test-jvm = ["junit", "mockk-jvm", "coroutines-test", "androidx-test-core-ktx", "androidx-work-testing", "core-test-kotlin", "core-test-quark", "robolectric"]
[plugins]
proton-detekt = { id = "me.proton.core.gradle-plugins.detekt", version.ref = "proton-detekt-plugin" }
diff --git a/verifier/build.gradle.kts b/verifier/build.gradle.kts
new file mode 100644
index 00000000..338d8993
--- /dev/null
+++ b/verifier/build.gradle.kts
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive 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.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+plugins {
+ id("com.android.library")
+}
+
+android {
+ namespace = "me.proton.android.drive.verifier"
+}
+
+driveModule(includeSubmodules = true)
diff --git a/verifier/data/build.gradle.kts b/verifier/data/build.gradle.kts
new file mode 100644
index 00000000..b3bb3a7b
--- /dev/null
+++ b/verifier/data/build.gradle.kts
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2021-2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core 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.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+plugins {
+ id("com.android.library")
+}
+
+android {
+ namespace = "me.proton.android.drive.verifier.data"
+}
+
+driveModule(
+ hilt = true,
+ serialization = true,
+) {
+ api(project(":verifier:domain"))
+ implementation(project(":drive:base:data"))
+ implementation(project(":drive:crypto:domain"))
+ implementation(project(":drive:i18n"))
+ implementation(libs.retrofit)
+ testImplementation(project(":drive:base:data-test"))
+}
diff --git a/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/VerifierApi.kt b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/VerifierApi.kt
new file mode 100644
index 00000000..e9cc0735
--- /dev/null
+++ b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/VerifierApi.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive 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.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.verifier.data.api
+
+import me.proton.android.drive.verifier.data.api.response.GetVerificationDataResponse
+import me.proton.core.network.data.protonApi.BaseRetrofitApi
+import retrofit2.http.GET
+import retrofit2.http.Path
+
+interface VerifierApi : BaseRetrofitApi {
+
+ @GET("drive/shares/@{enc_shareID}/links/@{enc_linkID}/revisions/@{enc_revisionID}/verification")
+ suspend fun getVerificationData(
+ @Path("enc_shareID") shareId: String,
+ @Path("enc_linkID") linkId: String,
+ @Path("enc_revisionID") revisionId: String,
+ ): GetVerificationDataResponse
+}
diff --git a/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/VerifierApiDataSource.kt b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/VerifierApiDataSource.kt
new file mode 100644
index 00000000..b279b2a6
--- /dev/null
+++ b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/VerifierApiDataSource.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive 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.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.verifier.data.api
+
+import me.proton.android.drive.verifier.data.api.response.GetVerificationDataResponse
+import me.proton.android.drive.verifier.domain.entity.VerificationData
+import me.proton.core.domain.entity.UserId
+import me.proton.core.network.data.ApiProvider
+import me.proton.core.network.domain.ApiException
+
+class VerifierApiDataSource(private val apiProvider: ApiProvider) {
+
+ @Throws(ApiException::class)
+ suspend fun getVerificationData(
+ userId: UserId,
+ shareId: String,
+ linkId: String,
+ revisionId: String,
+ ): GetVerificationDataResponse =
+ apiProvider.get(userId).invoke { getVerificationData(shareId, linkId, revisionId) }.valueOrThrow
+}
diff --git a/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/response/GetVerificationDataResponse.kt b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/response/GetVerificationDataResponse.kt
new file mode 100644
index 00000000..738754cc
--- /dev/null
+++ b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/api/response/GetVerificationDataResponse.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive 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.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.verifier.data.api.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import me.proton.core.drive.base.data.api.Dto.CODE
+import me.proton.core.drive.base.data.api.Dto.CONTENT_KEY_PACKET
+import me.proton.core.drive.base.data.api.Dto.REVISION
+import me.proton.core.drive.base.data.api.Dto.SIGNATURE_ADDRESS
+import me.proton.core.drive.base.data.api.Dto.STATE
+import me.proton.core.drive.base.data.api.Dto.VERIFICATION_CODE
+
+@Serializable
+data class GetVerificationDataResponse(
+ @SerialName(CODE)
+ val code: Long,
+ @SerialName(VERIFICATION_CODE)
+ val verificationCode: String,
+ @SerialName(CONTENT_KEY_PACKET)
+ val contentKeyPacket: String,
+)
diff --git a/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/di/VerifierBindModule.kt b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/di/VerifierBindModule.kt
new file mode 100644
index 00000000..d38e5303
--- /dev/null
+++ b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/di/VerifierBindModule.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive 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.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.verifier.data.di
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import me.proton.android.drive.verifier.data.repository.VerifierRepositoryImpl
+import me.proton.android.drive.verifier.domain.repository.VerifierRepository
+import javax.inject.Singleton
+
+@InstallIn(SingletonComponent::class)
+@Module
+interface VerifierBindModule {
+
+ @Binds
+ @Singleton
+ fun bindsRepositoryImpl(impl: VerifierRepositoryImpl): VerifierRepository
+}
diff --git a/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/di/VerifierModule.kt b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/di/VerifierModule.kt
new file mode 100644
index 00000000..83183a8c
--- /dev/null
+++ b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/di/VerifierModule.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive 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.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.verifier.data.di
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import me.proton.android.drive.verifier.data.api.VerifierApiDataSource
+import me.proton.android.drive.verifier.data.factory.VerifierFactoryImpl
+import me.proton.android.drive.verifier.domain.factory.VerifierFactory
+import me.proton.core.drive.base.domain.usecase.GetCacheTempFolder
+import me.proton.core.drive.crypto.domain.usecase.file.DecryptFiles
+import me.proton.core.network.data.ApiProvider
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object VerifierModule {
+ @Singleton
+ @Provides
+ fun provideVerifierApiDataSource(apiProvider: ApiProvider) =
+ VerifierApiDataSource(apiProvider)
+
+ @Singleton
+ @Provides
+ fun provideVerifierFactory(
+ decryptFiles: DecryptFiles,
+ getCacheTempFolder: GetCacheTempFolder,
+ ): VerifierFactory =
+ VerifierFactoryImpl(decryptFiles, getCacheTempFolder)
+}
diff --git a/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/entity/VerifierImpl.kt b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/entity/VerifierImpl.kt
new file mode 100644
index 00000000..9810bf20
--- /dev/null
+++ b/verifier/data/src/main/kotlin/me/proton/android/drive/verifier/data/entity/VerifierImpl.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive 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.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.verifier.data.entity
+
+import me.proton.android.drive.verifier.domain.exception.VerifierException
+import me.proton.android.drive.verifier.data.extension.head
+import me.proton.android.drive.verifier.data.extension.xor
+import me.proton.android.drive.verifier.domain.entity.Verifier
+import me.proton.core.drive.base.domain.extension.bytes
+import me.proton.core.drive.crypto.domain.usecase.file.DecryptFiles
+import me.proton.core.drive.key.domain.entity.ContentKey
+import java.io.File
+import java.util.UUID
+
+internal class VerifierImpl constructor(
+ private val decryptFiles: DecryptFiles,
+ private val contentKey: ContentKey,
+ private val verificationCode: ByteArray,
+ private val tempFolder: File
+) : Verifier {
+
+ init {
+ require(verificationCode.size == VERIFICATION_CODE_SIZE) {
+ "Invalid verification code size"
+ }
+ }
+
+ override suspend fun verifyBlocks(blocks: List): Result