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
;
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
;
67 * Transport over the non-Git aware SFTP (SSH based FTP) protocol.
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
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
) {
92 sch
= SshSessionFactory
.getInstance();
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
);
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();
109 final Session session
;
110 session
= sch
.getSession(user
, pass
, host
, port
);
111 if (!session
.isConnected())
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
{
126 final Channel channel
= sock
.openChannel("sftp");
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);
149 session
= openSession();
151 ftp
= TransportSftp
.this.open(session
);
154 objectsPath
= ftp
.pwd();
155 } catch (TransportException 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
;
169 ftp
= TransportSftp
.this.open(session
);
170 ftp
.cd(parent
.objectsPath
);
172 objectsPath
= ftp
.pwd();
173 } catch (TransportException err
) {
176 } catch (SftpException je
) {
177 throw new TransportException("Can't enter " + p
+ " from "
178 + parent
.objectsPath
+ ": " + je
.getMessage(), je
);
183 Collection
<WalkRemoteObjectDatabase
> getAlternates() throws IOException
{
185 return readAlternates(INFO_ALTERNATES
);
186 } catch (FileNotFoundException err
) {
192 WalkRemoteObjectDatabase
openAlternate(final String location
)
194 return new SftpObjectDB(this, location
);
198 Collection
<String
> getPackNames() throws IOException
{
199 final List
<String
> packs
= new ArrayList
<String
>();
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"))
215 final String in
= n
.substring(0, n
.length() - 5) + ".idx";
216 if (!files
.containsKey(in
))
219 mtimes
.put(n
, ent
.getAttrs().getMTime());
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
);
236 FileStream
open(final String path
) throws IOException
{
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
>();
252 final BufferedReader br
= openReader("../packed-refs");
254 readPackedRefs(avail
, br
);
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
{
272 String line
= br
.readLine();
275 if (line
.charAt(0) == '#')
277 if (line
.charAt(0) == '^') {
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
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(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
;
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(loose(r
), name
, r
.getObjectId(), r
348 .getPeeledObjectId());
354 if (ObjectId
.isId(line
)) {
355 final Ref r
= new Ref(loose(avail
.get(name
)), name
, ObjectId
357 avail
.put(r
.getName(), 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
;
374 if (ftp
.isConnected())
381 if (sessionOwner
&& session
!= null) {
383 sch
.releaseSession(session
);