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 okhttp3.*
import retrofit2.Retrofit
import java.io.IOException
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@Suppress("unused")
object HttpHelper {
@ -35,6 +37,24 @@ object HttpHelper {
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(
baseUrl: String,
encode: String? = null,

@ -1,4 +1,4 @@
package io.legado.app.help.http
package io.legado.app.help.http.parser
import okhttp3.Response
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.UTF8BOMFighter

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

@ -39,7 +39,7 @@ object BookWebDav {
return url
}
fun initWebDav(): Boolean {
suspend fun initWebDav(): Boolean {
val account = App.INSTANCE.getPrefString(PreferKey.webDavAccount)
val password = App.INSTANCE.getPrefString(PreferKey.webDavPassword)
if (!account.isNullOrBlank() && !password.isNullOrBlank()) {
@ -52,7 +52,7 @@ object BookWebDav {
}
@Throws(Exception::class)
private fun getWebDavFileNames(): ArrayList<String> {
private suspend fun getWebDavFileNames(): ArrayList<String> {
val url = rootWebDavUrl
val names = arrayListOf<String>()
if (initWebDav()) {
@ -77,7 +77,11 @@ object BookWebDav {
items = names
) { _, index ->
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) {
Coroutine.async {
rootWebDavUrl.let {
val webDav = WebDav(it + name)
webDav.downloadTo(zipFilePath, true)
@Suppress("BlockingMethodInNonBlockingContext")
ZipUtils.unzipFile(zipFilePath, Backup.backupPath)
Restore.restoreDatabase()
Restore.restoreConfig()
}
}.onError {
App.INSTANCE.toast("WebDavError:${it.localizedMessage}")
private suspend fun restoreWebDav(name: String) {
rootWebDavUrl.let {
val webDav = WebDav(it + name)
webDav.downloadTo(zipFilePath, true)
@Suppress("BlockingMethodInNonBlockingContext")
ZipUtils.unzipFile(zipFilePath, Backup.backupPath)
Restore.restoreDatabase()
Restore.restoreConfig()
}
}
fun backUpWebDav(path: String) {
suspend fun backUpWebDav(path: String) {
try {
if (initWebDav()) {
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 {
if (initWebDav()) {
// 默认导出到legado文件夹下exports目录
@ -155,16 +155,16 @@ object BookWebDav {
durChapterTitle = book.durChapterTitle
)
val json = GSON.toJson(bookProgress)
val url = geProtresstUrl(book)
val url = getProgressUrl(book)
if (initWebDav()) {
WebDav(url).upload(json.toByteArray())
}
}
}
fun getBookProgress(book: Book): BookProgress? {
suspend fun getBookProgress(book: Book): BookProgress? {
if (initWebDav()) {
val url = geProtresstUrl(book)
val url = getProgressUrl(book)
WebDav(url).download()?.let { byteArray ->
val json = String(byteArray)
GSON.fromJsonObject<BookProgress>(json)?.let {
@ -175,7 +175,7 @@ object BookWebDav {
return null
}
private fun geProtresstUrl(book: Book): String {
private fun getProgressUrl(book: Book): String {
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 okhttp3.*
import org.jsoup.Jsoup
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toInputStream
import java.io.File
import java.io.IOException
import java.io.InputStream
@ -54,17 +56,16 @@ class WebDav(urlStr: String) {
/**
* 填充文件信息实例化WebDAVFile对象时并没有将远程文件的信息填充到实例中需要手动填充
*
* @return 远程文件是否存在
*/
@Throws(IOException::class)
fun indexFileInfo(): Boolean {
suspend fun indexFileInfo(): Boolean {
propFindResponse(ArrayList())?.let { response ->
if (!response.isSuccessful) {
this.exists = false
return false
}
response.body()?.let {
@Suppress("BlockingMethodInNonBlockingContext")
if (it.string().isNotEmpty()) {
return true
}
@ -79,12 +80,11 @@ class WebDav(urlStr: String) {
* @param propsList 指定列出文件的哪些属性
* @return 文件列表
*/
@Throws(IOException::class)
@JvmOverloads
fun listFiles(propsList: ArrayList<String> = ArrayList()): List<WebDav> {
suspend fun listFiles(propsList: ArrayList<String> = ArrayList()): List<WebDav> {
propFindResponse(propsList)?.let { response ->
if (response.isSuccessful) {
response.body()?.let { body ->
@Suppress("BlockingMethodInNonBlockingContext")
return parseDir(body.string())
}
}
@ -93,7 +93,7 @@ class WebDav(urlStr: String) {
}
@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()
for (p in propsList) {
requestProps.append("<a:").append(p).append("/>\n")
@ -105,23 +105,18 @@ class WebDav(urlStr: String) {
String.format(DIR, requestProps.toString() + "\n")
}
httpUrl?.let { url ->
// 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性
// 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。
val requestBody = RequestBody.create(MediaType.parse("text/plain"), requestPropsStr)
val request = Request.Builder()
.url(url)
// 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性
// 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。
.method(
"PROPFIND",
RequestBody.create(MediaType.parse("text/plain"), requestPropsStr)
)
.method("PROPFIND", requestBody)
HttpAuth.auth?.let {
request.header(
"Authorization",
Credentials.basic(it.user, it.pass)
)
request.header("Authorization", Credentials.basic(it.user, it.pass))
}
request.header("Depth", if (depth < 0) "infinity" else depth.toString())
return HttpHelper.client.newCall(request.build()).execute()
return HttpHelper.awaitResponse(request.build())
}
return null
}
@ -165,8 +160,7 @@ class WebDav(urlStr: String) {
*
* @return 是否创建成功
*/
@Throws(IOException::class)
fun makeAsDir(): Boolean {
suspend fun makeAsDir(): Boolean {
httpUrl?.let { url ->
val request = Request.Builder()
.url(url)
@ -183,7 +177,7 @@ class WebDav(urlStr: String) {
* @param replaceExisting 是否替换本地的同名文件
* @return 下载是否成功
*/
fun downloadTo(savedPath: String, replaceExisting: Boolean): Boolean {
suspend fun downloadTo(savedPath: String, replaceExisting: Boolean): Boolean {
if (File(savedPath).exists()) {
if (!replaceExisting) return false
}
@ -192,7 +186,7 @@ class WebDav(urlStr: String) {
return true
}
fun download(): ByteArray? {
suspend fun download(): ByteArray? {
val inputS = getInputStream() ?: return null
return inputS.readBytes()
}
@ -200,8 +194,7 @@ class WebDav(urlStr: String) {
/**
* 上传文件
*/
@Throws(IOException::class)
fun upload(localPath: String, contentType: String? = null): Boolean {
suspend fun upload(localPath: String, contentType: String? = null): Boolean {
val file = File(localPath)
if (!file.exists()) return false
val mediaType = contentType?.let { MediaType.parse(it) }
@ -216,7 +209,7 @@ class WebDav(urlStr: String) {
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) }
// 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息
val fileBody = RequestBody.create(mediaType, byteArray)
@ -235,30 +228,22 @@ class WebDav(urlStr: String) {
* @return 请求执行的结果
*/
@Throws(IOException::class)
private fun execRequest(requestBuilder: Request.Builder): Boolean {
private suspend fun execRequest(requestBuilder: Request.Builder): Boolean {
HttpAuth.auth?.let {
requestBuilder.header(
"Authorization",
Credentials.basic(it.user, it.pass)
)
requestBuilder.header("Authorization", Credentials.basic(it.user, it.pass))
}
val response = HttpHelper.client.newCall(requestBuilder.build()).execute()
val response = HttpHelper.awaitResponse(requestBuilder.build())
return response.isSuccessful
}
private fun getInputStream(): InputStream? {
httpUrl?.let { url ->
val request = Request.Builder().url(url)
HttpAuth.auth?.let {
request.header("Authorization", Credentials.basic(it.user, it.pass))
}
try {
return HttpHelper.client.newCall(request.build()).execute().body()?.byteStream()
} catch (e: IOException) {
e.printStackTrace()
} catch (e: IllegalArgumentException) {
e.printStackTrace()
}
@Throws(IOException::class)
private suspend fun getInputStream(): InputStream? {
val url = httpUrl
val auth = HttpAuth.auth
if (url != null && auth != null) {
return RxHttp.get(url)
.addHeader("Authorization", Credentials.basic(auth.user, auth.pass))
.toInputStream().await()
}
return null
}

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

Loading…
Cancel
Save