pull/540/head
gedoor 4 years ago
parent abb686a1ab
commit 66126d77a3
  1. 20
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  2. 2
      app/src/main/java/io/legado/app/help/http/parser/ByteParser.kt
  3. 14
      app/src/main/java/io/legado/app/help/http/parser/InputStreamParser.kt
  4. 2
      app/src/main/java/io/legado/app/help/http/parser/TextParser.kt
  5. 88
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  6. 42
      app/src/main/java/io/legado/app/help/storage/BookWebDav.kt
  7. 73
      app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
  8. 6
      app/src/main/java/io/legado/app/utils/ZipUtils.kt

@ -3,10 +3,12 @@ package io.legado.app.help.http
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.* import okhttp3.*
import retrofit2.Retrofit import retrofit2.Retrofit
import java.io.IOException
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Proxy import java.net.Proxy
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@Suppress("unused") @Suppress("unused")
object HttpHelper { object HttpHelper {
@ -35,6 +37,24 @@ object HttpHelper {
builder.build() builder.build()
} }
suspend fun awaitResponse(request: Request): Response = suspendCancellableCoroutine { block ->
val call = client.newCall(request)
block.invokeOnCancellation {
call.cancel()
}
call.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
block.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
block.resume(response)
}
})
}
inline fun <reified T> getApiService( inline fun <reified T> getApiService(
baseUrl: String, baseUrl: String,
encode: String? = null, encode: String? = null,

@ -1,4 +1,4 @@
package io.legado.app.help.http package io.legado.app.help.http.parser
import okhttp3.Response import okhttp3.Response
import rxhttp.wrapper.annotation.Parser import rxhttp.wrapper.annotation.Parser

@ -0,0 +1,14 @@
package io.legado.app.help.http.parser
import okhttp3.Response
import rxhttp.wrapper.annotation.Parser
import java.io.InputStream
@Parser(name = "InputStream")
class InputStreamParser : rxhttp.wrapper.parse.Parser<InputStream> {
override fun onParse(response: Response): InputStream {
return response.body()!!.byteStream()
}
}

@ -1,4 +1,4 @@
package io.legado.app.help.http package io.legado.app.help.http.parser
import io.legado.app.utils.EncodingDetect import io.legado.app.utils.EncodingDetect
import io.legado.app.utils.UTF8BOMFighter import io.legado.app.utils.UTF8BOMFighter

@ -56,54 +56,52 @@ object Backup {
suspend fun backup(context: Context, path: String, isAuto: Boolean = false) { suspend fun backup(context: Context, path: String, isAuto: Boolean = false) {
context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis()) context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis())
withContext(IO) { withContext(IO) {
synchronized(this@Backup) { FileUtils.deleteFile(backupPath)
FileUtils.deleteFile(backupPath) writeListToJson(App.db.bookDao.all, "bookshelf.json", backupPath)
writeListToJson(App.db.bookDao.all, "bookshelf.json", backupPath) writeListToJson(App.db.bookmarkDao.all, "bookmark.json", backupPath)
writeListToJson(App.db.bookmarkDao.all, "bookmark.json", backupPath) writeListToJson(App.db.bookGroupDao.all, "bookGroup.json", backupPath)
writeListToJson(App.db.bookGroupDao.all, "bookGroup.json", backupPath) writeListToJson(App.db.bookSourceDao.all, "bookSource.json", backupPath)
writeListToJson(App.db.bookSourceDao.all, "bookSource.json", backupPath) writeListToJson(App.db.rssSourceDao.all, "rssSource.json", backupPath)
writeListToJson(App.db.rssSourceDao.all, "rssSource.json", backupPath) writeListToJson(App.db.rssStarDao.all, "rssStar.json", backupPath)
writeListToJson(App.db.rssStarDao.all, "rssStar.json", backupPath) writeListToJson(App.db.replaceRuleDao.all, "replaceRule.json", backupPath)
writeListToJson(App.db.replaceRuleDao.all, "replaceRule.json", backupPath) writeListToJson(App.db.readRecordDao.all, "readRecord.json", backupPath)
writeListToJson(App.db.readRecordDao.all, "readRecord.json", backupPath) writeListToJson(App.db.searchKeywordDao.all, "searchHistory.json", backupPath)
writeListToJson(App.db.searchKeywordDao.all, "searchHistory.json", backupPath) writeListToJson(App.db.ruleSubDao.all, "sourceSub.json", backupPath)
writeListToJson(App.db.ruleSubDao.all, "sourceSub.json", backupPath) writeListToJson(App.db.txtTocRule.all, DefaultData.txtTocRuleFileName, backupPath)
writeListToJson(App.db.txtTocRule.all, DefaultData.txtTocRuleFileName, backupPath) writeListToJson(App.db.httpTTSDao.all, DefaultData.httpTtsFileName, backupPath)
writeListToJson(App.db.httpTTSDao.all, DefaultData.httpTtsFileName, backupPath) GSON.toJson(ReadBookConfig.configList).let {
GSON.toJson(ReadBookConfig.configList).let { FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.configFileName)
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.configFileName) .writeText(it)
.writeText(it) }
} GSON.toJson(ReadBookConfig.shareConfig).let {
GSON.toJson(ReadBookConfig.shareConfig).let { FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.shareConfigFileName)
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.shareConfigFileName) }
} GSON.toJson(ThemeConfig.configList).let {
GSON.toJson(ThemeConfig.configList).let { FileUtils.createFileIfNotExist(backupPath + File.separator + ThemeConfig.configFileName)
FileUtils.createFileIfNotExist(backupPath + File.separator + ThemeConfig.configFileName) .writeText(it)
.writeText(it) }
} Preferences.getSharedPreferences(App.INSTANCE, backupPath, "config")?.let { sp ->
Preferences.getSharedPreferences(App.INSTANCE, backupPath, "config")?.let { sp -> val edit = sp.edit()
val edit = sp.edit() App.INSTANCE.defaultSharedPreferences.all.map {
App.INSTANCE.defaultSharedPreferences.all.map { when (val value = it.value) {
when (val value = it.value) { is Int -> edit.putInt(it.key, value)
is Int -> edit.putInt(it.key, value) is Boolean -> edit.putBoolean(it.key, value)
is Boolean -> edit.putBoolean(it.key, value) is Long -> edit.putLong(it.key, value)
is Long -> edit.putLong(it.key, value) is Float -> edit.putFloat(it.key, value)
is Float -> edit.putFloat(it.key, value) is String -> edit.putString(it.key, value)
is String -> edit.putString(it.key, value) else -> Unit
else -> Unit
}
} }
edit.commit()
} }
BookWebDav.backUpWebDav(backupPath) edit.commit()
if (path.isContentScheme()) { }
copyBackup(context, Uri.parse(path), isAuto) BookWebDav.backUpWebDav(backupPath)
if (path.isContentScheme()) {
copyBackup(context, Uri.parse(path), isAuto)
} else {
if (path.isEmpty()) {
copyBackup(context.getExternalFilesDir(null)!!, false)
} else { } else {
if (path.isEmpty()) { copyBackup(File(path), isAuto)
copyBackup(context.getExternalFilesDir(null)!!, false)
} else {
copyBackup(File(path), isAuto)
}
} }
} }
} }

@ -39,7 +39,7 @@ object BookWebDav {
return url return url
} }
fun initWebDav(): Boolean { suspend fun initWebDav(): Boolean {
val account = App.INSTANCE.getPrefString(PreferKey.webDavAccount) val account = App.INSTANCE.getPrefString(PreferKey.webDavAccount)
val password = App.INSTANCE.getPrefString(PreferKey.webDavPassword) val password = App.INSTANCE.getPrefString(PreferKey.webDavPassword)
if (!account.isNullOrBlank() && !password.isNullOrBlank()) { if (!account.isNullOrBlank() && !password.isNullOrBlank()) {
@ -52,7 +52,7 @@ object BookWebDav {
} }
@Throws(Exception::class) @Throws(Exception::class)
private fun getWebDavFileNames(): ArrayList<String> { private suspend fun getWebDavFileNames(): ArrayList<String> {
val url = rootWebDavUrl val url = rootWebDavUrl
val names = arrayListOf<String>() val names = arrayListOf<String>()
if (initWebDav()) { if (initWebDav()) {
@ -77,7 +77,11 @@ object BookWebDav {
items = names items = names
) { _, index -> ) { _, index ->
if (index in 0 until names.size) { if (index in 0 until names.size) {
restoreWebDav(names[index]) Coroutine.async {
restoreWebDav(names[index])
}.onError {
App.INSTANCE.toast("WebDavError:${it.localizedMessage}")
}
} }
} }
} }
@ -86,22 +90,18 @@ object BookWebDav {
} }
} }
private fun restoreWebDav(name: String) { private suspend fun restoreWebDav(name: String) {
Coroutine.async { rootWebDavUrl.let {
rootWebDavUrl.let { val webDav = WebDav(it + name)
val webDav = WebDav(it + name) webDav.downloadTo(zipFilePath, true)
webDav.downloadTo(zipFilePath, true) @Suppress("BlockingMethodInNonBlockingContext")
@Suppress("BlockingMethodInNonBlockingContext") ZipUtils.unzipFile(zipFilePath, Backup.backupPath)
ZipUtils.unzipFile(zipFilePath, Backup.backupPath) Restore.restoreDatabase()
Restore.restoreDatabase() Restore.restoreConfig()
Restore.restoreConfig()
}
}.onError {
App.INSTANCE.toast("WebDavError:${it.localizedMessage}")
} }
} }
fun backUpWebDav(path: String) { suspend fun backUpWebDav(path: String) {
try { try {
if (initWebDav()) { if (initWebDav()) {
val paths = arrayListOf(*Backup.backupFileNames) val paths = arrayListOf(*Backup.backupFileNames)
@ -123,7 +123,7 @@ object BookWebDav {
} }
} }
fun exportWebDav(path: String, fileName: String) { suspend fun exportWebDav(path: String, fileName: String) {
try { try {
if (initWebDav()) { if (initWebDav()) {
// 默认导出到legado文件夹下exports目录 // 默认导出到legado文件夹下exports目录
@ -155,16 +155,16 @@ object BookWebDav {
durChapterTitle = book.durChapterTitle durChapterTitle = book.durChapterTitle
) )
val json = GSON.toJson(bookProgress) val json = GSON.toJson(bookProgress)
val url = geProtresstUrl(book) val url = getProgressUrl(book)
if (initWebDav()) { if (initWebDav()) {
WebDav(url).upload(json.toByteArray()) WebDav(url).upload(json.toByteArray())
} }
} }
} }
fun getBookProgress(book: Book): BookProgress? { suspend fun getBookProgress(book: Book): BookProgress? {
if (initWebDav()) { if (initWebDav()) {
val url = geProtresstUrl(book) val url = getProgressUrl(book)
WebDav(url).download()?.let { byteArray -> WebDav(url).download()?.let { byteArray ->
val json = String(byteArray) val json = String(byteArray)
GSON.fromJsonObject<BookProgress>(json)?.let { GSON.fromJsonObject<BookProgress>(json)?.let {
@ -175,7 +175,7 @@ object BookWebDav {
return null return null
} }
private fun geProtresstUrl(book: Book): String { private fun getProgressUrl(book: Book): String {
return bookProgressUrl + book.name + "_" + book.author + ".json" return bookProgressUrl + book.name + "_" + book.author + ".json"
} }
} }

@ -3,6 +3,8 @@ package io.legado.app.lib.webdav
import io.legado.app.help.http.HttpHelper import io.legado.app.help.http.HttpHelper
import okhttp3.* import okhttp3.*
import org.jsoup.Jsoup import org.jsoup.Jsoup
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toInputStream
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@ -54,17 +56,16 @@ class WebDav(urlStr: String) {
/** /**
* 填充文件信息实例化WebDAVFile对象时并没有将远程文件的信息填充到实例中需要手动填充 * 填充文件信息实例化WebDAVFile对象时并没有将远程文件的信息填充到实例中需要手动填充
*
* @return 远程文件是否存在 * @return 远程文件是否存在
*/ */
@Throws(IOException::class) suspend fun indexFileInfo(): Boolean {
fun indexFileInfo(): Boolean {
propFindResponse(ArrayList())?.let { response -> propFindResponse(ArrayList())?.let { response ->
if (!response.isSuccessful) { if (!response.isSuccessful) {
this.exists = false this.exists = false
return false return false
} }
response.body()?.let { response.body()?.let {
@Suppress("BlockingMethodInNonBlockingContext")
if (it.string().isNotEmpty()) { if (it.string().isNotEmpty()) {
return true return true
} }
@ -79,12 +80,11 @@ class WebDav(urlStr: String) {
* @param propsList 指定列出文件的哪些属性 * @param propsList 指定列出文件的哪些属性
* @return 文件列表 * @return 文件列表
*/ */
@Throws(IOException::class) suspend fun listFiles(propsList: ArrayList<String> = ArrayList()): List<WebDav> {
@JvmOverloads
fun listFiles(propsList: ArrayList<String> = ArrayList()): List<WebDav> {
propFindResponse(propsList)?.let { response -> propFindResponse(propsList)?.let { response ->
if (response.isSuccessful) { if (response.isSuccessful) {
response.body()?.let { body -> response.body()?.let { body ->
@Suppress("BlockingMethodInNonBlockingContext")
return parseDir(body.string()) return parseDir(body.string())
} }
} }
@ -93,7 +93,7 @@ class WebDav(urlStr: String) {
} }
@Throws(IOException::class) @Throws(IOException::class)
private fun propFindResponse(propsList: ArrayList<String>, depth: Int = 1): Response? { private suspend fun propFindResponse(propsList: ArrayList<String>, depth: Int = 1): Response? {
val requestProps = StringBuilder() val requestProps = StringBuilder()
for (p in propsList) { for (p in propsList) {
requestProps.append("<a:").append(p).append("/>\n") requestProps.append("<a:").append(p).append("/>\n")
@ -105,23 +105,18 @@ class WebDav(urlStr: String) {
String.format(DIR, requestProps.toString() + "\n") String.format(DIR, requestProps.toString() + "\n")
} }
httpUrl?.let { url -> httpUrl?.let { url ->
// 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性
// 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。
val requestBody = RequestBody.create(MediaType.parse("text/plain"), requestPropsStr)
val request = Request.Builder() val request = Request.Builder()
.url(url) .url(url)
// 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性 .method("PROPFIND", requestBody)
// 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。
.method(
"PROPFIND",
RequestBody.create(MediaType.parse("text/plain"), requestPropsStr)
)
HttpAuth.auth?.let { HttpAuth.auth?.let {
request.header( request.header("Authorization", Credentials.basic(it.user, it.pass))
"Authorization",
Credentials.basic(it.user, it.pass)
)
} }
request.header("Depth", if (depth < 0) "infinity" else depth.toString()) request.header("Depth", if (depth < 0) "infinity" else depth.toString())
return HttpHelper.client.newCall(request.build()).execute() return HttpHelper.awaitResponse(request.build())
} }
return null return null
} }
@ -165,8 +160,7 @@ class WebDav(urlStr: String) {
* *
* @return 是否创建成功 * @return 是否创建成功
*/ */
@Throws(IOException::class) suspend fun makeAsDir(): Boolean {
fun makeAsDir(): Boolean {
httpUrl?.let { url -> httpUrl?.let { url ->
val request = Request.Builder() val request = Request.Builder()
.url(url) .url(url)
@ -183,7 +177,7 @@ class WebDav(urlStr: String) {
* @param replaceExisting 是否替换本地的同名文件 * @param replaceExisting 是否替换本地的同名文件
* @return 下载是否成功 * @return 下载是否成功
*/ */
fun downloadTo(savedPath: String, replaceExisting: Boolean): Boolean { suspend fun downloadTo(savedPath: String, replaceExisting: Boolean): Boolean {
if (File(savedPath).exists()) { if (File(savedPath).exists()) {
if (!replaceExisting) return false if (!replaceExisting) return false
} }
@ -192,7 +186,7 @@ class WebDav(urlStr: String) {
return true return true
} }
fun download(): ByteArray? { suspend fun download(): ByteArray? {
val inputS = getInputStream() ?: return null val inputS = getInputStream() ?: return null
return inputS.readBytes() return inputS.readBytes()
} }
@ -200,8 +194,7 @@ class WebDav(urlStr: String) {
/** /**
* 上传文件 * 上传文件
*/ */
@Throws(IOException::class) suspend fun upload(localPath: String, contentType: String? = null): Boolean {
fun upload(localPath: String, contentType: String? = null): Boolean {
val file = File(localPath) val file = File(localPath)
if (!file.exists()) return false if (!file.exists()) return false
val mediaType = contentType?.let { MediaType.parse(it) } val mediaType = contentType?.let { MediaType.parse(it) }
@ -216,7 +209,7 @@ class WebDav(urlStr: String) {
return false return false
} }
fun upload(byteArray: ByteArray, contentType: String? = null): Boolean { suspend fun upload(byteArray: ByteArray, contentType: String? = null): Boolean {
val mediaType = contentType?.let { MediaType.parse(it) } val mediaType = contentType?.let { MediaType.parse(it) }
// 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息 // 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息
val fileBody = RequestBody.create(mediaType, byteArray) val fileBody = RequestBody.create(mediaType, byteArray)
@ -235,30 +228,22 @@ class WebDav(urlStr: String) {
* @return 请求执行的结果 * @return 请求执行的结果
*/ */
@Throws(IOException::class) @Throws(IOException::class)
private fun execRequest(requestBuilder: Request.Builder): Boolean { private suspend fun execRequest(requestBuilder: Request.Builder): Boolean {
HttpAuth.auth?.let { HttpAuth.auth?.let {
requestBuilder.header( requestBuilder.header("Authorization", Credentials.basic(it.user, it.pass))
"Authorization",
Credentials.basic(it.user, it.pass)
)
} }
val response = HttpHelper.client.newCall(requestBuilder.build()).execute() val response = HttpHelper.awaitResponse(requestBuilder.build())
return response.isSuccessful return response.isSuccessful
} }
private fun getInputStream(): InputStream? { @Throws(IOException::class)
httpUrl?.let { url -> private suspend fun getInputStream(): InputStream? {
val request = Request.Builder().url(url) val url = httpUrl
HttpAuth.auth?.let { val auth = HttpAuth.auth
request.header("Authorization", Credentials.basic(it.user, it.pass)) if (url != null && auth != null) {
} return RxHttp.get(url)
try { .addHeader("Authorization", Credentials.basic(auth.user, auth.pass))
return HttpHelper.client.newCall(request.build()).execute().body()?.byteStream() .toInputStream().await()
} catch (e: IOException) {
e.printStackTrace()
} catch (e: IllegalArgumentException) {
e.printStackTrace()
}
} }
return null return null
} }

@ -18,8 +18,7 @@ object ZipUtils {
* @return `true`: success<br></br>`false`: fail * @return `true`: success<br></br>`false`: fail
* @throws IOException if an I/O error has occurred * @throws IOException if an I/O error has occurred
*/ */
@Throws(IOException::class) suspend fun zipFiles(
fun zipFiles(
srcFiles: Collection<String>, srcFiles: Collection<String>,
zipFilePath: String zipFilePath: String
): Boolean { ): Boolean {
@ -35,8 +34,7 @@ object ZipUtils {
* @return `true`: success<br></br>`false`: fail * @return `true`: success<br></br>`false`: fail
* @throws IOException if an I/O error has occurred * @throws IOException if an I/O error has occurred
*/ */
@Throws(IOException::class) suspend fun zipFiles(
fun zipFiles(
srcFilePaths: Collection<String>?, srcFilePaths: Collection<String>?,
zipFilePath: String?, zipFilePath: String?,
comment: String? comment: String?

Loading…
Cancel
Save