/* * JBoss, Home of Professional Open Source. * Copyright 2011, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.capedwarf.blobstore; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Enumeration; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.logging.Logger; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Part; import com.google.appengine.api.blobstore.BlobInfo; import com.google.appengine.api.blobstore.BlobKey; import com.google.appengine.api.blobstore.BlobstoreFailureException; import com.google.appengine.api.blobstore.ByteRange; import com.google.appengine.api.blobstore.FileInfo; import com.google.appengine.api.blobstore.RangeFormatException; import com.google.appengine.api.blobstore.UnsupportedRangeFormatException; import com.google.appengine.api.blobstore.UploadOptions; import com.google.appengine.api.files.AppEngineFile; import com.google.common.base.Function; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.jboss.capedwarf.common.io.IOUtils; import org.jboss.capedwarf.common.servlet.ServletUtils; /** * @author <a href="mailto:ales.justin@jboss.org">Ales Justin</a> * @author <a href="mailto:marko.luksa@gmail.com">Marko Luksa</a> */ public class CapedwarfBlobstoreService implements ExposedBlobstoreService { private static final Logger log = Logger.getLogger(CapedwarfBlobstoreService.class.getName()); private static final String UPLOADED_BLOBKEY_ATTR = "com.google.appengine.api.blobstore.upload.blobkeys"; private static final String UPLOADED_BLOBKEY_LIST_ATTR = "com.google.appengine.api.blobstore.upload.blobkeylists"; private Function<List<BlobKey>, List<BlobInfo>> BLOB_LIST_KEY_TO_INFO_FN = new Function<List<BlobKey>, List<BlobInfo>>() { public List<BlobInfo> apply(List<BlobKey> input) { return Lists.transform(input, BLOB_KEY_TO_INFO_FN); } }; private Function<BlobKey, BlobInfo> BLOB_KEY_TO_INFO_FN = new Function<BlobKey, BlobInfo>() { public BlobInfo apply(BlobKey input) { return getBlobInfo(input); } }; private Function<List<BlobKey>, List<FileInfo>> FILE_LIST_KEY_TO_INFO_FN = new Function<List<BlobKey>, List<FileInfo>>() { public List<FileInfo> apply(List<BlobKey> input) { return Lists.transform(input, FILE_KEY_TO_INFO_FN); } }; private Function<BlobKey, FileInfo> FILE_KEY_TO_INFO_FN = new Function<BlobKey, FileInfo>() { public FileInfo apply(BlobKey input) { return getStorage().getFileInfo(input); } }; private Storage storage; private synchronized Storage getStorage() { if (storage == null) { storage = StorageFactory.getStorage(); } return storage; } public String createUploadUrl(String successPath) { return createUploadUrl(successPath, UploadOptions.Builder.withDefaults()); } public String createUploadUrl(String successPath, UploadOptions uploadOptions) { return UploadServlet.createUploadUrl(successPath, uploadOptions); } public void delete(BlobKey... blobKeys) { getStorage().delete(blobKeys); } public void serve(BlobKey blobKey, HttpServletResponse response) throws IOException { serve(blobKey, (ByteRange) null, response); } public void serve(BlobKey blobKey, String rangeHeader, HttpServletResponse response) throws IOException { serve(blobKey, ByteRange.parse(rangeHeader), response); } public void serve(BlobKey blobKey, ByteRange byteRange, HttpServletResponse response) throws IOException { assertNotCommited(response); response.setStatus(HttpServletResponse.SC_OK); response.setHeader(BLOB_KEY_HEADER, blobKey.getKeyString()); if (byteRange != null) { response.setHeader(BLOB_RANGE_HEADER, byteRange.toString()); } } public void serveBlob(BlobKey blobKey, String byteRangeStr, HttpServletResponse response) throws IOException { assertNotCommited(response); BlobInfo blobInfo = getBlobInfo(blobKey); if (blobInfo == null) { throw new IOException(String.format("No such blob for key: %s", blobKey)); } response.setContentType(blobInfo.getContentType()); ByteRange byteRange = null; if (byteRangeStr == null) { response.setStatus(HttpServletResponse.SC_OK); } else { try { byteRange = ByteRange.parse(byteRangeStr); } catch (RangeFormatException e) { response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return; } if (byteRange.getStart() >= blobInfo.getSize()) { response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return; } if ((byteRange.hasEnd() == false) || (byteRange.getEnd() >= blobInfo.getSize())) { byteRange = new ByteRange(byteRange.getStart(), blobInfo.getSize() - 1); } response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); response.setHeader("Content-Range", "bytes " + byteRange.getStart() + "-" + byteRange.getEnd() + "/" + blobInfo.getSize()); } try { try (InputStream in = getStream(blobKey)) { copyStream(in, response.getOutputStream(), byteRange); } } catch (FileNotFoundException e) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); } } private BlobInfo getBlobInfo(BlobKey blobKey) { return getStorage().getBlobInfo(blobKey); } private void assertNotCommited(HttpServletResponse response) { if (response.isCommitted()) { throw new IllegalStateException("Response was already committed."); } } private void copyStream(InputStream in, OutputStream out, ByteRange range) throws IOException { if (range == null) { IOUtils.copyStream(in, out); } else { if (range.hasEnd()) { long length = range.getEnd() + 1 - range.getStart(); // end is inclusive, hence +1 IOUtils.copyStream(in, out, range.getStart(), length); } else { IOUtils.copyStream(in, out, range.getStart()); } } } public byte[] fetchData(BlobKey blobKey, long startIndex, long endIndex) { if (startIndex < 0) { throw new IllegalArgumentException("startIndex must be >= 0"); } if (endIndex < startIndex) { throw new IllegalArgumentException("endIndex must be >= startIndex"); } long fetchSize = endIndex - startIndex + 1; if (fetchSize > MAX_BLOB_FETCH_SIZE) { throw new IllegalArgumentException("Blob fetch size " + fetchSize + " is larger than MAX_BLOB_FETCH_SIZE (" + MAX_BLOB_FETCH_SIZE + ")"); } try { InputStream stream = getStream(blobKey); return IOUtils.toBytes(stream, startIndex, endIndex, true); } catch (FileNotFoundException e) { throw new IllegalArgumentException("Blob does not exist"); } catch (IOException e) { throw new BlobstoreFailureException("An unexpected error occured", e); } } @SuppressWarnings("unchecked") public ByteRange getByteRange(HttpServletRequest request) { Enumeration<String> rangeHeaders = request.getHeaders("range"); if (!rangeHeaders.hasMoreElements()) { return null; } String rangeHeader = rangeHeaders.nextElement(); if (rangeHeaders.hasMoreElements()) { throw new UnsupportedRangeFormatException("Cannot accept multiple range headers."); } return ByteRange.parse(rangeHeader); } public boolean storeUploadedBlobs(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { Map<String, BlobKey> map = new HashMap<String, BlobKey>(); Map<String, List<BlobKey>> map2 = new HashMap<String, List<BlobKey>>(); String perBlob = request.getParameter(UploadServlet.MAX_PER_BLOB); String all = request.getParameter(UploadServlet.MAX_ALL); long maxPerBlob = (perBlob != null) ? Long.parseLong(perBlob) : -1; long maxAll = (all != null) ? Long.parseLong(all) : -1; long total = 0; for (Part part : request.getParts()) { if (ServletUtils.isFile(part)) { long size = part.getSize(); if (size >= 0) { if (maxPerBlob >= 0 && size > maxPerBlob) { response.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, "Your client issued a request that was too large. Maximum upload size per blob limit exceeded."); return false; } total += size; if (maxAll >= 0 && total > maxAll) { response.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, "Your client issued a request that was too large. Maximum total upload size limit exceeded."); return false; } } else { log.warning("Unable to determine size of upload part named " + ServletUtils.getFileName(part) + ". Upload limit checks may not be accurate."); } BlobKey blobKey = storeUploadedBlob(part, request); String name = part.getName(); map.put(name, blobKey); List<BlobKey> list = map2.get(name); if (list == null) { list = new LinkedList<BlobKey>(); map2.put(name, list); } list.add(blobKey); } } request.setAttribute(UPLOADED_BLOBKEY_ATTR, map); request.setAttribute(UPLOADED_BLOBKEY_LIST_ATTR, map2); return true; } private BlobKey storeUploadedBlob(final Part part, HttpServletRequest request) throws IOException { String contentType = part.getContentType(); String fileName = ServletUtils.getFileName(part); String bucketName = request.getParameter(UploadServlet.BUCKET_NAME); return getStorage().store(new Storage.StreamProvider() { public InputStream get() throws IOException { return part.getInputStream(); } }, fileName, contentType, bucketName); } @SuppressWarnings("unchecked") public Map<String, BlobKey> getUploadedBlobs(HttpServletRequest request) { Map<String, BlobKey> map = (Map<String, BlobKey>) request.getAttribute(UPLOADED_BLOBKEY_ATTR); if (map == null) { throw new IllegalStateException("Must be called from a blob upload callback request."); } return map; } @SuppressWarnings("unchecked") public Map<String, List<BlobKey>> getUploads(HttpServletRequest request) { Map<String, List<BlobKey>> map = (Map<String, List<BlobKey>>) request.getAttribute(UPLOADED_BLOBKEY_LIST_ATTR); if (map == null) { throw new IllegalStateException("Must be called from a blob upload callback request."); } return map; } protected InputStream getStream(BlobKey blobKey) throws IOException { return getStorage().getStream(blobKey); } public BlobKey createGsBlobKey(String filename) { if (filename.startsWith("/gs/") == false) { throw new IllegalArgumentException("Google storage filenames must be prefixed with /gs/"); } //noinspection deprecation return getStorage().getBlobKey(new AppEngineFile(filename)); } public Map<String, List<BlobInfo>> getBlobInfos(HttpServletRequest httpServletRequest) { return Maps.transformValues(getUploads(httpServletRequest), BLOB_LIST_KEY_TO_INFO_FN); } public Map<String, List<FileInfo>> getFileInfos(HttpServletRequest httpServletRequest) { return Maps.transformValues(getUploads(httpServletRequest), FILE_LIST_KEY_TO_INFO_FN); } }