Add servlet to serve images The plugin originally relied on changes in Gerrit core to serve the images, but these changes were never merged [1]. Implement an own servlet to make images browsable. [1] https://gerrit-review.googlesource.com/53918 Change-Id: I9af6f9bc47ad462fd0323ea79ce62560812b63ac Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java index d72589c..daf5355 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java +++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java
@@ -24,6 +24,9 @@ @Override protected void configureServlets() { + serveRegex("^" + ImageServlet.PATH_PREFIX + "(.+)?$") + .with(ImageServlet.class); + DynamicSet.bind(binder(), WebUiPlugin.class) .toInstance(new GwtPlugin("imagare")); DynamicSet.bind(binder(), WebUiPlugin.class)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/ImageServlet.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/ImageServlet.java new file mode 100644 index 0000000..7396b9b --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/ImageServlet.java
@@ -0,0 +1,293 @@ +// Copyright (C) 2014 The Android Open Source Project +// +// 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.googlesource.gerrit.plugins.imagare; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED; + +import com.google.common.base.CharMatcher; +import com.google.common.hash.Hashing; +import com.google.common.net.HttpHeaders; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.IdString; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.FileTypeRegistry; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.project.GetHead; +import com.google.gerrit.server.project.NoSuchProjectException; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.project.ProjectControl; +import com.google.gerrit.server.project.ProjectResource; +import com.google.gerrit.server.project.ProjectState; +import com.google.gwtexpui.server.CacheHeaders; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.errors.RevisionSyntaxException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.PathFilter; + +import eu.medsea.mimeutil.MimeType; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Singleton +public class ImageServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + public static final String PATH_PREFIX = "/project/"; + + private final ProjectControl.Factory projectControlFactory; + private final ProjectCache projectCache; + private final Provider<GetHead> getHead; + private final GitRepositoryManager repoManager; + private final FileTypeRegistry fileTypeRegistry; + + @Inject + ImageServlet( + ProjectControl.Factory projectControlFactory, + ProjectCache projectCache, + Provider<GetHead> getHead, + GitRepositoryManager repoManager, + FileTypeRegistry fileTypeRegistry) { + this.projectControlFactory = projectControlFactory; + this.projectCache = projectCache; + this.getHead = getHead; + this.repoManager = repoManager; + this.fileTypeRegistry = fileTypeRegistry; + } + + @Override + public void service(HttpServletRequest req, HttpServletResponse res) + throws IOException { + if (!"GET".equals(req.getMethod()) && !"HEAD".equals(req.getMethod())) { + CacheHeaders.setNotCacheable(res); + res.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + + ResourceKey key = ResourceKey.fromPath(req.getPathInfo()); + ProjectState state = projectCache.get(key.project); + if (state == null || key.file == null) { + notFound(res); + return; + } + + MimeType mimeType = fileTypeRegistry.getMimeType(key.file, null); + if (!("image".equals(mimeType.getMediaType()) + && fileTypeRegistry.isSafeInline(mimeType))) { + notFound(res); + return; + } + + try { + ProjectControl projectControl = projectControlFactory.validateFor(key.project); + String rev = key.revision; + if (rev == null || Constants.HEAD.equals(rev)) { + rev = getHead.get().apply(new ProjectResource(projectControl)); + } else { + if (!ObjectId.isId(rev)) { + if (!rev.startsWith(Constants.R_REFS)) { + rev = Constants.R_HEADS + rev; + } + if (!projectControl.controlForRef(rev).isVisible()) { + notFound(res); + return; + } + } + } + Repository repo = repoManager.openRepository(key.project); + try { + ObjectId revId = + repo.resolve(rev != null ? rev : Constants.HEAD); + if (revId == null) { + notFound(res); + return; + } + + if (ObjectId.isId(rev)) { + RevWalk rw = new RevWalk(repo); + try { + RevCommit commit = rw.parseCommit(repo.resolve(rev)); + if (!projectControl.canReadCommit(rw, commit)) { + notFound(res); + return; + } + } finally { + rw.release(); + } + } + + String eTag = null; + String receivedETag = req.getHeader(HttpHeaders.IF_NONE_MATCH); + if (receivedETag != null) { + eTag = computeETag(key.project, revId, key.file); + if (eTag.equals(receivedETag)) { + res.sendError(SC_NOT_MODIFIED); + return; + } + } + + if (!"image".equals(mimeType.getMediaType())) { + notFound(res); + return; + } + + RevWalk rw = new RevWalk(repo); + try { + RevCommit commit = rw.parseCommit(revId); + RevTree tree = commit.getTree(); + TreeWalk tw = new TreeWalk(repo); + try { + tw.addTree(tree); + tw.setRecursive(true); + tw.setFilter(PathFilter.create(key.file)); + if (!tw.next()) { + notFound(res); + return; + } + ObjectId objectId = tw.getObjectId(0); + ObjectLoader loader = repo.open(objectId); + byte[] content = loader.getBytes(Integer.MAX_VALUE); + + mimeType = fileTypeRegistry.getMimeType(key.file, content); + if (!"image".equals(mimeType.getMediaType()) + || !fileTypeRegistry.isSafeInline(mimeType)) { + notFound(res); + return; + } + res.setHeader(HttpHeaders.ETAG, + eTag != null + ? eTag + : computeETag(key.project, revId, key.file)); + CacheHeaders.setCacheablePrivate(res, 7, TimeUnit.DAYS, false); + send(req, res, content, mimeType.toString(), commit.getCommitTime()); + return; + } finally { + tw.release(); + } + } catch (IOException e) { + notFound(res); + return; + } finally { + rw.release(); + } + } finally { + repo.close(); + } + } catch (RepositoryNotFoundException | NoSuchProjectException + | ResourceNotFoundException | AuthException | RevisionSyntaxException e) { + notFound(res); + return; + } + } + + private static String computeETag(Project.NameKey project, ObjectId revId, + String file) { + return Hashing.md5().newHasher() + .putUnencodedChars(project.get()) + .putUnencodedChars(revId.getName()) + .putUnencodedChars(file) + .hash().toString(); + } + + private void send(HttpServletRequest req, HttpServletResponse res, + byte[] content, String contentType, long lastModified) throws IOException { + if (0 < lastModified) { + long ifModifiedSince = req.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE); + if (ifModifiedSince > 0 && ifModifiedSince == lastModified) { + res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + return; + } else { + res.setDateHeader("Last-Modified", lastModified); + } + } + res.setContentType(contentType); + res.setCharacterEncoding(UTF_8.name()); + res.setContentLength(content.length); + res.getOutputStream().write(content); + } + + private static void notFound(HttpServletResponse res) throws IOException { + CacheHeaders.setNotCacheable(res); + res.sendError(HttpServletResponse.SC_NOT_FOUND); + } + + private static class ResourceKey { + final Project.NameKey project; + final String file; + final String revision; + + static ResourceKey fromPath(String path) { + String project; + String file = null; + String revision = null; + + if (!path.startsWith(PATH_PREFIX)) { + // should not happen since this servlet is only registered to handle + // paths that start with this prefix + throw new IllegalStateException("path must start with '" + PATH_PREFIX + "'"); + } + path = path.substring(PATH_PREFIX.length()); + + int i = path.indexOf('/'); + if (i != -1 && i != path.length() - 1) { + project = IdString.fromUrl(path.substring(0, i)).get(); + String rest = path.substring(i + 1); + + if (rest.startsWith("rev/")) { + if (rest.length() > 4) { + rest = rest.substring(4); + i = rest.indexOf('/'); + if (i != -1 && i != path.length() - 1) { + revision = IdString.fromUrl(rest.substring(0, i)).get(); + file = IdString.fromUrl(rest.substring(i + 1)).get(); + } else { + revision = IdString.fromUrl(rest).get(); + } + } + } else { + file = IdString.fromUrl(rest).get(); + } + + } else { + project = IdString.fromUrl(CharMatcher.is('/').trimTrailingFrom(path)).get(); + } + + return new ResourceKey(project, file, revision); + } + + private ResourceKey(String p, String f, String r) { + project = new Project.NameKey(p); + file = f; + revision = r; + } + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/PostImage.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/PostImage.java index ffc966a..de6d0da 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/imagare/PostImage.java +++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/PostImage.java
@@ -15,6 +15,7 @@ package com.googlesource.gerrit.plugins.imagare; import com.google.gerrit.common.ChangeHooks; +import com.google.gerrit.extensions.annotations.PluginName; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.IdString; @@ -76,12 +77,14 @@ private final PersonIdent myIdent; private final String canonicalWebUrl; private final Config cfg; + private final String pluginName; @Inject public PostImage(FileTypeRegistry registry, Provider<IdentifiedUser> self, GitRepositoryManager repoManager, GitReferenceUpdated referenceUpdated, ChangeHooks hooks, @GerritPersonIdent PersonIdent myIdent, - @CanonicalWebUrl String canonicalWebUrl, @GerritServerConfig Config cfg) { + @CanonicalWebUrl String canonicalWebUrl, @GerritServerConfig Config cfg, + @PluginName String pluginName) { this.registry = registry; this.imageDataPattern = Pattern.compile("data:([\\w/.-]+);([\\w]+),(.*)"); this.self = self; @@ -91,6 +94,7 @@ this.myIdent = myIdent; this.canonicalWebUrl = canonicalWebUrl; this.cfg = cfg; + this.pluginName = pluginName; } @Override @@ -238,9 +242,11 @@ StringBuilder url = new StringBuilder(); url.append(canonicalWebUrl); if (!canonicalWebUrl.endsWith("/")) { - url.append("src/"); + url.append("/"); } - url.append("src/"); + url.append("plugins/"); + url.append(IdString.fromDecoded(pluginName).encoded()); + url.append("/project/"); url.append(IdString.fromDecoded(project.get()).encoded()); url.append("/rev/"); url.append(IdString.fromDecoded(rev).encoded());
diff --git a/src/main/resources/static/imagare.js b/src/main/resources/static/imagare.js index c23e5db..322d889 100644 --- a/src/main/resources/static/imagare.js +++ b/src/main/resources/static/imagare.js
@@ -56,7 +56,7 @@ } function isImage(href) { - return href.match(window.location.hostname + '.*src/.*/rev/.*/.*\.(jpg|jpeg|png|gif|bmp|ico|svg|tif|tiff)') + return href.match(window.location.hostname + '.*project/.*/rev/.*/.*\.(jpg|jpeg|png|gif|bmp|ico|svg|tif|tiff)') } Gerrit.on('history', onHistory);