Files
QieKan-3.0/app/src/main/java/io/legato/kazusa/help/AppWebDav.kt
T
2025-07-10 04:25:54 +08:00

357 lines
13 KiB
Kotlin

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<String> {
val names = arrayListOf<String>()
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<WebDavFile?> {
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<List<WebDavFile>> {
return kotlin.runCatching {
if (!NetworkUtils.isAvailable())
throw NoStackTraceException("网络未连接")
authorization.let {
it ?: throw NoStackTraceException("webDav未配置")
WebDav(bgWebDavUrl, it).listFiles()
}
}
}
/**
* 上传背景图片
*/
suspend fun upBgs(files: Array<File>) {
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<BookProgress>(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<String, WebDavFile>()
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)
}
}
}
}
}