| /* |
| * Copyright 2015 gitblit.com. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.gitblit.servlet; |
| |
| import java.io.BufferedReader; |
| import java.io.IOException; |
| import java.io.Serializable; |
| import java.text.MessageFormat; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import com.google.inject.Inject; |
| import com.google.inject.Singleton; |
| |
| import javax.servlet.ServletException; |
| import javax.servlet.http.HttpServlet; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import com.gitblit.Constants; |
| import com.gitblit.IStoredSettings; |
| import com.gitblit.models.FilestoreModel; |
| import com.gitblit.models.RepositoryModel; |
| import com.gitblit.models.FilestoreModel.Status; |
| import com.gitblit.manager.FilestoreManager; |
| import com.gitblit.manager.IGitblit; |
| import com.gitblit.models.UserModel; |
| import com.gitblit.utils.JsonUtils; |
| |
| |
| /** |
| * Handles large file storage as per the Git LFS v1 Batch API |
| * |
| * Further details can be found at https://github.com/github/git-lfs |
| * |
| * @author Paul Martin |
| */ |
| @Singleton |
| public class FilestoreServlet extends HttpServlet { |
| |
| private static final long serialVersionUID = 1L; |
| public static final int PROTOCOL_VERSION = 1; |
| |
| public static final String GIT_LFS_META_MIME = "application/vnd.git-lfs+json"; |
| |
| public static final String REGEX_PATH = "^(.*?)/(r)/(.*?)/info/lfs/objects/(batch|" + Constants.REGEX_SHA256 + ")"; |
| public static final int REGEX_GROUP_BASE_URI = 1; |
| public static final int REGEX_GROUP_PREFIX = 2; |
| public static final int REGEX_GROUP_REPOSITORY = 3; |
| public static final int REGEX_GROUP_ENDPOINT = 4; |
| |
| protected final Logger logger; |
| |
| private static IGitblit gitblit; |
| |
| @Inject |
| public FilestoreServlet(IStoredSettings settings, IGitblit gitblit) { |
| |
| super(); |
| logger = LoggerFactory.getLogger(getClass()); |
| |
| FilestoreServlet.gitblit = gitblit; |
| } |
| |
| |
| /** |
| * Handles batch upload request (metadata) |
| * |
| * @param request |
| * @param response |
| * @throws javax.servlet.ServletException |
| * @throws java.io.IOException |
| */ |
| @Override |
| protected void doPost(HttpServletRequest request, |
| HttpServletResponse response) throws ServletException ,IOException { |
| |
| UrlInfo info = getInfoFromRequest(request); |
| if (info == null) { |
| sendError(response, HttpServletResponse.SC_NOT_FOUND); |
| return; |
| } |
| |
| //Post is for batch operations so no oid should be defined |
| if (info.oid != null) { |
| sendError(response, HttpServletResponse.SC_BAD_REQUEST); |
| return; |
| } |
| |
| IGitLFS.Batch batch = deserialize(request, response, IGitLFS.Batch.class); |
| |
| if (batch == null) { |
| sendError(response, HttpServletResponse.SC_BAD_REQUEST); |
| return; |
| } |
| |
| UserModel user = getUserOrAnonymous(request); |
| |
| IGitLFS.BatchResponse batchResponse = new IGitLFS.BatchResponse(); |
| |
| if (batch.operation.equalsIgnoreCase("upload")) { |
| for (IGitLFS.Request item : batch.objects) { |
| |
| Status state = gitblit.addObject(item.oid, item.size, user, info.repository); |
| |
| batchResponse.objects.add(getResponseForUpload(info.baseUrl, item.oid, item.size, user.getName(), info.repository.name, state)); |
| } |
| } else if (batch.operation.equalsIgnoreCase("download")) { |
| for (IGitLFS.Request item : batch.objects) { |
| |
| Status state = gitblit.downloadBlob(item.oid, user, info.repository, null); |
| batchResponse.objects.add(getResponseForDownload(info.baseUrl, item.oid, item.size, user.getName(), info.repository.name, state)); |
| } |
| } else { |
| sendError(response, HttpServletResponse.SC_NOT_IMPLEMENTED); |
| return; |
| } |
| |
| response.setStatus(HttpServletResponse.SC_OK); |
| serialize(response, batchResponse); |
| } |
| |
| /** |
| * Handles the actual upload (BLOB) |
| * |
| * @param request |
| * @param response |
| * @throws javax.servlet.ServletException |
| * @throws java.io.IOException |
| */ |
| @Override |
| protected void doPut(HttpServletRequest request, |
| HttpServletResponse response) throws ServletException ,IOException { |
| |
| UrlInfo info = getInfoFromRequest(request); |
| |
| if (info == null) { |
| sendError(response, HttpServletResponse.SC_NOT_FOUND); |
| return; |
| } |
| |
| //Put is a singular operation so must have oid |
| if (info.oid == null) { |
| sendError(response, HttpServletResponse.SC_BAD_REQUEST); |
| return; |
| } |
| |
| UserModel user = getUserOrAnonymous(request); |
| long size = FilestoreManager.UNDEFINED_SIZE; |
| |
| |
| |
| FilestoreModel.Status status = gitblit.uploadBlob(info.oid, size, user, info.repository, request.getInputStream()); |
| IGitLFS.Response responseObject = getResponseForUpload(info.baseUrl, info.oid, size, user.getName(), info.repository.name, status); |
| |
| logger.info(MessageFormat.format("FILESTORE-AUDIT {0}:{4} {1} {2}@{3}", |
| "PUT", info.oid, user.getName(), info.repository.name, status.toString() )); |
| |
| if (responseObject.error == null) { |
| response.setStatus(responseObject.successCode); |
| } else { |
| serialize(response, responseObject.error); |
| } |
| }; |
| |
| /** |
| * Handles a download |
| * Treated as hypermedia request if accept header contains Git-LFS MIME |
| * otherwise treated as a download of the blob |
| * @param request |
| * @param response |
| * @throws javax.servlet.ServletException |
| * @throws java.io.IOException |
| */ |
| @Override |
| protected void doGet(HttpServletRequest request, |
| HttpServletResponse response) throws ServletException ,IOException { |
| |
| UrlInfo info = getInfoFromRequest(request); |
| |
| if (info == null || info.oid == null) { |
| sendError(response, HttpServletResponse.SC_NOT_FOUND); |
| return; |
| } |
| |
| UserModel user = getUserOrAnonymous(request); |
| |
| FilestoreModel model = gitblit.getObject(info.oid, user, info.repository); |
| long size = FilestoreManager.UNDEFINED_SIZE; |
| |
| boolean isMetaRequest = AccessRestrictionFilter.hasContentInRequestHeader(request, "Accept", GIT_LFS_META_MIME); |
| FilestoreModel.Status status = Status.Unavailable; |
| |
| if (model != null) { |
| size = model.getSize(); |
| status = model.getStatus(); |
| } |
| |
| if (!isMetaRequest) { |
| status = gitblit.downloadBlob(info.oid, user, info.repository, response.getOutputStream()); |
| |
| logger.info(MessageFormat.format("FILESTORE-AUDIT {0}:{4} {1} {2}@{3}", |
| "GET", info.oid, user.getName(), info.repository.name, status.toString() )); |
| } |
| |
| if (status == Status.Error_Unexpected_Stream_End) { |
| return; |
| } |
| |
| IGitLFS.Response responseObject = getResponseForDownload(info.baseUrl, |
| info.oid, size, user.getName(), info.repository.name, status); |
| |
| if (responseObject.error == null) { |
| response.setStatus(responseObject.successCode); |
| |
| if (isMetaRequest) { |
| serialize(response, responseObject); |
| } |
| } else { |
| response.setStatus(responseObject.error.code); |
| |
| if (isMetaRequest) { |
| serialize(response, responseObject.error); |
| } |
| } |
| }; |
| |
| private void sendError(HttpServletResponse response, int code) throws IOException { |
| |
| String msg = ""; |
| |
| switch (code) |
| { |
| case HttpServletResponse.SC_NOT_FOUND: msg = "Not Found"; break; |
| case HttpServletResponse.SC_NOT_IMPLEMENTED: msg = "Not Implemented"; break; |
| case HttpServletResponse.SC_BAD_REQUEST: msg = "Malformed Git-LFS request"; break; |
| |
| default: msg = "Unknown Error"; |
| } |
| |
| response.setStatus(code); |
| serialize(response, new IGitLFS.ObjectError(code, msg)); |
| } |
| |
| @SuppressWarnings("incomplete-switch") |
| private IGitLFS.Response getResponseForUpload(String baseUrl, String oid, long size, String user, String repo, FilestoreModel.Status state) { |
| |
| switch (state) { |
| case AuthenticationRequired: |
| return new IGitLFS.Response(oid, size, 401, MessageFormat.format("Authentication required to write to repository {0}", repo)); |
| case Error_Unauthorized: |
| return new IGitLFS.Response(oid, size, 403, MessageFormat.format("User {0}, does not have write permissions to repository {1}", user, repo)); |
| case Error_Exceeds_Size_Limit: |
| return new IGitLFS.Response(oid, size, 509, MessageFormat.format("Object is larger than allowed limit of {1}", gitblit.getMaxUploadSize())); |
| case Error_Hash_Mismatch: |
| return new IGitLFS.Response(oid, size, 422, "Hash mismatch"); |
| case Error_Invalid_Oid: |
| return new IGitLFS.Response(oid, size, 422, MessageFormat.format("{0} is not a valid oid", oid)); |
| case Error_Invalid_Size: |
| return new IGitLFS.Response(oid, size, 422, MessageFormat.format("{0} is not a valid size", size)); |
| case Error_Size_Mismatch: |
| return new IGitLFS.Response(oid, size, 422, "Object size mismatch"); |
| case Deleted: |
| return new IGitLFS.Response(oid, size, 410, "Object was deleted : ".concat("TBD Reason") ); |
| case Upload_In_Progress: |
| return new IGitLFS.Response(oid, size, 503, "File currently being uploaded by another user"); |
| case Unavailable: |
| return new IGitLFS.Response(oid, size, 404, MessageFormat.format("Repository {0}, does not exist for user {1}", repo, user)); |
| case Upload_Pending: |
| return new IGitLFS.Response(oid, size, 202, "upload", getObjectUri(baseUrl, repo, oid) ); |
| case Available: |
| return new IGitLFS.Response(oid, size, 200, "upload", getObjectUri(baseUrl, repo, oid) ); |
| } |
| |
| return new IGitLFS.Response(oid, size, 500, "Unknown Error"); |
| } |
| |
| @SuppressWarnings("incomplete-switch") |
| private IGitLFS.Response getResponseForDownload(String baseUrl, String oid, long size, String user, String repo, FilestoreModel.Status state) { |
| |
| switch (state) { |
| case Error_Unauthorized: |
| return new IGitLFS.Response(oid, size, 403, MessageFormat.format("User {0}, does not have read permissions to repository {1}", user, repo)); |
| case Error_Invalid_Oid: |
| return new IGitLFS.Response(oid, size, 422, MessageFormat.format("{0} is not a valid oid", oid)); |
| case Error_Unknown: |
| return new IGitLFS.Response(oid, size, 500, "Unknown Error"); |
| case Deleted: |
| return new IGitLFS.Response(oid, size, 410, "Object was deleted : ".concat("TBD Reason") ); |
| case Available: |
| return new IGitLFS.Response(oid, size, 200, "download", getObjectUri(baseUrl, repo, oid) ); |
| } |
| |
| return new IGitLFS.Response(oid, size, 404, "Object not available"); |
| } |
| |
| |
| private String getObjectUri(String baseUrl, String repo, String oid) { |
| return baseUrl + "/" + repo + "/" + Constants.R_LFS + "objects/" + oid; |
| } |
| |
| |
| protected void serialize(HttpServletResponse response, Object o) throws IOException { |
| if (o != null) { |
| // Send JSON response |
| String json = JsonUtils.toJsonString(o); |
| response.setCharacterEncoding(Constants.ENCODING); |
| response.setContentType(GIT_LFS_META_MIME); |
| response.getWriter().append(json); |
| } |
| } |
| |
| protected <X> X deserialize(HttpServletRequest request, HttpServletResponse response, |
| Class<X> clazz) { |
| |
| String json = ""; |
| try { |
| |
| json = readJson(request, response); |
| |
| return JsonUtils.fromJsonString(json.toString(), clazz); |
| |
| } catch (Exception e) { |
| //Intentional silent fail |
| } |
| |
| return null; |
| } |
| |
| private String readJson(HttpServletRequest request, HttpServletResponse response) |
| throws IOException { |
| BufferedReader reader = request.getReader(); |
| StringBuilder json = new StringBuilder(); |
| String line = null; |
| while ((line = reader.readLine()) != null) { |
| json.append(line); |
| } |
| reader.close(); |
| |
| if (json.length() == 0) { |
| logger.error(MessageFormat.format("Failed to receive json data from {0}", |
| request.getRemoteAddr())); |
| response.setStatus(HttpServletResponse.SC_BAD_REQUEST); |
| return null; |
| } |
| return json.toString(); |
| } |
| |
| private UserModel getUserOrAnonymous(HttpServletRequest r) { |
| UserModel user = (UserModel) r.getUserPrincipal(); |
| if (user != null) { return user; } |
| return UserModel.ANONYMOUS; |
| } |
| |
| private static class UrlInfo { |
| public RepositoryModel repository; |
| public String oid; |
| public String baseUrl; |
| |
| public UrlInfo(RepositoryModel repo, String oid, String baseUrl) { |
| this.repository = repo; |
| this.oid = oid; |
| this.baseUrl = baseUrl; |
| } |
| } |
| |
| public static UrlInfo getInfoFromRequest(HttpServletRequest httpRequest) { |
| |
| String url = httpRequest.getRequestURL().toString(); |
| Pattern p = Pattern.compile(REGEX_PATH); |
| Matcher m = p.matcher(url); |
| |
| |
| if (m.find()) { |
| RepositoryModel repo = gitblit.getRepositoryModel(m.group(REGEX_GROUP_REPOSITORY)); |
| String baseUrl = m.group(REGEX_GROUP_BASE_URI) + "/" + m.group(REGEX_GROUP_PREFIX); |
| |
| if (m.group(REGEX_GROUP_ENDPOINT).equals("batch")) { |
| return new UrlInfo(repo, null, baseUrl); |
| } else { |
| return new UrlInfo(repo, m.group(REGEX_GROUP_ENDPOINT), baseUrl); |
| } |
| } |
| |
| return null; |
| } |
| |
| |
| public interface IGitLFS { |
| |
| @SuppressWarnings("serial") |
| public class Request implements Serializable |
| { |
| public String oid; |
| public long size; |
| } |
| |
| |
| @SuppressWarnings("serial") |
| public class Batch implements Serializable |
| { |
| public String operation; |
| public List<Request> objects; |
| } |
| |
| |
| @SuppressWarnings("serial") |
| public class Response implements Serializable |
| { |
| public String oid; |
| public long size; |
| public Map<String, HyperMediaLink> actions; |
| public ObjectError error; |
| public transient int successCode; |
| |
| public Response(String id, long itemSize, int errorCode, String errorText) { |
| oid = id; |
| size = itemSize; |
| actions = null; |
| successCode = 0; |
| error = new ObjectError(errorCode, errorText); |
| } |
| |
| public Response(String id, long itemSize, int actionCode, String action, String uri) { |
| oid = id; |
| size = itemSize; |
| error = null; |
| successCode = actionCode; |
| actions = new HashMap<String, HyperMediaLink>(); |
| actions.put(action, new HyperMediaLink(action, uri)); |
| } |
| |
| } |
| |
| @SuppressWarnings("serial") |
| public class BatchResponse implements Serializable { |
| public List<Response> objects; |
| |
| public BatchResponse() { |
| objects = new ArrayList<Response>(); |
| } |
| } |
| |
| |
| @SuppressWarnings("serial") |
| public class ObjectError implements Serializable |
| { |
| public String message; |
| public int code; |
| public String documentation_url; |
| public Integer request_id; |
| |
| public ObjectError(int errorCode, String errorText) { |
| code = errorCode; |
| message = errorText; |
| request_id = null; |
| } |
| } |
| |
| @SuppressWarnings("serial") |
| public class HyperMediaLink implements Serializable |
| { |
| public String href; |
| public transient String header; |
| //public Date expires_at; |
| |
| public HyperMediaLink(String action, String uri) { |
| header = action; |
| href = uri; |
| } |
| } |
| } |
| |
| |
| |
| } |