Switch jgit library to the EDL (3-clause BSD)
[egit/zawir.git] / org.spearce.jgit / src / org / spearce / jgit / transport / TransportSftp.java
blobcfe22c13b43da5be4e33d85cbf0c7ad1205c0c6b
1 /*
2 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
4 * All rights reserved.
6 * Redistribution and use in source and binary forms, with or
7 * without modification, are permitted provided that the following
8 * conditions are met:
10 * - Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
13 * - Redistributions in binary form must reproduce the above
14 * copyright notice, this list of conditions and the following
15 * disclaimer in the documentation and/or other materials provided
16 * with the distribution.
18 * - Neither the name of the Git Development Community nor the
19 * names of its contributors may be used to endorse or promote
20 * products derived from this software without specific prior
21 * written permission.
23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
24 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
25 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
26 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
28 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
30 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
31 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
33 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
34 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
35 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38 package org.spearce.jgit.transport;
40 import java.io.BufferedReader;
41 import java.io.FileNotFoundException;
42 import java.io.IOException;
43 import java.net.ConnectException;
44 import java.net.UnknownHostException;
45 import java.util.ArrayList;
46 import java.util.Collection;
47 import java.util.Collections;
48 import java.util.Comparator;
49 import java.util.HashMap;
50 import java.util.List;
51 import java.util.TreeMap;
53 import org.spearce.jgit.errors.TransportException;
54 import org.spearce.jgit.lib.ObjectId;
55 import org.spearce.jgit.lib.Ref;
56 import org.spearce.jgit.lib.Repository;
58 import com.jcraft.jsch.Channel;
59 import com.jcraft.jsch.ChannelSftp;
60 import com.jcraft.jsch.JSchException;
61 import com.jcraft.jsch.Session;
62 import com.jcraft.jsch.SftpATTRS;
63 import com.jcraft.jsch.SftpException;
65 /**
66 * Transport over the non-Git aware SFTP (SSH based FTP) protocol.
67 * <p>
68 * The SFTP transport does not require any specialized Git support on the remote
69 * (server side) repository. Object files are retrieved directly through secure
70 * shell's FTP protocol, making it possible to copy objects from a remote
71 * repository that is available over SSH, but whose remote host does not have
72 * Git installed.
73 * <p>
74 * Unlike the HTTP variant (see {@link TransportHttp}) we rely upon being able
75 * to list files in directories, as the SFTP protocol supports this function. By
76 * listing files through SFTP we can avoid needing to have current
77 * <code>objects/info/packs</code> or <code>info/refs</code> files on the
78 * remote repository and access the data directly, much as Git itself would.
80 * @see WalkFetchConnection
82 class TransportSftp extends WalkTransport {
83 static boolean canHandle(final URIish uri) {
84 return uri.isRemote() && "sftp".equals(uri.getScheme());
87 final SshSessionFactory sch;
89 TransportSftp(final Repository local, final URIish uri) {
90 super(local, uri);
91 sch = SshSessionFactory.getInstance();
94 @Override
95 public FetchConnection openFetch() throws TransportException {
96 final SftpObjectDB c = new SftpObjectDB(uri.getPath());
97 final WalkFetchConnection r = new WalkFetchConnection(this, c);
98 c.readAdvertisedRefs(r);
99 return r;
102 Session openSession() throws TransportException {
103 final String user = uri.getUser();
104 final String pass = uri.getPass();
105 final String host = uri.getHost();
106 final int port = uri.getPort();
107 try {
108 final Session session;
109 session = sch.getSession(user, pass, host, port);
110 if (!session.isConnected())
111 session.connect();
112 return session;
113 } catch (JSchException je) {
114 final String us = uri.toString();
115 final Throwable c = je.getCause();
116 if (c instanceof UnknownHostException)
117 throw new TransportException(us + ": Unknown host");
118 if (c instanceof ConnectException)
119 throw new TransportException(us + ": " + c.getMessage());
120 throw new TransportException(us + ": " + je.getMessage(), je);
124 ChannelSftp open(final Session sock) throws TransportException {
125 try {
126 final Channel channel = sock.openChannel("sftp");
127 channel.connect();
128 return (ChannelSftp) channel;
129 } catch (JSchException je) {
130 throw new TransportException(uri.toString() + ": "
131 + je.getMessage(), je);
135 class SftpObjectDB extends WalkRemoteObjectDatabase {
136 private final String objectsPath;
138 private final boolean sessionOwner;
140 private Session session;
142 private ChannelSftp ftp;
144 SftpObjectDB(String path) throws TransportException {
145 if (path.startsWith("/~"))
146 path = path.substring(1);
147 if (path.startsWith("~/"))
148 path = path.substring(2);
149 try {
150 session = openSession();
151 sessionOwner = true;
152 ftp = TransportSftp.this.open(session);
153 ftp.cd(path);
154 ftp.cd("objects");
155 objectsPath = ftp.pwd();
156 } catch (TransportException err) {
157 close();
158 throw err;
159 } catch (SftpException je) {
160 throw new TransportException("Can't enter " + path + "/objects"
161 + ": " + je.getMessage(), je);
165 SftpObjectDB(final SftpObjectDB parent, final String p)
166 throws TransportException {
167 sessionOwner = false;
168 session = parent.session;
169 try {
170 ftp = TransportSftp.this.open(session);
171 ftp.cd(parent.objectsPath);
172 ftp.cd(p);
173 objectsPath = ftp.pwd();
174 } catch (TransportException err) {
175 close();
176 throw err;
177 } catch (SftpException je) {
178 throw new TransportException("Can't enter " + p + " from "
179 + parent.objectsPath + ": " + je.getMessage(), je);
183 @Override
184 Collection<WalkRemoteObjectDatabase> getAlternates() throws IOException {
185 try {
186 return readAlternates(INFO_ALTERNATES);
187 } catch (FileNotFoundException err) {
188 return null;
192 @Override
193 WalkRemoteObjectDatabase openAlternate(final String location)
194 throws IOException {
195 return new SftpObjectDB(this, location);
198 @Override
199 Collection<String> getPackNames() throws IOException {
200 final List<String> packs = new ArrayList<String>();
201 try {
202 final Collection<ChannelSftp.LsEntry> list = ftp.ls("pack");
203 final HashMap<String, ChannelSftp.LsEntry> files;
204 final HashMap<String, Integer> mtimes;
206 files = new HashMap<String, ChannelSftp.LsEntry>();
207 mtimes = new HashMap<String, Integer>();
209 for (final ChannelSftp.LsEntry ent : list)
210 files.put(ent.getFilename(), ent);
211 for (final ChannelSftp.LsEntry ent : list) {
212 final String n = ent.getFilename();
213 if (!n.startsWith("pack-") || !n.endsWith(".pack"))
214 continue;
216 final String in = n.substring(0, n.length() - 5) + ".idx";
217 if (!files.containsKey(in))
218 continue;
220 mtimes.put(n, ent.getAttrs().getMTime());
221 packs.add(n);
224 Collections.sort(packs, new Comparator<String>() {
225 public int compare(final String o1, final String o2) {
226 return mtimes.get(o2) - mtimes.get(o1);
229 } catch (SftpException je) {
230 throw new TransportException("Can't ls " + objectsPath
231 + "/pack: " + je.getMessage(), je);
233 return packs;
236 @Override
237 FileStream open(final String path) throws IOException {
238 try {
239 final SftpATTRS a = ftp.lstat(path);
240 return new FileStream(ftp.get(path), a.getSize());
241 } catch (SftpException je) {
242 if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
243 throw new FileNotFoundException(path);
244 throw new TransportException("Can't get " + objectsPath + "/"
245 + path + ": " + je.getMessage(), je);
249 void readAdvertisedRefs(final WalkFetchConnection connection)
250 throws TransportException {
251 final TreeMap<String, Ref> avail = new TreeMap<String, Ref>();
252 try {
253 final BufferedReader br = openReader("../packed-refs");
254 try {
255 readPackedRefs(avail, br);
256 } finally {
257 br.close();
259 } catch (FileNotFoundException notPacked) {
260 // Perhaps it wasn't worthwhile, or is just an older repository.
261 } catch (IOException e) {
262 throw new TransportException(uri + ": error in packed-refs", e);
264 readRef(avail, "../HEAD", "HEAD");
265 readLooseRefs(avail, "../refs", "refs/");
266 connection.available(avail);
269 private void readPackedRefs(final TreeMap<String, Ref> avail,
270 final BufferedReader br) throws IOException {
271 Ref last = null;
272 for (;;) {
273 String line = br.readLine();
274 if (line == null)
275 break;
276 if (line.charAt(0) == '#')
277 continue;
278 if (line.charAt(0) == '^') {
279 if (last == null)
280 throw new TransportException("Peeled line before ref.");
281 final ObjectId id = ObjectId.fromString(line + 1);
282 last = new Ref(last.getName(), last.getObjectId(), id);
283 avail.put(last.getName(), last);
284 continue;
287 final int sp = line.indexOf(' ');
288 if (sp < 0)
289 throw new TransportException("Unrecognized ref: " + line);
290 final ObjectId id = ObjectId.fromString(line.substring(0, sp));
291 final String name = line.substring(sp + 1);
292 last = new Ref(name, id);
293 avail.put(last.getName(), last);
297 private void readLooseRefs(final TreeMap<String, Ref> avail,
298 final String dir, final String prefix)
299 throws TransportException {
300 final Collection<ChannelSftp.LsEntry> list;
301 try {
302 list = ftp.ls(dir);
303 } catch (SftpException je) {
304 throw new TransportException("Can't ls " + objectsPath + "/"
305 + dir + ": " + je.getMessage(), je);
308 for (final ChannelSftp.LsEntry ent : list) {
309 final String n = ent.getFilename();
310 if (".".equals(n) || "..".equals(n))
311 continue;
313 final String nPath = dir + "/" + n;
314 if (ent.getAttrs().isDir())
315 readLooseRefs(avail, nPath, prefix + n + "/");
316 else
317 readRef(avail, nPath, prefix + n);
321 private Ref readRef(final TreeMap<String, Ref> avail,
322 final String path, final String name) throws TransportException {
323 final String line;
324 try {
325 final BufferedReader br = openReader(path);
326 try {
327 line = br.readLine();
328 } finally {
329 br.close();
331 } catch (FileNotFoundException noRef) {
332 return null;
333 } catch (IOException err) {
334 throw new TransportException("Cannot read " + objectsPath + "/"
335 + path + ": " + err.getMessage(), err);
338 if (line == null)
339 throw new TransportException("Empty ref: " + name);
341 if (line.startsWith("ref: ")) {
342 final String p = line.substring("ref: ".length());
343 Ref r = readRef(avail, "../" + p, p);
344 if (r == null)
345 r = avail.get(p);
346 if (r != null) {
347 r = new Ref(name, r.getObjectId(), r.getPeeledObjectId());
348 avail.put(name, r);
350 return r;
353 if (ObjectId.isId(line)) {
354 final Ref r = new Ref(name, ObjectId.fromString(line));
355 avail.put(r.getName(), r);
356 return r;
359 throw new TransportException("Bad ref: " + name + ": " + line);
362 @Override
363 void close() {
364 if (ftp != null) {
365 try {
366 if (ftp.isConnected())
367 ftp.disconnect();
368 } finally {
369 ftp = null;
373 if (sessionOwner && session != null) {
374 try {
375 sch.releaseSession(session);
376 } finally {
377 session = null;