View Javadoc
1   /*
2    * Copyright (C) 2017, Markus Duft <markus.duft@ssi-schaefer.com>
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  package org.eclipse.jgit.lfs;
44  
45  import static java.nio.charset.StandardCharsets.UTF_8;
46  import static org.eclipse.jgit.lfs.Protocol.OPERATION_UPLOAD;
47  import static org.eclipse.jgit.lfs.internal.LfsConnectionFactory.toRequest;
48  import static org.eclipse.jgit.transport.http.HttpConnection.HTTP_OK;
49  import static org.eclipse.jgit.util.HttpSupport.METHOD_POST;
50  import static org.eclipse.jgit.util.HttpSupport.METHOD_PUT;
51  
52  import java.io.IOException;
53  import java.io.InputStream;
54  import java.io.InputStreamReader;
55  import java.io.OutputStream;
56  import java.io.PrintStream;
57  import java.nio.file.Files;
58  import java.nio.file.Path;
59  import java.text.MessageFormat;
60  import java.util.Collection;
61  import java.util.HashMap;
62  import java.util.List;
63  import java.util.Map;
64  import java.util.Set;
65  import java.util.TreeSet;
66  
67  import org.eclipse.jgit.api.errors.AbortedByHookException;
68  import org.eclipse.jgit.errors.IncorrectObjectTypeException;
69  import org.eclipse.jgit.errors.MissingObjectException;
70  import org.eclipse.jgit.hooks.PrePushHook;
71  import org.eclipse.jgit.lfs.Protocol.ObjectInfo;
72  import org.eclipse.jgit.lfs.errors.CorruptMediaFile;
73  import org.eclipse.jgit.lfs.internal.LfsConnectionFactory;
74  import org.eclipse.jgit.lfs.internal.LfsText;
75  import org.eclipse.jgit.lib.AnyObjectId;
76  import org.eclipse.jgit.lib.Constants;
77  import org.eclipse.jgit.lib.ObjectId;
78  import org.eclipse.jgit.lib.ObjectReader;
79  import org.eclipse.jgit.lib.Ref;
80  import org.eclipse.jgit.lib.RefDatabase;
81  import org.eclipse.jgit.lib.Repository;
82  import org.eclipse.jgit.revwalk.ObjectWalk;
83  import org.eclipse.jgit.revwalk.RevObject;
84  import org.eclipse.jgit.transport.RemoteRefUpdate;
85  import org.eclipse.jgit.transport.http.HttpConnection;
86  
87  import com.google.gson.Gson;
88  import com.google.gson.stream.JsonReader;
89  
90  /**
91   * Pre-push hook that handles uploading LFS artefacts.
92   *
93   * @since 4.11
94   */
95  public class LfsPrePushHook extends PrePushHook {
96  
97  	private static final String EMPTY = ""; //$NON-NLS-1$
98  	private Collection<RemoteRefUpdate> refs;
99  
100 	/**
101 	 * @param repo
102 	 *            the repository
103 	 * @param outputStream
104 	 *            not used by this implementation
105 	 */
106 	public LfsPrePushHook(Repository repo, PrintStream outputStream) {
107 		super(repo, outputStream);
108 	}
109 
110 	@Override
111 	public void setRefs(Collection<RemoteRefUpdate> toRefs) {
112 		this.refs = toRefs;
113 	}
114 
115 	@Override
116 	public String call() throws IOException, AbortedByHookException {
117 		Set<LfsPointer> toPush = findObjectsToPush();
118 		if (toPush.isEmpty()) {
119 			return EMPTY;
120 		}
121 		HttpConnection api = LfsConnectionFactory.getLfsConnection(
122 				getRepository(), METHOD_POST, OPERATION_UPLOAD);
123 		Map<String, LfsPointer> oid2ptr = requestBatchUpload(api, toPush);
124 		uploadContents(api, oid2ptr);
125 		return EMPTY;
126 
127 	}
128 
129 	private Set<LfsPointer> findObjectsToPush() throws IOException,
130 			MissingObjectException, IncorrectObjectTypeException {
131 		Set<LfsPointer> toPush = new TreeSet<>();
132 
133 		try (ObjectWalk walk = new ObjectWalk(getRepository())) {
134 			for (RemoteRefUpdate up : refs) {
135 				walk.setRewriteParents(false);
136 				excludeRemoteRefs(walk);
137 				walk.markStart(walk.parseCommit(up.getNewObjectId()));
138 				while (walk.next() != null) {
139 					// walk all commits to populate objects
140 				}
141 				findLfsPointers(toPush, walk);
142 			}
143 		}
144 		return toPush;
145 	}
146 
147 	private static void findLfsPointers(Set<LfsPointer> toPush, ObjectWalk walk)
148 			throws MissingObjectException, IncorrectObjectTypeException,
149 			IOException {
150 		RevObject obj;
151 		ObjectReader r = walk.getObjectReader();
152 		while ((obj = walk.nextObject()) != null) {
153 			if (obj.getType() == Constants.OBJ_BLOB
154 					&& getObjectSize(r, obj) < LfsPointer.SIZE_THRESHOLD) {
155 				LfsPointer ptr = loadLfsPointer(r, obj);
156 				if (ptr != null) {
157 					toPush.add(ptr);
158 				}
159 			}
160 		}
161 	}
162 
163 	private static long getObjectSize(ObjectReader r, RevObject obj)
164 			throws IOException {
165 		return r.getObjectSize(obj.getId(), Constants.OBJ_BLOB);
166 	}
167 
168 	private static LfsPointer loadLfsPointer(ObjectReader r, AnyObjectId obj)
169 			throws IOException {
170 		try (InputStream is = r.open(obj, Constants.OBJ_BLOB).openStream()) {
171 			return LfsPointer.parseLfsPointer(is);
172 		}
173 	}
174 
175 	private void excludeRemoteRefs(ObjectWalk walk) throws IOException {
176 		RefDatabase refDatabase = getRepository().getRefDatabase();
177 		List<Ref> remoteRefs = refDatabase.getRefsByPrefix(remote());
178 		for (Ref r : remoteRefs) {
179 			ObjectId oid = r.getPeeledObjectId();
180 			if (oid == null) {
181 				oid = r.getObjectId();
182 			}
183 			if (oid == null) {
184 				// ignore (e.g. symbolic, ...)
185 				continue;
186 			}
187 			RevObject o = walk.parseAny(oid);
188 			if (o.getType() == Constants.OBJ_COMMIT
189 					|| o.getType() == Constants.OBJ_TAG) {
190 				walk.markUninteresting(o);
191 			}
192 		}
193 	}
194 
195 	private String remote() {
196 		String remoteName = getRemoteName() == null
197 				? Constants.DEFAULT_REMOTE_NAME
198 				: getRemoteName();
199 		return Constants.R_REMOTES + remoteName;
200 	}
201 
202 	private Map<String, LfsPointer> requestBatchUpload(HttpConnection api,
203 			Set<LfsPointer> toPush) throws IOException {
204 		LfsPointer[] res = toPush.toArray(new LfsPointer[0]);
205 		Map<String, LfsPointer> oidStr2ptr = new HashMap<>();
206 		for (LfsPointer p : res) {
207 			oidStr2ptr.put(p.getOid().name(), p);
208 		}
209 		Gson gson = Protocol.gson();
210 		api.getOutputStream().write(
211 				gson.toJson(toRequest(OPERATION_UPLOAD, res)).getBytes(UTF_8));
212 		int responseCode = api.getResponseCode();
213 		if (responseCode != HTTP_OK) {
214 			throw new IOException(
215 					MessageFormat.format(LfsText.get().serverFailure,
216 							api.getURL(), Integer.valueOf(responseCode)));
217 		}
218 		return oidStr2ptr;
219 	}
220 
221 	private void uploadContents(HttpConnection api,
222 			Map<String, LfsPointer> oid2ptr) throws IOException {
223 		try (JsonReader reader = new JsonReader(
224 				new InputStreamReader(api.getInputStream(), UTF_8))) {
225 			for (Protocol.ObjectInfo o : parseObjects(reader)) {
226 				if (o.actions == null) {
227 					continue;
228 				}
229 				LfsPointer ptr = oid2ptr.get(o.oid);
230 				if (ptr == null) {
231 					// received an object we didn't request
232 					continue;
233 				}
234 				Protocol.Action uploadAction = o.actions.get(OPERATION_UPLOAD);
235 				if (uploadAction == null || uploadAction.href == null) {
236 					continue;
237 				}
238 
239 				Lfs lfs = new Lfs(getRepository());
240 				Path path = lfs.getMediaFile(ptr.getOid());
241 				if (!Files.exists(path)) {
242 					throw new IOException(MessageFormat
243 							.format(LfsText.get().missingLocalObject, path));
244 				}
245 				uploadFile(o, uploadAction, path);
246 			}
247 		}
248 	}
249 
250 	private List<ObjectInfo> parseObjects(JsonReader reader) {
251 		Gson gson = new Gson();
252 		Protocol.Response resp = gson.fromJson(reader, Protocol.Response.class);
253 		return resp.objects;
254 	}
255 
256 	private void uploadFile(Protocol.ObjectInfo o,
257 			Protocol.Action uploadAction, Path path)
258 			throws IOException, CorruptMediaFile {
259 		HttpConnection contentServer = LfsConnectionFactory
260 				.getLfsContentConnection(getRepository(), uploadAction,
261 						METHOD_PUT);
262 		contentServer.setDoOutput(true);
263 		try (OutputStream out = contentServer
264 				.getOutputStream()) {
265 			long size = Files.copy(path, out);
266 			if (size != o.size) {
267 				throw new CorruptMediaFile(path, o.size, size);
268 			}
269 		}
270 		int responseCode = contentServer.getResponseCode();
271 		if (responseCode != HTTP_OK) {
272 			throw new IOException(MessageFormat.format(
273 					LfsText.get().serverFailure, contentServer.getURL(),
274 					Integer.valueOf(responseCode)));
275 		}
276 	}
277 }