@@ -12,6 +12,7 @@ import kotlinx.coroutines.withContext
1212import org.greenrobot.eventbus.EventBus
1313import org.signal.core.util.Base64
1414import org.signal.core.util.EventTimer
15+ import org.signal.core.util.fullWalCheckpoint
1516import org.signal.core.util.logging.Log
1617import org.signal.core.util.money.FiatMoney
1718import org.signal.core.util.withinTransaction
@@ -43,6 +44,8 @@ import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter
4344import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
4445import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeature
4546import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
47+ import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
48+ import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
4649import org.thoughtcrime.securesms.database.DistributionListTables
4750import org.thoughtcrime.securesms.database.SignalDatabase
4851import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
@@ -71,6 +74,7 @@ import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
7174import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec
7275import java.io.ByteArrayOutputStream
7376import java.io.File
77+ import java.io.IOException
7478import java.io.InputStream
7579import java.io.OutputStream
7680import java.math.BigDecimal
@@ -83,6 +87,7 @@ object BackupRepository {
8387
8488 private val TAG = Log .tag(BackupRepository ::class .java)
8589 private const val VERSION = 1L
90+ private const val DB_SNAPSHOT_NAME = " signal-snapshot.db"
8691
8792 private val resetInitializedStateErrorAction: StatusCodeErrorAction = { error ->
8893 when (error.code) {
@@ -105,64 +110,106 @@ object BackupRepository {
105110 SignalStore .backup().backupTier = null
106111 }
107112
108- fun export (outputStream : OutputStream , append : (ByteArray ) -> Unit , plaintext : Boolean = false) {
109- val eventTimer = EventTimer ()
110- val writer: BackupExportWriter = if (plaintext) {
111- PlainTextBackupWriter (outputStream)
112- } else {
113- EncryptedBackupWriter (
114- key = SignalStore .svr().getOrCreateMasterKey().deriveBackupKey(),
115- aci = SignalStore .account().aci!! ,
116- outputStream = outputStream,
117- append = append
113+ private fun createSignalDatabaseSnapshot (): SignalDatabase {
114+ // Need to do a WAL checkpoint to ensure that the database file we're copying has all pending writes
115+ if (! SignalDatabase .rawDatabase.fullWalCheckpoint()) {
116+ Log .w(TAG , " Failed to checkpoint WAL! Not guaranteed to be using the most recent data." )
117+ }
118+
119+ // We make a copy of the database within a transaction to ensure that no writes occur while we're copying the file
120+ return SignalDatabase .rawDatabase.withinTransaction {
121+ val context = AppDependencies .application
122+
123+ val existingDbFile = context.getDatabasePath(SignalDatabase .DATABASE_NAME )
124+ val targetFile = File (existingDbFile.parentFile, DB_SNAPSHOT_NAME )
125+
126+ try {
127+ existingDbFile.copyTo(targetFile, overwrite = true )
128+ } catch (e: IOException ) {
129+ // TODO [backup] Gracefully handle this error
130+ throw IllegalStateException (" Failed to copy database file!" , e)
131+ }
132+
133+ SignalDatabase (
134+ context = context,
135+ databaseSecret = DatabaseSecretProvider .getOrCreateDatabaseSecret(context),
136+ attachmentSecret = AttachmentSecretProvider .getInstance(context).getOrCreateAttachmentSecret(),
137+ name = DB_SNAPSHOT_NAME
118138 )
119139 }
140+ }
141+
142+ private fun deleteDatabaseSnapshot () {
143+ val targetFile = AppDependencies .application.getDatabasePath(DB_SNAPSHOT_NAME )
144+ if (! targetFile.delete()) {
145+ Log .w(TAG , " Failed to delete database snapshot!" )
146+ }
147+ }
120148
121- val exportState = ExportState (backupTime = System .currentTimeMillis(), allowMediaBackup = SignalStore .backup().backsUpMedia)
149+ fun export (outputStream : OutputStream , append : (ByteArray ) -> Unit , plaintext : Boolean = false) {
150+ val eventTimer = EventTimer ()
151+ val dbSnapshot: SignalDatabase = createSignalDatabaseSnapshot()
122152
123- writer.use {
124- writer.write(
125- BackupInfo (
126- version = VERSION ,
127- backupTimeMs = exportState.backupTime
153+ try {
154+ val writer: BackupExportWriter = if (plaintext) {
155+ PlainTextBackupWriter (outputStream)
156+ } else {
157+ EncryptedBackupWriter (
158+ key = SignalStore .svr().getOrCreateMasterKey().deriveBackupKey(),
159+ aci = SignalStore .account().aci!! ,
160+ outputStream = outputStream,
161+ append = append
128162 )
129- )
130- // Note: Without a transaction, we may export inconsistent state. But because we have a transaction,
131- // writes from other threads are blocked. This is something to think more about.
132- SignalDatabase .rawDatabase.withinTransaction {
133- AccountDataProcessor .export {
134- writer.write(it)
135- eventTimer.emit(" account" )
136- }
163+ }
137164
138- RecipientBackupProcessor .export(exportState) {
139- writer.write(it)
140- eventTimer.emit(" recipient" )
141- }
165+ val exportState = ExportState (backupTime = System .currentTimeMillis(), allowMediaBackup = SignalStore .backup().backsUpMedia)
142166
143- ChatBackupProcessor .export(exportState) { frame ->
144- writer.write(frame)
145- eventTimer.emit(" thread" )
146- }
167+ writer.use {
168+ writer.write(
169+ BackupInfo (
170+ version = VERSION ,
171+ backupTimeMs = exportState.backupTime
172+ )
173+ )
174+ // Note: Without a transaction, we may export inconsistent state. But because we have a transaction,
175+ // writes from other threads are blocked. This is something to think more about.
176+ dbSnapshot.rawWritableDatabase.withinTransaction {
177+ AccountDataProcessor .export(dbSnapshot) {
178+ writer.write(it)
179+ eventTimer.emit(" account" )
180+ }
147181
148- AdHocCallBackupProcessor .export { frame ->
149- writer.write(frame )
150- eventTimer.emit(" call " )
151- }
182+ RecipientBackupProcessor .export(dbSnapshot, exportState) {
183+ writer.write(it )
184+ eventTimer.emit(" recipient " )
185+ }
152186
153- StickerBackupProcessor .export { frame ->
154- writer.write(frame)
155- eventTimer.emit(" sticker-pack " )
156- }
187+ ChatBackupProcessor .export(dbSnapshot, exportState) { frame ->
188+ writer.write(frame)
189+ eventTimer.emit(" thread " )
190+ }
157191
158- ChatItemBackupProcessor .export(exportState) { frame ->
159- writer.write(frame)
160- eventTimer.emit(" message" )
192+ AdHocCallBackupProcessor .export(dbSnapshot) { frame ->
193+ writer.write(frame)
194+ eventTimer.emit(" call" )
195+ }
196+
197+ StickerBackupProcessor .export(dbSnapshot) { frame ->
198+ writer.write(frame)
199+ eventTimer.emit(" sticker-pack" )
200+ }
201+
202+ ChatItemBackupProcessor .export(dbSnapshot, exportState) { frame ->
203+ writer.write(frame)
204+ eventTimer.emit(" message" )
205+ }
161206 }
162207 }
163- }
164208
165- Log .d(TAG , " export() ${eventTimer.stop().summary} " )
209+ Log .d(TAG , " export() ${eventTimer.stop().summary} " )
210+ } finally {
211+ deleteDatabaseSnapshot()
212+ }
166213 }
167214
168215 fun export (plaintext : Boolean = false): ByteArray {
0 commit comments