diff --git a/pom.xml b/pom.xml
index da4e9e1f34..b4947083cd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -268,6 +268,7 @@ limitations under the License.
log4j.properties
com/google/gerrit/client/GerritVersion.properties
+ com/google/gerrit/server/ssh/scproot/**
diff --git a/src/main/java/com/google/gerrit/server/ssh/GerritCommandFactory.java b/src/main/java/com/google/gerrit/server/ssh/GerritCommandFactory.java
index 6ef8307689..56ca1cb6b7 100644
--- a/src/main/java/com/google/gerrit/server/ssh/GerritCommandFactory.java
+++ b/src/main/java/com/google/gerrit/server/ssh/GerritCommandFactory.java
@@ -98,6 +98,8 @@ class GerritCommandFactory implements CommandFactory {
cmd += args;
args = "";
}
+ } else if ("scp".equals(cmd)) {
+ return new ScpCommand(args.split(" "));
}
final AbstractCommand c = create(cmd);
diff --git a/src/main/java/com/google/gerrit/server/ssh/ScpCommand.java b/src/main/java/com/google/gerrit/server/ssh/ScpCommand.java
new file mode 100644
index 0000000000..35bf40716d
--- /dev/null
+++ b/src/main/java/com/google/gerrit/server/ssh/ScpCommand.java
@@ -0,0 +1,335 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you 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.
+ */
+
+/*
+ * NB: This code was primarly ripped out of MINA SSHD.
+ *
+ * @author Apache MINA SSHD Project
+ */
+package com.google.gerrit.server.ssh;
+
+import org.apache.sshd.server.CommandFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.TreeMap;
+
+class ScpCommand implements CommandFactory.Command, Runnable {
+ private static final String TYPE_DIR = "D";
+ private static final String TYPE_FILE = "C";
+ private static final Logger log = LoggerFactory.getLogger(ScpCommand.class);
+
+ private boolean opt_r;
+ private boolean opt_t;
+ private boolean opt_f;
+ private boolean opt_v;
+ private boolean opt_p;
+ private String root;
+
+ private TreeMap toc;
+ private InputStream in;
+ private OutputStream out;
+ private OutputStream err;
+ private CommandFactory.ExitCallback callback;
+ private IOException error;
+
+ public ScpCommand(final String[] args) {
+ root = "";
+ for (int i = 0; i < args.length; i++) {
+ if (args[i].charAt(0) == '-') {
+ for (int j = 1; j < args[i].length(); j++) {
+ switch (args[i].charAt(j)) {
+ case 'f':
+ opt_f = true;
+ break;
+ case 'p':
+ opt_p = true;
+ break;
+ case 'r':
+ opt_r = true;
+ break;
+ case 't':
+ opt_t = true;
+ break;
+ case 'v':
+ opt_v = true;
+ break;
+ }
+ }
+ } else if (i == args.length - 1) {
+ root = args[args.length - 1];
+ }
+ }
+ if (!opt_f && !opt_t) {
+ error = new IOException("Either -f or -t option should be set");
+ }
+ }
+
+ public void setInputStream(InputStream in) {
+ this.in = in;
+ }
+
+ public void setOutputStream(OutputStream out) {
+ this.out = out;
+ }
+
+ public void setErrorStream(OutputStream err) {
+ this.err = err;
+ }
+
+ public void setExitCallback(CommandFactory.ExitCallback callback) {
+ this.callback = callback;
+ }
+
+ public void start() throws IOException {
+ if (error != null) {
+ throw error;
+ }
+ new Thread(this).start();
+ }
+
+ public void run() {
+ try {
+ readToc();
+ if (opt_f) {
+ if (root.startsWith("/")) {
+ root = root.substring(1);
+ }
+ if (root.endsWith("/")) {
+ root = root.substring(0, root.length() - 1);
+ }
+ if (root.equals(".")) {
+ root = "";
+ }
+
+ final Entry ent = toc.get(root);
+ if (ent == null) {
+ throw new IOException(root + " not found");
+
+ } else if (TYPE_FILE.equals(ent.type)) {
+ readFile(ent);
+
+ } else if (TYPE_DIR.equals(ent.type)) {
+ if (!opt_r) {
+ throw new IOException(root + " not a regular file");
+ }
+ readDir(ent);
+ } else {
+ throw new IOException(root + " not supported");
+ }
+ } else {
+ throw new IOException("Unsupported mode");
+ }
+ } catch (IOException e) {
+ try {
+ out.write(2);
+ out.write(e.getMessage().getBytes());
+ out.write('\n');
+ out.flush();
+ } catch (IOException e2) {
+ // Ignore
+ }
+ log.debug("Error in scp command", e);
+ } finally {
+ callback.onExit(0);
+ }
+ }
+
+ private void readToc() throws IOException {
+ toc = new TreeMap();
+ final BufferedReader br =
+ new BufferedReader(new InputStreamReader(new ByteArrayInputStream(
+ read("TOC")), "UTF-8"));
+ String line;
+ while ((line = br.readLine()) != null) {
+ if (line.length() > 0 && !line.startsWith("#")) {
+ final Entry e = new Entry(TYPE_FILE, line);
+ toc.put(e.path, e);
+ }
+ }
+
+ final List all = new ArrayList(toc.values());
+ for (Entry e : all) {
+ String path = dirOf(e.path);
+ while (path != null) {
+ Entry d = toc.get(path);
+ if (d == null) {
+ d = new Entry(TYPE_DIR, 0755, path);
+ toc.put(d.path, d);
+ }
+ d.children.add(e);
+ path = dirOf(path);
+ e = d;
+ }
+ }
+
+ final Entry top = new Entry(TYPE_DIR, 0755, "");
+ for (Entry e : toc.values()) {
+ if (dirOf(e.path) == null) {
+ top.children.add(e);
+ }
+ }
+ toc.put(top.path, top);
+ }
+
+ private String readLine() throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ for (;;) {
+ int c = in.read();
+ if (c == '\n') {
+ return baos.toString();
+ } else if (c == -1) {
+ throw new IOException("End of stream");
+ } else {
+ baos.write(c);
+ }
+ }
+ }
+
+ private static String nameOf(String path) {
+ final int s = path.lastIndexOf('/');
+ return s < 0 ? path : path.substring(s + 1);
+ }
+
+ private static String dirOf(String path) {
+ final int s = path.lastIndexOf('/');
+ return s < 0 ? null : path.substring(0, s);
+ }
+
+ private static byte[] read(String path) {
+ final InputStream in =
+ ScpCommand.class.getClassLoader().getResourceAsStream(
+ "com/google/gerrit/server/ssh/scproot/" + path);
+ if (in == null) {
+ return null;
+ }
+ try {
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try {
+ final byte[] buf = new byte[8192];
+ int n;
+ while ((n = in.read(buf, 0, buf.length)) > 0) {
+ out.write(buf, 0, n);
+ }
+ } finally {
+ in.close();
+ }
+ return out.toByteArray();
+ } catch (Exception e) {
+ log.debug("Cannot read " + path, e);
+ return null;
+ }
+ }
+
+ private void readFile(final Entry ent) throws IOException {
+ final byte[] data = read(ent.path);
+ if (data == null) {
+ throw new FileNotFoundException(ent.path);
+ }
+
+ header(ent, data.length);
+ readAck();
+
+ out.write(data);
+ ack();
+ readAck();
+ }
+
+ private void readDir(final Entry dir) throws IOException {
+ header(dir, 0);
+ readAck();
+
+ for (Entry e : dir.children) {
+ if (TYPE_DIR.equals(e.type)) {
+ readDir(e);
+ } else {
+ readFile(e);
+ }
+ }
+
+ out.write("E\n".getBytes("UTF-8"));
+ out.flush();
+ readAck();
+ }
+
+ private void header(final Entry dir, final int len) throws IOException,
+ UnsupportedEncodingException {
+ final StringBuilder buf = new StringBuilder();
+ buf.append(dir.type);
+ buf.append(dir.mode); // perms
+ buf.append(" ");
+ buf.append(len); // length
+ buf.append(" ");
+ buf.append(nameOf(dir.path));
+ buf.append("\n");
+ out.write(buf.toString().getBytes("UTF-8"));
+ out.flush();
+ }
+
+ private void ack() throws IOException {
+ out.write(0);
+ out.flush();
+ }
+
+ private void readAck() throws IOException {
+ switch (in.read()) {
+ case 0:
+ break;
+ case 1:
+ log.debug("Received warning: " + readLine());
+ break;
+ case 2:
+ throw new IOException("Received nack: " + readLine());
+ }
+ }
+
+ private static class Entry {
+ String type;
+ String mode;
+ String path;
+ List children;
+
+ Entry(String type, String line) {
+ this.type = type;
+ int s = line.indexOf(' ');
+ mode = line.substring(0, s);
+ path = line.substring(s + 1);
+
+ if (!mode.startsWith("0")) {
+ mode = "0" + mode;
+ }
+ }
+
+ Entry(String type, int mode, String path) {
+ this.type = type;
+ this.mode = "0" + Integer.toOctalString(mode);
+ this.path = path;
+ this.children = new ArrayList();
+ }
+ }
+}
diff --git a/src/main/java/com/google/gerrit/server/ssh/scproot/TOC b/src/main/java/com/google/gerrit/server/ssh/scproot/TOC
new file mode 100644
index 0000000000..e2c5dfac80
--- /dev/null
+++ b/src/main/java/com/google/gerrit/server/ssh/scproot/TOC
@@ -0,0 +1,3 @@
+# Format of this file is:
+# mode fullpath
+#