package io.legato.kazusa.help import android.net.Uri import io.legato.kazusa.R import io.legato.kazusa.constant.AppLog import io.legato.kazusa.constant.PreferKey import io.legato.kazusa.data.appDb import io.legato.kazusa.data.entities.Book import io.legato.kazusa.data.entities.BookProgress import io.legato.kazusa.exception.NoStackTraceException import io.legato.kazusa.help.config.AppConfig import io.legato.kazusa.help.storage.Backup import io.legato.kazusa.help.storage.Restore import io.legato.kazusa.lib.webdav.Authorization import io.legato.kazusa.lib.webdav.WebDav import io.legato.kazusa.lib.webdav.WebDavException import io.legato.kazusa.lib.webdav.WebDavFile import io.legato.kazusa.model.remote.RemoteBookWebDav import io.legato.kazusa.utils.AlphanumComparator import io.legato.kazusa.utils.FileUtils import io.legato.kazusa.utils.GSON import io.legato.kazusa.utils.NetworkUtils import io.legato.kazusa.utils.UrlUtil import io.legato.kazusa.utils.compress.ZipUtils import io.legato.kazusa.utils.fromJsonObject import io.legato.kazusa.utils.getPrefString import io.legato.kazusa.utils.isJson import io.legato.kazusa.utils.toastOnUi import kotlinx.coroutines.ensureActive import kotlinx.coroutines.runBlocking import splitties.init.appCtx import java.io.File import kotlin.coroutines.coroutineContext /** * webDav初始化会访问网络,不要放到主线程 */ object AppWebDav { private const val defaultWebDavUrl = "https://dav.jianguoyun.com/dav/" private val bookProgressUrl get() = "${rootWebDavUrl}bookProgress/" private val exportsWebDavUrl get() = "${rootWebDavUrl}books/" private val bgWebDavUrl get() = "${rootWebDavUrl}background/" var authorization: Authorization? = null private set var defaultBookWebDav: RemoteBookWebDav? = null val isOk get() = authorization != null val isJianGuoYun get() = rootWebDavUrl.startsWith(defaultWebDavUrl, true) init { runBlocking { upConfig() } } private val rootWebDavUrl: String get() { val configUrl = appCtx.getPrefString(PreferKey.webDavUrl)?.trim() var url = if (configUrl.isNullOrEmpty()) defaultWebDavUrl else configUrl if (!url.endsWith("/")) url = "${url}/" AppConfig.webDavDir?.trim()?.let { if (it.isNotEmpty()) { url = "${url}${it}/" } } return url } suspend fun upConfig() { kotlin.runCatching { authorization = null defaultBookWebDav = null val account = appCtx.getPrefString(PreferKey.webDavAccount) val password = appCtx.getPrefString(PreferKey.webDavPassword) if (!account.isNullOrEmpty() && !password.isNullOrEmpty()) { val mAuthorization = Authorization(account, password) checkAuthorization(mAuthorization) WebDav(rootWebDavUrl, mAuthorization).makeAsDir() WebDav(bookProgressUrl, mAuthorization).makeAsDir() WebDav(exportsWebDavUrl, mAuthorization).makeAsDir() WebDav(bgWebDavUrl, mAuthorization).makeAsDir() val rootBooksUrl = "${rootWebDavUrl}books/" defaultBookWebDav = RemoteBookWebDav(rootBooksUrl, mAuthorization) authorization = mAuthorization } } } @Throws(WebDavException::class) private suspend fun checkAuthorization(authorization: Authorization) { if (!WebDav(rootWebDavUrl, authorization).check()) { //appCtx.removePref(PreferKey.webDavPassword) appCtx.toastOnUi(R.string.webdav_application_authorization_error) throw WebDavException(appCtx.getString(R.string.webdav_application_authorization_error)) } } @Throws(Exception::class) suspend fun getBackupNames(): ArrayList { val names = arrayListOf() authorization?.let { var files = WebDav(rootWebDavUrl, it).listFiles() files = files.sortedWith { o1, o2 -> AlphanumComparator.compare(o1.displayName, o2.displayName) }.reversed() files.forEach { webDav -> val name = webDav.displayName if (name.startsWith("backup")) { names.add(name) } } } ?: throw NoStackTraceException("webDav没有配置") return names } @Throws(WebDavException::class) suspend fun restoreWebDav(name: String) { authorization?.let { val webDav = WebDav(rootWebDavUrl + name, it) webDav.downloadTo(Backup.zipFilePath, true) FileUtils.delete(Backup.backupPath) ZipUtils.unZipToPath(File(Backup.zipFilePath), Backup.backupPath) Restore.restoreLocked(Backup.backupPath) } } suspend fun hasBackUp(backUpName: String): Boolean { authorization?.let { val url = "$rootWebDavUrl${backUpName}" return WebDav(url, it).exists() } return false } suspend fun lastBackUp(): Result { return kotlin.runCatching { authorization?.let { var lastBackupFile: WebDavFile? = null WebDav(rootWebDavUrl, it).listFiles().reversed().forEach { webDavFile -> if (webDavFile.displayName.startsWith("backup")) { if (lastBackupFile == null || webDavFile.lastModify > lastBackupFile.lastModify ) { lastBackupFile = webDavFile } } } lastBackupFile } } } suspend fun testWebDav(): Boolean { return kotlin.runCatching { val account = appCtx.getPrefString(PreferKey.webDavAccount) val password = appCtx.getPrefString(PreferKey.webDavPassword) if (account.isNullOrEmpty() || password.isNullOrEmpty()) { appCtx.toastOnUi("账号或密码为空") return false } val auth = Authorization(account, password) checkAuthorization(auth) appCtx.toastOnUi("WebDAV 服务可用") true }.getOrElse { it.printStackTrace() if (it !is WebDavException) { appCtx.toastOnUi(it.message ?: "未知错误") } false } } /** * webDav备份 * @param fileName 备份文件名 */ @Throws(Exception::class) suspend fun backUpWebDav(fileName: String) { if (!NetworkUtils.isAvailable()) return authorization?.let { val putUrl = "$rootWebDavUrl$fileName" WebDav(putUrl, it).upload(Backup.zipFilePath) } } /** * 获取云端所有背景名称 */ private suspend fun getAllBgWebDavFiles(): Result> { return kotlin.runCatching { if (!NetworkUtils.isAvailable()) throw NoStackTraceException("网络未连接") authorization.let { it ?: throw NoStackTraceException("webDav未配置") WebDav(bgWebDavUrl, it).listFiles() } } } /** * 上传背景图片 */ suspend fun upBgs(files: Array) { val authorization = authorization ?: return if (!NetworkUtils.isAvailable()) return val bgWebDavFiles = getAllBgWebDavFiles().getOrThrow() .map { it.displayName } .toSet() files.forEach { if (!bgWebDavFiles.contains(it.name) && it.exists()) { WebDav("$bgWebDavUrl${it.name}", authorization) .upload(it) } } } /** * 下载背景图片 */ suspend fun downBgs() { val authorization = authorization ?: return if (!NetworkUtils.isAvailable()) return val bgWebDavFiles = getAllBgWebDavFiles().getOrThrow() .map { it.displayName } .toSet() } @Suppress("unused") suspend fun exportWebDav(byteArray: ByteArray, fileName: String) { if (!NetworkUtils.isAvailable()) return try { authorization?.let { // 如果导出的本地文件存在,开始上传 val putUrl = exportsWebDavUrl + fileName WebDav(putUrl, it).upload(byteArray, "text/plain") } } catch (e: Exception) { coroutineContext.ensureActive() AppLog.put("WebDav导出失败\n${e.localizedMessage}", e, true) } } suspend fun exportWebDav(uri: Uri, fileName: String) { if (!NetworkUtils.isAvailable()) return try { authorization?.let { // 如果导出的本地文件存在,开始上传 val putUrl = exportsWebDavUrl + fileName WebDav(putUrl, it).upload(uri, "text/plain") } } catch (e: Exception) { coroutineContext.ensureActive() AppLog.put("WebDav导出失败\n${e.localizedMessage}", e, true) } } suspend fun uploadBookProgress(book: Book) { val authorization = authorization ?: return if (!AppConfig.syncBookProgress) return if (!NetworkUtils.isAvailable()) return try { val bookProgress = BookProgress(book) val json = GSON.toJson(bookProgress) val url = getProgressUrl(book.name, book.author) WebDav(url, authorization).upload(json.toByteArray(), "application/json") book.syncTime = System.currentTimeMillis() } catch (e: Exception) { coroutineContext.ensureActive() AppLog.put("上传进度失败\n${e.localizedMessage}", e) } } suspend fun uploadBookProgress(bookProgress: BookProgress, onSuccess: (() -> Unit)? = null) { try { val authorization = authorization ?: return if (!AppConfig.syncBookProgress) return if (!NetworkUtils.isAvailable()) return val json = GSON.toJson(bookProgress) val url = getProgressUrl(bookProgress.name, bookProgress.author) WebDav(url, authorization).upload(json.toByteArray(), "application/json") onSuccess?.invoke() } catch (e: Exception) { coroutineContext.ensureActive() AppLog.put("上传进度失败\n${e.localizedMessage}", e) } } private fun getProgressUrl(name: String, author: String): String { return bookProgressUrl + getProgressFileName(name, author) } private fun getProgressFileName(name: String, author: String): String { return UrlUtil.replaceReservedChar("${name}_${author}") + ".json" } /** * 获取书籍进度 */ suspend fun getBookProgress(book: Book): BookProgress? { authorization?.let { val url = getProgressUrl(book.name, book.author) kotlin.runCatching { WebDav(url, it).download().let { byteArray -> val json = String(byteArray) if (json.isJson()) { return GSON.fromJsonObject(json).getOrNull() } } }.onFailure { coroutineContext.ensureActive() AppLog.put("获取书籍进度失败\n${it.localizedMessage}", it) } } return null } suspend fun downloadAllBookProgress() { val authorization = authorization ?: return if (!NetworkUtils.isAvailable()) return val bookProgressFiles = WebDav(bookProgressUrl, authorization).listFiles() val map = hashMapOf() bookProgressFiles.forEach { map[it.displayName] = it } appDb.bookDao.all.forEach { book -> val progressFileName = getProgressFileName(book.name, book.author) val webDavFile = map[progressFileName] webDavFile ?: return if (webDavFile.lastModify <= book.syncTime) { //本地同步时间大于上传时间不用同步 return } getBookProgress(book)?.let { bookProgress -> if (bookProgress.durChapterIndex > book.durChapterIndex || (bookProgress.durChapterIndex == book.durChapterIndex && bookProgress.durChapterPos > book.durChapterPos) ) { book.durChapterIndex = bookProgress.durChapterIndex book.durChapterPos = bookProgress.durChapterPos book.durChapterTitle = bookProgress.durChapterTitle book.durChapterTime = bookProgress.durChapterTime book.syncTime = System.currentTimeMillis() appDb.bookDao.update(book) } } } } }