doortts doortts 2016-12-16
git: Support source code download as zip file
@8afa58beb7c858bf83231c4657ee9e8a4d33c841
app/controllers/CodeApp.java
--- app/controllers/CodeApp.java
+++ 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;
@@ -119,6 +126,38 @@
     }
 
     @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<ObjectNode> recursiveData = RepositoryService.getMetaDataFromAncestorDirectories(
+                repository, targetBranch, targetPath);
+
+        if (recursiveData == null) {
+            return notFound(ErrorViews.NotFound.render());
+        }
+
+        // Prepare a chunked text stream
+        Chunks<byte[]> chunks = new ByteChunks() {
+            // Called when the stream is ready
+            public void onReady(Chunks.Out<byte[]> 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{
         CodeApp.hostName = request().host();
app/playRepository/GitRepository.java
--- app/playRepository/GitRepository.java
+++ 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 Repository getRepository() {
         return repository;
     }
+
+    public void getArchive(Chunks.Out<byte[]> 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");
+        }
+    }
 }
app/playRepository/PlayRepository.java
--- app/playRepository/PlayRepository.java
+++ 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 @@
     boolean move(String srcProjectOwner, String srcProjectName, String desrProjectOwner, String destProjectName);
 
     public File getDirectory();
+
+    public void getArchive(Chunks.Out<byte[]> out, String branchName);
 }
app/playRepository/SVNRepository.java
--- app/playRepository/SVNRepository.java
+++ 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 @@
         return new File(getRootDirectory(), ownerName + "/" + projectName);
     }
 
+    @Override
+    public void getArchive(Results.Chunks.Out<byte[]> out, String branchName) {
+
+    }
+
     public static File getRootDirectory() {
         return new File(Config.getYobiHome(), getRepoPrefix());
     }
 
app/utils/ChunkedOutputStream.java (added)
+++ app/utils/ChunkedOutputStream.java
@@ -0,0 +1,114 @@
+/**
+ *  Yona, 21st Century Project Hosting SW
+ *  <p>
+ *  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<byte[]> 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 <tt>0</tt> through <tt>buf.length</tt>; elements
+     * <tt>buf[0]</tt> through <tt>buf[count-1]</tt> contain valid
+     * byte data.
+     */
+    protected int count;
+
+    public ChunkedOutputStream(Chunks.Out<byte[]> 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 <code>len</code> bytes from the specified byte array
+     * starting at offset <code>off</code> to this buffered output stream.
+     *
+     * <p> 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
+     * <code>BufferedOutputStream</code>s 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;
+    }
+}
app/views/code/view.scala.html
--- app/views/code/view.scala.html
+++ app/views/code/view.scala.html
@@ -89,6 +89,12 @@
                     <a href="@routes.CodeApp.codeBrowserWithBranch(project.owner, project.name, URLEncoder.encode(branch, "UTF-8"), "")">@project.name</a>
                     @makeBreadCrumbs(path)
                 </div>
+                @if(project.isGit) {
+                <div class="pull-right">
+                    <a href="@routes.CodeApp.download(project.owner, project.name, URLEncoder.encode(branch, "UTF-8"))" class="ybtn">
+                        @Messages("code.download")</a>
+                </div>
+                }
             </div>
 
             <div class="code-viewer-wrap">
build.sbt
--- build.sbt
+++ build.sbt
@@ -22,6 +22,8 @@
   "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 @@
   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")
 )
 
conf/messages
--- conf/messages
+++ conf/messages
@@ -95,6 +95,7 @@
 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
conf/messages.ko-KR
--- conf/messages.ko-KR
+++ conf/messages.ko-KR
@@ -95,6 +95,7 @@
 code.copyUrl = 주소 복사
 code.copyUrl.copied = 주소가 복사 되었습니다.
 code.deletedPath = {0} (삭제됨)
+code.download = 압축 파일로 내려받기
 code.eolMissing = 파일 끝에 줄바꿈 문자 없음
 code.fileDiffLimitExceeded = 최대 {0}개의 파일까지만 보여드립니다.
 code.fileModeChanged = 파일 모드 변경됨
conf/routes
--- conf/routes
+++ conf/routes
@@ -262,6 +262,7 @@
 #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)
Add a comment
List