git: Support source code download as zip file
@8afa58beb7c858bf83231c4657ee9e8a4d33c841
--- app/controllers/CodeApp.java
+++ app/controllers/CodeApp.java
... | ... | @@ -21,6 +21,7 @@ |
21 | 21 |
package controllers; |
22 | 22 |
|
23 | 23 |
import actions.DefaultProjectCheckAction; |
24 |
+import com.fasterxml.jackson.databind.node.ObjectNode; |
|
24 | 25 |
import controllers.annotation.AnonymousCheck; |
25 | 26 |
import controllers.annotation.IsAllowed; |
26 | 27 |
import models.Project; |
... | ... | @@ -28,10 +29,16 @@ |
28 | 29 |
import org.apache.commons.io.FilenameUtils; |
29 | 30 |
import org.apache.tika.Tika; |
30 | 31 |
import org.apache.tika.mime.MediaType; |
31 |
-import com.fasterxml.jackson.databind.node.ObjectNode; |
|
32 |
+import org.eclipse.jgit.api.ArchiveCommand; |
|
33 |
+import org.eclipse.jgit.api.Git; |
|
32 | 34 |
import org.eclipse.jgit.api.errors.GitAPIException; |
35 |
+import org.eclipse.jgit.archive.ZipFormat; |
|
33 | 36 |
import org.tmatesoft.svn.core.SVNException; |
34 |
-import play.mvc.*; |
|
37 |
+import play.mvc.Controller; |
|
38 |
+import play.mvc.Http; |
|
39 |
+import play.mvc.Result; |
|
40 |
+import play.mvc.With; |
|
41 |
+import playRepository.GitRepository; |
|
35 | 42 |
import playRepository.PlayRepository; |
36 | 43 |
import playRepository.RepositoryService; |
37 | 44 |
import utils.ErrorViews; |
... | ... | @@ -119,6 +126,38 @@ |
119 | 126 |
} |
120 | 127 |
|
121 | 128 |
@With(DefaultProjectCheckAction.class) |
129 |
+ public static Result download(String userName, String projectName, String branch, String path) |
|
130 |
+ throws UnsupportedOperationException, IOException, SVNException, GitAPIException, ServletException { |
|
131 |
+ Project project = Project.findByOwnerAndProjectName(userName, projectName); |
|
132 |
+ |
|
133 |
+ if (!RepositoryService.VCS_GIT.equals(project.vcs) && !RepositoryService.VCS_SUBVERSION.equals(project.vcs)) { |
|
134 |
+ return status(Http.Status.NOT_IMPLEMENTED, project.vcs + " is not supported!"); |
|
135 |
+ } |
|
136 |
+ |
|
137 |
+ final String targetBranch = HttpUtil.decodePathSegment(branch); |
|
138 |
+ final String targetPath = HttpUtil.decodePathSegment(path); |
|
139 |
+ |
|
140 |
+ PlayRepository repository = RepositoryService.getRepository(project); |
|
141 |
+ List<ObjectNode> recursiveData = RepositoryService.getMetaDataFromAncestorDirectories( |
|
142 |
+ repository, targetBranch, targetPath); |
|
143 |
+ |
|
144 |
+ if (recursiveData == null) { |
|
145 |
+ return notFound(ErrorViews.NotFound.render()); |
|
146 |
+ } |
|
147 |
+ |
|
148 |
+ // Prepare a chunked text stream |
|
149 |
+ Chunks<byte[]> chunks = new ByteChunks() { |
|
150 |
+ // Called when the stream is ready |
|
151 |
+ public void onReady(Chunks.Out<byte[]> out) { |
|
152 |
+ repository.getArchive(out, targetBranch); |
|
153 |
+ } |
|
154 |
+ }; |
|
155 |
+ |
|
156 |
+ response().setHeader("Content-Disposition", "attachment; filename=" + projectName + "-" + branch + ".zip"); |
|
157 |
+ return ok(chunks); |
|
158 |
+ } |
|
159 |
+ |
|
160 |
+ @With(DefaultProjectCheckAction.class) |
|
122 | 161 |
public static Result ajaxRequestWithBranch(String userName, String projectName, String branch, String path) |
123 | 162 |
throws UnsupportedOperationException, IOException, SVNException, GitAPIException, ServletException{ |
124 | 163 |
CodeApp.hostName = request().host(); |
--- app/playRepository/GitRepository.java
+++ app/playRepository/GitRepository.java
... | ... | @@ -20,6 +20,8 @@ |
20 | 20 |
*/ |
21 | 21 |
package playRepository; |
22 | 22 |
|
23 |
+import com.fasterxml.jackson.databind.JsonNode; |
|
24 |
+import com.fasterxml.jackson.databind.node.ObjectNode; |
|
23 | 25 |
import controllers.UserApp; |
24 | 26 |
import controllers.routes; |
25 | 27 |
import models.Project; |
... | ... | @@ -33,19 +35,20 @@ |
33 | 35 |
import org.apache.commons.lang3.StringUtils; |
34 | 36 |
import org.apache.tika.Tika; |
35 | 37 |
import org.apache.tika.metadata.Metadata; |
36 |
-import com.fasterxml.jackson.databind.JsonNode; |
|
37 |
-import com.fasterxml.jackson.databind.node.ObjectNode; |
|
38 |
+import org.eclipse.jgit.api.ArchiveCommand; |
|
38 | 39 |
import org.eclipse.jgit.api.Git; |
39 | 40 |
import org.eclipse.jgit.api.LogCommand; |
40 | 41 |
import org.eclipse.jgit.api.errors.GitAPIException; |
42 |
+import org.eclipse.jgit.archive.ZipFormat; |
|
41 | 43 |
import org.eclipse.jgit.attributes.AttributesNode; |
42 | 44 |
import org.eclipse.jgit.attributes.AttributesNodeProvider; |
43 | 45 |
import org.eclipse.jgit.attributes.AttributesRule; |
44 | 46 |
import org.eclipse.jgit.blame.BlameResult; |
45 | 47 |
import org.eclipse.jgit.diff.*; |
46 | 48 |
import org.eclipse.jgit.diff.Edit.Type; |
49 |
+import org.eclipse.jgit.errors.AmbiguousObjectException; |
|
50 |
+import org.eclipse.jgit.errors.IncorrectObjectTypeException; |
|
47 | 51 |
import org.eclipse.jgit.errors.MissingObjectException; |
48 |
-import org.eclipse.jgit.internal.storage.dfs.DfsRepository; |
|
49 | 52 |
import org.eclipse.jgit.lib.*; |
50 | 53 |
import org.eclipse.jgit.lib.RefUpdate.Result; |
51 | 54 |
import org.eclipse.jgit.revwalk.RevCommit; |
... | ... | @@ -63,8 +66,9 @@ |
63 | 66 |
import org.eclipse.jgit.util.io.NullOutputStream; |
64 | 67 |
import org.tmatesoft.svn.core.SVNException; |
65 | 68 |
import play.Logger; |
66 |
-import play.api.Play; |
|
67 | 69 |
import play.libs.Json; |
70 |
+import play.mvc.Results.Chunks; |
|
71 |
+import utils.ChunkedOutputStream; |
|
68 | 72 |
import utils.FileUtil; |
69 | 73 |
import utils.GravatarUtil; |
70 | 74 |
|
... | ... | @@ -1940,4 +1944,23 @@ |
1940 | 1944 |
public Repository getRepository() { |
1941 | 1945 |
return repository; |
1942 | 1946 |
} |
1947 |
+ |
|
1948 |
+ public void getArchive(Chunks.Out<byte[]> out, String branchName){ |
|
1949 |
+ Git git = new Git(getRepository()); |
|
1950 |
+ ArchiveCommand.registerFormat("zip", new ZipFormat()); |
|
1951 |
+ try { |
|
1952 |
+ ChunkedOutputStream cos = new ChunkedOutputStream(out, 16384); |
|
1953 |
+ git.archive() |
|
1954 |
+ .setTree(getRepository().resolve(branchName)) |
|
1955 |
+ .setFormat("zip") |
|
1956 |
+ .setOutputStream(cos) |
|
1957 |
+ .call(); |
|
1958 |
+ } catch (IncorrectObjectTypeException | AmbiguousObjectException | GitAPIException e) { |
|
1959 |
+ play.Logger.error(e.getMessage()); |
|
1960 |
+ } catch (IOException e){ |
|
1961 |
+ play.Logger.error(e.getMessage()); |
|
1962 |
+ } finally{ |
|
1963 |
+ ArchiveCommand.unregisterFormat("zip"); |
|
1964 |
+ } |
|
1965 |
+ } |
|
1943 | 1966 |
} |
--- app/playRepository/PlayRepository.java
+++ app/playRepository/PlayRepository.java
... | ... | @@ -24,6 +24,7 @@ |
24 | 24 |
import models.resource.Resource; |
25 | 25 |
import org.eclipse.jgit.api.errors.GitAPIException; |
26 | 26 |
import org.tmatesoft.svn.core.SVNException; |
27 |
+import play.mvc.Results.Chunks; |
|
27 | 28 |
|
28 | 29 |
import java.io.File; |
29 | 30 |
import java.io.IOException; |
... | ... | @@ -79,4 +80,6 @@ |
79 | 80 |
boolean move(String srcProjectOwner, String srcProjectName, String desrProjectOwner, String destProjectName); |
80 | 81 |
|
81 | 82 |
public File getDirectory(); |
83 |
+ |
|
84 |
+ public void getArchive(Chunks.Out<byte[]> out, String branchName); |
|
82 | 85 |
} |
--- app/playRepository/SVNRepository.java
+++ app/playRepository/SVNRepository.java
... | ... | @@ -39,6 +39,7 @@ |
39 | 39 |
import org.tmatesoft.svn.core.wc.SVNDiffClient; |
40 | 40 |
import org.tmatesoft.svn.core.wc.SVNRevision; |
41 | 41 |
import play.libs.Json; |
42 |
+import play.mvc.Results; |
|
42 | 43 |
import utils.Config; |
43 | 44 |
import utils.FileUtil; |
44 | 45 |
import utils.GravatarUtil; |
... | ... | @@ -413,6 +414,11 @@ |
413 | 414 |
return new File(getRootDirectory(), ownerName + "/" + projectName); |
414 | 415 |
} |
415 | 416 |
|
417 |
+ @Override |
|
418 |
+ public void getArchive(Results.Chunks.Out<byte[]> out, String branchName) { |
|
419 |
+ |
|
420 |
+ } |
|
421 |
+ |
|
416 | 422 |
public static File getRootDirectory() { |
417 | 423 |
return new File(Config.getYobiHome(), getRepoPrefix()); |
418 | 424 |
} |
+++ app/utils/ChunkedOutputStream.java
... | ... | @@ -0,0 +1,114 @@ |
1 | +/** | |
2 | + * Yona, 21st Century Project Hosting SW | |
3 | + * <p> | |
4 | + * Copyright Yona & Yobi Authors & NAVER Corp. | |
5 | + * https://yona.io | |
6 | + **/ | |
7 | + | |
8 | +package utils; | |
9 | + | |
10 | +import play.mvc.Results.Chunks; | |
11 | + | |
12 | +import java.io.IOException; | |
13 | +import java.io.OutputStream; | |
14 | + | |
15 | +// | |
16 | +// ChunkedOutputStream is made by referring to BufferedOutputStream.java | |
17 | +// | |
18 | +public class ChunkedOutputStream extends OutputStream { | |
19 | + | |
20 | + Chunks.Out<byte[]> out; | |
21 | + /** | |
22 | + * The internal buffer where data is stored. | |
23 | + */ | |
24 | + protected byte buf[]; | |
25 | + | |
26 | + /** | |
27 | + * The number of valid bytes in the buffer. This value is always | |
28 | + * in the range <tt>0</tt> through <tt>buf.length</tt>; elements | |
29 | + * <tt>buf[0]</tt> through <tt>buf[count-1]</tt> contain valid | |
30 | + * byte data. | |
31 | + */ | |
32 | + protected int count; | |
33 | + | |
34 | + public ChunkedOutputStream(Chunks.Out<byte[]> out, int size) { | |
35 | + if (size <= 0) { | |
36 | + buf = new byte[16384]; | |
37 | + } else { | |
38 | + buf = new byte[size]; | |
39 | + } | |
40 | + this.out = out; | |
41 | + } | |
42 | + | |
43 | + /** | |
44 | + * Writes the specified byte to this buffered output stream. | |
45 | + * | |
46 | + * @param b the byte to be written. | |
47 | + * @exception IOException if an I/O error occurs. | |
48 | + */ | |
49 | + @Override | |
50 | + public synchronized void write(int b) throws IOException { | |
51 | + if (count >= buf.length) { | |
52 | + flushBuffer(); | |
53 | + } | |
54 | + buf[count++] = (byte)b; | |
55 | + } | |
56 | + | |
57 | + public void write(byte b[]) throws IOException { | |
58 | + throw new UnsupportedOperationException("write(byte b[])"); | |
59 | + } | |
60 | + | |
61 | + /** | |
62 | + * Writes <code>len</code> bytes from the specified byte array | |
63 | + * starting at offset <code>off</code> to this buffered output stream. | |
64 | + * | |
65 | + * <p> Ordinarily this method stores bytes from the given array into this | |
66 | + * stream's buffer, flushing the buffer to the underlying output stream as | |
67 | + * needed. If the requested length is at least as large as this stream's | |
68 | + * buffer, however, then this method will flush the buffer and write the | |
69 | + * bytes directly to the underlying output stream. Thus redundant | |
70 | + * <code>BufferedOutputStream</code>s will not copy data unnecessarily. | |
71 | + * | |
72 | + * @param b the data. | |
73 | + * @param off the start offset in the data. | |
74 | + * @param len the number of bytes to write. | |
75 | + * @exception IOException if an I/O error occurs. | |
76 | + */ | |
77 | + @Override | |
78 | + public synchronized void write(byte b[], int off, int len) throws IOException { | |
79 | + if (len >= buf.length) { | |
80 | + /* If the request length exceeds the size of the output buffer, | |
81 | + flush the output buffer and then write the data directly. | |
82 | + In this way buffered streams will cascade harmlessly. */ | |
83 | + flushBuffer(); | |
84 | + write(b, off, len); | |
85 | + return; | |
86 | + } | |
87 | + if (len > buf.length - count) { | |
88 | + flushBuffer(); | |
89 | + } | |
90 | + System.arraycopy(b, off, buf, count, len); | |
91 | + count += len; | |
92 | + } | |
93 | + | |
94 | + private void flushBuffer() throws IOException { | |
95 | + if (count > 0) { | |
96 | + chunkOut(); | |
97 | + } | |
98 | + } | |
99 | + | |
100 | + @Override | |
101 | + public void close() throws IOException { | |
102 | + if (count > 0) { | |
103 | + chunkOut(); | |
104 | + } | |
105 | + out.close(); | |
106 | + } | |
107 | + | |
108 | + private void chunkOut() { | |
109 | + byte remainBuf[] = new byte[count]; | |
110 | + System.arraycopy(buf, 0, remainBuf,0, count); | |
111 | + out.write(remainBuf); | |
112 | + count = 0; | |
113 | + } | |
114 | +} |
--- app/views/code/view.scala.html
+++ app/views/code/view.scala.html
... | ... | @@ -89,6 +89,12 @@ |
89 | 89 |
<a href="@routes.CodeApp.codeBrowserWithBranch(project.owner, project.name, URLEncoder.encode(branch, "UTF-8"), "")">@project.name</a> |
90 | 90 |
@makeBreadCrumbs(path) |
91 | 91 |
</div> |
92 |
+ @if(project.isGit) { |
|
93 |
+ <div class="pull-right"> |
|
94 |
+ <a href="@routes.CodeApp.download(project.owner, project.name, URLEncoder.encode(branch, "UTF-8"))" class="ybtn"> |
|
95 |
+ @Messages("code.download")</a> |
|
96 |
+ </div> |
|
97 |
+ } |
|
92 | 98 |
</div> |
93 | 99 |
|
94 | 100 |
<div class="code-viewer-wrap"> |
--- build.sbt
+++ build.sbt
... | ... | @@ -22,6 +22,8 @@ |
22 | 22 |
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.5.0.201609210915-r", |
23 | 23 |
// JGit Large File Storage |
24 | 24 |
"org.eclipse.jgit" % "org.eclipse.jgit.lfs" % "4.5.0.201609210915-r", |
25 |
+ // JGit Archive Formats |
|
26 |
+ "org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.5.0.201609210915-r", |
|
25 | 27 |
// svnkit |
26 | 28 |
"org.tmatesoft.svnkit" % "svnkit" % "1.8.12", |
27 | 29 |
// svnkit-dav |
... | ... | @@ -68,7 +70,6 @@ |
68 | 70 |
includeFilter in (Assets, LessKeys.less) := "*.less", |
69 | 71 |
excludeFilter in (Assets, LessKeys.less) := "_*.less", |
70 | 72 |
javaOptions in test ++= Seq("-Xmx2g", "-Xms1g", "-XX:MaxPermSize=1g", "-Dfile.encoding=UTF-8"), |
71 |
- javacOptions ++= Seq("-Xlint:all", "-Xlint:-path"), |
|
72 | 73 |
scalacOptions ++= Seq("-feature") |
73 | 74 |
) |
74 | 75 |
|
--- conf/messages
+++ conf/messages
... | ... | @@ -95,6 +95,7 @@ |
95 | 95 |
code.copyUrl = Copy URL |
96 | 96 |
code.copyUrl.copied = URL is copied |
97 | 97 |
code.deletedPath = {0} (deleted) |
98 |
+code.download = Download as .zip file |
|
98 | 99 |
code.eolMissing = No newline at end of file |
99 | 100 |
code.fileDiffLimitExceeded = Up to {0} files will be displayed. |
100 | 101 |
code.fileModeChanged = File mode has changed |
--- conf/messages.ko-KR
+++ conf/messages.ko-KR
... | ... | @@ -95,6 +95,7 @@ |
95 | 95 |
code.copyUrl = 주소 복사 |
96 | 96 |
code.copyUrl.copied = 주소가 복사 되었습니다. |
97 | 97 |
code.deletedPath = {0} (삭제됨) |
98 |
+code.download = 압축 파일로 내려받기 |
|
98 | 99 |
code.eolMissing = 파일 끝에 줄바꿈 문자 없음 |
99 | 100 |
code.fileDiffLimitExceeded = 최대 {0}개의 파일까지만 보여드립니다. |
100 | 101 |
code.fileModeChanged = 파일 모드 변경됨 |
--- conf/routes
+++ conf/routes
... | ... | @@ -262,6 +262,7 @@ |
262 | 262 |
#for normal |
263 | 263 |
GET /:user/:project/code controllers.CodeApp.codeBrowser(user, project) |
264 | 264 |
GET /:user/:project/code/:branch controllers.CodeApp.codeBrowserWithBranch(user, project, branch:String, path="") |
265 |
+GET /:user/:project/code/:branch/download controllers.CodeApp.download(user, project, branch:String, path="") |
|
265 | 266 |
GET /:user/:project/code/:branch/*path controllers.CodeApp.codeBrowserWithBranch(user, project, branch:String, path:String) |
266 | 267 |
GET /:user/:project/rawcode/:rev/*path controllers.CodeApp.showRawFile(user, project, rev:String, path:String) |
267 | 268 |
GET /:user/:project/files/:rev/*path controllers.CodeApp.openFile(user, project, rev:String, path:String) |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?