Merge branch 'master' of https://github.com/gedoor/legado
commit
4b8f6e3f9b
@ -0,0 +1 @@ |
|||||||
|
## 用于网络备份的WebDav |
@ -0,0 +1,297 @@ |
|||||||
|
package io.legado.app.lib.webdav |
||||||
|
|
||||||
|
import io.legado.app.lib.webdav.http.Handler |
||||||
|
import io.legado.app.lib.webdav.http.HttpAuth |
||||||
|
import io.legado.app.lib.webdav.http.OkHttp |
||||||
|
|
||||||
|
import org.jsoup.Jsoup |
||||||
|
import org.jsoup.nodes.Document |
||||||
|
import org.jsoup.nodes.Element |
||||||
|
import org.jsoup.select.Elements |
||||||
|
|
||||||
|
import java.io.File |
||||||
|
import java.io.FileOutputStream |
||||||
|
import java.io.IOException |
||||||
|
import java.io.InputStream |
||||||
|
import java.io.UnsupportedEncodingException |
||||||
|
import java.lang.reflect.Field |
||||||
|
import java.net.MalformedURLException |
||||||
|
import java.net.URL |
||||||
|
import java.net.URLEncoder |
||||||
|
import java.util.ArrayList |
||||||
|
|
||||||
|
import okhttp3.Credentials |
||||||
|
import okhttp3.MediaType |
||||||
|
import okhttp3.OkHttpClient |
||||||
|
import okhttp3.Request |
||||||
|
import okhttp3.RequestBody |
||||||
|
import okhttp3.Response |
||||||
|
|
||||||
|
class WebDav @Throws(MalformedURLException::class) |
||||||
|
constructor(url: String) { |
||||||
|
private val url: URL = URL(null, url, Handler.HANDLER) |
||||||
|
private var httpUrl: String? = null |
||||||
|
|
||||||
|
var displayName: String? = null |
||||||
|
var size: Long = 0 |
||||||
|
private var exists = false |
||||||
|
var parent = "" |
||||||
|
var urlName = "" |
||||||
|
get() { |
||||||
|
if (field.isEmpty()) { |
||||||
|
this.urlName = (if (parent.isEmpty()) url.file else url.toString().replace(parent, "")).replace("/", "") |
||||||
|
} |
||||||
|
return field |
||||||
|
} |
||||||
|
|
||||||
|
private val okHttpClient: OkHttpClient = OkHttp.SingletonHelper.okHttpClient |
||||||
|
|
||||||
|
val path: String |
||||||
|
get() = url.toString() |
||||||
|
|
||||||
|
private val inputStream: InputStream? |
||||||
|
get() = getUrl()?.let {url -> |
||||||
|
val request = Request.Builder().url(url) |
||||||
|
HttpAuth.auth?.let {request.header("Authorization", Credentials.basic(it.user, it.pass)) } |
||||||
|
|
||||||
|
try { |
||||||
|
return okHttpClient.newCall(request.build()).execute().body()?.byteStream() |
||||||
|
} catch (e: IOException) { |
||||||
|
e.printStackTrace() |
||||||
|
} catch (e: IllegalArgumentException) { |
||||||
|
e.printStackTrace() |
||||||
|
} |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
val host: String |
||||||
|
get() = url.host |
||||||
|
|
||||||
|
private fun getUrl(): String? { |
||||||
|
if (httpUrl == null) { |
||||||
|
val raw = url.toString().replace("davs://", "https://").replace("dav://", "http://") |
||||||
|
try { |
||||||
|
httpUrl = URLEncoder.encode(raw, "UTF-8") |
||||||
|
.replace("\\+".toRegex(), "%20") |
||||||
|
.replace("%3A".toRegex(), ":") |
||||||
|
.replace("%2F".toRegex(), "/") |
||||||
|
} catch (e: UnsupportedEncodingException) { |
||||||
|
e.printStackTrace() |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
return httpUrl |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 填充文件信息。实例化WebDAVFile对象时,并没有将远程文件的信息填充到实例中。需要手动填充! |
||||||
|
* |
||||||
|
* @return 远程文件是否存在 |
||||||
|
*/ |
||||||
|
@Throws(IOException::class) |
||||||
|
fun indexFileInfo(): Boolean { |
||||||
|
propFindResponse(ArrayList())?.let { response -> |
||||||
|
if (!response.isSuccessful) { |
||||||
|
this.exists = false |
||||||
|
return false |
||||||
|
} |
||||||
|
response.body()?.let { |
||||||
|
if (it.string().isNotEmpty()) { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 列出当前路径下的文件 |
||||||
|
* |
||||||
|
* @param propsList 指定列出文件的哪些属性 |
||||||
|
* @return 文件列表 |
||||||
|
*/ |
||||||
|
@Throws(IOException::class) |
||||||
|
@JvmOverloads |
||||||
|
fun listFiles(propsList: ArrayList<String> = ArrayList()): List<WebDav> { |
||||||
|
propFindResponse(propsList)?.let {response-> |
||||||
|
if (response.isSuccessful) { |
||||||
|
response.body()?.let {body-> |
||||||
|
return parseDir(body.string()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
return ArrayList() |
||||||
|
} |
||||||
|
|
||||||
|
@Throws(IOException::class) |
||||||
|
private fun propFindResponse(propsList: ArrayList<String>, depth: Int = 1): Response? { |
||||||
|
val requestProps = StringBuilder() |
||||||
|
for (p in propsList) { |
||||||
|
requestProps.append("<a:").append(p).append("/>\n") |
||||||
|
} |
||||||
|
val requestPropsStr: String |
||||||
|
requestPropsStr = if (requestProps.toString().isEmpty()) { |
||||||
|
DIR.replace("%s", "") |
||||||
|
} else { |
||||||
|
String.format(DIR, requestProps.toString() + "\n") |
||||||
|
} |
||||||
|
getUrl()?.let {url-> |
||||||
|
val request = Request.Builder() |
||||||
|
.url(url) |
||||||
|
// 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性 |
||||||
|
// 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。 |
||||||
|
.method("PROPFIND", RequestBody.create(MediaType.parse("text/plain"), requestPropsStr)) |
||||||
|
|
||||||
|
HttpAuth.auth?.let {request.header("Authorization", Credentials.basic(it.user, it.pass)) } |
||||||
|
|
||||||
|
request.header("Depth", if (depth < 0) "infinity" else Integer.toString(depth)) |
||||||
|
|
||||||
|
return okHttpClient.newCall(request.build()).execute() |
||||||
|
} |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
private fun parseDir(s: String): List<WebDav> { |
||||||
|
val list = ArrayList<WebDav>() |
||||||
|
val document = Jsoup.parse(s) |
||||||
|
val elements = document.getElementsByTag("d:response") |
||||||
|
getUrl()?.let { url-> |
||||||
|
val baseUrl = if (url.endsWith("/")) url else "$url/" |
||||||
|
for (element in elements) { |
||||||
|
val href = element.getElementsByTag("d:href")[0].text() |
||||||
|
if (!href.endsWith("/")) { |
||||||
|
val fileName = href.substring(href.lastIndexOf("/") + 1) |
||||||
|
val webDavFile: WebDav |
||||||
|
try { |
||||||
|
webDavFile = WebDav(baseUrl + fileName) |
||||||
|
webDavFile.displayName = fileName |
||||||
|
webDavFile.urlName = href |
||||||
|
list.add(webDavFile) |
||||||
|
} catch (e: MalformedURLException) { |
||||||
|
e.printStackTrace() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return list |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 根据自己的URL,在远程处创建对应的文件夹 |
||||||
|
* |
||||||
|
* @return 是否创建成功 |
||||||
|
*/ |
||||||
|
@Throws(IOException::class) |
||||||
|
fun makeAsDir(): Boolean { |
||||||
|
getUrl()?.let { url-> |
||||||
|
val request = Request.Builder() |
||||||
|
.url(url) |
||||||
|
.method("MKCOL", null) |
||||||
|
return execRequest(request) |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 下载到本地 |
||||||
|
* |
||||||
|
* @param savedPath 本地的完整路径,包括最后的文件名 |
||||||
|
* @param replaceExisting 是否替换本地的同名文件 |
||||||
|
* @return 下载是否成功 |
||||||
|
*/ |
||||||
|
fun downloadTo(savedPath: String, replaceExisting: Boolean): Boolean { |
||||||
|
if (File(savedPath).exists()) { |
||||||
|
if (!replaceExisting) { |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
val inputS = inputStream ?: return false |
||||||
|
File(savedPath).writeBytes(inputS.readBytes()) |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 上传文件 |
||||||
|
*/ |
||||||
|
@Throws(IOException::class) |
||||||
|
@JvmOverloads |
||||||
|
fun upload(localPath: String, contentType: String? = null): Boolean { |
||||||
|
val file = File(localPath) |
||||||
|
if (!file.exists()) return false |
||||||
|
val mediaType = if (contentType == null) null else MediaType.parse(contentType) |
||||||
|
// 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息 |
||||||
|
val fileBody = RequestBody.create(mediaType, file) |
||||||
|
getUrl()?.let { |
||||||
|
val request = Request.Builder() |
||||||
|
.url(it) |
||||||
|
.put(fileBody) |
||||||
|
return execRequest(request) |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 执行请求,获取响应结果 |
||||||
|
* |
||||||
|
* @param requestBuilder 因为还需要追加验证信息,所以此处传递Request.Builder的对象,而不是Request的对象 |
||||||
|
* @return 请求执行的结果 |
||||||
|
*/ |
||||||
|
@Throws(IOException::class) |
||||||
|
private fun execRequest(requestBuilder: Request.Builder): Boolean { |
||||||
|
HttpAuth.auth?.let { requestBuilder.header("Authorization", Credentials.basic(it.user, it.pass)) } |
||||||
|
|
||||||
|
val response = okHttpClient.newCall(requestBuilder.build()).execute() |
||||||
|
return response.isSuccessful |
||||||
|
} |
||||||
|
|
||||||
|
fun canRead(): Boolean { |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
fun canWrite(): Boolean { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
fun exists(): Boolean { |
||||||
|
return exists |
||||||
|
} |
||||||
|
|
||||||
|
fun exists(exists: Boolean) { |
||||||
|
this.exists = exists |
||||||
|
} |
||||||
|
|
||||||
|
companion object { |
||||||
|
val TAG = WebDav::class.java.simpleName |
||||||
|
val OBJECT_NOT_EXISTS_TAG = "ObjectNotFound" |
||||||
|
// 指定返回哪些属性 |
||||||
|
private val DIR = "<?xml version=\"1.0\"?>\n" + |
||||||
|
"<a:propfind xmlns:a=\"DAV:\">\n" + |
||||||
|
"<a:prop>\n" + |
||||||
|
"<a:displayname/>\n<a:resourcetype/>\n<a:getcontentlength/>\n<a:creationdate/>\n<a:getlastmodified/>\n%s" + |
||||||
|
"</a:prop>\n" + |
||||||
|
"</a:propfind>" |
||||||
|
|
||||||
|
/** |
||||||
|
* 打印对象内的所有属性 |
||||||
|
*/ |
||||||
|
fun <T> printAllAttrs(className: String, o: Any): T? { |
||||||
|
try { |
||||||
|
val c = Class.forName(className) |
||||||
|
val fields = c.declaredFields |
||||||
|
for (f in fields) { |
||||||
|
f.isAccessible = true |
||||||
|
} |
||||||
|
println("=============$className===============") |
||||||
|
for (f in fields) { |
||||||
|
val field = f.toString().substring(f.toString().lastIndexOf(".") + 1) //取出属性名称 |
||||||
|
println(field + " --> " + f.get(o)) |
||||||
|
} |
||||||
|
} catch (e: Exception) { |
||||||
|
e.printStackTrace() |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
package io.legado.app.lib.webdav.http |
||||||
|
|
||||||
|
import java.net.URL |
||||||
|
import java.net.URLConnection |
||||||
|
import java.net.URLStreamHandler |
||||||
|
|
||||||
|
class Handler : URLStreamHandler() { |
||||||
|
|
||||||
|
override fun getDefaultPort(): Int { |
||||||
|
return 80 |
||||||
|
} |
||||||
|
|
||||||
|
public override fun openConnection(u: URL): URLConnection? { |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
override fun parseURL(url: URL, spec: String, start: Int, end: Int) { |
||||||
|
super.parseURL(url, spec, start, end) |
||||||
|
} |
||||||
|
|
||||||
|
companion object { |
||||||
|
|
||||||
|
val HANDLER: URLStreamHandler = Handler() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
package io.legado.app.lib.webdav.http |
||||||
|
|
||||||
|
object HttpAuth { |
||||||
|
|
||||||
|
var auth: Auth? = null |
||||||
|
private set |
||||||
|
|
||||||
|
fun setAuth(user: String, password: String) { |
||||||
|
auth = Auth(user, password) |
||||||
|
} |
||||||
|
|
||||||
|
class Auth internal constructor(val user: String, val pass: String) |
||||||
|
|
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
package io.legado.app.lib.webdav.http |
||||||
|
|
||||||
|
import okhttp3.OkHttpClient |
||||||
|
|
||||||
|
class OkHttp private constructor() { |
||||||
|
|
||||||
|
object SingletonHelper { |
||||||
|
val okHttpClient: OkHttpClient = OkHttpClient.Builder().build() |
||||||
|
} |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue