Fix RemoteRefUpdate to delete local tracking ref upon successful deletion
[egit/zawir.git] / org.spearce.jgit / src / org / spearce / jgit / transport / TransportSftp.java
blob78f4ad82bfcb2a341bc882ad95902746b1db616e
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.net.ConnectException;
45 import java.net.UnknownHostException;
46 import java.util.ArrayList;
47 import java.util.Collection;
48 import java.util.Collections;
49 import java.util.Comparator;
50 import java.util.HashMap;
51 import java.util.List;
52 import java.util.Map;
53 import java.util.TreeMap;
55 import org.spearce.jgit.errors.TransportException;
56 import org.spearce.jgit.lib.ObjectId;
57 import org.spearce.jgit.lib.ProgressMonitor;
58 import org.spearce.jgit.lib.Ref;
59 import org.spearce.jgit.lib.Repository;
60 import org.spearce.jgit.lib.Ref.Storage;
62 import com.jcraft.jsch.Channel;
63 import com.jcraft.jsch.ChannelSftp;
64 import com.jcraft.jsch.JSchException;
65 import com.jcraft.jsch.Session;
66 import com.jcraft.jsch.SftpATTRS;
67 import com.jcraft.jsch.SftpException;
69 /**
70 * Transport over the non-Git aware SFTP (SSH based FTP) protocol.
71 * <p>
72 * The SFTP transport does not require any specialized Git support on the remote
73 * (server side) repository. Object files are retrieved directly through secure
74 * shell's FTP protocol, making it possible to copy objects from a remote
75 * repository that is available over SSH, but whose remote host does not have
76 * Git installed.
77 * <p>
78 * Unlike the HTTP variant (see {@link TransportHttp}) we rely upon being able
79 * to list files in directories, as the SFTP protocol supports this function. By
80 * listing files through SFTP we can avoid needing to have current
81 * <code>objects/info/packs</code> or <code>info/refs</code> files on the
82 * remote repository and access the data directly, much as Git itself would.
83 * <p>
84 * Concurrent pushing over this transport is not supported. Multiple concurrent
85 * push operations may cause confusion in the repository state.
87 * @see WalkFetchConnection
89 class TransportSftp extends WalkTransport {
90 static boolean canHandle(final URIish uri) {
91 return uri.isRemote() && "sftp".equals(uri.getScheme());
94 private final SshSessionFactory sch;
96 private Session sock;
98 TransportSftp(final Repository local, final URIish uri) {
99 super(local, uri);
100 sch = SshSessionFactory.getInstance();
103 @Override
104 public FetchConnection openFetch() throws TransportException {
105 final SftpObjectDB c = new SftpObjectDB(uri.getPath());
106 final WalkFetchConnection r = new WalkFetchConnection(this, c);
107 r.available(c.readAdvertisedRefs());
108 return r;
111 @Override
112 public PushConnection openPush() throws TransportException {
113 final SftpObjectDB c = new SftpObjectDB(uri.getPath());
114 final WalkPushConnection r = new WalkPushConnection(this, c);
115 r.available(c.readAdvertisedRefs());
116 return r;
119 @Override
120 public void close() {
121 if (sock != null) {
122 try {
123 sch.releaseSession(sock);
124 } finally {
125 sock = null;
130 private void initSession() throws TransportException {
131 if (sock != null)
132 return;
134 final String user = uri.getUser();
135 final String pass = uri.getPass();
136 final String host = uri.getHost();
137 final int port = uri.getPort();
138 try {
139 sock = sch.getSession(user, pass, host, port);
140 if (!sock.isConnected())
141 sock.connect();
142 } catch (JSchException je) {
143 final Throwable c = je.getCause();
144 if (c instanceof UnknownHostException)
145 throw new TransportException(uri, "unknown host");
146 if (c instanceof ConnectException)
147 throw new TransportException(uri, c.getMessage());
148 throw new TransportException(uri, je.getMessage(), je);
152 ChannelSftp newSftp() throws TransportException {
153 initSession();
155 try {
156 final Channel channel = sock.openChannel("sftp");
157 channel.connect();
158 return (ChannelSftp) channel;
159 } catch (JSchException je) {
160 throw new TransportException(uri, je.getMessage(), je);
164 class SftpObjectDB extends WalkRemoteObjectDatabase {
165 private final String objectsPath;
167 private ChannelSftp ftp;
169 SftpObjectDB(String path) throws TransportException {
170 if (path.startsWith("/~"))
171 path = path.substring(1);
172 if (path.startsWith("~/"))
173 path = path.substring(2);
174 try {
175 ftp = newSftp();
176 ftp.cd(path);
177 ftp.cd("objects");
178 objectsPath = ftp.pwd();
179 } catch (TransportException err) {
180 close();
181 throw err;
182 } catch (SftpException je) {
183 throw new TransportException("Can't enter " + path + "/objects"
184 + ": " + je.getMessage(), je);
188 SftpObjectDB(final SftpObjectDB parent, final String p)
189 throws TransportException {
190 try {
191 ftp = newSftp();
192 ftp.cd(parent.objectsPath);
193 ftp.cd(p);
194 objectsPath = ftp.pwd();
195 } catch (TransportException err) {
196 close();
197 throw err;
198 } catch (SftpException je) {
199 throw new TransportException("Can't enter " + p + " from "
200 + parent.objectsPath + ": " + je.getMessage(), je);
204 @Override
205 URIish getURI() {
206 return uri.setPath(objectsPath);
209 @Override
210 Collection<WalkRemoteObjectDatabase> getAlternates() throws IOException {
211 try {
212 return readAlternates(INFO_ALTERNATES);
213 } catch (FileNotFoundException err) {
214 return null;
218 @Override
219 WalkRemoteObjectDatabase openAlternate(final String location)
220 throws IOException {
221 return new SftpObjectDB(this, location);
224 @Override
225 Collection<String> getPackNames() throws IOException {
226 final List<String> packs = new ArrayList<String>();
227 try {
228 final Collection<ChannelSftp.LsEntry> list = ftp.ls("pack");
229 final HashMap<String, ChannelSftp.LsEntry> files;
230 final HashMap<String, Integer> mtimes;
232 files = new HashMap<String, ChannelSftp.LsEntry>();
233 mtimes = new HashMap<String, Integer>();
235 for (final ChannelSftp.LsEntry ent : list)
236 files.put(ent.getFilename(), ent);
237 for (final ChannelSftp.LsEntry ent : list) {
238 final String n = ent.getFilename();
239 if (!n.startsWith("pack-") || !n.endsWith(".pack"))
240 continue;
242 final String in = n.substring(0, n.length() - 5) + ".idx";
243 if (!files.containsKey(in))
244 continue;
246 mtimes.put(n, ent.getAttrs().getMTime());
247 packs.add(n);
250 Collections.sort(packs, new Comparator<String>() {
251 public int compare(final String o1, final String o2) {
252 return mtimes.get(o2) - mtimes.get(o1);
255 } catch (SftpException je) {
256 throw new TransportException("Can't ls " + objectsPath
257 + "/pack: " + je.getMessage(), je);
259 return packs;
262 @Override
263 FileStream open(final String path) throws IOException {
264 try {
265 final SftpATTRS a = ftp.lstat(path);
266 return new FileStream(ftp.get(path), a.getSize());
267 } catch (SftpException je) {
268 if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
269 throw new FileNotFoundException(path);
270 throw new TransportException("Can't get " + objectsPath + "/"
271 + path + ": " + je.getMessage(), je);
275 @Override
276 void deleteFile(final String path) throws IOException {
277 try {
278 ftp.rm(path);
279 } catch (SftpException je) {
280 if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
281 return;
282 throw new TransportException("Can't delete " + objectsPath
283 + "/" + path + ": " + je.getMessage(), je);
286 // Prune any now empty directories.
288 String dir = path;
289 int s = dir.lastIndexOf('/');
290 while (s > 0) {
291 try {
292 dir = dir.substring(0, s);
293 ftp.rmdir(dir);
294 s = dir.lastIndexOf('/');
295 } catch (SftpException je) {
296 // If we cannot delete it, leave it alone. It may have
297 // entries still in it, or maybe we lack write access on
298 // the parent. Either way it isn't a fatal error.
300 break;
305 @Override
306 OutputStream writeFile(final String path,
307 final ProgressMonitor monitor, final String monitorTask)
308 throws IOException {
309 try {
310 return ftp.put(path);
311 } catch (SftpException je) {
312 if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
313 mkdir_p(path);
314 try {
315 return ftp.put(path);
316 } catch (SftpException je2) {
317 je = je2;
321 throw new TransportException("Can't write " + objectsPath + "/"
322 + path + ": " + je.getMessage(), je);
326 @Override
327 void writeFile(final String path, final byte[] data) throws IOException {
328 final String lock = path + ".lock";
329 try {
330 super.writeFile(lock, data);
331 try {
332 ftp.rename(lock, path);
333 } catch (SftpException je) {
334 throw new TransportException("Can't write " + objectsPath
335 + "/" + path + ": " + je.getMessage(), je);
337 } catch (IOException err) {
338 try {
339 ftp.rm(lock);
340 } catch (SftpException e) {
341 // Ignore deletion failure, we are already
342 // failing anyway.
344 throw err;
348 private void mkdir_p(String path) throws IOException {
349 final int s = path.lastIndexOf('/');
350 if (s <= 0)
351 return;
353 path = path.substring(0, s);
354 try {
355 ftp.mkdir(path);
356 } catch (SftpException je) {
357 if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
358 mkdir_p(path);
359 try {
360 ftp.mkdir(path);
361 return;
362 } catch (SftpException je2) {
363 je = je2;
367 throw new TransportException("Can't mkdir " + objectsPath + "/"
368 + path + ": " + je.getMessage(), je);
372 Map<String, Ref> readAdvertisedRefs() throws TransportException {
373 final TreeMap<String, Ref> avail = new TreeMap<String, Ref>();
374 readPackedRefs(avail);
375 readRef(avail, ROOT_DIR + "HEAD", "HEAD");
376 readLooseRefs(avail, ROOT_DIR + "refs", "refs/");
377 return avail;
380 private void readLooseRefs(final TreeMap<String, Ref> avail,
381 final String dir, final String prefix)
382 throws TransportException {
383 final Collection<ChannelSftp.LsEntry> list;
384 try {
385 list = ftp.ls(dir);
386 } catch (SftpException je) {
387 throw new TransportException("Can't ls " + objectsPath + "/"
388 + dir + ": " + je.getMessage(), je);
391 for (final ChannelSftp.LsEntry ent : list) {
392 final String n = ent.getFilename();
393 if (".".equals(n) || "..".equals(n))
394 continue;
396 final String nPath = dir + "/" + n;
397 if (ent.getAttrs().isDir())
398 readLooseRefs(avail, nPath, prefix + n + "/");
399 else
400 readRef(avail, nPath, prefix + n);
404 private Ref readRef(final TreeMap<String, Ref> avail,
405 final String path, final String name) throws TransportException {
406 final String line;
407 try {
408 final BufferedReader br = openReader(path);
409 try {
410 line = br.readLine();
411 } finally {
412 br.close();
414 } catch (FileNotFoundException noRef) {
415 return null;
416 } catch (IOException err) {
417 throw new TransportException("Cannot read " + objectsPath + "/"
418 + path + ": " + err.getMessage(), err);
421 if (line == null)
422 throw new TransportException("Empty ref: " + name);
424 if (line.startsWith("ref: ")) {
425 final String p = line.substring("ref: ".length());
426 Ref r = readRef(avail, ROOT_DIR + p, p);
427 if (r == null)
428 r = avail.get(p);
429 if (r != null) {
430 r = new Ref(loose(r), name, r.getObjectId(), r
431 .getPeeledObjectId());
432 avail.put(name, r);
434 return r;
437 if (ObjectId.isId(line)) {
438 final Ref r = new Ref(loose(avail.get(name)), name, ObjectId
439 .fromString(line));
440 avail.put(r.getName(), r);
441 return r;
444 throw new TransportException("Bad ref: " + name + ": " + line);
447 private Storage loose(final Ref r) {
448 if (r != null && r.getStorage() == Storage.PACKED)
449 return Storage.LOOSE_PACKED;
450 return Storage.LOOSE;
453 @Override
454 void close() {
455 if (ftp != null) {
456 try {
457 if (ftp.isConnected())
458 ftp.disconnect();
459 } finally {
460 ftp = null;