diff --git a/src/main/resources/plugins/gitbucket-gist-plugin_2.12-4.9.0.jar b/src/main/resources/plugins/gitbucket-gist-plugin_2.12-4.9.0.jar index 88f789d..b803b4e 100644 --- a/src/main/resources/plugins/gitbucket-gist-plugin_2.12-4.9.0.jar +++ b/src/main/resources/plugins/gitbucket-gist-plugin_2.12-4.9.0.jar Binary files differ diff --git a/src/main/resources/plugins/plugins.json b/src/main/resources/plugins/plugins.json index 3cde815..93a4f40 100644 --- a/src/main/resources/plugins/plugins.json +++ b/src/main/resources/plugins/plugins.json @@ -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 } ] diff --git a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala index babda92..7841721 100644 --- a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala @@ -6,7 +6,7 @@ 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 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 @@ }) 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 @@ 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") }) diff --git a/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala b/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala index 1750af2..715bd91 100644 --- a/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala +++ b/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala @@ -24,11 +24,11 @@ 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 @@ 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 @@ */ 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 @@ } /** - * 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 @@ 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 @@ 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 @@ } 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 @@ } -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._ diff --git a/src/main/scala/gitbucket/core/plugin/PluginRepository.scala b/src/main/scala/gitbucket/core/plugin/PluginRepository.scala new file mode 100644 index 0000000..d1b98ff --- /dev/null +++ b/src/main/scala/gitbucket/core/plugin/PluginRepository.scala @@ -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 +) + diff --git a/src/main/scala/gitbucket/core/servlet/InitializeListener.scala b/src/main/scala/gitbucket/core/servlet/InitializeListener.scala index 2597eae..9e46f65 100644 --- a/src/main/scala/gitbucket/core/servlet/InitializeListener.scala +++ b/src/main/scala/gitbucket/core/servlet/InitializeListener.scala @@ -5,7 +5,7 @@ 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 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 @@ } // Install bundled plugins - installBundledPlugins() + extractBundledPlugins(gitbucketVersion) // Load plugins logger.info("Initialize plugins") @@ -130,38 +129,38 @@ } } - 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() diff --git a/src/main/twirl/gitbucket/core/admin/plugins.scala.html b/src/main/twirl/gitbucket/core/admin/plugins.scala.html index 4d65531..9f37b45 100644 --- a/src/main/twirl/gitbucket/core/admin/plugins.scala.html +++ b/src/main/twirl/gitbucket/core/admin/plugins.scala.html @@ -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 @@
@if(enabled){ -
+
} else { -
+
}