@@ -43,6 +43,10 @@ import kotlinx.coroutines.flow.transformLatest
4343import kotlinx.coroutines.launch
4444import kotlinx.coroutines.sync.withLock
4545import kotlinx.coroutines.withContext
46+ import okio.ByteString.Companion.decodeHex
47+ import okio.HashingSource.Companion.sha256
48+ import okio.buffer
49+ import okio.sink
4650import org.ccci.gto.android.common.kotlin.coroutines.MutexMap
4751import org.ccci.gto.android.common.kotlin.coroutines.ReadWriteMutex
4852import org.ccci.gto.android.common.kotlin.coroutines.flow.EmptyStateFlow
@@ -294,9 +298,10 @@ class GodToolsDownloadManager @VisibleForTesting internal constructor(
294298 val relatedFiles = manifest.relatedFiles
295299 val completedFiles = AtomicLong (0 )
296300 val successful = coroutineScope {
297- relatedFiles.map {
301+ relatedFiles.map { file ->
298302 async {
299- downloadTranslationFileIfNecessary(it).also {
303+ val src = file.src ? : return @async true
304+ downloadTranslationFileIfNecessary(src, sha256 = file.checksumSha256, size = file.size).also {
300305 do {
301306 val completed = completedFiles.get()
302307 updateProgress(key, completed + 1 , relatedFiles.size.toLong())
@@ -309,22 +314,42 @@ class GodToolsDownloadManager @VisibleForTesting internal constructor(
309314
310315 // record the translation as downloaded
311316 downloadedFilesRepository.insertOrIgnore(DownloadedTranslationFile (translation, manifestFileName))
312- relatedFiles.forEach { downloadedFilesRepository.insertOrIgnore(DownloadedTranslationFile (translation, it)) }
317+ relatedFiles.mapNotNull { it.src }.forEach {
318+ downloadedFilesRepository.insertOrIgnore(DownloadedTranslationFile (translation, it))
319+ }
313320 translationsRepository.markTranslationDownloaded(translation.id, true )
314321
315322 return true
316323 }
317324
318- private suspend fun downloadTranslationFileIfNecessary (fileName : String ): Boolean = filesMutex.withLock(fileName) {
325+ private suspend fun downloadTranslationFileIfNecessary (
326+ fileName : String ,
327+ sha256 : String? = null,
328+ size : Long? = null,
329+ ): Boolean = filesMutex.withLock(fileName) {
319330 if (downloadedFilesRepository.findDownloadedFile(fileName) != null ) return true
320- try {
321- val body = translationsApi.downloadFile(fileName).takeIf { it.isSuccessful }?.body() ? : return false
322- val downloadedFile = DownloadedFile (fileName)
323- withContext(Dispatchers .IO ) { body.byteStream().copyTo(downloadedFile) }
324- downloadedFilesRepository.insertOrIgnore(downloadedFile)
325- return true
326- } catch (e: IOException ) {
327- return false
331+
332+ return withContext(Dispatchers .IO ) {
333+ try {
334+ val body = translationsApi.downloadFile(fileName).takeIf { it.isSuccessful }?.body()
335+ ? : return @withContext false
336+
337+ val downloadedFile = DownloadedFile (fileName)
338+ downloadedFile.bufferedSink().use { sink ->
339+ body.source().use { source ->
340+ val digest = sha256(source)
341+ val bytesWritten = sink.writeAll(digest)
342+
343+ if (size != null && size != bytesWritten) return @withContext false
344+ if (sha256 != null && sha256.decodeHex() != digest.hash) return @withContext false
345+ }
346+ }
347+
348+ downloadedFilesRepository.insertOrIgnore(downloadedFile)
349+ true
350+ } catch (_: IOException ) {
351+ false
352+ }
328353 }
329354 }
330355
@@ -450,6 +475,8 @@ class GodToolsDownloadManager @VisibleForTesting internal constructor(
450475 }
451476 // endregion Cleanup
452477
478+ private suspend fun DownloadedFile.bufferedSink () = withContext(Dispatchers .IO ) { getFile(fs).sink().buffer() }
479+
453480 private suspend fun InputStream.copyTo (file : DownloadedFile ) = withContext(Dispatchers .IO ) {
454481 file.getFile(fs).outputStream().use { copyTo(it) }
455482 }
0 commit comments