Simplify walker transport ref advertisement setup
[egit/zawir.git] / org.spearce.jgit / src / org / spearce / jgit / transport / TransportSftp.java
blobc2f34f768b7f86d950313557decd0c31291ffdb2
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.Map;
52 import java.util.TreeMap;
54 import org.spearce.jgit.errors.TransportException;
55 import org.spearce.jgit.lib.ObjectId;
56 import org.spearce.jgit.lib.Ref;
57 import org.spearce.jgit.lib.Repository;
58 import org.spearce.jgit.lib.Ref.Storage;
60 import com.jcraft.jsch.Channel;
61 import com.jcraft.jsch.ChannelSftp;
62 import com.jcraft.jsch.JSchException;
63 import com.jcraft.jsch.Session;
64 import com.jcraft.jsch.SftpATTRS;
65 import com.jcraft.jsch.SftpException;
67 /**
68 * Transport over the non-Git aware SFTP (SSH based FTP) protocol.
69 * <p>
70 * The SFTP transport does not require any specialized Git support on the remote
71 * (server side) repository. Object files are retrieved directly through secure
72 * shell's FTP protocol, making it possible to copy objects from a remote
73 * repository that is available over SSH, but whose remote host does not have
74 * Git installed.
75 * <p>
76 * Unlike the HTTP variant (see {@link TransportHttp}) we rely upon being able
77 * to list files in directories, as the SFTP protocol supports this function. By
78 * listing files through SFTP we can avoid needing to have current
79 * <code>objects/info/packs</code> or <code>info/refs</code> files on the
80 * remote repository and access the data directly, much as Git itself would.
82 * @see WalkFetchConnection
84 class TransportSftp extends WalkTransport {
85 static boolean canHandle(final URIish uri) {
86 return uri.isRemote() && "sftp".equals(uri.getScheme());
89 final SshSessionFactory sch;
91 TransportSftp(final Repository local, final URIish uri) {
92 super(local, uri);
93 sch = SshSessionFactory.getInstance();
96 @Override
97 public FetchConnection openFetch() throws TransportException {
98 final SftpObjectDB c = new SftpObjectDB(uri.getPath());
99 final WalkFetchConnection r = new WalkFetchConnection(this, c);
100 r.available(c.readAdvertisedRefs());
101 return r;
104 Session openSession() throws TransportException {
105 final String user = uri.getUser();
106 final String pass = uri.getPass();
107 final String host = uri.getHost();
108 final int port = uri.getPort();
109 try {
110 final Session session;
111 session = sch.getSession(user, pass, host, port);
112 if (!session.isConnected())
113 session.connect();
114 return session;
115 } catch (JSchException je) {
116 final Throwable c = je.getCause();
117 if (c instanceof UnknownHostException)
118 throw new TransportException(uri, "unknown host");
119 if (c instanceof ConnectException)
120 throw new TransportException(uri, c.getMessage());
121 throw new TransportException(uri, je.getMessage(), je);
125 ChannelSftp open(final Session sock) throws TransportException {
126 try {
127 final Channel channel = sock.openChannel("sftp");
128 channel.connect();
129 return (ChannelSftp) channel;
130 } catch (JSchException je) {
131 throw new TransportException(uri, 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 Map<String, Ref> readAdvertisedRefs() throws TransportException {
250 final TreeMap<String, Ref> avail = new TreeMap<String, Ref>();
251 try {
252 final BufferedReader br = openReader("../packed-refs");
253 try {
254 readPackedRefs(avail, br);
255 } finally {
256 br.close();
258 } catch (FileNotFoundException notPacked) {
259 // Perhaps it wasn't worthwhile, or is just an older repository.
260 } catch (IOException e) {
261 throw new TransportException(uri, "error in packed-refs", e);
263 readRef(avail, "../HEAD", "HEAD");
264 readLooseRefs(avail, "../refs", "refs/");
265 return avail;
268 private void readPackedRefs(final TreeMap<String, Ref> avail,
269 final BufferedReader br) throws IOException {
270 Ref last = null;
271 for (;;) {
272 String line = br.readLine();
273 if (line == null)
274 break;
275 if (line.charAt(0) == '#')
276 continue;
277 if (line.charAt(0) == '^') {
278 if (last == null)
279 throw new TransportException("Peeled line before ref.");
280 final ObjectId id = ObjectId.fromString(line + 1);
281 last = new Ref(Ref.Storage.PACKED, last.getName(), last
282 .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(Ref.Storage.PACKED, 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(loose(r), name, r.getObjectId(), r
348 .getPeeledObjectId());
349 avail.put(name, r);
351 return r;
354 if (ObjectId.isId(line)) {
355 final Ref r = new Ref(loose(avail.get(name)), name, ObjectId
356 .fromString(line));
357 avail.put(r.getName(), r);
358 return r;
361 throw new TransportException("Bad ref: " + name + ": " + line);
364 private Storage loose(final Ref r) {
365 if (r != null && r.getStorage() == Storage.PACKED)
366 return Storage.LOOSE_PACKED;
367 return Storage.LOOSE;
370 @Override
371 void close() {
372 if (ftp != null) {
373 try {
374 if (ftp.isConnected())
375 ftp.disconnect();
376 } finally {
377 ftp = null;
381 if (sessionOwner && session != null) {
382 try {
383 sch.releaseSession(session);
384 } finally {
385 session = null;