summaryrefslogtreecommitdiffstats
path: root/src/main/java/com/gitblit/servlet/FilestoreServlet.java
diff options
context:
space:
mode:
authorPaul Martin <paul@paulsputer.com>2015-10-10 12:46:51 +0100
committerPaul Martin <paul@paulsputer.com>2015-10-10 12:50:00 +0100
commitbd0e83e350fc703bcae72a28c41b09d9a9cec594 (patch)
treef9c3d5112600f89f64ded2d56472664db185750a /src/main/java/com/gitblit/servlet/FilestoreServlet.java
parentf2a9b239d2605b36401dd723ac22c195b938f8e0 (diff)
downloadgitblit-bd0e83e350fc703bcae72a28c41b09d9a9cec594.tar.gz
gitblit-bd0e83e350fc703bcae72a28c41b09d9a9cec594.zip
Git-LFS support
+ Metadata maintained in append-only JSON file providing complete audit history. + Filestore menu item + Lists filestore items + Current size and availability + Link to GitBlit Filestore help page (top right) + Hooks into existing repository permissions + Uses default repository path for out-of-box operation with Git-LFS client + accessRestrictionFilter now has access to http method and auth header + Testing for servlet and manager
Diffstat (limited to 'src/main/java/com/gitblit/servlet/FilestoreServlet.java')
-rw-r--r--src/main/java/com/gitblit/servlet/FilestoreServlet.java493
1 files changed, 493 insertions, 0 deletions
diff --git a/src/main/java/com/gitblit/servlet/FilestoreServlet.java b/src/main/java/com/gitblit/servlet/FilestoreServlet.java
new file mode 100644
index 00000000..19751483
--- /dev/null
+++ b/src/main/java/com/gitblit/servlet/FilestoreServlet.java
@@ -0,0 +1,493 @@
+/*
+ * 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|git)/(.*?)/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);
+ 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;
+ }
+ }
+ }
+
+
+
+}