Capability of installing from the local repository

This commit is contained in:
Naoki Takezoe
2017-07-07 11:43:25 +09:00
parent aebcf5d183
commit d8fe6a0a55
7 changed files with 165 additions and 100 deletions

View File

@@ -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
}
]

View File

@@ -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")
})

View File

@@ -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._

View 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
)

View File

@@ -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()

View File

@@ -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>
}