mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-01 11:06:06 +01:00
Experiment of plugin installation from the remote repository
This commit is contained in:
@@ -322,21 +322,16 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
get("/admin/plugins")(adminOnly {
|
||||
// Installed plugins
|
||||
val enabledPlugins = PluginRegistry().getPlugins()
|
||||
val gitbucketVersion = GitBucketCoreModule.getVersions.asScala.last.getVersion
|
||||
|
||||
val gitbucketVersion = Semver.valueOf(GitBucketCoreModule.getVersions.asScala.last.getVersion)
|
||||
|
||||
// Plugins in the local repository
|
||||
// Plugins in the remote repository
|
||||
val repositoryPlugins = PluginRepository
|
||||
.getPlugins()
|
||||
.filterNot { meta =>
|
||||
enabledPlugins.exists { plugin =>
|
||||
plugin.pluginId == meta.id &&
|
||||
Semver.valueOf(plugin.pluginVersion).greaterThanOrEqualTo(Semver.valueOf(meta.latestVersion.version))
|
||||
}
|
||||
}
|
||||
.map { meta =>
|
||||
(meta, meta.versions.reverse.find { version =>
|
||||
gitbucketVersion.satisfies(version.range)
|
||||
gitbucketVersion == version.gitbucketVersion && !enabledPlugins.exists { plugin =>
|
||||
plugin.pluginId == meta.id && plugin.pluginVersion == version.version
|
||||
}
|
||||
})
|
||||
}
|
||||
.collect {
|
||||
@@ -345,12 +340,20 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
pluginId = meta.id,
|
||||
pluginName = meta.name,
|
||||
pluginVersion = version.version,
|
||||
gitbucketVersion = Some(version.gitbucketVersion),
|
||||
description = meta.description
|
||||
)
|
||||
}
|
||||
|
||||
// Merge
|
||||
val plugins = enabledPlugins.map((_, true)) ++ repositoryPlugins.map((_, false))
|
||||
val plugins = (enabledPlugins.map((_, true)) ++ repositoryPlugins.map((_, false)))
|
||||
.groupBy(_._1.pluginId)
|
||||
.map {
|
||||
case (pluginId, plugins) =>
|
||||
val (plugin, enabled) = plugins.head
|
||||
(plugin, enabled, if (plugins.length > 1) plugins.last._1.pluginVersion else "")
|
||||
}
|
||||
.toList
|
||||
|
||||
html.plugins(plugins, flash.get("info"))
|
||||
})
|
||||
@@ -378,21 +381,20 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
post("/admin/plugins/:pluginId/:version/_install")(adminOnly {
|
||||
val pluginId = params("pluginId")
|
||||
val version = params("version")
|
||||
/// TODO!!!!
|
||||
|
||||
PluginRepository
|
||||
.getPlugins()
|
||||
.collect { case meta if meta.id == pluginId => (meta, meta.versions.find(_.version == version)) }
|
||||
.foreach {
|
||||
case (meta, version) =>
|
||||
version.foreach { version =>
|
||||
// TODO Install version!
|
||||
PluginRegistry.install(
|
||||
new java.io.File(PluginHome, s".repository/${version.file}"),
|
||||
new java.net.URL(version.url),
|
||||
request.getServletContext,
|
||||
loadSystemSettings(),
|
||||
request2Session(request).conn
|
||||
)
|
||||
flash += "info" -> s"${pluginId} was installed."
|
||||
flash += "info" -> s"${pluginId}:${version.version} was installed."
|
||||
}
|
||||
}
|
||||
redirect("/admin/plugins")
|
||||
|
||||
@@ -9,6 +9,8 @@ import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.servlet.ServletContext
|
||||
|
||||
import com.github.zafarkhaja.semver.Version
|
||||
import gitbucket.core.GitBucketCoreModule
|
||||
import gitbucket.core.controller.{Context, ControllerBase}
|
||||
import gitbucket.core.model.{Account, Issue}
|
||||
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
|
||||
@@ -202,7 +204,7 @@ object PluginRegistry {
|
||||
|
||||
private var watcher: PluginWatchThread = null
|
||||
private var extraWatcher: PluginWatchThread = null
|
||||
private val initializing = new AtomicBoolean(false)
|
||||
//private val initializing = new AtomicBoolean(false)
|
||||
|
||||
/**
|
||||
* Returns the PluginRegistry singleton instance.
|
||||
@@ -234,7 +236,15 @@ object PluginRegistry {
|
||||
// logger.error(s"Error during uninstalling plugin: ${plugin.pluginJar.getName}", e)
|
||||
// }
|
||||
shutdown(context, settings)
|
||||
plugin.pluginJar.delete()
|
||||
|
||||
new File(PluginHome)
|
||||
.listFiles((_: File, name: String) => {
|
||||
name.startsWith(s"gitbucket-${pluginId}-plugin") && name.endsWith(".jar")
|
||||
})
|
||||
.foreach { file =>
|
||||
file.delete()
|
||||
}
|
||||
|
||||
instance = new PluginRegistry()
|
||||
initialize(context, settings, conn)
|
||||
}
|
||||
@@ -243,10 +253,11 @@ object PluginRegistry {
|
||||
/**
|
||||
* Install a plugin from a specified jar file.
|
||||
*/
|
||||
def install(file: File, context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit =
|
||||
def install(url: java.net.URL, context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit =
|
||||
synchronized {
|
||||
shutdown(context, settings)
|
||||
FileUtils.copyFile(file, new File(PluginHome, file.getName))
|
||||
val in = url.openStream()
|
||||
FileUtils.copyToFile(in, new File(PluginHome, new File(url.getFile).getName))
|
||||
instance = new PluginRegistry()
|
||||
initialize(context, settings, conn)
|
||||
}
|
||||
@@ -257,12 +268,27 @@ object PluginRegistry {
|
||||
override def accept(dir: File, name: String): Boolean = name.endsWith(".jar")
|
||||
})
|
||||
.toSeq
|
||||
.sortBy(_.getName)
|
||||
.sortBy(x => Version.valueOf(getPluginVersion(x.getName)))
|
||||
.reverse
|
||||
}
|
||||
|
||||
lazy val extraPluginDir: Option[String] = Option(System.getProperty("gitbucket.pluginDir"))
|
||||
|
||||
def getGitBucketVersion(pluginJarFileName: String): Option[String] = {
|
||||
val regex = ".+-gitbucket\\_(\\d+\\.\\d+\\.\\d+)-.+".r
|
||||
pluginJarFileName match {
|
||||
case regex(x) => Some(x)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
def getPluginVersion(pluginJarFileName: String): String = {
|
||||
val regex = ".+-(\\d+\\.\\d+\\.\\d+)\\.jar$".r
|
||||
pluginJarFileName match {
|
||||
case regex(x) => x
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes all installed plugins.
|
||||
*/
|
||||
@@ -278,6 +304,7 @@ object PluginRegistry {
|
||||
installedDir.mkdirs()
|
||||
|
||||
val pluginJars = listPluginJars(pluginDir)
|
||||
|
||||
val extraJars = extraPluginDir
|
||||
.map { extraDir =>
|
||||
listPluginJars(new File(extraDir))
|
||||
@@ -288,9 +315,9 @@ object PluginRegistry {
|
||||
val installedJar = new File(installedDir, pluginJar.getName)
|
||||
|
||||
FileUtils.copyFile(pluginJar, installedJar)
|
||||
|
||||
logger.info(s"Initialize ${pluginJar.getName}")
|
||||
val classLoader = new URLClassLoader(Array(installedJar.toURI.toURL), Thread.currentThread.getContextClassLoader)
|
||||
val classLoader =
|
||||
new URLClassLoader(Array(installedJar.toURI.toURL), Thread.currentThread.getContextClassLoader)
|
||||
try {
|
||||
val plugin = classLoader.loadClass("Plugin").getDeclaredConstructor().newInstance().asInstanceOf[Plugin]
|
||||
val pluginId = plugin.pluginId
|
||||
@@ -304,7 +331,12 @@ object PluginRegistry {
|
||||
// Migration
|
||||
val solidbase = new Solidbase()
|
||||
solidbase
|
||||
.migrate(conn, classLoader, DatabaseConfig.liquiDriver, new Module(plugin.pluginId, plugin.versions: _*))
|
||||
.migrate(
|
||||
conn,
|
||||
classLoader,
|
||||
DatabaseConfig.liquiDriver,
|
||||
new Module(plugin.pluginId, plugin.versions: _*)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
// Check database version
|
||||
@@ -323,6 +355,7 @@ object PluginRegistry {
|
||||
pluginId = plugin.pluginId,
|
||||
pluginName = plugin.pluginName,
|
||||
pluginVersion = plugin.versions.last.getVersion,
|
||||
gitbucketVersion = getGitBucketVersion(installedJar.getName),
|
||||
description = plugin.description,
|
||||
pluginClass = plugin,
|
||||
pluginJar = pluginJar,
|
||||
@@ -334,6 +367,7 @@ object PluginRegistry {
|
||||
} catch {
|
||||
case e: Throwable => logger.error(s"Error during plugin initialization: ${pluginJar.getName}", e)
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
if (watcher == null) {
|
||||
@@ -384,6 +418,7 @@ class PluginInfoBase(
|
||||
val pluginId: String,
|
||||
val pluginName: String,
|
||||
val pluginVersion: String,
|
||||
val gitbucketVersion: Option[String],
|
||||
val description: String
|
||||
)
|
||||
|
||||
@@ -391,11 +426,12 @@ case class PluginInfo(
|
||||
override val pluginId: String,
|
||||
override val pluginName: String,
|
||||
override val pluginVersion: String,
|
||||
override val gitbucketVersion: Option[String],
|
||||
override val description: String,
|
||||
pluginClass: Plugin,
|
||||
pluginJar: File,
|
||||
classLoader: URLClassLoader
|
||||
) extends PluginInfoBase(pluginId, pluginName, pluginVersion, description)
|
||||
) extends PluginInfoBase(pluginId, pluginName, pluginVersion, gitbucketVersion, description)
|
||||
|
||||
class PluginWatchThread(context: ServletContext, dir: String) extends Thread with SystemSettingsService {
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
|
||||
@@ -2,7 +2,7 @@ package gitbucket.core.plugin
|
||||
|
||||
import org.json4s._
|
||||
import gitbucket.core.util.Directory._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.{FileUtils, IOUtils}
|
||||
|
||||
object PluginRepository {
|
||||
implicit val formats = DefaultFormats
|
||||
@@ -15,9 +15,10 @@ object PluginRepository {
|
||||
lazy val LocalRepositoryIndexFile = new java.io.File(LocalRepositoryDir, "plugins.json")
|
||||
|
||||
def getPlugins(): Seq[PluginMetadata] = {
|
||||
if (LocalRepositoryIndexFile.exists) {
|
||||
parsePluginJson(FileUtils.readFileToString(LocalRepositoryIndexFile, "UTF-8"))
|
||||
} else Nil
|
||||
// TODO Pre-load the plugin list in background
|
||||
val url = new java.net.URL("https://plugins.gitbucket-community.org/releases/plugins.json")
|
||||
val str = IOUtils.toString(url, "UTF-8")
|
||||
parsePluginJson(str)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -36,7 +37,5 @@ case class PluginMetadata(
|
||||
case class VersionDef(
|
||||
version: String,
|
||||
url: String,
|
||||
range: String
|
||||
) {
|
||||
lazy val file = url.substring(url.lastIndexOf("/") + 1)
|
||||
}
|
||||
gitbucketVersion: String
|
||||
)
|
||||
|
||||
@@ -136,46 +136,46 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
|
||||
}
|
||||
|
||||
private def extractBundledPlugins(gitbucketVersion: String): Unit = {
|
||||
logger.info("Extract bundled plugins")
|
||||
val cl = Thread.currentThread.getContextClassLoader
|
||||
try {
|
||||
using(cl.getResourceAsStream("plugins/plugins.json")) { pluginsFile =>
|
||||
if (pluginsFile != null) {
|
||||
val pluginsJson = IOUtils.toString(pluginsFile, "UTF-8")
|
||||
|
||||
FileUtils.forceMkdir(PluginRepository.LocalRepositoryDir)
|
||||
FileUtils.write(PluginRepository.LocalRepositoryIndexFile, pluginsJson, "UTF-8")
|
||||
|
||||
val plugins = PluginRepository.parsePluginJson(pluginsJson)
|
||||
plugins.foreach { plugin =>
|
||||
plugin.versions
|
||||
.sortBy { x =>
|
||||
Semver.valueOf(x.version)
|
||||
}
|
||||
.reverse
|
||||
.zipWithIndex
|
||||
.foreach {
|
||||
case (version, i) =>
|
||||
val file = new File(PluginRepository.LocalRepositoryDir, version.file)
|
||||
if (!file.exists) {
|
||||
logger.info(s"Copy ${plugin} to ${file.getAbsolutePath}")
|
||||
FileUtils.forceMkdirParent(file)
|
||||
using(cl.getResourceAsStream("plugins/" + version.file), new FileOutputStream(file)) {
|
||||
case (in, out) => IOUtils.copy(in, out)
|
||||
}
|
||||
|
||||
if (plugin.default && i == 0) {
|
||||
logger.info(s"Enable ${file.getName} in default")
|
||||
FileUtils.copyFile(file, new File(PluginHome, version.file))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
case e: Exception => logger.error("Error in extracting bundled plugin", e)
|
||||
}
|
||||
// logger.info("Extract bundled plugins")
|
||||
// val cl = Thread.currentThread.getContextClassLoader
|
||||
// try {
|
||||
// using(cl.getResourceAsStream("plugins/plugins.json")) { pluginsFile =>
|
||||
// if (pluginsFile != null) {
|
||||
// val pluginsJson = IOUtils.toString(pluginsFile, "UTF-8")
|
||||
//
|
||||
// FileUtils.forceMkdir(PluginRepository.LocalRepositoryDir)
|
||||
// FileUtils.write(PluginRepository.LocalRepositoryIndexFile, pluginsJson, "UTF-8")
|
||||
//
|
||||
// val plugins = PluginRepository.parsePluginJson(pluginsJson)
|
||||
// plugins.foreach { plugin =>
|
||||
// plugin.versions
|
||||
// .sortBy { x =>
|
||||
// Semver.valueOf(x.version)
|
||||
// }
|
||||
// .reverse
|
||||
// .zipWithIndex
|
||||
// .foreach {
|
||||
// case (version, i) =>
|
||||
// val file = new File(PluginRepository.LocalRepositoryDir, version.file)
|
||||
// if (!file.exists) {
|
||||
// logger.info(s"Copy ${plugin} to ${file.getAbsolutePath}")
|
||||
// FileUtils.forceMkdirParent(file)
|
||||
// using(cl.getResourceAsStream("plugins/" + version.file), new FileOutputStream(file)) {
|
||||
// case (in, out) => IOUtils.copy(in, out)
|
||||
// }
|
||||
//
|
||||
// if (plugin.default && i == 0) {
|
||||
// logger.info(s"Enable ${file.getName} in default")
|
||||
// FileUtils.copyFile(file, new File(PluginHome, version.file))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch {
|
||||
// case e: Exception => logger.error("Error in extracting bundled plugin", e)
|
||||
// }
|
||||
}
|
||||
|
||||
override def contextDestroyed(event: ServletContextEvent): Unit = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@(plugins: List[(gitbucket.core.plugin.PluginInfoBase, Boolean)], info: Option[Any])(implicit context: gitbucket.core.controller.Context)
|
||||
@(plugins: List[(gitbucket.core.plugin.PluginInfoBase, Boolean, String)], info: Option[Any])(implicit context: gitbucket.core.controller.Context)
|
||||
@gitbucket.core.html.main("Plugins"){
|
||||
@gitbucket.core.admin.html.menu("plugins") {
|
||||
@gitbucket.core.helper.html.information(info)
|
||||
@@ -8,18 +8,24 @@
|
||||
<h1 class="system-settings-title">Plugins</h1>
|
||||
@if(plugins.size > 0) {
|
||||
<ul>
|
||||
@plugins.map { case (plugin, enabled) =>
|
||||
@plugins.map { case (plugin, enabled, updatableVersion) =>
|
||||
<li><a href="#@plugin.pluginId">@plugin.pluginId:@plugin.pluginVersion</a></li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@plugins.map { case (plugin, enabled) =>
|
||||
@plugins.map { case (plugin, enabled, updatableVersion) =>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading strong" id="@plugin.pluginId">
|
||||
@if(enabled){
|
||||
<form action="@{context.path}/admin/plugins/@{plugin.pluginId}/@{plugin.pluginVersion}/_uninstall" method="POST" class="pull-right uninstall-form">
|
||||
<input type="submit" value="Uninstall" class="btn btn-danger btn-sm" style="position: relative; top: -5px; left: 10px;" data-name="@plugin.pluginName">
|
||||
</form>
|
||||
@if(updatableVersion.isEmpty){
|
||||
<form action="@{context.path}/admin/plugins/@{plugin.pluginId}/@{plugin.pluginVersion}/_uninstall" method="POST" class="pull-right uninstall-form">
|
||||
<input type="submit" value="Uninstall" class="btn btn-danger btn-sm" style="position: relative; top: -5px; left: 10px;" data-name="@plugin.pluginName">
|
||||
</form>
|
||||
} else {
|
||||
<form action="@{context.path}/admin/plugins/@{plugin.pluginId}/@{updatableVersion}/_install" method="POST" class="pull-right install-form">
|
||||
<input type="submit" value="Update" class="btn btn-primary btn-sm" style="position: relative; top: -5px; left: 10px;" data-name="@plugin.pluginName">
|
||||
</form>
|
||||
}
|
||||
} else {
|
||||
<form action="@{context.path}/admin/plugins/@{plugin.pluginId}/@{plugin.pluginVersion}/_install" method="POST" class="pull-right install-form">
|
||||
<input type="submit" value="Install" class="btn btn-success btn-sm" style="position: relative; top: -5px; left: 10px;" data-name="@plugin.pluginName">
|
||||
|
||||
Reference in New Issue
Block a user