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
;
42 import java
.io
.FileNotFoundException
;
43 import java
.io
.IOException
;
44 import java
.io
.InputStream
;
45 import java
.io
.OutputStream
;
46 import java
.net
.URLConnection
;
47 import java
.util
.ArrayList
;
48 import java
.util
.Collection
;
49 import java
.util
.HashSet
;
51 import java
.util
.Properties
;
52 import java
.util
.TreeMap
;
54 import org
.spearce
.jgit
.errors
.NotSupportedException
;
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
;
61 import org
.spearce
.jgit
.util
.FS
;
64 * Transport over the non-Git aware Amazon S3 protocol.
66 * This transport communicates with the Amazon S3 servers (a non-free commercial
67 * hosting service that users must subscribe to). Some users may find transport
68 * to and from S3 to be a useful backup service.
70 * The transport does not require any specialized Git support on the remote
71 * (server side) repository, as Amazon does not provide any such support.
72 * Repository files are retrieved directly through the S3 API, which uses
73 * extended HTTP/1.1 semantics. This make it possible to read or write Git data
74 * from a remote repository that is stored on S3.
76 * Unlike the HTTP variant (see {@link TransportHttp}) we rely upon being able
77 * to list objects in a bucket, as the S3 API supports this function. By listing
78 * the bucket contents we can avoid relying on <code>objects/info/packs</code>
79 * or <code>info/refs</code> in the remote repository.
81 * Concurrent pushing over this transport is not supported. Multiple concurrent
82 * push operations may cause confusion in the repository state.
84 * @see WalkFetchConnection
85 * @see WalkPushConnection
87 class TransportAmazonS3
extends WalkTransport
{
88 static final String S3_SCHEME
= "amazon-s3";
90 static boolean canHandle(final URIish uri
) {
93 return S3_SCHEME
.equals(uri
.getScheme());
96 /** User information necessary to connect to S3. */
97 private final AmazonS3 s3
;
99 /** Bucket the remote repository is stored in. */
100 private final String bucket
;
103 * Key prefix which all objects related to the repository start with.
105 * The prefix does not start with "/".
107 * The prefix does not end with "/". The trailing slash is stripped during
108 * the constructor if a trailing slash was supplied in the URIish.
110 * All files within the remote repository start with
111 * <code>keyPrefix + "/"</code>.
113 private final String keyPrefix
;
115 TransportAmazonS3(final Repository local
, final URIish uri
)
116 throws NotSupportedException
{
119 Properties props
= null;
120 File propsFile
= new File(local
.getDirectory(), uri
.getUser());
121 if (!propsFile
.isFile())
122 propsFile
= new File(FS
.userHome(), uri
.getUser());
123 if (propsFile
.isFile()) {
125 props
= AmazonS3
.properties(propsFile
);
126 } catch (IOException e
) {
127 throw new NotSupportedException("cannot read " + propsFile
, e
);
130 props
= new Properties();
131 props
.setProperty("accesskey", uri
.getUser());
132 props
.setProperty("secretkey", uri
.getPass());
135 s3
= new AmazonS3(props
);
136 bucket
= uri
.getHost();
138 String p
= uri
.getPath();
139 if (p
.startsWith("/"))
142 p
= p
.substring(0, p
.length() - 1);
147 public FetchConnection
openFetch() throws TransportException
{
148 final DatabaseS3 c
= new DatabaseS3(bucket
, keyPrefix
+ "/objects");
149 final WalkFetchConnection r
= new WalkFetchConnection(this, c
);
150 r
.available(c
.readAdvertisedRefs());
155 public PushConnection
openPush() throws TransportException
{
156 final DatabaseS3 c
= new DatabaseS3(bucket
, keyPrefix
+ "/objects");
157 final WalkPushConnection r
= new WalkPushConnection(this, c
);
158 r
.available(c
.readAdvertisedRefs());
163 public void close() {
164 // No explicit connections are maintained.
167 class DatabaseS3
extends WalkRemoteObjectDatabase
{
168 private final String bucketName
;
170 private final String objectsKey
;
172 DatabaseS3(final String b
, final String o
) {
177 private String
resolveKey(String subpath
) {
178 if (subpath
.endsWith("/"))
179 subpath
= subpath
.substring(0, subpath
.length() - 1);
180 String k
= objectsKey
;
181 while (subpath
.startsWith(ROOT_DIR
)) {
182 k
= k
.substring(0, k
.lastIndexOf('/'));
183 subpath
= subpath
.substring(3);
185 return k
+ "/" + subpath
;
190 URIish u
= new URIish();
191 u
= u
.setScheme(S3_SCHEME
);
192 u
= u
.setHost(bucketName
);
193 u
= u
.setPath("/" + objectsKey
);
198 Collection
<WalkRemoteObjectDatabase
> getAlternates() throws IOException
{
200 return readAlternates(INFO_ALTERNATES
);
201 } catch (FileNotFoundException err
) {
208 WalkRemoteObjectDatabase
openAlternate(final String location
)
210 return new DatabaseS3(bucketName
, resolveKey(location
));
214 Collection
<String
> getPackNames() throws IOException
{
215 final HashSet
<String
> have
= new HashSet
<String
>();
216 have
.addAll(s3
.list(bucket
, resolveKey("pack")));
218 final Collection
<String
> packs
= new ArrayList
<String
>();
219 for (final String n
: have
) {
220 if (!n
.startsWith("pack-") || !n
.endsWith(".pack"))
223 final String in
= n
.substring(0, n
.length() - 5) + ".idx";
224 if (have
.contains(in
))
231 FileStream
open(final String path
) throws IOException
{
232 final URLConnection c
= s3
.get(bucket
, resolveKey(path
));
233 final InputStream raw
= c
.getInputStream();
234 final InputStream in
= s3
.decrypt(c
);
235 final int len
= c
.getContentLength();
236 return new FileStream(in
, raw
== in ? len
: -1);
240 void deleteFile(final String path
) throws IOException
{
241 s3
.delete(bucket
, resolveKey(path
));
245 OutputStream
writeFile(final String path
,
246 final ProgressMonitor monitor
, final String monitorTask
)
248 return s3
.beginPut(bucket
, resolveKey(path
), monitor
, monitorTask
);
252 void writeFile(final String path
, final byte[] data
) throws IOException
{
253 s3
.put(bucket
, resolveKey(path
), data
);
256 Map
<String
, Ref
> readAdvertisedRefs() throws TransportException
{
257 final TreeMap
<String
, Ref
> avail
= new TreeMap
<String
, Ref
>();
258 readPackedRefs(avail
);
259 readLooseRefs(avail
);
260 readRef(avail
, "HEAD");
264 private void readLooseRefs(final TreeMap
<String
, Ref
> avail
)
265 throws TransportException
{
267 for (final String n
: s3
.list(bucket
, resolveKey(ROOT_DIR
269 readRef(avail
, "refs/" + n
);
270 } catch (IOException e
) {
271 throw new TransportException(getURI(), "cannot list refs", e
);
275 private Ref
readRef(final TreeMap
<String
, Ref
> avail
, final String rn
)
276 throws TransportException
{
278 String ref
= ROOT_DIR
+ rn
;
280 final BufferedReader br
= openReader(ref
);
286 } catch (FileNotFoundException noRef
) {
288 } catch (IOException err
) {
289 throw new TransportException(getURI(), "read " + ref
, err
);
293 throw new TransportException(getURI(), "Empty ref: " + rn
);
295 if (s
.startsWith("ref: ")) {
296 final String target
= s
.substring("ref: ".length());
297 Ref r
= avail
.get(target
);
299 r
= readRef(avail
, target
);
302 r
= new Ref(r
.getStorage(), rn
, r
.getObjectId(), r
303 .getPeeledObjectId());
304 avail
.put(r
.getName(), r
);
308 if (ObjectId
.isId(s
)) {
309 final Ref r
= new Ref(loose(avail
.get(rn
)), rn
, ObjectId
311 avail
.put(r
.getName(), r
);
315 throw new TransportException(getURI(), "Bad ref: " + rn
+ ": " + s
);
318 private Storage
loose(final Ref r
) {
319 if (r
!= null && r
.getStorage() == Storage
.PACKED
)
320 return Storage
.LOOSE_PACKED
;
321 return Storage
.LOOSE
;
326 // We do not maintain persistent connections.