(refs #32)Add plugin install tab

This commit is contained in:
Naoki Takezoe
2014-06-15 13:11:08 +09:00
parent 3b2e42fd61
commit 4af4c4e7c6
6 changed files with 133 additions and 13 deletions

View File

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

View File

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

View File

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

View File

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

View 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>

View File

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