Experiment of plugin installation from the remote repository

This commit is contained in:
Naoki Takezoe
2018-06-10 20:29:09 +09:00
parent 65ac7b7b13
commit 5fc3ce34a3
5 changed files with 121 additions and 78 deletions

View File

@@ -322,21 +322,16 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
get("/admin/plugins")(adminOnly { get("/admin/plugins")(adminOnly {
// Installed plugins // Installed plugins
val enabledPlugins = PluginRegistry().getPlugins() val enabledPlugins = PluginRegistry().getPlugins()
val gitbucketVersion = GitBucketCoreModule.getVersions.asScala.last.getVersion
val gitbucketVersion = Semver.valueOf(GitBucketCoreModule.getVersions.asScala.last.getVersion) // Plugins in the remote repository
// Plugins in the local repository
val repositoryPlugins = PluginRepository val repositoryPlugins = PluginRepository
.getPlugins() .getPlugins()
.filterNot { meta =>
enabledPlugins.exists { plugin =>
plugin.pluginId == meta.id &&
Semver.valueOf(plugin.pluginVersion).greaterThanOrEqualTo(Semver.valueOf(meta.latestVersion.version))
}
}
.map { meta => .map { meta =>
(meta, meta.versions.reverse.find { version => (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 { .collect {
@@ -345,12 +340,20 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
pluginId = meta.id, pluginId = meta.id,
pluginName = meta.name, pluginName = meta.name,
pluginVersion = version.version, pluginVersion = version.version,
gitbucketVersion = Some(version.gitbucketVersion),
description = meta.description description = meta.description
) )
} }
// Merge // 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")) html.plugins(plugins, flash.get("info"))
}) })
@@ -378,21 +381,20 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
post("/admin/plugins/:pluginId/:version/_install")(adminOnly { post("/admin/plugins/:pluginId/:version/_install")(adminOnly {
val pluginId = params("pluginId") val pluginId = params("pluginId")
val version = params("version") val version = params("version")
/// TODO!!!!
PluginRepository PluginRepository
.getPlugins() .getPlugins()
.collect { case meta if meta.id == pluginId => (meta, meta.versions.find(_.version == version)) } .collect { case meta if meta.id == pluginId => (meta, meta.versions.find(_.version == version)) }
.foreach { .foreach {
case (meta, version) => case (meta, version) =>
version.foreach { version => version.foreach { version =>
// TODO Install version!
PluginRegistry.install( PluginRegistry.install(
new java.io.File(PluginHome, s".repository/${version.file}"), new java.net.URL(version.url),
request.getServletContext, request.getServletContext,
loadSystemSettings(), loadSystemSettings(),
request2Session(request).conn request2Session(request).conn
) )
flash += "info" -> s"${pluginId} was installed." flash += "info" -> s"${pluginId}:${version.version} was installed."
} }
} }
redirect("/admin/plugins") redirect("/admin/plugins")

View File

@@ -9,6 +9,8 @@ import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.servlet.ServletContext import javax.servlet.ServletContext
import com.github.zafarkhaja.semver.Version
import gitbucket.core.GitBucketCoreModule
import gitbucket.core.controller.{Context, ControllerBase} import gitbucket.core.controller.{Context, ControllerBase}
import gitbucket.core.model.{Account, Issue} import gitbucket.core.model.{Account, Issue}
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
@@ -202,7 +204,7 @@ object PluginRegistry {
private var watcher: PluginWatchThread = null private var watcher: PluginWatchThread = null
private var extraWatcher: PluginWatchThread = null private var extraWatcher: PluginWatchThread = null
private val initializing = new AtomicBoolean(false) //private val initializing = new AtomicBoolean(false)
/** /**
* Returns the PluginRegistry singleton instance. * Returns the PluginRegistry singleton instance.
@@ -234,7 +236,15 @@ object PluginRegistry {
// logger.error(s"Error during uninstalling plugin: ${plugin.pluginJar.getName}", e) // logger.error(s"Error during uninstalling plugin: ${plugin.pluginJar.getName}", e)
// } // }
shutdown(context, settings) 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() instance = new PluginRegistry()
initialize(context, settings, conn) initialize(context, settings, conn)
} }
@@ -243,10 +253,11 @@ object PluginRegistry {
/** /**
* Install a plugin from a specified jar file. * 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 { synchronized {
shutdown(context, settings) 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() instance = new PluginRegistry()
initialize(context, settings, conn) initialize(context, settings, conn)
} }
@@ -257,12 +268,27 @@ object PluginRegistry {
override def accept(dir: File, name: String): Boolean = name.endsWith(".jar") override def accept(dir: File, name: String): Boolean = name.endsWith(".jar")
}) })
.toSeq .toSeq
.sortBy(_.getName) .sortBy(x => Version.valueOf(getPluginVersion(x.getName)))
.reverse .reverse
} }
lazy val extraPluginDir: Option[String] = Option(System.getProperty("gitbucket.pluginDir")) 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. * Initializes all installed plugins.
*/ */
@@ -278,6 +304,7 @@ object PluginRegistry {
installedDir.mkdirs() installedDir.mkdirs()
val pluginJars = listPluginJars(pluginDir) val pluginJars = listPluginJars(pluginDir)
val extraJars = extraPluginDir val extraJars = extraPluginDir
.map { extraDir => .map { extraDir =>
listPluginJars(new File(extraDir)) listPluginJars(new File(extraDir))
@@ -288,9 +315,9 @@ object PluginRegistry {
val installedJar = new File(installedDir, pluginJar.getName) val installedJar = new File(installedDir, pluginJar.getName)
FileUtils.copyFile(pluginJar, installedJar) FileUtils.copyFile(pluginJar, installedJar)
logger.info(s"Initialize ${pluginJar.getName}") 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 { try {
val plugin = classLoader.loadClass("Plugin").getDeclaredConstructor().newInstance().asInstanceOf[Plugin] val plugin = classLoader.loadClass("Plugin").getDeclaredConstructor().newInstance().asInstanceOf[Plugin]
val pluginId = plugin.pluginId val pluginId = plugin.pluginId
@@ -304,7 +331,12 @@ object PluginRegistry {
// Migration // Migration
val solidbase = new Solidbase() val solidbase = new Solidbase()
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() conn.commit()
// Check database version // Check database version
@@ -323,6 +355,7 @@ object PluginRegistry {
pluginId = plugin.pluginId, pluginId = plugin.pluginId,
pluginName = plugin.pluginName, pluginName = plugin.pluginName,
pluginVersion = plugin.versions.last.getVersion, pluginVersion = plugin.versions.last.getVersion,
gitbucketVersion = getGitBucketVersion(installedJar.getName),
description = plugin.description, description = plugin.description,
pluginClass = plugin, pluginClass = plugin,
pluginJar = pluginJar, pluginJar = pluginJar,
@@ -334,6 +367,7 @@ object PluginRegistry {
} catch { } 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)
} }
// }
} }
if (watcher == null) { if (watcher == null) {
@@ -384,6 +418,7 @@ class PluginInfoBase(
val pluginId: String, val pluginId: String,
val pluginName: String, val pluginName: String,
val pluginVersion: String, val pluginVersion: String,
val gitbucketVersion: Option[String],
val description: String val description: String
) )
@@ -391,11 +426,12 @@ case class PluginInfo(
override val pluginId: String, override val pluginId: String,
override val pluginName: String, override val pluginName: String,
override val pluginVersion: String, override val pluginVersion: String,
override val gitbucketVersion: Option[String],
override val description: String, override val description: String,
pluginClass: Plugin, pluginClass: Plugin,
pluginJar: File, pluginJar: File,
classLoader: URLClassLoader 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 { class PluginWatchThread(context: ServletContext, dir: String) extends Thread with SystemSettingsService {
import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.model.Profile.profile.blockingApi._

View File

@@ -2,7 +2,7 @@ package gitbucket.core.plugin
import org.json4s._ import org.json4s._
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import org.apache.commons.io.FileUtils import org.apache.commons.io.{FileUtils, IOUtils}
object PluginRepository { object PluginRepository {
implicit val formats = DefaultFormats implicit val formats = DefaultFormats
@@ -15,9 +15,10 @@ object PluginRepository {
lazy val LocalRepositoryIndexFile = new java.io.File(LocalRepositoryDir, "plugins.json") lazy val LocalRepositoryIndexFile = new java.io.File(LocalRepositoryDir, "plugins.json")
def getPlugins(): Seq[PluginMetadata] = { def getPlugins(): Seq[PluginMetadata] = {
if (LocalRepositoryIndexFile.exists) { // TODO Pre-load the plugin list in background
parsePluginJson(FileUtils.readFileToString(LocalRepositoryIndexFile, "UTF-8")) val url = new java.net.URL("https://plugins.gitbucket-community.org/releases/plugins.json")
} else Nil val str = IOUtils.toString(url, "UTF-8")
parsePluginJson(str)
} }
} }
@@ -36,7 +37,5 @@ case class PluginMetadata(
case class VersionDef( case class VersionDef(
version: String, version: String,
url: String, url: String,
range: String gitbucketVersion: String
) { )
lazy val file = url.substring(url.lastIndexOf("/") + 1)
}

View File

@@ -136,46 +136,46 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
} }
private def extractBundledPlugins(gitbucketVersion: String): Unit = { private def extractBundledPlugins(gitbucketVersion: String): Unit = {
logger.info("Extract bundled plugins") // logger.info("Extract bundled plugins")
val cl = Thread.currentThread.getContextClassLoader // val cl = Thread.currentThread.getContextClassLoader
try { // try {
using(cl.getResourceAsStream("plugins/plugins.json")) { pluginsFile => // using(cl.getResourceAsStream("plugins/plugins.json")) { pluginsFile =>
if (pluginsFile != null) { // if (pluginsFile != null) {
val pluginsJson = IOUtils.toString(pluginsFile, "UTF-8") // val pluginsJson = IOUtils.toString(pluginsFile, "UTF-8")
//
FileUtils.forceMkdir(PluginRepository.LocalRepositoryDir) // FileUtils.forceMkdir(PluginRepository.LocalRepositoryDir)
FileUtils.write(PluginRepository.LocalRepositoryIndexFile, pluginsJson, "UTF-8") // FileUtils.write(PluginRepository.LocalRepositoryIndexFile, pluginsJson, "UTF-8")
//
val plugins = PluginRepository.parsePluginJson(pluginsJson) // val plugins = PluginRepository.parsePluginJson(pluginsJson)
plugins.foreach { plugin => // plugins.foreach { plugin =>
plugin.versions // plugin.versions
.sortBy { x => // .sortBy { x =>
Semver.valueOf(x.version) // Semver.valueOf(x.version)
} // }
.reverse // .reverse
.zipWithIndex // .zipWithIndex
.foreach { // .foreach {
case (version, i) => // case (version, i) =>
val file = new File(PluginRepository.LocalRepositoryDir, version.file) // val file = new File(PluginRepository.LocalRepositoryDir, version.file)
if (!file.exists) { // if (!file.exists) {
logger.info(s"Copy ${plugin} to ${file.getAbsolutePath}") // logger.info(s"Copy ${plugin} to ${file.getAbsolutePath}")
FileUtils.forceMkdirParent(file) // FileUtils.forceMkdirParent(file)
using(cl.getResourceAsStream("plugins/" + version.file), new FileOutputStream(file)) { // using(cl.getResourceAsStream("plugins/" + version.file), new FileOutputStream(file)) {
case (in, out) => IOUtils.copy(in, out) // case (in, out) => IOUtils.copy(in, out)
} // }
//
if (plugin.default && i == 0) { // if (plugin.default && i == 0) {
logger.info(s"Enable ${file.getName} in default") // logger.info(s"Enable ${file.getName} in default")
FileUtils.copyFile(file, new File(PluginHome, version.file)) // FileUtils.copyFile(file, new File(PluginHome, version.file))
} // }
} // }
} // }
} // }
} // }
} // }
} catch { // } catch {
case e: Exception => logger.error("Error in extracting bundled plugin", e) // case e: Exception => logger.error("Error in extracting bundled plugin", e)
} // }
} }
override def contextDestroyed(event: ServletContextEvent): Unit = { override def contextDestroyed(event: ServletContextEvent): Unit = {

View File

@@ -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.html.main("Plugins"){
@gitbucket.core.admin.html.menu("plugins") { @gitbucket.core.admin.html.menu("plugins") {
@gitbucket.core.helper.html.information(info) @gitbucket.core.helper.html.information(info)
@@ -8,18 +8,24 @@
<h1 class="system-settings-title">Plugins</h1> <h1 class="system-settings-title">Plugins</h1>
@if(plugins.size > 0) { @if(plugins.size > 0) {
<ul> <ul>
@plugins.map { case (plugin, enabled) => @plugins.map { case (plugin, enabled, updatableVersion) =>
<li><a href="#@plugin.pluginId">@plugin.pluginId:@plugin.pluginVersion</a></li> <li><a href="#@plugin.pluginId">@plugin.pluginId:@plugin.pluginVersion</a></li>
} }
</ul> </ul>
@plugins.map { case (plugin, enabled) => @plugins.map { case (plugin, enabled, updatableVersion) =>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading strong" id="@plugin.pluginId"> <div class="panel-heading strong" id="@plugin.pluginId">
@if(enabled){ @if(enabled){
@if(updatableVersion.isEmpty){
<form action="@{context.path}/admin/plugins/@{plugin.pluginId}/@{plugin.pluginVersion}/_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"> <input type="submit" value="Uninstall" class="btn btn-danger btn-sm" style="position: relative; top: -5px; left: 10px;" data-name="@plugin.pluginName">
</form> </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 { } else {
<form action="@{context.path}/admin/plugins/@{plugin.pluginId}/@{plugin.pluginVersion}/_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"> <input type="submit" value="Install" class="btn btn-success btn-sm" style="position: relative; top: -5px; left: 10px;" data-name="@plugin.pluginName">