/* * 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.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.Constants; 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_LOOSE_OBJECT_STALE_READ_ATTEMPTS = 5; private final File directory; private final UnpackedObjectCache unpackedObjectCache; /** * Initialize a reference to an on-disk object directory. * * @param dir * the location of the objects directory. */ LooseObjects(File dir) { directory = dir; unpackedObjectCache = new UnpackedObjectCache(); } /** * 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(); } /** {@inheritDoc} */ @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) { 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 { int readAttempts = 0; while (readAttempts < MAX_LOOSE_OBJECT_STALE_READ_ATTEMPTS) { readAttempts++; File path = fileFor(id); 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(readAttempts), Integer.valueOf( MAX_LOOSE_OBJECT_STALE_READ_ATTEMPTS))); } } } 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 (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 { File f = fileFor(id); try (FileInputStream in = new FileInputStream(f)) { unpackedObjectCache().add(id); return UnpackedObject.getSize(in, id, curs); } catch (FileNotFoundException noFile) { if (f.exists()) { throw noFile; } unpackedObjectCache().remove(id); return -1; } } 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); 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); 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); 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); } }