diff --git a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala index 69f0223..dd9edc9 100644 --- a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala @@ -322,21 +322,16 @@ 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 @@ 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 @@ 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") diff --git a/src/main/scala/gitbucket/core/plugin/PluginRegistry.scala b/src/main/scala/gitbucket/core/plugin/PluginRegistry.scala index 287a5bc..857e501 100644 --- a/src/main/scala/gitbucket/core/plugin/PluginRegistry.scala +++ b/src/main/scala/gitbucket/core/plugin/PluginRegistry.scala @@ -9,6 +9,8 @@ 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 @@ 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 @@ // 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 @@ /** * 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 @@ 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 @@ installedDir.mkdirs() val pluginJars = listPluginJars(pluginDir) + val extraJars = extraPluginDir .map { extraDir => listPluginJars(new File(extraDir)) @@ -288,9 +315,9 @@ 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 @@ // 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 @@ 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 @@ } catch { case e: Throwable => logger.error(s"Error during plugin initialization: ${pluginJar.getName}", e) } +// } } if (watcher == null) { @@ -384,6 +418,7 @@ val pluginId: String, val pluginName: String, val pluginVersion: String, + val gitbucketVersion: Option[String], val description: String ) @@ -391,11 +426,12 @@ 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._ diff --git a/src/main/scala/gitbucket/core/plugin/PluginRepository.scala b/src/main/scala/gitbucket/core/plugin/PluginRepository.scala index e3d9d1c..39a909f 100644 --- a/src/main/scala/gitbucket/core/plugin/PluginRepository.scala +++ b/src/main/scala/gitbucket/core/plugin/PluginRepository.scala @@ -2,7 +2,7 @@ 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 @@ 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 VersionDef( version: String, url: String, - range: String -) { - lazy val file = url.substring(url.lastIndexOf("/") + 1) -} + gitbucketVersion: String +) diff --git a/src/main/scala/gitbucket/core/servlet/InitializeListener.scala b/src/main/scala/gitbucket/core/servlet/InitializeListener.scala index 326608b..2818dbd 100644 --- a/src/main/scala/gitbucket/core/servlet/InitializeListener.scala +++ b/src/main/scala/gitbucket/core/servlet/InitializeListener.scala @@ -136,46 +136,46 @@ } 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 = { diff --git a/src/main/twirl/gitbucket/core/admin/plugins.scala.html b/src/main/twirl/gitbucket/core/admin/plugins.scala.html index db8f498..d97d893 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.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 @@

Plugins

@if(plugins.size > 0) { - @plugins.map { case (plugin, enabled) => + @plugins.map { case (plugin, enabled, updatableVersion) =>
@if(enabled){ -
- -
+ @if(updatableVersion.isEmpty){ +
+ +
+ } else { +
+ +
+ } } else {