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 }