Я храню объекты в облачном хранилище Google в следующем формате
bucketname/UUID/object
У меня есть метод Java, который возвращает ссылку для загрузки в следующем формате, чтобы просмотреть файл
https://storage.googleapis.com/bucket/uuid/object?GoogleAccessId
Теперь, что я пытаюсь сделать, это написать java-метод, который бы изменял размеры объекта в случае изображений.
EDIT: это класс, который подключается к GCS.
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.Base64;
import com.google.api.gax.core.FixedCredentialsProvider;
import com.google.api.gax.paging.Page;
import com.google.api.services.storage.StorageScopes;
import com.google.api.services.storage.model.Notification;
import com.google.appengine.api.images.ImagesService;
import com.google.appengine.api.images.ImagesServiceFactory;
import com.google.appengine.api.images.ServingUrlOptions;
import com.google.appengine.tools.cloudstorage.GcsFilename;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.cloud.pubsub.v1.SubscriptionAdminClient;
import com.google.cloud.pubsub.v1.SubscriptionAdminSettings;
import com.google.cloud.pubsub.v1.TopicAdminClient;
import com.google.cloud.pubsub.v1.TopicAdminSettings;
import com.google.cloud.storage.Blob;
import com.google.cloud.storage.BlobInfo;
import com.google.cloud.storage.BucketInfo;
import com.google.cloud.storage.HttpMethod;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.Storage.BlobListOption;
import com.google.cloud.storage.Storage.SignUrlOption;
import com.google.cloud.storage.StorageClass;
import com.google.cloud.storage.StorageOptions;
import com.google.pubsub.v1.ProjectSubscriptionName;
import com.google.pubsub.v1.ProjectTopicName;
import com.google.pubsub.v1.PushConfig;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class connects to the Google cloud storage and handles bucket/object related operations.
*
* @author Roshan
*/
public class GoogleCloudStorage {
private ServiceAccountCredentials creds;
private String saEmail;
private Storage storage;
private ClassLoader classloader;
private TopicAdminSettings topicAdminSettings;
private SubscriptionAdminSettings subscriptionAdminSettings;
private String projectId;
private Logger log;
public GoogleCloudStorage() throws IOException {
log = LoggerFactory.getLogger(this.getClass());
/* Initialize credentials, service account email and other required stuffs. */
this.classloader = Thread.currentThread().getContextClassLoader();
InputStream is = classloader.getResourceAsStream("Key.json");
if (is != null) {
this.creds = ServiceAccountCredentials.fromStream(is);
} else {
log.error("Inputstream from credentials file cannot be null.");
}
this.storage = StorageOptions.newBuilder()
.setCredentials(creds)
.build()
.getService();
this.saEmail = creds.getClientEmail();
this.projectId = creds.getProjectId();
this.topicAdminSettings = TopicAdminSettings.newBuilder()
.setCredentialsProvider(FixedCredentialsProvider.create(creds)).build();
this.subscriptionAdminSettings = SubscriptionAdminSettings.newBuilder()
.setCredentialsProvider(FixedCredentialsProvider.create(creds)).build();
}
/**
* @param bucketName String
*
* This method creates a bucket, named as the given argument.
*/
public void createBucket(String bucketName) {
String topicId = "give topic id here";
if (checkIfBucketExists(bucketName)) {
log.info("Bucket already exists!");
}
storage.create(
BucketInfo.newBuilder(bucketName)
// See here for possible values: http://g.co/cloud/storage/docs/storage-classes
.setStorageClass(StorageClass.COLDLINE)
// Possible values: http://g.co/cloud/storage/docs/bucket-locations#location-mr
.setLocation("europe-north1")
.build());
setBucketNotification(bucketName, topicId);
}
/**
* @param bucketName String
* @param uuid String
* @param objectName String
* @param mimeType String
* @return String
* @throws IOException This method sends POST request to the signed URL using custom headers and
* an empty body, which returns the actual upload location in the response header.
*/
public String getUploadLink(String bucketName, String uuid, String objectName, String mimeType)
throws IOException {
if (!checkIfBucketExists(bucketName)) {
createBucket(bucketName);
}
URL myURL = new URL(getSignedUrlToPost(bucketName, uuid, objectName, mimeType));
HttpURLConnection myURLConnection = (HttpURLConnection) myURL.openConnection();
myURLConnection.setRequestMethod("POST");
myURLConnection.setRequestProperty("Content-Type", mimeType);
myURLConnection.setRequestProperty("x-goog-resumable", "start");
// Send post request
myURLConnection.setDoOutput(true);
DataOutputStream wr = new DataOutputStream(myURLConnection.getOutputStream());
wr.flush();
wr.close();
int responseCode = myURLConnection.getResponseCode();
if (responseCode != 201) {
log.error("Request Failed");
}
return myURLConnection.getHeaderField("Location");
}
/**
* @param bucketName String
* @param blobName String
* @return String
*
* This method returns a downloadlink, valid for 10 minutes, based on the given bucket and object
* name.
*/
public String getDownloadLink(String bucketName, String blobName) {
return storage.signUrl(
BlobInfo.newBuilder(bucketName, blobName).build(),
10,
TimeUnit.MINUTES,
SignUrlOption.httpMethod(HttpMethod.GET)
).toString();
}
/**
* @param bucketName String
* @param uuid String
* @return String
*
* This method returns an object name, based on the given bucketname and uuid.
*/
public String getFileName(String bucketName, String uuid) {
final String[] fileName = {null};
Page<Blob> blobs = storage.list(bucketName, BlobListOption.prefix(uuid));
blobs.iterateAll().forEach(
blob -> fileName[0] = blob.getName()//.substring(blob.getName().lastIndexOf('/') + 1)
);
return fileName[0];
}
/**
* @param bucketName String
* @param uuid String
* @param objectName String
* @param mimeType String
* @return String
*
* This method signs and return the URL to POST, using credentials from above. An uploadlink is
* generated after a POST request is sent to the URL returned by this method, as used by
* 'getDownloadLink()' above.
*/
private String getSignedUrlToPost(String bucketName, String uuid, String objectName,
String mimeType) {
String signedUrl = null;
try {
String verb = "POST";
long expiration = System.currentTimeMillis() / 1000 + 60;
String canonicalizedExtensionHeaders = "x-goog-resumable:start";
byte[] sr = creds.sign(
(verb + "\n\n" + mimeType + "\n" + expiration + "\n" + canonicalizedExtensionHeaders
+
"\n" + "/" + bucketName + "/" + uuid + "/" + objectName).getBytes());
String urlSignature = new String(Base64.encodeBase64(sr));
signedUrl = "https://storage.googleapis.com/" + bucketName + "/" + uuid + "/" + objectName +
"?GoogleAccessId=" + saEmail +
"&Expires=" + expiration +
"&Signature=" + URLEncoder.encode(urlSignature, "UTF-8");
} catch (Exception ex) {
log.error("Caught an Exception {}", ex);
}
return signedUrl;
}
/**
* @param topicName String
* @throws IOException This method creates a topic, based on the given topic name, needed to
* publish/subscribe messages.
*/
public void createTopic(String topicName) throws IOException {
try (TopicAdminClient topicAdminClient = TopicAdminClient.create(topicAdminSettings)) {
ProjectTopicName projectTopicName = ProjectTopicName.of(projectId, topicName);
topicAdminClient.createTopic(projectTopicName);
}
}
/**
* @param topicId String
* @param subscriptionName String
* @throws IOException This method creates a subscription, that handles pull message delivery.
*/
public void createSubscription(String topicId, String subscriptionName)
throws IOException {
try (SubscriptionAdminClient subscriptionAdminClient = SubscriptionAdminClient
.create(subscriptionAdminSettings)) {
ProjectTopicName topic = ProjectTopicName.of(projectId, topicId);
ProjectSubscriptionName subscription = ProjectSubscriptionName
.of(projectId, subscriptionName);
subscriptionAdminClient
.createSubscription(subscription, topic, PushConfig.getDefaultInstance(), 10);
}
}
public String getResizedImage(String bucketName, String uuid) {
GcsFilename gcsFilename = new GcsFilename(bucketName, getFileName(bucketName, uuid));
ImagesService is = ImagesServiceFactory.getImagesService();
String filename = String.format("/gs/%s/%s", gcsFilename.getBucketName(), gcsFilename.getObjectName());
return is.getServingUrl(ServingUrlOptions.Builder.withGoogleStorageFileName(filename));
}
/**
* @param bucketName String
* @param topicId String
*
* This method sets bucket notification that publish messages on the event of an upload.
*/
private void setBucketNotification(String bucketName, String topicId) {
List<String> eventType = new ArrayList<>();
eventType.add("OBJECT_FINALIZE");
try {
Notification notification = new Notification();
notification.setTopic(topicId);
notification.setEventTypes(eventType);
notification.setPayloadFormat("JSON_API_V1");
final GoogleCredential googleCredential = GoogleCredential
.fromStream(Objects.requireNonNull(classloader.getResourceAsStream("Key.json")))
.createScoped(Collections.singletonList(StorageScopes.DEVSTORAGE_FULL_CONTROL));
final com.google.api.services.storage.Storage myStorage = new com.google.api.services.storage.Storage.Builder(
new NetHttpTransport(), new JacksonFactory(), googleCredential).build();
Notification v = myStorage.notifications().insert(bucketName, notification).execute();
log.info("{}", v);
} catch (IOException e) {
log.error("Caught an IOException {}", e);
}
}
/**
* @param bucketName String
* @return boolean
*
* This method checks whether a bucket with given bucket name exists or not and returns a boolean
* value of either true or false.
*/
public boolean checkIfBucketExists(String bucketName) {
return storage.get(bucketName, Storage.BucketGetOption.fields()) != null;
}
}
Это класс Junit Test.
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import service.GoogleCloudStorage;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class GoogleCloudOperationsTest {
private GoogleCloudStorage service;
private Logger log;
@Before
public void setup() throws IOException {
service = new GoogleCloudStorage();
log = LoggerFactory.getLogger(this.getClass());
}
@Test
public void testIfBucketExist() {
assertTrue(service.checkIfBucketExists("bucketName"));
}
@Test
public void testDownloadLinkGeneration() {
String uuid = "uuid";
String objectName = service.getFileName("bucket", uuid);
String downloadLink = service.getDownloadLink("uuid", objectName);
assertNotNull(downloadLink);
if (!StringUtils.isEmpty(downloadLink)) {
log.info(downloadLink);
}
}
@Test
public void testDownloadLinkGenerationAfterResize() {
String uuid = "uuid";
String objectName = service.getFileName("bucket", uuid);
String downloadLink = service.getResizedImage("bucket", objectName);
assertNotNull(downloadLink);
log.info(downloadLink);
}
}
Это pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>google-cloud-operations</groupId>
<artifactId>google-cloud-operations</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.5</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.5</version>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-pubsub</artifactId>
<version>1.72.0</version>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-storage</artifactId>
<version>1.72.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.appengine.tools</groupId>
<artifactId>appengine-gcs-client</artifactId>
<version>0.8</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
<inherited>true</inherited>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Файл учетных данных Key.json
находится в каталоге resources
.Но, когда я проверяю getResizedImage(String bucketName, String uuid, Integer size)
, он выдает следующую ошибку:
com.google.apphosting.api.ApiProxy$CallNotFoundException: Can't make API call blobstore.CreateEncodedGoogleStorageKey in a thread that is neither the original request thread nor a thread created by ThreadManager
at com.google.apphosting.api.ApiProxy$CallNotFoundException.foreignThread(ApiProxy.java:800)
at com.google.apphosting.api.ApiProxy.makeSyncCall(ApiProxy.java:112)
at com.google.apphosting.api.ApiProxy.makeSyncCall(ApiProxy.java:65)
at com.google.appengine.api.blobstore.BlobstoreServiceImpl.createGsBlobKey(BlobstoreServiceImpl.java:312)
at com.google.appengine.api.images.ImagesServiceImpl.getServingUrl(ImagesServiceImpl.java:266)
at service.GoogleCloudStorage.getResizedImage(GoogleCloudStorage.java:240)
at operationstest.GoogleCloudOperationsTest.testDownloadLinkGenerationAfterResize(GoogleCloudOperationsTest.java:64)
Ошибка происходит на
return is.getServingUrl(ServingUrlOptions.Builder.withGoogleStorageFileName(filename).imageSize(size).crop(true));
Я понятия не имею, что это вызывает.Может действительно помочь.
Редактировать: Моя попытка решения основана на этом ответе .