1 // Copyright 2009 Google Inc. All Rights Reserved.
3 package com
.google
.appengine
.api
.blobstore
;
5 import com
.google
.appengine
.api
.blobstore
.BlobstoreServicePb
.BlobstoreServiceError
;
6 import com
.google
.appengine
.api
.blobstore
.BlobstoreServicePb
.CreateEncodedGoogleStorageKeyRequest
;
7 import com
.google
.appengine
.api
.blobstore
.BlobstoreServicePb
.CreateEncodedGoogleStorageKeyResponse
;
8 import com
.google
.appengine
.api
.blobstore
.BlobstoreServicePb
.CreateUploadURLRequest
;
9 import com
.google
.appengine
.api
.blobstore
.BlobstoreServicePb
.CreateUploadURLResponse
;
10 import com
.google
.appengine
.api
.blobstore
.BlobstoreServicePb
.DeleteBlobRequest
;
11 import com
.google
.appengine
.api
.blobstore
.BlobstoreServicePb
.FetchDataRequest
;
12 import com
.google
.appengine
.api
.blobstore
.BlobstoreServicePb
.FetchDataResponse
;
13 import com
.google
.apphosting
.api
.ApiProxy
;
14 import com
.google
.common
.annotations
.VisibleForTesting
;
16 import java
.text
.ParseException
;
17 import java
.text
.SimpleDateFormat
;
18 import java
.util
.ArrayList
;
19 import java
.util
.Date
;
20 import java
.util
.Enumeration
;
21 import java
.util
.HashMap
;
22 import java
.util
.List
;
25 import javax
.servlet
.http
.HttpServletRequest
;
26 import javax
.servlet
.http
.HttpServletResponse
;
29 * {@code BlobstoreServiceImpl} is an implementation of {@link
30 * BlobstoreService} that makes API calls to {@link ApiProxy}.
33 class BlobstoreServiceImpl
implements BlobstoreService
{
34 static final String PACKAGE
= "blobstore";
35 static final String SERVE_HEADER
= "X-AppEngine-BlobKey";
36 static final String UPLOADED_BLOBKEY_ATTR
= "com.google.appengine.api.blobstore.upload.blobkeys";
37 static final String UPLOADED_BLOBINFO_ATTR
=
38 "com.google.appengine.api.blobstore.upload.blobinfos";
39 static final String BLOB_RANGE_HEADER
= "X-AppEngine-BlobRange";
40 static final String CREATION_DATE_FORMAT
= "yyyy-MM-dd HH:mm:ss.SSS";
42 public String
createUploadUrl(String successPath
) {
43 return createUploadUrl(successPath
, UploadOptions
.Builder
.withDefaults());
46 public String
createUploadUrl(String successPath
, UploadOptions uploadOptions
) {
47 if (successPath
== null) {
48 throw new NullPointerException("Success path must not be null.");
51 CreateUploadURLRequest request
= new CreateUploadURLRequest();
52 request
.setSuccessPath(successPath
);
54 if (uploadOptions
.hasMaxUploadSizeBytesPerBlob()) {
55 request
.setMaxUploadSizePerBlobBytes(uploadOptions
.getMaxUploadSizeBytesPerBlob());
58 if (uploadOptions
.hasMaxUploadSizeBytes()) {
59 request
.setMaxUploadSizeBytes(uploadOptions
.getMaxUploadSizeBytes());
62 if (uploadOptions
.hasGoogleStorageBucketName()) {
63 request
.setGsBucketName(uploadOptions
.getGoogleStorageBucketName());
68 responseBytes
= ApiProxy
.makeSyncCall(PACKAGE
, "CreateUploadURL", request
.toByteArray());
69 } catch (ApiProxy
.ApplicationException ex
) {
70 switch (BlobstoreServiceError
.ErrorCode
.valueOf(ex
.getApplicationError())) {
72 throw new IllegalArgumentException("The resulting URL was too long.");
74 throw new BlobstoreFailureException("An internal blobstore error occured.");
76 throw new BlobstoreFailureException("An unexpected error occurred.", ex
);
80 CreateUploadURLResponse response
= new CreateUploadURLResponse();
81 response
.mergeFrom(responseBytes
);
82 return response
.getUrl();
85 public void serve(BlobKey blobKey
, HttpServletResponse response
) {
86 serve(blobKey
, (ByteRange
) null, response
);
89 public void serve(BlobKey blobKey
, String rangeHeader
, HttpServletResponse response
) {
90 serve(blobKey
, ByteRange
.parse(rangeHeader
), response
);
93 public void serve(BlobKey blobKey
, ByteRange byteRange
, HttpServletResponse response
) {
94 if (response
.isCommitted()) {
95 throw new IllegalStateException("Response was already committed.");
98 response
.setStatus(HttpServletResponse
.SC_OK
);
99 response
.setHeader(SERVE_HEADER
, blobKey
.getKeyString());
100 if (byteRange
!= null) {
101 response
.setHeader(BLOB_RANGE_HEADER
, byteRange
.toString());
105 public ByteRange
getByteRange(HttpServletRequest request
) {
106 @SuppressWarnings("unchecked")
107 Enumeration
<String
> rangeHeaders
= request
.getHeaders("range");
108 if (!rangeHeaders
.hasMoreElements()) {
112 String rangeHeader
= rangeHeaders
.nextElement();
113 if (rangeHeaders
.hasMoreElements()) {
114 throw new UnsupportedRangeFormatException("Cannot accept multiple range headers.");
117 return ByteRange
.parse(rangeHeader
);
120 public void delete(BlobKey
... blobKeys
) {
121 DeleteBlobRequest request
= new DeleteBlobRequest();
122 for (BlobKey blobKey
: blobKeys
) {
123 request
.addBlobKey(blobKey
.getKeyString());
126 if (request
.blobKeySize() == 0) {
130 byte[] responseBytes
;
132 responseBytes
= ApiProxy
.makeSyncCall(PACKAGE
, "DeleteBlob", request
.toByteArray());
133 } catch (ApiProxy
.ApplicationException ex
) {
134 switch (BlobstoreServiceError
.ErrorCode
.valueOf(ex
.getApplicationError())) {
136 throw new BlobstoreFailureException("An internal blobstore error occured.");
138 throw new BlobstoreFailureException("An unexpected error occurred.", ex
);
143 @Deprecated public Map
<String
, BlobKey
> getUploadedBlobs(HttpServletRequest request
) {
144 Map
<String
, List
<BlobKey
>> blobKeys
= getUploads(request
);
145 Map
<String
, BlobKey
> result
= new HashMap
<String
, BlobKey
>(blobKeys
.size());
147 for (Map
.Entry
<String
, List
<BlobKey
>> entry
: blobKeys
.entrySet()) {
148 if (!entry
.getValue().isEmpty()) {
149 result
.put(entry
.getKey(), entry
.getValue().get(0));
155 public Map
<String
, List
<BlobKey
>> getUploads(HttpServletRequest request
) {
156 @SuppressWarnings("unchecked")
157 Map
<String
, List
<String
>> attributes
=
158 (Map
<String
, List
<String
>>) request
.getAttribute(UPLOADED_BLOBKEY_ATTR
);
159 if (attributes
== null) {
160 throw new IllegalStateException("Must be called from a blob upload callback request.");
162 Map
<String
, List
<BlobKey
>> blobKeys
= new HashMap
<String
, List
<BlobKey
>>(attributes
.size());
163 for (Map
.Entry
<String
, List
<String
>> attr
: attributes
.entrySet()) {
164 List
<BlobKey
> blobs
= new ArrayList
<BlobKey
>(attr
.getValue().size());
165 for (String key
: attr
.getValue()) {
166 blobs
.add(new BlobKey(key
));
168 blobKeys
.put(attr
.getKey(), blobs
);
173 public Map
<String
, List
<BlobInfo
>> getBlobInfos(HttpServletRequest request
) {
174 @SuppressWarnings("unchecked")
175 Map
<String
, List
<Map
<String
, String
>>> attributes
=
176 (Map
<String
, List
<Map
<String
, String
>>>) request
.getAttribute(UPLOADED_BLOBINFO_ATTR
);
177 if (attributes
== null) {
178 throw new IllegalStateException("Must be called from a blob upload callback request.");
180 Map
<String
, List
<BlobInfo
>> blobInfos
= new HashMap
<String
, List
<BlobInfo
>>(attributes
.size());
181 for (Map
.Entry
<String
, List
<Map
<String
, String
>>> attr
: attributes
.entrySet()) {
182 List
<BlobInfo
> blobs
= new ArrayList
<BlobInfo
>(attr
.getValue().size());
183 for (Map
<String
, String
> info
: attr
.getValue()) {
184 BlobKey key
= new BlobKey(info
.get("key"));
185 String contentType
= info
.get("content-type");
186 Date creationDate
= parseCreationDate(info
.get("creation-date"));
187 String filename
= info
.get("filename");
188 int size
= Integer
.parseInt(info
.get("size"));
189 String md5Hash
= info
.get("md5-hash");
190 blobs
.add(new BlobInfo(key
, contentType
, creationDate
, filename
, size
, md5Hash
));
192 blobInfos
.put(attr
.getKey(), blobs
);
197 public Map
<String
, List
<FileInfo
>> getFileInfos(HttpServletRequest request
) {
198 @SuppressWarnings("unchecked")
199 Map
<String
, List
<Map
<String
, String
>>> attributes
=
200 (Map
<String
, List
<Map
<String
, String
>>>) request
.getAttribute(UPLOADED_BLOBINFO_ATTR
);
201 if (attributes
== null) {
202 throw new IllegalStateException("Must be called from a blob upload callback request.");
204 Map
<String
, List
<FileInfo
>> fileInfos
= new HashMap
<String
, List
<FileInfo
>>(attributes
.size());
205 for (Map
.Entry
<String
, List
<Map
<String
, String
>>> attr
: attributes
.entrySet()) {
206 List
<FileInfo
> files
= new ArrayList
<FileInfo
>(attr
.getValue().size());
207 for (Map
<String
, String
> info
: attr
.getValue()) {
208 String contentType
= info
.get("content-type");
209 Date creationDate
= parseCreationDate(info
.get("creation-date"));
210 String filename
= info
.get("filename");
211 int size
= Integer
.parseInt(info
.get("size"));
212 String md5Hash
= info
.get("md5-hash");
213 String gsObjectName
= null;
214 if (info
.containsKey("gs-name")) {
215 gsObjectName
= info
.get("gs-name");
217 files
.add(new FileInfo(contentType
, creationDate
, filename
, size
, md5Hash
,
220 fileInfos
.put(attr
.getKey(), files
);
226 protected static Date
parseCreationDate(String date
) {
227 Date creationDate
= null;
229 date
= date
.trim().substring(0, CREATION_DATE_FORMAT
.length());
230 SimpleDateFormat dateFormat
= new SimpleDateFormat(CREATION_DATE_FORMAT
);
231 dateFormat
.setLenient(false);
232 creationDate
= dateFormat
.parse(date
);
233 } catch (IndexOutOfBoundsException e
) {
234 } catch (ParseException e
) {
239 public byte[] fetchData(BlobKey blobKey
, long startIndex
, long endIndex
) {
240 if (startIndex
< 0) {
241 throw new IllegalArgumentException("Start index must be >= 0.");
244 if (endIndex
< startIndex
) {
245 throw new IllegalArgumentException("End index must be >= startIndex.");
248 long fetchSize
= endIndex
- startIndex
+ 1;
249 if (fetchSize
> MAX_BLOB_FETCH_SIZE
) {
250 throw new IllegalArgumentException("Blob fetch size " + fetchSize
+ " it larger " +
251 "than maximum size " + MAX_BLOB_FETCH_SIZE
+ " bytes.");
254 FetchDataRequest request
= new FetchDataRequest();
255 request
.setBlobKey(blobKey
.getKeyString());
256 request
.setStartIndex(startIndex
);
257 request
.setEndIndex(endIndex
);
259 byte[] responseBytes
;
261 responseBytes
= ApiProxy
.makeSyncCall(PACKAGE
, "FetchData", request
.toByteArray());
262 } catch (ApiProxy
.ApplicationException ex
) {
263 switch (BlobstoreServiceError
.ErrorCode
.valueOf(ex
.getApplicationError())) {
264 case PERMISSION_DENIED
:
265 throw new SecurityException("This application does not have access to that blob.");
267 throw new IllegalArgumentException("Blob not found.");
269 throw new BlobstoreFailureException("An internal blobstore error occured.");
271 throw new BlobstoreFailureException("An unexpected error occurred.", ex
);
275 FetchDataResponse response
= new FetchDataResponse();
276 response
.mergeFrom(responseBytes
);
277 return response
.getDataAsBytes();
280 public BlobKey
createGsBlobKey(String filename
) {
282 if (!filename
.startsWith("/gs/")) {
283 throw new IllegalArgumentException("Google storage filenames must be" +
284 " prefixed with /gs/");
286 CreateEncodedGoogleStorageKeyRequest request
= new CreateEncodedGoogleStorageKeyRequest();
287 request
.setFilename(filename
);
289 byte[] responseBytes
;
291 responseBytes
= ApiProxy
.makeSyncCall(PACKAGE
,
292 "CreateEncodedGoogleStorageKey", request
.toByteArray());
293 } catch (ApiProxy
.ApplicationException ex
) {
294 switch (BlobstoreServiceError
.ErrorCode
.valueOf(ex
.getApplicationError())) {
296 throw new BlobstoreFailureException("An internal blobstore error occured.");
298 throw new BlobstoreFailureException("An unexpected error occurred.", ex
);
302 CreateEncodedGoogleStorageKeyResponse response
= new CreateEncodedGoogleStorageKeyResponse();
303 response
.mergeFrom(responseBytes
);
304 return new BlobKey(response
.getBlobKey());