├── .github └── workflows │ └── build.yml ├── .gitignore ├── README.md ├── build.sbt ├── maven-repository-settings.png ├── project ├── build.properties └── plugins.sbt └── src └── main ├── resources └── update │ └── gitbucket-maven-repository_1.1.0.xml ├── scala ├── Plugin.scala └── io │ └── github │ └── gitbucket │ └── mavenrepository │ ├── command │ ├── AbstractCommand.scala │ ├── LsCommand.scala │ └── MkdirCommand.scala │ ├── controller │ └── MavenRepositoryController.scala │ ├── model │ ├── Registry.scala │ └── RegistryProfile.scala │ ├── package.scala │ └── service │ └── MavenRepositoryService.scala └── twirl └── gitbucket └── mavenrepository ├── files.scala.html ├── form.scala.html └── settings.scala.html /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | java: [8, 11] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Cache 14 | uses: actions/cache@v2 15 | env: 16 | cache-name: cache-sbt-libs 17 | with: 18 | path: | 19 | ~/.ivy2/cache 20 | ~/.sbt 21 | ~/.coursier 22 | key: build-${{ env.cache-name }}-${{ hashFiles('build.sbt') }} 23 | - name: Set up JDK 24 | uses: actions/setup-java@v1 25 | with: 26 | java-version: ${{ matrix.java }} 27 | - name: Run tests 28 | run: | 29 | git clone https://github.com/gitbucket/gitbucket.git 30 | cd gitbucket 31 | sbt publishLocal 32 | cd ../ 33 | sbt test 34 | - name: Assembly 35 | run: sbt assembly 36 | - name: Upload artifacts 37 | uses: actions/upload-artifact@v2 38 | with: 39 | name: gitbucket-gist-plugin-java${{ matrix.java }}-${{ github.sha }} 40 | path: ./target/scala-2.13/*.jar 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | dist/* 6 | target/ 7 | lib_managed/ 8 | src_managed/ 9 | project/boot/ 10 | project/plugins/project/ 11 | 12 | # Scala-IDE specific 13 | .scala_dependencies 14 | .classpath 15 | .project 16 | .cache 17 | .settings 18 | 19 | # IntelliJ specific 20 | .idea/ 21 | .idea_modules/ 22 | 23 | # Ensime 24 | .ensime 25 | .ensime_cache/ 26 | 27 | # Metals 28 | .bloop/ 29 | .metals/ 30 | .vscode/ 31 | **/metals.sbt 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gitbucket-maven-repository-plugin [![build](https://github.com/takezoe/gitbucket-maven-repository-plugin/workflows/build/badge.svg?branch=master)](https://github.com/takezoe/gitbucket-maven-repository-plugin/actions?query=workflow%3Abuild+branch%3Amaster) 2 | ======== 3 | A GitBucket plugin that provides Maven repository hosting on GitBucket. 4 | 5 | ## Features 6 | 7 | In default, following Maven repositories become available by installing this plugin to GitBucket. 8 | 9 | - `http(s)://GITBUCKET_HOST/maven/releases` 10 | - `http(s)://GITBUCKET_HOST/maven/snapshots` 11 | 12 | You can deploy artifacts to these repositories via WebDAV with your GitBucket account. 13 | 14 | Also you can deploy via SSH (SCP) with public key authentication using keys registered in GitBucket. In this case, use following configurations to connect via SSH: 15 | 16 | - Host: Hostname of GitBucket 17 | - Port: SSH port configured in GitBucket system settings 18 | - Path: `/maven/releases` or `/maven/snapshots` 19 | 20 | It's possible to add more repositories and configure them at the administration console: 21 | 22 | ![Maven repository settings](maven-repository-settings.png) 23 | 24 | You can specify whether artifacts are overwritable for each repository. In addition, it's possible to make repository private. Private repositories require basic authentication by GitBucket account to access. 25 | 26 | ## Compatibility 27 | 28 | Plugin version | GitBucket version 29 | :--------------|:-------------------- 30 | 1.8.x | 4.37.1 - 31 | 1.7.x | 4.36.x - 32 | 1.6.x | 4.35.x - 33 | 1.5.x | 4.32.x - 34 | 1.4.x | 4.30.x - 35 | 1.3.x - | 4.23.x - 36 | 1.1.x - 1.2.x | 4.21.x - 37 | 1.0.x | 4.19.x - 38 | 39 | ## Installation 40 | 41 | Download jar file from [the release page](https://github.com/takezoe/gitbucket-maven-repository-plugin/releases) and put into `GITBUCKET_HOME/plugins`. 42 | 43 | ## Build 44 | 45 | Run `sbt assembly` and copy generated `/target/scala-2.13/gitbucket-maven-repository-plugin-x.x.x.jar` to `~/.gitbucket/plugins/` (If the directory does not exist, create it by hand before copying the jar), or just run `sbt install`. 46 | 47 | ## Configuration 48 | 49 | ### sbt 50 | 51 | Resolvers: 52 | 53 | ```scala 54 | resolvers ++= Seq( 55 | "GitBucket Snapshots Repository" at "http://localhost:8080/maven/snapshots", 56 | "GitBucket Releases Repository" at "http://localhost:8080/maven/releases" 57 | ) 58 | 59 | // If repository is private, you have to add authentication information 60 | credentials += Credentials("GitBucket Maven Repository", "localhost", "username", "password") 61 | ``` 62 | 63 | Publish via WebDAV: 64 | 65 | ```scala 66 | publishTo := { 67 | val base = "http://localhost:8080/maven/" 68 | if (version.value.endsWith("SNAPSHOT")) Some("snapshots" at base + "snapshots") 69 | else Some("releases" at base + "releases") 70 | } 71 | 72 | credentials += Credentials("GitBucket Maven Repository", "localhost", "username", "password") 73 | ``` 74 | 75 | Publish via SSH: 76 | 77 | ```scala 78 | publishTo := { 79 | val repoInfo = 80 | if (version.value.endsWith("SNAPSHOT")) ("snapshots" -> "/maven/snapshots") 81 | else ("releases" -> "/maven/releases") 82 | Some(Resolver.ssh(repoInfo._1, "localhost", 29418, repoInfo._2) 83 | as(System.getProperty("user.name"), (Path.userHome / ".ssh" / "id_rsa").asFile)) 84 | } 85 | ``` 86 | 87 | ### Maven 88 | 89 | Add distribution settings to your `pom.xml`: 90 | 91 | ```xml 92 | 93 | ... 94 | 95 | 96 | gitbucket-maven-repository-releases 97 | http://localhost:8080/maven/releases 98 | 99 | 100 | gitbucket-maven-repository-snapshots 101 | http://localhost:8080/maven/snapshots 102 | 103 | 104 | ... 105 | 106 | ``` 107 | 108 | Also you need to add authentication settings in `~/.m2/settings.xml` (replace username and password with your GitBucket account's one): 109 | 110 | ```xml 111 | 112 | ... 113 | 114 | 115 | gitbucket-maven-repository-releases 116 | root 117 | root 118 | 119 | 120 | gitbucket-maven-repository-snapshots 121 | root 122 | root 123 | 124 | 125 | ... 126 | 127 | ``` 128 | 129 | ### Gradle 130 | 131 | To publish your artifacts to a Maven repo hosted by **this** GitBucket plug-in, only the urls 132 | in your `uploadArchives.repositories` section of your `build.gradle` need to be changed (using your installation's URLs): 133 | ```groovy 134 | // ... 135 | def mvnUser = hasProperty("mvnUser") ? mvnUser : "no_user" 136 | def mvnPassword = hasProperty("mvnPassword") ? mvnPassword : "no_pwd" 137 | // ... 138 | uploadArchives { 139 | repositories { 140 | mavenDeployer { 141 | repository(url:"http://localhost:8080/maven/releases") { 142 | authentication(userName: mvnUser, password: mvnPassword) 143 | } 144 | snapshotRepository(url: "http://localhost:8080/maven/snapshots") { 145 | authentication(userName: mvnUser, password: mvnPassword) 146 | } 147 | } 148 | } 149 | } 150 | // ... 151 | ``` 152 | where `mvnUser` and `mvnPassword` are set in your `~/.gradle/gradle.properties` -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "gitbucket-maven-repository-plugin" 2 | organization := "io.github.gitbucket" 3 | version := "1.8.0" 4 | scalaVersion := "2.13.7" 5 | gitbucketVersion := "4.37.1" 6 | scalacOptions += "-deprecation" 7 | resolvers += Resolver.mavenLocal 8 | libraryDependencies ++= Seq( 9 | "org.apache.sshd" % "sshd-scp" % "2.8.0" 10 | ) 11 | 12 | assembly / assemblyMergeStrategy := { 13 | case PathList("META-INF", xs @ _*) => 14 | (xs map { _.toLowerCase }) match { 15 | case ("manifest.mf" :: Nil) => MergeStrategy.discard 16 | case _ => MergeStrategy.discard 17 | } 18 | case x => MergeStrategy.first 19 | } -------------------------------------------------------------------------------- /maven-repository-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takezoe/gitbucket-maven-repository-plugin/674e1c0382fbfbf37ef35492797822920be4b8df/maven-repository-settings.png -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.5.6 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("io.github.gitbucket" % "sbt-gitbucket-plugin" % "1.5.1") 2 | -------------------------------------------------------------------------------- /src/main/resources/update/gitbucket-maven-repository_1.1.0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/scala/Plugin.scala: -------------------------------------------------------------------------------- 1 | import java.io.{File, IOException, OutputStream} 2 | import java.nio.file.{Files, OpenOption, Path} 3 | import java.nio.file.attribute.PosixFilePermission 4 | 5 | import gitbucket.core.controller.Context 6 | import gitbucket.core.plugin.Link 7 | import gitbucket.core.servlet.Database 8 | import gitbucket.core.model.Profile.profile.blockingApi._ 9 | import io.github.gitbucket.mavenrepository._ 10 | import io.github.gitbucket.mavenrepository.command.{LsCommand, MkdirCommand} 11 | import io.github.gitbucket.mavenrepository.controller.MavenRepositoryController 12 | import io.github.gitbucket.mavenrepository.service.MavenRepositoryService 13 | import io.github.gitbucket.solidbase.migration.{LiquibaseMigration, Migration} 14 | import io.github.gitbucket.solidbase.model.Version 15 | import org.apache.sshd.scp.common.helpers.DefaultScpFileOpener 16 | import org.apache.sshd.scp.server.ScpCommand 17 | import org.apache.sshd.common.session.Session 18 | import org.apache.sshd.server.channel.ChannelSession 19 | 20 | class Plugin extends gitbucket.core.plugin.Plugin with MavenRepositoryService { 21 | override val pluginId: String = "maven-repository" 22 | override val pluginName: String = "Maven Repository Plugin" 23 | override val description: String = "Host Maven repository on GitBucket." 24 | override val versions: List[Version] = List( 25 | new Version("1.0.0"), 26 | new Version("1.0.1"), 27 | new Version("1.1.0", 28 | new LiquibaseMigration("update/gitbucket-maven-repository_1.1.0.xml"), 29 | (moduleId: String, version: String, context: java.util.Map[String, AnyRef]) => { 30 | new File(s"${RegistryPath}/releases").mkdirs() 31 | new File(s"${RegistryPath}/snapshots").mkdirs() 32 | } 33 | ), 34 | new Version("1.2.0"), 35 | new Version("1.2.1"), 36 | new Version("1.3.0"), 37 | new Version("1.3.1"), 38 | new Version("1.3.2"), 39 | new Version("1.4.0"), 40 | new Version("1.5.0"), 41 | new Version("1.6.0"), 42 | new Version("1.7.0"), 43 | new Version("1.8.0") 44 | ) 45 | 46 | override val sshCommandProviders = Seq({ 47 | case command: String if checkCommand(command) => (session: ChannelSession) => { 48 | val index = command.indexOf('/') 49 | val path = command.substring(index + "/maven".length) 50 | val registryName = path.split("/")(1) 51 | val registry = Database() withTransaction { implicit session => getMavenRepository(registryName).get } 52 | val fullPath = s"${RegistryPath}/${path}" 53 | 54 | if(command.startsWith("scp")){ 55 | new ScpCommand( 56 | session, 57 | s"scp -t -d ${fullPath}", 58 | null, // executorService 59 | 1024 * 128, // sendSize 60 | 1024 * 128, // receiveSize 61 | new DefaultScpFileOpener(){ 62 | override def openWrite(session: Session, file: Path, size: Long, permissions: java.util.Set[PosixFilePermission], options: OpenOption*): OutputStream = { 63 | val fileName = file.getFileName.toString 64 | if(fileName == "maven-metadata.xml" || fileName.startsWith("maven-metadata.xml.")){ 65 | // accept 66 | } else if(registry.overwrite == false && Files.exists(file)){ 67 | throw new IOException("Rejected.") 68 | } 69 | super.openWrite(session, file, size, permissions, options: _*) 70 | } 71 | }, 72 | null // eventListener 73 | ) 74 | } else if(command.startsWith("mkdir")){ 75 | new MkdirCommand(new File(fullPath)) 76 | } else { 77 | new LsCommand(new File(fullPath)) 78 | } 79 | } 80 | }) 81 | 82 | /** 83 | * Check the existence of the library repository. 84 | */ 85 | private def checkCommand(command: String): Boolean = { 86 | Database() withTransaction { implicit session => 87 | getMavenRepositories().exists { registry => 88 | command.matches(s"scp .* /maven/${registry.name}/.*") || 89 | command.startsWith(s"ls /maven/${registry.name}") || 90 | command.startsWith(s"mkdir /maven/${registry.name}") 91 | } 92 | } 93 | } 94 | 95 | private val controller = new MavenRepositoryController() 96 | 97 | override val controllers = Seq( 98 | "/maven/*" -> controller, 99 | "/admin/maven/*" -> controller 100 | ) 101 | 102 | override val anonymousAccessiblePaths = Seq("/maven") 103 | 104 | override val systemSettingMenus: Seq[Context => Option[Link]] = Seq( 105 | _ => Some(Link("maven", "Maven repositories", "admin/maven", Some("package"))) 106 | ) 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/main/scala/io/github/gitbucket/mavenrepository/command/AbstractCommand.scala: -------------------------------------------------------------------------------- 1 | package io.github.gitbucket.mavenrepository.command 2 | 3 | import java.io.{InputStream, OutputStream} 4 | 5 | import org.apache.sshd.server.command.Command 6 | import org.apache.sshd.server.{Environment, ExitCallback} 7 | import org.apache.sshd.server.channel.ChannelSession 8 | 9 | abstract class AbstractCommand extends Command { 10 | 11 | protected val Success = 0 12 | protected val Failure = -1 13 | 14 | protected var in: InputStream = null 15 | protected var out: OutputStream = null 16 | protected var err: OutputStream = null 17 | protected var callback: ExitCallback = null 18 | override def setErrorStream(err: OutputStream): Unit = this.err = err 19 | override def setOutputStream(out: OutputStream): Unit = this.out = out 20 | override def setInputStream(in: InputStream): Unit = this.in = in 21 | override def setExitCallback(callback: ExitCallback): Unit = this.callback = callback 22 | override def start(session: ChannelSession, env: Environment): Unit = { 23 | val exitCode = execute() 24 | out.flush() 25 | 26 | in.close() 27 | out.close() 28 | err.close() 29 | 30 | callback.onExit(exitCode) 31 | } 32 | override def destroy(session: ChannelSession): Unit = {} 33 | protected def execute(): Int 34 | } 35 | -------------------------------------------------------------------------------- /src/main/scala/io/github/gitbucket/mavenrepository/command/LsCommand.scala: -------------------------------------------------------------------------------- 1 | package io.github.gitbucket.mavenrepository.command 2 | 3 | import java.io.File 4 | 5 | class LsCommand(dir: File) extends AbstractCommand { 6 | override protected def execute(): Int = { 7 | if(dir.exists && dir.isDirectory){ 8 | val result = dir.listFiles.map(_.getName).mkString("\t") 9 | out.write(result.getBytes("UTF-8")) 10 | Success 11 | } else { 12 | Failure 13 | } 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/main/scala/io/github/gitbucket/mavenrepository/command/MkdirCommand.scala: -------------------------------------------------------------------------------- 1 | package io.github.gitbucket.mavenrepository.command 2 | 3 | import java.io.File 4 | 5 | class MkdirCommand(dir: File) extends AbstractCommand { 6 | override def execute(): Int = { 7 | dir.mkdirs() 8 | Success 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/scala/io/github/gitbucket/mavenrepository/controller/MavenRepositoryController.scala: -------------------------------------------------------------------------------- 1 | package io.github.gitbucket.mavenrepository.controller 2 | 3 | import java.io.{File, FileInputStream, FileOutputStream} 4 | import java.nio.file.{Files, Paths} 5 | 6 | import io.github.gitbucket.mavenrepository._ 7 | import gitbucket.core.controller.ControllerBase 8 | import gitbucket.core.model.Account 9 | import gitbucket.core.service.AccountService 10 | import gitbucket.core.util.{AdminAuthenticator, AuthUtil, FileUtil} 11 | import gitbucket.core.util.Implicits._ 12 | import io.github.gitbucket.mavenrepository.service.MavenRepositoryService 13 | import org.apache.commons.io.{FileUtils, IOUtils} 14 | import org.scalatra.forms._ 15 | import org.scalatra.i18n.Messages 16 | import org.scalatra.{ActionResult, NotAcceptable, Ok} 17 | import scala.util.Using 18 | import org.scalatra.BadRequest 19 | 20 | class MavenRepositoryController extends ControllerBase with AccountService with MavenRepositoryService 21 | with AdminAuthenticator { 22 | 23 | case class RepositoryCreateForm(name: String, description: Option[String], overwrite: Boolean, isPrivate: Boolean) 24 | case class RepositoryEditForm(description: Option[String], overwrite: Boolean, isPrivate: Boolean) 25 | 26 | val repositoryCreateForm = mapping( 27 | "name" -> trim(label("Name", text(required, identifier, maxlength(100), unique))), 28 | "description" -> trim(label("Description", optional(text()))), 29 | "overwrite" -> trim(boolean()), 30 | "isPrivate" -> trim(boolean()) 31 | )(RepositoryCreateForm.apply) 32 | 33 | val repositoryEditForm = mapping( 34 | "description" -> trim(label("Description", optional(text()))), 35 | "overwrite" -> trim(boolean()), 36 | "isPrivate" -> trim(boolean()) 37 | )(RepositoryEditForm.apply) 38 | 39 | 40 | get("/admin/maven")(adminOnly { 41 | gitbucket.mavenrepository.html.settings(getMavenRepositories()) 42 | }) 43 | 44 | get("/admin/maven/_new")(adminOnly { 45 | gitbucket.mavenrepository.html.form(None) 46 | }) 47 | 48 | post("/admin/maven/_new", repositoryCreateForm)(adminOnly { form => 49 | createRegistry(form.name, form.description, form.overwrite, form.isPrivate) 50 | redirect("/admin/maven") 51 | }) 52 | 53 | get("/admin/maven/:name/_edit")(adminOnly { 54 | gitbucket.mavenrepository.html.form(getMavenRepository(params("name"))) 55 | }) 56 | 57 | post("/admin/maven/:name/_edit", repositoryEditForm)(adminOnly { form => 58 | updateRegistry(params("name"), form.description, form.overwrite, form.isPrivate) 59 | redirect("/admin/maven") 60 | }) 61 | 62 | post("/admin/maven/:name/_delete")(adminOnly { 63 | deleteRegistry(params("name")) 64 | redirect("/admin/maven") 65 | }) 66 | 67 | private def basicAuthentication(): Either[ActionResult, Account] = { 68 | request.header("Authorization").flatMap { 69 | case auth if auth.startsWith("Basic ") => { 70 | val Array(username, password) = AuthUtil.decodeAuthHeader(auth).split(":", 2) 71 | authenticate(context.settings, username, password) 72 | } 73 | case _ => None 74 | }.toRight { 75 | response.setHeader("WWW-Authenticate", "Basic realm=\"GitBucket Maven Repository\"") 76 | org.scalatra.Unauthorized() 77 | } 78 | } 79 | 80 | post("/admin/maven/:name/_deletefiles")(adminOnly { 81 | val name = params("name") 82 | val path = validatePath(params("path")) 83 | val files = multiParams("files") 84 | 85 | files.foreach { file => 86 | val fullPath = if (path.nonEmpty) { 87 | s"${RegistryPath}/${name}/${path}/${file}" 88 | } else { 89 | s"${RegistryPath}/${name}/${file}" 90 | } 91 | val f = new File(fullPath) 92 | FileUtils.deleteQuietly(f) 93 | } 94 | if (path.nonEmpty) { 95 | redirect(s"/maven/${name}/${path}/") 96 | } else { 97 | redirect(s"/maven/${name}/") 98 | } 99 | }) 100 | 101 | get("/maven/:name"){ 102 | download(params("name"), "") 103 | } 104 | 105 | get("/maven/:name/*"){ 106 | val path = validatePath(multiParams("splat").head) 107 | download(params("name"), path) 108 | } 109 | 110 | private def download(name: String, path: String) = { 111 | val result = for { 112 | // Find registry 113 | registry <- getMavenRepository(name).toRight { NotFound() } 114 | // Basic authentication 115 | _ <- if(registry.isPrivate){ basicAuthentication().map(x => Some(x)) } else Right(None) 116 | //path = multiParams("splat").head 117 | file = new File(s"${RegistryPath}/${name}/${path}") 118 | } yield { 119 | file match { 120 | // Download the file 121 | case f if f.exists && f.isFile => 122 | contentType = FileUtil.getMimeType(path) 123 | response.setContentLength(file.length.toInt) 124 | Using.resource(new FileInputStream(file)){ in => 125 | IOUtils.copy(in, response.getOutputStream) 126 | } 127 | 128 | // Render the directory index 129 | case f if f.exists && f.isDirectory => 130 | val files = file.listFiles.toSeq.sortWith { (file1, file2) => 131 | (file1.isDirectory, file2.isDirectory) match { 132 | case (true , false) => true 133 | case (false, true ) => false 134 | case _ => file1.getName.compareTo(file2.getName) < 0 135 | } 136 | } 137 | 138 | gitbucket.mavenrepository.html.files(name, path, files) 139 | 140 | // Otherwise 141 | case _ => NotFound() 142 | } 143 | } 144 | 145 | result.fold(identity, identity) 146 | } 147 | 148 | put("/maven/:name/*"){ 149 | val name = params("name") 150 | val path = validatePath(multiParams("splat").head) 151 | 152 | val result = for { 153 | // Find registry 154 | registry <- getMavenRepository(name).toRight { NotFound() } 155 | // Basic authentication 156 | _ <- if(registry.isPrivate){ basicAuthentication().map(x => Some(x)) } else Right(None) 157 | // Overwrite check 158 | file = new File(s"${RegistryPath}/${name}/${path}") 159 | _ <- if(file.getName == "maven-metadata.xml" || file.getName.startsWith("maven-metadata.xml.")){ 160 | Right(()) 161 | } else if(!registry.overwrite && file.exists){ 162 | Left(NotAcceptable()) 163 | } else { 164 | Right(()) 165 | } 166 | } yield { 167 | val parent = file.getParentFile 168 | if(!parent.exists){ 169 | parent.mkdirs() 170 | } 171 | Using.resource(new FileOutputStream(file)){ out => 172 | IOUtils.copy(request.getInputStream, out) 173 | } 174 | Ok() 175 | } 176 | 177 | result.fold(identity, identity) 178 | } 179 | 180 | // authentication required 181 | // delete artifacts, only if the registry is overwritable 182 | delete("/maven/:name/*") { 183 | val name = params("name") 184 | val path = validatePath(multiParams("splat").head) 185 | 186 | val result = for { 187 | registry <- getMavenRepository(name).toRight(NotFound()) 188 | _ <- basicAuthentication() 189 | path = multiParams("splat").head 190 | file = Paths.get(RegistryPath, name, path) 191 | repoBase = Paths.get(RegistryPath, registry.name) 192 | _ <- if (!Files.exists(file) || !registry.overwrite) Left(NotAcceptable()) else Right(()) 193 | } yield { 194 | if (Files.isSameFile(repoBase, file)) { 195 | // clean up repository 196 | FileUtils.cleanDirectory(file.toFile) 197 | } else { 198 | // remove file and remove the directory if it's empty 199 | FileUtils.deleteDirectory(file.toFile) 200 | val parent = file.getParent 201 | if (!Files.isSameFile(repoBase, parent)) { 202 | FileUtil.deleteDirectoryIfEmpty(parent.toFile) 203 | } 204 | } 205 | Ok() 206 | } 207 | 208 | result.fold(identity, identity) 209 | } 210 | 211 | private def unique: Constraint = new Constraint(){ 212 | override def validate(name: String, value: String, messages: Messages): Option[String] = { 213 | getMavenRepository(value).map { _ => "Repository already exist." } 214 | } 215 | } 216 | 217 | private def validatePath(path: String): String = { 218 | if (path != null && path.contains("..")) { 219 | halt(BadRequest()) 220 | } 221 | path 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/main/scala/io/github/gitbucket/mavenrepository/model/Registry.scala: -------------------------------------------------------------------------------- 1 | package io.github.gitbucket.mavenrepository.model 2 | 3 | trait RegistryComponent { self: gitbucket.core.model.Profile => 4 | import profile.api._ 5 | import self._ 6 | 7 | lazy val Registries = TableQuery[Registries] 8 | 9 | class Registries(tag: Tag) extends Table[Registry](tag, "REGISTRY"){ 10 | val name = column[String]("NAME") 11 | val description = column[String]("DESCRIPTION") 12 | val overwrite = column[Boolean]("OVERWRITE") 13 | val isPrivate = column[Boolean]("PRIVATE") 14 | def * = (name, description.?, overwrite, isPrivate) <> (Registry.tupled, Registry.unapply) 15 | } 16 | 17 | } 18 | 19 | case class Registry( 20 | name: String, 21 | description: Option[String], 22 | overwrite: Boolean, 23 | isPrivate: Boolean 24 | ) -------------------------------------------------------------------------------- /src/main/scala/io/github/gitbucket/mavenrepository/model/RegistryProfile.scala: -------------------------------------------------------------------------------- 1 | package io.github.gitbucket.mavenrepository.model 2 | 3 | import gitbucket.core.model._ 4 | 5 | object Profile extends CoreProfile 6 | with RegistryComponent 7 | -------------------------------------------------------------------------------- /src/main/scala/io/github/gitbucket/mavenrepository/package.scala: -------------------------------------------------------------------------------- 1 | package io.github.gitbucket 2 | 3 | import gitbucket.core.util.Directory 4 | 5 | package object mavenrepository { 6 | 7 | val RegistryPath = s"${Directory.GitBucketHome}/maven" 8 | 9 | } 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/scala/io/github/gitbucket/mavenrepository/service/MavenRepositoryService.scala: -------------------------------------------------------------------------------- 1 | package io.github.gitbucket.mavenrepository.service 2 | 3 | import java.io.File 4 | 5 | import io.github.gitbucket.mavenrepository.RegistryPath 6 | import io.github.gitbucket.mavenrepository.model.Registry 7 | import io.github.gitbucket.mavenrepository.model.Profile._ 8 | import io.github.gitbucket.mavenrepository.model.Profile.profile.blockingApi._ 9 | import org.apache.commons.io.FileUtils 10 | import gitbucket.core.util.SyntaxSugars._ 11 | 12 | trait MavenRepositoryService { 13 | 14 | def getMavenRepository(name: String)(implicit s: Session): Option[Registry] = { 15 | Registries.filter(_.name === name.bind).firstOption 16 | } 17 | 18 | def getMavenRepositories()(implicit s: Session): Seq[Registry] = { 19 | Registries.sortBy(_.name).list 20 | } 21 | 22 | def createRegistry(name: String, description: Option[String], overwrite: Boolean, isPrivate: Boolean) 23 | (implicit s: Session): Unit = { 24 | Registries.insert(Registry(name, description, overwrite, isPrivate)) 25 | 26 | val dir = new File(s"${RegistryPath}/${name}") 27 | dir.mkdirs() 28 | } 29 | 30 | def updateRegistry(name: String, description: Option[String], overwrite: Boolean, isPrivate: Boolean) 31 | (implicit s: Session): Unit = { 32 | Registries.filter(_.name === name.bind) 33 | .map(t => (t.description.?, t.overwrite, t.isPrivate)) 34 | .update((description, overwrite, isPrivate)) 35 | } 36 | 37 | def deleteRegistry(name: String)(implicit s: Session): Unit = { 38 | Registries.filter(_.name === name.bind).delete 39 | 40 | val dir = new File(s"${RegistryPath}/${name}") 41 | FileUtils.deleteDirectory(dir) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/twirl/gitbucket/mavenrepository/files.scala.html: -------------------------------------------------------------------------------- 1 | @(name: String, path: String, files: Seq[java.io.File])(implicit context: gitbucket.core.controller.Context) 2 | @import gitbucket.core.view.helpers._ 3 | @gitbucket.core.html.main(s"$name - /$path"){ 4 |
5 |
6 |
7 | @context.loginAccount.collect { case account if account.isAdmin => 8 | 9 | 10 | } 11 |

12 | @name 13 | @defining(path.split("/")){ fragments => 14 | @fragments.zipWithIndex.map { case (fragment, i) => 15 | / 16 | @if(i == fragments.size - 1){ 17 | @fragment 18 | } else { 19 | @fragment 20 | } 21 | } 22 | } 23 |

24 | 25 | 26 | @context.loginAccount.collect { case account if account.isAdmin=> 27 | 28 | } 29 | 30 | 31 | 32 | 33 | @files.map { file => 34 | 35 | @context.loginAccount.collect { case account if account.isAdmin => 36 | 39 | } 40 | 57 | 62 | 63 | 64 | } 65 |
NameSizeDate
37 | 38 | 41 | @if(file.isDirectory) { 42 | 43 | @if(path.nonEmpty){ 44 | @file.getName/ 45 | } else { 46 | @file.getName/ 47 | } 48 | }else{ 49 | 50 | @if(path.nonEmpty){ 51 | @file.getName 52 | } else { 53 | @file.getName 54 | } 55 | } 56 | 58 | @if(file.isFile){ 59 | @{Math.ceil(file.length.toDouble / 1024 * 10) / 10}KB 60 | } 61 | @datetime(new java.util.Date(file.lastModified))
66 |
67 |
68 |
69 | } 70 | @context.loginAccount.collect { case account if account.isAdmin => 71 | 83 | } -------------------------------------------------------------------------------- /src/main/twirl/gitbucket/mavenrepository/form.scala.html: -------------------------------------------------------------------------------- 1 | @(registry: Option[io.github.gitbucket.mavenrepository.model.Registry])(implicit context: gitbucket.core.controller.Context) 2 | @gitbucket.core.html.main("Maven repositories") { 3 | @gitbucket.core.admin.html.menu("maven") { 4 |
5 |
6 | 7 |
8 | 9 | 10 |
11 |
12 |
13 | 14 |
15 | 16 | 17 |
18 |
19 |
20 | 23 |
24 |
25 | 28 |
29 |
30 | @if(registry.isEmpty){ 31 | 32 | } else { 33 | 34 | } 35 | Cancel 36 |
37 |
38 | } 39 | } -------------------------------------------------------------------------------- /src/main/twirl/gitbucket/mavenrepository/settings.scala.html: -------------------------------------------------------------------------------- 1 | @(registries: Seq[io.github.gitbucket.mavenrepository.model.Registry])(implicit context: gitbucket.core.controller.Context) 2 | @gitbucket.core.html.main("Maven repositories") { 3 | @gitbucket.core.admin.html.menu("maven") { 4 |
5 | New maven repository 6 |
7 |

Maven repositories

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | @registries.map { registry => 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | } 34 |
NameDescriptionTypeURLOverwritePrivate
@registry.name@registry.descriptionMaven@context.baseUrl/maven/@registry.name/@registry.overwrite@registry.isPrivate 27 | 28 | Edit 29 | Delete 30 | 31 |
35 |
36 | } 37 | } 38 | 48 | 49 | --------------------------------------------------------------------------------