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