WalkRemoteObjectDatabase.java

  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. package org.eclipse.jgit.transport;

  11. import static java.nio.charset.StandardCharsets.UTF_8;

  12. import java.io.BufferedReader;
  13. import java.io.ByteArrayOutputStream;
  14. import java.io.FileNotFoundException;
  15. import java.io.IOException;
  16. import java.io.InputStream;
  17. import java.io.InputStreamReader;
  18. import java.io.OutputStream;
  19. import java.text.MessageFormat;
  20. import java.util.ArrayList;
  21. import java.util.Collection;
  22. import java.util.Map;

  23. import org.eclipse.jgit.errors.TransportException;
  24. import org.eclipse.jgit.internal.JGitText;
  25. import org.eclipse.jgit.internal.storage.file.RefDirectory;
  26. import org.eclipse.jgit.lib.Constants;
  27. import org.eclipse.jgit.lib.ObjectId;
  28. import org.eclipse.jgit.lib.ObjectIdRef;
  29. import org.eclipse.jgit.lib.ProgressMonitor;
  30. import org.eclipse.jgit.lib.Ref;
  31. import org.eclipse.jgit.util.IO;

  32. /**
  33.  * Transfers object data through a dumb transport.
  34.  * <p>
  35.  * Implementations are responsible for resolving path names relative to the
  36.  * <code>objects/</code> subdirectory of a single remote Git repository or
  37.  * naked object database and make the content available as a Java input stream
  38.  * for reading during fetch. The actual object traversal logic to determine the
  39.  * names of files to retrieve is handled through the generic, protocol
  40.  * independent {@link WalkFetchConnection}.
  41.  */
  42. abstract class WalkRemoteObjectDatabase {
  43.     static final String ROOT_DIR = "../"; //$NON-NLS-1$

  44.     static final String INFO_PACKS = "info/packs"; //$NON-NLS-1$

  45.     static final String INFO_REFS = ROOT_DIR + Constants.INFO_REFS;

  46.     abstract URIish getURI();

  47.     /**
  48.      * Obtain the list of available packs (if any).
  49.      * <p>
  50.      * Pack names should be the file name in the packs directory, that is
  51.      * <code>pack-035760ab452d6eebd123add421f253ce7682355a.pack</code>. Index
  52.      * names should not be included in the returned collection.
  53.      *
  54.      * @return list of pack names; null or empty list if none are available.
  55.      * @throws IOException
  56.      *             The connection is unable to read the remote repository's list
  57.      *             of available pack files.
  58.      */
  59.     abstract Collection<String> getPackNames() throws IOException;

  60.     /**
  61.      * Obtain alternate connections to alternate object databases (if any).
  62.      * <p>
  63.          * Alternates are typically read from the file
  64.          * {@link org.eclipse.jgit.lib.Constants#INFO_ALTERNATES} or
  65.          * {@link org.eclipse.jgit.lib.Constants#INFO_HTTP_ALTERNATES}.
  66.          * The content of each line must be resolved
  67.      * by the implementation and a new database reference should be returned to
  68.      * represent the additional location.
  69.      * <p>
  70.      * Alternates may reuse the same network connection handle, however the
  71.      * fetch connection will {@link #close()} each created alternate.
  72.      *
  73.      * @return list of additional object databases the caller could fetch from;
  74.      *         null or empty list if none are configured.
  75.      * @throws IOException
  76.      *             The connection is unable to read the remote repository's list
  77.      *             of configured alternates.
  78.      */
  79.     abstract Collection<WalkRemoteObjectDatabase> getAlternates()
  80.             throws IOException;

  81.     /**
  82.      * Open a single file for reading.
  83.      * <p>
  84.      * Implementors should make every attempt possible to ensure
  85.      * {@link FileNotFoundException} is used when the remote object does not
  86.      * exist. However when fetching over HTTP some misconfigured servers may
  87.      * generate a 200 OK status message (rather than a 404 Not Found) with an
  88.      * HTML formatted message explaining the requested resource does not exist.
  89.      * Callers such as {@link WalkFetchConnection} are prepared to handle this
  90.      * by validating the content received, and assuming content that fails to
  91.      * match its hash is an incorrectly phrased FileNotFoundException.
  92.      * <p>
  93.      * This method is recommended for already compressed files like loose objects
  94.      * and pack files. For text files, see {@link #openReader(String)}.
  95.      *
  96.      * @param path
  97.      *            location of the file to read, relative to this objects
  98.      *            directory (e.g.
  99.      *            <code>cb/95df6ab7ae9e57571511ef451cf33767c26dd2</code> or
  100.      *            <code>pack/pack-035760ab452d6eebd123add421f253ce7682355a.pack</code>).
  101.      * @return a stream to read from the file. Never null.
  102.      * @throws FileNotFoundException
  103.      *             the requested file does not exist at the given location.
  104.      * @throws IOException
  105.      *             The connection is unable to read the remote's file, and the
  106.      *             failure occurred prior to being able to determine if the file
  107.      *             exists, or after it was determined to exist but before the
  108.      *             stream could be created.
  109.      */
  110.     abstract FileStream open(String path) throws FileNotFoundException,
  111.             IOException;

  112.     /**
  113.      * Create a new connection for a discovered alternate object database
  114.      * <p>
  115.      * This method is typically called by {@link #readAlternates(String)} when
  116.      * subclasses us the generic alternate parsing logic for their
  117.      * implementation of {@link #getAlternates()}.
  118.      *
  119.      * @param location
  120.      *            the location of the new alternate, relative to the current
  121.      *            object database.
  122.      * @return a new database connection that can read from the specified
  123.      *         alternate.
  124.      * @throws IOException
  125.      *             The database connection cannot be established with the
  126.      *             alternate, such as if the alternate location does not
  127.      *             actually exist and the connection's constructor attempts to
  128.      *             verify that.
  129.      */
  130.     abstract WalkRemoteObjectDatabase openAlternate(String location)
  131.             throws IOException;

  132.     /**
  133.      * Close any resources used by this connection.
  134.      * <p>
  135.      * If the remote repository is contacted by a network socket this method
  136.      * must close that network socket, disconnecting the two peers. If the
  137.      * remote repository is actually local (same system) this method must close
  138.      * any open file handles used to read the "remote" repository.
  139.      */
  140.     abstract void close();

  141.     /**
  142.      * Delete a file from the object database.
  143.      * <p>
  144.      * Path may start with <code>../</code> to request deletion of a file that
  145.      * resides in the repository itself.
  146.      * <p>
  147.      * When possible empty directories must be removed, up to but not including
  148.      * the current object database directory itself.
  149.      * <p>
  150.      * This method does not support deletion of directories.
  151.      *
  152.      * @param path
  153.      *            name of the item to be removed, relative to the current object
  154.      *            database.
  155.      * @throws IOException
  156.      *             deletion is not supported, or deletion failed.
  157.      */
  158.     void deleteFile(String path) throws IOException {
  159.         throw new IOException(MessageFormat.format(JGitText.get().deletingNotSupported, path));
  160.     }

  161.     /**
  162.      * Open a remote file for writing.
  163.      * <p>
  164.      * Path may start with <code>../</code> to request writing of a file that
  165.      * resides in the repository itself.
  166.      * <p>
  167.      * The requested path may or may not exist. If the path already exists as a
  168.      * file the file should be truncated and completely replaced.
  169.      * <p>
  170.      * This method creates any missing parent directories, if necessary.
  171.      *
  172.      * @param path
  173.      *            name of the file to write, relative to the current object
  174.      *            database.
  175.      * @return stream to write into this file. Caller must close the stream to
  176.      *         complete the write request. The stream is not buffered and each
  177.      *         write may cause a network request/response so callers should
  178.      *         buffer to smooth out small writes.
  179.      * @param monitor
  180.      *            (optional) progress monitor to post write completion to during
  181.      *            the stream's close method.
  182.      * @param monitorTask
  183.      *            (optional) task name to display during the close method.
  184.      * @throws IOException
  185.      *             writing is not supported, or attempting to write the file
  186.      *             failed, possibly due to permissions or remote disk full, etc.
  187.      */
  188.     OutputStream writeFile(final String path, final ProgressMonitor monitor,
  189.             final String monitorTask) throws IOException {
  190.         throw new IOException(MessageFormat.format(JGitText.get().writingNotSupported, path));
  191.     }

  192.     /**
  193.      * Atomically write a remote file.
  194.      * <p>
  195.      * This method attempts to perform as atomic of an update as it can,
  196.      * reducing (or eliminating) the time that clients might be able to see
  197.      * partial file content. This method is not suitable for very large
  198.      * transfers as the complete content must be passed as an argument.
  199.      * <p>
  200.      * Path may start with <code>../</code> to request writing of a file that
  201.      * resides in the repository itself.
  202.      * <p>
  203.      * The requested path may or may not exist. If the path already exists as a
  204.      * file the file should be truncated and completely replaced.
  205.      * <p>
  206.      * This method creates any missing parent directories, if necessary.
  207.      *
  208.      * @param path
  209.      *            name of the file to write, relative to the current object
  210.      *            database.
  211.      * @param data
  212.      *            complete new content of the file.
  213.      * @throws IOException
  214.      *             writing is not supported, or attempting to write the file
  215.      *             failed, possibly due to permissions or remote disk full, etc.
  216.      */
  217.     void writeFile(String path, byte[] data) throws IOException {
  218.         try (OutputStream os = writeFile(path, null, null)) {
  219.             os.write(data);
  220.         }
  221.     }

  222.     /**
  223.      * Delete a loose ref from the remote repository.
  224.      *
  225.      * @param name
  226.      *            name of the ref within the ref space, for example
  227.      *            <code>refs/heads/pu</code>.
  228.      * @throws IOException
  229.      *             deletion is not supported, or deletion failed.
  230.      */
  231.     void deleteRef(String name) throws IOException {
  232.         deleteFile(ROOT_DIR + name);
  233.     }

  234.     /**
  235.      * Delete a reflog from the remote repository.
  236.      *
  237.      * @param name
  238.      *            name of the ref within the ref space, for example
  239.      *            <code>refs/heads/pu</code>.
  240.      * @throws IOException
  241.      *             deletion is not supported, or deletion failed.
  242.      */
  243.     void deleteRefLog(String name) throws IOException {
  244.         deleteFile(ROOT_DIR + Constants.LOGS + "/" + name); //$NON-NLS-1$
  245.     }

  246.     /**
  247.      * Overwrite (or create) a loose ref in the remote repository.
  248.      * <p>
  249.      * This method creates any missing parent directories, if necessary.
  250.      *
  251.      * @param name
  252.      *            name of the ref within the ref space, for example
  253.      *            <code>refs/heads/pu</code>.
  254.      * @param value
  255.      *            new value to store in this ref. Must not be null.
  256.      * @throws IOException
  257.      *             writing is not supported, or attempting to write the file
  258.      *             failed, possibly due to permissions or remote disk full, etc.
  259.      */
  260.     void writeRef(String name, ObjectId value) throws IOException {
  261.         final ByteArrayOutputStream b;

  262.         b = new ByteArrayOutputStream(Constants.OBJECT_ID_STRING_LENGTH + 1);
  263.         value.copyTo(b);
  264.         b.write('\n');

  265.         writeFile(ROOT_DIR + name, b.toByteArray());
  266.     }

  267.     /**
  268.      * Rebuild the {@link #INFO_PACKS} for dumb transport clients.
  269.      * <p>
  270.      * This method rebuilds the contents of the {@link #INFO_PACKS} file to
  271.      * match the passed list of pack names.
  272.      *
  273.      * @param packNames
  274.      *            names of available pack files, in the order they should appear
  275.      *            in the file. Valid pack name strings are of the form
  276.      *            <code>pack-035760ab452d6eebd123add421f253ce7682355a.pack</code>.
  277.      * @throws IOException
  278.      *             writing is not supported, or attempting to write the file
  279.      *             failed, possibly due to permissions or remote disk full, etc.
  280.      */
  281.     void writeInfoPacks(Collection<String> packNames) throws IOException {
  282.         final StringBuilder w = new StringBuilder();
  283.         for (String n : packNames) {
  284.             w.append("P "); //$NON-NLS-1$
  285.             w.append(n);
  286.             w.append('\n');
  287.         }
  288.         writeFile(INFO_PACKS, Constants.encodeASCII(w.toString()));
  289.     }

  290.     /**
  291.      * Open a buffered reader around a file.
  292.      * <p>
  293.      * This method is suitable for reading line-oriented resources like
  294.      * <code>info/packs</code>, <code>info/refs</code>, and the alternates list.
  295.      *
  296.      * @return a stream to read from the file. Never null.
  297.      * @param path
  298.      *            location of the file to read, relative to this objects
  299.      *            directory (e.g. <code>info/packs</code>).
  300.      * @throws FileNotFoundException
  301.      *             the requested file does not exist at the given location.
  302.      * @throws IOException
  303.      *             The connection is unable to read the remote's file, and the
  304.      *             failure occurred prior to being able to determine if the file
  305.      *             exists, or after it was determined to exist but before the
  306.      *             stream could be created.
  307.      */
  308.     BufferedReader openReader(String path) throws IOException {
  309.         final InputStream is = open(path).in;
  310.         return new BufferedReader(new InputStreamReader(is, UTF_8));
  311.     }

  312.     /**
  313.      * Read a standard Git alternates file to discover other object databases.
  314.      * <p>
  315.      * This method is suitable for reading the standard formats of the
  316.      * alternates file, such as found in <code>objects/info/alternates</code>
  317.      * or <code>objects/info/http-alternates</code> within a Git repository.
  318.      * <p>
  319.      * Alternates appear one per line, with paths expressed relative to this
  320.      * object database.
  321.      *
  322.      * @param listPath
  323.      *            location of the alternate file to read, relative to this
  324.      *            object database (e.g. <code>info/alternates</code>).
  325.      * @return the list of discovered alternates. Empty list if the file exists,
  326.      *         but no entries were discovered.
  327.      * @throws FileNotFoundException
  328.      *             the requested file does not exist at the given location.
  329.      * @throws IOException
  330.      *             The connection is unable to read the remote's file, and the
  331.      *             failure occurred prior to being able to determine if the file
  332.      *             exists, or after it was determined to exist but before the
  333.      *             stream could be created.
  334.      */
  335.     Collection<WalkRemoteObjectDatabase> readAlternates(final String listPath)
  336.             throws IOException {
  337.         try (BufferedReader br = openReader(listPath)) {
  338.             final Collection<WalkRemoteObjectDatabase> alts = new ArrayList<>();
  339.             for (;;) {
  340.                 String line = br.readLine();
  341.                 if (line == null)
  342.                     break;
  343.                 if (!line.endsWith("/")) //$NON-NLS-1$
  344.                     line += "/"; //$NON-NLS-1$
  345.                 alts.add(openAlternate(line));
  346.             }
  347.             return alts;
  348.         }
  349.     }

  350.     /**
  351.      * Read a standard Git packed-refs file to discover known references.
  352.      *
  353.      * @param avail
  354.      *            return collection of references. Any existing entries will be
  355.      *            replaced if they are found in the packed-refs file.
  356.      * @throws org.eclipse.jgit.errors.TransportException
  357.      *             an error occurred reading from the packed refs file.
  358.      */
  359.     protected void readPackedRefs(Map<String, Ref> avail)
  360.             throws TransportException {
  361.         try (BufferedReader br = openReader(ROOT_DIR + Constants.PACKED_REFS)) {
  362.             readPackedRefsImpl(avail, br);
  363.         } catch (FileNotFoundException notPacked) {
  364.             // Perhaps it wasn't worthwhile, or is just an older repository.
  365.         } catch (IOException e) {
  366.             throw new TransportException(getURI(), JGitText.get().errorInPackedRefs, e);
  367.         }
  368.     }

  369.     private void readPackedRefsImpl(final Map<String, Ref> avail,
  370.             final BufferedReader br) throws IOException {
  371.         Ref last = null;
  372.         boolean peeled = false;
  373.         for (;;) {
  374.             String line = br.readLine();
  375.             if (line == null)
  376.                 break;
  377.             if (line.charAt(0) == '#') {
  378.                 if (line.startsWith(RefDirectory.PACKED_REFS_HEADER)) {
  379.                     line = line.substring(RefDirectory.PACKED_REFS_HEADER.length());
  380.                     peeled = line.contains(RefDirectory.PACKED_REFS_PEELED);
  381.                 }
  382.                 continue;
  383.             }
  384.             if (line.charAt(0) == '^') {
  385.                 if (last == null)
  386.                     throw new TransportException(JGitText.get().peeledLineBeforeRef);
  387.                 final ObjectId id = ObjectId.fromString(line.substring(1));
  388.                 last = new ObjectIdRef.PeeledTag(Ref.Storage.PACKED, last
  389.                         .getName(), last.getObjectId(), id);
  390.                 avail.put(last.getName(), last);
  391.                 continue;
  392.             }

  393.             final int sp = line.indexOf(' ');
  394.             if (sp < 0)
  395.                 throw new TransportException(MessageFormat.format(JGitText.get().unrecognizedRef, line));
  396.             final ObjectId id = ObjectId.fromString(line.substring(0, sp));
  397.             final String name = line.substring(sp + 1);
  398.             if (peeled)
  399.                 last = new ObjectIdRef.PeeledNonTag(Ref.Storage.PACKED, name, id);
  400.             else
  401.                 last = new ObjectIdRef.Unpeeled(Ref.Storage.PACKED, name, id);
  402.             avail.put(last.getName(), last);
  403.         }
  404.     }

  405.     static final class FileStream {
  406.         final InputStream in;

  407.         final long length;

  408.         /**
  409.          * Create a new stream of unknown length.
  410.          *
  411.          * @param i
  412.          *            stream containing the file data. This stream will be
  413.          *            closed by the caller when reading is complete.
  414.          */
  415.         FileStream(InputStream i) {
  416.             in = i;
  417.             length = -1;
  418.         }

  419.         /**
  420.          * Create a new stream of known length.
  421.          *
  422.          * @param i
  423.          *            stream containing the file data. This stream will be
  424.          *            closed by the caller when reading is complete.
  425.          * @param n
  426.          *            total number of bytes available for reading through
  427.          *            <code>i</code>.
  428.          */
  429.         FileStream(InputStream i, long n) {
  430.             in = i;
  431.             length = n;
  432.         }

  433.         byte[] toArray() throws IOException {
  434.             try {
  435.                 if (length >= 0) {
  436.                     final byte[] r = new byte[(int) length];
  437.                     IO.readFully(in, r, 0, r.length);
  438.                     return r;
  439.                 }

  440.                 final ByteArrayOutputStream r = new ByteArrayOutputStream();
  441.                 final byte[] buf = new byte[2048];
  442.                 int n;
  443.                 while ((n = in.read(buf)) >= 0)
  444.                     r.write(buf, 0, n);
  445.                 return r.toByteArray();
  446.             } finally {
  447.                 in.close();
  448.             }
  449.         }
  450.     }
  451. }