/* * Copyright © 2015 Cask Data, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package co.cask.cdap.service; import co.cask.cdap.api.Transactional; import co.cask.cdap.api.TxRunnable; import co.cask.cdap.api.app.AbstractApplication; import co.cask.cdap.api.common.Bytes; import co.cask.cdap.api.data.DatasetContext; import co.cask.cdap.api.dataset.lib.KeyValueTable; import co.cask.cdap.api.dataset.lib.PartitionKey; import co.cask.cdap.api.dataset.lib.PartitionOutput; import co.cask.cdap.api.dataset.lib.PartitionedFileSet; import co.cask.cdap.api.dataset.lib.PartitionedFileSetProperties; import co.cask.cdap.api.dataset.lib.Partitioning; import co.cask.cdap.api.service.http.AbstractHttpServiceHandler; import co.cask.cdap.api.service.http.HttpContentConsumer; import co.cask.cdap.api.service.http.HttpServiceRequest; import co.cask.cdap.api.service.http.HttpServiceResponder; import com.google.common.base.Throwables; import com.google.common.io.BaseEncoding; import com.google.common.io.Closeables; import org.apache.hadoop.mapreduce.lib.input.TextInputFormat; import org.apache.twill.filesystem.Location; import java.io.IOException; import java.net.HttpURLConnection; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.WritableByteChannel; import java.security.MessageDigest; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; /** * A app for testing file upload through service to partitioned file set. */ public class FileUploadApp extends AbstractApplication { public static final String PFS_NAME = "files"; public static final String KV_TABLE_NAME = "tracking"; public static final String SERVICE_NAME = "pfs"; @Override public void configure() { // A PFS for storing uploaded file createDataset(PFS_NAME, PartitionedFileSet.class, PartitionedFileSetProperties.builder() .setPartitioning(Partitioning.builder().addLongField("time").build()) .setInputFormat(TextInputFormat.class) .build() ); // A KV table for tracking chunks sizes createDataset(KV_TABLE_NAME, KeyValueTable.class); addService(SERVICE_NAME, new FileHandler()); } public static final class FileHandler extends AbstractHttpServiceHandler { /** * Accepts file upload through the usage of {@link HttpContentConsumer}. It will store the file * under the given partition. It also verifies the upload content MD5. */ @POST @Path("/upload/{dataset}/{partition}") public HttpContentConsumer upload(HttpServiceRequest request, HttpServiceResponder responder, @PathParam("dataset") String dataset, @PathParam("partition") long partition) throws Exception { final String md5 = request.getHeader("Content-MD5"); if (md5 == null) { responder.sendError(HttpURLConnection.HTTP_BAD_REQUEST, "Missing header \"Content-MD5\""); return null; } // Construct the partition and the partition location PartitionKey partitionKey = PartitionKey.builder().addLongField("time", partition).build(); PartitionedFileSet pfs = getContext().getDataset(dataset); final PartitionOutput partitionOutput = pfs.getPartitionOutput(partitionKey); final Location partitionDir = partitionOutput.getLocation(); if (!partitionDir.mkdirs()) { responder.sendError(HttpURLConnection.HTTP_CONFLICT, String.format("Partition for key '%s' already exists for dataset '%s'", partitionKey, dataset)); return null; } final MessageDigest messageDigest = MessageDigest.getInstance("MD5"); final Location location = partitionDir.append("upload-" + System.currentTimeMillis()); final WritableByteChannel channel = Channels.newChannel(location.getOutputStream()); // Handle upload content. The onReceived method is non-transactional. return new HttpContentConsumer() { @Override public void onReceived(final ByteBuffer chunk, Transactional transactional) throws Exception { transactional.execute(new TxRunnable() { @Override public void run(DatasetContext context) throws Exception { KeyValueTable trackingTable = context.getDataset(KV_TABLE_NAME); trackingTable.increment(Bytes.toBytes(chunk.remaining()), 1L); } }); chunk.mark(); messageDigest.update(chunk); chunk.reset(); channel.write(chunk); } @Override public void onFinish(HttpServiceResponder responder) throws Exception { channel.close(); String uploadedMd5 = BaseEncoding.base64().encode(messageDigest.digest()); if (!md5.equals(uploadedMd5)) { throw new IllegalArgumentException("MD5 not match. Expected '" + md5 + "', received '" + uploadedMd5 + "'"); } partitionOutput.addPartition(); responder.sendStatus(HttpURLConnection.HTTP_OK); } @Override public void onError(HttpServiceResponder responder, Throwable failureCause) { try { Closeables.closeQuietly(channel); partitionDir.delete(true); } catch (IOException e) { // Nothing much can be done. throw Throwables.propagate(e); } finally { if (Throwables.getRootCause(failureCause) instanceof IllegalArgumentException) { responder.sendStatus(HttpURLConnection.HTTP_BAD_REQUEST); } else { responder.sendStatus(HttpURLConnection.HTTP_INTERNAL_ERROR); } } } }; } } }