package org.activityinfo.service.blob;
import com.google.appengine.api.appidentity.AppIdentityService;
import com.google.appengine.api.appidentity.AppIdentityServiceFactory;
import com.google.appengine.api.blobstore.BlobKey;
import com.google.appengine.api.blobstore.BlobstoreService;
import com.google.appengine.api.blobstore.BlobstoreServiceFactory;
import com.google.appengine.api.images.Image;
import com.google.appengine.api.images.ImagesService;
import com.google.appengine.api.images.ImagesServiceFactory;
import com.google.appengine.api.images.Transform;
import com.google.appengine.tools.cloudstorage.GcsFileOptions;
import com.google.appengine.tools.cloudstorage.GcsFileOptions.Builder;
import com.google.appengine.tools.cloudstorage.GcsFilename;
import com.google.appengine.tools.cloudstorage.GcsInputChannel;
import com.google.appengine.tools.cloudstorage.GcsOutputChannel;
import com.google.appengine.tools.cloudstorage.GcsService;
import com.google.appengine.tools.cloudstorage.GcsServiceFactory;
import com.google.appengine.tools.cloudstorage.RetryParams;
import com.google.common.io.ByteSource;
import com.google.common.io.ByteStreams;
import com.google.inject.Inject;
import com.sun.jersey.api.core.InjectParam;
import org.activityinfo.model.auth.AuthenticatedUser;
import org.activityinfo.model.resource.ResourceId;
import org.activityinfo.model.resource.Resources;
import org.activityinfo.server.DeploymentEnvironment;
import org.activityinfo.server.util.blob.DevAppIdentityService;
import org.activityinfo.service.DeploymentConfiguration;
import org.activityinfo.service.gcs.GcsAppIdentityServiceUrlSigner;
import org.joda.time.Period;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.channels.Channels;
import java.util.logging.Logger;
import static javax.ws.rs.core.Response.Status.UNAUTHORIZED;
@Path("/service/blob")
public class GcsBlobFieldStorageService implements BlobFieldStorageService {
private static final int ONE_MEGABYTE = 1 << 20;
private static final Logger LOGGER = Logger.getLogger(GcsBlobFieldStorageService.class.getName());
private final String bucketName;
private AppIdentityService appIdentityService;
@Inject
public GcsBlobFieldStorageService(DeploymentConfiguration config) {
this.bucketName = config.getBlobServiceBucketName();
appIdentityService = DeploymentEnvironment.isAppEngineDevelopment() ?
new DevAppIdentityService(config) : AppIdentityServiceFactory.getAppIdentityService();
LOGGER.info("Service account: " + appIdentityService.getServiceAccountName());
}
@Override
public URI getBlobUrl(BlobId blobId) {
GcsAppIdentityServiceUrlSigner signer = new GcsAppIdentityServiceUrlSigner();
try {
return new URI(signer.getSignedUrl("GET", bucketName + "/" + blobId.asString()));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void put(AuthenticatedUser authenticatedUser, String contentDisposition, String mimeType, BlobId blobId,
ByteSource byteSource) throws IOException {
GcsFilename gcsFilename = new GcsFilename(bucketName, blobId.asString());
GcsService gcsService = GcsServiceFactory.createGcsService(RetryParams.getDefaultInstance());
Builder builder = new Builder();
builder.contentDisposition(contentDisposition);
builder.mimeType(mimeType);
GcsFileOptions gcsFileOptions = builder.build();
GcsOutputChannel channel = gcsService.createOrReplace(gcsFilename, gcsFileOptions);
try (OutputStream outputStream = Channels.newOutputStream(channel)) {
byteSource.copyTo(outputStream);
}
}
@GET
@Path("{resourceId}/{fieldId}/{blobId}/image")
@Override
public Response getImage(@InjectParam AuthenticatedUser user,
@PathParam("resourceId") ResourceId resourceId,
@PathParam("fieldId") ResourceId fieldId,
@PathParam("blobId") BlobId blobId) throws IOException {
/* TODO: Ensure that users can download images they've just uploaded
ImageRowValue imageRowValue = getImageRowValue(user, resourceId, fieldId, blobId);
*/
GcsFilename gcsFilename = new GcsFilename(bucketName, blobId.asString());
GcsService gcsService = GcsServiceFactory.createGcsService(RetryParams.getDefaultInstance());
GcsInputChannel gcsInputChannel = gcsService.openPrefetchingReadChannel(gcsFilename, 0, ONE_MEGABYTE);
try (InputStream inputStream = Channels.newInputStream(gcsInputChannel)) {
return Response.ok(ByteStreams.toByteArray(inputStream))/*.type(imageRowValue.getMimeType())*/.build();
}
}
@GET
@Path("{resourceId}/{fieldId}/{blobId}/thumbnail")
@Override
public Response getThumbnail(@InjectParam AuthenticatedUser user,
@PathParam("resourceId") ResourceId resourceId,
@PathParam("fieldId") ResourceId fieldId,
@PathParam("blobId") BlobId blobId,
@QueryParam("width") int width,
@QueryParam("height") int height) {
/* TODO: Ensure that users can see thumbnails of images they've just uploaded
ImageRowValue imageRowValue = getImageRowValue(user, resourceId, fieldId, blobId);
*/
ImagesService imagesService = ImagesServiceFactory.getImagesService();
BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService();
BlobKey blobKey = blobstoreService.createGsBlobKey("/gs/" + bucketName + "/" + blobId.asString());
Image image = ImagesServiceFactory.makeImageFromBlob(blobKey);
/*
if (width != imageRowValue.getWidth() || imageRowValue.getHeight() != height) {
*/
Transform resize = ImagesServiceFactory.makeResize(width, height);
Image newImage = imagesService.applyTransform(resize, image);
byte[] imageData = newImage.getImageData();
return Response.ok(imageData)/*.type(imageRowValue.getMimeType())*/.build();
/*
} else {
return Response.ok(image.getImageData()).type(imageRowValue.getMimeType()).build();
}
*/
}
@POST
@Path("credentials/{blobId}")
@Override
public Response getUploadCredentials(@InjectParam AuthenticatedUser user,
@PathParam("blobId") BlobId blobId) {
if (user == null || user.isAnonymous()) throw new WebApplicationException(UNAUTHORIZED);
GcsUploadCredentialBuilder builder = new GcsUploadCredentialBuilder(appIdentityService);
builder.setBucket(bucketName);
builder.setKey(blobId.asString());
builder.setMaxContentLengthInMegabytes(10);
builder.expireAfter(Period.minutes(5));
return Response.ok(Resources.toJsonObject(builder.build().asRecord())).build();
}
}