Skip to content

Commit 2fb8c46

Browse files
committed
fix: download close #17
1 parent e84fe85 commit 2fb8c46

File tree

4 files changed

+149
-51
lines changed

4 files changed

+149
-51
lines changed

build.gradle.kts

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,12 @@ dependencies {
3737
exclude(group = "org.jetbrains.kotlinx")
3838
exclude(group = "org.slf4j")
3939
}
40-
implementation("com.squareup.okhttp3:okhttp:4.10.0") {
40+
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.10.0") {
4141
exclude(group = "org.jetbrains.kotlin")
4242
exclude(group = "org.jetbrains.kotlinx")
4343
exclude(group = "org.slf4j")
4444
}
45+
implementation("org.apache.commons:commons-compress:1.21")
4546
api("org.jetbrains.skiko:skiko-awt:0.7.34") {
4647
exclude(group = "org.jetbrains.kotlin")
4748
exclude(group = "org.jetbrains.kotlinx")

src/main/kotlin/xyz/cssxsh/mirai/skia/MiraiSkiaDowloader.kt renamed to src/main/kotlin/xyz/cssxsh/mirai/skia/MiraiSkiaDownloader.kt

+101-42
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package xyz.cssxsh.mirai.skia
22

33
import io.ktor.client.*
4+
import io.ktor.client.content.*
45
import io.ktor.client.engine.okhttp.*
56
import io.ktor.client.plugins.*
67
import io.ktor.client.plugins.compression.*
@@ -10,6 +11,12 @@ import io.ktor.http.*
1011
import io.ktor.utils.io.jvm.javaio.*
1112
import kotlinx.coroutines.*
1213
import net.mamoe.mirai.utils.*
14+
import okhttp3.Dns
15+
import okhttp3.HttpUrl.Companion.toHttpUrl
16+
import okhttp3.dnsoverhttps.DnsOverHttps
17+
import org.apache.commons.compress.archivers.sevenz.*
18+
import org.apache.commons.compress.archivers.tar.*
19+
import org.apache.commons.compress.compressors.gzip.*
1320
import org.jetbrains.skiko.*
1421
import xyz.cssxsh.skia.*
1522
import java.io.*
@@ -33,34 +40,56 @@ private val http = HttpClient(OkHttp) {
3340
connectTimeoutMillis = 30_000
3441
socketTimeoutMillis = 30_000
3542
}
36-
}
43+
engine {
44+
config {
45+
dns(object : Dns {
46+
private val url = System.getProperty("xyz.cssxsh.mirai.skia.doh", "https://public.dns.iij.jp/dns-query")
47+
private val doh = DnsOverHttps.Builder()
48+
.client(okhttp3.OkHttpClient())
49+
.url(url.toHttpUrl())
50+
.includeIPv6(true)
51+
.build()
3752

38-
internal val sevenZ: String by lazy {
39-
System.getProperty("xyz.cssxsh.mirai.skia.sevenZ", "7za")
53+
@Throws(java.net.UnknownHostException::class)
54+
override fun lookup(hostname: String): List<java.net.InetAddress> {
55+
return try {
56+
doh.lookup(hostname)
57+
} catch (_: java.net.UnknownHostException) {
58+
Dns.SYSTEM.lookup(hostname)
59+
}
60+
}
61+
})
62+
}
63+
}
4064
}
4165

42-
internal suspend fun download(urlString: String, folder: File): File = supervisorScope {
43-
http.prepareGet(urlString).execute { response ->
44-
val relative = response.headers[HttpHeaders.ContentDisposition]
45-
?.let { ContentDisposition.parse(it).parameter(ContentDisposition.Parameters.FileName) }
46-
?: response.request.url.encodedPath.substringAfterLast('/').decodeURLPart()
66+
internal var listener: (urlString: String) -> ProgressListener? = { null }
4767

48-
val file = folder.resolve(relative)
68+
internal suspend fun download(urlString: String, folder: File): File {
69+
val name = urlString.substringAfterLast('/').decodeURLPart()
70+
val listener = listener("<${name}>下载中")
4971

50-
if (file.isFile && response.contentLength() == file.length()) {
51-
logger.info { "文件 ${file.name} 已存在,跳过下载" }
52-
} else {
53-
file.delete()
54-
logger.info { "文件 ${file.name} 开始下载" }
55-
file.outputStream().use { output ->
56-
val channel = response.bodyAsChannel()
72+
val response = http.get(urlString) {
73+
onDownload(listener)
74+
}
75+
val relative = response.headers[HttpHeaders.ContentDisposition]
76+
?.let { ContentDisposition.parse(it).parameter(ContentDisposition.Parameters.FileName) }
77+
?: response.request.url.encodedPath.substringAfterLast('/').decodeURLPart()
5778

58-
while (!channel.isClosedForRead) channel.copyTo(output)
59-
}
60-
}
79+
val file = folder.resolve(relative)
80+
81+
if (file.isFile && response.contentLength() == file.length()) {
82+
logger.info { "文件 ${file.name} 已存在,跳过下载" }
83+
} else {
84+
file.delete()
85+
logger.info { "文件 ${file.name} 开始下载" }
86+
file.outputStream().use { output ->
87+
val channel = response.bodyAsChannel()
6188

62-
file
89+
while (!channel.isClosedForRead) channel.copyTo(output)
90+
}
6391
}
92+
return file
6493
}
6594

6695
/**
@@ -70,7 +99,7 @@ internal suspend fun download(urlString: String, folder: File): File = superviso
7099
*/
71100
@JvmSynthetic
72101
public suspend fun downloadTypeface(folder: File, vararg links: String) {
73-
val downloaded: MutableList<File> = ArrayList()
102+
val downloaded: MutableList<File> = ArrayList(links.size)
74103
val temp = runInterruptible(Dispatchers.IO) {
75104
Files.createTempDirectory("skia")
76105
.toFile()
@@ -89,32 +118,59 @@ public suspend fun downloadTypeface(folder: File, vararg links: String) {
89118
for (pack in downloaded) {
90119
when (pack.extension) {
91120
"7z" -> runInterruptible(Dispatchers.IO) {
92-
ProcessBuilder(sevenZ, "x", pack.absolutePath, "-y")
93-
.directory(folder)
94-
.start()
95-
// 防止卡顿
96-
.apply { inputStream.transferTo(OutputStream.nullOutputStream()) }
97-
.waitFor()
121+
SevenZFile(pack).use { sevenZ ->
122+
for (entry in sevenZ.entries) {
123+
if (entry.isDirectory) continue
124+
if (entry.hasStream().not()) continue
125+
val target = folder.resolve(entry.name)
126+
if (target.extension !in FontExtensions) continue
127+
target.parentFile.mkdirs()
128+
target.outputStream().use { output ->
129+
sevenZ.getInputStream(entry).use { input ->
130+
input.copyTo(output)
131+
}
132+
}
133+
target.setLastModified(entry.lastModifiedDate.time)
134+
}
135+
}
98136
}
99137
"zip" -> runInterruptible(Dispatchers.IO) {
100138
ZipFile(pack).use { zip ->
101139
for (entry in zip.entries()) {
102140
if (entry.isDirectory) continue
103141
if (entry.name.startsWith("__MACOSX")) continue
104-
with(folder.resolve(entry.name)) {
105-
parentFile.mkdirs()
106-
if (exists().not()) {
107-
outputStream().use { output ->
108-
zip.getInputStream(entry).use { input ->
109-
input.transferTo(output)
110-
}
111-
}
142+
val target = folder.resolve(entry.name)
143+
if (target.extension !in FontExtensions) continue
144+
target.parentFile.mkdirs()
145+
target.outputStream().use { output ->
146+
zip.getInputStream(entry).use { input ->
147+
input.copyTo(output)
112148
}
113-
setLastModified(entry.lastModifiedTime.toMillis())
114149
}
150+
target.setLastModified(entry.lastModifiedTime.toMillis())
115151
}
116152
}
117153
}
154+
"gz" -> runInterruptible(Dispatchers.IO) {
155+
pack.inputStream()
156+
.buffered()
157+
.let(::GzipCompressorInputStream)
158+
.let(::TarArchiveInputStream)
159+
.use { input ->
160+
while (true) {
161+
val entry = input.nextTarEntry ?: break
162+
if (entry.isFile.not()) continue
163+
if (input.canReadEntryData(entry).not()) continue
164+
val target = folder.resolve(entry.name)
165+
if (target.extension !in FontExtensions) continue
166+
target.parentFile.mkdirs()
167+
target.outputStream().use { output ->
168+
input.copyTo(output)
169+
}
170+
target.setLastModified(entry.modTime.time)
171+
}
172+
}
173+
}
118174
else -> runInterruptible(Dispatchers.IO) {
119175
Files.move(pack.toPath(), folder.resolve(pack.name).toPath())
120176
}
@@ -149,16 +205,19 @@ public fun loadTypeface(folder: File) {
149205
}
150206
}
151207

208+
209+
public val FontExtensions: Array<String> = arrayOf("ttf", "otf", "eot", "fon", "font", "woff", "woff2", "ttc")
210+
152211
/**
153212
* 一些免费字体链接
154213
*/
155214
public val FreeFontLinks: Array<String> = arrayOf(
156-
"https://raw.fastgit.org/googlefonts/noto-emoji/main/fonts/NotoColorEmoji_WindowsCompatible.ttf",
157-
"https://raw.fastgit.org/wordshub/free-font/master/assets/font/中文/方正字体系列/方正书宋简体.ttf",
158-
"https://raw.fastgit.org/wordshub/free-font/master/assets/font/中文/方正字体系列/方正仿宋简体.ttf",
159-
"https://raw.fastgit.org/wordshub/free-font/master/assets/font/中文/方正字体系列/方正楷体简体.ttf",
160-
"https://raw.fastgit.org/wordshub/free-font/master/assets/font/中文/方正字体系列/方正黑体简体.ttf",
161-
"https://cdn.cnbj1.fds.api.mi-img.com/vipmlmodel/font/MiSans/MiSans.zip"
215+
"https://github.com/googlefonts/noto-emoji/raw/main/fonts/NotoColorEmoji_WindowsCompatible.ttf",
216+
// "https://cdn.cnbj1.fds.api.mi-img.com/vipmlmodel/font/MiSans/MiSans.zip",
217+
"https://mirai.mamoe.net/assets/uploads/files/1666870589379-方正书宋简体.ttf",
218+
"https://mirai.mamoe.net/assets/uploads/files/1666870589357-方正仿宋简体.ttf",
219+
"https://mirai.mamoe.net/assets/uploads/files/1666870589334-方正楷体简体.ttf",
220+
"https://mirai.mamoe.net/assets/uploads/files/1666870589312-方正黑体简体.ttf"
162221
)
163222

164223
private val SKIKO_MAVEN: String by lazy {

src/main/kotlin/xyz/cssxsh/mirai/skia/MiraiSkiaPlugin.kt

+39-5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import net.mamoe.mirai.console.util.*
99
import net.mamoe.mirai.utils.*
1010
import org.jetbrains.skiko.*
1111
import xyz.cssxsh.skia.*
12+
import java.io.Closeable
1213

1314
public object MiraiSkiaPlugin : KotlinPlugin(
1415
JvmPluginDescription(
@@ -20,8 +21,31 @@ public object MiraiSkiaPlugin : KotlinPlugin(
2021
}
2122
) {
2223

24+
@OptIn(ConsoleExperimentalApi::class)
25+
internal val process: Closeable? by lazy {
26+
try {
27+
val impl = MiraiConsole.newProcessProgress()
28+
listener = listener@{ message ->
29+
launch {
30+
impl.updateText(message)
31+
}
32+
33+
return@listener { total, contentLength ->
34+
if (contentLength != 0L) {
35+
impl.update(total, contentLength)
36+
impl.rerender()
37+
}
38+
}
39+
}
40+
impl
41+
} catch (_: Throwable) {
42+
null
43+
}
44+
}
45+
2346
internal val loadJob: Job = launch {
2447
checkPlatform()
48+
process
2549
loadJNILibrary(folder = resolveDataFile("lib"))
2650
if (resolveDataFile("fonts").list().isNullOrEmpty()) {
2751
downloadTypeface(folder = resolveDataFile("fonts"), links = FreeFontLinks)
@@ -39,17 +63,27 @@ public object MiraiSkiaPlugin : KotlinPlugin(
3963
}
4064
logger.info { "platform: ${hostId}, skia: ${Version.skia}, skiko: ${Version.skiko}" }
4165
loadJob.invokeOnCompletion { cause ->
42-
if (cause is UnsatisfiedLinkError) {
43-
val message = cause.message
44-
if (message != null && message.endsWith(": cannot open shared object file: No such file or directory")) {
66+
val message = cause?.message
67+
if (cause is UnsatisfiedLinkError && message != null) {
68+
if (message.endsWith(": cannot open shared object file: No such file or directory")) {
4569
val lib = message.substringBeforeLast(": cannot open shared object file: No such file or directory")
4670
.substringAfterLast(": ")
4771
logger.warning { "可能缺少相应库文件,请参阅: https://pkgs.org/search/?q=${lib}" }
4872
}
73+
if ("GLIBC_" in message) {
74+
val version = message.substringAfterLast("version `")
75+
.substringBeforeLast("' not found")
76+
logger.warning { "可能缺少 ${version}, 请安装此版本 glibc" }
77+
}
4978
}
5079
}
51-
runBlocking {
52-
loadJob.join()
80+
try {
81+
runBlocking {
82+
loadJob.join()
83+
}
84+
} finally {
85+
listener = { null }
86+
process?.close()
5387
}
5488
loadTypeface(folder = resolveDataFile("fonts"))
5589
logger.info { "fonts: ${FontUtils.provider.makeFamilies().keys}" }

src/test/kotlin/xyz/cssxsh/skia/ExampleKtTest.kt

+7-3
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@ internal class ExampleKtTest {
1616
@BeforeAll
1717
fun init() {
1818
fonts.mkdirs()
19+
System.setProperty("xyz.cssxsh.mirai.gif.release", "https://github.com/cssxsh/gif-jni")
20+
val links = arrayOf(
21+
"https://cdn.cnbj1.fds.api.mi-img.com/vipmlmodel/font/MiSans/MiSans.zip",
22+
"https://github.com/googlefonts/noto-emoji/archive/refs/tags/v2.038.tar.gz",
23+
"https://github.com/be5invis/Sarasa-Gothic/releases/download/v0.37.4/sarasa-gothic-ttf-0.37.4.7z"
24+
)
1925
runBlocking {
20-
System.setProperty("xyz.cssxsh.mirai.gif.release", "https://github.com/cssxsh/gif-jni")
2126
loadJNILibrary(folder = File("./run/lib"))
22-
val links = FreeFontLinks.map { it.replace("raw.fastgit.org","raw.githubusercontent.com") }.toTypedArray()
23-
if (fonts.list().isEmpty()) downloadTypeface(folder = fonts, links = links)
27+
if (fonts.list().isNullOrEmpty()) downloadTypeface(folder = fonts, links = links)
2428
}
2529
}
2630

0 commit comments

Comments
 (0)