Ver Fonte

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

kunfei há 3 anos atrás
pai
commit
1ce9bd4f96

+ 11 - 29
app/src/main/java/io/legado/app/help/AppWebDav.kt

@@ -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)
         }
     }
 

+ 49 - 41
app/src/main/java/io/legado/app/help/storage/Backup.kt

@@ -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)
             }
         }
     }

+ 6 - 20
app/src/main/java/io/legado/app/help/storage/Restore.kt

@@ -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)

+ 12 - 48
app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt

@@ -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 - 0
app/src/main/java/io/legado/app/utils/FileDocExtensions.kt

@@ -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)

+ 32 - 5
app/src/main/java/io/legado/app/utils/ZipUtils.kt

@@ -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.
      *