本地备份也采用压缩包,和webDav备份保持一致,从webDav下载的备份文件不需要解压可以直接恢复

This commit is contained in:
kunfei
2023-03-11 22:39:54 +08:00
parent 88a99b016d
commit 1e2de40d58
6 changed files with 122 additions and 143 deletions
@@ -27,8 +27,6 @@ import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import splitties.init.appCtx import splitties.init.appCtx
import java.io.File
import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
@@ -37,7 +35,6 @@ import kotlin.coroutines.coroutineContext
*/ */
object AppWebDav { object AppWebDav {
private const val defaultWebDavUrl = "https://dav.jianguoyun.com/dav/" private const val defaultWebDavUrl = "https://dav.jianguoyun.com/dav/"
private val zipFilePath = "${appCtx.externalFiles.absolutePath}${File.separator}backup.zip"
private val bookProgressUrl get() = "${rootWebDavUrl}bookProgress/" private val bookProgressUrl get() = "${rootWebDavUrl}bookProgress/"
private val exportsWebDavUrl get() = "${rootWebDavUrl}books/" private val exportsWebDavUrl get() = "${rootWebDavUrl}books/"
@@ -69,18 +66,6 @@ object AppWebDav {
return url return url
} }
private val backupFileName: String
get() {
val backupDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
.format(Date(System.currentTimeMillis()))
val deviceName = AppConfig.webDavDeviceName
return if (deviceName?.isNotBlank() == true) {
"backup${backupDate}-${deviceName}.zip"
} else {
"backup${backupDate}.zip"
}
}
suspend fun upConfig() { suspend fun upConfig() {
kotlin.runCatching { kotlin.runCatching {
authorization = null authorization = null
@@ -153,17 +138,17 @@ object AppWebDav {
suspend fun restoreWebDav(name: String) { suspend fun restoreWebDav(name: String) {
authorization?.let { authorization?.let {
val webDav = WebDav(rootWebDavUrl + name, it) val webDav = WebDav(rootWebDavUrl + name, it)
webDav.downloadTo(zipFilePath, true) webDav.downloadTo(Backup.zipFilePath, true)
FileUtils.delete(Backup.backupPath) FileUtils.delete(Backup.backupPath)
ZipUtils.unzipFile(zipFilePath, Backup.backupPath) ZipUtils.unzipFile(Backup.zipFilePath, Backup.backupPath)
Restore.restoreDatabase() Restore.restoreDatabase()
Restore.restoreConfig() Restore.restoreConfig()
} }
} }
suspend fun hasBackUp(): Boolean { suspend fun hasBackUp(backUpName: String): Boolean {
authorization?.let { authorization?.let {
val url = "$rootWebDavUrl$backupFileName" val url = "$rootWebDavUrl${backUpName}"
return WebDav(url, it).exists() return WebDav(url, it).exists()
} }
return false return false
@@ -187,19 +172,16 @@ object AppWebDav {
} }
} }
/**
* webDav备份
* @param fileName 备份文件名
*/
@Throws(Exception::class) @Throws(Exception::class)
suspend fun backUpWebDav(path: String) { suspend fun backUpWebDav(fileName: String) {
if (!NetworkUtils.isAvailable()) return if (!NetworkUtils.isAvailable()) return
authorization?.let { authorization?.let {
val paths = arrayListOf(*Backup.backupFileNames) val putUrl = "$rootWebDavUrl$fileName"
for (i in 0 until paths.size) { WebDav(putUrl, it).upload(Backup.zipFilePath)
paths[i] = path + File.separator + paths[i]
}
FileUtils.delete(zipFilePath)
if (ZipUtils.zipFiles(paths, zipFilePath)) {
val putUrl = "$rootWebDavUrl$backupFileName"
WebDav(putUrl, it).upload(zipFilePath)
}
} }
} }
@@ -8,6 +8,7 @@ import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.help.AppWebDav import io.legado.app.help.AppWebDav
import io.legado.app.help.DirectLinkUpload import io.legado.app.help.DirectLinkUpload
import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.LocalConfig import io.legado.app.help.config.LocalConfig
import io.legado.app.help.config.ReadBookConfig import io.legado.app.help.config.ReadBookConfig
import io.legado.app.help.config.ThemeConfig import io.legado.app.help.config.ThemeConfig
@@ -18,7 +19,10 @@ import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import splitties.init.appCtx import splitties.init.appCtx
import java.io.File import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
/** /**
@@ -29,8 +33,9 @@ object Backup {
val backupPath: String by lazy { val backupPath: String by lazy {
appCtx.filesDir.getFile("backup").createFolderIfNotExist().absolutePath appCtx.filesDir.getFile("backup").createFolderIfNotExist().absolutePath
} }
val zipFilePath = "${appCtx.externalFiles.absolutePath}${File.separator}backup.zip"
val backupFileNames by lazy { private val backupFileNames by lazy {
arrayOf( arrayOf(
"bookshelf.json", "bookshelf.json",
"bookmark.json", "bookmark.json",
@@ -55,12 +60,24 @@ object Backup {
) )
} }
private fun getNowZipFileName(): String {
val backupDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
.format(Date(System.currentTimeMillis()))
val deviceName = AppConfig.webDavDeviceName
return if (deviceName?.isNotBlank() == true) {
"backup${backupDate}-${deviceName}.zip"
} else {
"backup${backupDate}.zip"
}
}
fun autoBack(context: Context) { fun autoBack(context: Context) {
val lastBackup = LocalConfig.lastBackup val lastBackup = LocalConfig.lastBackup
if (lastBackup + TimeUnit.DAYS.toMillis(1) < System.currentTimeMillis()) { if (lastBackup + TimeUnit.DAYS.toMillis(1) < System.currentTimeMillis()) {
Coroutine.async { Coroutine.async {
if (!AppWebDav.hasBackUp()) { val backupZipFileName = getNowZipFileName()
backup(context, context.getPrefString(PreferKey.backupPath), true) if (!AppWebDav.hasBackUp(backupZipFileName)) {
backup(context, context.getPrefString(PreferKey.backupPath))
} else { } else {
LocalConfig.lastBackup = System.currentTimeMillis() LocalConfig.lastBackup = System.currentTimeMillis()
} }
@@ -70,7 +87,7 @@ object Backup {
} }
} }
suspend fun backup(context: Context, path: String?, isAuto: Boolean = false) { suspend fun backup(context: Context, path: String?) {
LocalConfig.lastBackup = System.currentTimeMillis() LocalConfig.lastBackup = System.currentTimeMillis()
withContext(IO) { withContext(IO) {
FileUtils.delete(backupPath) FileUtils.delete(backupPath)
@@ -124,18 +141,26 @@ object Backup {
edit.commit() edit.commit()
} }
ensureActive() ensureActive()
when { val zipFileName = getNowZipFileName()
path.isNullOrBlank() -> { val paths = arrayListOf(*backupFileNames)
copyBackup(context.getExternalFilesDir(null)!!, false) for (i in 0 until paths.size) {
paths[i] = backupPath + File.separator + paths[i]
}
FileUtils.delete(zipFilePath)
if (ZipUtils.zipFiles(paths, zipFilePath)) {
when {
path.isNullOrBlank() -> {
copyBackup(context.getExternalFilesDir(null)!!, zipFileName)
}
path.isContentScheme() -> {
copyBackup(context, Uri.parse(path), zipFileName)
}
else -> {
copyBackup(File(path), zipFileName)
}
} }
path.isContentScheme() -> { AppWebDav.backUpWebDav(zipFileName)
copyBackup(context, Uri.parse(path), isAuto)
}
else -> {
copyBackup(File(path), isAuto)
}
} }
AppWebDav.backUpWebDav(backupPath)
} }
} }
@@ -149,40 +174,23 @@ object Backup {
} }
@Throws(Exception::class) @Throws(Exception::class)
private fun copyBackup(context: Context, uri: Uri, isAuto: Boolean) { private fun copyBackup(context: Context, uri: Uri, fileName: String) {
DocumentFile.fromTreeUri(context, uri)?.let { treeDoc -> DocumentFile.fromTreeUri(context, uri)?.let { treeDoc ->
for (fileName in backupFileNames) { treeDoc.findFile(fileName)?.delete()
val file = File(backupPath + File.separator + fileName) treeDoc.createFile("", fileName)?.openOutputStream()?.use { outputS ->
if (file.exists()) { FileInputStream(File(zipFilePath)).use { inputS ->
if (isAuto) { inputS.copyTo(outputS)
treeDoc.findFile("auto")?.findFile(fileName)?.delete()
DocumentUtils.createFileIfNotExist(
treeDoc,
fileName,
subDirs = arrayOf("auto")
)?.writeBytes(context, file.readBytes())
} else {
treeDoc.findFile(fileName)?.delete()
treeDoc.createFile("", fileName)
?.writeBytes(context, file.readBytes())
}
} }
} }
} }
} }
@Throws(Exception::class) @Throws(Exception::class)
private fun copyBackup(rootFile: File, isAuto: Boolean) { private fun copyBackup(rootFile: File, fileName: String) {
for (fileName in backupFileNames) { FileInputStream(File(zipFilePath)).use { inputS ->
val file = File(backupPath + File.separator + fileName) val file = FileUtils.createFileIfNotExist(rootFile, fileName)
if (file.exists()) { FileOutputStream(file).use { outputS ->
file.copyTo( inputS.copyTo(outputS)
if (isAuto) {
FileUtils.createFileIfNotExist(rootFile, "auto", fileName)
} else {
FileUtils.createFileIfNotExist(rootFile, fileName)
}, true
)
} }
} }
} }
@@ -27,35 +27,21 @@ import kotlinx.coroutines.withContext
import splitties.init.appCtx import splitties.init.appCtx
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileOutputStream
/** /**
* 恢复 * 恢复
*/ */
object Restore { object Restore {
suspend fun restore(context: Context, path: String) { suspend fun restore(context: Context, uri: Uri) {
kotlin.runCatching { kotlin.runCatching {
if (path.isContentScheme()) { FileUtils.delete(Backup.backupPath)
DocumentFile.fromTreeUri(context, Uri.parse(path))?.listFiles()?.forEach { doc -> if (uri.isContentScheme()) {
if (Backup.backupFileNames.contains(doc.name)) { DocumentFile.fromTreeUri(context, uri)?.openInputStream()!!.use {
context.contentResolver.openInputStream(doc.uri)?.use { inputStream -> ZipUtils.unZipToPath(it, Backup.backupPath)
val file = File("${Backup.backupPath}${File.separator}${doc.name}")
FileOutputStream(file).use { outputStream ->
inputStream.copyTo(outputStream)
}
}
}
} }
} else { } else {
val dir = File(path) ZipUtils.unzipFile(uri.path!!, Backup.backupPath)
for (fileName in Backup.backupFileNames) {
val file = dir.getFile(fileName)
if (file.exists()) {
val target = File("${Backup.backupPath}${File.separator}$fileName")
file.copyTo(target, true)
}
}
} }
}.onFailure { }.onFailure {
AppLog.put("恢复复制文件出错\n${it.localizedMessage}", it) AppLog.put("恢复复制文件出错\n${it.localizedMessage}", it)
@@ -11,7 +11,6 @@ import android.view.View
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
@@ -38,7 +37,6 @@ import io.legado.app.ui.widget.dialog.WaitDialog
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import splitties.init.appCtx import splitties.init.appCtx
import kotlin.collections.set import kotlin.collections.set
@@ -87,20 +85,10 @@ class BackupConfigFragment : PreferenceFragment(),
} }
} }
} }
private val restoreDir = registerForActivityResult(HandleFileContract()) { private val restoreDoc = registerForActivityResult(HandleFileContract()) {
it.uri?.let { uri -> it.uri?.let { uri ->
if (uri.isContentScheme()) { Coroutine.async {
AppConfig.backupPath = uri.toString() Restore.restore(appCtx, uri)
Coroutine.async {
Restore.restore(appCtx, uri.toString())
}
} else {
uri.path?.let { path ->
AppConfig.backupPath = path
Coroutine.async {
Restore.restore(appCtx, path)
}
}
} }
} }
} }
@@ -135,7 +123,10 @@ class BackupConfigFragment : PreferenceFragment(),
upPreferenceSummary(PreferKey.webDavDeviceName, AppConfig.webDavDeviceName) upPreferenceSummary(PreferKey.webDavDeviceName, AppConfig.webDavDeviceName)
upPreferenceSummary(PreferKey.backupPath, getPrefString(PreferKey.backupPath)) upPreferenceSummary(PreferKey.backupPath, getPrefString(PreferKey.backupPath))
findPreference<io.legado.app.lib.prefs.Preference>("web_dav_restore") findPreference<io.legado.app.lib.prefs.Preference>("web_dav_restore")
?.onLongClick { restoreDir.launch(); true } ?.onLongClick {
restoreFromLocal()
true
}
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -333,7 +324,7 @@ class BackupConfigFragment : PreferenceFragment(),
AppLog.put("恢复备份出错WebDavError\n${it.localizedMessage}", it) AppLog.put("恢复备份出错WebDavError\n${it.localizedMessage}", it)
alert { alert {
setTitle(R.string.restore) setTitle(R.string.restore)
setMessage("WebDavError\n${it.localizedMessage}\n将从本地备份恢复。\n从WebDav手动下载备份文件需要解压才能恢复。") setMessage("WebDavError\n${it.localizedMessage}\n将从本地备份恢复。")
okButton { okButton {
restoreFromLocal() restoreFromLocal()
} }
@@ -345,38 +336,11 @@ class BackupConfigFragment : PreferenceFragment(),
} }
private fun restoreFromLocal() { private fun restoreFromLocal() {
val backupPath = getPrefString(PreferKey.backupPath) restoreDoc.launch {
if (backupPath?.isNotEmpty() == true) { title = getString(R.string.select_restore_file)
if (backupPath.isContentScheme()) { mode = HandleFileContract.FILE
val uri = Uri.parse(backupPath) allowExtensions = arrayOf("zip")
val doc = DocumentFile.fromTreeUri(requireContext(), uri)
if (doc?.canWrite() == true) {
lifecycleScope.launch {
Restore.restore(requireContext(), backupPath)
}
} else {
restoreDir.launch()
}
} else {
restoreUsePermission(backupPath)
}
} else {
restoreDir.launch()
} }
} }
private fun restoreUsePermission(path: String) {
PermissionsCompat.Builder()
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
Coroutine.async {
AppConfig.backupPath = path
Restore.restoreDatabase(path)
Restore.restoreConfig(path)
}
}
.request()
}
} }
@@ -12,6 +12,8 @@ import io.legado.app.exception.NoStackTraceException
import splitties.init.appCtx import splitties.init.appCtx
import splitties.systemservices.downloadManager import splitties.systemservices.downloadManager
import java.io.File import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.nio.charset.Charset import java.nio.charset.Charset
@@ -236,6 +238,16 @@ fun DocumentFile.listFileDocs(filter: FileDocFilter? = null): ArrayList<FileDoc>
return FileDoc.fromDocumentFile(this).list(filter) return FileDoc.fromDocumentFile(this).list(filter)
} }
@Throws(Exception::class)
fun DocumentFile.openInputStream(): InputStream? {
return appCtx.contentResolver.openInputStream(uri)
}
@Throws(Exception::class)
fun DocumentFile.openOutputStream(): OutputStream? {
return appCtx.contentResolver.openOutputStream(uri)
}
@Throws(Exception::class) @Throws(Exception::class)
fun DocumentFile.writeText(context: Context, data: String, charset: Charset = Charsets.UTF_8) { fun DocumentFile.writeText(context: Context, data: String, charset: Charset = Charsets.UTF_8) {
uri.writeText(context, data, charset) uri.writeText(context, data, charset)
@@ -2,12 +2,9 @@ package io.legado.app.utils
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.* import java.io.*
import java.util.zip.GZIPOutputStream import java.util.zip.*
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
@Suppress("unused", "BlockingMethodInNonBlockingContext", "MemberVisibilityCanBePrivate") @Suppress("unused", "BlockingMethodInNonBlockingContext", "MemberVisibilityCanBePrivate")
object ZipUtils { object ZipUtils {
@@ -177,6 +174,36 @@ object ZipUtils {
return true return true
} }
fun unZipToPath(inputStream: InputStream, path: String) {
val zipInputStream = ZipInputStream(inputStream)
unZipToPath(zipInputStream, path)
}
fun unZipToPath(zipInputStream: ZipInputStream, path: String) {
var entry: ZipEntry
while (zipInputStream.nextEntry.also { entry = it } != null) {
val entryFile = File(path, entry.name)
if (entry.isDirectory) {
if (!entryFile.exists()) {
entryFile.mkdirs()
}
continue
}
if (entryFile.parentFile?.exists() != true) {
entryFile.parentFile?.mkdirs()
}
if (!entryFile.exists()) {
entryFile.createNewFile()
entryFile.setReadable(true)
entryFile.setExecutable(true)
}
FileOutputStream(entryFile).use {
zipInputStream.copyTo(it)
}
}
zipInputStream.close()
}
/** /**
* Unzip the file. * Unzip the file.
* *