mirror of
				https://github.com/gitbucket/gitbucket.git
				synced 2025-11-03 20:15:59 +01:00 
			
		
		
		
	Experiment of plugin installation from the remote repository
This commit is contained in:
		@@ -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")
 | 
			
		||||
 
 | 
			
		||||
@@ -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._
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -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 = {
 | 
			
		||||
 
 | 
			
		||||
@@ -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){
 | 
			
		||||
              @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">
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user