James Moger
2015-12-03 0aecfc10a08474a8ae19c2a46ea9ed77d75b2f6e
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.servlet;
17
18 import java.io.BufferedReader;
19 import java.io.IOException;
20 import java.io.Serializable;
21 import java.text.MessageFormat;
22 import java.util.ArrayList;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28
29 import com.google.inject.Inject;
30 import com.google.inject.Singleton;
31
32 import javax.servlet.ServletException;
33 import javax.servlet.http.HttpServlet;
34 import javax.servlet.http.HttpServletRequest;
35 import javax.servlet.http.HttpServletResponse;
36
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 import com.gitblit.Constants;
41 import com.gitblit.IStoredSettings;
42 import com.gitblit.models.FilestoreModel;
43 import com.gitblit.models.RepositoryModel;
44 import com.gitblit.models.FilestoreModel.Status;
45 import com.gitblit.manager.FilestoreManager;
46 import com.gitblit.manager.IGitblit;
47 import com.gitblit.models.UserModel;
48 import com.gitblit.utils.JsonUtils;
49
50
51 /**
52  * Handles large file storage as per the Git LFS v1 Batch API
53  * 
54  * Further details can be found at https://github.com/github/git-lfs
55  * 
56  * @author Paul Martin
57  */
58 @Singleton
59 public class FilestoreServlet extends HttpServlet {
60
61     private static final long serialVersionUID = 1L;
62     public static final int PROTOCOL_VERSION = 1;
63     
64     public static final String GIT_LFS_META_MIME = "application/vnd.git-lfs+json";
65     
da29a4 66     public static final String REGEX_PATH = "^(.*?)/(r)/(.*?)/info/lfs/objects/(batch|" + Constants.REGEX_SHA256 + ")";
bd0e83 67     public static final int REGEX_GROUP_BASE_URI = 1;
PM 68     public static final int REGEX_GROUP_PREFIX = 2;
69     public static final int REGEX_GROUP_REPOSITORY = 3;
70     public static final int REGEX_GROUP_ENDPOINT = 4;
71     
72     protected final Logger logger;
73     
74     private static IGitblit gitblit;
75
76     @Inject
77     public FilestoreServlet(IStoredSettings settings, IGitblit gitblit) {
78         
79         super();
80         logger = LoggerFactory.getLogger(getClass());
81         
82         FilestoreServlet.gitblit = gitblit;
83     }
84
85         
86     /**
87      * Handles batch upload request (metadata)
88      *
89      * @param request
90      * @param response
91      * @throws javax.servlet.ServletException
92      * @throws java.io.IOException
93      */
94     @Override
95     protected void doPost(HttpServletRequest request, 
96             HttpServletResponse response) throws ServletException ,IOException {
97         
98         UrlInfo info = getInfoFromRequest(request);
99         if (info == null) {
100             sendError(response, HttpServletResponse.SC_NOT_FOUND);
101             return;
102         }
103
104         //Post is for batch operations so no oid should be defined
105         if (info.oid != null) {
106             sendError(response, HttpServletResponse.SC_BAD_REQUEST);
107             return;
108         }
109         
110         IGitLFS.Batch batch = deserialize(request, response, IGitLFS.Batch.class);
111         
112         if (batch == null) { 
113             sendError(response, HttpServletResponse.SC_BAD_REQUEST);
114             return;
115         }
116
117         UserModel user = getUserOrAnonymous(request);
118         
119         IGitLFS.BatchResponse batchResponse = new IGitLFS.BatchResponse();
120         
121         if (batch.operation.equalsIgnoreCase("upload")) {
122             for (IGitLFS.Request item : batch.objects) {
123                 
124                 Status state = gitblit.addObject(item.oid, item.size, user, info.repository);
125
126                 batchResponse.objects.add(getResponseForUpload(info.baseUrl, item.oid, item.size, user.getName(), info.repository.name, state));
127             }
128         } else if (batch.operation.equalsIgnoreCase("download")) {
129             for (IGitLFS.Request item : batch.objects) {
130                 
131                 Status state = gitblit.downloadBlob(item.oid, user, info.repository, null);
132                 batchResponse.objects.add(getResponseForDownload(info.baseUrl, item.oid, item.size, user.getName(), info.repository.name, state));
133             }
134         } else {
135             sendError(response, HttpServletResponse.SC_NOT_IMPLEMENTED);
136             return;
137         }
138         
139         response.setStatus(HttpServletResponse.SC_OK);
140         serialize(response, batchResponse);
141     }
142     
143     /**
144      * Handles the actual upload (BLOB)
145      * 
146      * @param request
147      * @param response
148      * @throws javax.servlet.ServletException
149      * @throws java.io.IOException
150      */
151     @Override
152     protected void doPut(HttpServletRequest request, 
153             HttpServletResponse response) throws ServletException ,IOException {
154         
155         UrlInfo info = getInfoFromRequest(request);
156         
157         if (info == null) {
158             sendError(response, HttpServletResponse.SC_NOT_FOUND);
159             return;
160         }
161
162         //Put is a singular operation so must have oid
163         if (info.oid == null) {
164             sendError(response, HttpServletResponse.SC_BAD_REQUEST);
165             return;
166         }
167         
168         UserModel user = getUserOrAnonymous(request);
169         long size = FilestoreManager.UNDEFINED_SIZE;
170         
171         
172         
173         FilestoreModel.Status status = gitblit.uploadBlob(info.oid, size, user, info.repository, request.getInputStream());
174         IGitLFS.Response responseObject = getResponseForUpload(info.baseUrl, info.oid, size, user.getName(), info.repository.name, status);
175         
176         logger.info(MessageFormat.format("FILESTORE-AUDIT {0}:{4} {1} {2}@{3}", 
177                 "PUT", info.oid, user.getName(), info.repository.name, status.toString() ));
178         
179         if (responseObject.error == null) {
180             response.setStatus(responseObject.successCode);
181         } else {
182             serialize(response, responseObject.error);
183         }
184     };
185     
186     /**
187      * Handles a download
188      * Treated as hypermedia request if accept header contains Git-LFS MIME
189      * otherwise treated as a download of the blob
190      * @param request
191      * @param response
192      * @throws javax.servlet.ServletException
193      * @throws java.io.IOException
194      */
195     @Override
196     protected void doGet(HttpServletRequest request, 
197             HttpServletResponse response) throws ServletException ,IOException {
198         
199         UrlInfo info = getInfoFromRequest(request);
200         
201         if (info == null || info.oid == null) {
202             sendError(response, HttpServletResponse.SC_NOT_FOUND);
203             return;
204         }
205         
206         UserModel user = getUserOrAnonymous(request);
207         
208         FilestoreModel model = gitblit.getObject(info.oid, user, info.repository);
209         long size = FilestoreManager.UNDEFINED_SIZE;
210         
211         boolean isMetaRequest = AccessRestrictionFilter.hasContentInRequestHeader(request, "Accept", GIT_LFS_META_MIME);
212         FilestoreModel.Status status = Status.Unavailable;
213         
214         if (model != null) {
215             size = model.getSize();
216             status = model.getStatus();
217         }
218         
219         if (!isMetaRequest) {
220             status = gitblit.downloadBlob(info.oid, user, info.repository, response.getOutputStream());
221             
222             logger.info(MessageFormat.format("FILESTORE-AUDIT {0}:{4} {1} {2}@{3}", 
223                     "GET", info.oid, user.getName(), info.repository.name, status.toString() ));
224         }
225         
226         if (status == Status.Error_Unexpected_Stream_End) {
227             return;
228         }
229
230         IGitLFS.Response responseObject = getResponseForDownload(info.baseUrl, 
231                 info.oid, size, user.getName(), info.repository.name, status);
232         
233         if (responseObject.error == null) {
234             response.setStatus(responseObject.successCode);
235             
236             if (isMetaRequest) {
237                 serialize(response, responseObject);
238             }
239         } else {
240             response.setStatus(responseObject.error.code);
241             serialize(response, responseObject.error);
242         }
243     };
244     
245     private void sendError(HttpServletResponse response, int code) throws IOException {
246         
247         String msg = "";
248         
249         switch (code)
250         {
251             case HttpServletResponse.SC_NOT_FOUND: msg = "Not Found"; break;
252             case HttpServletResponse.SC_NOT_IMPLEMENTED: msg = "Not Implemented"; break;
253             case HttpServletResponse.SC_BAD_REQUEST: msg = "Malformed Git-LFS request"; break;
254             
255             default: msg = "Unknown Error";
256         }
257         
258         response.setStatus(code);
259         serialize(response, new IGitLFS.ObjectError(code, msg));
260     }
261     
262     @SuppressWarnings("incomplete-switch")
263     private IGitLFS.Response getResponseForUpload(String baseUrl, String oid, long size, String user, String repo, FilestoreModel.Status state) {
264
265         switch (state) {
266             case AuthenticationRequired:
267                 return new IGitLFS.Response(oid, size, 401, MessageFormat.format("Authentication required to write to repository {0}", repo));
268             case Error_Unauthorized: 
269                 return new IGitLFS.Response(oid, size, 403, MessageFormat.format("User {0}, does not have write permissions to repository {1}", user, repo));
270             case Error_Exceeds_Size_Limit: 
271                 return new IGitLFS.Response(oid, size, 509, MessageFormat.format("Object is larger than allowed limit of {1}",  gitblit.getMaxUploadSize()));
272             case Error_Hash_Mismatch: 
273                 return new IGitLFS.Response(oid, size, 422, "Hash mismatch");
274             case Error_Invalid_Oid: 
275                 return new IGitLFS.Response(oid, size, 422, MessageFormat.format("{0} is not a valid oid", oid));
276             case Error_Invalid_Size: 
277                 return new IGitLFS.Response(oid, size, 422, MessageFormat.format("{0} is not a valid size", size));
278             case Error_Size_Mismatch: 
279                 return new IGitLFS.Response(oid, size, 422, "Object size mismatch");
280             case Deleted: 
281                 return new IGitLFS.Response(oid, size, 410, "Object was deleted : ".concat("TBD Reason") );
282             case Upload_In_Progress:
283                 return new IGitLFS.Response(oid, size, 503, "File currently being uploaded by another user");
284             case Unavailable: 
285                 return new IGitLFS.Response(oid, size, 404, MessageFormat.format("Repository {0}, does not exist for user {1}", repo, user));
286             case Upload_Pending: 
287                 return new IGitLFS.Response(oid, size, 202, "upload", getObjectUri(baseUrl, repo, oid) );
288             case Available: 
289                 return new IGitLFS.Response(oid, size, 200, "upload", getObjectUri(baseUrl, repo, oid) );
290         }
291         
292         return new IGitLFS.Response(oid, size, 500, "Unknown Error");
293     }
294
295     @SuppressWarnings("incomplete-switch")
296     private IGitLFS.Response getResponseForDownload(String baseUrl, String oid, long size, String user, String repo, FilestoreModel.Status state) {
297
298         switch (state) {
299             case Error_Unauthorized: 
300                 return new IGitLFS.Response(oid, size, 403, MessageFormat.format("User {0}, does not have read permissions to repository {1}", user, repo));
301             case Error_Invalid_Oid: 
302                 return new IGitLFS.Response(oid, size, 422, MessageFormat.format("{0} is not a valid oid", oid));
303             case Error_Unknown:
304                 return new IGitLFS.Response(oid, size, 500, "Unknown Error");
305             case Deleted: 
306                 return new IGitLFS.Response(oid, size, 410, "Object was deleted : ".concat("TBD Reason") );
307             case Available: 
308                 return new IGitLFS.Response(oid, size, 200, "download", getObjectUri(baseUrl, repo, oid) );
309         }
310         
311         return new IGitLFS.Response(oid, size, 404, "Object not available");
312     }
313
314     
315     private String getObjectUri(String baseUrl, String repo, String oid) {
316         return baseUrl + "/" + repo + "/" + Constants.R_LFS + "objects/" + oid;
317     }
318     
319     
320     protected void serialize(HttpServletResponse response, Object o) throws IOException {
321         if (o != null) {
322             // Send JSON response
323             String json = JsonUtils.toJsonString(o);
324             response.setCharacterEncoding(Constants.ENCODING);
325             response.setContentType(GIT_LFS_META_MIME);
326             response.getWriter().append(json);
327         }
328     }
329     
330     protected <X> X deserialize(HttpServletRequest request, HttpServletResponse response,
331             Class<X> clazz) {
332         
333         String json = "";
334         try {
335             
336             json = readJson(request, response);
337             
338             return JsonUtils.fromJsonString(json.toString(), clazz);
339             
340         } catch (Exception e) {
341             //Intentional silent fail
342         }
343         
344         return null;
345     }
346     
347     private String readJson(HttpServletRequest request, HttpServletResponse response)
348             throws IOException {
349         BufferedReader reader = request.getReader();
350         StringBuilder json = new StringBuilder();
351         String line = null;
352         while ((line = reader.readLine()) != null) {
353             json.append(line);
354         }
355         reader.close();
356
357         if (json.length() == 0) {
358             logger.error(MessageFormat.format("Failed to receive json data from {0}",
359                     request.getRemoteAddr()));
360             response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
361             return null;
362         }
363         return json.toString();
364     }
365     
366     private UserModel getUserOrAnonymous(HttpServletRequest r) {
367         UserModel user = (UserModel) r.getUserPrincipal();
368         if (user != null) { return user; }
369         return UserModel.ANONYMOUS;
370     }
371     
372     private static class UrlInfo {
373         public RepositoryModel repository;
374         public String oid;
375         public String baseUrl;
376         
377         public UrlInfo(RepositoryModel repo, String oid, String baseUrl) {
378             this.repository = repo;
379             this.oid = oid;
380             this.baseUrl = baseUrl;
381         }
382     }
383     
384     public static UrlInfo getInfoFromRequest(HttpServletRequest httpRequest) {
385         
386         String url = httpRequest.getRequestURL().toString();
387         Pattern p = Pattern.compile(REGEX_PATH);
388         Matcher m = p.matcher(url);
389         
390         
391         if (m.find()) {
392             RepositoryModel repo = gitblit.getRepositoryModel(m.group(REGEX_GROUP_REPOSITORY));
393             String baseUrl = m.group(REGEX_GROUP_BASE_URI) + "/" + m.group(REGEX_GROUP_PREFIX);
394             
395             if (m.group(REGEX_GROUP_ENDPOINT).equals("batch")) {
396                 return new UrlInfo(repo, null, baseUrl);
397             } else {
398                 return new UrlInfo(repo, m.group(REGEX_GROUP_ENDPOINT), baseUrl);
399             }
400         }
401         
402         return null;
403     }
404     
405     
406     public interface IGitLFS {
407     
408         @SuppressWarnings("serial")
409         public class Request implements Serializable
410         {
411             public String oid;
412             public long size;
413         }
414         
415         
416         @SuppressWarnings("serial")
417         public class Batch implements Serializable
418         {
419             public String operation;
420             public List<Request> objects;
421         }
422         
423         
424         @SuppressWarnings("serial")
425         public class Response implements Serializable
426         {
427             public String oid;
428             public long size;
429             public Map<String, HyperMediaLink> actions;
430             public ObjectError error;
431             public transient int successCode; 
432             
433             public Response(String id, long itemSize, int errorCode, String errorText) {
434                 oid = id;
435                 size = itemSize;
436                 actions = null;
437                 successCode = 0;
438                 error = new ObjectError(errorCode, errorText);
439             }
440             
441             public Response(String id, long itemSize, int actionCode, String action, String uri) {
442                 oid = id;
443                 size = itemSize;
444                 error = null;
445                 successCode = actionCode;
446                 actions = new HashMap<String, HyperMediaLink>();
447                 actions.put(action, new HyperMediaLink(action, uri));
448             }
449             
450         }
451         
452         @SuppressWarnings("serial")
453         public class BatchResponse implements Serializable {
454             public List<Response> objects;
455             
456             public BatchResponse() {
457                 objects = new ArrayList<Response>();
458             }
459         }
460         
461         
462         @SuppressWarnings("serial")
463         public class ObjectError implements Serializable
464         {
465             public String message;
466             public int code;
467             public String documentation_url;
468             public Integer request_id;
469             
470             public ObjectError(int errorCode, String errorText) {
471                 code = errorCode;
472                 message = errorText;
473                 request_id = null;
474             }
475         }
476         
477         @SuppressWarnings("serial")
478         public class HyperMediaLink implements Serializable
479         {
480             public String href;
481             public transient String header;
482             //public Date expires_at;
483             
484             public HyperMediaLink(String action, String uri) {
485                 header = action;
486                 href = uri;
487             }
488         }
489     }
490
491
492     
493 }