remote downloading

This commit is contained in:
downthecrop 2025-09-18 20:08:53 -07:00
parent 905393d2be
commit dc70d4406e
3 changed files with 836 additions and 26 deletions

View file

@ -0,0 +1,413 @@
package KondoKit.components.ReflectiveEditorComponents
import KondoKit.views.ReflectiveEditorView
import plugin.PluginRepository
import java.awt.EventQueue
import java.io.*
import java.net.HttpURLConnection
import java.net.URL
import java.util.concurrent.*
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import javax.swing.SwingUtilities
/**
* Manages downloading and installing plugins from GitLab with concurrent support
*/
object PluginDownloadManager {
private const val GITLAB_PROJECT_PATH = "2009scape/tools/client-plugins"
private const val GITLAB_BRANCH = "master"
private const val CHROME_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
private const val MAX_CONCURRENT_DOWNLOADS = 3
private const val DEBUG = true // Set to false to disable debug logging
// Thread pool for concurrent downloads
private val downloadExecutor = Executors.newFixedThreadPool(MAX_CONCURRENT_DOWNLOADS)
// Callback for download progress updates
interface DownloadProgressCallback {
fun onProgress(pluginName: String, progress: Int)
fun onComplete(pluginName: String, success: Boolean, errorMessage: String? = null)
}
// Debug logging function
private fun debugLog(message: String) {
if (DEBUG) {
println("[PluginDownloadManager] $message")
}
}
// Test URL method for debugging
fun testDownloadUrl(plugin: GitLabPlugin): String {
return "https://gitlab.com/$GITLAB_PROJECT_PATH/-/archive/$GITLAB_BRANCH/${GITLAB_PROJECT_PATH.replace("/", "-")}-$GITLAB_BRANCH.zip?path=${plugin.path}"
}
/**
* Download a single plugin
*/
fun downloadPlugin(plugin: GitLabPlugin, callback: (String, Boolean, String?) -> Unit) {
downloadExecutor.submit {
try {
debugLog("Starting download for plugin: ${plugin.path}")
// Download the plugin as a ZIP archive
val success = downloadAndExtractPlugin(plugin, object : DownloadProgressCallback {
override fun onProgress(pluginName: String, progress: Int) {
// We don't need to do anything here for the simple callback
}
override fun onComplete(pluginName: String, success: Boolean, errorMessage: String?) {
callback(pluginName, success, errorMessage)
}
})
if (success) {
debugLog("Successfully downloaded and extracted plugin: ${plugin.path}")
SwingUtilities.invokeLater {
callback(plugin.path, true, null)
}
} else {
debugLog("Failed to download plugin: ${plugin.path}")
SwingUtilities.invokeLater {
callback(plugin.path, false, "Failed to download plugin")
}
}
} catch (e: Exception) {
debugLog("Exception during download of ${plugin.path}: ${e.message}")
e.printStackTrace()
SwingUtilities.invokeLater {
callback(plugin.path, false, e.message)
}
}
}
}
/**
* Download a single plugin with progress updates
*/
fun downloadPlugin(plugin: GitLabPlugin, callback: DownloadProgressCallback) {
downloadExecutor.submit {
try {
debugLog("Starting download for plugin: ${plugin.path}")
callback.onProgress(plugin.path, 0)
// Download the plugin as a ZIP archive
val success = downloadAndExtractPlugin(plugin, callback)
if (success) {
debugLog("Successfully downloaded and extracted plugin: ${plugin.path}")
SwingUtilities.invokeLater {
callback.onComplete(plugin.path, true)
}
} else {
debugLog("Failed to download plugin: ${plugin.path}")
SwingUtilities.invokeLater {
callback.onComplete(plugin.path, false, "Failed to download plugin")
}
}
} catch (e: Exception) {
debugLog("Exception during download of ${plugin.path}: ${e.message}")
e.printStackTrace()
SwingUtilities.invokeLater {
callback.onComplete(plugin.path, false, e.message)
}
}
}
}
/**
* Download multiple plugins concurrently
*/
fun downloadPlugins(plugins: List<GitLabPlugin>, callback: (String, Boolean, String?) -> Unit) {
debugLog("Starting concurrent download of ${plugins.size} plugins")
// Submit all downloads to the executor
for (plugin in plugins) {
downloadPlugin(plugin, object : DownloadProgressCallback {
override fun onProgress(pluginName: String, progress: Int) {
// We don't need to do anything here for the simple callback
}
override fun onComplete(pluginName: String, success: Boolean, errorMessage: String?) {
callback(pluginName, success, errorMessage)
}
})
}
}
/**
* Download and extract a plugin from GitLab
*/
private fun downloadAndExtractPlugin(plugin: GitLabPlugin, callback: DownloadProgressCallback): Boolean {
try {
// Validate plugin path
if (plugin.path.isBlank()) {
debugLog("Plugin path is blank for plugin: ${plugin.path}")
return false
}
debugLog("Starting download for plugin: ${plugin.path}")
// Try multiple URL formats since GitLab has changed their API
val downloadUrls = listOf(
// Format 1: Using ref_type parameter
"https://gitlab.com/$GITLAB_PROJECT_PATH/-/archive/$GITLAB_BRANCH/${GITLAB_PROJECT_PATH.replace("/", "-")}-$GITLAB_BRANCH.zip?path=${plugin.path}&ref_type=heads",
// Format 2: Using ref parameter
"https://gitlab.com/$GITLAB_PROJECT_PATH/-/archive/$GITLAB_BRANCH/${GITLAB_PROJECT_PATH.replace("/", "-")}-$GITLAB_BRANCH.zip?path=${plugin.path}",
// Format 3: Direct archive URL without path parameter (we'll filter later)
"https://gitlab.com/$GITLAB_PROJECT_PATH/-/archive/$GITLAB_BRANCH/${GITLAB_PROJECT_PATH.replace("/", "-")}-$GITLAB_BRANCH.zip"
)
for (downloadUrl in downloadUrls) {
debugLog("Trying download URL: $downloadUrl")
// Validate URL
val url = try {
URL(downloadUrl)
} catch (e: Exception) {
debugLog("Invalid URL: $downloadUrl for plugin: ${plugin.path}. Error: ${e.message}")
continue
}
// Create URL connection
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.setRequestProperty("User-Agent", CHROME_USER_AGENT)
// Add Accept header to avoid 406 errors
connection.setRequestProperty("Accept", "*/*")
debugLog("Connection opened for plugin: ${plugin.path}")
debugLog("Request headers - User-Agent: ${connection.getRequestProperty("User-Agent")}")
debugLog("Request headers - Accept: ${connection.getRequestProperty("Accept")}")
// Get content length for progress tracking
val contentLength = connection.contentLength
debugLog("Content length: $contentLength bytes for plugin: ${plugin.path}")
// Read response
val responseCode = connection.responseCode
debugLog("Response code: $responseCode for plugin: ${plugin.path}")
// Log response message
val responseMessage = connection.responseMessage
debugLog("Response message: $responseMessage for plugin: ${plugin.path}")
if (responseCode == HttpURLConnection.HTTP_OK) {
// Check if input stream is available
val inputStream = connection.inputStream
if (inputStream == null) {
debugLog("Input stream is null for plugin: ${plugin.path}")
continue
}
debugLog("Input stream available for plugin: ${plugin.path}")
// Create output directory
val pluginsDir = ReflectiveEditorView.pluginsDirectory
debugLog("Plugins directory: ${pluginsDir.absolutePath}")
// Validate plugins directory
if (!pluginsDir.exists()) {
debugLog("Plugins directory does not exist: ${pluginsDir.absolutePath}")
if (!pluginsDir.mkdirs()) {
debugLog("Failed to create plugins directory: ${pluginsDir.absolutePath}")
return false
}
debugLog("Created plugins directory: ${pluginsDir.absolutePath}")
}
if (!pluginsDir.isDirectory) {
debugLog("Plugins path is not a directory: ${pluginsDir.absolutePath}")
return false
}
val pluginDir = File(pluginsDir, plugin.path)
debugLog("Plugin directory: ${pluginDir.absolutePath}")
// Create directory if it doesn't exist
if (!pluginDir.exists()) {
pluginDir.mkdirs()
debugLog("Created plugin directory: ${pluginDir.absolutePath}")
}
// Download the ZIP file
val tempZipFile = File.createTempFile("plugin_", ".zip")
tempZipFile.deleteOnExit()
debugLog("Created temp file: ${tempZipFile.absolutePath}")
var totalBytesRead = 0L
inputStream.use { input ->
FileOutputStream(tempZipFile).use { outputStream ->
val buffer = ByteArray(8192)
var bytesRead: Int
debugLog("Starting to read data for plugin: ${plugin.path}")
while (input.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
totalBytesRead += bytesRead
// Report progress if content length is known
if (contentLength > 0) {
val progress = (totalBytesRead * 100 / contentLength).toInt()
// Limit progress updates to avoid flooding the UI
if (progress % 5 == 0) {
SwingUtilities.invokeLater {
callback.onProgress(plugin.path, progress)
}
}
}
}
debugLog("Finished reading data for plugin: ${plugin.path}. Total bytes: $totalBytesRead")
}
}
debugLog("Downloaded ${totalBytesRead} bytes to ${tempZipFile.absolutePath}")
// Extract the ZIP file
if (extractZipFile(tempZipFile, pluginDir, plugin.path)) {
debugLog("Successfully extracted plugin to ${pluginDir.absolutePath}")
// Clean up temp file
tempZipFile.delete()
return true
} else {
debugLog("Failed to extract plugin")
tempZipFile.delete()
continue // Try next URL format
}
} else {
debugLog("HTTP error: $responseCode for plugin: ${plugin.path} with URL: $downloadUrl")
// Try to read error stream for more details
try {
val errorStream = connection.errorStream
if (errorStream != null) {
val errorReader = BufferedReader(InputStreamReader(errorStream))
val errorResponse = errorReader.use { it.readText() }
debugLog("Error response: $errorResponse for plugin: ${plugin.path}")
} else {
debugLog("Error stream is null for plugin: ${plugin.path}")
}
} catch (e: Exception) {
debugLog("Exception while reading error stream: ${e.message} for plugin: ${plugin.path}")
}
continue // Try next URL format
}
}
debugLog("All URL formats failed for plugin: ${plugin.path}")
return false
} catch (e: Exception) {
debugLog("Exception during download and extraction for plugin ${plugin.path}: ${e.message}")
e.printStackTrace()
return false
}
}
/**
* Extract a ZIP file to the specified directory
*/
private fun extractZipFile(zipFile: File, targetDir: File, pluginPath: String): Boolean {
try {
debugLog("Extracting ZIP file: ${zipFile.absolutePath} to ${targetDir.absolutePath}")
// Validate inputs
if (!zipFile.exists()) {
debugLog("ZIP file does not exist: ${zipFile.absolutePath}")
return false
}
if (!targetDir.exists() && !targetDir.mkdirs()) {
debugLog("Failed to create target directory: ${targetDir.absolutePath}")
return false
}
ZipInputStream(FileInputStream(zipFile)).use { zis ->
var entry: ZipEntry?
var entryCount = 0
while (zis.nextEntry.also { entry = it } != null) {
entryCount++
val entryName = entry!!.name
debugLog("Processing ZIP entry $entryCount: $entryName")
// Skip the top-level directory in the ZIP (GitLab adds a project-branch-hash directory)
// We want to extract the contents of the plugin directory directly
val relativePath = if (entryName.contains("/")) {
entryName.substring(entryName.indexOf("/") + 1)
} else {
entryName
}
debugLog("Relative path for entry: $relativePath")
// Only extract files that are part of this specific plugin
if (relativePath.startsWith(pluginPath) && relativePath != pluginPath) {
val fileName = relativePath.substring(pluginPath.length + 1) // +1 for the trailing slash
debugLog("File name to extract: $fileName")
if (fileName.isNotEmpty()) {
val file = File(targetDir, fileName)
debugLog("Target file path: ${file.absolutePath}")
// Create parent directories if needed
val parent = file.parentFile
if (parent != null && !parent.exists()) {
if (parent.mkdirs()) {
debugLog("Created parent directories: ${parent.absolutePath}")
} else {
debugLog("Failed to create parent directories: ${parent.absolutePath}")
zis.closeEntry()
continue
}
}
// Extract file or directory
if (entry!!.isDirectory) {
if (!file.exists()) {
if (file.mkdirs()) {
debugLog("Created directory: ${file.absolutePath}")
} else {
debugLog("Failed to create directory: ${file.absolutePath}")
}
} else {
debugLog("Directory already exists: ${file.absolutePath}")
}
} else {
// Create file
try {
FileOutputStream(file).use { fos ->
zis.copyTo(fos)
}
debugLog("Extracted file: ${file.absolutePath}")
} catch (e: Exception) {
debugLog("Failed to extract file ${file.absolutePath}: ${e.message}")
}
}
} else {
debugLog("Skipping empty file name")
}
} else {
debugLog("Skipping entry (not part of plugin): $relativePath")
}
zis.closeEntry()
}
debugLog("Processed $entryCount entries from ZIP file")
}
debugLog("Successfully extracted ZIP file")
return true
} catch (e: Exception) {
debugLog("Exception during ZIP extraction: ${e.message}")
e.printStackTrace()
return false
}
}
}

View file

@ -11,5 +11,7 @@ data class PluginStatus(
val author: String?,
val gitLabPlugin: GitLabPlugin?,
val isInstalled: Boolean,
val needsUpdate: Boolean
val needsUpdate: Boolean,
val isDownloading: Boolean = false,
val downloadProgress: Int = 0
)