View Javadoc
1   /*
2    * Copyright (C) 2009, Google Inc. 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  
11  package org.eclipse.jgit.internal.storage.file;
12  
13  import java.io.File;
14  import java.io.FileInputStream;
15  import java.io.FileNotFoundException;
16  import java.io.IOException;
17  import java.nio.file.Files;
18  import java.nio.file.NoSuchFileException;
19  import java.nio.file.StandardCopyOption;
20  import java.text.MessageFormat;
21  import java.util.Set;
22  
23  import org.eclipse.jgit.internal.JGitText;
24  import org.eclipse.jgit.internal.storage.file.FileObjectDatabase.InsertLooseObjectResult;
25  import org.eclipse.jgit.lib.AbbreviatedObjectId;
26  import org.eclipse.jgit.lib.AnyObjectId;
27  import org.eclipse.jgit.lib.Constants;
28  import org.eclipse.jgit.lib.ObjectId;
29  import org.eclipse.jgit.lib.ObjectLoader;
30  import org.eclipse.jgit.util.FileUtils;
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  
34  /**
35   * Traditional file system based loose objects handler.
36   * <p>
37   * This is the loose object representation for a Git object database, where
38   * objects are stored loose by hashing them into directories by their
39   * {@link org.eclipse.jgit.lib.ObjectId}.
40   */
41  class LooseObjects {
42  	private static final Logger LOG = LoggerFactory
43  			.getLogger(LooseObjects.class);
44  
45  	/**
46  	 * Maximum number of attempts to read a loose object for which a stale file
47  	 * handle exception is thrown
48  	 */
49  	private final static int MAX_LOOSE_OBJECT_STALE_READ_ATTEMPTS = 5;
50  
51  	private final File directory;
52  
53  	private final UnpackedObjectCache unpackedObjectCache;
54  
55  	/**
56  	 * Initialize a reference to an on-disk object directory.
57  	 *
58  	 * @param dir
59  	 *            the location of the <code>objects</code> directory.
60  	 */
61  	LooseObjects(File dir) {
62  		directory = dir;
63  		unpackedObjectCache = new UnpackedObjectCache();
64  	}
65  
66  	/**
67  	 * Getter for the field <code>directory</code>.
68  	 *
69  	 * @return the location of the <code>objects</code> directory.
70  	 */
71  	File getDirectory() {
72  		return directory;
73  	}
74  
75  	void create() throws IOException {
76  		FileUtils.mkdirs(directory);
77  	}
78  
79  	void close() {
80  		unpackedObjectCache().clear();
81  	}
82  
83  	/** {@inheritDoc} */
84  	@Override
85  	public String toString() {
86  		return "LooseObjects[" + directory + "]"; //$NON-NLS-1$ //$NON-NLS-2$
87  	}
88  
89  	boolean hasCached(AnyObjectId id) {
90  		return unpackedObjectCache().isUnpacked(id);
91  	}
92  
93  	/**
94  	 * Does the requested object exist as a loose object?
95  	 *
96  	 * @param objectId
97  	 *            identity of the object to test for existence of.
98  	 * @return {@code true} if the specified object is stored as a loose object.
99  	 */
100 	boolean has(AnyObjectId objectId) {
101 		return fileFor(objectId).exists();
102 	}
103 
104 	/**
105 	 * Find objects matching the prefix abbreviation.
106 	 *
107 	 * @param matches
108 	 *            set to add any located ObjectIds to. This is an output
109 	 *            parameter.
110 	 * @param id
111 	 *            prefix to search for.
112 	 * @param matchLimit
113 	 *            maximum number of results to return. At most this many
114 	 *            ObjectIds should be added to matches before returning.
115 	 * @return {@code true} if the matches were exhausted before reaching
116 	 *         {@code maxLimit}.
117 	 */
118 	boolean resolve(Set<ObjectId> matches, AbbreviatedObjectId id,
119 			int matchLimit) {
120 		String fanOut = id.name().substring(0, 2);
121 		String[] entries = new File(directory, fanOut).list();
122 		if (entries != null) {
123 			for (String e : entries) {
124 				if (e.length() != Constants.OBJECT_ID_STRING_LENGTH - 2) {
125 					continue;
126 				}
127 				try {
128 					ObjectId entId = ObjectId.fromString(fanOut + e);
129 					if (id.prefixCompare(entId) == 0) {
130 						matches.add(entId);
131 					}
132 				} catch (IllegalArgumentException notId) {
133 					continue;
134 				}
135 				if (matches.size() > matchLimit) {
136 					return false;
137 				}
138 			}
139 		}
140 		return true;
141 	}
142 
143 	ObjectLoader open(WindowCursor curs, AnyObjectId id) throws IOException {
144 		int readAttempts = 0;
145 		while (readAttempts < MAX_LOOSE_OBJECT_STALE_READ_ATTEMPTS) {
146 			readAttempts++;
147 			File path = fileFor(id);
148 			try {
149 				return getObjectLoader(curs, path, id);
150 			} catch (FileNotFoundException noFile) {
151 				if (path.exists()) {
152 					throw noFile;
153 				}
154 				break;
155 			} catch (IOException e) {
156 				if (!FileUtils.isStaleFileHandleInCausalChain(e)) {
157 					throw e;
158 				}
159 				if (LOG.isDebugEnabled()) {
160 					LOG.debug(MessageFormat.format(
161 							JGitText.get().looseObjectHandleIsStale, id.name(),
162 							Integer.valueOf(readAttempts), Integer.valueOf(
163 									MAX_LOOSE_OBJECT_STALE_READ_ATTEMPTS)));
164 				}
165 			}
166 		}
167 		unpackedObjectCache().remove(id);
168 		return null;
169 	}
170 
171 	/**
172 	 * Provides a loader for an objectId
173 	 *
174 	 * @param curs
175 	 *            cursor on the database
176 	 * @param path
177 	 *            the path of the loose object
178 	 * @param id
179 	 *            the object id
180 	 * @return a loader for the loose file object
181 	 * @throws IOException
182 	 *             when file does not exist or it could not be opened
183 	 */
184 	ObjectLoader getObjectLoader(WindowCursor curs, File path, AnyObjectId id)
185 			throws IOException {
186 		try (FileInputStream in = new FileInputStream(path)) {
187 			unpackedObjectCache().add(id);
188 			return UnpackedObject.open(in, path, id, curs);
189 		}
190 	}
191 
192 	/**
193 	 * <p>
194 	 * Getter for the field <code>unpackedObjectCache</code>.
195 	 * </p>
196 	 * This accessor is particularly useful to allow mocking of this class for
197 	 * testing purposes.
198 	 *
199 	 * @return the cache of the objects currently unpacked.
200 	 */
201 	UnpackedObjectCache unpackedObjectCache() {
202 		return unpackedObjectCache;
203 	}
204 
205 	long getSize(WindowCursor curs, AnyObjectId id) throws IOException {
206 		File f = fileFor(id);
207 		try (FileInputStream in = new FileInputStream(f)) {
208 			unpackedObjectCache().add(id);
209 			return UnpackedObject.getSize(in, id, curs);
210 		} catch (FileNotFoundException noFile) {
211 			if (f.exists()) {
212 				throw noFile;
213 			}
214 			unpackedObjectCache().remove(id);
215 			return -1;
216 		}
217 	}
218 
219 	InsertLooseObjectResult insert(File tmp, ObjectId id) throws IOException {
220 		final File dst = fileFor(id);
221 		if (dst.exists()) {
222 			// We want to be extra careful and avoid replacing an object
223 			// that already exists. We can't be sure renameTo() would
224 			// fail on all platforms if dst exists, so we check first.
225 			//
226 			FileUtils.delete(tmp, FileUtils.RETRY);
227 			return InsertLooseObjectResult.EXISTS_LOOSE;
228 		}
229 
230 		try {
231 			return tryMove(tmp, dst, id);
232 		} catch (NoSuchFileException e) {
233 			// It's possible the directory doesn't exist yet as the object
234 			// directories are always lazily created. Note that we try the
235 			// rename/move first as the directory likely does exist.
236 			//
237 			// Create the directory.
238 			//
239 			FileUtils.mkdir(dst.getParentFile(), true);
240 		} catch (IOException e) {
241 			// Any other IO error is considered a failure.
242 			//
243 			LOG.error(e.getMessage(), e);
244 			FileUtils.delete(tmp, FileUtils.RETRY);
245 			return InsertLooseObjectResult.FAILURE;
246 		}
247 
248 		try {
249 			return tryMove(tmp, dst, id);
250 		} catch (IOException e) {
251 			// The object failed to be renamed into its proper location and
252 			// it doesn't exist in the repository either. We really don't
253 			// know what went wrong, so fail.
254 			//
255 			LOG.error(e.getMessage(), e);
256 			FileUtils.delete(tmp, FileUtils.RETRY);
257 			return InsertLooseObjectResult.FAILURE;
258 		}
259 	}
260 
261 	private InsertLooseObjectResult tryMove(File tmp, File dst, ObjectId id)
262 			throws IOException {
263 		Files.move(FileUtils.toPath(tmp), FileUtils.toPath(dst),
264 				StandardCopyOption.ATOMIC_MOVE);
265 		dst.setReadOnly();
266 		unpackedObjectCache().add(id);
267 		return InsertLooseObjectResult.INSERTED;
268 	}
269 
270 	/**
271 	 * Compute the location of a loose object file.
272 	 *
273 	 * @param objectId
274 	 *            identity of the object to get the File location for.
275 	 * @return {@link java.io.File} location of the specified loose object.
276 	 */
277 	File fileFor(AnyObjectId objectId) {
278 		String n = objectId.name();
279 		String d = n.substring(0, 2);
280 		String f = n.substring(2);
281 		return new File(new File(getDirectory(), d), f);
282 	}
283 }