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