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
.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
;
58 import com
.jcraft
.jsch
.Channel
;
59 import com
.jcraft
.jsch
.ChannelSftp
;
60 import com
.jcraft
.jsch
.JSchException
;
61 import com
.jcraft
.jsch
.Session
;
62 import com
.jcraft
.jsch
.SftpATTRS
;
63 import com
.jcraft
.jsch
.SftpException
;
66 * Transport over the non-Git aware SFTP (SSH based FTP) protocol.
68 * The SFTP transport does not require any specialized Git support on the remote
69 * (server side) repository. Object files are retrieved directly through secure
70 * shell's FTP protocol, making it possible to copy objects from a remote
71 * repository that is available over SSH, but whose remote host does not have
74 * Unlike the HTTP variant (see {@link TransportHttp}) we rely upon being able
75 * to list files in directories, as the SFTP protocol supports this function. By
76 * listing files through SFTP we can avoid needing to have current
77 * <code>objects/info/packs</code> or <code>info/refs</code> files on the
78 * remote repository and access the data directly, much as Git itself would.
80 * @see WalkFetchConnection
82 class TransportSftp
extends WalkTransport
{
83 static boolean canHandle(final URIish uri
) {
84 return uri
.isRemote() && "sftp".equals(uri
.getScheme());
87 final SshSessionFactory sch
;
89 TransportSftp(final Repository local
, final URIish uri
) {
91 sch
= SshSessionFactory
.getInstance();
95 public FetchConnection
openFetch() throws TransportException
{
96 final SftpObjectDB c
= new SftpObjectDB(uri
.getPath());
97 final WalkFetchConnection r
= new WalkFetchConnection(this, c
);
98 c
.readAdvertisedRefs(r
);
102 Session
openSession() throws TransportException
{
103 final String user
= uri
.getUser();
104 final String pass
= uri
.getPass();
105 final String host
= uri
.getHost();
106 final int port
= uri
.getPort();
108 final Session session
;
109 session
= sch
.getSession(user
, pass
, host
, port
);
110 if (!session
.isConnected())
113 } catch (JSchException je
) {
114 final String us
= uri
.toString();
115 final Throwable c
= je
.getCause();
116 if (c
instanceof UnknownHostException
)
117 throw new TransportException(us
+ ": Unknown host");
118 if (c
instanceof ConnectException
)
119 throw new TransportException(us
+ ": " + c
.getMessage());
120 throw new TransportException(us
+ ": " + je
.getMessage(), je
);
124 ChannelSftp
open(final Session sock
) throws TransportException
{
126 final Channel channel
= sock
.openChannel("sftp");
128 return (ChannelSftp
) channel
;
129 } catch (JSchException je
) {
130 throw new TransportException(uri
.toString() + ": "
131 + 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);
150 session
= openSession();
152 ftp
= TransportSftp
.this.open(session
);
155 objectsPath
= ftp
.pwd();
156 } catch (TransportException 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
;
170 ftp
= TransportSftp
.this.open(session
);
171 ftp
.cd(parent
.objectsPath
);
173 objectsPath
= ftp
.pwd();
174 } catch (TransportException err
) {
177 } catch (SftpException je
) {
178 throw new TransportException("Can't enter " + p
+ " from "
179 + parent
.objectsPath
+ ": " + je
.getMessage(), je
);
184 Collection
<WalkRemoteObjectDatabase
> getAlternates() throws IOException
{
186 return readAlternates(INFO_ALTERNATES
);
187 } catch (FileNotFoundException err
) {
193 WalkRemoteObjectDatabase
openAlternate(final String location
)
195 return new SftpObjectDB(this, location
);
199 Collection
<String
> getPackNames() throws IOException
{
200 final List
<String
> packs
= new ArrayList
<String
>();
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"))
216 final String in
= n
.substring(0, n
.length() - 5) + ".idx";
217 if (!files
.containsKey(in
))
220 mtimes
.put(n
, ent
.getAttrs().getMTime());
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
);
237 FileStream
open(final String path
) throws IOException
{
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 void readAdvertisedRefs(final WalkFetchConnection connection
)
250 throws TransportException
{
251 final TreeMap
<String
, Ref
> avail
= new TreeMap
<String
, Ref
>();
253 final BufferedReader br
= openReader("../packed-refs");
255 readPackedRefs(avail
, br
);
259 } catch (FileNotFoundException notPacked
) {
260 // Perhaps it wasn't worthwhile, or is just an older repository.
261 } catch (IOException e
) {
262 throw new TransportException(uri
+ ": error in packed-refs", e
);
264 readRef(avail
, "../HEAD", "HEAD");
265 readLooseRefs(avail
, "../refs", "refs/");
266 connection
.available(avail
);
269 private void readPackedRefs(final TreeMap
<String
, Ref
> avail
,
270 final BufferedReader br
) throws IOException
{
273 String line
= br
.readLine();
276 if (line
.charAt(0) == '#')
278 if (line
.charAt(0) == '^') {
280 throw new TransportException("Peeled line before ref.");
281 final ObjectId id
= ObjectId
.fromString(line
+ 1);
282 last
= new Ref(last
.getName(), last
.getObjectId(), id
);
283 avail
.put(last
.getName(), last
);
287 final int sp
= line
.indexOf(' ');
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(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
;
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
))
313 final String nPath
= dir
+ "/" + n
;
314 if (ent
.getAttrs().isDir())
315 readLooseRefs(avail
, nPath
, prefix
+ n
+ "/");
317 readRef(avail
, nPath
, prefix
+ n
);
321 private Ref
readRef(final TreeMap
<String
, Ref
> avail
,
322 final String path
, final String name
) throws TransportException
{
325 final BufferedReader br
= openReader(path
);
327 line
= br
.readLine();
331 } catch (FileNotFoundException noRef
) {
333 } catch (IOException err
) {
334 throw new TransportException("Cannot read " + objectsPath
+ "/"
335 + path
+ ": " + err
.getMessage(), err
);
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
);
347 r
= new Ref(name
, r
.getObjectId(), r
.getPeeledObjectId());
353 if (ObjectId
.isId(line
)) {
354 final Ref r
= new Ref(name
, ObjectId
.fromString(line
));
355 avail
.put(r
.getName(), r
);
359 throw new TransportException("Bad ref: " + name
+ ": " + line
);
366 if (ftp
.isConnected())
373 if (sessionOwner
&& session
!= null) {
375 sch
.releaseSession(session
);