commit
342ea3cd51
@ -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