diff --git a/etc/deploy-assemby-jar.sh b/etc/deploy-assemby-jar.sh new file mode 100644 index 0000000..11f276d --- /dev/null +++ b/etc/deploy-assemby-jar.sh @@ -0,0 +1,9 @@ +#!/bin/sh +mvn deploy:deploy-file \ + -DgroupId=jp.sf.amateras\ + -DartifactId=gitbucket-assembly\ + -Dversion=0.0.1\ + -Dpackaging=jar\ + -Dfile=../target/scala-2.11/gitbucket-assembly-0.0.1.jar\ + -DrepositoryId=sourceforge.jp\ + -Durl=scp://shell.sourceforge.jp/home/groups/a/am/amateras/htdocs/mvn/ \ No newline at end of file diff --git a/etc/pom.xml b/etc/pom.xml new file mode 100644 index 0000000..40693f2 --- /dev/null +++ b/etc/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + jp.sf.amateras + gitbucket-assembly + 0.0.1 + + + + org.apache.maven.wagon + wagon-ssh + 1.0-beta-6 + + + + \ No newline at end of file diff --git a/project/build.scala b/project/build.scala index e767078..7d41538 100644 --- a/project/build.scala +++ b/project/build.scala @@ -4,6 +4,8 @@ import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys import play.twirl.sbt.SbtTwirl import play.twirl.sbt.Import.TwirlKeys._ +import sbtassembly._ +import sbtassembly.AssemblyKeys._ object MyBuild extends Build { val Organization = "jp.sf.amateras" @@ -18,6 +20,17 @@ ) .settings(ScalatraPlugin.scalatraWithJRebel: _*) .settings( + test in assembly := {}, + assemblyMergeStrategy in assembly := { + case PathList("META-INF", xs @ _*) => + (xs map {_.toLowerCase}) match { + case ("manifest.mf" :: Nil) => MergeStrategy.discard + case _ => MergeStrategy.discard + } + case x => MergeStrategy.first + } + ) + .settings( sourcesInBase := false, organization := Organization, name := Name, @@ -45,7 +58,7 @@ "com.typesafe.slick" %% "slick" % "2.1.0", "com.novell.ldap" % "jldap" % "2009-10-07", "com.h2database" % "h2" % "1.4.180", - "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime", +// "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime", "org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided", "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"), "junit" % "junit" % "4.11" % "test", diff --git a/project/plugins.sbt b/project/plugins.sbt index de95ab6..dee3cdf 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -7,3 +7,5 @@ addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.2") addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4") + +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0") \ No newline at end of file diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala index bdbd898..cfa37e6 100644 --- a/src/main/scala/ScalatraBootstrap.scala +++ b/src/main/scala/ScalatraBootstrap.scala @@ -1,5 +1,7 @@ import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter} import app._ +import plugin.PluginRegistry + //import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider import org.scalatra._ import javax.servlet._ @@ -15,6 +17,11 @@ // Register controllers context.mount(new AnonymousAccessController, "/*") + + PluginRegistry().getControllers.foreach { case (controller, path) => + context.mount(controller, path) + } + context.mount(new IndexController, "/") context.mount(new SearchController, "/") context.mount(new FileUploadController, "/upload") diff --git a/src/main/scala/plugin/Plugin.scala b/src/main/scala/plugin/Plugin.scala new file mode 100644 index 0000000..c176874 --- /dev/null +++ b/src/main/scala/plugin/Plugin.scala @@ -0,0 +1,30 @@ +package plugin + +import javax.servlet.ServletContext + +import util.Version + +/** + * Trait for define plugin interface. + * To provide plugin, put Plugin class which mixed in this trait into the package root. + */ +trait Plugin { + + val pluginId: String + val pluginName: String + val description: String + val versions: Seq[Version] + + /** + * This method is invoked in initialization of plugin system. + * Register plugin functionality to PluginRegistry. + */ + def initialize(registry: PluginRegistry): Unit + + /** + * This method is invoked in shutdown of plugin system. + * If the plugin has any resources, release them in this method. + */ + def shutdown(registry: PluginRegistry): Unit + +} diff --git a/src/main/scala/plugin/PluginRegistory.scala b/src/main/scala/plugin/PluginRegistory.scala new file mode 100644 index 0000000..bb7c2cb --- /dev/null +++ b/src/main/scala/plugin/PluginRegistory.scala @@ -0,0 +1,145 @@ +package plugin + +import java.io.{FilenameFilter, File} +import java.net.URLClassLoader +import javax.servlet.ServletContext +import javax.servlet.http.{HttpServletRequest, HttpServletResponse} + +import org.slf4j.LoggerFactory +import service.RepositoryService.RepositoryInfo +import util.Directory._ +import util.JDBCUtil._ +import util.{Version, Versions} + +import scala.collection.mutable.ListBuffer +import app.{ControllerBase, Context} + +class PluginRegistry { + + private val plugins = new ListBuffer[PluginInfo] + private val javaScripts = new ListBuffer[(String, String)] + private val controllers = new ListBuffer[(ControllerBase, String)] + + def addPlugin(pluginInfo: PluginInfo): Unit = { + plugins += pluginInfo + } + + def getPlugins(): List[PluginInfo] = plugins.toList + + def addController(controller: ControllerBase, path: String): Unit = { + controllers += ((controller, path)) + } + + def getControllers(): List[(ControllerBase, String)] = controllers.toList + + def addJavaScript(path: String, script: String): Unit = { + javaScripts += Tuple2(path, script) + } + + //def getJavaScripts(): List[(String, String)] = javaScripts.toList + + def getJavaScript(currentPath: String): Option[String] = { + javaScripts.find(x => currentPath.matches(x._1)).map(_._2) + } + + private case class GlobalAction( + method: String, + path: String, + function: (HttpServletRequest, HttpServletResponse, Context) => Any + ) + + private case class RepositoryAction( + method: String, + path: String, + function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any + ) + +} + +/** + * Provides entry point to PluginRegistry. + */ +object PluginRegistry { + + private val logger = LoggerFactory.getLogger(classOf[PluginRegistry]) + + private val instance = new PluginRegistry() + + /** + * Returns the PluginRegistry singleton instance. + */ + def apply(): PluginRegistry = instance + + /** + * Initializes all installed plugins. + */ + def initialize(context: ServletContext, conn: java.sql.Connection): Unit = { + val pluginDir = new File(PluginHome) + if(pluginDir.exists && pluginDir.isDirectory){ + pluginDir.listFiles(new FilenameFilter { + override def accept(dir: File, name: String): Boolean = name.endsWith(".jar") + }).foreach { pluginJar => + val classLoader = new URLClassLoader(Array(pluginJar.toURI.toURL), Thread.currentThread.getContextClassLoader) + try { + val plugin = classLoader.loadClass("Plugin").newInstance().asInstanceOf[Plugin] + + // Migration + val headVersion = plugin.versions.head + val currentVersion = conn.find("SELECT * FROM PLUGIN WHERE PLUGIN_ID = ?", plugin.pluginId)(_.getString("VERSION")) match { + case Some(x) => { + val dim = x.split("\\.") + Version(dim(0).toInt, dim(1).toInt) + } + case None => Version(0, 0) + } + + Versions.update(conn, headVersion, currentVersion, plugin.versions, new URLClassLoader(Array(pluginJar.toURI.toURL))){ conn => + currentVersion.versionString match { + case "0.0" => + conn.update("INSERT INTO PLUGIN (PLUGIN_ID, VERSION) VALUES (?, ?)", plugin.pluginId, headVersion.versionString) + case _ => + conn.update("UPDATE PLUGIN SET VERSION = ? WHERE PLUGIN_ID = ?", headVersion.versionString, plugin.pluginId) + } + } + + // Initialize + plugin.initialize(instance) + instance.addPlugin(PluginInfo( + pluginId = plugin.pluginId, + pluginName = plugin.pluginName, + version = plugin.versions.head.versionString, + description = plugin.description, + pluginClass = plugin + )) + + } catch { + case e: Exception => { + logger.error(s"Error during plugin initialization", e) + } + } + } + } + } + + def shutdown(context: ServletContext): Unit = { + instance.getPlugins().foreach { pluginInfo => + try { + pluginInfo.pluginClass.shutdown(instance) + } catch { + case e: Exception => { + logger.error(s"Error during plugin shutdown", e) + } + } + } + } + + +} + +case class PluginInfo( + pluginId: String, + pluginName: String, + version: String, + description: String, + pluginClass: Plugin +) \ No newline at end of file diff --git a/src/main/scala/plugin/Results.scala b/src/main/scala/plugin/Results.scala new file mode 100644 index 0000000..18fdb7f --- /dev/null +++ b/src/main/scala/plugin/Results.scala @@ -0,0 +1,11 @@ +package plugin + +import play.twirl.api.Html + +/** + * Defines result case classes returned by plugin controller. + */ +object Results { + case class Redirect(path: String) + case class Fragment(html: Html) +} diff --git a/src/main/scala/plugin/Sessions.scala b/src/main/scala/plugin/Sessions.scala new file mode 100644 index 0000000..7398c9a --- /dev/null +++ b/src/main/scala/plugin/Sessions.scala @@ -0,0 +1,11 @@ +package plugin + +import slick.jdbc.JdbcBackend.Session + +/** + * Provides Slick Session to Plug-ins. + */ +object Sessions { + val sessions = new ThreadLocal[Session] + implicit def session: Session = sessions.get() +} diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala deleted file mode 100644 index 30610bc..0000000 --- a/src/main/scala/servlet/AutoUpdateListener.scala +++ /dev/null @@ -1,245 +0,0 @@ -package servlet - -import java.io.File -import java.sql.{DriverManager, Connection} -import org.apache.commons.io.FileUtils -import javax.servlet.{ServletContextListener, ServletContextEvent} -import org.apache.commons.io.IOUtils -import org.slf4j.LoggerFactory -import util.Directory._ -import util.ControlUtil._ -import util.JDBCUtil._ -import org.eclipse.jgit.api.Git -import util.{DatabaseConfig, Directory} - -object AutoUpdate { - - /** - * Version of GitBucket - * - * @param majorVersion the major version - * @param minorVersion the minor version - */ - case class Version(majorVersion: Int, minorVersion: Int){ - - private val logger = LoggerFactory.getLogger(classOf[servlet.AutoUpdate.Version]) - - /** - * Execute update/MAJOR_MINOR.sql to update schema to this version. - * If corresponding SQL file does not exist, this method do nothing. - */ - def update(conn: Connection): Unit = { - val sqlPath = s"update/${majorVersion}_${minorVersion}.sql" - - using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in => - if(in != null){ - val sql = IOUtils.toString(in, "UTF-8") - using(conn.createStatement()){ stmt => - logger.debug(sqlPath + "=" + sql) - stmt.executeUpdate(sql) - } - } - } - } - - /** - * MAJOR.MINOR - */ - val versionString = s"${majorVersion}.${minorVersion}" - } - - /** - * The history of versions. A head of this sequence is the current BitBucket version. - */ - val versions = Seq( - new Version(2, 8), - new Version(2, 7) { - override def update(conn: Connection): Unit = { - super.update(conn) - conn.select("SELECT * FROM REPOSITORY"){ rs => - // Rename attached files directory from /issues to /comments - val userName = rs.getString("USER_NAME") - val repoName = rs.getString("REPOSITORY_NAME") - defining(Directory.getAttachedDir(userName, repoName)){ newDir => - val oldDir = new File(newDir.getParentFile, "issues") - if(oldDir.exists && oldDir.isDirectory){ - oldDir.renameTo(newDir) - } - } - // Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist - val originalUserName = rs.getString("ORIGIN_USER_NAME") - val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME") - if(originalUserName != null && originalRepoName != null){ - if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", - originalUserName, originalRepoName) == 0){ - conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " + - "WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName) - } - } - // Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist - val parentUserName = rs.getString("PARENT_USER_NAME") - val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME") - if(parentUserName != null && parentRepoName != null){ - if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", - parentUserName, parentRepoName) == 0){ - conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " + - "WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName) - } - } - } - } - }, - new Version(2, 6), - new Version(2, 5), - new Version(2, 4), - new Version(2, 3) { - override def update(conn: Connection): Unit = { - super.update(conn) - conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs => - val curInfo = rs.getString("ADDITIONAL_INFO") - val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n") - if (curInfo != newInfo) { - conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID")) - } - } - FileUtils.deleteDirectory(Directory.getPluginCacheDir()) - FileUtils.deleteDirectory(new File(Directory.PluginHome)) - } - }, - new Version(2, 2), - new Version(2, 1), - new Version(2, 0){ - override def update(conn: Connection): Unit = { - import eu.medsea.mimeutil.{MimeUtil2, MimeType} - - val mimeUtil = new MimeUtil2() - mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector") - - super.update(conn) - conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs => - defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir => - if(dir.exists && dir.isDirectory){ - dir.listFiles.foreach { file => - if(file.getName.indexOf('.') < 0){ - val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString - if(mimeType.startsWith("image/")){ - file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1))) - } - } - } - } - } - } - } - }, - Version(1, 13), - Version(1, 12), - Version(1, 11), - Version(1, 10), - Version(1, 9), - Version(1, 8), - Version(1, 7), - Version(1, 6), - Version(1, 5), - Version(1, 4), - new Version(1, 3){ - override def update(conn: Connection): Unit = { - super.update(conn) - // Fix wiki repository configuration - conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs => - using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git => - defining(git.getRepository.getConfig){ config => - if(!config.getBoolean("http", "receivepack", false)){ - config.setBoolean("http", null, "receivepack", true) - config.save - } - } - } - } - } - }, - Version(1, 2), - Version(1, 1), - Version(1, 0), - Version(0, 0) - ) - - /** - * The head version of BitBucket. - */ - val headVersion = versions.head - - /** - * The version file (GITBUCKET_HOME/version). - */ - lazy val versionFile = new File(GitBucketHome, "version") - - /** - * Returns the current version from the version file. - */ - def getCurrentVersion(): Version = { - if(versionFile.exists){ - FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match { - case Array(majorVersion, minorVersion) => { - versions.find { v => - v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt - }.getOrElse(Version(0, 0)) - } - case _ => Version(0, 0) - } - } else Version(0, 0) - } - -} - -/** - * Update database schema automatically in the context initializing. - */ -class AutoUpdateListener extends ServletContextListener { - import AutoUpdate._ - - private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener]) -// private val scheduler = StdSchedulerFactory.getDefaultScheduler - - override def contextInitialized(event: ServletContextEvent): Unit = { - val dataDir = event.getServletContext.getInitParameter("gitbucket.home") - if(dataDir != null){ - System.setProperty("gitbucket.home", dataDir) - } - org.h2.Driver.load() - - defining(getConnection()){ conn => - logger.debug("Start schema update") - try { - defining(getCurrentVersion()){ currentVersion => - if(currentVersion == headVersion){ - logger.debug("No update") - } else if(!versions.contains(currentVersion)){ - logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.") - } else { - versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn)) - FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8") - logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}") - } - } - } catch { - case ex: Throwable => { - logger.error("Failed to schema update", ex) - ex.printStackTrace() - conn.rollback() - } - } - logger.debug("End schema update") - } - } - - def contextDestroyed(sce: ServletContextEvent): Unit = { - } - - private def getConnection(): Connection = - DriverManager.getConnection( - DatabaseConfig.url, - DatabaseConfig.user, - DatabaseConfig.password) - -} diff --git a/src/main/scala/servlet/InitializeListener.scala b/src/main/scala/servlet/InitializeListener.scala new file mode 100644 index 0000000..fdc3c48 --- /dev/null +++ b/src/main/scala/servlet/InitializeListener.scala @@ -0,0 +1,204 @@ +package servlet + +import java.io.File +import java.sql.{DriverManager, Connection} +import org.apache.commons.io.FileUtils +import javax.servlet.{ServletContextListener, ServletContextEvent} +import org.slf4j.LoggerFactory +import util.Directory._ +import util.ControlUtil._ +import util.JDBCUtil._ +import org.eclipse.jgit.api.Git +import util.{Version, Versions} +import plugin._ +import util.{DatabaseConfig, Directory} + +object AutoUpdate { + + /** + * The history of versions. A head of this sequence is the current BitBucket version. + */ + val versions = Seq( + new Version(2, 8), + new Version(2, 7) { + override def update(conn: Connection, cl: ClassLoader): Unit = { + super.update(conn, cl) + conn.select("SELECT * FROM REPOSITORY"){ rs => + // Rename attached files directory from /issues to /comments + val userName = rs.getString("USER_NAME") + val repoName = rs.getString("REPOSITORY_NAME") + defining(Directory.getAttachedDir(userName, repoName)){ newDir => + val oldDir = new File(newDir.getParentFile, "issues") + if(oldDir.exists && oldDir.isDirectory){ + oldDir.renameTo(newDir) + } + } + // Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist + val originalUserName = rs.getString("ORIGIN_USER_NAME") + val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME") + if(originalUserName != null && originalRepoName != null){ + if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", + originalUserName, originalRepoName) == 0){ + conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " + + "WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName) + } + } + // Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist + val parentUserName = rs.getString("PARENT_USER_NAME") + val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME") + if(parentUserName != null && parentRepoName != null){ + if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", + parentUserName, parentRepoName) == 0){ + conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " + + "WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName) + } + } + } + } + }, + new Version(2, 6), + new Version(2, 5), + new Version(2, 4), + new Version(2, 3) { + override def update(conn: Connection, cl: ClassLoader): Unit = { + super.update(conn, cl) + conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs => + val curInfo = rs.getString("ADDITIONAL_INFO") + val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n") + if (curInfo != newInfo) { + conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID")) + } + } + ignore { + FileUtils.deleteDirectory(Directory.getPluginCacheDir()) + //FileUtils.deleteDirectory(new File(Directory.PluginHome)) + } + } + }, + new Version(2, 2), + new Version(2, 1), + new Version(2, 0){ + override def update(conn: Connection, cl: ClassLoader): Unit = { + import eu.medsea.mimeutil.{MimeUtil2, MimeType} + + val mimeUtil = new MimeUtil2() + mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector") + + super.update(conn, cl) + conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs => + defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir => + if(dir.exists && dir.isDirectory){ + dir.listFiles.foreach { file => + if(file.getName.indexOf('.') < 0){ + val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString + if(mimeType.startsWith("image/")){ + file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1))) + } + } + } + } + } + } + } + }, + Version(1, 13), + Version(1, 12), + Version(1, 11), + Version(1, 10), + Version(1, 9), + Version(1, 8), + Version(1, 7), + Version(1, 6), + Version(1, 5), + Version(1, 4), + new Version(1, 3){ + override def update(conn: Connection, cl: ClassLoader): Unit = { + super.update(conn, cl) + // Fix wiki repository configuration + conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs => + using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git => + defining(git.getRepository.getConfig){ config => + if(!config.getBoolean("http", "receivepack", false)){ + config.setBoolean("http", null, "receivepack", true) + config.save + } + } + } + } + } + }, + Version(1, 2), + Version(1, 1), + Version(1, 0), + Version(0, 0) + ) + + /** + * The head version of BitBucket. + */ + val headVersion = versions.head + + /** + * The version file (GITBUCKET_HOME/version). + */ + lazy val versionFile = new File(GitBucketHome, "version") + + /** + * Returns the current version from the version file. + */ + def getCurrentVersion(): Version = { + if(versionFile.exists){ + FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match { + case Array(majorVersion, minorVersion) => { + versions.find { v => + v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt + }.getOrElse(Version(0, 0)) + } + case _ => Version(0, 0) + } + } else Version(0, 0) + } + +} + +/** + * Initialize GitBucket system. + * Update database schema and load plug-ins automatically in the context initializing. + */ +class InitializeListener extends ServletContextListener { + import AutoUpdate._ + + private val logger = LoggerFactory.getLogger(classOf[InitializeListener]) + + override def contextInitialized(event: ServletContextEvent): Unit = { + val dataDir = event.getServletContext.getInitParameter("gitbucket.home") + if(dataDir != null){ + System.setProperty("gitbucket.home", dataDir) + } + org.h2.Driver.load() + + defining(getConnection()){ conn => + // Migration + logger.debug("Start schema update") + Versions.update(conn, headVersion, getCurrentVersion(), versions, Thread.currentThread.getContextClassLoader){ conn => + FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8") + } + // Load plugins + logger.debug("Initialize plugins") + PluginRegistry.initialize(event.getServletContext, conn) + } + + } + + def contextDestroyed(event: ServletContextEvent): Unit = { + // Shutdown plugins + PluginRegistry.shutdown(event.getServletContext) + } + + private def getConnection(): Connection = + DriverManager.getConnection( + DatabaseConfig.url, + DatabaseConfig.user, + DatabaseConfig.password) + +} diff --git a/src/main/scala/util/ControlUtil.scala b/src/main/scala/util/ControlUtil.scala index c231fb0..7945f32 100644 --- a/src/main/scala/util/ControlUtil.scala +++ b/src/main/scala/util/ControlUtil.scala @@ -37,4 +37,10 @@ def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T = try f(treeWalk) finally treeWalk.release() + def ignore[T](f: => Unit): Unit = try { + f + } catch { + case e: Exception => () + } + } diff --git a/src/main/scala/util/JDBCUtil.scala b/src/main/scala/util/JDBCUtil.scala index 375ea14..5d880d0 100644 --- a/src/main/scala/util/JDBCUtil.scala +++ b/src/main/scala/util/JDBCUtil.scala @@ -18,6 +18,14 @@ } } + def find[T](sql: String, params: Any*)(f: ResultSet => T): Option[T] = { + execute(sql, params: _*){ stmt => + using(stmt.executeQuery()){ rs => + if(rs.next) Some(f(rs)) else None + } + } + } + def select[T](sql: String, params: Any*)(f: ResultSet => T): Seq[T] = { execute(sql, params: _*){ stmt => using(stmt.executeQuery()){ rs => diff --git a/src/main/scala/util/Version.scala b/src/main/scala/util/Version.scala new file mode 100644 index 0000000..fed74a8 --- /dev/null +++ b/src/main/scala/util/Version.scala @@ -0,0 +1,67 @@ +package util + +import java.sql.Connection + +import org.apache.commons.io.IOUtils +import org.slf4j.LoggerFactory +import util.ControlUtil._ + +case class Version(majorVersion: Int, minorVersion: Int) { + + private val logger = LoggerFactory.getLogger(classOf[Version]) + + /** + * Execute update/MAJOR_MINOR.sql to update schema to this version. + * If corresponding SQL file does not exist, this method do nothing. + */ + def update(conn: Connection, cl: ClassLoader): Unit = { + val sqlPath = s"update/${majorVersion}_${minorVersion}.sql" + + using(cl.getResourceAsStream(sqlPath)){ in => + if(in != null){ + val sql = IOUtils.toString(in, "UTF-8") + using(conn.createStatement()){ stmt => + logger.debug(sqlPath + "=" + sql) + stmt.executeUpdate(sql) + } + } + } + } + + + /** + * MAJOR.MINOR + */ + val versionString = s"${majorVersion}.${minorVersion}" + +} + +object Versions { + + private val logger = LoggerFactory.getLogger(Versions.getClass) + + def update(conn: Connection, headVersion: Version, currentVersion: Version, versions: Seq[Version], cl: ClassLoader) + (save: Connection => Unit): Unit = { + logger.debug("Start schema update") + try { + if(currentVersion == headVersion){ + logger.debug("No update") + } else if(currentVersion.versionString != "0.0" && !versions.contains(currentVersion)){ + logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.") + } else { + versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn, cl)) + save(conn) + logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}") + } + } catch { + case ex: Throwable => { + logger.error("Failed to schema update", ex) + ex.printStackTrace() + conn.rollback() + } + } + logger.debug("End schema update") + } + +} + diff --git a/src/main/twirl/main.scala.html b/src/main/twirl/main.scala.html index 9b8ae02..6d35ff1 100644 --- a/src/main/twirl/main.scala.html +++ b/src/main/twirl/main.scala.html @@ -82,5 +82,10 @@ }); }); + @plugin.PluginRegistry().getJavaScript(request.getRequestURI).map { script => + + } diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 51513a1..c015d24 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -12,10 +12,10 @@ - + - servlet.AutoUpdateListener + servlet.InitializeListener