| /* |
| * 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); |
| } |
| } |