blob: 3e6a26159b0e4cdbbb24f1347ea5380d9523e0e2 [file] [log] [blame]
/*
* Copyright (C) 2017, Markus Duft <markus.duft@ssi-schaefer.com>
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.lfs;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lfs.Protocol.OPERATION_UPLOAD;
import static org.eclipse.jgit.lfs.internal.LfsConnectionFactory.toRequest;
import static org.eclipse.jgit.transport.http.HttpConnection.HTTP_OK;
import static org.eclipse.jgit.util.HttpSupport.METHOD_POST;
import static org.eclipse.jgit.util.HttpSupport.METHOD_PUT;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.eclipse.jgit.api.errors.AbortedByHookException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.hooks.PrePushHook;
import org.eclipse.jgit.lfs.Protocol.ObjectInfo;
import org.eclipse.jgit.lfs.errors.CorruptMediaFile;
import org.eclipse.jgit.lfs.internal.LfsConnectionFactory;
import org.eclipse.jgit.lfs.internal.LfsText;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.ObjectWalk;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.eclipse.jgit.transport.http.HttpConnection;
import com.google.gson.Gson;
import com.google.gson.stream.JsonReader;
/**
* Pre-push hook that handles uploading LFS artefacts.
*
* @since 4.11
*/
public class LfsPrePushHook extends PrePushHook {
private static final String EMPTY = ""; //$NON-NLS-1$
private Collection<RemoteRefUpdate> refs;
/**
* @param repo
* the repository
* @param outputStream
* not used by this implementation
*/
public LfsPrePushHook(Repository repo, PrintStream outputStream) {
super(repo, outputStream);
}
@Override
public void setRefs(Collection<RemoteRefUpdate> toRefs) {
this.refs = toRefs;
}
@Override
public String call() throws IOException, AbortedByHookException {
Set<LfsPointer> toPush = findObjectsToPush();
if (toPush.isEmpty()) {
return EMPTY;
}
HttpConnection api = LfsConnectionFactory.getLfsConnection(
getRepository(), METHOD_POST, OPERATION_UPLOAD);
Map<String, LfsPointer> oid2ptr = requestBatchUpload(api, toPush);
uploadContents(api, oid2ptr);
return EMPTY;
}
private Set<LfsPointer> findObjectsToPush() throws IOException,
MissingObjectException, IncorrectObjectTypeException {
Set<LfsPointer> toPush = new TreeSet<>();
try (ObjectWalk walk = new ObjectWalk(getRepository())) {
for (RemoteRefUpdate up : refs) {
walk.setRewriteParents(false);
excludeRemoteRefs(walk);
walk.markStart(walk.parseCommit(up.getNewObjectId()));
while (walk.next() != null) {
// walk all commits to populate objects
}
findLfsPointers(toPush, walk);
}
}
return toPush;
}
private static void findLfsPointers(Set<LfsPointer> toPush, ObjectWalk walk)
throws MissingObjectException, IncorrectObjectTypeException,
IOException {
RevObject obj;
ObjectReader r = walk.getObjectReader();
while ((obj = walk.nextObject()) != null) {
if (obj.getType() == Constants.OBJ_BLOB
&& getObjectSize(r, obj) < LfsPointer.SIZE_THRESHOLD) {
LfsPointer ptr = loadLfsPointer(r, obj);
if (ptr != null) {
toPush.add(ptr);
}
}
}
}
private static long getObjectSize(ObjectReader r, RevObject obj)
throws IOException {
return r.getObjectSize(obj.getId(), Constants.OBJ_BLOB);
}
private static LfsPointer loadLfsPointer(ObjectReader r, AnyObjectId obj)
throws IOException {
try (InputStream is = r.open(obj, Constants.OBJ_BLOB).openStream()) {
return LfsPointer.parseLfsPointer(is);
}
}
private void excludeRemoteRefs(ObjectWalk walk) throws IOException {
RefDatabase refDatabase = getRepository().getRefDatabase();
List<Ref> remoteRefs = refDatabase.getRefsByPrefix(remote());
for (Ref r : remoteRefs) {
ObjectId oid = r.getPeeledObjectId();
if (oid == null) {
oid = r.getObjectId();
}
if (oid == null) {
// ignore (e.g. symbolic, ...)
continue;
}
RevObject o = walk.parseAny(oid);
if (o.getType() == Constants.OBJ_COMMIT
|| o.getType() == Constants.OBJ_TAG) {
walk.markUninteresting(o);
}
}
}
private String remote() {
String remoteName = getRemoteName() == null
? Constants.DEFAULT_REMOTE_NAME
: getRemoteName();
return Constants.R_REMOTES + remoteName;
}
private Map<String, LfsPointer> requestBatchUpload(HttpConnection api,
Set<LfsPointer> toPush) throws IOException {
LfsPointer[] res = toPush.toArray(new LfsPointer[0]);
Map<String, LfsPointer> oidStr2ptr = new HashMap<>();
for (LfsPointer p : res) {
oidStr2ptr.put(p.getOid().name(), p);
}
Gson gson = Protocol.gson();
api.getOutputStream().write(
gson.toJson(toRequest(OPERATION_UPLOAD, res)).getBytes(UTF_8));
int responseCode = api.getResponseCode();
if (responseCode != HTTP_OK) {
throw new IOException(
MessageFormat.format(LfsText.get().serverFailure,
api.getURL(), Integer.valueOf(responseCode)));
}
return oidStr2ptr;
}
private void uploadContents(HttpConnection api,
Map<String, LfsPointer> oid2ptr) throws IOException {
try (JsonReader reader = new JsonReader(
new InputStreamReader(api.getInputStream(), UTF_8))) {
for (Protocol.ObjectInfo o : parseObjects(reader)) {
if (o.actions == null) {
continue;
}
LfsPointer ptr = oid2ptr.get(o.oid);
if (ptr == null) {
// received an object we didn't request
continue;
}
Protocol.Action uploadAction = o.actions.get(OPERATION_UPLOAD);
if (uploadAction == null || uploadAction.href == null) {
continue;
}
Lfs lfs = new Lfs(getRepository());
Path path = lfs.getMediaFile(ptr.getOid());
if (!Files.exists(path)) {
throw new IOException(MessageFormat
.format(LfsText.get().missingLocalObject, path));
}
uploadFile(o, uploadAction, path);
}
}
}
private List<ObjectInfo> parseObjects(JsonReader reader) {
Gson gson = new Gson();
Protocol.Response resp = gson.fromJson(reader, Protocol.Response.class);
return resp.objects;
}
private void uploadFile(Protocol.ObjectInfo o,
Protocol.Action uploadAction, Path path)
throws IOException, CorruptMediaFile {
HttpConnection contentServer = LfsConnectionFactory
.getLfsContentConnection(getRepository(), uploadAction,
METHOD_PUT);
contentServer.setDoOutput(true);
try (OutputStream out = contentServer
.getOutputStream()) {
long size = Files.copy(path, out);
if (size != o.size) {
throw new CorruptMediaFile(path, o.size, size);
}
}
int responseCode = contentServer.getResponseCode();
if (responseCode != HTTP_OK) {
throw new IOException(MessageFormat.format(
LfsText.get().serverFailure, contentServer.getURL(),
Integer.valueOf(responseCode)));
}
}
}