2 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
6 * Redistribution and use in source and binary forms, with or
7 * without modification, are permitted provided that the following
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
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
;
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
;
70 * Transport over the non-Git aware SFTP (SSH based FTP) protocol.
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
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.
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
;
98 TransportSftp(final Repository local
, final URIish uri
) {
100 sch
= SshSessionFactory
.getInstance();
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());
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());
120 public void close() {
123 sch
.releaseSession(sock
);
130 private void initSession() throws TransportException
{
134 final String user
= uri
.getUser();
135 final String pass
= uri
.getPass();
136 final String host
= uri
.getHost();
137 final int port
= uri
.getPort();
139 sock
= sch
.getSession(user
, pass
, host
, port
);
140 if (!sock
.isConnected())
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
{
156 final Channel channel
= sock
.openChannel("sftp");
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);
178 objectsPath
= ftp
.pwd();
179 } catch (TransportException 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
{
192 ftp
.cd(parent
.objectsPath
);
194 objectsPath
= ftp
.pwd();
195 } catch (TransportException err
) {
198 } catch (SftpException je
) {
199 throw new TransportException("Can't enter " + p
+ " from "
200 + parent
.objectsPath
+ ": " + je
.getMessage(), je
);
206 return uri
.setPath(objectsPath
);
210 Collection
<WalkRemoteObjectDatabase
> getAlternates() throws IOException
{
212 return readAlternates(INFO_ALTERNATES
);
213 } catch (FileNotFoundException err
) {
219 WalkRemoteObjectDatabase
openAlternate(final String location
)
221 return new SftpObjectDB(this, location
);
225 Collection
<String
> getPackNames() throws IOException
{
226 final List
<String
> packs
= new ArrayList
<String
>();
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"))
242 final String in
= n
.substring(0, n
.length() - 5) + ".idx";
243 if (!files
.containsKey(in
))
246 mtimes
.put(n
, ent
.getAttrs().getMTime());
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
);
263 FileStream
open(final String path
) throws IOException
{
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
);
276 void deleteFile(final String path
) throws IOException
{
279 } catch (SftpException je
) {
280 if (je
.id
== ChannelSftp
.SSH_FX_NO_SUCH_FILE
)
282 throw new TransportException("Can't delete " + objectsPath
283 + "/" + path
+ ": " + je
.getMessage(), je
);
286 // Prune any now empty directories.
289 int s
= dir
.lastIndexOf('/');
292 dir
= dir
.substring(0, s
);
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.
306 OutputStream
writeFile(final String path
,
307 final ProgressMonitor monitor
, final String monitorTask
)
310 return ftp
.put(path
);
311 } catch (SftpException je
) {
312 if (je
.id
== ChannelSftp
.SSH_FX_NO_SUCH_FILE
) {
315 return ftp
.put(path
);
316 } catch (SftpException je2
) {
321 throw new TransportException("Can't write " + objectsPath
+ "/"
322 + path
+ ": " + je
.getMessage(), je
);
327 void writeFile(final String path
, final byte[] data
) throws IOException
{
328 final String lock
= path
+ ".lock";
330 super.writeFile(lock
, data
);
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
) {
340 } catch (SftpException e
) {
341 // Ignore deletion failure, we are already
348 private void mkdir_p(String path
) throws IOException
{
349 final int s
= path
.lastIndexOf('/');
353 path
= path
.substring(0, s
);
356 } catch (SftpException je
) {
357 if (je
.id
== ChannelSftp
.SSH_FX_NO_SUCH_FILE
) {
362 } catch (SftpException 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/");
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
;
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
))
396 final String nPath
= dir
+ "/" + n
;
397 if (ent
.getAttrs().isDir())
398 readLooseRefs(avail
, nPath
, prefix
+ n
+ "/");
400 readRef(avail
, nPath
, prefix
+ n
);
404 private Ref
readRef(final TreeMap
<String
, Ref
> avail
,
405 final String path
, final String name
) throws TransportException
{
408 final BufferedReader br
= openReader(path
);
410 line
= br
.readLine();
414 } catch (FileNotFoundException noRef
) {
416 } catch (IOException err
) {
417 throw new TransportException("Cannot read " + objectsPath
+ "/"
418 + path
+ ": " + err
.getMessage(), err
);
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
);
430 r
= new Ref(loose(r
), name
, r
.getObjectId(), r
431 .getPeeledObjectId());
437 if (ObjectId
.isId(line
)) {
438 final Ref r
= new Ref(loose(avail
.get(name
)), name
, ObjectId
440 avail
.put(r
.getName(), 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
;
457 if (ftp
.isConnected())