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 }