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 {
// 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")

View File

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

View File

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

View File

@@ -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 = {

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.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">