/* * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available * under the terms of the Eclipse Distribution License v1.0 which * accompanies this distribution, is reproduced below, and is * available at http://www.eclipse.org/org/documents/edl-v10.php * * All rights reserved. * * Redistribution and use in source and binary forms, with or * without modification, are permitted provided that the following * conditions are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * - Neither the name of the Eclipse Foundation, Inc. nor the * names of its contributors may be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.eclipse.jgit.transport; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Map; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.file.RefDirectory; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.util.IO; /** * Transfers object data through a dumb transport. * <p> * Implementations are responsible for resolving path names relative to the * <code>objects/</code> subdirectory of a single remote Git repository or * naked object database and make the content available as a Java input stream * for reading during fetch. The actual object traversal logic to determine the * names of files to retrieve is handled through the generic, protocol * independent {@link WalkFetchConnection}. */ abstract class WalkRemoteObjectDatabase { static final String ROOT_DIR = "../"; //$NON-NLS-1$ static final String INFO_PACKS = "info/packs"; //$NON-NLS-1$ static final String INFO_ALTERNATES = "info/alternates"; //$NON-NLS-1$ static final String INFO_HTTP_ALTERNATES = "info/http-alternates"; //$NON-NLS-1$ static final String INFO_REFS = ROOT_DIR + Constants.INFO_REFS; abstract URIish getURI(); /** * Obtain the list of available packs (if any). * <p> * Pack names should be the file name in the packs directory, that is * <code>pack-035760ab452d6eebd123add421f253ce7682355a.pack</code>. Index * names should not be included in the returned collection. * * @return list of pack names; null or empty list if none are available. * @throws IOException * The connection is unable to read the remote repository's list * of available pack files. */ abstract Collection<String> getPackNames() throws IOException; /** * Obtain alternate connections to alternate object databases (if any). * <p> * Alternates are typically read from the file {@link #INFO_ALTERNATES} or * {@link #INFO_HTTP_ALTERNATES}. The content of each line must be resolved * by the implementation and a new database reference should be returned to * represent the additional location. * <p> * Alternates may reuse the same network connection handle, however the * fetch connection will {@link #close()} each created alternate. * * @return list of additional object databases the caller could fetch from; * null or empty list if none are configured. * @throws IOException * The connection is unable to read the remote repository's list * of configured alternates. */ abstract Collection<WalkRemoteObjectDatabase> getAlternates() throws IOException; /** * Open a single file for reading. * <p> * Implementors should make every attempt possible to ensure * {@link FileNotFoundException} is used when the remote object does not * exist. However when fetching over HTTP some misconfigured servers may * generate a 200 OK status message (rather than a 404 Not Found) with an * HTML formatted message explaining the requested resource does not exist. * Callers such as {@link WalkFetchConnection} are prepared to handle this * by validating the content received, and assuming content that fails to * match its hash is an incorrectly phrased FileNotFoundException. * <p> * This method is recommended for already compressed files like loose objects * and pack files. For text files, see {@link #openReader(String)}. * * @param path * location of the file to read, relative to this objects * directory (e.g. * <code>cb/95df6ab7ae9e57571511ef451cf33767c26dd2</code> or * <code>pack/pack-035760ab452d6eebd123add421f253ce7682355a.pack</code>). * @return a stream to read from the file. Never null. * @throws FileNotFoundException * the requested file does not exist at the given location. * @throws IOException * The connection is unable to read the remote's file, and the * failure occurred prior to being able to determine if the file * exists, or after it was determined to exist but before the * stream could be created. */ abstract FileStream open(String path) throws FileNotFoundException, IOException; /** * Create a new connection for a discovered alternate object database * <p> * This method is typically called by {@link #readAlternates(String)} when * subclasses us the generic alternate parsing logic for their * implementation of {@link #getAlternates()}. * * @param location * the location of the new alternate, relative to the current * object database. * @return a new database connection that can read from the specified * alternate. * @throws IOException * The database connection cannot be established with the * alternate, such as if the alternate location does not * actually exist and the connection's constructor attempts to * verify that. */ abstract WalkRemoteObjectDatabase openAlternate(String location) throws IOException; /** * Close any resources used by this connection. * <p> * If the remote repository is contacted by a network socket this method * must close that network socket, disconnecting the two peers. If the * remote repository is actually local (same system) this method must close * any open file handles used to read the "remote" repository. */ abstract void close(); /** * Delete a file from the object database. * <p> * Path may start with <code>../</code> to request deletion of a file that * resides in the repository itself. * <p> * When possible empty directories must be removed, up to but not including * the current object database directory itself. * <p> * This method does not support deletion of directories. * * @param path * name of the item to be removed, relative to the current object * database. * @throws IOException * deletion is not supported, or deletion failed. */ void deleteFile(final String path) throws IOException { throw new IOException(MessageFormat.format(JGitText.get().deletingNotSupported, path)); } /** * Open a remote file for writing. * <p> * Path may start with <code>../</code> to request writing of a file that * resides in the repository itself. * <p> * The requested path may or may not exist. If the path already exists as a * file the file should be truncated and completely replaced. * <p> * This method creates any missing parent directories, if necessary. * * @param path * name of the file to write, relative to the current object * database. * @return stream to write into this file. Caller must close the stream to * complete the write request. The stream is not buffered and each * write may cause a network request/response so callers should * buffer to smooth out small writes. * @param monitor * (optional) progress monitor to post write completion to during * the stream's close method. * @param monitorTask * (optional) task name to display during the close method. * @throws IOException * writing is not supported, or attempting to write the file * failed, possibly due to permissions or remote disk full, etc. */ OutputStream writeFile(final String path, final ProgressMonitor monitor, final String monitorTask) throws IOException { throw new IOException(MessageFormat.format(JGitText.get().writingNotSupported, path)); } /** * Atomically write a remote file. * <p> * This method attempts to perform as atomic of an update as it can, * reducing (or eliminating) the time that clients might be able to see * partial file content. This method is not suitable for very large * transfers as the complete content must be passed as an argument. * <p> * Path may start with <code>../</code> to request writing of a file that * resides in the repository itself. * <p> * The requested path may or may not exist. If the path already exists as a * file the file should be truncated and completely replaced. * <p> * This method creates any missing parent directories, if necessary. * * @param path * name of the file to write, relative to the current object * database. * @param data * complete new content of the file. * @throws IOException * writing is not supported, or attempting to write the file * failed, possibly due to permissions or remote disk full, etc. */ void writeFile(final String path, final byte[] data) throws IOException { final OutputStream os = writeFile(path, null, null); try { os.write(data); } finally { os.close(); } } /** * Delete a loose ref from the remote repository. * * @param name * name of the ref within the ref space, for example * <code>refs/heads/pu</code>. * @throws IOException * deletion is not supported, or deletion failed. */ void deleteRef(final String name) throws IOException { deleteFile(ROOT_DIR + name); } /** * Delete a reflog from the remote repository. * * @param name * name of the ref within the ref space, for example * <code>refs/heads/pu</code>. * @throws IOException * deletion is not supported, or deletion failed. */ void deleteRefLog(final String name) throws IOException { deleteFile(ROOT_DIR + Constants.LOGS + "/" + name); //$NON-NLS-1$ } /** * Overwrite (or create) a loose ref in the remote repository. * <p> * This method creates any missing parent directories, if necessary. * * @param name * name of the ref within the ref space, for example * <code>refs/heads/pu</code>. * @param value * new value to store in this ref. Must not be null. * @throws IOException * writing is not supported, or attempting to write the file * failed, possibly due to permissions or remote disk full, etc. */ void writeRef(final String name, final ObjectId value) throws IOException { final ByteArrayOutputStream b; b = new ByteArrayOutputStream(Constants.OBJECT_ID_STRING_LENGTH + 1); value.copyTo(b); b.write('\n'); writeFile(ROOT_DIR + name, b.toByteArray()); } /** * Rebuild the {@link #INFO_PACKS} for dumb transport clients. * <p> * This method rebuilds the contents of the {@link #INFO_PACKS} file to * match the passed list of pack names. * * @param packNames * names of available pack files, in the order they should appear * in the file. Valid pack name strings are of the form * <code>pack-035760ab452d6eebd123add421f253ce7682355a.pack</code>. * @throws IOException * writing is not supported, or attempting to write the file * failed, possibly due to permissions or remote disk full, etc. */ void writeInfoPacks(final Collection<String> packNames) throws IOException { final StringBuilder w = new StringBuilder(); for (final String n : packNames) { w.append("P "); //$NON-NLS-1$ w.append(n); w.append('\n'); } writeFile(INFO_PACKS, Constants.encodeASCII(w.toString())); } /** * Open a buffered reader around a file. * <p> * This method is suitable for for reading line-oriented resources like * <code>info/packs</code>, <code>info/refs</code>, and the alternates list. * * @return a stream to read from the file. Never null. * @param path * location of the file to read, relative to this objects * directory (e.g. <code>info/packs</code>). * @throws FileNotFoundException * the requested file does not exist at the given location. * @throws IOException * The connection is unable to read the remote's file, and the * failure occurred prior to being able to determine if the file * exists, or after it was determined to exist but before the * stream could be created. */ BufferedReader openReader(final String path) throws IOException { final InputStream is = open(path).in; return new BufferedReader(new InputStreamReader(is, Constants.CHARSET)); } /** * Read a standard Git alternates file to discover other object databases. * <p> * This method is suitable for reading the standard formats of the * alternates file, such as found in <code>objects/info/alternates</code> * or <code>objects/info/http-alternates</code> within a Git repository. * <p> * Alternates appear one per line, with paths expressed relative to this * object database. * * @param listPath * location of the alternate file to read, relative to this * object database (e.g. <code>info/alternates</code>). * @return the list of discovered alternates. Empty list if the file exists, * but no entries were discovered. * @throws FileNotFoundException * the requested file does not exist at the given location. * @throws IOException * The connection is unable to read the remote's file, and the * failure occurred prior to being able to determine if the file * exists, or after it was determined to exist but before the * stream could be created. */ Collection<WalkRemoteObjectDatabase> readAlternates(final String listPath) throws IOException { final BufferedReader br = openReader(listPath); try { final Collection<WalkRemoteObjectDatabase> alts = new ArrayList<WalkRemoteObjectDatabase>(); for (;;) { String line = br.readLine(); if (line == null) break; if (!line.endsWith("/")) //$NON-NLS-1$ line += "/"; //$NON-NLS-1$ alts.add(openAlternate(line)); } return alts; } finally { br.close(); } } /** * Read a standard Git packed-refs file to discover known references. * * @param avail * return collection of references. Any existing entries will be * replaced if they are found in the packed-refs file. * @throws TransportException * an error occurred reading from the packed refs file. */ protected void readPackedRefs(final Map<String, Ref> avail) throws TransportException { try { final BufferedReader br = openReader(ROOT_DIR + Constants.PACKED_REFS); try { readPackedRefsImpl(avail, br); } finally { br.close(); } } catch (FileNotFoundException notPacked) { // Perhaps it wasn't worthwhile, or is just an older repository. } catch (IOException e) { throw new TransportException(getURI(), JGitText.get().errorInPackedRefs, e); } } private void readPackedRefsImpl(final Map<String, Ref> avail, final BufferedReader br) throws IOException { Ref last = null; boolean peeled = false; for (;;) { String line = br.readLine(); if (line == null) break; if (line.charAt(0) == '#') { if (line.startsWith(RefDirectory.PACKED_REFS_HEADER)) { line = line.substring(RefDirectory.PACKED_REFS_HEADER.length()); peeled = line.contains(RefDirectory.PACKED_REFS_PEELED); } continue; } if (line.charAt(0) == '^') { if (last == null) throw new TransportException(JGitText.get().peeledLineBeforeRef); final ObjectId id = ObjectId.fromString(line.substring(1)); last = new ObjectIdRef.PeeledTag(Ref.Storage.PACKED, last .getName(), last.getObjectId(), id); avail.put(last.getName(), last); continue; } final int sp = line.indexOf(' '); if (sp < 0) throw new TransportException(MessageFormat.format(JGitText.get().unrecognizedRef, line)); final ObjectId id = ObjectId.fromString(line.substring(0, sp)); final String name = line.substring(sp + 1); if (peeled) last = new ObjectIdRef.PeeledNonTag(Ref.Storage.PACKED, name, id); else last = new ObjectIdRef.Unpeeled(Ref.Storage.PACKED, name, id); avail.put(last.getName(), last); } } static final class FileStream { final InputStream in; final long length; /** * Create a new stream of unknown length. * * @param i * stream containing the file data. This stream will be * closed by the caller when reading is complete. */ FileStream(final InputStream i) { in = i; length = -1; } /** * Create a new stream of known length. * * @param i * stream containing the file data. This stream will be * closed by the caller when reading is complete. * @param n * total number of bytes available for reading through * <code>i</code>. */ FileStream(final InputStream i, final long n) { in = i; length = n; } byte[] toArray() throws IOException { try { if (length >= 0) { final byte[] r = new byte[(int) length]; IO.readFully(in, r, 0, r.length); return r; } final ByteArrayOutputStream r = new ByteArrayOutputStream(); final byte[] buf = new byte[2048]; int n; while ((n = in.read(buf)) >= 0) r.write(buf, 0, n); return r.toByteArray(); } finally { in.close(); } } } }