mirror of
https://github.com/gedoor/legado.git
synced 2025-08-10 00:52:30 +00:00
本地备份也采用压缩包,和webDav备份保持一致,从webDav下载的备份文件不需要解压可以直接恢复
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user