diff --git a/app/src/main/java/io/legado/app/lib/webdav/README.md b/app/src/main/java/io/legado/app/lib/webdav/README.md new file mode 100644 index 000000000..f6ac57a9d --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/webdav/README.md @@ -0,0 +1 @@ +## 用于网络备份的WebDav \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt b/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt new file mode 100644 index 000000000..a3f695171 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt @@ -0,0 +1,320 @@ +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 + private var httpUrl: String? = null + + var displayName: String? = null + var createTime: Long = 0 + var lastModified: Long = 0 + var size: Long = 0 + var isDirectory = true + 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 + + val path: String + get() = url.toString() + + private val inputStream: InputStream? + get() { + val request = Request.Builder() + .url(getUrl()!!) + + val auth = HttpAuth.auth + + if (auth != null) { + request.header("Authorization", Credentials.basic(auth!!.user, auth!!.pass)) + } + + try { + val response = okHttpClient.newCall(request.build()).execute() + return response.body()!!.byteStream() + } catch (e: IOException) { + e.printStackTrace() + } catch (e: IllegalArgumentException) { + e.printStackTrace() + } + + return null + } + + val host: String + get() = url.host + + init { + this.url = URL(null, url, Handler.HANDLER) + okHttpClient = OkHttp.instance.okHttpClient + } + + 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 { + val response = propFindResponse(ArrayList()) + var s = "" + try { + if (response == null || !response.isSuccessful) { + this.exists = false + return false + } + s = response.body()!!.string() + } catch (e: Exception) { + e.printStackTrace() + } + + + return false + } + + /** + * 列出当前路径下的文件 + * + * @param propsList 指定列出文件的哪些属性 + * @return 文件列表 + */ + @Throws(IOException::class) + @JvmOverloads + fun listFiles(propsList: ArrayList = ArrayList()): List { + val response = propFindResponse(propsList) + try { + assert(response != null) + if (response.isSuccessful) { + return parseDir(response.body()!!.string()) + } + } catch (e: Exception) { + e.printStackTrace() + } + + return ArrayList() + } + + @Throws(IOException::class) + private fun propFindResponse(propsList: ArrayList, depth: Int = 1): Response { + val requestProps = StringBuilder() + for (p in propsList) { + requestProps.append("\n") + } + val requestPropsStr: String + if (requestProps.toString().isEmpty()) { + requestPropsStr = DIR.replace("%s", "") + } else { + requestPropsStr = String.format(DIR, requestProps.toString() + "\n") + } + val request = Request.Builder() + .url(getUrl()!!) + // 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性 + // 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。 + .method("PROPFIND", RequestBody.create(MediaType.parse("text/plain"), requestPropsStr)) + + val auth = HttpAuth.auth + if (auth != null) { + request.header("Authorization", Credentials.basic(auth!!.user, auth!!.pass)) + } + request.header("Depth", if (depth < 0) "infinity" else Integer.toString(depth)) + + return okHttpClient.newCall(request.build()).execute() + } + + private fun parseDir(s: String): List { + val list = ArrayList() + val document = Jsoup.parse(s) + val elements = document.getElementsByTag("d:response") + val baseUrl = if (getUrl()!!.endsWith("/")) getUrl() else getUrl()!! + "/" + 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 { + val request = Request.Builder() + .url(getUrl()!!) + .method("MKCOL", null) + return execRequest(request) + } + + /** + * 下载到本地 + * + * @param savedPath 本地的完整路径,包括最后的文件名 + * @param replaceExisting 是否替换本地的同名文件 + * @return 下载是否成功 + */ + fun download(savedPath: String, replaceExisting: Boolean): Boolean { + val file = File(savedPath) + if (file.exists()) { + if (replaceExisting) { + file.delete() + } else { + return false + } + } + val inputS = inputStream ?: return false + try { + file.createNewFile() + file.writeBytes(inputS.readBytes()) + return true + } catch (e: Exception) { + e.printStackTrace() + } + return false + } + + @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) + val request = Request.Builder() + .url(getUrl()!!) + .put(fileBody) + + return execRequest(request) + } + + /** + * 执行请求,获取响应结果 + * + * @param requestBuilder 因为还需要追加验证信息,所以此处传递Request.Builder的对象,而不是Request的对象 + * @return 请求执行的结果 + */ + @Throws(IOException::class) + private fun execRequest(requestBuilder: Request.Builder): Boolean { + val auth = HttpAuth.auth + if (auth != null) { + requestBuilder.header("Authorization", Credentials.basic(auth!!.user, auth!!.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 = "\n" + + "\n" + + "\n" + + "\n\n\n\n\n%s" + + "\n" + + "" + + /** + * 打印对象内的所有属性 + */ + fun 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/webdav/http/Handler.kt b/app/src/main/java/io/legado/app/lib/webdav/http/Handler.kt new file mode 100644 index 000000000..2bafb9780 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/webdav/http/Handler.kt @@ -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() + } +} diff --git a/app/src/main/java/io/legado/app/lib/webdav/http/HttpAuth.kt b/app/src/main/java/io/legado/app/lib/webdav/http/HttpAuth.kt new file mode 100644 index 000000000..3a6f31b48 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/webdav/http/HttpAuth.kt @@ -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) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/webdav/http/OkHttp.kt b/app/src/main/java/io/legado/app/lib/webdav/http/OkHttp.kt new file mode 100644 index 000000000..f63e6a9bc --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/webdav/http/OkHttp.kt @@ -0,0 +1,19 @@ +package io.legado.app.lib.webdav.http + +import okhttp3.OkHttpClient + +class OkHttp private constructor() { + + val okHttpClient: OkHttpClient = OkHttpClient.Builder().build() + + private object SingletonHelper { + val INSTANCE = OkHttp() + } + + companion object { + + val instance: OkHttp + get() = SingletonHelper.INSTANCE + } + +}