You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

FilestoreManager.java 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. /*
  2. * Copyright 2015 gitblit.com.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package com.gitblit.manager;
  17. import java.io.EOFException;
  18. import java.io.File;
  19. import java.io.FileInputStream;
  20. import java.io.FileOutputStream;
  21. import java.io.FileReader;
  22. import java.io.IOException;
  23. import java.io.InputStream;
  24. import java.io.OutputStream;
  25. import java.io.RandomAccessFile;
  26. import java.lang.reflect.Type;
  27. import java.nio.file.Files;
  28. import java.text.MessageFormat;
  29. import java.util.ArrayList;
  30. import java.util.Collection;
  31. import java.util.Date;
  32. import java.util.Iterator;
  33. import java.util.List;
  34. import java.util.Map;
  35. import java.util.concurrent.ConcurrentHashMap;
  36. import java.util.regex.Pattern;
  37. import org.apache.commons.codec.digest.DigestUtils;
  38. import org.apache.commons.io.FileUtils;
  39. import org.apache.commons.io.IOUtils;
  40. import org.slf4j.Logger;
  41. import org.slf4j.LoggerFactory;
  42. import com.gitblit.IStoredSettings;
  43. import com.gitblit.Keys;
  44. import com.gitblit.models.FilestoreModel;
  45. import com.gitblit.models.FilestoreModel.Status;
  46. import com.gitblit.models.RepositoryModel;
  47. import com.gitblit.models.UserModel;
  48. import com.gitblit.utils.ArrayUtils;
  49. import com.gitblit.utils.JsonUtils.GmtDateTypeAdapter;
  50. import com.google.gson.ExclusionStrategy;
  51. import com.google.gson.Gson;
  52. import com.google.gson.GsonBuilder;
  53. import com.google.gson.reflect.TypeToken;
  54. import com.google.inject.Inject;
  55. import com.google.inject.Singleton;
  56. /**
  57. * FilestoreManager handles files uploaded via:
  58. * + git-lfs
  59. * + ticket attachment (TBD)
  60. *
  61. * Files are stored using their SHA256 hash (as per git-lfs)
  62. * If the same file is uploaded through different repositories no additional space is used
  63. * Access is controlled through the current repository permissions.
  64. *
  65. * TODO: Identify what and how the actual BLOBs should work with federation
  66. *
  67. * @author Paul Martin
  68. *
  69. */
  70. @Singleton
  71. public class FilestoreManager implements IFilestoreManager {
  72. private final Logger logger = LoggerFactory.getLogger(getClass());
  73. private final IRuntimeManager runtimeManager;
  74. private final IRepositoryManager repositoryManager;
  75. private final IStoredSettings settings;
  76. public static final int UNDEFINED_SIZE = -1;
  77. private static final String METAFILE = "filestore.json";
  78. private static final String METAFILE_TMP = "filestore.json.tmp";
  79. protected static final Type METAFILE_TYPE = new TypeToken<Collection<FilestoreModel>>() {}.getType();
  80. private Map<String, FilestoreModel > fileCache = new ConcurrentHashMap<String, FilestoreModel>();
  81. @Inject
  82. FilestoreManager(
  83. IRuntimeManager runtimeManager,
  84. IRepositoryManager repositoryManager) {
  85. this.runtimeManager = runtimeManager;
  86. this.repositoryManager = repositoryManager;
  87. this.settings = runtimeManager.getSettings();
  88. }
  89. @Override
  90. public IManager start() {
  91. // Try to load any existing metadata
  92. File dir = getStorageFolder();
  93. dir.mkdirs();
  94. File metadata = new File(dir, METAFILE);
  95. if (metadata.exists()) {
  96. Collection<FilestoreModel> items = null;
  97. Gson gson = gson();
  98. try (FileReader file = new FileReader(metadata)) {
  99. items = gson.fromJson(file, METAFILE_TYPE);
  100. file.close();
  101. } catch (IOException e) {
  102. e.printStackTrace();
  103. }
  104. for(Iterator<FilestoreModel> itr = items.iterator(); itr.hasNext(); ) {
  105. FilestoreModel model = itr.next();
  106. fileCache.put(model.oid, model);
  107. }
  108. logger.info("Loaded {} items from filestore metadata file", fileCache.size());
  109. }
  110. else
  111. {
  112. logger.info("No filestore metadata file found");
  113. }
  114. return this;
  115. }
  116. @Override
  117. public IManager stop() {
  118. return this;
  119. }
  120. @Override
  121. public boolean isValidOid(String oid) {
  122. //NOTE: Assuming SHA256 support only as per git-lfs
  123. return Pattern.matches("[a-fA-F0-9]{64}", oid);
  124. }
  125. @Override
  126. public FilestoreModel.Status addObject(String oid, long size, UserModel user, RepositoryModel repo) {
  127. //Handle access control
  128. if (!user.canPush(repo)) {
  129. if (user == UserModel.ANONYMOUS) {
  130. return Status.AuthenticationRequired;
  131. } else {
  132. return Status.Error_Unauthorized;
  133. }
  134. }
  135. //Handle object details
  136. if (!isValidOid(oid)) { return Status.Error_Invalid_Oid; }
  137. if (fileCache.containsKey(oid)) {
  138. FilestoreModel item = fileCache.get(oid);
  139. if (!item.isInErrorState() && (size != UNDEFINED_SIZE) && (item.getSize() != size)) {
  140. return Status.Error_Size_Mismatch;
  141. }
  142. item.addRepository(repo.name);
  143. if (item.isInErrorState()) {
  144. item.reset(user, size);
  145. }
  146. } else {
  147. if (size < 0) {return Status.Error_Invalid_Size; }
  148. if ((getMaxUploadSize() != UNDEFINED_SIZE) && (size > getMaxUploadSize())) { return Status.Error_Exceeds_Size_Limit; }
  149. FilestoreModel model = new FilestoreModel(oid, size, user, repo.name);
  150. fileCache.put(oid, model);
  151. saveFilestoreModel(model);
  152. }
  153. return fileCache.get(oid).getStatus();
  154. }
  155. @Override
  156. public FilestoreModel.Status uploadBlob(String oid, long size, UserModel user, RepositoryModel repo, InputStream streamIn) {
  157. //Access control and object logic
  158. Status state = addObject(oid, size, user, repo);
  159. if (state != Status.Upload_Pending) {
  160. return state;
  161. }
  162. FilestoreModel model = fileCache.get(oid);
  163. if (!model.actionUpload(user)) {
  164. return Status.Upload_In_Progress;
  165. } else {
  166. long actualSize = 0;
  167. File file = getStoragePath(oid);
  168. try {
  169. file.getParentFile().mkdirs();
  170. file.createNewFile();
  171. try (FileOutputStream streamOut = new FileOutputStream(file)) {
  172. actualSize = IOUtils.copyLarge(streamIn, streamOut);
  173. streamOut.flush();
  174. streamOut.close();
  175. if (model.getSize() != actualSize) {
  176. model.setStatus(Status.Error_Size_Mismatch, user);
  177. logger.warn(MessageFormat.format("Failed to upload blob {0} due to size mismatch, expected {1} got {2}",
  178. oid, model.getSize(), actualSize));
  179. } else {
  180. String actualOid = "";
  181. try (FileInputStream fileForHash = new FileInputStream(file)) {
  182. actualOid = DigestUtils.sha256Hex(fileForHash);
  183. fileForHash.close();
  184. }
  185. if (oid.equalsIgnoreCase(actualOid)) {
  186. model.setStatus(Status.Available, user);
  187. } else {
  188. model.setStatus(Status.Error_Hash_Mismatch, user);
  189. logger.warn(MessageFormat.format("Failed to upload blob {0} due to hash mismatch, got {1}", oid, actualOid));
  190. }
  191. }
  192. }
  193. } catch (Exception e) {
  194. model.setStatus(Status.Error_Unknown, user);
  195. logger.warn(MessageFormat.format("Failed to upload blob {0}", oid), e);
  196. } finally {
  197. saveFilestoreModel(model);
  198. }
  199. if (model.isInErrorState()) {
  200. file.delete();
  201. model.removeRepository(repo.name);
  202. }
  203. }
  204. return model.getStatus();
  205. }
  206. private FilestoreModel.Status canGetObject(String oid, UserModel user, RepositoryModel repo) {
  207. //Access Control
  208. if (!user.canView(repo)) {
  209. if (user == UserModel.ANONYMOUS) {
  210. return Status.AuthenticationRequired;
  211. } else {
  212. return Status.Error_Unauthorized;
  213. }
  214. }
  215. //Object Logic
  216. if (!isValidOid(oid)) {
  217. return Status.Error_Invalid_Oid;
  218. }
  219. if (!fileCache.containsKey(oid)) {
  220. return Status.Unavailable;
  221. }
  222. FilestoreModel item = fileCache.get(oid);
  223. if (item.getStatus() == Status.Available) {
  224. return Status.Available;
  225. }
  226. return Status.Unavailable;
  227. }
  228. @Override
  229. public FilestoreModel getObject(String oid, UserModel user, RepositoryModel repo) {
  230. if (canGetObject(oid, user, repo) == Status.Available) {
  231. return fileCache.get(oid);
  232. }
  233. return null;
  234. }
  235. @Override
  236. public FilestoreModel.Status downloadBlob(String oid, UserModel user, RepositoryModel repo, OutputStream streamOut) {
  237. //Access control and object logic
  238. Status status = canGetObject(oid, user, repo);
  239. if (status != Status.Available) {
  240. return status;
  241. }
  242. FilestoreModel item = fileCache.get(oid);
  243. if (streamOut != null) {
  244. try (FileInputStream streamIn = new FileInputStream(getStoragePath(oid))) {
  245. IOUtils.copyLarge(streamIn, streamOut);
  246. streamOut.flush();
  247. streamIn.close();
  248. } catch (EOFException e) {
  249. logger.error(MessageFormat.format("Client aborted connection for {0}", oid), e);
  250. return Status.Error_Unexpected_Stream_End;
  251. } catch (Exception e) {
  252. logger.error(MessageFormat.format("Failed to download blob {0}", oid), e);
  253. return Status.Error_Unknown;
  254. }
  255. }
  256. return item.getStatus();
  257. }
  258. @Override
  259. public List<FilestoreModel> getAllObjects(UserModel user) {
  260. final List<RepositoryModel> viewableRepositories = repositoryManager.getRepositoryModels(user);
  261. List<String> viewableRepositoryNames = new ArrayList<String>(viewableRepositories.size());
  262. for (RepositoryModel repository : viewableRepositories) {
  263. viewableRepositoryNames.add(repository.name);
  264. }
  265. if (viewableRepositoryNames.size() == 0) {
  266. return null;
  267. }
  268. final Collection<FilestoreModel> allFiles = fileCache.values();
  269. List<FilestoreModel> userViewableFiles = new ArrayList<FilestoreModel>(allFiles.size());
  270. for (FilestoreModel file : allFiles) {
  271. if (file.isInRepositoryList(viewableRepositoryNames)) {
  272. userViewableFiles.add(file);
  273. }
  274. }
  275. return userViewableFiles;
  276. }
  277. @Override
  278. public File getStorageFolder() {
  279. return runtimeManager.getFileOrFolder(Keys.filestore.storageFolder, "${baseFolder}/lfs");
  280. }
  281. @Override
  282. public File getStoragePath(String oid) {
  283. return new File(getStorageFolder(), oid.substring(0, 2).concat("/").concat(oid.substring(2)));
  284. }
  285. @Override
  286. public long getMaxUploadSize() {
  287. return settings.getLong(Keys.filestore.maxUploadSize, -1);
  288. }
  289. @Override
  290. public long getFilestoreUsedByteCount() {
  291. Iterator<FilestoreModel> iterator = fileCache.values().iterator();
  292. long total = 0;
  293. while (iterator.hasNext()) {
  294. FilestoreModel item = iterator.next();
  295. if (item.getStatus() == Status.Available) {
  296. total += item.getSize();
  297. }
  298. }
  299. return total;
  300. }
  301. @Override
  302. public long getFilestoreAvailableByteCount() {
  303. try {
  304. return Files.getFileStore(getStorageFolder().toPath()).getUsableSpace();
  305. } catch (IOException e) {
  306. logger.error(MessageFormat.format("Failed to retrive available space in Filestore {0}", e));
  307. }
  308. return UNDEFINED_SIZE;
  309. };
  310. private synchronized void saveFilestoreModel(FilestoreModel model) {
  311. File metaFile = new File(getStorageFolder(), METAFILE);
  312. File metaFileTmp = new File(getStorageFolder(), METAFILE_TMP);
  313. boolean isNewFile = false;
  314. try {
  315. if (!metaFile.exists()) {
  316. metaFile.getParentFile().mkdirs();
  317. metaFile.createNewFile();
  318. isNewFile = true;
  319. }
  320. FileUtils.copyFile(metaFile, metaFileTmp);
  321. } catch (IOException e) {
  322. logger.error("Writing filestore model to file {0}, {1}", METAFILE, e);
  323. }
  324. try (RandomAccessFile fs = new RandomAccessFile(metaFileTmp, "rw")) {
  325. if (isNewFile) {
  326. fs.writeBytes("[");
  327. } else {
  328. fs.seek(fs.length() - 1);
  329. fs.writeBytes(",");
  330. }
  331. fs.writeBytes(gson().toJson(model));
  332. fs.writeBytes("]");
  333. fs.close();
  334. } catch (IOException e) {
  335. logger.error("Writing filestore model to file {0}, {1}", METAFILE_TMP, e);
  336. }
  337. try {
  338. if (metaFileTmp.exists()) {
  339. FileUtils.copyFile(metaFileTmp, metaFile);
  340. metaFileTmp.delete();
  341. } else {
  342. logger.error("Writing filestore model to file {0}", METAFILE);
  343. }
  344. }
  345. catch (IOException e) {
  346. logger.error("Writing filestore model to file {0}, {1}", METAFILE, e);
  347. }
  348. }
  349. /*
  350. * Intended for testing purposes only
  351. */
  352. @Override
  353. public void clearFilestoreCache() {
  354. fileCache.clear();
  355. }
  356. private static Gson gson(ExclusionStrategy... strategies) {
  357. GsonBuilder builder = new GsonBuilder();
  358. builder.registerTypeAdapter(Date.class, new GmtDateTypeAdapter());
  359. if (!ArrayUtils.isEmpty(strategies)) {
  360. builder.setExclusionStrategies(strategies);
  361. }
  362. return builder.create();
  363. }
  364. }