1c0bb9928SMarkus Duft /* 2*5c5f7c6bSMatthias Sohn * Copyright (C) 2017, Markus Duft <markus.duft@ssi-schaefer.com> and others 3c0bb9928SMarkus Duft * 4*5c5f7c6bSMatthias Sohn * This program and the accompanying materials are made available under the 5*5c5f7c6bSMatthias Sohn * terms of the Eclipse Distribution License v. 1.0 which is available at 6*5c5f7c6bSMatthias Sohn * https://www.eclipse.org/org/documents/edl-v10.php. 7c0bb9928SMarkus Duft * 8*5c5f7c6bSMatthias Sohn * SPDX-License-Identifier: BSD-3-Clause 9c0bb9928SMarkus Duft */ 10c0bb9928SMarkus Duft package org.eclipse.jgit.lfs; 11c0bb9928SMarkus Duft 1230c6c754SDavid Pursehouse import static java.nio.charset.StandardCharsets.UTF_8; 13c0bb9928SMarkus Duft import static org.eclipse.jgit.lfs.Protocol.OPERATION_UPLOAD; 14ea2f7e93SMarkus Duft import static org.eclipse.jgit.lfs.internal.LfsConnectionFactory.toRequest; 15c0bb9928SMarkus Duft import static org.eclipse.jgit.transport.http.HttpConnection.HTTP_OK; 16c0bb9928SMarkus Duft import static org.eclipse.jgit.util.HttpSupport.METHOD_POST; 17c0bb9928SMarkus Duft import static org.eclipse.jgit.util.HttpSupport.METHOD_PUT; 18c0bb9928SMarkus Duft 19c0bb9928SMarkus Duft import java.io.IOException; 20c0bb9928SMarkus Duft import java.io.InputStream; 21c0bb9928SMarkus Duft import java.io.InputStreamReader; 22c0bb9928SMarkus Duft import java.io.OutputStream; 23c0bb9928SMarkus Duft import java.io.PrintStream; 24c0bb9928SMarkus Duft import java.nio.file.Files; 25c0bb9928SMarkus Duft import java.nio.file.Path; 26c0bb9928SMarkus Duft import java.text.MessageFormat; 27c0bb9928SMarkus Duft import java.util.Collection; 28c0bb9928SMarkus Duft import java.util.HashMap; 29c0bb9928SMarkus Duft import java.util.List; 30c0bb9928SMarkus Duft import java.util.Map; 31c0bb9928SMarkus Duft import java.util.Set; 32c0bb9928SMarkus Duft import java.util.TreeSet; 33c0bb9928SMarkus Duft 34c0bb9928SMarkus Duft import org.eclipse.jgit.api.errors.AbortedByHookException; 35c0bb9928SMarkus Duft import org.eclipse.jgit.errors.IncorrectObjectTypeException; 36c0bb9928SMarkus Duft import org.eclipse.jgit.errors.MissingObjectException; 37c0bb9928SMarkus Duft import org.eclipse.jgit.hooks.PrePushHook; 38c0bb9928SMarkus Duft import org.eclipse.jgit.lfs.Protocol.ObjectInfo; 39c0bb9928SMarkus Duft import org.eclipse.jgit.lfs.errors.CorruptMediaFile; 40c0bb9928SMarkus Duft import org.eclipse.jgit.lfs.internal.LfsConnectionFactory; 41c0bb9928SMarkus Duft import org.eclipse.jgit.lfs.internal.LfsText; 42c0bb9928SMarkus Duft import org.eclipse.jgit.lib.AnyObjectId; 43c0bb9928SMarkus Duft import org.eclipse.jgit.lib.Constants; 44c0bb9928SMarkus Duft import org.eclipse.jgit.lib.ObjectId; 45c0bb9928SMarkus Duft import org.eclipse.jgit.lib.ObjectReader; 46c0bb9928SMarkus Duft import org.eclipse.jgit.lib.Ref; 47c0bb9928SMarkus Duft import org.eclipse.jgit.lib.RefDatabase; 48c0bb9928SMarkus Duft import org.eclipse.jgit.lib.Repository; 49c0bb9928SMarkus Duft import org.eclipse.jgit.revwalk.ObjectWalk; 50c0bb9928SMarkus Duft import org.eclipse.jgit.revwalk.RevObject; 51c0bb9928SMarkus Duft import org.eclipse.jgit.transport.RemoteRefUpdate; 52c0bb9928SMarkus Duft import org.eclipse.jgit.transport.http.HttpConnection; 53c0bb9928SMarkus Duft 54c0bb9928SMarkus Duft import com.google.gson.Gson; 55c0bb9928SMarkus Duft import com.google.gson.stream.JsonReader; 56c0bb9928SMarkus Duft 57c0bb9928SMarkus Duft /** 58c0bb9928SMarkus Duft * Pre-push hook that handles uploading LFS artefacts. 59c0bb9928SMarkus Duft * 60c0bb9928SMarkus Duft * @since 4.11 61c0bb9928SMarkus Duft */ 62c0bb9928SMarkus Duft public class LfsPrePushHook extends PrePushHook { 63c0bb9928SMarkus Duft 64c0bb9928SMarkus Duft private static final String EMPTY = ""; //$NON-NLS-1$ 65c0bb9928SMarkus Duft private Collection<RemoteRefUpdate> refs; 66c0bb9928SMarkus Duft 67c0bb9928SMarkus Duft /** 68c0bb9928SMarkus Duft * @param repo 69c0bb9928SMarkus Duft * the repository 70c0bb9928SMarkus Duft * @param outputStream 71c0bb9928SMarkus Duft * not used by this implementation 72c0bb9928SMarkus Duft */ LfsPrePushHook(Repository repo, PrintStream outputStream)73c0bb9928SMarkus Duft public LfsPrePushHook(Repository repo, PrintStream outputStream) { 74c0bb9928SMarkus Duft super(repo, outputStream); 75c0bb9928SMarkus Duft } 76c0bb9928SMarkus Duft 7723125abcSTim Neumann /** 7823125abcSTim Neumann * @param repo 7923125abcSTim Neumann * the repository 8023125abcSTim Neumann * @param outputStream 8123125abcSTim Neumann * not used by this implementation 8223125abcSTim Neumann * @param errorStream 8323125abcSTim Neumann * not used by this implementation 8423125abcSTim Neumann * @since 5.6 8523125abcSTim Neumann */ LfsPrePushHook(Repository repo, PrintStream outputStream, PrintStream errorStream)8623125abcSTim Neumann public LfsPrePushHook(Repository repo, PrintStream outputStream, 8723125abcSTim Neumann PrintStream errorStream) { 8823125abcSTim Neumann super(repo, outputStream, errorStream); 8923125abcSTim Neumann } 9023125abcSTim Neumann 91c0bb9928SMarkus Duft @Override setRefs(Collection<RemoteRefUpdate> toRefs)92c0bb9928SMarkus Duft public void setRefs(Collection<RemoteRefUpdate> toRefs) { 93c0bb9928SMarkus Duft this.refs = toRefs; 94c0bb9928SMarkus Duft } 95c0bb9928SMarkus Duft 96c0bb9928SMarkus Duft @Override call()97c0bb9928SMarkus Duft public String call() throws IOException, AbortedByHookException { 98c0bb9928SMarkus Duft Set<LfsPointer> toPush = findObjectsToPush(); 99c0bb9928SMarkus Duft if (toPush.isEmpty()) { 100c0bb9928SMarkus Duft return EMPTY; 101c0bb9928SMarkus Duft } 102c0bb9928SMarkus Duft HttpConnection api = LfsConnectionFactory.getLfsConnection( 103c0bb9928SMarkus Duft getRepository(), METHOD_POST, OPERATION_UPLOAD); 104c0bb9928SMarkus Duft Map<String, LfsPointer> oid2ptr = requestBatchUpload(api, toPush); 105c0bb9928SMarkus Duft uploadContents(api, oid2ptr); 106c0bb9928SMarkus Duft return EMPTY; 107ea2f7e93SMarkus Duft 108c0bb9928SMarkus Duft } 109c0bb9928SMarkus Duft findObjectsToPush()110c0bb9928SMarkus Duft private Set<LfsPointer> findObjectsToPush() throws IOException, 111c0bb9928SMarkus Duft MissingObjectException, IncorrectObjectTypeException { 112c0bb9928SMarkus Duft Set<LfsPointer> toPush = new TreeSet<>(); 113c0bb9928SMarkus Duft 114c0bb9928SMarkus Duft try (ObjectWalk walk = new ObjectWalk(getRepository())) { 115c0bb9928SMarkus Duft for (RemoteRefUpdate up : refs) { 116c0bb9928SMarkus Duft walk.setRewriteParents(false); 117c0bb9928SMarkus Duft excludeRemoteRefs(walk); 118c0bb9928SMarkus Duft walk.markStart(walk.parseCommit(up.getNewObjectId())); 119c0bb9928SMarkus Duft while (walk.next() != null) { 120c0bb9928SMarkus Duft // walk all commits to populate objects 121c0bb9928SMarkus Duft } 122c0bb9928SMarkus Duft findLfsPointers(toPush, walk); 123c0bb9928SMarkus Duft } 124c0bb9928SMarkus Duft } 125c0bb9928SMarkus Duft return toPush; 126c0bb9928SMarkus Duft } 127c0bb9928SMarkus Duft findLfsPointers(Set<LfsPointer> toPush, ObjectWalk walk)128c0bb9928SMarkus Duft private static void findLfsPointers(Set<LfsPointer> toPush, ObjectWalk walk) 129c0bb9928SMarkus Duft throws MissingObjectException, IncorrectObjectTypeException, 130c0bb9928SMarkus Duft IOException { 131c0bb9928SMarkus Duft RevObject obj; 132c0bb9928SMarkus Duft ObjectReader r = walk.getObjectReader(); 133c0bb9928SMarkus Duft while ((obj = walk.nextObject()) != null) { 134c0bb9928SMarkus Duft if (obj.getType() == Constants.OBJ_BLOB 135c0bb9928SMarkus Duft && getObjectSize(r, obj) < LfsPointer.SIZE_THRESHOLD) { 136c0bb9928SMarkus Duft LfsPointer ptr = loadLfsPointer(r, obj); 137c0bb9928SMarkus Duft if (ptr != null) { 138c0bb9928SMarkus Duft toPush.add(ptr); 139c0bb9928SMarkus Duft } 140c0bb9928SMarkus Duft } 141c0bb9928SMarkus Duft } 142c0bb9928SMarkus Duft } 143c0bb9928SMarkus Duft getObjectSize(ObjectReader r, RevObject obj)144c0bb9928SMarkus Duft private static long getObjectSize(ObjectReader r, RevObject obj) 145c0bb9928SMarkus Duft throws IOException { 146c0bb9928SMarkus Duft return r.getObjectSize(obj.getId(), Constants.OBJ_BLOB); 147c0bb9928SMarkus Duft } 148c0bb9928SMarkus Duft loadLfsPointer(ObjectReader r, AnyObjectId obj)149c0bb9928SMarkus Duft private static LfsPointer loadLfsPointer(ObjectReader r, AnyObjectId obj) 150c0bb9928SMarkus Duft throws IOException { 151c0bb9928SMarkus Duft try (InputStream is = r.open(obj, Constants.OBJ_BLOB).openStream()) { 152c0bb9928SMarkus Duft return LfsPointer.parseLfsPointer(is); 153c0bb9928SMarkus Duft } 154c0bb9928SMarkus Duft } 155c0bb9928SMarkus Duft excludeRemoteRefs(ObjectWalk walk)156c0bb9928SMarkus Duft private void excludeRemoteRefs(ObjectWalk walk) throws IOException { 157c0bb9928SMarkus Duft RefDatabase refDatabase = getRepository().getRefDatabase(); 1589edf9bf2SMatthias Sohn List<Ref> remoteRefs = refDatabase.getRefsByPrefix(remote()); 1599edf9bf2SMatthias Sohn for (Ref r : remoteRefs) { 160c0bb9928SMarkus Duft ObjectId oid = r.getPeeledObjectId(); 161c0bb9928SMarkus Duft if (oid == null) { 162c0bb9928SMarkus Duft oid = r.getObjectId(); 163c0bb9928SMarkus Duft } 164312e61a3SMarkus Duft if (oid == null) { 165312e61a3SMarkus Duft // ignore (e.g. symbolic, ...) 166312e61a3SMarkus Duft continue; 167312e61a3SMarkus Duft } 168c0bb9928SMarkus Duft RevObject o = walk.parseAny(oid); 169c0bb9928SMarkus Duft if (o.getType() == Constants.OBJ_COMMIT 170c0bb9928SMarkus Duft || o.getType() == Constants.OBJ_TAG) { 171c0bb9928SMarkus Duft walk.markUninteresting(o); 172c0bb9928SMarkus Duft } 173c0bb9928SMarkus Duft } 174c0bb9928SMarkus Duft } 175c0bb9928SMarkus Duft remote()176c0bb9928SMarkus Duft private String remote() { 177c0bb9928SMarkus Duft String remoteName = getRemoteName() == null 178c0bb9928SMarkus Duft ? Constants.DEFAULT_REMOTE_NAME 179c0bb9928SMarkus Duft : getRemoteName(); 180c0bb9928SMarkus Duft return Constants.R_REMOTES + remoteName; 181c0bb9928SMarkus Duft } 182c0bb9928SMarkus Duft requestBatchUpload(HttpConnection api, Set<LfsPointer> toPush)183c0bb9928SMarkus Duft private Map<String, LfsPointer> requestBatchUpload(HttpConnection api, 184c0bb9928SMarkus Duft Set<LfsPointer> toPush) throws IOException { 1852fc00af4SMichael Keppler LfsPointer[] res = toPush.toArray(new LfsPointer[0]); 186c0bb9928SMarkus Duft Map<String, LfsPointer> oidStr2ptr = new HashMap<>(); 187c0bb9928SMarkus Duft for (LfsPointer p : res) { 188c0bb9928SMarkus Duft oidStr2ptr.put(p.getOid().name(), p); 189c0bb9928SMarkus Duft } 190ea2f7e93SMarkus Duft Gson gson = Protocol.gson(); 191c0bb9928SMarkus Duft api.getOutputStream().write( 19230c6c754SDavid Pursehouse gson.toJson(toRequest(OPERATION_UPLOAD, res)).getBytes(UTF_8)); 193c0bb9928SMarkus Duft int responseCode = api.getResponseCode(); 194c0bb9928SMarkus Duft if (responseCode != HTTP_OK) { 195c0bb9928SMarkus Duft throw new IOException( 196c0bb9928SMarkus Duft MessageFormat.format(LfsText.get().serverFailure, 197c0bb9928SMarkus Duft api.getURL(), Integer.valueOf(responseCode))); 198c0bb9928SMarkus Duft } 199c0bb9928SMarkus Duft return oidStr2ptr; 200c0bb9928SMarkus Duft } 201c0bb9928SMarkus Duft uploadContents(HttpConnection api, Map<String, LfsPointer> oid2ptr)202c0bb9928SMarkus Duft private void uploadContents(HttpConnection api, 203c0bb9928SMarkus Duft Map<String, LfsPointer> oid2ptr) throws IOException { 204c0bb9928SMarkus Duft try (JsonReader reader = new JsonReader( 20530c6c754SDavid Pursehouse new InputStreamReader(api.getInputStream(), UTF_8))) { 206c0bb9928SMarkus Duft for (Protocol.ObjectInfo o : parseObjects(reader)) { 207c0bb9928SMarkus Duft if (o.actions == null) { 208c0bb9928SMarkus Duft continue; 209c0bb9928SMarkus Duft } 210c0bb9928SMarkus Duft LfsPointer ptr = oid2ptr.get(o.oid); 211c0bb9928SMarkus Duft if (ptr == null) { 212c0bb9928SMarkus Duft // received an object we didn't request 213c0bb9928SMarkus Duft continue; 214c0bb9928SMarkus Duft } 215c0bb9928SMarkus Duft Protocol.Action uploadAction = o.actions.get(OPERATION_UPLOAD); 216c0bb9928SMarkus Duft if (uploadAction == null || uploadAction.href == null) { 217c0bb9928SMarkus Duft continue; 218c0bb9928SMarkus Duft } 219c0bb9928SMarkus Duft 220c0bb9928SMarkus Duft Lfs lfs = new Lfs(getRepository()); 221c0bb9928SMarkus Duft Path path = lfs.getMediaFile(ptr.getOid()); 222c0bb9928SMarkus Duft if (!Files.exists(path)) { 223c0bb9928SMarkus Duft throw new IOException(MessageFormat 224c0bb9928SMarkus Duft .format(LfsText.get().missingLocalObject, path)); 225c0bb9928SMarkus Duft } 226c0bb9928SMarkus Duft uploadFile(o, uploadAction, path); 227c0bb9928SMarkus Duft } 228c0bb9928SMarkus Duft } 229c0bb9928SMarkus Duft } 230c0bb9928SMarkus Duft parseObjects(JsonReader reader)231c0bb9928SMarkus Duft private List<ObjectInfo> parseObjects(JsonReader reader) { 232c0bb9928SMarkus Duft Gson gson = new Gson(); 233c0bb9928SMarkus Duft Protocol.Response resp = gson.fromJson(reader, Protocol.Response.class); 234c0bb9928SMarkus Duft return resp.objects; 235c0bb9928SMarkus Duft } 236c0bb9928SMarkus Duft uploadFile(Protocol.ObjectInfo o, Protocol.Action uploadAction, Path path)237c0bb9928SMarkus Duft private void uploadFile(Protocol.ObjectInfo o, 238c0bb9928SMarkus Duft Protocol.Action uploadAction, Path path) 239c0bb9928SMarkus Duft throws IOException, CorruptMediaFile { 240c0bb9928SMarkus Duft HttpConnection contentServer = LfsConnectionFactory 241c0bb9928SMarkus Duft .getLfsContentConnection(getRepository(), uploadAction, 242c0bb9928SMarkus Duft METHOD_PUT); 243c0bb9928SMarkus Duft contentServer.setDoOutput(true); 244c0bb9928SMarkus Duft try (OutputStream out = contentServer 245c0bb9928SMarkus Duft .getOutputStream()) { 246c0bb9928SMarkus Duft long size = Files.copy(path, out); 247c0bb9928SMarkus Duft if (size != o.size) { 248c0bb9928SMarkus Duft throw new CorruptMediaFile(path, o.size, size); 249c0bb9928SMarkus Duft } 250c0bb9928SMarkus Duft } 251c0bb9928SMarkus Duft int responseCode = contentServer.getResponseCode(); 252c0bb9928SMarkus Duft if (responseCode != HTTP_OK) { 253c0bb9928SMarkus Duft throw new IOException(MessageFormat.format( 254c0bb9928SMarkus Duft LfsText.get().serverFailure, contentServer.getURL(), 255c0bb9928SMarkus Duft Integer.valueOf(responseCode))); 256c0bb9928SMarkus Duft } 257c0bb9928SMarkus Duft } 258c0bb9928SMarkus Duft } 259