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

This commit is contained in:
kunfei
2023-03-11 22:39:54 +08:00
parent 88a99b016d
commit 1e2de40d58
6 changed files with 123 additions and 144 deletions

View File

@@ -27,8 +27,6 @@ import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import splitties.init.appCtx
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import kotlin.coroutines.coroutineContext
@@ -37,7 +35,6 @@ import kotlin.coroutines.coroutineContext
*/
object AppWebDav {
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 exportsWebDavUrl get() = "${rootWebDavUrl}books/"
@@ -69,18 +66,6 @@ object AppWebDav {
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() {
kotlin.runCatching {
authorization = null
@@ -153,17 +138,17 @@ object AppWebDav {
suspend fun restoreWebDav(name: String) {
authorization?.let {
val webDav = WebDav(rootWebDavUrl + name, it)
webDav.downloadTo(zipFilePath, true)
webDav.downloadTo(Backup.zipFilePath, true)
FileUtils.delete(Backup.backupPath)
ZipUtils.unzipFile(zipFilePath, Backup.backupPath)
ZipUtils.unzipFile(Backup.zipFilePath, Backup.backupPath)
Restore.restoreDatabase()
Restore.restoreConfig()
}
}
suspend fun hasBackUp(): Boolean {
suspend fun hasBackUp(backUpName: String): Boolean {
authorization?.let {
val url = "$rootWebDavUrl$backupFileName"
val url = "$rootWebDavUrl${backUpName}"
return WebDav(url, it).exists()
}
return false
@@ -187,19 +172,16 @@ object AppWebDav {
}
}
/**
* webDav备份
* @param fileName 备份文件名
*/
@Throws(Exception::class)
suspend fun backUpWebDav(path: String) {
suspend fun backUpWebDav(fileName: String) {
if (!NetworkUtils.isAvailable()) return
authorization?.let {
val paths = arrayListOf(*Backup.backupFileNames)
for (i in 0 until paths.size) {
paths[i] = path + File.separator + paths[i]
}
FileUtils.delete(zipFilePath)
if (ZipUtils.zipFiles(paths, zipFilePath)) {
val putUrl = "$rootWebDavUrl$backupFileName"
WebDav(putUrl, it).upload(zipFilePath)
}
val putUrl = "$rootWebDavUrl$fileName"
WebDav(putUrl, it).upload(Backup.zipFilePath)
}
}

View File

@@ -8,6 +8,7 @@ import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb
import io.legado.app.help.AppWebDav
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.ReadBookConfig
import io.legado.app.help.config.ThemeConfig
@@ -18,7 +19,10 @@ import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import splitties.init.appCtx
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
/**
@@ -29,8 +33,9 @@ object Backup {
val backupPath: String by lazy {
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(
"bookshelf.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) {
val lastBackup = LocalConfig.lastBackup
if (lastBackup + TimeUnit.DAYS.toMillis(1) < System.currentTimeMillis()) {
Coroutine.async {
if (!AppWebDav.hasBackUp()) {
backup(context, context.getPrefString(PreferKey.backupPath), true)
val backupZipFileName = getNowZipFileName()
if (!AppWebDav.hasBackUp(backupZipFileName)) {
backup(context, context.getPrefString(PreferKey.backupPath))
} else {
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()
withContext(IO) {
FileUtils.delete(backupPath)
@@ -124,18 +141,26 @@ object Backup {
edit.commit()
}
ensureActive()
when {
path.isNullOrBlank() -> {
copyBackup(context.getExternalFilesDir(null)!!, false)
}
path.isContentScheme() -> {
copyBackup(context, Uri.parse(path), isAuto)
}
else -> {
copyBackup(File(path), isAuto)
}
val zipFileName = getNowZipFileName()
val paths = arrayListOf(*backupFileNames)
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)
}
}
AppWebDav.backUpWebDav(zipFileName)
}
AppWebDav.backUpWebDav(backupPath)
}
}
@@ -149,40 +174,23 @@ object Backup {
}
@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 ->
for (fileName in backupFileNames) {
val file = File(backupPath + File.separator + fileName)
if (file.exists()) {
if (isAuto) {
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())
}
treeDoc.findFile(fileName)?.delete()
treeDoc.createFile("", fileName)?.openOutputStream()?.use { outputS ->
FileInputStream(File(zipFilePath)).use { inputS ->
inputS.copyTo(outputS)
}
}
}
}
@Throws(Exception::class)
private fun copyBackup(rootFile: File, isAuto: Boolean) {
for (fileName in backupFileNames) {
val file = File(backupPath + File.separator + fileName)
if (file.exists()) {
file.copyTo(
if (isAuto) {
FileUtils.createFileIfNotExist(rootFile, "auto", fileName)
} else {
FileUtils.createFileIfNotExist(rootFile, fileName)
}, true
)
private fun copyBackup(rootFile: File, fileName: String) {
FileInputStream(File(zipFilePath)).use { inputS ->
val file = FileUtils.createFileIfNotExist(rootFile, fileName)
FileOutputStream(file).use { outputS ->
inputS.copyTo(outputS)
}
}
}

View File

@@ -27,35 +27,21 @@ import kotlinx.coroutines.withContext
import splitties.init.appCtx
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
/**
* 恢复
*/
object Restore {
suspend fun restore(context: Context, path: String) {
suspend fun restore(context: Context, uri: Uri) {
kotlin.runCatching {
if (path.isContentScheme()) {
DocumentFile.fromTreeUri(context, Uri.parse(path))?.listFiles()?.forEach { doc ->
if (Backup.backupFileNames.contains(doc.name)) {
context.contentResolver.openInputStream(doc.uri)?.use { inputStream ->
val file = File("${Backup.backupPath}${File.separator}${doc.name}")
FileOutputStream(file).use { outputStream ->
inputStream.copyTo(outputStream)
}
}
}
FileUtils.delete(Backup.backupPath)
if (uri.isContentScheme()) {
DocumentFile.fromTreeUri(context, uri)?.openInputStream()!!.use {
ZipUtils.unZipToPath(it, Backup.backupPath)
}
} else {
val dir = File(path)
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)
}
}
ZipUtils.unzipFile(uri.path!!, Backup.backupPath)
}
}.onFailure {
AppLog.put("恢复复制文件出错\n${it.localizedMessage}", it)

View File

@@ -11,7 +11,6 @@ import android.view.View
import androidx.core.view.MenuProvider
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
@@ -38,7 +37,6 @@ import io.legado.app.ui.widget.dialog.WaitDialog
import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import splitties.init.appCtx
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 ->
if (uri.isContentScheme()) {
AppConfig.backupPath = uri.toString()
Coroutine.async {
Restore.restore(appCtx, uri.toString())
}
} else {
uri.path?.let { path ->
AppConfig.backupPath = path
Coroutine.async {
Restore.restore(appCtx, path)
}
}
Coroutine.async {
Restore.restore(appCtx, uri)
}
}
}
@@ -135,7 +123,10 @@ class BackupConfigFragment : PreferenceFragment(),
upPreferenceSummary(PreferKey.webDavDeviceName, AppConfig.webDavDeviceName)
upPreferenceSummary(PreferKey.backupPath, getPrefString(PreferKey.backupPath))
findPreference<io.legado.app.lib.prefs.Preference>("web_dav_restore")
?.onLongClick { restoreDir.launch(); true }
?.onLongClick {
restoreFromLocal()
true
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -333,7 +324,7 @@ class BackupConfigFragment : PreferenceFragment(),
AppLog.put("恢复备份出错WebDavError\n${it.localizedMessage}", it)
alert {
setTitle(R.string.restore)
setMessage("WebDavError\n${it.localizedMessage}\n将从本地备份恢复。\n从WebDav手动下载备份文件需要解压才能恢复。")
setMessage("WebDavError\n${it.localizedMessage}\n将从本地备份恢复。")
okButton {
restoreFromLocal()
}
@@ -345,38 +336,11 @@ class BackupConfigFragment : PreferenceFragment(),
}
private fun restoreFromLocal() {
val backupPath = getPrefString(PreferKey.backupPath)
if (backupPath?.isNotEmpty() == true) {
if (backupPath.isContentScheme()) {
val uri = Uri.parse(backupPath)
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()
restoreDoc.launch {
title = getString(R.string.select_restore_file)
mode = HandleFileContract.FILE
allowExtensions = arrayOf("zip")
}
}
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()
}
}

View File

@@ -12,6 +12,8 @@ import io.legado.app.exception.NoStackTraceException
import splitties.init.appCtx
import splitties.systemservices.downloadManager
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.nio.charset.Charset
@@ -236,6 +238,16 @@ fun DocumentFile.listFileDocs(filter: FileDocFilter? = null): ArrayList<FileDoc>
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)
fun DocumentFile.writeText(context: Context, data: String, charset: Charset = Charsets.UTF_8) {
uri.writeText(context, data, charset)

View File

@@ -2,12 +2,9 @@ package io.legado.app.utils
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
import java.io.*
import java.util.zip.GZIPOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
import java.util.zip.*
@Suppress("unused", "BlockingMethodInNonBlockingContext", "MemberVisibilityCanBePrivate")
object ZipUtils {
@@ -177,6 +174,36 @@ object ZipUtils {
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.
*