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