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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  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 IStoredSettings settings;
  75. public static final int UNDEFINED_SIZE = -1;
  76. private static final String METAFILE = "filestore.json";
  77. private static final String METAFILE_TMP = "filestore.json.tmp";
  78. protected static final Type METAFILE_TYPE = new TypeToken<Collection<FilestoreModel>>() {}.getType();
  79. private Map<String, FilestoreModel > fileCache = new ConcurrentHashMap<String, FilestoreModel>();
  80. @Inject
  81. public FilestoreManager(IRuntimeManager runtimeManager) {
  82. this.runtimeManager = runtimeManager;
  83. this.settings = runtimeManager.getSettings();
  84. }
  85. @Override
  86. public IManager start() {
  87. // Try to load any existing metadata
  88. File dir = getStorageFolder();
  89. dir.mkdirs();
  90. File metadata = new File(dir, METAFILE);
  91. if (metadata.exists()) {
  92. Collection<FilestoreModel> items = null;
  93. Gson gson = gson();
  94. try (FileReader file = new FileReader(metadata)) {
  95. items = gson.fromJson(file, METAFILE_TYPE);
  96. file.close();
  97. } catch (IOException e) {
  98. e.printStackTrace();
  99. }
  100. for(Iterator<FilestoreModel> itr = items.iterator(); itr.hasNext(); ) {
  101. FilestoreModel model = itr.next();
  102. fileCache.put(model.oid, model);
  103. }
  104. logger.info("Loaded {} items from filestore metadata file", fileCache.size());
  105. }
  106. else
  107. {
  108. logger.info("No filestore metadata file found");
  109. }
  110. return this;
  111. }
  112. @Override
  113. public IManager stop() {
  114. return this;
  115. }
  116. @Override
  117. public boolean isValidOid(String oid) {
  118. //NOTE: Assuming SHA256 support only as per git-lfs
  119. return Pattern.matches("[a-fA-F0-9]{64}", oid);
  120. }
  121. @Override
  122. public FilestoreModel.Status addObject(String oid, long size, UserModel user, RepositoryModel repo) {
  123. //Handle access control
  124. if (!user.canPush(repo)) {
  125. if (user == UserModel.ANONYMOUS) {
  126. return Status.AuthenticationRequired;
  127. } else {
  128. return Status.Error_Unauthorized;
  129. }
  130. }
  131. //Handle object details
  132. if (!isValidOid(oid)) { return Status.Error_Invalid_Oid; }
  133. if (fileCache.containsKey(oid)) {
  134. FilestoreModel item = fileCache.get(oid);
  135. if (!item.isInErrorState() && (size != UNDEFINED_SIZE) && (item.getSize() != size)) {
  136. return Status.Error_Size_Mismatch;
  137. }
  138. item.addRepository(repo.name);
  139. if (item.isInErrorState()) {
  140. item.reset(user, size);
  141. }
  142. } else {
  143. if (size < 0) {return Status.Error_Invalid_Size; }
  144. if ((getMaxUploadSize() != UNDEFINED_SIZE) && (size > getMaxUploadSize())) { return Status.Error_Exceeds_Size_Limit; }
  145. FilestoreModel model = new FilestoreModel(oid, size, user, repo.name);
  146. fileCache.put(oid, model);
  147. saveFilestoreModel(model);
  148. }
  149. return fileCache.get(oid).getStatus();
  150. }
  151. @Override
  152. public FilestoreModel.Status uploadBlob(String oid, long size, UserModel user, RepositoryModel repo, InputStream streamIn) {
  153. //Access control and object logic
  154. Status state = addObject(oid, size, user, repo);
  155. if (state != Status.Upload_Pending) {
  156. return state;
  157. }
  158. FilestoreModel model = fileCache.get(oid);
  159. if (!model.actionUpload(user)) {
  160. return Status.Upload_In_Progress;
  161. } else {
  162. long actualSize = 0;
  163. File file = getStoragePath(oid);
  164. try {
  165. file.getParentFile().mkdirs();
  166. file.createNewFile();
  167. try (FileOutputStream streamOut = new FileOutputStream(file)) {
  168. actualSize = IOUtils.copyLarge(streamIn, streamOut);
  169. streamOut.flush();
  170. streamOut.close();
  171. if (model.getSize() != actualSize) {
  172. model.setStatus(Status.Error_Size_Mismatch, user);
  173. logger.warn(MessageFormat.format("Failed to upload blob {0} due to size mismatch, expected {1} got {2}",
  174. oid, model.getSize(), actualSize));
  175. } else {
  176. String actualOid = "";
  177. try (FileInputStream fileForHash = new FileInputStream(file)) {
  178. actualOid = DigestUtils.sha256Hex(fileForHash);
  179. fileForHash.close();
  180. }
  181. if (oid.equalsIgnoreCase(actualOid)) {
  182. model.setStatus(Status.Available, user);
  183. } else {
  184. model.setStatus(Status.Error_Hash_Mismatch, user);
  185. logger.warn(MessageFormat.format("Failed to upload blob {0} due to hash mismatch, got {1}", oid, actualOid));
  186. }
  187. }
  188. }
  189. } catch (Exception e) {
  190. model.setStatus(Status.Error_Unknown, user);
  191. logger.warn(MessageFormat.format("Failed to upload blob {0}", oid), e);
  192. } finally {
  193. saveFilestoreModel(model);
  194. }
  195. if (model.isInErrorState()) {
  196. file.delete();
  197. model.removeRepository(repo.name);
  198. }
  199. }
  200. return model.getStatus();
  201. }
  202. private FilestoreModel.Status canGetObject(String oid, UserModel user, RepositoryModel repo) {
  203. //Access Control
  204. if (!user.canView(repo)) {
  205. if (user == UserModel.ANONYMOUS) {
  206. return Status.AuthenticationRequired;
  207. } else {
  208. return Status.Error_Unauthorized;
  209. }
  210. }
  211. //Object Logic
  212. if (!isValidOid(oid)) {
  213. return Status.Error_Invalid_Oid;
  214. }
  215. if (!fileCache.containsKey(oid)) {
  216. return Status.Unavailable;
  217. }
  218. FilestoreModel item = fileCache.get(oid);
  219. if (item.getStatus() == Status.Available) {
  220. return Status.Available;
  221. }
  222. return Status.Unavailable;
  223. }
  224. @Override
  225. public FilestoreModel getObject(String oid, UserModel user, RepositoryModel repo) {
  226. if (canGetObject(oid, user, repo) == Status.Available) {
  227. return fileCache.get(oid);
  228. }
  229. return null;
  230. }
  231. @Override
  232. public FilestoreModel.Status downloadBlob(String oid, UserModel user, RepositoryModel repo, OutputStream streamOut) {
  233. //Access control and object logic
  234. Status status = canGetObject(oid, user, repo);
  235. if (status != Status.Available) {
  236. return status;
  237. }
  238. FilestoreModel item = fileCache.get(oid);
  239. if (streamOut != null) {
  240. try (FileInputStream streamIn = new FileInputStream(getStoragePath(oid))) {
  241. IOUtils.copyLarge(streamIn, streamOut);
  242. streamOut.flush();
  243. streamIn.close();
  244. } catch (EOFException e) {
  245. logger.error(MessageFormat.format("Client aborted connection for {0}", oid), e);
  246. return Status.Error_Unexpected_Stream_End;
  247. } catch (Exception e) {
  248. logger.error(MessageFormat.format("Failed to download blob {0}", oid), e);
  249. return Status.Error_Unknown;
  250. }
  251. }
  252. return item.getStatus();
  253. }
  254. @Override
  255. public List<FilestoreModel> getAllObjects(List<RepositoryModel> viewableRepositories) {
  256. List<String> viewableRepositoryNames = new ArrayList<String>(viewableRepositories.size());
  257. for (RepositoryModel repository : viewableRepositories) {
  258. viewableRepositoryNames.add(repository.name);
  259. }
  260. if (viewableRepositoryNames.size() == 0) {
  261. return null;
  262. }
  263. final Collection<FilestoreModel> allFiles = fileCache.values();
  264. List<FilestoreModel> userViewableFiles = new ArrayList<FilestoreModel>(allFiles.size());
  265. for (FilestoreModel file : allFiles) {
  266. if (file.isInRepositoryList(viewableRepositoryNames)) {
  267. userViewableFiles.add(file);
  268. }
  269. }
  270. return userViewableFiles;
  271. }
  272. @Override
  273. public File getStorageFolder() {
  274. return runtimeManager.getFileOrFolder(Keys.filestore.storageFolder, "${baseFolder}/lfs");
  275. }
  276. @Override
  277. public File getStoragePath(String oid) {
  278. return new File(getStorageFolder(), oid.substring(0, 2).concat("/").concat(oid.substring(2)));
  279. }
  280. @Override
  281. public long getMaxUploadSize() {
  282. return settings.getLong(Keys.filestore.maxUploadSize, -1);
  283. }
  284. @Override
  285. public long getFilestoreUsedByteCount() {
  286. Iterator<FilestoreModel> iterator = fileCache.values().iterator();
  287. long total = 0;
  288. while (iterator.hasNext()) {
  289. FilestoreModel item = iterator.next();
  290. if (item.getStatus() == Status.Available) {
  291. total += item.getSize();
  292. }
  293. }
  294. return total;
  295. }
  296. @Override
  297. public long getFilestoreAvailableByteCount() {
  298. try {
  299. return Files.getFileStore(getStorageFolder().toPath()).getUsableSpace();
  300. } catch (IOException e) {
  301. logger.error(MessageFormat.format("Failed to retrive available space in Filestore {0}", e));
  302. }
  303. return UNDEFINED_SIZE;
  304. };
  305. private synchronized void saveFilestoreModel(FilestoreModel model) {
  306. File metaFile = new File(getStorageFolder(), METAFILE);
  307. File metaFileTmp = new File(getStorageFolder(), METAFILE_TMP);
  308. boolean isNewFile = false;
  309. try {
  310. if (!metaFile.exists()) {
  311. metaFile.getParentFile().mkdirs();
  312. metaFile.createNewFile();
  313. isNewFile = true;
  314. }
  315. FileUtils.copyFile(metaFile, metaFileTmp);
  316. } catch (IOException e) {
  317. logger.error("Writing filestore model to file {0}, {1}", METAFILE, e);
  318. }
  319. try (RandomAccessFile fs = new RandomAccessFile(metaFileTmp, "rw")) {
  320. if (isNewFile) {
  321. fs.writeBytes("[");
  322. } else {
  323. fs.seek(fs.length() - 1);
  324. fs.writeBytes(",");
  325. }
  326. fs.writeBytes(gson().toJson(model));
  327. fs.writeBytes("]");
  328. fs.close();
  329. } catch (IOException e) {
  330. logger.error("Writing filestore model to file {0}, {1}", METAFILE_TMP, e);
  331. }
  332. try {
  333. if (metaFileTmp.exists()) {
  334. FileUtils.copyFile(metaFileTmp, metaFile);
  335. metaFileTmp.delete();
  336. } else {
  337. logger.error("Writing filestore model to file {0}", METAFILE);
  338. }
  339. }
  340. catch (IOException e) {
  341. logger.error("Writing filestore model to file {0}, {1}", METAFILE, e);
  342. }
  343. }
  344. /*
  345. * Intended for testing purposes only
  346. */
  347. @Override
  348. public void clearFilestoreCache() {
  349. fileCache.clear();
  350. }
  351. private static Gson gson(ExclusionStrategy... strategies) {
  352. GsonBuilder builder = new GsonBuilder();
  353. builder.registerTypeAdapter(Date.class, new GmtDateTypeAdapter());
  354. if (!ArrayUtils.isEmpty(strategies)) {
  355. builder.setExclusionStrategies(strategies);
  356. }
  357. return builder.create();
  358. }
  359. }