357 lines
13 KiB
Kotlin
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} |