View Javadoc
1   /*
2    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.transport;
45  
46  import java.io.BufferedReader;
47  import java.io.ByteArrayOutputStream;
48  import java.io.FileNotFoundException;
49  import java.io.IOException;
50  import java.io.InputStream;
51  import java.io.InputStreamReader;
52  import java.io.OutputStream;
53  import java.text.MessageFormat;
54  import java.util.ArrayList;
55  import java.util.Collection;
56  import java.util.Map;
57  
58  import org.eclipse.jgit.errors.TransportException;
59  import org.eclipse.jgit.internal.JGitText;
60  import org.eclipse.jgit.internal.storage.file.RefDirectory;
61  import org.eclipse.jgit.lib.Constants;
62  import org.eclipse.jgit.lib.ObjectId;
63  import org.eclipse.jgit.lib.ObjectIdRef;
64  import org.eclipse.jgit.lib.ProgressMonitor;
65  import org.eclipse.jgit.lib.Ref;
66  import org.eclipse.jgit.util.IO;
67  
68  /**
69   * Transfers object data through a dumb transport.
70   * <p>
71   * Implementations are responsible for resolving path names relative to the
72   * <code>objects/</code> subdirectory of a single remote Git repository or
73   * naked object database and make the content available as a Java input stream
74   * for reading during fetch. The actual object traversal logic to determine the
75   * names of files to retrieve is handled through the generic, protocol
76   * independent {@link WalkFetchConnection}.
77   */
78  abstract class WalkRemoteObjectDatabase {
79  	static final String ROOT_DIR = "../"; //$NON-NLS-1$
80  
81  	static final String INFO_PACKS = "info/packs"; //$NON-NLS-1$
82  
83  	static final String INFO_ALTERNATES = "info/alternates"; //$NON-NLS-1$
84  
85  	static final String INFO_HTTP_ALTERNATES = "info/http-alternates"; //$NON-NLS-1$
86  
87  	static final String INFO_REFS = ROOT_DIR + Constants.INFO_REFS;
88  
89  	abstract URIish getURI();
90  
91  	/**
92  	 * Obtain the list of available packs (if any).
93  	 * <p>
94  	 * Pack names should be the file name in the packs directory, that is
95  	 * <code>pack-035760ab452d6eebd123add421f253ce7682355a.pack</code>. Index
96  	 * names should not be included in the returned collection.
97  	 *
98  	 * @return list of pack names; null or empty list if none are available.
99  	 * @throws IOException
100 	 *             The connection is unable to read the remote repository's list
101 	 *             of available pack files.
102 	 */
103 	abstract Collection<String> getPackNames() throws IOException;
104 
105 	/**
106 	 * Obtain alternate connections to alternate object databases (if any).
107 	 * <p>
108 	 * Alternates are typically read from the file {@link #INFO_ALTERNATES} or
109 	 * {@link #INFO_HTTP_ALTERNATES}. The content of each line must be resolved
110 	 * by the implementation and a new database reference should be returned to
111 	 * represent the additional location.
112 	 * <p>
113 	 * Alternates may reuse the same network connection handle, however the
114 	 * fetch connection will {@link #close()} each created alternate.
115 	 *
116 	 * @return list of additional object databases the caller could fetch from;
117 	 *         null or empty list if none are configured.
118 	 * @throws IOException
119 	 *             The connection is unable to read the remote repository's list
120 	 *             of configured alternates.
121 	 */
122 	abstract Collection<WalkRemoteObjectDatabase> getAlternates()
123 			throws IOException;
124 
125 	/**
126 	 * Open a single file for reading.
127 	 * <p>
128 	 * Implementors should make every attempt possible to ensure
129 	 * {@link FileNotFoundException} is used when the remote object does not
130 	 * exist. However when fetching over HTTP some misconfigured servers may
131 	 * generate a 200 OK status message (rather than a 404 Not Found) with an
132 	 * HTML formatted message explaining the requested resource does not exist.
133 	 * Callers such as {@link WalkFetchConnection} are prepared to handle this
134 	 * by validating the content received, and assuming content that fails to
135 	 * match its hash is an incorrectly phrased FileNotFoundException.
136 	 * <p>
137 	 * This method is recommended for already compressed files like loose objects
138 	 * and pack files. For text files, see {@link #openReader(String)}.
139 	 *
140 	 * @param path
141 	 *            location of the file to read, relative to this objects
142 	 *            directory (e.g.
143 	 *            <code>cb/95df6ab7ae9e57571511ef451cf33767c26dd2</code> or
144 	 *            <code>pack/pack-035760ab452d6eebd123add421f253ce7682355a.pack</code>).
145 	 * @return a stream to read from the file. Never null.
146 	 * @throws FileNotFoundException
147 	 *             the requested file does not exist at the given location.
148 	 * @throws IOException
149 	 *             The connection is unable to read the remote's file, and the
150 	 *             failure occurred prior to being able to determine if the file
151 	 *             exists, or after it was determined to exist but before the
152 	 *             stream could be created.
153 	 */
154 	abstract FileStream open(String path) throws FileNotFoundException,
155 			IOException;
156 
157 	/**
158 	 * Create a new connection for a discovered alternate object database
159 	 * <p>
160 	 * This method is typically called by {@link #readAlternates(String)} when
161 	 * subclasses us the generic alternate parsing logic for their
162 	 * implementation of {@link #getAlternates()}.
163 	 *
164 	 * @param location
165 	 *            the location of the new alternate, relative to the current
166 	 *            object database.
167 	 * @return a new database connection that can read from the specified
168 	 *         alternate.
169 	 * @throws IOException
170 	 *             The database connection cannot be established with the
171 	 *             alternate, such as if the alternate location does not
172 	 *             actually exist and the connection's constructor attempts to
173 	 *             verify that.
174 	 */
175 	abstract WalkRemoteObjectDatabase openAlternate(String location)
176 			throws IOException;
177 
178 	/**
179 	 * Close any resources used by this connection.
180 	 * <p>
181 	 * If the remote repository is contacted by a network socket this method
182 	 * must close that network socket, disconnecting the two peers. If the
183 	 * remote repository is actually local (same system) this method must close
184 	 * any open file handles used to read the "remote" repository.
185 	 */
186 	abstract void close();
187 
188 	/**
189 	 * Delete a file from the object database.
190 	 * <p>
191 	 * Path may start with <code>../</code> to request deletion of a file that
192 	 * resides in the repository itself.
193 	 * <p>
194 	 * When possible empty directories must be removed, up to but not including
195 	 * the current object database directory itself.
196 	 * <p>
197 	 * This method does not support deletion of directories.
198 	 *
199 	 * @param path
200 	 *            name of the item to be removed, relative to the current object
201 	 *            database.
202 	 * @throws IOException
203 	 *             deletion is not supported, or deletion failed.
204 	 */
205 	void deleteFile(final String path) throws IOException {
206 		throw new IOException(MessageFormat.format(JGitText.get().deletingNotSupported, path));
207 	}
208 
209 	/**
210 	 * Open a remote file for writing.
211 	 * <p>
212 	 * Path may start with <code>../</code> to request writing of a file that
213 	 * resides in the repository itself.
214 	 * <p>
215 	 * The requested path may or may not exist. If the path already exists as a
216 	 * file the file should be truncated and completely replaced.
217 	 * <p>
218 	 * This method creates any missing parent directories, if necessary.
219 	 *
220 	 * @param path
221 	 *            name of the file to write, relative to the current object
222 	 *            database.
223 	 * @return stream to write into this file. Caller must close the stream to
224 	 *         complete the write request. The stream is not buffered and each
225 	 *         write may cause a network request/response so callers should
226 	 *         buffer to smooth out small writes.
227 	 * @param monitor
228 	 *            (optional) progress monitor to post write completion to during
229 	 *            the stream's close method.
230 	 * @param monitorTask
231 	 *            (optional) task name to display during the close method.
232 	 * @throws IOException
233 	 *             writing is not supported, or attempting to write the file
234 	 *             failed, possibly due to permissions or remote disk full, etc.
235 	 */
236 	OutputStream writeFile(final String path, final ProgressMonitor monitor,
237 			final String monitorTask) throws IOException {
238 		throw new IOException(MessageFormat.format(JGitText.get().writingNotSupported, path));
239 	}
240 
241 	/**
242 	 * Atomically write a remote file.
243 	 * <p>
244 	 * This method attempts to perform as atomic of an update as it can,
245 	 * reducing (or eliminating) the time that clients might be able to see
246 	 * partial file content. This method is not suitable for very large
247 	 * transfers as the complete content must be passed as an argument.
248 	 * <p>
249 	 * Path may start with <code>../</code> to request writing of a file that
250 	 * resides in the repository itself.
251 	 * <p>
252 	 * The requested path may or may not exist. If the path already exists as a
253 	 * file the file should be truncated and completely replaced.
254 	 * <p>
255 	 * This method creates any missing parent directories, if necessary.
256 	 *
257 	 * @param path
258 	 *            name of the file to write, relative to the current object
259 	 *            database.
260 	 * @param data
261 	 *            complete new content of the file.
262 	 * @throws IOException
263 	 *             writing is not supported, or attempting to write the file
264 	 *             failed, possibly due to permissions or remote disk full, etc.
265 	 */
266 	void writeFile(final String path, final byte[] data) throws IOException {
267 		final OutputStream os = writeFile(path, null, null);
268 		try {
269 			os.write(data);
270 		} finally {
271 			os.close();
272 		}
273 	}
274 
275 	/**
276 	 * Delete a loose ref from the remote repository.
277 	 *
278 	 * @param name
279 	 *            name of the ref within the ref space, for example
280 	 *            <code>refs/heads/pu</code>.
281 	 * @throws IOException
282 	 *             deletion is not supported, or deletion failed.
283 	 */
284 	void deleteRef(final String name) throws IOException {
285 		deleteFile(ROOT_DIR + name);
286 	}
287 
288 	/**
289 	 * Delete a reflog from the remote repository.
290 	 *
291 	 * @param name
292 	 *            name of the ref within the ref space, for example
293 	 *            <code>refs/heads/pu</code>.
294 	 * @throws IOException
295 	 *             deletion is not supported, or deletion failed.
296 	 */
297 	void deleteRefLog(final String name) throws IOException {
298 		deleteFile(ROOT_DIR + Constants.LOGS + "/" + name); //$NON-NLS-1$
299 	}
300 
301 	/**
302 	 * Overwrite (or create) a loose ref in the remote repository.
303 	 * <p>
304 	 * This method creates any missing parent directories, if necessary.
305 	 *
306 	 * @param name
307 	 *            name of the ref within the ref space, for example
308 	 *            <code>refs/heads/pu</code>.
309 	 * @param value
310 	 *            new value to store in this ref. Must not be null.
311 	 * @throws IOException
312 	 *             writing is not supported, or attempting to write the file
313 	 *             failed, possibly due to permissions or remote disk full, etc.
314 	 */
315 	void writeRef(final String name, final ObjectId value) throws IOException {
316 		final ByteArrayOutputStream b;
317 
318 		b = new ByteArrayOutputStream(Constants.OBJECT_ID_STRING_LENGTH + 1);
319 		value.copyTo(b);
320 		b.write('\n');
321 
322 		writeFile(ROOT_DIR + name, b.toByteArray());
323 	}
324 
325 	/**
326 	 * Rebuild the {@link #INFO_PACKS} for dumb transport clients.
327 	 * <p>
328 	 * This method rebuilds the contents of the {@link #INFO_PACKS} file to
329 	 * match the passed list of pack names.
330 	 *
331 	 * @param packNames
332 	 *            names of available pack files, in the order they should appear
333 	 *            in the file. Valid pack name strings are of the form
334 	 *            <code>pack-035760ab452d6eebd123add421f253ce7682355a.pack</code>.
335 	 * @throws IOException
336 	 *             writing is not supported, or attempting to write the file
337 	 *             failed, possibly due to permissions or remote disk full, etc.
338 	 */
339 	void writeInfoPacks(final Collection<String> packNames) throws IOException {
340 		final StringBuilder w = new StringBuilder();
341 		for (final String n : packNames) {
342 			w.append("P "); //$NON-NLS-1$
343 			w.append(n);
344 			w.append('\n');
345 		}
346 		writeFile(INFO_PACKS, Constants.encodeASCII(w.toString()));
347 	}
348 
349 	/**
350 	 * Open a buffered reader around a file.
351 	 * <p>
352 	 * This method is suitable for for reading line-oriented resources like
353 	 * <code>info/packs</code>, <code>info/refs</code>, and the alternates list.
354 	 *
355 	 * @return a stream to read from the file. Never null.
356 	 * @param path
357 	 *            location of the file to read, relative to this objects
358 	 *            directory (e.g. <code>info/packs</code>).
359 	 * @throws FileNotFoundException
360 	 *             the requested file does not exist at the given location.
361 	 * @throws IOException
362 	 *             The connection is unable to read the remote's file, and the
363 	 *             failure occurred prior to being able to determine if the file
364 	 *             exists, or after it was determined to exist but before the
365 	 *             stream could be created.
366 	 */
367 	BufferedReader openReader(final String path) throws IOException {
368 		final InputStream is = open(path).in;
369 		return new BufferedReader(new InputStreamReader(is, Constants.CHARSET));
370 	}
371 
372 	/**
373 	 * Read a standard Git alternates file to discover other object databases.
374 	 * <p>
375 	 * This method is suitable for reading the standard formats of the
376 	 * alternates file, such as found in <code>objects/info/alternates</code>
377 	 * or <code>objects/info/http-alternates</code> within a Git repository.
378 	 * <p>
379 	 * Alternates appear one per line, with paths expressed relative to this
380 	 * object database.
381 	 *
382 	 * @param listPath
383 	 *            location of the alternate file to read, relative to this
384 	 *            object database (e.g. <code>info/alternates</code>).
385 	 * @return the list of discovered alternates. Empty list if the file exists,
386 	 *         but no entries were discovered.
387 	 * @throws FileNotFoundException
388 	 *             the requested file does not exist at the given location.
389 	 * @throws IOException
390 	 *             The connection is unable to read the remote's file, and the
391 	 *             failure occurred prior to being able to determine if the file
392 	 *             exists, or after it was determined to exist but before the
393 	 *             stream could be created.
394 	 */
395 	Collection<WalkRemoteObjectDatabase> readAlternates(final String listPath)
396 			throws IOException {
397 		final BufferedReader br = openReader(listPath);
398 		try {
399 			final Collection<WalkRemoteObjectDatabase> alts = new ArrayList<>();
400 			for (;;) {
401 				String line = br.readLine();
402 				if (line == null)
403 					break;
404 				if (!line.endsWith("/")) //$NON-NLS-1$
405 					line += "/"; //$NON-NLS-1$
406 				alts.add(openAlternate(line));
407 			}
408 			return alts;
409 		} finally {
410 			br.close();
411 		}
412 	}
413 
414 	/**
415 	 * Read a standard Git packed-refs file to discover known references.
416 	 *
417 	 * @param avail
418 	 *            return collection of references. Any existing entries will be
419 	 *            replaced if they are found in the packed-refs file.
420 	 * @throws org.eclipse.jgit.errors.TransportException
421 	 *             an error occurred reading from the packed refs file.
422 	 */
423 	protected void readPackedRefs(final Map<String, Ref> avail)
424 			throws TransportException {
425 		try {
426 			final BufferedReader br = openReader(ROOT_DIR
427 					+ Constants.PACKED_REFS);
428 			try {
429 				readPackedRefsImpl(avail, br);
430 			} finally {
431 				br.close();
432 			}
433 		} catch (FileNotFoundException notPacked) {
434 			// Perhaps it wasn't worthwhile, or is just an older repository.
435 		} catch (IOException e) {
436 			throw new TransportException(getURI(), JGitText.get().errorInPackedRefs, e);
437 		}
438 	}
439 
440 	private void readPackedRefsImpl(final Map<String, Ref> avail,
441 			final BufferedReader br) throws IOException {
442 		Ref last = null;
443 		boolean peeled = false;
444 		for (;;) {
445 			String line = br.readLine();
446 			if (line == null)
447 				break;
448 			if (line.charAt(0) == '#') {
449 				if (line.startsWith(RefDirectory.PACKED_REFS_HEADER)) {
450 					line = line.substring(RefDirectory.PACKED_REFS_HEADER.length());
451 					peeled = line.contains(RefDirectory.PACKED_REFS_PEELED);
452 				}
453 				continue;
454 			}
455 			if (line.charAt(0) == '^') {
456 				if (last == null)
457 					throw new TransportException(JGitText.get().peeledLineBeforeRef);
458 				final ObjectId id = ObjectId.fromString(line.substring(1));
459 				last = new ObjectIdRef.PeeledTag(Ref.Storage.PACKED, last
460 						.getName(), last.getObjectId(), id);
461 				avail.put(last.getName(), last);
462 				continue;
463 			}
464 
465 			final int sp = line.indexOf(' ');
466 			if (sp < 0)
467 				throw new TransportException(MessageFormat.format(JGitText.get().unrecognizedRef, line));
468 			final ObjectId id = ObjectId.fromString(line.substring(0, sp));
469 			final String name = line.substring(sp + 1);
470 			if (peeled)
471 				last = new ObjectIdRef.PeeledNonTag(Ref.Storage.PACKED, name, id);
472 			else
473 				last = new ObjectIdRef.Unpeeled(Ref.Storage.PACKED, name, id);
474 			avail.put(last.getName(), last);
475 		}
476 	}
477 
478 	static final class FileStream {
479 		final InputStream in;
480 
481 		final long length;
482 
483 		/**
484 		 * Create a new stream of unknown length.
485 		 *
486 		 * @param i
487 		 *            stream containing the file data. This stream will be
488 		 *            closed by the caller when reading is complete.
489 		 */
490 		FileStream(final InputStream i) {
491 			in = i;
492 			length = -1;
493 		}
494 
495 		/**
496 		 * Create a new stream of known length.
497 		 *
498 		 * @param i
499 		 *            stream containing the file data. This stream will be
500 		 *            closed by the caller when reading is complete.
501 		 * @param n
502 		 *            total number of bytes available for reading through
503 		 *            <code>i</code>.
504 		 */
505 		FileStream(final InputStream i, final long n) {
506 			in = i;
507 			length = n;
508 		}
509 
510 		byte[] toArray() throws IOException {
511 			try {
512 				if (length >= 0) {
513 					final byte[] r = new byte[(int) length];
514 					IO.readFully(in, r, 0, r.length);
515 					return r;
516 				}
517 
518 				final ByteArrayOutputStream r = new ByteArrayOutputStream();
519 				final byte[] buf = new byte[2048];
520 				int n;
521 				while ((n = in.read(buf)) >= 0)
522 					r.write(buf, 0, n);
523 				return r.toByteArray();
524 			} finally {
525 				in.close();
526 			}
527 		}
528 	}
529 }