Implementing file upload on the repository viewer

This commit is contained in:
Naoki Takezoe
2017-05-24 14:39:55 +09:00
parent 4727aa90ab
commit 43f7a61c4b
5 changed files with 187 additions and 78 deletions

View File

@@ -31,6 +31,13 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
}, FileUtil.isImage) }, FileUtil.isImage)
} }
post("/tmp"){
execute({ (file, fileId) =>
FileUtils.writeByteArrayToFile(new java.io.File(getTemporaryDir(session.getId), fileId), file.get)
session += Keys.Session.Upload(fileId) -> file.name
}, _ => true)
}
post("/file/:owner/:repository"){ post("/file/:owner/:repository"){
execute({ (file, fileId) => execute({ (file, fileId) =>
FileUtils.writeByteArrayToFile(new java.io.File( FileUtils.writeByteArrayToFile(new java.io.File(

View File

@@ -1,6 +1,6 @@
package gitbucket.core.controller package gitbucket.core.controller
import java.io.FileInputStream import java.io.File
import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import gitbucket.core.plugin.PluginRegistry import gitbucket.core.plugin.PluginRegistry
@@ -18,14 +18,12 @@ import gitbucket.core.service.WebHookService._
import gitbucket.core.view import gitbucket.core.view
import gitbucket.core.view.helpers import gitbucket.core.view.helpers
import io.github.gitbucket.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.IOUtils import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.{ArchiveCommand, Git} import org.eclipse.jgit.api.{ArchiveCommand, Git}
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat} import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
import org.eclipse.jgit.dircache.{DirCache, DirCacheBuilder} import org.eclipse.jgit.dircache.{DirCache, DirCacheBuilder}
import org.eclipse.jgit.errors.MissingObjectException import org.eclipse.jgit.errors.MissingObjectException
import org.eclipse.jgit.lib._ import org.eclipse.jgit.lib._
import org.eclipse.jgit.revwalk.RevCommit
import org.eclipse.jgit.treewalk._
import org.scalatra._ import org.scalatra._
@@ -45,6 +43,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
ArchiveCommand.registerFormat("zip", new ZipFormat) ArchiveCommand.registerFormat("zip", new ZipFormat)
ArchiveCommand.registerFormat("tar.gz", new TgzFormat) ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
case class UploadForm(
branch: String,
path: String,
uploadFiles: String,
message: Option[String]
)
case class EditorForm( case class EditorForm(
branch: String, branch: String,
path: String, path: String,
@@ -71,6 +76,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
issueId: Option[Int] issueId: Option[Int]
) )
val uploadForm = mapping(
"branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())),
"uploadFiles" -> trim(label("Upload files", text(required))),
"message" -> trim(label("Message", optional(text()))),
)(UploadForm.apply)
val editorForm = mapping( val editorForm = mapping(
"branch" -> trim(label("Branch", text(required))), "branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())), "path" -> trim(label("Path", text())),
@@ -173,10 +185,37 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val (branch, path) = repository.splitPath(multiParams("splat").head) val (branch, path) = repository.splitPath(multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName) val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList, html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
None, JGitUtil.ContentInfo("text", None, None, Some("UTF-8")), None, JGitUtil.ContentInfo("text", None, None, Some("UTF-8")), protectedBranch)
protectedBranch)
}) })
get("/:owner/:repository/upload/*")(writableUsersOnly { repository =>
val (branch, path) = repository.splitPath(multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
html.upload(branch, repository, if(path.length == 0) Nil else path.split("/").toList, protectedBranch)
})
post("/:owner/:repository/upload", uploadForm)(writableUsersOnly { (form, repository) =>
val files = form.uploadFiles.split("\n").map { line =>
val i = line.indexOf(":")
CommitFile(line.substring(0, i).trim, line.substring(i + 1).trim)
}
commitFiles(
repository = repository,
branch = form.branch,
path = form.path,
files = files,
message = form.message.getOrElse(s"Add files via upload")
)
if(form.path.length == 0){
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}")
} else {
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}/${form.path}")
}
})
get("/:owner/:repository/edit/*")(writableUsersOnly { repository => get("/:owner/:repository/edit/*")(writableUsersOnly { repository =>
val (branch, path) = repository.splitPath(multiParams("splat").head) val (branch, path) = repository.splitPath(multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName) val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
@@ -547,65 +586,30 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
}) })
/**
* Upload file to a branch.
*/
post("/:owner/:repository/files/upload")(writableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) =>
(for {
data <- extractFromJsonBody[UploadFiles] if data.isValid
} yield {
Directory.getAttachedDir(owner, name) match {
case dir if (dir.exists && dir.isDirectory) =>
val _commitFiles = data.fileIds.map { case (fileName, id) =>
dir.listFiles.find(_.getName.startsWith(id + ".")).map { file =>
CommitFile(id, fileName, using(new FileInputStream(file))(IOUtils.toByteArray))
}
}.toList
val finalCommitFiles = _commitFiles.flatten
if(finalCommitFiles.size == data.fileIds.size) {
commitFiles(
repository,
files = finalCommitFiles,
branch = data.branch,
path = data.path,
message = data.message)
}
else {
org.scalatra.NotAcceptable(
s"""{"message":
|"$repository doesn't contain all the files you specified in the body"}""".stripMargin)
}
case _ => org.scalatra.NotFound(s"""{"message": "$repository doesn't contain any attached files"}""")
}
}) getOrElse
org.scalatra.NotAcceptable("""{"message": "FileIds can't be an empty list"}""")
}
})
case class UploadFiles(branch: String, path: String, fileIds : Map[String,String], message: String) { case class UploadFiles(branch: String, path: String, fileIds : Map[String,String], message: String) {
lazy val isValid: Boolean = fileIds.size > 0 lazy val isValid: Boolean = fileIds.size > 0
} }
case class CommitFile(fileId: String, name: String, fileBytes: Array[Byte]) case class CommitFile(id: String, name: String)
private def commitFiles(repository: RepositoryService.RepositoryInfo, private def commitFiles(repository: RepositoryService.RepositoryInfo,
files: List[CommitFile], files: Seq[CommitFile],
branch: String, path: String, message: String) = { branch: String, path: String, message: String) = {
// prepend path to the filename
val newFiles = files.map { file =>
file.copy(name = if(path.length == 0) file.name else s"${path}/${file.name}")
}
_commitFile(repository, branch, path, message) { case (git, headTip, builder, inserter) => _commitFile(repository, branch, message) { case (git, headTip, builder, inserter) =>
JGitUtil.processTree(git, headTip) { (path, tree) => JGitUtil.processTree(git, headTip) { (path, tree) =>
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) if(!newFiles.exists(_.name.contains(path))) {
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
}
} }
files.foreach { item => newFiles.foreach { file =>
val fileName = item.name val bytes = FileUtils.readFileToByteArray(new File(getTemporaryDir(session.getId), file.id))
val bytes = item.fileBytes builder.add(JGitUtil.createDirCacheEntry(file.name,
builder.add(JGitUtil.createDirCacheEntry(fileName,
FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, bytes))) FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, bytes)))
builder.finish() builder.finish()
} }
@@ -619,7 +623,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val newPath = newFileName.map { newFileName => if(path.length == 0) newFileName else s"${path}/${newFileName}" } val newPath = newFileName.map { newFileName => if(path.length == 0) newFileName else s"${path}/${newFileName}" }
val oldPath = oldFileName.map { oldFileName => if(path.length == 0) oldFileName else s"${path}/${oldFileName}" } val oldPath = oldFileName.map { oldFileName => if(path.length == 0) oldFileName else s"${path}/${oldFileName}" }
_commitFile(repository, branch, path, message){ case (git, headTip, builder, inserter) => _commitFile(repository, branch, message){ case (git, headTip, builder, inserter) =>
val permission = JGitUtil.processTree(git, headTip){ (path, tree) => val permission = JGitUtil.processTree(git, headTip){ (path, tree) =>
// Add all entries except the editing file // Add all entries except the editing file
if(!newPath.contains(path) && !oldPath.contains(path)){ if(!newPath.contains(path) && !oldPath.contains(path)){
@@ -639,7 +643,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
private def _commitFile(repository: RepositoryService.RepositoryInfo, private def _commitFile(repository: RepositoryService.RepositoryInfo,
branch: String, path: String, message: String)(f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => Unit) = { branch: String, message: String)(f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => Unit) = {
LockUtil.lock(s"${repository.owner}/${repository.name}") { LockUtil.lock(s"${repository.owner}/${repository.name}") {
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>

View File

@@ -22,7 +22,7 @@
s"${(repository.name :: pathList).mkString("/")} at ${helpers.encodeRefName(branch)} - ${repository.owner}/${repository.name}" s"${(repository.name :: pathList).mkString("/")} at ${helpers.encodeRefName(branch)} - ${repository.owner}/${repository.name}"
}, Some(repository)) { }, Some(repository)) {
@gitbucket.core.html.menu("files", repository, Some(branch), info, error){ @gitbucket.core.html.menu("files", repository, Some(branch), info, error){
<div class="head"> <div class="head" style="height: 24px;">
<div class="pull-right"> <div class="pull-right">
<div class="btn-group"> <div class="btn-group">
<a href="@helpers.url(repository)/find/@helpers.encodeRefName(branch)" class="btn btn-sm btn-default" data-hotkey="t"><i class="octicon octicon-search"></i></a> <a href="@helpers.url(repository)/find/@helpers.encodeRefName(branch)" class="btn btn-sm btn-default" data-hotkey="t"><i class="octicon octicon-search"></i></a>
@@ -67,27 +67,24 @@
</div> </div>
</div> </div>
} }
@gitbucket.core.helper.html.branchcontrol(branch, repository, hasWritePermission){ <div class="pull-left">
@repository.branchList.map { x => @gitbucket.core.helper.html.branchcontrol(branch, repository, hasWritePermission){
<li><a href="@helpers.url(repository)/tree/@helpers.encodeRefName(x)">@gitbucket.core.helper.html.checkicon(x == branch) @x</a></li> @repository.branchList.map { x =>
<li><a href="@helpers.url(repository)/tree/@helpers.encodeRefName(x)">@gitbucket.core.helper.html.checkicon(x == branch) @x</a></li>
}
} }
} @if(pathList.nonEmpty){
@if(pathList.isEmpty){ <a href="@helpers.url(repository)/tree/@helpers.encodeRefName(branch)">@repository.name</a> /
@* @pathList.zipWithIndex.map { case (section, i) =>
@branchPullRequest.map{ case (pullRequest, issue) => <a href="@helpers.url(repository)/tree/@helpers.encodeRefName(branch)/@pathList.take(i + 1).mkString("/")">@section</a> /
<a href="@url(repository)/pull/@pullRequest.issueId" class="btn btn-sm btn-pullrequest-branch" title="@issue.title" data-toggle="tooltip">View #@pullRequest.issueId</a> }
}.getOrElse {
<a href="@url(repository)/compare?head=@urlEncode(encodeRefName(branch))" class="btn btn-sm btn-success" @if(loginAccount.isEmpty){disabled}>New pull request</a>
} }
*@ </div>
} else {
<a href="@helpers.url(repository)/tree/@helpers.encodeRefName(branch)">@repository.name</a> /
@pathList.zipWithIndex.map { case (section, i) =>
<a href="@helpers.url(repository)/tree/@helpers.encodeRefName(branch)/@pathList.take(i + 1).mkString("/")">@section</a> /
}
}
@if(hasWritePermission){ @if(hasWritePermission){
<a href="@helpers.url(repository)/new/@helpers.encodeRefName(branch)/@pathList.mkString("/")" class="btn btn-sm btn-default pc" title="Create a new file here"><i class="octicon octicon-plus"></i></a> <div class="btn-group pull-left" style="margin-left: 4px;">
<a href="@helpers.url(repository)/new/@helpers.encodeRefName(branch)/@pathList.mkString("/")" class="btn btn-sm btn-default pc" title="Create a new file"><i class="octicon octicon-plus"></i></a>
<a href="@helpers.url(repository)/upload/@helpers.encodeRefName(branch)/@pathList.mkString("/")" class="btn btn-sm btn-default pc" title="Upload files"><i class="octicon octicon-cloud-upload"></i></a>
</div>
} }
</div> </div>
<table class="table table-hover"> <table class="table table-hover">

View File

@@ -0,0 +1,103 @@
@(branch: String,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
pathList: List[String],
protectedBranch: Boolean)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@import gitbucket.core.util.FileUtil
@gitbucket.core.html.main(s"Upload Files at ${branch} - ${repository.owner}/${repository.name}", Some(repository)) {
@gitbucket.core.html.menu("files", repository){
@if(protectedBranch){
<div class="alert alert-danger">branch @branch is protected.</div>
}
<form method="POST" action="@helpers.url(repository)/upload" id="upload-form">
@*
<span class="error" id="error-newFileName"></span>
*@
<div class="head">
<a href="@helpers.url(repository)/tree/@helpers.encodeRefName(branch)">@repository.name</a> /
@pathList.zipWithIndex.map { case (section, i) =>
<a href="@helpers.url(repository)/tree/@helpers.encodeRefName(branch)/@pathList.take(i + 1).mkString("/")">@section</a> /
}
<input type="hidden" name="branch" id="branch" value="@branch"/>
<input type="hidden" name="path" id="path" value="@pathList.mkString("/")"/>
</div>
<table class="table table-bordered">
<tr>
<td style="padding: 0px; background-color: #f4f4f4; border: 1px #ddd solid;">
<div id="upload-area" style="text-align: center; padding-top: 20px; padding-bottom: 20px; font-size: 120%;">
Drag files here to add them to your repository
</div>
<ul id="upload-files">
</ul>
</td>
</tr>
</table>
<div class="panel panel-default issue-comment-box">
<div class="panel-body">
<div>
<strong>Commit changes</strong>
</div>
<div class="form-group">
<input type="text" name="message" class="form-control"/>
</div>
<div style="text-align: right;">
<a href="@helpers.url(repository)/tree/@helpers.encodeRefName(branch)/@{pathList.mkString("/")}" class="btn btn-danger">Cancel</a>
<input type="submit" id="commit" class="btn btn-success" value="Commit changes" disabled="true"/>
<input type="hidden" id="upload-files-data" name="uploadFiles" value=""/>
</div>
</div>
</div>
</form>
}
}
<style type="text/css">
ul#upload-files {
list-style: none;
padding-left: 0px;
margin-bottom: 0px;
}
li.upload-file {
border-top: 1px #f4f4f4 solid;
background-color: white;
padding: 4px;
}
</style>
<script>
$(function(){
$('#upload-area').dropzone({
url: '@context.path/upload/tmp',
maxFilesize: 10,
clickable: true,
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) {
file.previewElement.remove();
$('#upload-files').append($('<li class="upload-file">')
.append($('<span>').data('id', id).text(file.name))
.append($('<a class="delete" href="javascript:void(0);" style="margin-left: 4px;">(delete)</a>')));
updateCommitButtonStatus();
}
});
$('#upload-form').submit(function(){
try {
var data = '';
$.each($('li.upload-file span'), function(i, e){
data = data + $(e).data('id') + ':' + $(e).text() + '\n';
});
$('#upload-files-data').val(data);
} catch(e){
console.log(e);
}
});
$(document).on('click', 'a.delete', function(e){
$(e.target).parent().remove();
updateCommitButtonStatus();
});
function updateCommitButtonStatus(){
$('#commit').attr('disabled', $('.upload-file').length == 0);
}
});
</script>

View File

@@ -38,9 +38,7 @@ h6 {
.octicon,.mega-octicon{ .octicon,.mega-octicon{
color : #999; color : #999;
width: 14px; font-size: 14px;
height: 14px;
/*font-size: 14px;*/
text-align: center; text-align: center;
} }