Improvements and fixes

This commit is contained in:
Syer10
2026-07-12 23:43:11 -04:00
parent c0586e7f36
commit a830782b4b

View File

@@ -17,9 +17,6 @@ import io.github.oshai.kotlinlogging.KotlinLogging
import net.dongliu.apk.parser.ApkFile
import net.dongliu.apk.parser.bean.Icon
import okhttp3.CacheControl
import okio.buffer
import okio.sink
import okio.source
import org.apache.commons.compress.archivers.zip.ZipFile
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.deleteWhere
@@ -61,6 +58,8 @@ import kotlin.io.path.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.copyTo
import kotlin.io.path.createDirectories
import kotlin.io.path.createParentDirectories
import kotlin.io.path.deleteExisting
import kotlin.io.path.deleteIfExists
import kotlin.io.path.deleteRecursively
import kotlin.io.path.div
@@ -92,7 +91,7 @@ object Extension {
jarUrl != null -> {
installJAR {
val jarName = Uri.parse(jarUrl).lastPathSegment!!
val jarSavePath = "${applicationDirs.extensionsRoot}/$jarName"
val jarSavePath = Path(applicationDirs.tempRoot) / "extensions" / jarName
// download jar file
downloadExtension(jarUrl, jarSavePath)
@@ -103,7 +102,7 @@ object Extension {
apkUrl != null -> {
installAPK {
val apkName = Uri.parse(apkUrl).lastPathSegment!!
val apkSavePath = "${applicationDirs.extensionsRoot}/$apkName"
val apkSavePath = Path(applicationDirs.tempRoot) / "extensions" / apkName
// download apk file
downloadExtension(apkUrl, apkSavePath)
@@ -122,20 +121,20 @@ object Extension {
extensionName: String,
): String {
val copyToExtensionsRoot = {
val rootPath = Path(applicationDirs.extensionsRoot)
val rootPath = Path(applicationDirs.tempRoot) / "extensions"
val downloadedFile = rootPath.resolve(extensionName).normalize()
check(downloadedFile.startsWith(rootPath) && downloadedFile.parent == rootPath) {
"File '$extensionName' is not a valid extension file"
}
logger.debug { "Saving jar at $extensionName" }
// download jar file
downloadedFile.outputStream().sink().buffer().use { sink ->
inputStream.source().use { source ->
sink.writeAll(source)
sink.flush()
downloadedFile.createParentDirectories()
downloadedFile.outputStream().buffered().use { out ->
inputStream.use {
it.copyTo(out)
}
}
downloadedFile.absolutePathString()
downloadedFile
}
return when {
@@ -147,9 +146,9 @@ object Extension {
suspend fun installAPK(
forceReinstall: Boolean = false,
fetcher: suspend () -> String,
fetcher: suspend () -> Path,
): String {
val apkFile = Path(fetcher())
val apkFile = fetcher()
val packageInfo = getPackageInfo(apkFile)
val pkgName = packageInfo.packageName
@@ -161,14 +160,16 @@ object Extension {
ExtensionTable.select(ExtensionTable.isInstalled).where { ExtensionTable.pkgName eq pkgName }.firstOrNull()
}?.get(ExtensionTable.isInstalled) ?: false
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/${apkFile.nameWithoutExtension}"
val jarFile = Path("$dirPathWithoutType.jar")
val extensionsRoot = Path(applicationDirs.extensionsRoot)
val jarFile = extensionsRoot / (apkFile.nameWithoutExtension + ".jar")
if (isInstalled && forceReinstall) {
uninstallExtension(pkgName)
} else if (isInstalled) {
apkFile.deleteExisting()
return pkgName
}
if (!isInstalled || forceReinstall) {
if (!packageInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }) {
throw Exception("This apk is not a Tachiyomi extension")
}
@@ -224,7 +225,7 @@ object Extension {
extractAndCacheApkIcon(apkFile, pkgName)
// clean up
apkFile.deleteIfExists()
apkFile.deleteExisting()
try {
val extensionName =
@@ -261,43 +262,62 @@ object Extension {
}
throw e
}
}
return pkgName
}
suspend fun installJAR(
forceReinstall: Boolean = false,
fetcher: suspend () -> String,
fetcher: suspend () -> Path,
): String {
val jarFile = Path(fetcher())
val tmpJarFile = fetcher()
val jarFile = Path(applicationDirs.extensionsRoot) / tmpJarFile.name
val tmpIcon = jarFile.parent / (jarFile.nameWithoutExtension + ".icon")
val jarZip =
ZipFile
.builder()
.setPath(jarFile)
.setPath(tmpJarFile)
.get()
return jarZip.use { jarZip ->
val manifest =
val manifest: AndroidManifestParser.AndroidManifest
jarZip.use { jarZip ->
manifest =
jarZip
.getInputStream(jarZip.getEntry("AndroidManifest.xml"))
.use {
AndroidManifestParser.parse(it)
}
val pkgName = manifest.packageName
try {
ResourceArscIconParser.extractIcon(jarZip).use {
tmpIcon.outputStream().use { out ->
it.copyTo(out)
}
}
} catch (e: Exception) {
logger.warn(e) { "Failed to extract icon from JAR ${manifest.packageName}" }
}
}
val pkgName: String = manifest.packageName
// check if we don't have the extension already installed
// if it's installed and we want to update, it first has to be uninstalled
val isInstalled =
transaction {
ExtensionTable.select(ExtensionTable.isInstalled).where { ExtensionTable.pkgName eq pkgName }.firstOrNull()
ExtensionTable
.select(ExtensionTable.isInstalled)
.where { ExtensionTable.pkgName eq pkgName }
.firstOrNull()
}?.get(ExtensionTable.isInstalled) ?: false
if (isInstalled && forceReinstall) {
uninstallExtension(pkgName)
} else if (isInstalled) {
tmpJarFile.deleteExisting()
tmpIcon.deleteIfExists()
return pkgName
}
if (!isInstalled || forceReinstall) {
if (!manifest.usesFeatures.any { it.name == EXTENSION_FEATURE }) {
throw Exception("This apk is not a Tachiyomi extension")
}
@@ -311,6 +331,18 @@ object Extension {
)
}
if (tmpIcon.exists()) {
try {
cacheIcon(pkgName, tmpIcon.inputStream())
} catch (e: Exception) {
logger.warn(e) { "Failed to copy icon from JAR ${manifest.packageName}" }
}
}
tmpIcon.deleteIfExists()
tmpJarFile.copyTo(jarFile)
tmpJarFile.deleteExisting()
// TODO: allow trusting keys
// val signatureHash = getSignatureHash(packageInfo)
//
@@ -348,8 +380,6 @@ object Extension {
logger.debug { "Main class for extension is $className" }
extractAndCacheJarIcon(jarZip, pkgName)
try {
val extensionName =
manifest.application.metaData.getString(METADATA_NAME)
@@ -360,7 +390,7 @@ object Extension {
manifest.application.metaData
.getString(METADATA_EXTENSION_LIB)
.takeUnless { it == "0" }
?: manifest.versionName.substringBeforeLast('.')
?: manifest.versionName!!.substringBeforeLast('.')
setupJar(
jarFile,
@@ -369,7 +399,7 @@ object Extension {
extensionLibVersion,
jarFile.name.removeSuffix(".jar") + ".apk",
pkgName,
manifest.versionName,
manifest.versionName!!,
manifest.versionCode!!,
contentWarning,
)
@@ -384,9 +414,7 @@ object Extension {
}
throw e
}
}
return@use pkgName
}
return pkgName
}
private fun setupJar(
@@ -499,7 +527,7 @@ object Extension {
pkgName: String,
inputStream: InputStream,
) {
val iconCacheDir = Path("${applicationDirs.extensionsRoot}/icon")
val iconCacheDir = Path(applicationDirs.extensionsRoot) / "icon"
iconCacheDir.createDirectories()
clearCachedImage(iconCacheDir.absolutePathString(), pkgName)
saveImage("$iconCacheDir/$pkgName", inputStream, null)
@@ -517,7 +545,7 @@ object Extension {
while (zipEntry != null) {
if (zipEntry.name.startsWith("assets/") && !zipEntry.isDirectory) {
val assetFile = assetsFolder / zipEntry.name
assetFile.parent.createDirectories()
assetFile.createParentDirectories()
assetFile.outputStream().use { outputStream ->
zipInputStream.copyTo(outputStream)
}
@@ -551,7 +579,7 @@ object Extension {
jarFile.deleteIfExists()
tempJarFile.copyTo(jarFile)
tempJarFile.deleteIfExists()
tempJarFile.deleteExisting()
assetsFolder.deleteRecursively()
}
@@ -560,7 +588,7 @@ object Extension {
private suspend fun downloadExtension(
url: String,
savePath: String,
savePath: Path,
) {
val response =
network.client
@@ -568,9 +596,9 @@ object Extension {
GET(url, cache = CacheControl.FORCE_NETWORK),
).await()
val downloadedFile = Path(savePath)
savePath.createParentDirectories()
response.body.byteStream().use {
downloadedFile.outputStream().buffered().use { out ->
savePath.outputStream().buffered().use { out ->
it.copyTo(out)
}
}
@@ -612,7 +640,7 @@ object Extension {
// clear all loaded sources
sources.forEach { GetSource.unregisterSource(it) }
jarPath.deleteIfExists()
jarPath.deleteExisting()
}
}