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";
43 public String
createUploadUrl(String successPath
) {
44 return createUploadUrl(successPath
, UploadOptions
.Builder
.withDefaults());
48 public String
createUploadUrl(String successPath
, UploadOptions uploadOptions
) {
49 if (successPath
== null) {
50 throw new NullPointerException("Success path must not be null.");
53 CreateUploadURLRequest request
= new CreateUploadURLRequest();
54 request
.setSuccessPath(successPath
);
56 if (uploadOptions
.hasMaxUploadSizeBytesPerBlob()) {
57 request
.setMaxUploadSizePerBlobBytes(uploadOptions
.getMaxUploadSizeBytesPerBlob());
60 if (uploadOptions
.hasMaxUploadSizeBytes()) {
61 request
.setMaxUploadSizeBytes(uploadOptions
.getMaxUploadSizeBytes());
64 if (uploadOptions
.hasGoogleStorageBucketName()) {
65 request
.setGsBucketName(uploadOptions
.getGoogleStorageBucketName());
70 responseBytes
= ApiProxy
.makeSyncCall(PACKAGE
, "CreateUploadURL", request
.toByteArray());
71 } catch (ApiProxy
.ApplicationException ex
) {
72 switch (BlobstoreServiceError
.ErrorCode
.valueOf(ex
.getApplicationError())) {
74 throw new IllegalArgumentException("The resulting URL was too long.");
76 throw new BlobstoreFailureException("An internal blobstore error occured.");
78 throw new BlobstoreFailureException("An unexpected error occurred.", ex
);
82 CreateUploadURLResponse response
= new CreateUploadURLResponse();
83 response
.mergeFrom(responseBytes
);
84 return response
.getUrl();
88 public void serve(BlobKey blobKey
, HttpServletResponse response
) {
89 serve(blobKey
, (ByteRange
) null, response
);
93 public void serve(BlobKey blobKey
, String rangeHeader
, HttpServletResponse response
) {
94 serve(blobKey
, ByteRange
.parse(rangeHeader
), response
);
98 public void serve(BlobKey blobKey
, ByteRange byteRange
, HttpServletResponse response
) {
99 if (response
.isCommitted()) {
100 throw new IllegalStateException("Response was already committed.");
103 response
.setStatus(HttpServletResponse
.SC_OK
);
104 response
.setHeader(SERVE_HEADER
, blobKey
.getKeyString());
105 if (byteRange
!= null) {
106 response
.setHeader(BLOB_RANGE_HEADER
, byteRange
.toString());
111 public ByteRange
getByteRange(HttpServletRequest request
) {
112 @SuppressWarnings("unchecked")
113 Enumeration
<String
> rangeHeaders
= request
.getHeaders("range");
114 if (!rangeHeaders
.hasMoreElements()) {
118 String rangeHeader
= rangeHeaders
.nextElement();
119 if (rangeHeaders
.hasMoreElements()) {
120 throw new UnsupportedRangeFormatException("Cannot accept multiple range headers.");
123 return ByteRange
.parse(rangeHeader
);
127 public void delete(BlobKey
... blobKeys
) {
128 DeleteBlobRequest request
= new DeleteBlobRequest();
129 for (BlobKey blobKey
: blobKeys
) {
130 request
.addBlobKey(blobKey
.getKeyString());
133 if (request
.blobKeySize() == 0) {
137 byte[] responseBytes
;
139 responseBytes
= ApiProxy
.makeSyncCall(PACKAGE
, "DeleteBlob", request
.toByteArray());
140 } catch (ApiProxy
.ApplicationException ex
) {
141 switch (BlobstoreServiceError
.ErrorCode
.valueOf(ex
.getApplicationError())) {
143 throw new BlobstoreFailureException("An internal blobstore error occured.");
145 throw new BlobstoreFailureException("An unexpected error occurred.", ex
);
151 @Deprecated public Map
<String
, BlobKey
> getUploadedBlobs(HttpServletRequest request
) {
152 Map
<String
, List
<BlobKey
>> blobKeys
= getUploads(request
);
153 Map
<String
, BlobKey
> result
= new HashMap
<String
, BlobKey
>(blobKeys
.size());
155 for (Map
.Entry
<String
, List
<BlobKey
>> entry
: blobKeys
.entrySet()) {
156 if (!entry
.getValue().isEmpty()) {
157 result
.put(entry
.getKey(), entry
.getValue().get(0));
164 public Map
<String
, List
<BlobKey
>> getUploads(HttpServletRequest request
) {
165 @SuppressWarnings("unchecked")
166 Map
<String
, List
<String
>> attributes
=
167 (Map
<String
, List
<String
>>) request
.getAttribute(UPLOADED_BLOBKEY_ATTR
);
168 if (attributes
== null) {
169 throw new IllegalStateException("Must be called from a blob upload callback request.");
171 Map
<String
, List
<BlobKey
>> blobKeys
= new HashMap
<String
, List
<BlobKey
>>(attributes
.size());
172 for (Map
.Entry
<String
, List
<String
>> attr
: attributes
.entrySet()) {
173 List
<BlobKey
> blobs
= new ArrayList
<BlobKey
>(attr
.getValue().size());
174 for (String key
: attr
.getValue()) {
175 blobs
.add(new BlobKey(key
));
177 blobKeys
.put(attr
.getKey(), blobs
);
183 public Map
<String
, List
<BlobInfo
>> getBlobInfos(HttpServletRequest request
) {
184 @SuppressWarnings("unchecked")
185 Map
<String
, List
<Map
<String
, String
>>> attributes
=
186 (Map
<String
, List
<Map
<String
, String
>>>) request
.getAttribute(UPLOADED_BLOBINFO_ATTR
);
187 if (attributes
== null) {
188 throw new IllegalStateException("Must be called from a blob upload callback request.");
190 Map
<String
, List
<BlobInfo
>> blobInfos
= new HashMap
<String
, List
<BlobInfo
>>(attributes
.size());
191 for (Map
.Entry
<String
, List
<Map
<String
, String
>>> attr
: attributes
.entrySet()) {
192 List
<BlobInfo
> blobs
= new ArrayList
<BlobInfo
>(attr
.getValue().size());
193 for (Map
<String
, String
> info
: attr
.getValue()) {
194 BlobKey key
= new BlobKey(info
.get("key"));
195 String contentType
= info
.get("content-type");
196 Date creationDate
= parseCreationDate(info
.get("creation-date"));
197 String filename
= info
.get("filename");
198 int size
= Integer
.parseInt(info
.get("size"));
199 String md5Hash
= info
.get("md5-hash");
200 blobs
.add(new BlobInfo(key
, contentType
, creationDate
, filename
, size
, md5Hash
));
202 blobInfos
.put(attr
.getKey(), blobs
);
208 public Map
<String
, List
<FileInfo
>> getFileInfos(HttpServletRequest request
) {
209 @SuppressWarnings("unchecked")
210 Map
<String
, List
<Map
<String
, String
>>> attributes
=
211 (Map
<String
, List
<Map
<String
, String
>>>) request
.getAttribute(UPLOADED_BLOBINFO_ATTR
);
212 if (attributes
== null) {
213 throw new IllegalStateException("Must be called from a blob upload callback request.");
215 Map
<String
, List
<FileInfo
>> fileInfos
= new HashMap
<String
, List
<FileInfo
>>(attributes
.size());
216 for (Map
.Entry
<String
, List
<Map
<String
, String
>>> attr
: attributes
.entrySet()) {
217 List
<FileInfo
> files
= new ArrayList
<FileInfo
>(attr
.getValue().size());
218 for (Map
<String
, String
> info
: attr
.getValue()) {
219 String contentType
= info
.get("content-type");
220 Date creationDate
= parseCreationDate(info
.get("creation-date"));
221 String filename
= info
.get("filename");
222 long size
= Long
.parseLong(info
.get("size"));
223 String md5Hash
= info
.get("md5-hash");
224 String gsObjectName
= null;
225 if (info
.containsKey("gs-name")) {
226 gsObjectName
= info
.get("gs-name");
228 files
.add(new FileInfo(contentType
, creationDate
, filename
, size
, md5Hash
,
231 fileInfos
.put(attr
.getKey(), files
);
237 protected static Date
parseCreationDate(String date
) {
238 Date creationDate
= null;
240 date
= date
.trim().substring(0, CREATION_DATE_FORMAT
.length());
241 SimpleDateFormat dateFormat
= new SimpleDateFormat(CREATION_DATE_FORMAT
);
242 dateFormat
.setLenient(false);
243 creationDate
= dateFormat
.parse(date
);
244 } catch (IndexOutOfBoundsException e
) {
245 } catch (ParseException e
) {
251 public byte[] fetchData(BlobKey blobKey
, long startIndex
, long endIndex
) {
252 if (startIndex
< 0) {
253 throw new IllegalArgumentException("Start index must be >= 0.");
256 if (endIndex
< startIndex
) {
257 throw new IllegalArgumentException("End index must be >= startIndex.");
260 long fetchSize
= endIndex
- startIndex
+ 1;
261 if (fetchSize
> MAX_BLOB_FETCH_SIZE
) {
262 throw new IllegalArgumentException("Blob fetch size " + fetchSize
+ " is larger " +
263 "than maximum size " + MAX_BLOB_FETCH_SIZE
+ " bytes.");
266 FetchDataRequest request
= new FetchDataRequest();
267 request
.setBlobKey(blobKey
.getKeyString());
268 request
.setStartIndex(startIndex
);
269 request
.setEndIndex(endIndex
);
271 byte[] responseBytes
;
273 responseBytes
= ApiProxy
.makeSyncCall(PACKAGE
, "FetchData", request
.toByteArray());
274 } catch (ApiProxy
.ApplicationException ex
) {
275 switch (BlobstoreServiceError
.ErrorCode
.valueOf(ex
.getApplicationError())) {
276 case PERMISSION_DENIED
:
277 throw new SecurityException("This application does not have access to that blob.");
279 throw new IllegalArgumentException("Blob not found.");
281 throw new BlobstoreFailureException("An internal blobstore error occured.");
283 throw new BlobstoreFailureException("An unexpected error occurred.", ex
);
287 FetchDataResponse response
= new FetchDataResponse();
288 response
.mergeFrom(responseBytes
);
289 return response
.getDataAsBytes();
293 public BlobKey
createGsBlobKey(String filename
) {
295 if (!filename
.startsWith("/gs/")) {
296 throw new IllegalArgumentException("Google storage filenames must be" +
297 " prefixed with /gs/");
299 CreateEncodedGoogleStorageKeyRequest request
= new CreateEncodedGoogleStorageKeyRequest();
300 request
.setFilename(filename
);
302 byte[] responseBytes
;
304 responseBytes
= ApiProxy
.makeSyncCall(PACKAGE
,
305 "CreateEncodedGoogleStorageKey", request
.toByteArray());
306 } catch (ApiProxy
.ApplicationException ex
) {
307 switch (BlobstoreServiceError
.ErrorCode
.valueOf(ex
.getApplicationError())) {
309 throw new BlobstoreFailureException("An internal blobstore error occured.");
311 throw new BlobstoreFailureException("An unexpected error occurred.", ex
);
315 CreateEncodedGoogleStorageKeyResponse response
= new CreateEncodedGoogleStorageKeyResponse();
316 response
.mergeFrom(responseBytes
);
317 return new BlobKey(response
.getBlobKey());