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