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
.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
;
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
;
68 * Transport over the non-Git aware SFTP (SSH based FTP) protocol.
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
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.
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
) {
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());
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());
112 ChannelSftp
newSftp() throws TransportException
{
116 final Channel channel
= sock
.openChannel("sftp");
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);
138 objectsPath
= ftp
.pwd();
139 } catch (TransportException 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
{
152 ftp
.cd(parent
.objectsPath
);
154 objectsPath
= ftp
.pwd();
155 } catch (TransportException err
) {
158 } catch (SftpException je
) {
159 throw new TransportException("Can't enter " + p
+ " from "
160 + parent
.objectsPath
+ ": " + je
.getMessage(), je
);
166 return uri
.setPath(objectsPath
);
170 Collection
<WalkRemoteObjectDatabase
> getAlternates() throws IOException
{
172 return readAlternates(INFO_ALTERNATES
);
173 } catch (FileNotFoundException err
) {
179 WalkRemoteObjectDatabase
openAlternate(final String location
)
181 return new SftpObjectDB(this, location
);
185 Collection
<String
> getPackNames() throws IOException
{
186 final List
<String
> packs
= new ArrayList
<String
>();
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"))
202 final String in
= n
.substring(0, n
.length() - 5) + ".idx";
203 if (!files
.containsKey(in
))
206 mtimes
.put(n
, ent
.getAttrs().getMTime());
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
);
223 FileStream
open(final String path
) throws IOException
{
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
);
236 void deleteFile(final String path
) throws IOException
{
239 } catch (SftpException je
) {
240 if (je
.id
== ChannelSftp
.SSH_FX_NO_SUCH_FILE
)
242 throw new TransportException("Can't delete " + objectsPath
243 + "/" + path
+ ": " + je
.getMessage(), je
);
246 // Prune any now empty directories.
249 int s
= dir
.lastIndexOf('/');
252 dir
= dir
.substring(0, s
);
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.
266 OutputStream
writeFile(final String path
,
267 final ProgressMonitor monitor
, final String monitorTask
)
270 return ftp
.put(path
);
271 } catch (SftpException je
) {
272 if (je
.id
== ChannelSftp
.SSH_FX_NO_SUCH_FILE
) {
275 return ftp
.put(path
);
276 } catch (SftpException je2
) {
281 throw new TransportException("Can't write " + objectsPath
+ "/"
282 + path
+ ": " + je
.getMessage(), je
);
287 void writeFile(final String path
, final byte[] data
) throws IOException
{
288 final String lock
= path
+ ".lock";
290 super.writeFile(lock
, data
);
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
) {
300 } catch (SftpException e
) {
301 // Ignore deletion failure, we are already
308 private void mkdir_p(String path
) throws IOException
{
309 final int s
= path
.lastIndexOf('/');
313 path
= path
.substring(0, s
);
316 } catch (SftpException je
) {
317 if (je
.id
== ChannelSftp
.SSH_FX_NO_SUCH_FILE
) {
322 } catch (SftpException 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/");
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
;
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
))
356 final String nPath
= dir
+ "/" + n
;
357 if (ent
.getAttrs().isDir())
358 readLooseRefs(avail
, nPath
, prefix
+ n
+ "/");
360 readRef(avail
, nPath
, prefix
+ n
);
364 private Ref
readRef(final TreeMap
<String
, Ref
> avail
,
365 final String path
, final String name
) throws TransportException
{
368 final BufferedReader br
= openReader(path
);
370 line
= br
.readLine();
374 } catch (FileNotFoundException noRef
) {
376 } catch (IOException err
) {
377 throw new TransportException("Cannot read " + objectsPath
+ "/"
378 + path
+ ": " + err
.getMessage(), err
);
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
);
390 r
= new Ref(loose(r
), name
, r
.getObjectId(), r
391 .getPeeledObjectId(), true);
397 if (ObjectId
.isId(line
)) {
398 final Ref r
= new Ref(loose(avail
.get(name
)), name
, ObjectId
400 avail
.put(r
.getName(), 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
;
417 if (ftp
.isConnected())