feat: Update database version to 10 and implement migration to clear large blob attachments; adjust blob handling for IMAGE/FILE types in MessageRepository and ChatViewModel
This commit is contained in:
@@ -828,7 +828,8 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
for (attachment in attachments) {
|
for (attachment in attachments) {
|
||||||
val jsonObj = JSONObject()
|
val jsonObj = JSONObject()
|
||||||
|
|
||||||
// Для MESSAGES типа расшифровываем и re-encrypt
|
// ⚠️ НЕ сохраняем blob для IMAGE/FILE - слишком большие (SQLite CursorWindow 2MB limit)
|
||||||
|
// Только MESSAGES (reply) сохраняем - они небольшие
|
||||||
if (attachment.type == AttachmentType.MESSAGES && attachment.blob.isNotEmpty()) {
|
if (attachment.type == AttachmentType.MESSAGES && attachment.blob.isNotEmpty()) {
|
||||||
try {
|
try {
|
||||||
// 1. Расшифровываем с ChaCha ключом сообщения
|
// 1. Расшифровываем с ChaCha ключом сообщения
|
||||||
@@ -842,7 +843,7 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
// 2. Re-encrypt с приватным ключом для хранения (как в Desktop Архиве)
|
// 2. Re-encrypt с приватным ключом для хранения (как в Desktop Архиве)
|
||||||
val reEncryptedBlob = CryptoManager.encryptWithPassword(decryptedBlob, privateKey)
|
val reEncryptedBlob = CryptoManager.encryptWithPassword(decryptedBlob, privateKey)
|
||||||
|
|
||||||
// 3. Сохраняем ЗАШИФРОВАННЫЙ blob в БД
|
// 3. Сохраняем ЗАШИФРОВАННЫЙ blob в БД (только для MESSAGES - они небольшие)
|
||||||
jsonObj.put("id", attachment.id)
|
jsonObj.put("id", attachment.id)
|
||||||
jsonObj.put("blob", reEncryptedBlob) // 🔒 Зашифрован приватным ключом!
|
jsonObj.put("blob", reEncryptedBlob) // 🔒 Зашифрован приватным ключом!
|
||||||
jsonObj.put("type", attachment.type.value)
|
jsonObj.put("type", attachment.type.value)
|
||||||
@@ -850,27 +851,27 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
jsonObj.put("width", attachment.width)
|
jsonObj.put("width", attachment.width)
|
||||||
jsonObj.put("height", attachment.height)
|
jsonObj.put("height", attachment.height)
|
||||||
} else {
|
} else {
|
||||||
// Fallback - сохраняем как есть
|
// Fallback - пустой blob для IMAGE/FILE
|
||||||
jsonObj.put("id", attachment.id)
|
jsonObj.put("id", attachment.id)
|
||||||
jsonObj.put("blob", attachment.blob)
|
jsonObj.put("blob", "")
|
||||||
jsonObj.put("type", attachment.type.value)
|
jsonObj.put("type", attachment.type.value)
|
||||||
jsonObj.put("preview", attachment.preview)
|
jsonObj.put("preview", attachment.preview)
|
||||||
jsonObj.put("width", attachment.width)
|
jsonObj.put("width", attachment.width)
|
||||||
jsonObj.put("height", attachment.height)
|
jsonObj.put("height", attachment.height)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Fallback - сохраняем как есть
|
// Fallback - пустой blob
|
||||||
jsonObj.put("id", attachment.id)
|
jsonObj.put("id", attachment.id)
|
||||||
jsonObj.put("blob", attachment.blob)
|
jsonObj.put("blob", "")
|
||||||
jsonObj.put("type", attachment.type.value)
|
jsonObj.put("type", attachment.type.value)
|
||||||
jsonObj.put("preview", attachment.preview)
|
jsonObj.put("preview", attachment.preview)
|
||||||
jsonObj.put("width", attachment.width)
|
jsonObj.put("width", attachment.width)
|
||||||
jsonObj.put("height", attachment.height)
|
jsonObj.put("height", attachment.height)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Для других типов сохраняем как есть
|
// Для IMAGE/FILE - НЕ сохраняем blob (пустой)
|
||||||
jsonObj.put("id", attachment.id)
|
jsonObj.put("id", attachment.id)
|
||||||
jsonObj.put("blob", attachment.blob)
|
jsonObj.put("blob", "") // Пустой blob для IMAGE/FILE
|
||||||
jsonObj.put("type", attachment.type.value)
|
jsonObj.put("type", attachment.type.value)
|
||||||
jsonObj.put("preview", attachment.preview)
|
jsonObj.put("preview", attachment.preview)
|
||||||
jsonObj.put("width", attachment.width)
|
jsonObj.put("width", attachment.width)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
BlacklistEntity::class,
|
BlacklistEntity::class,
|
||||||
AvatarCacheEntity::class
|
AvatarCacheEntity::class
|
||||||
],
|
],
|
||||||
version = 9,
|
version = 10,
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
abstract class RosettaDatabase : RoomDatabase() {
|
abstract class RosettaDatabase : RoomDatabase() {
|
||||||
@@ -84,6 +84,21 @@ abstract class RosettaDatabase : RoomDatabase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔥 МИГРАЦИЯ 9->10: Повторная очистка blob из attachments
|
||||||
|
* Для пользователей которые уже были на версии 9
|
||||||
|
*/
|
||||||
|
private val MIGRATION_9_10 = object : Migration(9, 10) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
// Очищаем все attachments с большими blob'ами
|
||||||
|
database.execSQL("""
|
||||||
|
UPDATE messages
|
||||||
|
SET attachments = '[]'
|
||||||
|
WHERE length(attachments) > 10000
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getDatabase(context: Context): RosettaDatabase {
|
fun getDatabase(context: Context): RosettaDatabase {
|
||||||
return INSTANCE ?: synchronized(this) {
|
return INSTANCE ?: synchronized(this) {
|
||||||
val instance = Room.databaseBuilder(
|
val instance = Room.databaseBuilder(
|
||||||
@@ -92,7 +107,7 @@ abstract class RosettaDatabase : RoomDatabase() {
|
|||||||
"rosetta_secure.db"
|
"rosetta_secure.db"
|
||||||
)
|
)
|
||||||
.setJournalMode(JournalMode.WRITE_AHEAD_LOGGING) // WAL mode for performance
|
.setJournalMode(JournalMode.WRITE_AHEAD_LOGGING) // WAL mode for performance
|
||||||
.addMigrations(MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
|
.addMigrations(MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10)
|
||||||
.fallbackToDestructiveMigration() // Для разработки - только если миграция не найдена
|
.fallbackToDestructiveMigration() // Для разработки - только если миграция не найдена
|
||||||
.build()
|
.build()
|
||||||
INSTANCE = instance
|
INSTANCE = instance
|
||||||
|
|||||||
@@ -1268,7 +1268,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
updateMessageStatus(messageId, MessageStatus.SENT)
|
updateMessageStatus(messageId, MessageStatus.SENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 💾 Сохранение в БД с attachments (зашифрованный blob для MESSAGES)
|
// 4. 💾 Сохранение в БД с attachments
|
||||||
|
// ⚠️ НЕ сохраняем blob для IMAGE/FILE - слишком большие (SQLite CursorWindow 2MB limit)
|
||||||
|
// Только MESSAGES (reply) сохраняем - они небольшие
|
||||||
val attachmentsJson = if (messageAttachments.isNotEmpty()) {
|
val attachmentsJson = if (messageAttachments.isNotEmpty()) {
|
||||||
JSONArray().apply {
|
JSONArray().apply {
|
||||||
messageAttachments.forEach { att ->
|
messageAttachments.forEach { att ->
|
||||||
@@ -1276,8 +1278,13 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
put("id", att.id)
|
put("id", att.id)
|
||||||
put("type", att.type.value)
|
put("type", att.type.value)
|
||||||
put("preview", att.preview)
|
put("preview", att.preview)
|
||||||
// 🔥 Для MESSAGES сохраняем зашифрованный приватным ключом, для остальных - как есть
|
// Только для MESSAGES сохраняем blob (reply data небольшие)
|
||||||
put("blob", if (att.type == AttachmentType.MESSAGES) replyBlobForDatabase else att.blob)
|
// Для IMAGE/FILE - пустой blob
|
||||||
|
val blobToSave = when (att.type) {
|
||||||
|
AttachmentType.MESSAGES -> replyBlobForDatabase ?: ""
|
||||||
|
else -> "" // IMAGE, FILE - не сохраняем blob
|
||||||
|
}
|
||||||
|
put("blob", blobToSave)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}.toString()
|
}.toString()
|
||||||
@@ -1397,13 +1404,14 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
updateMessageStatus(messageId, MessageStatus.SENT)
|
updateMessageStatus(messageId, MessageStatus.SENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сохраняем в БД с зашифрованным blob'ом
|
// ⚠️ НЕ сохраняем blob в БД - он слишком большой (SQLite CursorWindow 2MB limit)
|
||||||
|
// Изображение должно храниться в файловой системе или загружаться с сервера при необходимости
|
||||||
val attachmentsJson = JSONArray().apply {
|
val attachmentsJson = JSONArray().apply {
|
||||||
put(JSONObject().apply {
|
put(JSONObject().apply {
|
||||||
put("id", imageAttachment.id)
|
put("id", imageAttachment.id)
|
||||||
put("type", AttachmentType.IMAGE.value)
|
put("type", AttachmentType.IMAGE.value)
|
||||||
put("preview", blurhash)
|
put("preview", blurhash)
|
||||||
put("blob", imageBlobForDatabase)
|
put("blob", "") // Пустой blob - не сохраняем в БД!
|
||||||
})
|
})
|
||||||
}.toString()
|
}.toString()
|
||||||
|
|
||||||
@@ -1521,12 +1529,14 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
updateMessageStatus(messageId, MessageStatus.SENT)
|
updateMessageStatus(messageId, MessageStatus.SENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ⚠️ НЕ сохраняем blob в БД - он слишком большой (SQLite CursorWindow 2MB limit)
|
||||||
|
// Файл должен храниться в файловой системе или загружаться с сервера при необходимости
|
||||||
val attachmentsJson = JSONArray().apply {
|
val attachmentsJson = JSONArray().apply {
|
||||||
put(JSONObject().apply {
|
put(JSONObject().apply {
|
||||||
put("id", fileAttachment.id)
|
put("id", fileAttachment.id)
|
||||||
put("type", AttachmentType.FILE.value)
|
put("type", AttachmentType.FILE.value)
|
||||||
put("preview", preview)
|
put("preview", preview)
|
||||||
put("blob", fileBlobForDatabase)
|
put("blob", "") // Пустой blob - не сохраняем в БД!
|
||||||
})
|
})
|
||||||
}.toString()
|
}.toString()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user