package io.fathom.cloud.storage.api.os.resources; import io.fathom.cloud.CloudException; import io.fathom.cloud.protobuf.CloudCommons.Attributes; import io.fathom.cloud.protobuf.CloudCommons.KeyValueData; import io.fathom.cloud.protobuf.FileModel.FileData; import io.fathom.cloud.server.model.Project; import io.fathom.cloud.server.model.User; import io.fathom.cloud.storage.FileBlob; import io.fathom.cloud.storage.FileServiceInternal; import io.fathom.cloud.storage.FsBucket; import io.fathom.cloud.storage.FsFile; import io.fathom.cloud.storage.services.MimeTypes; import java.io.File; import java.util.Date; import java.util.Enumeration; import java.util.Map; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.HEAD; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.StreamingOutput; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fathomdb.utils.Hex; import com.google.common.base.Strings; import com.google.common.collect.Maps; import com.google.inject.persist.Transactional; @Path("/openstack/storage/{project}/{bucket}/{name:.+}") @Transactional public class ObjectResource extends ObjectstoreResourceBase { private static final Logger log = LoggerFactory.getLogger(ObjectResource.class); private static final String OBJECT_META_PREFIX = "x-object-meta-"; @PathParam("bucket") String bucketName; @PathParam("name") String name; @Inject FileServiceInternal fs; @PUT public Response createFile(File src) throws Exception { // TODO: Check bucket access before upload? Project project = getProject(); // TODO: This doesn't work for even moderate-sized data String contentType = httpRequest.getHeader(HttpHeaders.CONTENT_TYPE); log.info("Create file {} with contentType {}", name, contentType); if (Strings.isNullOrEmpty(contentType)) { contentType = MimeTypes.INSTANCE.guessMimeType(name); log.info("Content type for file {} guessed as {}", name, contentType); } Map<String, String> userAttributes = Maps.newHashMap(); Enumeration<String> headerNames = httpRequest.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); // Header names are case-insensitive String normalized = headerName.toLowerCase(); if (normalized.startsWith(OBJECT_META_PREFIX)) { String key = headerName.substring(OBJECT_META_PREFIX.length()); String value = httpRequest.getHeader(headerName); userAttributes.put(key, value); } } FileBlob fileData = FileBlob.build(src); fs.putFile(project, bucketName, name, fileData, contentType, userAttributes); ResponseBuilder response = Response.status(Status.CREATED); return response.build(); } @POST public Response appendToFile(File src) throws Exception { // TODO: Special handler for small data? // TODO: Check bucket access before upload? Project project = getProject(); FileBlob fileData = FileBlob.build(src); // No position specified Long position = null; fs.append(project, bucketName, name, position, fileData); ResponseBuilder response = Response.status(Status.CREATED); return response.build(); } @DELETE public Response deleteFile() throws Exception { fs.deleteFile(getProject(), bucketName, name); ResponseBuilder response = Response.status(Status.NO_CONTENT); return response.build(); } public static Response buildReadResponse(HttpServletRequest httpRequest, FileServiceInternal fs, FsFile found) { ResponseBuilder response; if (found == null) { // TODO: Check for meta error page?? throw new WebApplicationException(Status.NOT_FOUND); } boolean head = httpRequest.getMethod().equalsIgnoreCase("head"); boolean includeContentLength = true; if (head) { response = Response.ok(); } else { String range = httpRequest.getHeader("Range"); Long from = null; Long to = null; if (!Strings.isNullOrEmpty(range)) { if (range.startsWith("bytes=")) { range = range.substring(6); int dashIndex = range.indexOf('-'); if (dashIndex == -1) { throw new IllegalArgumentException(); } String fromString = range.substring(0, dashIndex); String toString = range.substring(dashIndex + 1); if (!Strings.isNullOrEmpty(fromString)) { from = Long.valueOf(fromString); } if (!Strings.isNullOrEmpty(toString)) { to = Long.valueOf(toString); } if (from == null && to != null) { // This means the last N bytes from = found.getLength() - to; to = null; } } else { throw new UnsupportedOperationException(); } } StreamingOutput stream = fs.open(found, from, to); if (from != null || to != null) { response = Response.status(206); long rangeStart = 0; if (from != null) { rangeStart = Math.max(from, 0); } long rangeEnd = found.getLength() - 1; if (to != null) { rangeEnd = Math.min(rangeEnd, to - 1); } String contentRange = "bytes " + rangeStart + "-" + rangeEnd + "/" + found.getLength(); response.header(HttpHeaders.CONTENT_LENGTH, 1 + (rangeEnd - rangeStart)); includeContentLength = false; response.header("Content-Range", contentRange); } else { response = Response.ok(); } response.entity(stream); } setHeaders(found, response); // TODO: Support range with head?? // We do always set the length, even on a HEAD // But on a range, it is specially handled! if (includeContentLength && found.getData().hasLength()) { response.header(HttpHeaders.CONTENT_LENGTH, found.getData().getLength()); } response.header("Server", "JustInoughOpenStack"); return response.build(); } private FsFile findFile() throws CloudException { FsFile found; User user = null; FsBucket bucket; if (!isAuthenticated()) { // Check for a public file String projectName = getProjectName(); Project project = new Project(Long.valueOf(projectName)); bucket = fs.findBucket(user, project, bucketName); } else { user = getAuth().getUser(); bucket = fs.findBucket(user, getProject(), bucketName); } if (bucket == null) { return null; } found = fs.findFileInfo(bucket, name); if (found == null && !isAuthenticated()) { // X-Container-Meta-Web-Index String indexPage = bucket.getMetaWebIndex(); if (indexPage != null) { String index; if (!name.endsWith("/")) { index = name + "/" + indexPage; } else { index = name + indexPage; } log.debug("Page not found, so trying index page: {}", index); found = fs.findFileInfo(bucket, index); } } return found; } @HEAD public Response getFileHead() throws CloudException { FsFile found = findFile(); return buildReadResponse(httpRequest, fs, found); } @GET public Response getFile() throws CloudException { FsFile found = findFile(); return buildReadResponse(httpRequest, fs, found); } private static void setHeaders(FsFile found, ResponseBuilder response) { FileData data = found.getData(); if (data.hasLastModified()) { response.lastModified(new Date(data.getLastModified())); } if (data.hasHash()) { response.header(HttpHeaders.ETAG, Hex.toHex(data.getHash().toByteArray())); } if (data.hasContentType()) { response.header(HttpHeaders.CONTENT_TYPE, data.getContentType()); } if (data.hasAttributes()) { Attributes attributes = data.getAttributes(); for (KeyValueData entry : attributes.getUserAttributesList()) { response.header(OBJECT_META_PREFIX + entry.getKey(), entry.getValue()); } } } }