blob: 326c5f6457579aa1aa77fab860bbb48775303bcc [file] [log] [blame]
/*
* 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.ConfigConstants;
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.
* <p>
* 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;
private final boolean trustFolderStat;
/**
* Initialize a reference to an on-disk object directory.
*
* @param config
* configuration for the loose objects handler.
* @param dir
* the location of the <code>objects</code> directory.
*/
LooseObjects(Config config, File dir) {
directory = dir;
unpackedObjectCache = new UnpackedObjectCache();
trustFolderStat = config.getBoolean(
ConfigConstants.CONFIG_CORE_SECTION,
ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, true);
}
/**
* Getter for the field <code>directory</code>.
*
* @return the location of the <code>objects</code> 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) {
boolean exists = hasWithoutRefresh(objectId);
if (trustFolderStat || 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<ObjectId> 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 {
return getObjectLoaderWithoutRefresh(curs, path, id);
} catch (FileNotFoundException e) {
if (trustFolderStat) {
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);
}
}
/**
* <p>
* Getter for the field <code>unpackedObjectCache</code>.
* </p>
* 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 (trustFolderStat) {
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);
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);
}
}