You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SmudgeFilter.java 7.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. /*
  2. * Copyright (C) 2016, 2021 Christian Halstrick <christian.halstrick@sap.com> and others
  3. *
  4. * This program and the accompanying materials are made available under the
  5. * terms of the Eclipse Distribution License v. 1.0 which is available at
  6. * https://www.eclipse.org/org/documents/edl-v10.php.
  7. *
  8. * SPDX-License-Identifier: BSD-3-Clause
  9. */
  10. package org.eclipse.jgit.lfs;
  11. import static java.nio.charset.StandardCharsets.UTF_8;
  12. import java.io.BufferedInputStream;
  13. import java.io.IOException;
  14. import java.io.InputStream;
  15. import java.io.InputStreamReader;
  16. import java.io.OutputStream;
  17. import java.nio.file.Files;
  18. import java.nio.file.Path;
  19. import java.text.MessageFormat;
  20. import java.util.ArrayList;
  21. import java.util.Collection;
  22. import java.util.HashMap;
  23. import java.util.Map;
  24. import org.eclipse.jgit.attributes.FilterCommand;
  25. import org.eclipse.jgit.attributes.FilterCommandFactory;
  26. import org.eclipse.jgit.attributes.FilterCommandRegistry;
  27. import org.eclipse.jgit.lfs.internal.LfsConnectionFactory;
  28. import org.eclipse.jgit.lfs.internal.LfsText;
  29. import org.eclipse.jgit.lfs.lib.AnyLongObjectId;
  30. import org.eclipse.jgit.lfs.lib.Constants;
  31. import org.eclipse.jgit.lib.Repository;
  32. import org.eclipse.jgit.transport.http.HttpConnection;
  33. import org.eclipse.jgit.util.HttpSupport;
  34. import com.google.gson.Gson;
  35. import com.google.gson.stream.JsonReader;
  36. /**
  37. * Built-in LFS smudge filter
  38. *
  39. * When content is read from git's object-database and written to the filesystem
  40. * and this filter is configured for that content, then this filter will replace
  41. * the content of LFS pointer files with the original content. This happens e.g.
  42. * when a checkout needs to update a working tree file which is under LFS
  43. * control.
  44. *
  45. * @since 4.6
  46. */
  47. public class SmudgeFilter extends FilterCommand {
  48. /**
  49. * Max number of bytes to copy in a single {@link #run()} call.
  50. */
  51. private static final int MAX_COPY_BYTES = 1024 * 1024 * 256;
  52. /**
  53. * The factory is responsible for creating instances of
  54. * {@link org.eclipse.jgit.lfs.SmudgeFilter}
  55. */
  56. public static final FilterCommandFactory FACTORY = SmudgeFilter::new;
  57. /**
  58. * Register this filter in JGit
  59. */
  60. static void register() {
  61. FilterCommandRegistry
  62. .register(org.eclipse.jgit.lib.Constants.BUILTIN_FILTER_PREFIX
  63. + Constants.ATTR_FILTER_DRIVER_PREFIX
  64. + org.eclipse.jgit.lib.Constants.ATTR_FILTER_TYPE_SMUDGE,
  65. FACTORY);
  66. }
  67. /**
  68. * Constructor for SmudgeFilter.
  69. *
  70. * @param db
  71. * a {@link org.eclipse.jgit.lib.Repository} object.
  72. * @param in
  73. * a {@link java.io.InputStream} object. The stream is closed in
  74. * any case.
  75. * @param out
  76. * a {@link java.io.OutputStream} object.
  77. * @throws java.io.IOException
  78. * in case of an error
  79. */
  80. public SmudgeFilter(Repository db, InputStream in, OutputStream out)
  81. throws IOException {
  82. this(in.markSupported() ? in : new BufferedInputStream(in), out, db);
  83. }
  84. private SmudgeFilter(InputStream in, OutputStream out, Repository db)
  85. throws IOException {
  86. super(in, out);
  87. InputStream from = in;
  88. try {
  89. LfsPointer res = LfsPointer.parseLfsPointer(from);
  90. if (res != null) {
  91. AnyLongObjectId oid = res.getOid();
  92. Lfs lfs = new Lfs(db);
  93. Path mediaFile = lfs.getMediaFile(oid);
  94. if (!Files.exists(mediaFile)) {
  95. downloadLfsResource(lfs, db, res);
  96. }
  97. this.in = Files.newInputStream(mediaFile);
  98. } else {
  99. // Not swapped; stream was reset, don't close!
  100. from = null;
  101. }
  102. } finally {
  103. if (from != null) {
  104. from.close(); // Close the swapped-out stream
  105. }
  106. }
  107. }
  108. /**
  109. * Download content which is hosted on a LFS server
  110. *
  111. * @param lfs
  112. * local {@link Lfs} storage.
  113. * @param db
  114. * the repository to work with
  115. * @param res
  116. * the objects to download
  117. * @return the paths of all mediafiles which have been downloaded
  118. * @throws IOException
  119. * @since 4.11
  120. */
  121. public static Collection<Path> downloadLfsResource(Lfs lfs, Repository db,
  122. LfsPointer... res) throws IOException {
  123. Collection<Path> downloadedPaths = new ArrayList<>();
  124. Map<String, LfsPointer> oidStr2ptr = new HashMap<>();
  125. for (LfsPointer p : res) {
  126. oidStr2ptr.put(p.getOid().name(), p);
  127. }
  128. HttpConnection lfsServerConn = LfsConnectionFactory.getLfsConnection(db,
  129. HttpSupport.METHOD_POST, Protocol.OPERATION_DOWNLOAD);
  130. Gson gson = Protocol.gson();
  131. lfsServerConn.getOutputStream()
  132. .write(gson
  133. .toJson(LfsConnectionFactory
  134. .toRequest(Protocol.OPERATION_DOWNLOAD, res))
  135. .getBytes(UTF_8));
  136. int responseCode = lfsServerConn.getResponseCode();
  137. if (!(responseCode == HttpConnection.HTTP_OK
  138. || responseCode == HttpConnection.HTTP_NOT_AUTHORITATIVE)) {
  139. throw new IOException(
  140. MessageFormat.format(LfsText.get().serverFailure,
  141. lfsServerConn.getURL(),
  142. Integer.valueOf(responseCode)));
  143. }
  144. try (JsonReader reader = new JsonReader(
  145. new InputStreamReader(lfsServerConn.getInputStream(),
  146. UTF_8))) {
  147. Protocol.Response resp = gson.fromJson(reader,
  148. Protocol.Response.class);
  149. for (Protocol.ObjectInfo o : resp.objects) {
  150. if (o.error != null) {
  151. throw new IOException(
  152. MessageFormat.format(LfsText.get().protocolError,
  153. Integer.valueOf(o.error.code),
  154. o.error.message));
  155. }
  156. if (o.actions == null) {
  157. continue;
  158. }
  159. LfsPointer ptr = oidStr2ptr.get(o.oid);
  160. if (ptr == null) {
  161. // received an object we didn't request
  162. continue;
  163. }
  164. if (ptr.getSize() != o.size) {
  165. throw new IOException(MessageFormat.format(
  166. LfsText.get().inconsistentContentLength,
  167. lfsServerConn.getURL(), Long.valueOf(ptr.getSize()),
  168. Long.valueOf(o.size)));
  169. }
  170. Protocol.Action downloadAction = o.actions
  171. .get(Protocol.OPERATION_DOWNLOAD);
  172. if (downloadAction == null || downloadAction.href == null) {
  173. continue;
  174. }
  175. HttpConnection contentServerConn = LfsConnectionFactory
  176. .getLfsContentConnection(db, downloadAction,
  177. HttpSupport.METHOD_GET);
  178. responseCode = contentServerConn.getResponseCode();
  179. if (responseCode != HttpConnection.HTTP_OK) {
  180. throw new IOException(
  181. MessageFormat.format(LfsText.get().serverFailure,
  182. contentServerConn.getURL(),
  183. Integer.valueOf(responseCode)));
  184. }
  185. Path path = lfs.getMediaFile(ptr.getOid());
  186. Path parent = path.getParent();
  187. if (parent != null) {
  188. parent.toFile().mkdirs();
  189. }
  190. try (InputStream contentIn = contentServerConn
  191. .getInputStream()) {
  192. long bytesCopied = Files.copy(contentIn, path);
  193. if (bytesCopied != o.size) {
  194. throw new IOException(MessageFormat.format(
  195. LfsText.get().wrongAmoutOfDataReceived,
  196. contentServerConn.getURL(),
  197. Long.valueOf(bytesCopied),
  198. Long.valueOf(o.size)));
  199. }
  200. downloadedPaths.add(path);
  201. }
  202. }
  203. }
  204. return downloadedPaths;
  205. }
  206. /** {@inheritDoc} */
  207. @Override
  208. public int run() throws IOException {
  209. try {
  210. int totalRead = 0;
  211. int length = 0;
  212. if (in != null) {
  213. byte[] buf = new byte[8192];
  214. while ((length = in.read(buf)) != -1) {
  215. out.write(buf, 0, length);
  216. totalRead += length;
  217. // when threshold reached, loop back to the caller.
  218. // otherwise we could only support files up to 2GB (int
  219. // return type) properly. we will be called again as long as
  220. // we don't return -1 here.
  221. if (totalRead >= MAX_COPY_BYTES) {
  222. // leave streams open - we need them in the next call.
  223. return totalRead;
  224. }
  225. }
  226. }
  227. if (totalRead == 0 && length == -1) {
  228. // we're totally done :) cleanup all streams
  229. in.close();
  230. out.close();
  231. return length;
  232. }
  233. return totalRead;
  234. } catch (IOException e) {
  235. in.close(); // clean up - we swapped this stream.
  236. out.close();
  237. throw e;
  238. }
  239. }
  240. }