summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/sshd/commands/UploadArchive.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/sshd/commands/UploadArchive.java')
-rw-r--r--java/com/google/gerrit/sshd/commands/UploadArchive.java266
1 files changed, 266 insertions, 0 deletions
diff --git a/java/com/google/gerrit/sshd/commands/UploadArchive.java b/java/com/google/gerrit/sshd/commands/UploadArchive.java
new file mode 100644
index 0000000000..ac914a5268
--- /dev/null
+++ b/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -0,0 +1,266 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.server.change.ArchiveFormat;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.restapi.change.AllowedFormats;
+import com.google.gerrit.server.restapi.project.CommitsCollection;
+import com.google.gerrit.sshd.AbstractGitCommand;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.api.ArchiveCommand;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PacketLineIn;
+import org.eclipse.jgit.transport.PacketLineOut;
+import org.eclipse.jgit.transport.SideBandOutputStream;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.Option;
+import org.kohsuke.args4j.ParserProperties;
+
+/** Allows getting archives for Git repositories over SSH using the Git upload-archive protocol. */
+public class UploadArchive extends AbstractGitCommand {
+ /**
+ * Options for parsing Git commands.
+ *
+ * <p>These options are not passed on command line, but received through input stream in pkt-line
+ * format.
+ */
+ static class Options {
+ @Option(
+ name = "-f",
+ aliases = {"--format"},
+ usage =
+ "Format of the"
+ + " resulting archive: tar or zip... If this option is not given, and"
+ + " the output file is specified, the format is inferred from the"
+ + " filename if possible (e.g. writing to \"foo.zip\" makes the output"
+ + " to be in the zip format). Otherwise the output format is tar.")
+ private String format = "tar";
+
+ @Option(name = "--prefix", usage = "Prepend <prefix>/ to each filename in the archive.")
+ private String prefix;
+
+ @Option(name = "-0", usage = "Store the files instead of deflating them.")
+ private boolean level0;
+
+ @Option(name = "-1")
+ private boolean level1;
+
+ @Option(name = "-2")
+ private boolean level2;
+
+ @Option(name = "-3")
+ private boolean level3;
+
+ @Option(name = "-4")
+ private boolean level4;
+
+ @Option(name = "-5")
+ private boolean level5;
+
+ @Option(name = "-6")
+ private boolean level6;
+
+ @Option(name = "-7")
+ private boolean level7;
+
+ @Option(name = "-8")
+ private boolean level8;
+
+ @Option(
+ name = "-9",
+ usage =
+ "Highest and slowest compression level. You "
+ + "can specify any number from 1 to 9 to adjust compression speed and "
+ + "ratio.")
+ private boolean level9;
+
+ @Argument(index = 0, required = true, usage = "The tree or commit to produce an archive for.")
+ private String treeIsh = "master";
+
+ @Argument(
+ index = 1,
+ multiValued = true,
+ usage =
+ "Without an optional path parameter, all files and subdirectories of "
+ + "the current working directory are included in the archive. If one "
+ + "or more paths are specified, only these are included.")
+ private List<String> path;
+ }
+
+ @Inject private PermissionBackend permissionBackend;
+ @Inject private CommitsCollection commits;
+ @Inject private AllowedFormats allowedFormats;
+ @Inject private ProjectCache projectCache;
+ private Options options = new Options();
+
+ /**
+ * Read and parse arguments from input stream. This method gets the arguments from input stream,
+ * in Pkt-line format, then parses them to fill the options object.
+ */
+ protected void readArguments() throws IOException, Failure {
+ String argCmd = "argument ";
+ List<String> args = new ArrayList<>();
+
+ // Read arguments in Pkt-Line format
+ PacketLineIn packetIn = new PacketLineIn(in);
+ for (; ; ) {
+ String s = packetIn.readString();
+ if (s == PacketLineIn.END) {
+ break;
+ }
+ if (!s.startsWith(argCmd)) {
+ throw new Failure(1, "fatal: 'argument' token or flush expected, got " + s);
+ }
+ for (String p : Splitter.on('=').limit(2).split(s.substring(argCmd.length()))) {
+ args.add(p);
+ }
+ }
+
+ try {
+ // Parse them into the 'options' field
+ CmdLineParser parser =
+ new CmdLineParser(options, ParserProperties.defaults().withAtSyntax(false));
+ parser.parseArgument(args);
+ if (options.path == null || Arrays.asList(".").equals(options.path)) {
+ options.path = Collections.emptyList();
+ }
+ } catch (CmdLineException e) {
+ throw new Failure(2, "fatal: unable to parse arguments, " + e);
+ }
+ }
+
+ @Override
+ protected void runImpl() throws IOException, PermissionBackendException, Failure {
+ PacketLineOut packetOut = new PacketLineOut(out);
+ packetOut.setFlushOnEnd(true);
+ packetOut.writeString("ACK");
+ packetOut.end();
+
+ try {
+ // Parse Git arguments
+ readArguments();
+
+ ArchiveFormat f = allowedFormats.getExtensions().get("." + options.format);
+ if (f == null) {
+ throw new Failure(3, "fatal: upload-archive not permitted for format " + options.format);
+ }
+
+ // Find out the object to get from the specified reference and paths
+ ObjectId treeId = repo.resolve(options.treeIsh);
+ if (treeId == null) {
+ throw new Failure(4, "fatal: reference not found: " + options.treeIsh);
+ }
+
+ // Verify the user has permissions to read the specified tree.
+ if (!canRead(treeId)) {
+ throw new Failure(5, "fatal: no permission to read tree" + options.treeIsh);
+ }
+
+ // The archive is sent in DATA sideband channel
+ try (SideBandOutputStream sidebandOut =
+ new SideBandOutputStream(
+ SideBandOutputStream.CH_DATA, SideBandOutputStream.MAX_BUF, out)) {
+ new ArchiveCommand(repo)
+ .setFormat(f.name())
+ .setFormatOptions(getFormatOptions(f))
+ .setTree(treeId)
+ .setPaths(options.path.toArray(new String[0]))
+ .setPrefix(options.prefix)
+ .setOutputStream(sidebandOut)
+ .call();
+ sidebandOut.flush();
+ } catch (GitAPIException e) {
+ throw new Failure(7, "fatal: git api exception, " + e);
+ }
+ } catch (Throwable t) {
+ // Report the error in ERROR sideband channel. Catch Throwable too so we can also catch
+ // NoClassDefFound.
+ try (SideBandOutputStream sidebandError =
+ new SideBandOutputStream(
+ SideBandOutputStream.CH_ERROR, SideBandOutputStream.MAX_BUF, out)) {
+ sidebandError.write(t.getMessage().getBytes(UTF_8));
+ sidebandError.flush();
+ }
+ throw t;
+ } finally {
+ // In any case, cleanly close the packetOut channel
+ packetOut.end();
+ }
+ }
+
+ private Map<String, Object> getFormatOptions(ArchiveFormat f) {
+ if (f == ArchiveFormat.ZIP) {
+ int value =
+ Arrays.asList(
+ options.level0,
+ options.level1,
+ options.level2,
+ options.level3,
+ options.level4,
+ options.level5,
+ options.level6,
+ options.level7,
+ options.level8,
+ options.level9)
+ .indexOf(true);
+ if (value >= 0) {
+ return ImmutableMap.<String, Object>of("level", Integer.valueOf(value));
+ }
+ }
+ return Collections.emptyMap();
+ }
+
+ private boolean canRead(ObjectId revId) throws IOException, PermissionBackendException {
+ ProjectState projectState = projectCache.get(projectName);
+ requireNonNull(projectState, () -> String.format("Failed to load project %s", projectName));
+
+ if (!projectState.statePermitsRead()) {
+ return false;
+ }
+
+ try {
+ permissionBackend.user(user).project(projectName).check(ProjectPermission.READ);
+ return true;
+ } catch (AuthException e) {
+ // Check reachability of the specific revision.
+ try (RevWalk rw = new RevWalk(repo)) {
+ RevCommit commit = rw.parseCommit(revId);
+ return commits.canRead(projectState, repo, commit);
+ }
+ }
+ }
+}