From 1e2de40d581c5d586146b5439780f87f7b38d21a Mon Sep 17 00:00:00 2001 From: kunfei Date: Sat, 11 Mar 2023 22:39:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=A4=87=E4=BB=BD=E4=B9=9F?= =?UTF-8?q?=E9=87=87=E7=94=A8=E5=8E=8B=E7=BC=A9=E5=8C=85,=E5=92=8CwebDav?= =?UTF-8?q?=E5=A4=87=E4=BB=BD=E4=BF=9D=E6=8C=81=E4=B8=80=E8=87=B4,?= =?UTF-8?q?=E4=BB=8EwebDav=E4=B8=8B=E8=BD=BD=E7=9A=84=E5=A4=87=E4=BB=BD?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8D=E9=9C=80=E8=A6=81=E8=A7=A3=E5=8E=8B?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E7=9B=B4=E6=8E=A5=E6=81=A2=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/io/legado/app/help/AppWebDav.kt | 40 +++----- .../java/io/legado/app/help/storage/Backup.kt | 92 ++++++++++--------- .../io/legado/app/help/storage/Restore.kt | 26 ++---- .../app/ui/config/BackupConfigFragment.kt | 60 +++--------- .../io/legado/app/utils/FileDocExtensions.kt | 12 +++ .../main/java/io/legado/app/utils/ZipUtils.kt | 37 +++++++- 6 files changed, 123 insertions(+), 144 deletions(-) diff --git a/app/src/main/java/io/legado/app/help/AppWebDav.kt b/app/src/main/java/io/legado/app/help/AppWebDav.kt index c935884c4..d4d27999f 100644 --- a/app/src/main/java/io/legado/app/help/AppWebDav.kt +++ b/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) } } diff --git a/app/src/main/java/io/legado/app/help/storage/Backup.kt b/app/src/main/java/io/legado/app/help/storage/Backup.kt index e36da82ab..f8dda491a 100644 --- a/app/src/main/java/io/legado/app/help/storage/Backup.kt +++ b/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) } } } diff --git a/app/src/main/java/io/legado/app/help/storage/Restore.kt b/app/src/main/java/io/legado/app/help/storage/Restore.kt index 6ea181b42..d4540cb55 100644 --- a/app/src/main/java/io/legado/app/help/storage/Restore.kt +++ b/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) diff --git a/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt index 35f16450b..ef94b0e48 100644 --- a/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt +++ b/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("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() - } - } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/FileDocExtensions.kt b/app/src/main/java/io/legado/app/utils/FileDocExtensions.kt index 6debc91e0..f36e33388 100644 --- a/app/src/main/java/io/legado/app/utils/FileDocExtensions.kt +++ b/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 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) diff --git a/app/src/main/java/io/legado/app/utils/ZipUtils.kt b/app/src/main/java/io/legado/app/utils/ZipUtils.kt index 43656bcb4..21510084a 100644 --- a/app/src/main/java/io/legado/app/utils/ZipUtils.kt +++ b/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. *