James Moger
2015-12-08 f75535759570bbc4784ee8324b0d1b8dfb01766f
commit | author | age
bd0e83 1 /*
PM 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
28cf62 18 import java.io.EOFException;
bd0e83 19 import java.io.File;
PM 20 import java.io.FileInputStream;
21 import java.io.FileOutputStream;
22 import java.io.FileReader;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.OutputStream;
26 import java.io.RandomAccessFile;
27 import java.lang.reflect.Type;
28 import java.nio.file.Files;
29 import java.text.MessageFormat;
30 import java.util.ArrayList;
31 import java.util.Collection;
32 import java.util.Date;
33 import java.util.Iterator;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.concurrent.ConcurrentHashMap;
37 import java.util.regex.Pattern;
38
39 import org.apache.commons.codec.digest.DigestUtils;
40 import org.apache.commons.io.FileUtils;
41 import org.apache.commons.io.IOUtils;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 import com.gitblit.IStoredSettings;
46 import com.gitblit.Keys;
47 import com.gitblit.models.FilestoreModel;
28cf62 48 import com.gitblit.models.FilestoreModel.Status;
bd0e83 49 import com.gitblit.models.RepositoryModel;
PM 50 import com.gitblit.models.UserModel;
51 import com.gitblit.utils.ArrayUtils;
52 import com.gitblit.utils.JsonUtils.GmtDateTypeAdapter;
53 import com.google.gson.ExclusionStrategy;
54 import com.google.gson.Gson;
55 import com.google.gson.GsonBuilder;
56 import com.google.gson.reflect.TypeToken;
57 import com.google.inject.Inject;
58 import com.google.inject.Singleton;
59
60 /**
61  * FilestoreManager handles files uploaded via:
62  *     + git-lfs
63  *  + ticket attachment (TBD)
28cf62 64  *
bd0e83 65  * Files are stored using their SHA256 hash (as per git-lfs)
PM 66  * If the same file is uploaded through different repositories no additional space is used
67  * Access is controlled through the current repository permissions.
68  *
69  * TODO: Identify what and how the actual BLOBs should work with federation
70  *
71  * @author Paul Martin
72  *
73  */
74 @Singleton
75 public class FilestoreManager implements IFilestoreManager {
76
77     private final Logger logger = LoggerFactory.getLogger(getClass());
28cf62 78
bd0e83 79     private final IRuntimeManager runtimeManager;
697905 80     
PM 81     private final IRepositoryManager repositoryManager;
28cf62 82
bd0e83 83     private final IStoredSettings settings;
28cf62 84
bd0e83 85     public static final int UNDEFINED_SIZE = -1;
28cf62 86
bd0e83 87     private static final String METAFILE = "filestore.json";
28cf62 88
bd0e83 89     private static final String METAFILE_TMP = "filestore.json.tmp";
28cf62 90
bd0e83 91     protected static final Type METAFILE_TYPE = new TypeToken<Collection<FilestoreModel>>() {}.getType();
28cf62 92
bd0e83 93     private Map<String, FilestoreModel > fileCache = new ConcurrentHashMap<String, FilestoreModel>();
28cf62 94
JM 95
bd0e83 96     @Inject
PM 97     FilestoreManager(
697905 98             IRuntimeManager runtimeManager,
PM 99             IRepositoryManager repositoryManager) {
bd0e83 100         this.runtimeManager = runtimeManager;
697905 101         this.repositoryManager = repositoryManager;
bd0e83 102         this.settings = runtimeManager.getSettings();
PM 103     }
28cf62 104
bd0e83 105     @Override
PM 106     public IManager start() {
107
e41e8f 108         // Try to load any existing metadata
JM 109         File dir = getStorageFolder();
110         dir.mkdirs();
111         File metadata = new File(dir, METAFILE);
28cf62 112
bd0e83 113         if (metadata.exists()) {
PM 114             Collection<FilestoreModel> items = null;
28cf62 115
bd0e83 116             Gson gson = gson();
PM 117             try (FileReader file = new FileReader(metadata)) {
118                 items = gson.fromJson(file, METAFILE_TYPE);
119                 file.close();
28cf62 120
bd0e83 121             } catch (IOException e) {
PM 122                 e.printStackTrace();
123             }
28cf62 124
bd0e83 125             for(Iterator<FilestoreModel> itr = items.iterator(); itr.hasNext(); ) {
PM 126                 FilestoreModel model = itr.next();
127                 fileCache.put(model.oid, model);
128             }
129
130             logger.info("Loaded {} items from filestore metadata file", fileCache.size());
131         }
132         else
133         {
134             logger.info("No filestore metadata file found");
135         }
28cf62 136
bd0e83 137         return this;
PM 138     }
139
140     @Override
141     public IManager stop() {
142         return this;
143     }
144
145
146     @Override
147     public boolean isValidOid(String oid) {
148         //NOTE: Assuming SHA256 support only as per git-lfs
149         return Pattern.matches("[a-fA-F0-9]{64}", oid);
150     }
28cf62 151
bd0e83 152     @Override
PM 153     public FilestoreModel.Status addObject(String oid, long size, UserModel user, RepositoryModel repo) {
154
155         //Handle access control
156         if (!user.canPush(repo)) {
157             if (user == UserModel.ANONYMOUS) {
158                 return Status.AuthenticationRequired;
159             } else {
160                 return Status.Error_Unauthorized;
161             }
162         }
28cf62 163
bd0e83 164         //Handle object details
PM 165         if (!isValidOid(oid)) { return Status.Error_Invalid_Oid; }
28cf62 166
bd0e83 167         if (fileCache.containsKey(oid)) {
PM 168             FilestoreModel item = fileCache.get(oid);
28cf62 169
bd0e83 170             if (!item.isInErrorState() && (size != UNDEFINED_SIZE) && (item.getSize() != size)) {
PM 171                 return Status.Error_Size_Mismatch;
172             }
28cf62 173
bd0e83 174             item.addRepository(repo.name);
28cf62 175
bd0e83 176             if (item.isInErrorState()) {
PM 177                 item.reset(user, size);
178             }
179         } else {
28cf62 180
bd0e83 181             if (size  < 0) {return Status.Error_Invalid_Size; }
PM 182             if ((getMaxUploadSize() != UNDEFINED_SIZE) && (size > getMaxUploadSize())) { return Status.Error_Exceeds_Size_Limit; }
28cf62 183
JM 184             FilestoreModel model = new FilestoreModel(oid, size, user, repo.name);
bd0e83 185             fileCache.put(oid, model);
PM 186             saveFilestoreModel(model);
187         }
28cf62 188
bd0e83 189         return fileCache.get(oid).getStatus();
PM 190     }
191
192     @Override
193     public FilestoreModel.Status uploadBlob(String oid, long size, UserModel user, RepositoryModel repo, InputStream streamIn) {
194
195         //Access control and object logic
28cf62 196         Status state = addObject(oid, size, user, repo);
JM 197
198         if (state != Status.Upload_Pending) {
bd0e83 199             return state;
PM 200         }
28cf62 201
bd0e83 202         FilestoreModel model = fileCache.get(oid);
28cf62 203
bd0e83 204         if (!model.actionUpload(user)) {
PM 205             return Status.Upload_In_Progress;
206         } else {
207             long actualSize = 0;
208             File file = getStoragePath(oid);
209
210             try {
211                 file.getParentFile().mkdirs();
212                 file.createNewFile();
28cf62 213
bd0e83 214                 try (FileOutputStream streamOut = new FileOutputStream(file)) {
28cf62 215
bd0e83 216                     actualSize = IOUtils.copyLarge(streamIn, streamOut);
28cf62 217
bd0e83 218                     streamOut.flush();
PM 219                     streamOut.close();
28cf62 220
bd0e83 221                     if (model.getSize() != actualSize) {
PM 222                         model.setStatus(Status.Error_Size_Mismatch, user);
28cf62 223
JM 224                         logger.warn(MessageFormat.format("Failed to upload blob {0} due to size mismatch, expected {1} got {2}",
bd0e83 225                                 oid, model.getSize(), actualSize));
PM 226                     } else {
227                         String actualOid = "";
28cf62 228
bd0e83 229                         try (FileInputStream fileForHash = new FileInputStream(file)) {
PM 230                             actualOid = DigestUtils.sha256Hex(fileForHash);
231                             fileForHash.close();
232                         }
28cf62 233
bd0e83 234                         if (oid.equalsIgnoreCase(actualOid)) {
PM 235                             model.setStatus(Status.Available, user);
236                         } else {
237                             model.setStatus(Status.Error_Hash_Mismatch, user);
28cf62 238
bd0e83 239                             logger.warn(MessageFormat.format("Failed to upload blob {0} due to hash mismatch, got {1}", oid, actualOid));
PM 240                         }
241                     }
242                 }
243             } catch (Exception e) {
28cf62 244
bd0e83 245                 model.setStatus(Status.Error_Unknown, user);
PM 246                 logger.warn(MessageFormat.format("Failed to upload blob {0}", oid), e);
247             } finally {
248                 saveFilestoreModel(model);
249             }
28cf62 250
bd0e83 251             if (model.isInErrorState()) {
PM 252                 file.delete();
253                 model.removeRepository(repo.name);
254             }
255         }
28cf62 256
bd0e83 257         return model.getStatus();
PM 258     }
28cf62 259
bd0e83 260     private FilestoreModel.Status canGetObject(String oid, UserModel user, RepositoryModel repo) {
28cf62 261
bd0e83 262         //Access Control
PM 263         if (!user.canView(repo)) {
264             if (user == UserModel.ANONYMOUS) {
265                 return Status.AuthenticationRequired;
266             } else {
267                 return Status.Error_Unauthorized;
268             }
269         }
270
271         //Object Logic
28cf62 272         if (!isValidOid(oid)) {
bd0e83 273             return Status.Error_Invalid_Oid;
PM 274         }
28cf62 275
JM 276         if (!fileCache.containsKey(oid)) {
bd0e83 277             return Status.Unavailable;
PM 278         }
28cf62 279
bd0e83 280         FilestoreModel item = fileCache.get(oid);
28cf62 281
bd0e83 282         if (item.getStatus() == Status.Available) {
PM 283             return Status.Available;
284         }
28cf62 285
bd0e83 286         return Status.Unavailable;
PM 287     }
28cf62 288
bd0e83 289     @Override
PM 290     public FilestoreModel getObject(String oid, UserModel user, RepositoryModel repo) {
28cf62 291
bd0e83 292         if (canGetObject(oid, user, repo) == Status.Available) {
PM 293             return fileCache.get(oid);
294         }
28cf62 295
bd0e83 296         return null;
PM 297     }
298
299     @Override
300     public FilestoreModel.Status downloadBlob(String oid, UserModel user, RepositoryModel repo, OutputStream streamOut) {
28cf62 301
bd0e83 302         //Access control and object logic
PM 303         Status status = canGetObject(oid, user, repo);
28cf62 304
JM 305         if (status != Status.Available) {
bd0e83 306             return status;
PM 307         }
28cf62 308
bd0e83 309         FilestoreModel item = fileCache.get(oid);
28cf62 310
bd0e83 311         if (streamOut != null) {
PM 312             try (FileInputStream streamIn = new FileInputStream(getStoragePath(oid))) {
28cf62 313
bd0e83 314                 IOUtils.copyLarge(streamIn, streamOut);
28cf62 315
bd0e83 316                 streamOut.flush();
PM 317                 streamIn.close();
28cf62 318             } catch (EOFException e) {
bd0e83 319                 logger.error(MessageFormat.format("Client aborted connection for {0}", oid), e);
PM 320                 return Status.Error_Unexpected_Stream_End;
321             } catch (Exception e) {
322                 logger.error(MessageFormat.format("Failed to download blob {0}", oid), e);
323                 return Status.Error_Unknown;
324             }
325         }
28cf62 326
bd0e83 327         return item.getStatus();
PM 328     }
329
330     @Override
697905 331     public List<FilestoreModel> getAllObjects(UserModel user) {
PM 332         
333         final List<RepositoryModel> viewableRepositories = repositoryManager.getRepositoryModels(user);
334         List<String> viewableRepositoryNames = new ArrayList<String>(viewableRepositories.size());
335         
336         for (RepositoryModel repository : viewableRepositories) {
337             viewableRepositoryNames.add(repository.name);
338         }
339         
340         if (viewableRepositoryNames.size() == 0) {
341             return null;
342         }
343         
344         final Collection<FilestoreModel> allFiles = fileCache.values();
345         List<FilestoreModel> userViewableFiles = new ArrayList<FilestoreModel>(allFiles.size());
346         
347         for (FilestoreModel file : allFiles) {
348             if (file.isInRepositoryList(viewableRepositoryNames)) {
349                 userViewableFiles.add(file);
350             }
351         }
352         
353         return userViewableFiles;                
bd0e83 354     }
PM 355
356     @Override
357     public File getStorageFolder() {
358         return runtimeManager.getFileOrFolder(Keys.filestore.storageFolder, "${baseFolder}/lfs");
359     }
28cf62 360
bd0e83 361     @Override
PM 362     public File getStoragePath(String oid) {
363          return new File(getStorageFolder(), oid.substring(0, 2).concat("/").concat(oid.substring(2)));
364     }
365
366     @Override
367     public long getMaxUploadSize() {
368         return settings.getLong(Keys.filestore.maxUploadSize, -1);
369     }
28cf62 370
bd0e83 371     @Override
PM 372     public long getFilestoreUsedByteCount() {
373         Iterator<FilestoreModel> iterator = fileCache.values().iterator();
374         long total = 0;
28cf62 375
bd0e83 376         while (iterator.hasNext()) {
28cf62 377
bd0e83 378             FilestoreModel item = iterator.next();
PM 379             if (item.getStatus() == Status.Available) {
380                 total += item.getSize();
381             }
382         }
28cf62 383
bd0e83 384         return total;
PM 385     }
28cf62 386
bd0e83 387     @Override
PM 388     public long getFilestoreAvailableByteCount() {
28cf62 389
bd0e83 390         try {
PM 391             return Files.getFileStore(getStorageFolder().toPath()).getUsableSpace();
392         } catch (IOException e) {
393             logger.error(MessageFormat.format("Failed to retrive available space in Filestore {0}", e));
394         }
28cf62 395
bd0e83 396         return UNDEFINED_SIZE;
PM 397     };
28cf62 398
bd0e83 399     private synchronized void saveFilestoreModel(FilestoreModel model) {
28cf62 400
bd0e83 401         File metaFile = new File(getStorageFolder(), METAFILE);
PM 402         File metaFileTmp = new File(getStorageFolder(), METAFILE_TMP);
403         boolean isNewFile = false;
28cf62 404
bd0e83 405         try {
PM 406             if (!metaFile.exists()) {
407                 metaFile.getParentFile().mkdirs();
408                 metaFile.createNewFile();
409                 isNewFile = true;
410             }
411             FileUtils.copyFile(metaFile, metaFileTmp);
28cf62 412
bd0e83 413         } catch (IOException e) {
PM 414             logger.error("Writing filestore model to file {0}, {1}", METAFILE, e);
415         }
28cf62 416
bd0e83 417         try (RandomAccessFile fs = new RandomAccessFile(metaFileTmp, "rw")) {
28cf62 418
bd0e83 419             if (isNewFile) {
PM 420                 fs.writeBytes("[");
421             } else {
422                 fs.seek(fs.length() - 1);
423                 fs.writeBytes(",");
424             }
28cf62 425
bd0e83 426             fs.writeBytes(gson().toJson(model));
PM 427             fs.writeBytes("]");
28cf62 428
bd0e83 429             fs.close();
28cf62 430
bd0e83 431         } catch (IOException e) {
PM 432             logger.error("Writing filestore model to file {0}, {1}", METAFILE_TMP, e);
433         }
28cf62 434
bd0e83 435         try {
PM 436             if (metaFileTmp.exists()) {
437                 FileUtils.copyFile(metaFileTmp, metaFile);
28cf62 438
bd0e83 439                 metaFileTmp.delete();
PM 440             } else {
441                 logger.error("Writing filestore model to file {0}", METAFILE);
442             }
443         }
444         catch (IOException e) {
445             logger.error("Writing filestore model to file {0}, {1}", METAFILE, e);
446         }
447     }
28cf62 448
bd0e83 449     /*
PM 450      * Intended for testing purposes only
451      */
28cf62 452     @Override
bd0e83 453     public void clearFilestoreCache() {
PM 454         fileCache.clear();
455     }
28cf62 456
bd0e83 457     private static Gson gson(ExclusionStrategy... strategies) {
PM 458         GsonBuilder builder = new GsonBuilder();
459         builder.registerTypeAdapter(Date.class, new GmtDateTypeAdapter());
460         if (!ArrayUtils.isEmpty(strategies)) {
461             builder.setExclusionStrategies(strategies);
462         }
463         return builder.create();
464     }
28cf62 465
bd0e83 466 }