Remember how a Ref was read in from disk and created
[egit/zawir.git] / org.spearce.jgit / src / org / spearce / jgit / transport / TransportSftp.java
blob092c5d33db93d2cc6d4c4d86e5191b83c436801c
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;
57 import org.spearce.jgit.lib.Ref.Storage;
59 import com.jcraft.jsch.Channel;
60 import com.jcraft.jsch.ChannelSftp;
61 import com.jcraft.jsch.JSchException;
62 import com.jcraft.jsch.Session;
63 import com.jcraft.jsch.SftpATTRS;
64 import com.jcraft.jsch.SftpException;
66 /**
67 * Transport over the non-Git aware SFTP (SSH based FTP) protocol.
68 * <p>
69 * The SFTP transport does not require any specialized Git support on the remote
70 * (server side) repository. Object files are retrieved directly through secure
71 * shell's FTP protocol, making it possible to copy objects from a remote
72 * repository that is available over SSH, but whose remote host does not have
73 * Git installed.
74 * <p>
75 * Unlike the HTTP variant (see {@link TransportHttp}) we rely upon being able
76 * to list files in directories, as the SFTP protocol supports this function. By
77 * listing files through SFTP we can avoid needing to have current
78 * <code>objects/info/packs</code> or <code>info/refs</code> files on the
79 * remote repository and access the data directly, much as Git itself would.
81 * @see WalkFetchConnection
83 class TransportSftp extends WalkTransport {
84 static boolean canHandle(final URIish uri) {
85 return uri.isRemote() && "sftp".equals(uri.getScheme());
88 final SshSessionFactory sch;
90 TransportSftp(final Repository local, final URIish uri) {
91 super(local, uri);
92 sch = SshSessionFactory.getInstance();
95 @Override
96 public FetchConnection openFetch() throws TransportException {
97 final SftpObjectDB c = new SftpObjectDB(uri.getPath());
98 final WalkFetchConnection r = new WalkFetchConnection(this, c);
99 c.readAdvertisedRefs(r);
100 return r;
103 Session openSession() throws TransportException {
104 final String user = uri.getUser();
105 final String pass = uri.getPass();
106 final String host = uri.getHost();
107 final int port = uri.getPort();
108 try {
109 final Session session;
110 session = sch.getSession(user, pass, host, port);
111 if (!session.isConnected())
112 session.connect();
113 return session;
114 } catch (JSchException je) {
115 final Throwable c = je.getCause();
116 if (c instanceof UnknownHostException)
117 throw new TransportException(uri, "unknown host");
118 if (c instanceof ConnectException)
119 throw new TransportException(uri, c.getMessage());
120 throw new TransportException(uri, 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, je.getMessage(), je);
134 class SftpObjectDB extends WalkRemoteObjectDatabase {
135 private final String objectsPath;
137 private final boolean sessionOwner;
139 private Session session;
141 private ChannelSftp ftp;
143 SftpObjectDB(String path) throws TransportException {
144 if (path.startsWith("/~"))
145 path = path.substring(1);
146 if (path.startsWith("~/"))
147 path = path.substring(2);
148 try {
149 session = openSession();
150 sessionOwner = true;
151 ftp = TransportSftp.this.open(session);
152 ftp.cd(path);
153 ftp.cd("objects");
154 objectsPath = ftp.pwd();
155 } catch (TransportException err) {
156 close();
157 throw err;
158 } catch (SftpException je) {
159 throw new TransportException("Can't enter " + path + "/objects"
160 + ": " + je.getMessage(), je);
164 SftpObjectDB(final SftpObjectDB parent, final String p)
165 throws TransportException {
166 sessionOwner = false;
167 session = parent.session;
168 try {
169 ftp = TransportSftp.this.open(session);
170 ftp.cd(parent.objectsPath);
171 ftp.cd(p);
172 objectsPath = ftp.pwd();
173 } catch (TransportException err) {
174 close();
175 throw err;
176 } catch (SftpException je) {
177 throw new TransportException("Can't enter " + p + " from "
178 + parent.objectsPath + ": " + je.getMessage(), je);
182 @Override
183 Collection<WalkRemoteObjectDatabase> getAlternates() throws IOException {
184 try {
185 return readAlternates(INFO_ALTERNATES);
186 } catch (FileNotFoundException err) {
187 return null;
191 @Override
192 WalkRemoteObjectDatabase openAlternate(final String location)
193 throws IOException {
194 return new SftpObjectDB(this, location);
197 @Override
198 Collection<String> getPackNames() throws IOException {
199 final List<String> packs = new ArrayList<String>();
200 try {
201 final Collection<ChannelSftp.LsEntry> list = ftp.ls("pack");
202 final HashMap<String, ChannelSftp.LsEntry> files;
203 final HashMap<String, Integer> mtimes;
205 files = new HashMap<String, ChannelSftp.LsEntry>();
206 mtimes = new HashMap<String, Integer>();
208 for (final ChannelSftp.LsEntry ent : list)
209 files.put(ent.getFilename(), ent);
210 for (final ChannelSftp.LsEntry ent : list) {
211 final String n = ent.getFilename();
212 if (!n.startsWith("pack-") || !n.endsWith(".pack"))
213 continue;
215 final String in = n.substring(0, n.length() - 5) + ".idx";
216 if (!files.containsKey(in))
217 continue;
219 mtimes.put(n, ent.getAttrs().getMTime());
220 packs.add(n);
223 Collections.sort(packs, new Comparator<String>() {
224 public int compare(final String o1, final String o2) {
225 return mtimes.get(o2) - mtimes.get(o1);
228 } catch (SftpException je) {
229 throw new TransportException("Can't ls " + objectsPath
230 + "/pack: " + je.getMessage(), je);
232 return packs;
235 @Override
236 FileStream open(final String path) throws IOException {
237 try {
238 final SftpATTRS a = ftp.lstat(path);
239 return new FileStream(ftp.get(path), a.getSize());
240 } catch (SftpException je) {
241 if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
242 throw new FileNotFoundException(path);
243 throw new TransportException("Can't get " + objectsPath + "/"
244 + path + ": " + je.getMessage(), je);
248 void readAdvertisedRefs(final WalkFetchConnection connection)
249 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 connection.available(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;