tachiyomi/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt
2023-12-25 16:31:40 -05:00

157 lines
6.1 KiB
Kotlin

package eu.kanade.tachiyomi.data.backup.create
import android.content.Context
import android.net.Uri
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_APP_PREFS
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_SOURCE_PREFS
import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator
import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator
import eu.kanade.tachiyomi.data.backup.create.creators.PreferenceBackupCreator
import eu.kanade.tachiyomi.data.backup.create.creators.SourcesBackupCreator
import kotlinx.serialization.protobuf.ProtoBuf
import logcat.LogPriority
import okio.buffer
import okio.gzip
import okio.sink
import tachiyomi.core.i18n.stringResource
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.backup.model.Backup
import tachiyomi.domain.backup.model.BackupCategory
import tachiyomi.domain.backup.model.BackupManga
import tachiyomi.domain.backup.model.BackupPreference
import tachiyomi.domain.backup.model.BackupSerializer
import tachiyomi.domain.backup.model.BackupSource
import tachiyomi.domain.backup.model.BackupSourcePreferences
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.time.Instant
import java.util.Date
import java.util.Locale
class BackupCreator(
private val context: Context,
private val isAutoBackup: Boolean,
private val parser: ProtoBuf = Injekt.get(),
private val getFavorites: GetFavorites = Injekt.get(),
private val backupPreferences: BackupPreferences = Injekt.get(),
private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(),
private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(),
private val preferenceBackupCreator: PreferenceBackupCreator = PreferenceBackupCreator(),
private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(),
) {
suspend fun backup(uri: Uri, flags: Int): String {
var file: UniFile? = null
try {
file = (
if (isAutoBackup) {
// Get dir of file and create
val dir = UniFile.fromUri(context, uri)
// Delete older backups
dir?.listFiles { _, filename -> FILENAME_REGEX.matches(filename) }
.orEmpty()
.sortedByDescending { it.name }
.drop(MAX_AUTO_BACKUPS - 1)
.forEach { it.delete() }
// Create new file to place backup
dir?.createFile(getFilename())
} else {
UniFile.fromUri(context, uri)
}
)
?: throw Exception(context.stringResource(MR.strings.create_backup_file_error))
if (!file.isFile) {
throw IllegalStateException("Failed to get handle on a backup file")
}
val databaseManga = getFavorites.await()
val backup = Backup(
backupManga = backupMangas(databaseManga, flags),
backupCategories = backupCategories(flags),
backupSources = backupSources(databaseManga),
backupPreferences = backupAppPreferences(flags),
backupSourcePreferences = backupSourcePreferences(flags),
)
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
if (byteArray.isEmpty()) {
throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error))
}
file.openOutputStream()
.also {
// Force overwrite old file
(it as? FileOutputStream)?.channel?.truncate(0)
}
.sink().gzip().buffer().use {
it.write(byteArray)
}
val fileUri = file.uri
// Make sure it's a valid backup file
BackupFileValidator(context).validate(fileUri)
if (isAutoBackup) {
backupPreferences.lastAutoBackupTimestamp().set(Instant.now().toEpochMilli())
}
return fileUri.toString()
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
file?.delete()
throw e
}
}
private suspend fun backupCategories(options: Int): List<BackupCategory> {
if (options and BACKUP_CATEGORY != BACKUP_CATEGORY) return emptyList()
return categoriesBackupCreator.backupCategories()
}
private suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
return mangaBackupCreator.backupMangas(mangas, flags)
}
private fun backupSources(mangas: List<Manga>): List<BackupSource> {
return sourcesBackupCreator.backupSources(mangas)
}
private fun backupAppPreferences(flags: Int): List<BackupPreference> {
if (flags and BACKUP_APP_PREFS != BACKUP_APP_PREFS) return emptyList()
return preferenceBackupCreator.backupAppPreferences()
}
private fun backupSourcePreferences(flags: Int): List<BackupSourcePreferences> {
if (flags and BACKUP_SOURCE_PREFS != BACKUP_SOURCE_PREFS) return emptyList()
return preferenceBackupCreator.backupSourcePreferences()
}
companion object {
private const val MAX_AUTO_BACKUPS: Int = 4
private val FILENAME_REGEX = """${BuildConfig.APPLICATION_ID}_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}.tachibk""".toRegex()
fun getFilename(): String {
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.ENGLISH).format(Date())
return "${BuildConfig.APPLICATION_ID}_$date.tachibk"
}
}
}