Skip to content

Commit b015321

Browse files
committed
Halt file backup when switched to metered network
1 parent d36397a commit b015321

File tree

10 files changed

+134
-17
lines changed

10 files changed

+134
-17
lines changed

app/src/main/java/com/stevesoltys/seedvault/backend/BackendManager.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ class BackendManager(
110110
* @return true if a backup is possible, false if not.
111111
*/
112112
@WorkerThread
113-
fun canDoBackupNow(): Boolean {
113+
override fun canDoBackupNow(): Boolean {
114114
val storage = backendProperties ?: return false
115115
return !isOnUnavailableUsb() &&
116116
!storage.isUnavailableNetwork(context, settingsManager.useMeteredNetwork)

core/src/main/java/org/calyxos/seedvault/core/backends/IBackendManager.kt

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public interface IBackendManager {
99
public val backend: Backend
1010
public val isOnRemovableDrive: Boolean
1111
public val requiresNetwork: Boolean
12+
public fun canDoBackupNow(): Boolean
1213
}
1314

1415
public enum class BackendId {

storage/demo/src/main/java/de/grobox/storagebackuptester/App.kt

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class App : Application() {
2525
override val backend: Backend get() = plugin
2626
override val isOnRemovableDrive: Boolean = false
2727
override val requiresNetwork: Boolean = false
28+
override fun canDoBackupNow(): Boolean = true
2829
}
2930
val settingsManager: SettingsManager by lazy { SettingsManager(applicationContext) }
3031
val storageBackup: StorageBackup by lazy {

storage/lib/src/main/java/org/calyxos/backup/storage/backup/Backup.kt

+20-2
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ internal class Backup(
100100

101101
@Throws(IOException::class, GeneralSecurityException::class)
102102
suspend fun runBackup(backupObserver: BackupObserver?) {
103+
if (!backendManager.canDoBackupNow()) {
104+
Log.w(TAG, "runBackup(): Can't do backup right now, aborting...")
105+
throw IOException("Metered Network")
106+
}
103107
backupObserver?.onStartScanning()
104108
var duration: Duration? = null
105109
try {
@@ -142,15 +146,29 @@ internal class Backup(
142146
availableChunkIds: Set<String>,
143147
backupObserver: BackupObserver?,
144148
) {
149+
if (!backendManager.canDoBackupNow()) {
150+
Log.w(TAG, "backupFiles(): Can't do backup right now, aborting...")
151+
throw IOException("Metered Network")
152+
}
153+
val wasAborted = { !backendManager.canDoBackupNow() }
145154
val startTime = System.currentTimeMillis()
146155
val numSmallFiles = filesResult.smallFiles.size
147156
val smallResult = measure("Backing up $numSmallFiles small files") {
148-
smallFileBackup.backupFiles(filesResult.smallFiles, availableChunkIds, backupObserver)
157+
smallFileBackup.backupFiles(
158+
files = filesResult.smallFiles,
159+
availableChunkIds = availableChunkIds,
160+
wasAborted = wasAborted,
161+
backupObserver = backupObserver,
162+
)
163+
}
164+
if (!backendManager.canDoBackupNow()) {
165+
Log.w(TAG, "backupFiles(): Can't do backup right now, aborting...")
166+
throw IOException("Metered Network")
149167
}
150168
MemoryLogger.log()
151169
val numLargeFiles = filesResult.files.size
152170
val largeResult = measure("Backing up $numLargeFiles files") {
153-
fileBackup.backupFiles(filesResult.files, availableChunkIds, backupObserver)
171+
fileBackup.backupFiles(filesResult.files, availableChunkIds, wasAborted, backupObserver)
154172
}
155173
MemoryLogger.log()
156174
val result = largeResult + smallResult

storage/lib/src/main/java/org/calyxos/backup/storage/backup/FileBackup.kt

+2
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@ internal class FileBackup(
3434
suspend fun backupFiles(
3535
files: List<ContentFile>,
3636
availableChunkIds: Set<String>,
37+
wasAborted: () -> Boolean,
3738
backupObserver: BackupObserver?,
3839
): BackupResult {
3940
val chunkIds = HashSet<String>()
4041
val backupMediaFiles = ArrayList<BackupMediaFile>()
4142
val backupDocumentFiles = ArrayList<BackupDocumentFile>()
4243
var bytesWritten = 0L
4344
files.forEach { file ->
45+
if (wasAborted()) throw IOException("Metered Network")
4446
val result = try {
4547
backupFile(file, availableChunkIds)
4648
} catch (e: IOException) {

storage/lib/src/main/java/org/calyxos/backup/storage/backup/SmallFileBackup.kt

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ internal class SmallFileBackup(
3333
suspend fun backupFiles(
3434
files: List<ContentFile>,
3535
availableChunkIds: Set<String>,
36+
wasAborted: () -> Boolean,
3637
backupObserver: BackupObserver?,
3738
): BackupResult {
3839
val chunkIds = HashSet<String>()
@@ -66,6 +67,7 @@ internal class SmallFileBackup(
6667
} else true
6768
}
6869
changedFiles.windowed(2, 1, true).forEach { window ->
70+
if (wasAborted()) throw IOException("Metered Network")
6971
val file = window[0]
7072
val result = try {
7173
makeZipChunk(window, missingChunkIds)

storage/lib/src/test/java/org/calyxos/backup/storage/BackupRestoreTest.kt

+2
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ internal class BackupRestoreTest {
9292
mockkStatic("org.calyxos.backup.storage.UriUtilsKt")
9393

9494
every { backendManager.backend } returns backend
95+
every { backendManager.canDoBackupNow() } returns true
9596
every { db.getFilesCache() } returns filesCache
9697
every { db.getChunksCache() } returns chunksCache
9798
every { keyManager.getMainKey() } returns SecretKeySpec(
@@ -526,6 +527,7 @@ internal class BackupRestoreTest {
526527
keyManager = keyManager,
527528
cacheRepopulater = cacheRepopulater,
528529
)
530+
every { backendManagerNew.canDoBackupNow() } returns true
529531
every { backendManagerNew.backend } returnsMany listOf(backend1, backend2)
530532

531533
coEvery { backend1.list(any(), Blob::class, callback = any()) } just Runs

storage/lib/src/test/java/org/calyxos/backup/storage/backup/BackupTest.kt

+81-7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import io.mockk.just
1717
import io.mockk.mockk
1818
import io.mockk.mockkStatic
1919
import io.mockk.slot
20+
import io.mockk.verify
2021
import kotlinx.coroutines.runBlocking
2122
import org.calyxos.backup.storage.backup.Backup.Companion.CHUNK_SIZE_MAX
2223
import org.calyxos.backup.storage.db.CachedChunk
@@ -37,13 +38,15 @@ import org.calyxos.seedvault.core.crypto.CoreCrypto.ALGORITHM_HMAC
3738
import org.calyxos.seedvault.core.crypto.CoreCrypto.KEY_SIZE_BYTES
3839
import org.calyxos.seedvault.core.crypto.KeyManager
3940
import org.junit.Assert.assertArrayEquals
41+
import org.junit.Assert.assertEquals
42+
import org.junit.Assert.assertThrows
4043
import org.junit.Assert.assertTrue
4144
import org.junit.Test
4245
import java.io.ByteArrayInputStream
4346
import java.io.ByteArrayOutputStream
47+
import java.io.IOException
4448
import javax.crypto.spec.SecretKeySpec
4549
import kotlin.random.Random
46-
import kotlin.test.assertEquals
4750

4851
internal class BackupTest {
4952

@@ -97,12 +100,7 @@ internal class BackupTest {
97100
)
98101

99102
// preliminaries find the file above
100-
coEvery { backend.list(any(), Blob::class, callback = any()) } just Runs
101-
every { chunksCache.areAllAvailableChunksCached(emptySet()) } returns true
102-
every { fileScanner.getFiles() } returns scannedFiles
103-
every { filesCache.getByUri(any()) } returns null // nothing is cached, all is new
104-
every { chunksCache.get(any()) } returns null // no chunks are cached, all are new
105-
every { chunksCache.hasCorruptedChunks(any()) } returns false // no chunks corrupted
103+
prepareBackup(scannedFiles)
106104

107105
// backup file and save its blob
108106
every {
@@ -143,4 +141,80 @@ internal class BackupTest {
143141
assertEquals(size2, outputStream2.size().toLong())
144142
}
145143

144+
@Test
145+
fun testAbortBackupEarly() {
146+
every { backendManager.canDoBackupNow() } returns false
147+
148+
val e = assertThrows(IOException::class.java) {
149+
runBlocking {
150+
backup.runBackup(null)
151+
}
152+
}
153+
assertEquals("Metered Network", e.message)
154+
}
155+
156+
@Test
157+
fun testAbortBackupBeforeSmallFiles() {
158+
// define one file in backup
159+
val fileMBytes = Random.nextBytes(Random.nextInt(1, CHUNK_SIZE_MAX))
160+
val fileM = getRandomDocFile(fileMBytes.size)
161+
val scannedFiles = FileScannerResult(
162+
smallFiles = listOf(fileM),
163+
files = listOf(fileM),
164+
)
165+
166+
prepareBackup(scannedFiles)
167+
every { backendManager.canDoBackupNow() } returns true andThen false
168+
every {
169+
contentResolver.openInputStream(fileM.uri)
170+
} returns ByteArrayInputStream(fileMBytes)
171+
172+
val e = assertThrows(IOException::class.java) {
173+
runBlocking {
174+
backup.runBackup(null)
175+
}
176+
}
177+
assertEquals("Metered Network", e.message)
178+
}
179+
180+
@Test
181+
fun testAbortBackupBeforeLargeFiles() {
182+
// define one file in backup
183+
val fileMBytes = Random.nextBytes(Random.nextInt(1, CHUNK_SIZE_MAX))
184+
val fileM = getRandomDocFile(fileMBytes.size)
185+
val scannedFiles = FileScannerResult(
186+
smallFiles = listOf(fileM),
187+
files = listOf(fileM),
188+
)
189+
190+
prepareBackup(scannedFiles)
191+
every { backendManager.canDoBackupNow() } returnsMany listOf(true, true, true, false)
192+
every {
193+
contentResolver.openInputStream(fileM.uri)
194+
} returns ByteArrayInputStream(fileMBytes)
195+
coEvery { backend.save(match { it is Blob }, any()) } returns 42L
196+
every { chunksCache.insert(any<CachedChunk>()) } just Runs
197+
every { filesCache.upsert(any()) } just Runs
198+
199+
val e = assertThrows(IOException::class.java) {
200+
runBlocking {
201+
backup.runBackup(null)
202+
}
203+
}
204+
assertEquals("Metered Network", e.message)
205+
206+
verify {
207+
filesCache.upsert(any()) // small file got backed up
208+
}
209+
}
210+
211+
private fun prepareBackup(scannedFiles: FileScannerResult) {
212+
every { backendManager.canDoBackupNow() } returns true
213+
coEvery { backend.list(any(), Blob::class, callback = any()) } just Runs
214+
every { chunksCache.areAllAvailableChunksCached(emptySet()) } returns true
215+
every { fileScanner.getFiles() } returns scannedFiles
216+
every { filesCache.getByUri(any()) } returns null // nothing is cached, all is new
217+
every { chunksCache.get(any()) } returns null // no chunks are cached, all are new
218+
every { chunksCache.hasCorruptedChunks(any()) } returns false // no chunks corrupted
219+
}
146220
}

storage/lib/src/test/java/org/calyxos/backup/storage/backup/SmallFileBackupIntegrationTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ internal class SmallFileBackupIntegrationTest {
121121
observer.onFileBackedUp(file2, true, 0, match<Long> { it <= outputStream2.size() }, "S")
122122
} just Runs
123123

124-
val result = smallFileBackup.backupFiles(files, availableChunkIds, observer)
124+
val result = smallFileBackup.backupFiles(files, availableChunkIds, { false }, observer)
125125
assertEquals(setOf(chunkId.toHexString()), result.chunkIds)
126126
assertEquals(1, result.backupDocumentFiles.size)
127127
assertEquals(backupFile, result.backupDocumentFiles[0])

storage/lib/src/test/java/org/calyxos/backup/storage/backup/SmallFileBackupTest.kt

+23-6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import org.calyxos.backup.storage.getRandomString
2222
import org.calyxos.backup.storage.mockLog
2323
import org.calyxos.backup.storage.sameCachedFile
2424
import org.junit.Assert.assertEquals
25+
import org.junit.Assert.assertThrows
2526
import org.junit.Test
2627
import java.io.IOException
2728
import java.io.InputStream
@@ -53,7 +54,7 @@ internal class SmallFileBackupTest {
5354
every { filesCache.getByUri(files[0].uri) } returns cachedFile
5455
every { chunksCache.hasCorruptedChunks(cachedFile.chunks) } returns false
5556

56-
val result = smallFileBackup.backupFiles(files, availableChunkIds, null)
57+
val result = smallFileBackup.backupFiles(files, availableChunkIds, { false }, null)
5758
assertEquals(cachedFile.chunks.toSet(), result.chunkIds)
5859
assertEquals(1, result.backupDocumentFiles.size)
5960
assertEquals(backupFile, result.backupDocumentFiles[0])
@@ -102,6 +103,22 @@ internal class SmallFileBackupTest {
102103
singleFileBackup(files, cachedFile, availableChunkIds, corrupted = true)
103104
}
104105

106+
@Test
107+
fun `no file gets backed up if we aborted`() {
108+
val file = getRandomDocFile()
109+
val files = listOf(file)
110+
val availableChunkIds = hashSetOf(getRandomString(6))
111+
112+
addFile(file)
113+
114+
val e = assertThrows(IOException::class.java) {
115+
runBlocking {
116+
smallFileBackup.backupFiles(files, availableChunkIds, { true }, null)
117+
}
118+
}
119+
assertEquals("Metered Network", e.message)
120+
}
121+
105122
private suspend fun singleFileBackup(
106123
files: List<DocFile>,
107124
cachedFile: CachedFile?,
@@ -119,7 +136,7 @@ internal class SmallFileBackupTest {
119136
coEvery { zipChunker.finalizeAndReset(missingChunks) } returns zipChunk
120137
every { filesCache.upsert(sameCachedFile(newCachedFile)) } just Runs
121138

122-
val result = smallFileBackup.backupFiles(files, availableChunkIds, null)
139+
val result = smallFileBackup.backupFiles(files, availableChunkIds, { false }, null)
123140
assertEquals(newCachedFile.chunks.toSet(), result.chunkIds)
124141
assertEquals(1, result.backupDocumentFiles.size)
125142
assertEquals(backupFile, result.backupDocumentFiles[0])
@@ -143,7 +160,7 @@ internal class SmallFileBackupTest {
143160
coEvery { zipChunker.finalizeAndReset(emptyList()) } returns zipChunk
144161
every { filesCache.upsert(sameCachedFile(cachedFile2)) } just Runs
145162

146-
val result = smallFileBackup.backupFiles(files, availableChunkIds, null)
163+
val result = smallFileBackup.backupFiles(files, availableChunkIds, { false }, null)
147164
assertEquals(cachedFile2.chunks.toSet(), result.chunkIds)
148165
assertEquals(1, result.backupDocumentFiles.size)
149166
assertEquals(backupFile, result.backupDocumentFiles[0])
@@ -171,7 +188,7 @@ internal class SmallFileBackupTest {
171188
// zipChunker.finalizeAndReset defined above for both files
172189
every { filesCache.upsert(sameCachedFile(cachedFile2)) } just Runs
173190

174-
val result = smallFileBackup.backupFiles(files, hashSetOf(), null)
191+
val result = smallFileBackup.backupFiles(files, hashSetOf(), { false }, null)
175192
assertEquals(listOf(zipChunk1.id, zipChunk2.id).toSet(), result.chunkIds)
176193
assertEquals(
177194
listOf(backupFile1, backupFile2).sortedBy { it.name },
@@ -199,7 +216,7 @@ internal class SmallFileBackupTest {
199216
every { filesCache.upsert(sameCachedFile(cachedFile1)) } just Runs
200217
every { filesCache.upsert(sameCachedFile(cachedFile2)) } just Runs
201218

202-
val result = smallFileBackup.backupFiles(files, hashSetOf(), null)
219+
val result = smallFileBackup.backupFiles(files, hashSetOf(), { false }, null)
203220
assertEquals(listOf(zipChunk.id).toSet(), result.chunkIds)
204221
assertEquals(
205222
listOf(backupFile1, backupFile2).sortedBy { it.name },
@@ -219,7 +236,7 @@ internal class SmallFileBackupTest {
219236
addFile(file2)
220237
coEvery { zipChunker.finalizeAndReset(emptyList()) } throws IOException()
221238

222-
val result = smallFileBackup.backupFiles(files, hashSetOf(), null)
239+
val result = smallFileBackup.backupFiles(files, hashSetOf(), { false }, null)
223240
assertEquals(emptySet<String>(), result.chunkIds)
224241
assertEquals(0, result.backupDocumentFiles.size)
225242
assertEquals(0, result.backupMediaFiles.size)

0 commit comments

Comments
 (0)