diff --git a/app/controllers/CodeApp.java b/app/controllers/CodeApp.java index 01b4c24f0..8c6af47d6 100644 --- a/app/controllers/CodeApp.java +++ b/app/controllers/CodeApp.java @@ -21,6 +21,7 @@ package controllers; import actions.DefaultProjectCheckAction; +import com.fasterxml.jackson.databind.node.ObjectNode; import controllers.annotation.AnonymousCheck; import controllers.annotation.IsAllowed; import models.Project; @@ -28,10 +29,16 @@ import org.apache.commons.io.FilenameUtils; import org.apache.tika.Tika; import org.apache.tika.mime.MediaType; -import com.fasterxml.jackson.databind.node.ObjectNode; +import org.eclipse.jgit.api.ArchiveCommand; +import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.archive.ZipFormat; import org.tmatesoft.svn.core.SVNException; -import play.mvc.*; +import play.mvc.Controller; +import play.mvc.Http; +import play.mvc.Result; +import play.mvc.With; +import playRepository.GitRepository; import playRepository.PlayRepository; import playRepository.RepositoryService; import utils.ErrorViews; @@ -118,6 +125,38 @@ public static Result ajaxRequest(String userName, String projectName, String pat } } + @With(DefaultProjectCheckAction.class) + public static Result download(String userName, String projectName, String branch, String path) + throws UnsupportedOperationException, IOException, SVNException, GitAPIException, ServletException { + Project project = Project.findByOwnerAndProjectName(userName, projectName); + + if (!RepositoryService.VCS_GIT.equals(project.vcs) && !RepositoryService.VCS_SUBVERSION.equals(project.vcs)) { + return status(Http.Status.NOT_IMPLEMENTED, project.vcs + " is not supported!"); + } + + final String targetBranch = HttpUtil.decodePathSegment(branch); + final String targetPath = HttpUtil.decodePathSegment(path); + + PlayRepository repository = RepositoryService.getRepository(project); + List recursiveData = RepositoryService.getMetaDataFromAncestorDirectories( + repository, targetBranch, targetPath); + + if (recursiveData == null) { + return notFound(ErrorViews.NotFound.render()); + } + + // Prepare a chunked text stream + Chunks chunks = new ByteChunks() { + // Called when the stream is ready + public void onReady(Chunks.Out out) { + repository.getArchive(out, targetBranch); + } + }; + + response().setHeader("Content-Disposition", "attachment; filename=" + projectName + "-" + branch + ".zip"); + return ok(chunks); + } + @With(DefaultProjectCheckAction.class) public static Result ajaxRequestWithBranch(String userName, String projectName, String branch, String path) throws UnsupportedOperationException, IOException, SVNException, GitAPIException, ServletException{ diff --git a/app/playRepository/GitRepository.java b/app/playRepository/GitRepository.java index 33f24c97e..d20d744e8 100644 --- a/app/playRepository/GitRepository.java +++ b/app/playRepository/GitRepository.java @@ -20,6 +20,8 @@ */ package playRepository; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import controllers.UserApp; import controllers.routes; import models.Project; @@ -33,19 +35,20 @@ import org.apache.commons.lang3.StringUtils; import org.apache.tika.Tika; import org.apache.tika.metadata.Metadata; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; +import org.eclipse.jgit.api.ArchiveCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.LogCommand; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.archive.ZipFormat; import org.eclipse.jgit.attributes.AttributesNode; import org.eclipse.jgit.attributes.AttributesNodeProvider; import org.eclipse.jgit.attributes.AttributesRule; import org.eclipse.jgit.blame.BlameResult; import org.eclipse.jgit.diff.*; import org.eclipse.jgit.diff.Edit.Type; +import org.eclipse.jgit.errors.AmbiguousObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; -import org.eclipse.jgit.internal.storage.dfs.DfsRepository; import org.eclipse.jgit.lib.*; import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.revwalk.RevCommit; @@ -63,8 +66,9 @@ import org.eclipse.jgit.util.io.NullOutputStream; import org.tmatesoft.svn.core.SVNException; import play.Logger; -import play.api.Play; import play.libs.Json; +import play.mvc.Results.Chunks; +import utils.ChunkedOutputStream; import utils.FileUtil; import utils.GravatarUtil; @@ -1940,4 +1944,23 @@ public File getDirectory() { public Repository getRepository() { return repository; } + + public void getArchive(Chunks.Out out, String branchName){ + Git git = new Git(getRepository()); + ArchiveCommand.registerFormat("zip", new ZipFormat()); + try { + ChunkedOutputStream cos = new ChunkedOutputStream(out, 16384); + git.archive() + .setTree(getRepository().resolve(branchName)) + .setFormat("zip") + .setOutputStream(cos) + .call(); + } catch (IncorrectObjectTypeException | AmbiguousObjectException | GitAPIException e) { + play.Logger.error(e.getMessage()); + } catch (IOException e){ + play.Logger.error(e.getMessage()); + } finally{ + ArchiveCommand.unregisterFormat("zip"); + } + } } diff --git a/app/playRepository/PlayRepository.java b/app/playRepository/PlayRepository.java index 7ae4840b2..ace4aa981 100644 --- a/app/playRepository/PlayRepository.java +++ b/app/playRepository/PlayRepository.java @@ -24,6 +24,7 @@ import models.resource.Resource; import org.eclipse.jgit.api.errors.GitAPIException; import org.tmatesoft.svn.core.SVNException; +import play.mvc.Results.Chunks; import java.io.File; import java.io.IOException; @@ -79,4 +80,6 @@ public interface PlayRepository { boolean move(String srcProjectOwner, String srcProjectName, String desrProjectOwner, String destProjectName); public File getDirectory(); + + public void getArchive(Chunks.Out out, String branchName); } diff --git a/app/playRepository/SVNRepository.java b/app/playRepository/SVNRepository.java index 197442694..dd77b2162 100644 --- a/app/playRepository/SVNRepository.java +++ b/app/playRepository/SVNRepository.java @@ -39,6 +39,7 @@ import org.tmatesoft.svn.core.wc.SVNDiffClient; import org.tmatesoft.svn.core.wc.SVNRevision; import play.libs.Json; +import play.mvc.Results; import utils.Config; import utils.FileUtil; import utils.GravatarUtil; @@ -413,6 +414,11 @@ public File getDirectory() { return new File(getRootDirectory(), ownerName + "/" + projectName); } + @Override + public void getArchive(Results.Chunks.Out out, String branchName) { + + } + public static File getRootDirectory() { return new File(Config.getYobiHome(), getRepoPrefix()); } diff --git a/app/utils/ChunkedOutputStream.java b/app/utils/ChunkedOutputStream.java new file mode 100644 index 000000000..60c25fe58 --- /dev/null +++ b/app/utils/ChunkedOutputStream.java @@ -0,0 +1,114 @@ +/** + * Yona, 21st Century Project Hosting SW + *

+ * Copyright Yona & Yobi Authors & NAVER Corp. + * https://yona.io + **/ + +package utils; + +import play.mvc.Results.Chunks; + +import java.io.IOException; +import java.io.OutputStream; + +// +// ChunkedOutputStream is made by referring to BufferedOutputStream.java +// +public class ChunkedOutputStream extends OutputStream { + + Chunks.Out out; + /** + * The internal buffer where data is stored. + */ + protected byte buf[]; + + /** + * The number of valid bytes in the buffer. This value is always + * in the range 0 through buf.length; elements + * buf[0] through buf[count-1] contain valid + * byte data. + */ + protected int count; + + public ChunkedOutputStream(Chunks.Out out, int size) { + if (size <= 0) { + buf = new byte[16384]; + } else { + buf = new byte[size]; + } + this.out = out; + } + + /** + * Writes the specified byte to this buffered output stream. + * + * @param b the byte to be written. + * @exception IOException if an I/O error occurs. + */ + @Override + public synchronized void write(int b) throws IOException { + if (count >= buf.length) { + flushBuffer(); + } + buf[count++] = (byte)b; + } + + public void write(byte b[]) throws IOException { + throw new UnsupportedOperationException("write(byte b[])"); + } + + /** + * Writes len bytes from the specified byte array + * starting at offset off to this buffered output stream. + * + *

Ordinarily this method stores bytes from the given array into this + * stream's buffer, flushing the buffer to the underlying output stream as + * needed. If the requested length is at least as large as this stream's + * buffer, however, then this method will flush the buffer and write the + * bytes directly to the underlying output stream. Thus redundant + * BufferedOutputStreams will not copy data unnecessarily. + * + * @param b the data. + * @param off the start offset in the data. + * @param len the number of bytes to write. + * @exception IOException if an I/O error occurs. + */ + @Override + public synchronized void write(byte b[], int off, int len) throws IOException { + if (len >= buf.length) { + /* If the request length exceeds the size of the output buffer, + flush the output buffer and then write the data directly. + In this way buffered streams will cascade harmlessly. */ + flushBuffer(); + write(b, off, len); + return; + } + if (len > buf.length - count) { + flushBuffer(); + } + System.arraycopy(b, off, buf, count, len); + count += len; + } + + private void flushBuffer() throws IOException { + if (count > 0) { + chunkOut(); + } + } + + @Override + public void close() throws IOException { + if (count > 0) { + chunkOut(); + } + out.close(); + } + + private void chunkOut() { + byte remainBuf[] = new byte[count]; + System.arraycopy(buf, 0, remainBuf,0, count); + out.write(remainBuf); + count = 0; + } +} diff --git a/app/views/code/view.scala.html b/app/views/code/view.scala.html index 4bced0229..9055418ca 100644 --- a/app/views/code/view.scala.html +++ b/app/views/code/view.scala.html @@ -89,6 +89,12 @@ @project.name @makeBreadCrumbs(path) + @if(project.isGit) { +

+ }
diff --git a/build.sbt b/build.sbt index 5ab8cc2c9..092ccc288 100644 --- a/build.sbt +++ b/build.sbt @@ -22,6 +22,8 @@ libraryDependencies ++= Seq( "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.5.0.201609210915-r", // JGit Large File Storage "org.eclipse.jgit" % "org.eclipse.jgit.lfs" % "4.5.0.201609210915-r", + // JGit Archive Formats + "org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.5.0.201609210915-r", // svnkit "org.tmatesoft.svnkit" % "svnkit" % "1.8.12", // svnkit-dav @@ -68,7 +70,6 @@ val projectSettings = Seq( includeFilter in (Assets, LessKeys.less) := "*.less", excludeFilter in (Assets, LessKeys.less) := "_*.less", javaOptions in test ++= Seq("-Xmx2g", "-Xms1g", "-XX:MaxPermSize=1g", "-Dfile.encoding=UTF-8"), - javacOptions ++= Seq("-Xlint:all", "-Xlint:-path"), scalacOptions ++= Seq("-feature") ) diff --git a/conf/messages b/conf/messages index 666a9e915..b4739c7c5 100644 --- a/conf/messages +++ b/conf/messages @@ -95,6 +95,7 @@ code.copyCommitId.copied = Commit ID is copied code.copyUrl = Copy URL code.copyUrl.copied = URL is copied code.deletedPath = {0} (deleted) +code.download = Download as .zip file code.eolMissing = No newline at end of file code.fileDiffLimitExceeded = Up to {0} files will be displayed. code.fileModeChanged = File mode has changed diff --git a/conf/messages.ko-KR b/conf/messages.ko-KR index 59a47aa04..91d2cd074 100644 --- a/conf/messages.ko-KR +++ b/conf/messages.ko-KR @@ -95,6 +95,7 @@ code.copyCommitId.copied = 커밋 ID가 복사되었습니다 code.copyUrl = 주소 복사 code.copyUrl.copied = 주소가 복사 되었습니다. code.deletedPath = {0} (삭제됨) +code.download = 압축 파일로 내려받기 code.eolMissing = 파일 끝에 줄바꿈 문자 없음 code.fileDiffLimitExceeded = 최대 {0}개의 파일까지만 보여드립니다. code.fileModeChanged = 파일 모드 변경됨 diff --git a/conf/routes b/conf/routes index 0ee461648..8ee3c653d 100644 --- a/conf/routes +++ b/conf/routes @@ -262,6 +262,7 @@ GET /:user/:project/code/:branch/!/*path #for normal GET /:user/:project/code controllers.CodeApp.codeBrowser(user, project) GET /:user/:project/code/:branch controllers.CodeApp.codeBrowserWithBranch(user, project, branch:String, path="") +GET /:user/:project/code/:branch/download controllers.CodeApp.download(user, project, branch:String, path="") GET /:user/:project/code/:branch/*path controllers.CodeApp.codeBrowserWithBranch(user, project, branch:String, path:String) GET /:user/:project/rawcode/:rev/*path controllers.CodeApp.showRawFile(user, project, rev:String, path:String) GET /:user/:project/files/:rev/*path controllers.CodeApp.openFile(user, project, rev:String, path:String)