Fix foreign key constraint issues with backup restore.

This commit is contained in:
Greyson Parrelli
2023-04-25 15:52:09 -04:00
committed by GitHub
parent 0e631508b2
commit a01fb7ff1c
7 changed files with 104 additions and 27 deletions

View File

@@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -80,6 +81,7 @@ public class FullBackupImporter extends FullBackupBase {
SQLiteDatabase keyValueDatabase = KeyValueDatabase.getInstance(ApplicationDependencies.getApplication()).getSqlCipherDatabase();
db.setForeignKeyConstraintsEnabled(false);
db.beginTransaction();
keyValueDatabase.beginTransaction();
try {
@@ -94,7 +96,7 @@ public class FullBackupImporter extends FullBackupBase {
count++;
if (frame.version != null) processVersion(db, frame.version);
else if (frame.statement != null) tryProcessStatement(db, frame.statement);
else if (frame.statement != null) processStatement(db, frame.statement);
else if (frame.preference != null) processPreference(context, frame.preference);
else if (frame.attachment != null) processAttachment(context, attachmentSecret, db, frame.attachment, inputStream);
else if (frame.sticker != null) processSticker(context, attachmentSecret, db, frame.sticker, inputStream);
@@ -106,8 +108,20 @@ public class FullBackupImporter extends FullBackupBase {
db.setTransactionSuccessful();
keyValueDatabase.setTransactionSuccessful();
} finally {
List<SqlUtil.ForeignKeyViolation> violations = SqlUtil.getForeignKeyViolations(db)
.stream()
.filter(it -> !it.getTable().startsWith("msl_"))
.collect(Collectors.toList());
if (violations.size() > 0) {
Log.w(TAG, "Foreign key constraints failed!\n" + Util.join(violations, "\n"));
//noinspection ThrowFromFinallyBlock
throw new ForeignKeyViolationException(violations);
}
db.endTransaction();
keyValueDatabase.endTransaction();
db.setForeignKeyConstraintsEnabled(true);
}
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, count, 0));
@@ -129,31 +143,6 @@ public class FullBackupImporter extends FullBackupBase {
db.setVersion(version.version);
}
private static void tryProcessStatement(@NonNull SQLiteDatabase db, SqlStatement statement) {
try {
processStatement(db, statement);
} catch (SQLiteConstraintException e) {
String tableName = "?";
String statementString = statement.statement;
if (statementString != null && statementString.startsWith("INSERT INTO ")) {
int nameStart = "INSERT INTO ".length();
int nameEnd = statementString.indexOf(" ", "INSERT INTO ".length());
if (nameStart < statementString.length() && nameEnd > nameStart) {
tableName = statementString.substring(nameStart, nameEnd);
}
}
if (tableName.startsWith("msl_")) {
Log.w(TAG, "Constraint failed when inserting into " + tableName + ". Ignoring.");
} else {
Log.w(TAG, "Constraint failed when inserting into " + tableName + ". Throwing!");
throw e;
}
}
}
private static void processStatement(@NonNull SQLiteDatabase db, SqlStatement statement) {
if (statement.statement == null) {
Log.w(TAG, "Null statement!");
@@ -373,4 +362,19 @@ public class FullBackupImporter extends FullBackupBase {
super("Tried to import a backup with version " + backupVersion + " into a database with version " + currentVersion);
}
}
public static class ForeignKeyViolationException extends IOException {
public ForeignKeyViolationException(List<SqlUtil.ForeignKeyViolation> violations) {
super(buildMessage(violations));
}
private static String buildMessage(List<SqlUtil.ForeignKeyViolation> violations) {
Set<String> unique = violations
.stream()
.map(it -> it.getTable() + " -> " + it.getColumn())
.collect(Collectors.toSet());
return Util.join(unique, ", ");
}
}
}

View File

@@ -218,7 +218,11 @@ object V185_MessageRecipientsAndEditMessageMigration : SignalDatabaseMigration {
}
stopwatch.split("recreate-dependents")
db.execSQL("PRAGMA foreign_key_check")
val foreignKeyViolations: List<SqlUtil.ForeignKeyViolation> = SqlUtil.getForeignKeyViolations(db, "message")
if (foreignKeyViolations.isNotEmpty()) {
Log.w(TAG, "Foreign key violations!\n${foreignKeyViolations.joinToString(separator = "\n")}")
throw IllegalStateException("Foreign key violations!")
}
stopwatch.split("fk-check")
stopwatch.stop(TAG)

View File

@@ -58,6 +58,9 @@ final class NewDeviceServerTask implements ServerTask {
} catch (FullBackupImporter.DatabaseDowngradeException e) {
Log.w(TAG, "Failed due to the backup being from a newer version of Signal.", e);
EventBus.getDefault().post(new Status(0, Status.State.FAILURE_VERSION_DOWNGRADE));
} catch (FullBackupImporter.ForeignKeyViolationException e) {
Log.w(TAG, "Failed due to foreign key constraint violations.", e);
EventBus.getDefault().post(new Status(0, Status.State.FAILURE_FOREIGN_KEY));
} catch (IOException e) {
Log.w(TAG, e);
EventBus.getDefault().post(new Status(0, Status.State.FAILURE_UNKNOWN));
@@ -99,6 +102,7 @@ final class NewDeviceServerTask implements ServerTask {
IN_PROGRESS,
SUCCESS,
FAILURE_VERSION_DOWNGRADE,
FAILURE_FOREIGN_KEY,
FAILURE_UNKNOWN
}
}

View File

@@ -66,6 +66,9 @@ public final class NewDeviceTransferFragment extends DeviceTransferFragment {
case FAILURE_VERSION_DOWNGRADE:
abort(R.string.NewDeviceTransfer__cannot_transfer_from_a_newer_version_of_signal);
break;
case FAILURE_FOREIGN_KEY:
abort(R.string.NewDeviceTransfer__failure_foreign_key);
break;
case FAILURE_UNKNOWN:
abort();
break;

View File

@@ -303,6 +303,9 @@ public final class RestoreBackupFragment extends LoggingFragment {
} catch (FullBackupImporter.DatabaseDowngradeException e) {
Log.w(TAG, "Failed due to the backup being from a newer version of Signal.", e);
return BackupImportResult.FAILURE_VERSION_DOWNGRADE;
} catch (FullBackupImporter.ForeignKeyViolationException e) {
Log.w(TAG, "Failed due to foreign key constraint violations.", e);
return BackupImportResult.FAILURE_FOREIGN_KEY;
} catch (IOException e) {
Log.w(TAG, e);
return BackupImportResult.FAILURE_UNKNOWN;
@@ -324,6 +327,9 @@ public final class RestoreBackupFragment extends LoggingFragment {
case FAILURE_VERSION_DOWNGRADE:
Toast.makeText(context, R.string.RegistrationActivity_backup_failure_downgrade, Toast.LENGTH_LONG).show();
break;
case FAILURE_FOREIGN_KEY:
Toast.makeText(context, R.string.RegistrationActivity_backup_failure_foreign_key, Toast.LENGTH_LONG).show();
break;
case FAILURE_UNKNOWN:
Toast.makeText(context, R.string.RegistrationActivity_incorrect_backup_passphrase, Toast.LENGTH_LONG).show();
break;
@@ -415,6 +421,7 @@ public final class RestoreBackupFragment extends LoggingFragment {
private enum BackupImportResult {
SUCCESS,
FAILURE_VERSION_DOWNGRADE,
FAILURE_FOREIGN_KEY,
FAILURE_UNKNOWN
}