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 }