/* * Copyright (C) 2009, Google Inc. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.internal.storage.file; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.StandardCopyOption; import java.text.MessageFormat; import java.util.Set; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.file.FileObjectDatabase.InsertLooseObjectResult; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.CoreConfig; import org.eclipse.jgit.lib.CoreConfig.TrustStat; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.util.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Traditional file system based loose objects handler. *

* This is the loose object representation for a Git object database, where * objects are stored loose by hashing them into directories by their * {@link org.eclipse.jgit.lib.ObjectId}. */ class LooseObjects { private static final Logger LOG = LoggerFactory .getLogger(LooseObjects.class); /** * Maximum number of attempts to read a loose object for which a stale file * handle exception is thrown */ private final static int MAX_STALE_READ_RETRIES = 5; private final File directory; private final UnpackedObjectCache unpackedObjectCache; private final TrustStat trustLooseObjectStat; /** * Initialize a reference to an on-disk object directory. * * @param config * configuration for the loose objects handler. * @param dir * the location of the objects directory. */ LooseObjects(Config config, File dir) { directory = dir; unpackedObjectCache = new UnpackedObjectCache(); trustLooseObjectStat = config.get(CoreConfig.KEY) .getTrustLooseObjectStat(); } /** * Getter for the field directory. * * @return the location of the objects directory. */ File getDirectory() { return directory; } void create() throws IOException { FileUtils.mkdirs(directory); } void close() { unpackedObjectCache().clear(); } @Override public String toString() { return "LooseObjects[" + directory + "]"; //$NON-NLS-1$ //$NON-NLS-2$ } boolean hasCached(AnyObjectId id) { return unpackedObjectCache().isUnpacked(id); } /** * Does the requested object exist as a loose object? * * @param objectId * identity of the object to test for existence of. * @return {@code true} if the specified object is stored as a loose object. */ boolean has(AnyObjectId objectId) { boolean exists = hasWithoutRefresh(objectId); if (trustLooseObjectStat == TrustStat.ALWAYS || exists) { return exists; } try (InputStream stream = Files.newInputStream(directory.toPath())) { // refresh directory to work around NFS caching issue } catch (IOException e) { return false; } return hasWithoutRefresh(objectId); } private boolean hasWithoutRefresh(AnyObjectId objectId) { return fileFor(objectId).exists(); } /** * Find objects matching the prefix abbreviation. * * @param matches * set to add any located ObjectIds to. This is an output * parameter. * @param id * prefix to search for. * @param matchLimit * maximum number of results to return. At most this many * ObjectIds should be added to matches before returning. * @return {@code true} if the matches were exhausted before reaching * {@code maxLimit}. */ boolean resolve(Set matches, AbbreviatedObjectId id, int matchLimit) { String fanOut = id.name().substring(0, 2); String[] entries = new File(directory, fanOut).list(); if (entries != null) { for (String e : entries) { if (e.length() != Constants.OBJECT_ID_STRING_LENGTH - 2) { continue; } try { ObjectId entId = ObjectId.fromString(fanOut + e); if (id.prefixCompare(entId) == 0) { matches.add(entId); } } catch (IllegalArgumentException notId) { continue; } if (matches.size() > matchLimit) { return false; } } } return true; } ObjectLoader open(WindowCursor curs, AnyObjectId id) throws IOException { File path = fileFor(id); for (int retries = 0; retries < MAX_STALE_READ_RETRIES; retries++) { boolean reload = true; switch (trustLooseObjectStat) { case NEVER: break; case AFTER_OPEN: try (InputStream stream = Files .newInputStream(path.getParentFile().toPath())) { // open the loose object's fanout directory to refresh // attributes (on some NFS clients) } catch (FileNotFoundException | NoSuchFileException e) { // ignore } //$FALL-THROUGH$ case ALWAYS: if (!path.exists()) { reload = false; } break; case INHERIT: // only used in CoreConfig internally throw new IllegalStateException(); } if (reload) { try { return getObjectLoader(curs, path, id); } catch (FileNotFoundException noFile) { if (path.exists()) { throw noFile; } break; } catch (IOException e) { if (!FileUtils.isStaleFileHandleInCausalChain(e)) { throw e; } if (LOG.isDebugEnabled()) { LOG.debug(MessageFormat.format( JGitText.get().looseObjectHandleIsStale, id.name(), Integer.valueOf(retries), Integer.valueOf(MAX_STALE_READ_RETRIES))); } } } } unpackedObjectCache().remove(id); return null; } /** * Provides a loader for an objectId * * @param curs * cursor on the database * @param path * the path of the loose object * @param id * the object id * @return a loader for the loose file object * @throws IOException * when file does not exist or it could not be opened */ ObjectLoader getObjectLoader(WindowCursor curs, File path, AnyObjectId id) throws IOException { try { return getObjectLoaderWithoutRefresh(curs, path, id); } catch (FileNotFoundException e) { if (trustLooseObjectStat == TrustStat.ALWAYS) { throw e; } try (InputStream stream = Files .newInputStream(directory.toPath())) { // refresh directory to work around NFS caching issues } return getObjectLoaderWithoutRefresh(curs, path, id); } } private ObjectLoader getObjectLoaderWithoutRefresh(WindowCursor curs, File path, AnyObjectId id) throws IOException { try (FileInputStream in = new FileInputStream(path)) { unpackedObjectCache().add(id); return UnpackedObject.open(in, path, id, curs); } } /** *

* Getter for the field unpackedObjectCache. *

* This accessor is particularly useful to allow mocking of this class for * testing purposes. * * @return the cache of the objects currently unpacked. */ UnpackedObjectCache unpackedObjectCache() { return unpackedObjectCache; } long getSize(WindowCursor curs, AnyObjectId id) throws IOException { try { return getSizeWithoutRefresh(curs, id); } catch (FileNotFoundException noFile) { try { if (trustLooseObjectStat == TrustStat.ALWAYS) { throw noFile; } try (InputStream stream = Files .newInputStream(directory.toPath())) { // refresh directory to work around NFS caching issue } return getSizeWithoutRefresh(curs, id); } catch (FileNotFoundException unused) { if (fileFor(id).exists()) { throw noFile; } unpackedObjectCache().remove(id); return -1; } } } private long getSizeWithoutRefresh(WindowCursor curs, AnyObjectId id) throws IOException { File f = fileFor(id); try (FileInputStream in = new FileInputStream(f)) { unpackedObjectCache().add(id); return UnpackedObject.getSize(in, id, curs); } } InsertLooseObjectResult insert(File tmp, ObjectId id) throws IOException { final File dst = fileFor(id); if (dst.exists()) { // We want to be extra careful and avoid replacing an object // that already exists. We can't be sure renameTo() would // fail on all platforms if dst exists, so we check first. // FileUtils.delete(tmp, FileUtils.RETRY | FileUtils.SKIP_MISSING); return InsertLooseObjectResult.EXISTS_LOOSE; } try { return tryMove(tmp, dst, id); } catch (NoSuchFileException e) { // It's possible the directory doesn't exist yet as the object // directories are always lazily created. Note that we try the // rename/move first as the directory likely does exist. // // Create the directory. // FileUtils.mkdir(dst.getParentFile(), true); } catch (IOException e) { // Any other IO error is considered a failure. // LOG.error(e.getMessage(), e); FileUtils.delete(tmp, FileUtils.RETRY | FileUtils.SKIP_MISSING); return InsertLooseObjectResult.FAILURE; } try { return tryMove(tmp, dst, id); } catch (IOException e) { // The object failed to be renamed into its proper location and // it doesn't exist in the repository either. We really don't // know what went wrong, so fail. // LOG.error(e.getMessage(), e); FileUtils.delete(tmp, FileUtils.RETRY | FileUtils.SKIP_MISSING); return InsertLooseObjectResult.FAILURE; } } private InsertLooseObjectResult tryMove(File tmp, File dst, ObjectId id) throws IOException { Files.move(FileUtils.toPath(tmp), FileUtils.toPath(dst), StandardCopyOption.ATOMIC_MOVE); dst.setReadOnly(); unpackedObjectCache().add(id); return InsertLooseObjectResult.INSERTED; } /** * Compute the location of a loose object file. * * @param objectId * identity of the object to get the File location for. * @return {@link java.io.File} location of the specified loose object. */ File fileFor(AnyObjectId objectId) { String n = objectId.name(); String d = n.substring(0, 2); String f = n.substring(2); return new File(new File(getDirectory(), d), f); } }