Moved common SSH related functionality to the base class SshTransport
[egit/chris.git] / org.spearce.jgit / src / org / spearce / jgit / transport / TransportSftp.java
blobe18d12833ec10979e5a10c93d0e9560f9e92be5d
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.io.OutputStream;
44 import java.util.ArrayList;
45 import java.util.Collection;
46 import java.util.Collections;
47 import java.util.Comparator;
48 import java.util.HashMap;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.TreeMap;
53 import org.spearce.jgit.errors.TransportException;
54 import org.spearce.jgit.lib.Constants;
55 import org.spearce.jgit.lib.ObjectId;
56 import org.spearce.jgit.lib.ProgressMonitor;
57 import org.spearce.jgit.lib.Ref;
58 import org.spearce.jgit.lib.Repository;
59 import org.spearce.jgit.lib.Ref.Storage;
61 import com.jcraft.jsch.Channel;
62 import com.jcraft.jsch.ChannelSftp;
63 import com.jcraft.jsch.JSchException;
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.
81 * <p>
82 * Concurrent pushing over this transport is not supported. Multiple concurrent
83 * push operations may cause confusion in the repository state.
85 * @see WalkFetchConnection
87 public class TransportSftp extends SshTransport implements WalkTransport {
88 static boolean canHandle(final URIish uri) {
89 return uri.isRemote() && "sftp".equals(uri.getScheme());
92 TransportSftp(final Repository local, final URIish uri) {
93 super(local, uri);
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 @Override
105 public PushConnection openPush() throws TransportException {
106 final SftpObjectDB c = new SftpObjectDB(uri.getPath());
107 final WalkPushConnection r = new WalkPushConnection(this, c);
108 r.available(c.readAdvertisedRefs());
109 return r;
112 ChannelSftp newSftp() throws TransportException {
113 initSession();
115 try {
116 final Channel channel = sock.openChannel("sftp");
117 channel.connect();
118 return (ChannelSftp) channel;
119 } catch (JSchException je) {
120 throw new TransportException(uri, je.getMessage(), je);
124 class SftpObjectDB extends WalkRemoteObjectDatabase {
125 private final String objectsPath;
127 private ChannelSftp ftp;
129 SftpObjectDB(String path) throws TransportException {
130 if (path.startsWith("/~"))
131 path = path.substring(1);
132 if (path.startsWith("~/"))
133 path = path.substring(2);
134 try {
135 ftp = newSftp();
136 ftp.cd(path);
137 ftp.cd("objects");
138 objectsPath = ftp.pwd();
139 } catch (TransportException err) {
140 close();
141 throw err;
142 } catch (SftpException je) {
143 throw new TransportException("Can't enter " + path + "/objects"
144 + ": " + je.getMessage(), je);
148 SftpObjectDB(final SftpObjectDB parent, final String p)
149 throws TransportException {
150 try {
151 ftp = newSftp();
152 ftp.cd(parent.objectsPath);
153 ftp.cd(p);
154 objectsPath = ftp.pwd();
155 } catch (TransportException err) {
156 close();
157 throw err;
158 } catch (SftpException je) {
159 throw new TransportException("Can't enter " + p + " from "
160 + parent.objectsPath + ": " + je.getMessage(), je);
164 @Override
165 URIish getURI() {
166 return uri.setPath(objectsPath);
169 @Override
170 Collection<WalkRemoteObjectDatabase> getAlternates() throws IOException {
171 try {
172 return readAlternates(INFO_ALTERNATES);
173 } catch (FileNotFoundException err) {
174 return null;
178 @Override
179 WalkRemoteObjectDatabase openAlternate(final String location)
180 throws IOException {
181 return new SftpObjectDB(this, location);
184 @Override
185 Collection<String> getPackNames() throws IOException {
186 final List<String> packs = new ArrayList<String>();
187 try {
188 final Collection<ChannelSftp.LsEntry> list = ftp.ls("pack");
189 final HashMap<String, ChannelSftp.LsEntry> files;
190 final HashMap<String, Integer> mtimes;
192 files = new HashMap<String, ChannelSftp.LsEntry>();
193 mtimes = new HashMap<String, Integer>();
195 for (final ChannelSftp.LsEntry ent : list)
196 files.put(ent.getFilename(), ent);
197 for (final ChannelSftp.LsEntry ent : list) {
198 final String n = ent.getFilename();
199 if (!n.startsWith("pack-") || !n.endsWith(".pack"))
200 continue;
202 final String in = n.substring(0, n.length() - 5) + ".idx";
203 if (!files.containsKey(in))
204 continue;
206 mtimes.put(n, ent.getAttrs().getMTime());
207 packs.add(n);
210 Collections.sort(packs, new Comparator<String>() {
211 public int compare(final String o1, final String o2) {
212 return mtimes.get(o2) - mtimes.get(o1);
215 } catch (SftpException je) {
216 throw new TransportException("Can't ls " + objectsPath
217 + "/pack: " + je.getMessage(), je);
219 return packs;
222 @Override
223 FileStream open(final String path) throws IOException {
224 try {
225 final SftpATTRS a = ftp.lstat(path);
226 return new FileStream(ftp.get(path), a.getSize());
227 } catch (SftpException je) {
228 if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
229 throw new FileNotFoundException(path);
230 throw new TransportException("Can't get " + objectsPath + "/"
231 + path + ": " + je.getMessage(), je);
235 @Override
236 void deleteFile(final String path) throws IOException {
237 try {
238 ftp.rm(path);
239 } catch (SftpException je) {
240 if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
241 return;
242 throw new TransportException("Can't delete " + objectsPath
243 + "/" + path + ": " + je.getMessage(), je);
246 // Prune any now empty directories.
248 String dir = path;
249 int s = dir.lastIndexOf('/');
250 while (s > 0) {
251 try {
252 dir = dir.substring(0, s);
253 ftp.rmdir(dir);
254 s = dir.lastIndexOf('/');
255 } catch (SftpException je) {
256 // If we cannot delete it, leave it alone. It may have
257 // entries still in it, or maybe we lack write access on
258 // the parent. Either way it isn't a fatal error.
260 break;
265 @Override
266 OutputStream writeFile(final String path,
267 final ProgressMonitor monitor, final String monitorTask)
268 throws IOException {
269 try {
270 return ftp.put(path);
271 } catch (SftpException je) {
272 if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
273 mkdir_p(path);
274 try {
275 return ftp.put(path);
276 } catch (SftpException je2) {
277 je = je2;
281 throw new TransportException("Can't write " + objectsPath + "/"
282 + path + ": " + je.getMessage(), je);
286 @Override
287 void writeFile(final String path, final byte[] data) throws IOException {
288 final String lock = path + ".lock";
289 try {
290 super.writeFile(lock, data);
291 try {
292 ftp.rename(lock, path);
293 } catch (SftpException je) {
294 throw new TransportException("Can't write " + objectsPath
295 + "/" + path + ": " + je.getMessage(), je);
297 } catch (IOException err) {
298 try {
299 ftp.rm(lock);
300 } catch (SftpException e) {
301 // Ignore deletion failure, we are already
302 // failing anyway.
304 throw err;
308 private void mkdir_p(String path) throws IOException {
309 final int s = path.lastIndexOf('/');
310 if (s <= 0)
311 return;
313 path = path.substring(0, s);
314 try {
315 ftp.mkdir(path);
316 } catch (SftpException je) {
317 if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
318 mkdir_p(path);
319 try {
320 ftp.mkdir(path);
321 return;
322 } catch (SftpException je2) {
323 je = je2;
327 throw new TransportException("Can't mkdir " + objectsPath + "/"
328 + path + ": " + je.getMessage(), je);
332 Map<String, Ref> readAdvertisedRefs() throws TransportException {
333 final TreeMap<String, Ref> avail = new TreeMap<String, Ref>();
334 readPackedRefs(avail);
335 readRef(avail, ROOT_DIR + Constants.HEAD, Constants.HEAD);
336 readLooseRefs(avail, ROOT_DIR + "refs", "refs/");
337 return avail;
340 private void readLooseRefs(final TreeMap<String, Ref> avail,
341 final String dir, final String prefix)
342 throws TransportException {
343 final Collection<ChannelSftp.LsEntry> list;
344 try {
345 list = ftp.ls(dir);
346 } catch (SftpException je) {
347 throw new TransportException("Can't ls " + objectsPath + "/"
348 + dir + ": " + je.getMessage(), je);
351 for (final ChannelSftp.LsEntry ent : list) {
352 final String n = ent.getFilename();
353 if (".".equals(n) || "..".equals(n))
354 continue;
356 final String nPath = dir + "/" + n;
357 if (ent.getAttrs().isDir())
358 readLooseRefs(avail, nPath, prefix + n + "/");
359 else
360 readRef(avail, nPath, prefix + n);
364 private Ref readRef(final TreeMap<String, Ref> avail,
365 final String path, final String name) throws TransportException {
366 final String line;
367 try {
368 final BufferedReader br = openReader(path);
369 try {
370 line = br.readLine();
371 } finally {
372 br.close();
374 } catch (FileNotFoundException noRef) {
375 return null;
376 } catch (IOException err) {
377 throw new TransportException("Cannot read " + objectsPath + "/"
378 + path + ": " + err.getMessage(), err);
381 if (line == null)
382 throw new TransportException("Empty ref: " + name);
384 if (line.startsWith("ref: ")) {
385 final String p = line.substring("ref: ".length());
386 Ref r = readRef(avail, ROOT_DIR + p, p);
387 if (r == null)
388 r = avail.get(p);
389 if (r != null) {
390 r = new Ref(loose(r), name, r.getObjectId(), r
391 .getPeeledObjectId(), true);
392 avail.put(name, r);
394 return r;
397 if (ObjectId.isId(line)) {
398 final Ref r = new Ref(loose(avail.get(name)), name, ObjectId
399 .fromString(line));
400 avail.put(r.getName(), r);
401 return r;
404 throw new TransportException("Bad ref: " + name + ": " + line);
407 private Storage loose(final Ref r) {
408 if (r != null && r.getStorage() == Storage.PACKED)
409 return Storage.LOOSE_PACKED;
410 return Storage.LOOSE;
413 @Override
414 void close() {
415 if (ftp != null) {
416 try {
417 if (ftp.isConnected())
418 ftp.disconnect();
419 } finally {
420 ftp = null;