|
|
@@ -9,6 +9,13 @@ import java.nio.charset.StandardCharsets
|
|
|
|
|
|
object UrlUtil {
|
|
|
|
|
|
+ // 有时候文件名在query里,截取path会截到其他内容
|
|
|
+ // https://www.example.com/download.php?filename=文件.txt
|
|
|
+ // https://www.example.com/txt/文件.txt?token=123456
|
|
|
+ private val unExpectFileSuffixs = arrayOf(
|
|
|
+ "php", "html"
|
|
|
+ )
|
|
|
+
|
|
|
fun replaceReservedChar(text: String): String {
|
|
|
return text.replace("%", "%25")
|
|
|
.replace(" ", "%20")
|
|
|
@@ -41,56 +48,93 @@ object UrlUtil {
|
|
|
* 根据网络url获取文件信息 文件名
|
|
|
*/
|
|
|
fun getFileName(fileUrl: String, headerMap: Map<String, String>? = null): String? {
|
|
|
- // 如果获取到后缀可直接截取链接
|
|
|
- if (getSuffix(fileUrl, "") != "") return fileUrl.substringAfterLast("/")
|
|
|
return kotlin.runCatching {
|
|
|
- var fileName: String = ""
|
|
|
val url = URL(fileUrl)
|
|
|
- val conn: HttpURLConnection = url.openConnection() as HttpURLConnection
|
|
|
- // HEAD方式获取返回头信息
|
|
|
- conn.requestMethod = "HEAD"
|
|
|
- // 下载链接可能还需要书源header才能成功访问
|
|
|
- headerMap?.forEach { key, value ->
|
|
|
- conn.setRequestProperty(key, value)
|
|
|
+ var fileName: String? = getFileNameFromPath(url)
|
|
|
+ if (fileName == null) {
|
|
|
+ fileName = getFileNameFromResponseHeader(url, headerMap)
|
|
|
}
|
|
|
- conn.connect()
|
|
|
+ fileName
|
|
|
+ }.getOrNull()
|
|
|
+ }
|
|
|
|
|
|
- // val contentLength = conn.getContentLengthLong()
|
|
|
- // Content-Disposition
|
|
|
- // 解析文件名 filename= filename*=
|
|
|
- val raw: String? = conn.getHeaderField("Content-Disposition")
|
|
|
- if (raw != null && raw.indexOf("=") > 0) {
|
|
|
- fileName = raw.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1]
|
|
|
- fileName =
|
|
|
- String(
|
|
|
- fileName.toByteArray(StandardCharsets.ISO_8859_1),
|
|
|
- StandardCharsets.UTF_8 //?
|
|
|
+ private fun getFileNameFromResponseHeader(
|
|
|
+ url: URL,
|
|
|
+ headerMap: Map<String, String>? = null
|
|
|
+ ): String? {
|
|
|
+ // HEAD方式获取链接响应头信息
|
|
|
+ val conn: HttpURLConnection = url.openConnection() as HttpURLConnection
|
|
|
+ conn.requestMethod = "HEAD"
|
|
|
+ // 下载链接可能还需要header才能成功访问
|
|
|
+ headerMap?.forEach { key, value ->
|
|
|
+ conn.setRequestProperty(key, value)
|
|
|
+ }
|
|
|
+ // 禁止重定向 否则获取不到响应头返回的Location
|
|
|
+ conn.setInstanceFollowRedirects(false)
|
|
|
+ conn.connect()
|
|
|
+
|
|
|
+ // val fileSize = conn.getContentLengthLong() / 1024
|
|
|
+ /** Content-Disposition 存在三种情况
|
|
|
+ * filename="filename"
|
|
|
+ * filename=filename
|
|
|
+ * filename*=charset''filename
|
|
|
+ */
|
|
|
+ val raw: String? = conn.getHeaderField("Content-Disposition")
|
|
|
+ // Location跳转到实际链接
|
|
|
+ val redirectUrl: String? = conn.getHeaderField("Location")
|
|
|
+
|
|
|
+ return if (raw != null) {
|
|
|
+ val fileNames = raw.split(";".toRegex()).filter { it.contains("filename") }
|
|
|
+ val names = hashSetOf<String>()
|
|
|
+ fileNames.forEach {
|
|
|
+ var fileName = it.substringAfter("=")
|
|
|
+ if (it.contains("filename*")) {
|
|
|
+ val data = fileName.split("''")
|
|
|
+ names.add(URLDecoder.decode(data[1], data[0]))
|
|
|
+ } else {
|
|
|
+ fileName = fileName
|
|
|
+ .replace("^\"".toRegex(), "")
|
|
|
+ .replace("\"$".toRegex(), "")
|
|
|
+ names.add(
|
|
|
+ String(
|
|
|
+ fileName.toByteArray(StandardCharsets.ISO_8859_1),
|
|
|
+ StandardCharsets.UTF_8
|
|
|
+ )
|
|
|
)
|
|
|
- } else {
|
|
|
- // Location跳转到实际的下载链接
|
|
|
- var newUrl: String = conn.url.path ?: return null
|
|
|
- newUrl = URLDecoder.decode(newUrl, "UTF-8")
|
|
|
- if (getSuffix(newUrl, "") != "") {
|
|
|
- fileName = newUrl.substringAfterLast("/")
|
|
|
}
|
|
|
- }
|
|
|
- if (fileName == "") {
|
|
|
- // 其余情况 返回响应头
|
|
|
- val headers = conn.getHeaderFields()
|
|
|
- val headersString = buildString {
|
|
|
- headers.forEach { key, value ->
|
|
|
- value.forEach {
|
|
|
- append(key)
|
|
|
- append(": ")
|
|
|
- append(it)
|
|
|
- append("\n")
|
|
|
- }
|
|
|
+ }
|
|
|
+ names.firstOrNull()
|
|
|
+ } else if (redirectUrl != null) {
|
|
|
+ val newUrl= URL(URLDecoder.decode(redirectUrl, "UTF-8"))
|
|
|
+ getFileNameFromPath(newUrl)
|
|
|
+ } else {
|
|
|
+ // 其余情况 返回响应头
|
|
|
+ val headers = conn.getHeaderFields()
|
|
|
+ val headersString = buildString {
|
|
|
+ headers.forEach { key, value ->
|
|
|
+ value.forEach {
|
|
|
+ append(key)
|
|
|
+ append(": ")
|
|
|
+ append(it)
|
|
|
+ append("\n")
|
|
|
}
|
|
|
}
|
|
|
- AppLog.put("Cannot obtain URL file name:\n$headersString")
|
|
|
}
|
|
|
- fileName
|
|
|
- }.getOrNull()
|
|
|
+ AppLog.put("Cannot obtain URL file name:\n$headersString")
|
|
|
+ null
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun getFileNameFromPath(fileUrl: URL): String? {
|
|
|
+ val path = fileUrl.path ?: return null
|
|
|
+ val suffix = getSuffix(path, "")
|
|
|
+ return if (
|
|
|
+ suffix != "" && !unExpectFileSuffixs.contains(suffix)
|
|
|
+ ) {
|
|
|
+ path.substringAfterLast("/")
|
|
|
+ } else {
|
|
|
+ null
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/* 获取合法的文件后缀 */
|
|
|
@@ -99,7 +143,8 @@ object UrlUtil {
|
|
|
//检查截取的后缀字符是否合法 [a-zA-Z0-9]
|
|
|
val fileSuffixRegex = Regex("^[a-z\\d]+$", RegexOption.IGNORE_CASE)
|
|
|
return if (suffix.length > 5 || !suffix.matches(fileSuffixRegex)) {
|
|
|
- default ?: throw IllegalArgumentException("Cannot find illegal suffix:\n target: $str\nsuffix: $suffix")
|
|
|
+ AppLog.put("Cannot find illegal suffix:\n target: $str\nsuffix: $suffix")
|
|
|
+ default ?: "ext"
|
|
|
} else {
|
|
|
suffix
|
|
|
}
|