mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-09 04:26:32 +02:00
Capability of installing from the local repository
This commit is contained in:
Binary file not shown.
@@ -1,6 +1,17 @@
|
||||
[
|
||||
{
|
||||
"filename": "gitbucket-gist-plugin_2.12-4.9.0.jar",
|
||||
"id": "gist",
|
||||
"name": "Gist Plugin",
|
||||
"description": "Provides Gist feature on GitBucket.",
|
||||
"provider": "GitBucket Organization",
|
||||
"homepage": "https://github.com/gitbucket/gitbucket-gist-plugin",
|
||||
"versions": [
|
||||
{
|
||||
"version": "4.9.0",
|
||||
"range": ">4.14.0",
|
||||
"file": "gitbucket-gist-plugin_2.12-4.9.0.jar"
|
||||
}
|
||||
],
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -6,7 +6,7 @@ import gitbucket.core.admin.html
|
||||
import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService}
|
||||
import gitbucket.core.util.{AdminAuthenticator, Mailer}
|
||||
import gitbucket.core.ssh.SshServer
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.plugin.{PluginInfoBase, PluginRegistry, PluginRepository}
|
||||
import SystemSettingsService._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.SyntaxSugars._
|
||||
@@ -15,6 +15,10 @@ import gitbucket.core.util.StringUtil._
|
||||
import io.github.gitbucket.scalatra.forms._
|
||||
import org.apache.commons.io.{FileUtils, IOUtils}
|
||||
import org.scalatra.i18n.Messages
|
||||
import com.github.zafarkhaja.semver.{Version => Semver}
|
||||
import gitbucket.core.GitBucketCoreModule
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
|
||||
class SystemSettingsController extends SystemSettingsControllerBase
|
||||
with AccountService with RepositoryService with AdminAuthenticator
|
||||
@@ -181,7 +185,32 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
})
|
||||
|
||||
get("/admin/plugins")(adminOnly {
|
||||
html.plugins(PluginRegistry().getPlugins(), flash.get("info"))
|
||||
// Installed plugins
|
||||
val enabledPlugins = PluginRegistry().getPlugins()
|
||||
|
||||
val gitbucketVersion = Semver.valueOf(GitBucketCoreModule.getVersions.asScala.last.getVersion)
|
||||
|
||||
// Plugins in the local 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) })
|
||||
}.collect { case (meta, Some(version)) =>
|
||||
new PluginInfoBase(
|
||||
pluginId = meta.id,
|
||||
pluginName = meta.name,
|
||||
pluginVersion = version.version,
|
||||
description = meta.description
|
||||
)
|
||||
}
|
||||
|
||||
// Merge
|
||||
val plugins = enabledPlugins.map((_, true)) ++ repositoryPlugins.map((_, false))
|
||||
|
||||
html.plugins(plugins, flash.get("info"))
|
||||
})
|
||||
|
||||
post("/admin/plugins/_reload")(adminOnly {
|
||||
@@ -190,24 +219,35 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
redirect("/admin/plugins")
|
||||
})
|
||||
|
||||
post("/admin/plugins/:pluginId/_uninstall")(adminOnly {
|
||||
post("/admin/plugins/:pluginId/:version/_uninstall")(adminOnly {
|
||||
val pluginId = params("pluginId")
|
||||
val version = params("version")
|
||||
PluginRegistry().getPlugins()
|
||||
.collect { case (plugin, true) if plugin.pluginId == pluginId => plugin }
|
||||
.collect { case plugin if (plugin.pluginId == pluginId && plugin.pluginVersion == version) => plugin }
|
||||
.foreach { _ =>
|
||||
PluginRegistry.uninstall(pluginId, request.getServletContext, loadSystemSettings(), request2Session(request).conn)
|
||||
flash += "info" -> s"${pluginId} was uninstalled."
|
||||
}
|
||||
PluginRegistry.uninstall(pluginId, request.getServletContext, loadSystemSettings(), request2Session(request).conn)
|
||||
flash += "info" -> s"${pluginId} was uninstalled."
|
||||
}
|
||||
redirect("/admin/plugins")
|
||||
})
|
||||
|
||||
post("/admin/plugins/:pluginId/_install")(adminOnly {
|
||||
post("/admin/plugins/:pluginId/:version/_install")(adminOnly {
|
||||
val pluginId = params("pluginId")
|
||||
PluginRegistry().getPlugins()
|
||||
.collect { case (plugin, false) if plugin.pluginId == pluginId => plugin }
|
||||
.foreach { _ =>
|
||||
PluginRegistry.install(pluginId, request.getServletContext, loadSystemSettings(), request2Session(request).conn)
|
||||
flash += "info" -> s"${pluginId} was installed."
|
||||
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}"),
|
||||
request.getServletContext,
|
||||
loadSystemSettings(),
|
||||
request2Session(request).conn
|
||||
)
|
||||
flash += "info" -> s"${pluginId} was installed."
|
||||
}
|
||||
}
|
||||
redirect("/admin/plugins")
|
||||
})
|
||||
|
||||
@@ -24,11 +24,11 @@ import play.twirl.api.Html
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import com.github.zafarkhaja.semver.Version
|
||||
import com.github.zafarkhaja.semver.{Version => Semver}
|
||||
|
||||
class PluginRegistry {
|
||||
|
||||
private val plugins = new ListBuffer[(PluginInfo, Boolean)]
|
||||
private val plugins = new ListBuffer[PluginInfo]
|
||||
private val javaScripts = new ListBuffer[(String, String)]
|
||||
private val controllers = new ListBuffer[(ControllerBase, String)]
|
||||
private val images = mutable.Map[String, String]()
|
||||
@@ -62,9 +62,9 @@ class PluginRegistry {
|
||||
private val suggestionProviders = new ListBuffer[SuggestionProvider]
|
||||
suggestionProviders += new UserNameSuggestionProvider()
|
||||
|
||||
def addPlugin(pluginInfo: PluginInfo, enabled: Boolean): Unit = plugins += ((pluginInfo, enabled))
|
||||
def addPlugin(pluginInfo: PluginInfo): Unit = plugins += pluginInfo
|
||||
|
||||
def getPlugins(): List[(PluginInfo, Boolean)] = plugins.toList
|
||||
def getPlugins(): List[PluginInfo] = plugins.toList
|
||||
|
||||
def addImage(id: String, bytes: Array[Byte]): Unit = {
|
||||
val encoded = Base64.getEncoder.encodeToString(bytes)
|
||||
@@ -207,7 +207,7 @@ object PluginRegistry {
|
||||
*/
|
||||
def uninstall(pluginId: String, context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized {
|
||||
instance.getPlugins()
|
||||
.collect { case (plugin, true) if plugin.pluginId == pluginId => plugin }
|
||||
.collect { case plugin if plugin.pluginId == pluginId => plugin }
|
||||
.foreach { plugin =>
|
||||
// try {
|
||||
// plugin.pluginClass.uninstall(instance, context, settings)
|
||||
@@ -223,18 +223,14 @@ object PluginRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a specified plugin from local repository.
|
||||
* Install a plugin from a specified jar file.
|
||||
*/
|
||||
def install(pluginId: String, context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized {
|
||||
instance.getPlugins()
|
||||
.collect { case (plugin, false) if plugin.pluginId == pluginId => plugin }
|
||||
.foreach { plugin =>
|
||||
FileUtils.copyFile(plugin.pluginJar, new File(PluginHome, plugin.pluginJar.getName))
|
||||
def install(file: File, context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized {
|
||||
FileUtils.copyFile(file, new File(PluginHome, file.getName))
|
||||
|
||||
shutdown(context, settings)
|
||||
instance = new PluginRegistry()
|
||||
initialize(context, settings, conn)
|
||||
}
|
||||
shutdown(context, settings)
|
||||
instance = new PluginRegistry()
|
||||
initialize(context, settings, conn)
|
||||
}
|
||||
|
||||
private class PluginJarFileFilter extends FilenameFilter {
|
||||
@@ -244,7 +240,7 @@ object PluginRegistry {
|
||||
private def listPluginJars(dir: File): Seq[File] = {
|
||||
dir.listFiles(new PluginJarFileFilter()).map { file =>
|
||||
val Array(name, version) = file.getName.split("_2.12-")
|
||||
(name, Version.valueOf(version.replaceFirst("\\.jar$", "")), file)
|
||||
(name, Semver.valueOf(version.replaceFirst("\\.jar$", "")), file)
|
||||
}.groupBy { case (name, _, _) =>
|
||||
name
|
||||
}.map { case (name, versions) =>
|
||||
@@ -303,44 +299,10 @@ object PluginRegistry {
|
||||
pluginClass = plugin,
|
||||
pluginJar = pluginJar,
|
||||
classLoader = classLoader
|
||||
), true)
|
||||
))
|
||||
|
||||
} catch {
|
||||
case e: Throwable => {
|
||||
logger.error(s"Error during plugin initialization: ${pluginJar.getName}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scan repository
|
||||
val repositoryDir = new File(PluginHome, ".repository")
|
||||
if (repositoryDir.exists) {
|
||||
listPluginJars(repositoryDir).foreach { pluginJar =>
|
||||
val classLoader = new URLClassLoader(Array(pluginJar.toURI.toURL), Thread.currentThread.getContextClassLoader)
|
||||
try {
|
||||
val plugin = classLoader.loadClass("Plugin").getDeclaredConstructor().newInstance().asInstanceOf[Plugin]
|
||||
|
||||
val enableSameOrNewer = instance.plugins.exists { case (installedPlugin, true) =>
|
||||
installedPlugin.pluginId == plugin.pluginId &&
|
||||
Version.valueOf(installedPlugin.pluginVersion).greaterThanOrEqualTo(Version.valueOf(plugin.versions.last.getVersion))
|
||||
}
|
||||
|
||||
if(!enableSameOrNewer){
|
||||
instance.addPlugin(PluginInfo(
|
||||
pluginId = plugin.pluginId,
|
||||
pluginName = plugin.pluginName,
|
||||
pluginVersion = plugin.versions.last.getVersion,
|
||||
description = plugin.description,
|
||||
pluginClass = plugin,
|
||||
pluginJar = pluginJar,
|
||||
classLoader = classLoader
|
||||
), false)
|
||||
}
|
||||
} catch {
|
||||
case e: Throwable => {
|
||||
logger.error(s"Error during plugin initialization: ${pluginJar.getName}", e)
|
||||
}
|
||||
}
|
||||
case e: Throwable => logger.error(s"Error during plugin initialization: ${pluginJar.getName}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,9 +314,7 @@ object PluginRegistry {
|
||||
}
|
||||
|
||||
def shutdown(context: ServletContext, settings: SystemSettings): Unit = synchronized {
|
||||
instance.getPlugins()
|
||||
.collect { case (plugin, true) => plugin }
|
||||
.foreach { plugin =>
|
||||
instance.getPlugins().foreach { plugin =>
|
||||
try {
|
||||
plugin.pluginClass.shutdown(instance, context, settings)
|
||||
} catch {
|
||||
@@ -369,17 +329,29 @@ object PluginRegistry {
|
||||
|
||||
}
|
||||
|
||||
case class Link(id: String, label: String, path: String, icon: Option[String] = None)
|
||||
case class Link(
|
||||
id: String,
|
||||
label: String,
|
||||
path: String,
|
||||
icon: Option[String] = None
|
||||
)
|
||||
|
||||
class PluginInfoBase(
|
||||
val pluginId: String,
|
||||
val pluginName: String,
|
||||
val pluginVersion: String,
|
||||
val description: String
|
||||
)
|
||||
|
||||
case class PluginInfo(
|
||||
pluginId: String,
|
||||
pluginName: String,
|
||||
pluginVersion: String,
|
||||
description: String,
|
||||
override val pluginId: String,
|
||||
override val pluginName: String,
|
||||
override val pluginVersion: String,
|
||||
override val description: String,
|
||||
pluginClass: Plugin,
|
||||
pluginJar: File,
|
||||
classLoader: URLClassLoader
|
||||
)
|
||||
) extends PluginInfoBase(pluginId, pluginName, pluginVersion, description)
|
||||
|
||||
class PluginWatchThread(context: ServletContext) extends Thread with SystemSettingsService {
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
|
||||
43
src/main/scala/gitbucket/core/plugin/PluginRepository.scala
Normal file
43
src/main/scala/gitbucket/core/plugin/PluginRepository.scala
Normal file
@@ -0,0 +1,43 @@
|
||||
package gitbucket.core.plugin
|
||||
|
||||
import org.json4s._
|
||||
import gitbucket.core.util.Directory._
|
||||
import org.apache.commons.io.FileUtils
|
||||
|
||||
object PluginRepository {
|
||||
implicit val formats = DefaultFormats
|
||||
|
||||
def parsePluginJson(json: String): Seq[PluginMetadata] = {
|
||||
org.json4s.jackson.JsonMethods.parse(json).extract[Seq[PluginMetadata]]
|
||||
}
|
||||
|
||||
lazy val LocalRepositoryDir = new java.io.File(PluginHome, ".repository")
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Mapped from plugins.json
|
||||
case class PluginMetadata(
|
||||
id: String,
|
||||
name: String,
|
||||
description: String,
|
||||
provider: String,
|
||||
homepage: String,
|
||||
versions: Seq[VersionDef],
|
||||
default: Boolean = false
|
||||
){
|
||||
lazy val latestVersion: VersionDef = versions.last
|
||||
}
|
||||
|
||||
case class VersionDef(
|
||||
version: String,
|
||||
file: String,
|
||||
range: String
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import java.io.{File, FileOutputStream}
|
||||
import akka.event.Logging
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import gitbucket.core.GitBucketCoreModule
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.plugin.{PluginRegistry, PluginRepository}
|
||||
import gitbucket.core.service.{ActivityService, SystemSettingsService}
|
||||
import gitbucket.core.util.DatabaseConfig
|
||||
import gitbucket.core.util.Directory._
|
||||
@@ -18,10 +18,9 @@ import javax.servlet.{ServletContextEvent, ServletContextListener}
|
||||
|
||||
import org.apache.commons.io.{FileUtils, IOUtils}
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.json4s._
|
||||
import org.json4s.jackson.JsonMethods._
|
||||
import akka.actor.{Actor, ActorSystem, Props}
|
||||
import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension
|
||||
import com.github.zafarkhaja.semver.{Version => Semver}
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
@@ -80,7 +79,7 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
|
||||
}
|
||||
|
||||
// Install bundled plugins
|
||||
installBundledPlugins()
|
||||
extractBundledPlugins(gitbucketVersion)
|
||||
|
||||
// Load plugins
|
||||
logger.info("Initialize plugins")
|
||||
@@ -130,38 +129,38 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
|
||||
}
|
||||
}
|
||||
|
||||
private def installBundledPlugins(): Unit = {
|
||||
logger.info("Install bundled plugins")
|
||||
private def extractBundledPlugins(gitbucketVersion: String): Unit = {
|
||||
logger.info("Extract bundled plugins")
|
||||
val cl = Thread.currentThread.getContextClassLoader
|
||||
try {
|
||||
using(cl.getResourceAsStream("plugins/plugins.json")){ pluginsFile =>
|
||||
val pluginRepositoryDir = new File(PluginHome, ".repository")
|
||||
if(!pluginRepositoryDir.exists){
|
||||
pluginRepositoryDir.mkdirs()
|
||||
}
|
||||
val pluginsJson = IOUtils.toString(pluginsFile, "UTF-8")
|
||||
|
||||
implicit val formats = DefaultFormats
|
||||
val plugins = parse(IOUtils.toString(pluginsFile, "UTF-8")).extract[Seq[Plugin]]
|
||||
FileUtils.forceMkdir(PluginRepository.LocalRepositoryDir)
|
||||
FileUtils.write(PluginRepository.LocalRepositoryIndexFile, pluginsJson, "UTF-8")
|
||||
|
||||
val plugins = PluginRepository.parsePluginJson(pluginsJson)
|
||||
plugins.foreach { plugin =>
|
||||
val file = new File(pluginRepositoryDir, plugin.filename)
|
||||
if(!file.exists){
|
||||
logger.info(s"Copy ${plugin} to ${file.getAbsolutePath}")
|
||||
using(cl.getResourceAsStream("plugins/" + plugin), new FileOutputStream(file)){ case (in, out) => IOUtils.copy(in, out) }
|
||||
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){
|
||||
logger.info(s"Enable ${file.getName} in default")
|
||||
FileUtils.copyFile(file, new File(PluginHome, plugin.filename))
|
||||
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 installing bundled plugin", e)
|
||||
case e: Exception => logger.error("Error in extracting bundled plugin", e)
|
||||
}
|
||||
}
|
||||
|
||||
case class Plugin(filename: String, default: Boolean = false)
|
||||
|
||||
override def contextDestroyed(event: ServletContextEvent): Unit = {
|
||||
// Shutdown Quartz scheduler
|
||||
system.terminate()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@(plugins: List[(gitbucket.core.plugin.PluginInfo, Boolean)], info: Option[Any])(implicit context: gitbucket.core.controller.Context)
|
||||
@(plugins: List[(gitbucket.core.plugin.PluginInfoBase, Boolean)], 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)
|
||||
@@ -17,11 +17,11 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading strong" id="@plugin.pluginId">
|
||||
@if(enabled){
|
||||
<form action="@{context.path}/admin/plugins/@{plugin.pluginId}/_uninstall" method="POST" class="pull-right uninstall-form">
|
||||
<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}/_install" method="POST" class="pull-right install-form">
|
||||
<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">
|
||||
</form>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user