blob: 5d9eb8c56b79a90769210149460dcfc7e69f2855 [file] [log] [blame]
// 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 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;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.mime.FileTypeRegistry;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.RefControl;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.googlesource.gerrit.plugins.imagare.PostImage.Input;
import eu.medsea.mimeutil.MimeType;
import org.apache.commons.lang.ArrayUtils;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.TreeFormatter;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.Base64;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PostImage implements RestModifyView<ProjectResource, Input> {
public static class Input {
public String imageData;
public String fileName;
}
private final FileTypeRegistry registry;
private final Pattern imageDataPattern;
private final Provider<IdentifiedUser> self;
private final GitRepositoryManager repoManager;
private final GitReferenceUpdated referenceUpdated;
private final ChangeHooks hooks;
private final PersonIdent myIdent;
private final String canonicalWebUrl;
private final Config cfg;
private final Provider<ReviewDb> db;
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,
Provider<ReviewDb> db, @PluginName String pluginName) {
this.registry = registry;
this.imageDataPattern = Pattern.compile("data:([\\w/.-]+);([\\w]+),(.*)");
this.self = self;
this.repoManager = repoManager;
this.referenceUpdated = referenceUpdated;
this.hooks = hooks;
this.myIdent = myIdent;
this.canonicalWebUrl = canonicalWebUrl;
this.cfg = cfg;
this.db = db;
this.pluginName = pluginName;
}
@Override
public Response<ImageInfo> apply(ProjectResource rsrc, Input input)
throws MethodNotAllowedException, BadRequestException, AuthException,
IOException, ResourceConflictException {
if (input == null) {
input = new Input();
}
ImageInfo info;
if (input.imageData != null) {
info = storeImage(rsrc.getControl(), input.imageData, input.fileName);
} else {
throw new BadRequestException("no image data");
}
return Response.created(info);
}
private ImageInfo storeImage(ProjectControl pc, String imageData,
String fileName) throws MethodNotAllowedException, BadRequestException,
AuthException, IOException, ResourceConflictException {
Matcher m = imageDataPattern.matcher(imageData);
if (m.matches()) {
String receivedMimeType = m.group(1);
String encoding = m.group(2);
String encodedContent = m.group(3);
if (fileName == null) {
int pos = receivedMimeType.indexOf('/');
if (pos > 0 && pos + 1 < receivedMimeType.length()) {
fileName = "img." + receivedMimeType.substring(pos + 1);
} else {
throw new BadRequestException("bad mime type: " + receivedMimeType);
}
}
if ("base64".equals(encoding)) {
byte[] content = Base64.decode(encodedContent);
MimeType mimeType =
registry.getMimeType("img." + receivedMimeType, content);
if (!"image".equals(mimeType.getMediaType())) {
throw new MethodNotAllowedException("no image");
}
if (!receivedMimeType.equals(mimeType.toString())) {
throw new BadRequestException("incorrect mime type");
}
return new ImageInfo(storeImage(pc, mimeType, content, fileName));
} else {
throw new MethodNotAllowedException("unsupported encoding");
}
} else {
throw new BadRequestException("invalid image data");
}
}
private String storeImage(ProjectControl pc, MimeType mimeType,
byte[] content, String fileName) throws AuthException, IOException,
ResourceConflictException {
long maxSize = getEffectiveMaxObjectSizeLimit(pc.getProjectState());
// maxSize == 0 means that there is no limit
if (maxSize > 0 && content.length > maxSize) {
throw new ResourceConflictException("image too large");
}
String ref = getRef(content, fileName);
RefControl rc = pc.controlForRef(ref);
try (Repository repo = repoManager.openRepository(pc.getProject().getNameKey())) {
ObjectId commitId = repo.resolve(ref);
if (commitId != null) {
// this image exists already
return getUrl(pc.getProject().getNameKey(), ref, fileName);
}
try (RevWalk rw = new RevWalk(repo);
ObjectInserter oi = repo.newObjectInserter()) {
ObjectId blobId = oi.insert(Constants.OBJ_BLOB, content);
oi.flush();
TreeFormatter tf = new TreeFormatter();
tf.append(fileName, FileMode.REGULAR_FILE, blobId);
ObjectId treeId = tf.insertTo(oi);
oi.flush();
PersonIdent authorIdent =
self.get().newCommitterIdent(myIdent.getWhen(), myIdent.getTimeZone());
CommitBuilder cb = new CommitBuilder();
cb.setTreeId(treeId);
cb.setAuthor(authorIdent);
cb.setCommitter(authorIdent);
cb.setMessage("Image Upload");
commitId = oi.insert(cb);
oi.flush();
if (!rc.canCreate(db.get(), rw, rw.parseCommit(commitId))) {
throw new AuthException(String.format(
"Project %s doesn't allow image upload.", pc.getProject().getName()));
}
RefUpdate ru = repo.updateRef(ref);
ru.setExpectedOldObjectId(ObjectId.zeroId());
ru.setNewObjectId(commitId);
ru.disableRefLog();
if (ru.update(rw) == RefUpdate.Result.NEW) {
referenceUpdated.fire(pc.getProject().getNameKey(), ru);
hooks.doRefUpdatedHook(new Branch.NameKey(pc.getProject()
.getNameKey(), ref), ru, self.get().getAccount());
} else {
throw new IOException(String.format(
"Failed to create ref %s in %s: %s", ref,
pc.getProject().getName(), ru.getResult()));
}
return getUrl(pc.getProject().getNameKey(), ref, fileName);
}
}
}
private String getRef(byte[] content, String fileName) {
try (ObjectInserter oi = new ObjectInserter.Formatter()) {
String id = oi.idFor(Constants.OBJ_BLOB,
ArrayUtils.addAll(content, fileName.getBytes())).getName();
StringBuilder ref = new StringBuilder();
ref.append(Constants.R_REFS);
ref.append("images/");
ref.append(id.substring(0, 2));
ref.append("/");
ref.append(id.substring(2));
return ref.toString();
}
}
private String getUrl(Project.NameKey project, String rev, String fileName) {
StringBuilder url = new StringBuilder();
url.append(canonicalWebUrl);
if (!canonicalWebUrl.endsWith("/")) {
url.append("/");
}
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());
url.append("/");
url.append(IdString.fromDecoded(fileName).encoded());
return url.toString();
}
private long getEffectiveMaxObjectSizeLimit(ProjectState p) {
long global = cfg.getLong("receive", "maxObjectSizeLimit", 0);
long local = p.getMaxObjectSizeLimit();
if (global > 0 && local > 0) {
return Math.min(global, local);
} else {
// zero means "no limit", in this case the max is more limiting
return Math.max(global, local);
}
}
public static class ImageInfo {
public String url;
public ImageInfo(String url) {
this.url = url;
}
}
}