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:
k1ngsterr1
2026-01-26 17:19:30 +05:00
parent 7f3edd2ad8
commit 44c0151294
3 changed files with 42 additions and 16 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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()