mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-07 05:55:51 +01:00
(refs #32)Add plugin install tab
This commit is contained in:
@@ -4,10 +4,13 @@ import service.{AccountService, SystemSettingsService}
|
|||||||
import SystemSettingsService._
|
import SystemSettingsService._
|
||||||
import util.AdminAuthenticator
|
import util.AdminAuthenticator
|
||||||
import util.Directory._
|
import util.Directory._
|
||||||
|
import util.ControlUtil._
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
import ssh.SshServer
|
import ssh.SshServer
|
||||||
import org.scalatra.Ok
|
import org.scalatra.Ok
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import plugin.PluginSystem
|
||||||
|
|
||||||
class SystemSettingsController extends SystemSettingsControllerBase
|
class SystemSettingsController extends SystemSettingsControllerBase
|
||||||
with AccountService with AdminAuthenticator
|
with AccountService with AdminAuthenticator
|
||||||
@@ -89,11 +92,28 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
|||||||
val dir = new java.io.File(PluginHome, pluginId)
|
val dir = new java.io.File(PluginHome, pluginId)
|
||||||
if(dir.exists && dir.isDirectory){
|
if(dir.exists && dir.isDirectory){
|
||||||
FileUtils.deleteQuietly(dir)
|
FileUtils.deleteQuietly(dir)
|
||||||
|
PluginSystem.uninstall(pluginId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
redirect("/admin/plugins")
|
redirect("/admin/plugins")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
get("/admin/plugins/available")(adminOnly {
|
||||||
|
admin.plugins.html.available(getAvailablePlugins())
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/admin/plugins/_install", pluginForm)(adminOnly { form =>
|
||||||
|
val dir = getPluginCacheDir()
|
||||||
|
getAvailablePlugins().filter(x => form.pluginIds.contains(x.id)).foreach { plugin =>
|
||||||
|
val pluginDir = new java.io.File(PluginHome, plugin.id)
|
||||||
|
if(!pluginDir.exists){
|
||||||
|
FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir)
|
||||||
|
}
|
||||||
|
PluginSystem.installPlugin(plugin.id)
|
||||||
|
}
|
||||||
|
redirect("/admin/plugins")
|
||||||
|
})
|
||||||
|
|
||||||
get("/admin/plugins/console")(adminOnly {
|
get("/admin/plugins/console")(adminOnly {
|
||||||
admin.plugins.html.console()
|
admin.plugins.html.console()
|
||||||
})
|
})
|
||||||
@@ -103,4 +123,35 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
|||||||
val result = plugin.JavaScriptPlugin.evaluateJavaScript(script)
|
val result = plugin.JavaScriptPlugin.evaluateJavaScript(script)
|
||||||
Ok(result)
|
Ok(result)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO Move to PluginSystem or Service?
|
||||||
|
private def getAvailablePlugins(): List[SystemSettingsControllerBase.AvailablePlugin] = {
|
||||||
|
val dir = getPluginCacheDir()
|
||||||
|
if(dir.exists && dir.isDirectory){
|
||||||
|
PluginSystem.repositories.flatMap { repo =>
|
||||||
|
val repoDir = new java.io.File(dir, repo.id)
|
||||||
|
if(repoDir.exists && repoDir.isDirectory){
|
||||||
|
repoDir.listFiles.filter(d => d.isDirectory && !d.getName.startsWith(".")).map { plugin =>
|
||||||
|
val propertyFile = new java.io.File(plugin, "plugin.properties")
|
||||||
|
val properties = new java.util.Properties()
|
||||||
|
if(propertyFile.exists && propertyFile.isFile){
|
||||||
|
using(new FileInputStream(propertyFile)){ in =>
|
||||||
|
properties.load(in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SystemSettingsControllerBase.AvailablePlugin(
|
||||||
|
repo.id,
|
||||||
|
properties.getProperty("id"),
|
||||||
|
properties.getProperty("author"),
|
||||||
|
properties.getProperty("url"),
|
||||||
|
properties.getProperty("description"))
|
||||||
|
}
|
||||||
|
} else Nil
|
||||||
|
}
|
||||||
|
} else Nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object SystemSettingsControllerBase {
|
||||||
|
case class AvailablePlugin(repository: String, id: String, author: String, url: String, description: String)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,12 +67,15 @@ object JavaScriptPlugin {
|
|||||||
|
|
||||||
def define(id: String, author: String, url: String, description: String) = new JavaScriptPlugin(id, author, url, description)
|
def define(id: String, author: String, url: String, description: String) = new JavaScriptPlugin(id, author, url, description)
|
||||||
|
|
||||||
def evaluateJavaScript(script: String): Any = {
|
def evaluateJavaScript(script: String, vars: Map[String, Any] = Map.empty): Any = {
|
||||||
val context = JsContext.enter()
|
val context = JsContext.enter()
|
||||||
try {
|
try {
|
||||||
val scope = context.initStandardObjects()
|
val scope = context.initStandardObjects()
|
||||||
scope.put("PluginSystem", scope, PluginSystem)
|
scope.put("PluginSystem", scope, PluginSystem)
|
||||||
scope.put("JavaScriptPlugin", scope, this)
|
scope.put("JavaScriptPlugin", scope, this)
|
||||||
|
vars.foreach { case (key, value) =>
|
||||||
|
scope.put(key, scope, value)
|
||||||
|
}
|
||||||
val result = context.evaluateString(scope, script, "<cmd>", 1, null)
|
val result = context.evaluateString(scope, script, "<cmd>", 1, null)
|
||||||
result
|
result
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
|||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import util.Directory._
|
import util.Directory._
|
||||||
|
import util.ControlUtil._
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,6 +17,7 @@ object PluginSystem {
|
|||||||
|
|
||||||
private val initialized = new AtomicBoolean(false)
|
private val initialized = new AtomicBoolean(false)
|
||||||
private val pluginsMap = scala.collection.mutable.Map[String, Plugin]()
|
private val pluginsMap = scala.collection.mutable.Map[String, Plugin]()
|
||||||
|
private val repositoriesList = scala.collection.mutable.ListBuffer[PluginRepository]()
|
||||||
|
|
||||||
def install(plugin: Plugin): Unit = {
|
def install(plugin: Plugin): Unit = {
|
||||||
pluginsMap.put(plugin.id, plugin)
|
pluginsMap.put(plugin.id, plugin)
|
||||||
@@ -27,24 +29,45 @@ object PluginSystem {
|
|||||||
pluginsMap.remove(id)
|
pluginsMap.remove(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def repositories: List[PluginRepository] = repositoriesList.toList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the plugin system. Load scripts from GITBUCKET_HOME/plugins.
|
* Initializes the plugin system. Load scripts from GITBUCKET_HOME/plugins.
|
||||||
*/
|
*/
|
||||||
def init(): Unit = {
|
def init(): Unit = {
|
||||||
if(initialized.compareAndSet(false, true)){
|
if(initialized.compareAndSet(false, true)){
|
||||||
|
// Load installed plugins
|
||||||
val pluginDir = new java.io.File(PluginHome)
|
val pluginDir = new java.io.File(PluginHome)
|
||||||
if(pluginDir.exists && pluginDir.isDirectory){
|
if(pluginDir.exists && pluginDir.isDirectory){
|
||||||
pluginDir.listFiles.filter(f => f.isDirectory && !f.getName.startsWith(".")).foreach { dir =>
|
pluginDir.listFiles.filter(f => f.isDirectory && !f.getName.startsWith(".")).foreach { dir =>
|
||||||
val file = new java.io.File(dir, "plugin.js")
|
installPlugin(dir.getName)
|
||||||
if(file.exists && file.isFile){
|
}
|
||||||
val script = FileUtils.readFileToString(file, "UTF-8")
|
}
|
||||||
|
// Add default plugin repositories
|
||||||
|
repositoriesList += PluginRepository("central", "https://github.com/takezoe/gitbucket_plugins.git")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def installPlugin(id: String): Unit = {
|
||||||
|
val pluginDir = new java.io.File(PluginHome)
|
||||||
|
val javaScriptFile = new java.io.File(pluginDir, id + "/plugin.js")
|
||||||
|
|
||||||
|
if(javaScriptFile.exists && javaScriptFile.isFile){
|
||||||
|
val properties = new java.util.Properties()
|
||||||
|
using(new java.io.FileInputStream(new java.io.File(pluginDir, id + "/plugin.properties"))){ in =>
|
||||||
|
properties.load(in)
|
||||||
|
}
|
||||||
|
|
||||||
|
val script = FileUtils.readFileToString(javaScriptFile, "UTF-8")
|
||||||
try {
|
try {
|
||||||
JavaScriptPlugin.evaluateJavaScript(script)
|
JavaScriptPlugin.evaluateJavaScript(script, Map(
|
||||||
|
"id" -> properties.getProperty("id"),
|
||||||
|
"author" -> properties.getProperty("author"),
|
||||||
|
"url" -> properties.getProperty("url"),
|
||||||
|
"description" -> properties.getProperty("description")
|
||||||
|
))
|
||||||
} catch {
|
} catch {
|
||||||
case e: Exception => logger.warn(s"Error in plugin loading for ${file.getAbsolutePath}", e)
|
case e: Exception => logger.warn(s"Error in plugin loading for ${javaScriptFile.getAbsolutePath}", e)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,6 +78,7 @@ object PluginSystem {
|
|||||||
def globalActions : List[Action] = pluginsMap.values.flatMap(_.globalActions).toList
|
def globalActions : List[Action] = pluginsMap.values.flatMap(_.globalActions).toList
|
||||||
|
|
||||||
// Case classes to hold plug-ins information internally in GitBucket
|
// Case classes to hold plug-ins information internally in GitBucket
|
||||||
|
case class PluginRepository(id: String, url: String)
|
||||||
case class GlobalMenu(label: String, url: String, icon: String, condition: Context => Boolean)
|
case class GlobalMenu(label: String, url: String, icon: String, condition: Context => Boolean)
|
||||||
case class RepositoryMenu(label: String, name: String, url: String, icon: String, condition: Context => Boolean)
|
case class RepositoryMenu(label: String, name: String, url: String, icon: String, condition: Context => Boolean)
|
||||||
case class Action(path: String, function: (HttpServletRequest, HttpServletResponse) => Any)
|
case class Action(path: String, function: (HttpServletRequest, HttpServletResponse) => Any)
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ object Directory {
|
|||||||
|
|
||||||
val PluginHome = s"${GitBucketHome}/plugins"
|
val PluginHome = s"${GitBucketHome}/plugins"
|
||||||
|
|
||||||
|
val TemporaryHome = s"${GitBucketHome}/tmp"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Substance directory of the repository.
|
* Substance directory of the repository.
|
||||||
*/
|
*/
|
||||||
@@ -57,13 +59,18 @@ object Directory {
|
|||||||
* Root of temporary directories for the upload file.
|
* Root of temporary directories for the upload file.
|
||||||
*/
|
*/
|
||||||
def getTemporaryDir(sessionId: String): File =
|
def getTemporaryDir(sessionId: String): File =
|
||||||
new File(s"${GitBucketHome}/tmp/_upload/${sessionId}")
|
new File(s"${TemporaryHome}/_upload/${sessionId}")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Root of temporary directories for the specified repository.
|
* Root of temporary directories for the specified repository.
|
||||||
*/
|
*/
|
||||||
def getTemporaryDir(owner: String, repository: String): File =
|
def getTemporaryDir(owner: String, repository: String): File =
|
||||||
new File(s"${GitBucketHome}/tmp/${owner}/${repository}")
|
new File(s"${TemporaryHome}/${owner}/${repository}")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root of plugin cache directory. Plugin repositories are cloned into this directory.
|
||||||
|
*/
|
||||||
|
def getPluginCacheDir(): File = new File(s"${TemporaryHome}/_plugins")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temporary directory which is used to create an archive to download repository contents.
|
* Temporary directory which is used to create an archive to download repository contents.
|
||||||
|
|||||||
35
src/main/twirl/admin/plugins/available.scala.html
Normal file
35
src/main/twirl/admin/plugins/available.scala.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
@(plugins: List[app.SystemSettingsControllerBase.AvailablePlugin])(implicit context: app.Context)
|
||||||
|
@import context._
|
||||||
|
@import view.helpers._
|
||||||
|
@html.main("Plugins"){
|
||||||
|
@admin.html.menu("plugins"){
|
||||||
|
@tab("available")
|
||||||
|
<form action="@path/admin/plugins/_install" method="POST" validate="true">
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Provider</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
@plugins.zipWithIndex.map { case (plugin, i) =>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" name="pluginId[@i]" value="@plugin.id"/>
|
||||||
|
@plugin.id
|
||||||
|
</td>
|
||||||
|
<td><a href="@plugin.url">@plugin.author</a></td>
|
||||||
|
<td>@plugin.description</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
<input type="submit" id="install-plugins" class="btn btn-primary" value="Install selected plugins"/>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<script>
|
||||||
|
$(function(){
|
||||||
|
$('#install-plugins').click(function(){
|
||||||
|
return confirm('Selected plugin will be installed. Are you sure?');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
@import context._
|
@import context._
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
<li@if(active == "installed"){ class="active"}><a href="@path/admin/plugins">Installed plugins</a></li>
|
<li@if(active == "installed"){ class="active"}><a href="@path/admin/plugins">Installed plugins</a></li>
|
||||||
<li@if(active == "available"){ class="active"}><a href="@path/admin/plugins">Available plugins</a></li>
|
<li@if(active == "available"){ class="active"}><a href="@path/admin/plugins/available">Available plugins</a></li>
|
||||||
<li@if(active == "console" ){ class="active"}><a href="@path/admin/plugins/console">JavaScript console</a></li>
|
<li@if(active == "console" ){ class="active"}><a href="@path/admin/plugins/console">JavaScript console</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
Reference in New Issue
Block a user