package io.blobkeeper.server.handler;
/*
* Copyright (C) 2015 by Denis M. Gabaydulin
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
import io.blobkeeper.cluster.service.ClusterMembershipService;
import io.blobkeeper.common.domain.Result;
import io.blobkeeper.common.domain.api.ApiRequest;
import io.blobkeeper.common.domain.api.ReturnValue;
import io.blobkeeper.common.service.IdGeneratorService;
import io.blobkeeper.file.configuration.FileConfiguration;
import io.blobkeeper.file.domain.StorageFile;
import io.blobkeeper.index.domain.IndexTempElt;
import io.blobkeeper.index.service.IndexService;
import io.blobkeeper.server.handler.api.RequestHandler;
import io.blobkeeper.server.handler.api.RequestMapper;
import io.blobkeeper.file.service.WriterTaskQueue;
import io.blobkeeper.server.util.HttpUtils;
import io.blobkeeper.server.util.MetadataParser;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.multipart.*;
import org.slf4j.Logger;
import javax.inject.Inject;
import java.io.IOException;
import static io.blobkeeper.common.domain.Error.createError;
import static io.blobkeeper.common.domain.ErrorCode.*;
import static io.blobkeeper.index.domain.IndexElt.DEFAULT_TYPE;
import static io.blobkeeper.server.util.HttpUtils.*;
import static io.netty.handler.codec.http.HttpMethod.*;
import static io.netty.handler.codec.http.HttpResponseStatus.*;
import static io.netty.handler.codec.http.LastHttpContent.EMPTY_LAST_CONTENT;
import static io.netty.handler.codec.http.multipart.InterfaceHttpData.HttpDataType.FileUpload;
import static java.lang.String.format;
import static org.joda.time.DateTime.now;
import static org.joda.time.DateTimeZone.UTC;
import static org.slf4j.LoggerFactory.getLogger;
public class FileWriterHandler extends BaseFileHandler<HttpObject> {
private static final Logger log = getLogger(FileWriterHandler.class);
private static final int DEFAULT_SHARD_ID = 1;
@Inject
private IdGeneratorService isGeneratorService;
@Inject
private IndexService indexService;
@Inject
private WriterTaskQueue writerTaskQueue;
@Inject
private ClusterMembershipService clusterMembershipService;
@Inject
private RequestMapper requestMapper;
@Inject
private FileConfiguration fileConfiguration;
private HttpRequest request;
private HttpCustomPostRequestDecoder decoder;
private boolean requestIsSent = false;
private boolean errorRequest = false;
// clean up garbage
static {
DiskFileUpload.deleteOnExitTemporaryFile = false;
DiskAttribute.deleteOnExitTemporaryFile = true;
}
// set up base upload directory
public void init() {
DiskFileUpload.baseDirectory = fileConfiguration.getUploadPath();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.warn("Unknown exception", cause);
ctx.channel().close();
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
if (decoder != null) {
reset();
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
if (decoder != null) {
errorRequest = true;
reset();
}
}
@Override
protected void channelRead0(ChannelHandlerContext context, HttpObject object) throws Exception {
setContext();
log.debug("{}", object);
if (object instanceof HttpRequest) {
HttpRequest request = (HttpRequest) object;
if (request.getMethod() == GET) {
jumpToReader(context, object);
return;
}
if (request.getMethod() == DELETE) {
jumpToDeleter(context, object);
return;
}
if (request.getMethod() == PUT) {
// FIXME: restore will be here
return;
}
}
if (object.equals(EMPTY_LAST_CONTENT)) {
String errorMessage = "There is no upload file in the request";
log.error(errorMessage);
sendError(context, BAD_REQUEST, createError(INVALID_REQUEST, errorMessage));
return;
}
createPostDecoderIfNotExists(context, object);
if (null == decoder) {
// FIXME: send error
return;
}
if (object instanceof HttpContent) {
// new chunk is received
HttpContent chunk = (HttpContent) object;
try {
decoder.offer(chunk);
} catch (HttpPostRequestDecoder.ErrorDataDecoderException e1) {
log.error("Can't decode chunk", e1);
sendError(context, BAD_REQUEST, createError(INVALID_REQUEST, "Can't decode chunk"));
return;
}
readHttpDataChunkByChunk(context);
if (chunk instanceof LastHttpContent) {
if (!requestIsSent) {
String errorMessage = "There is no upload file in the request";
log.error(errorMessage);
sendError(context, BAD_REQUEST, createError(INVALID_REQUEST, errorMessage));
}
reset();
}
}
}
@Override
protected void sendError(ChannelHandlerContext ctx, HttpResponseStatus status, io.blobkeeper.common.domain.Error error) {
this.errorRequest = true;
super.sendError(ctx, status, error);
}
private void handleApiRequest(ChannelHandlerContext ctx, String value) {
try {
RequestHandler<?, ? extends ApiRequest> requestHandler = requestMapper.getByUri(this.request.getUri());
ReturnValue<?> returnValue = requestHandler.handleRequest(value);
writeResponse(ctx, returnValue, request);
} catch (Exception e) {
log.error("Can't handle request", e);
}
}
private void jumpToDeleter(ChannelHandlerContext context, HttpObject object) {
context.pipeline().addBefore("deleter", "aggregator", new HttpObjectAggregator(65536));
context.pipeline().remove(FileWriterHandler.class);
context.fireChannelRead(object);
}
private void jumpToReader(ChannelHandlerContext context, HttpObject object) {
context.pipeline().addBefore("reader", "aggregator", new HttpObjectAggregator(65536));
context.pipeline().remove(this);
context.fireChannelRead(object);
}
private void createPostDecoderIfNotExists(ChannelHandlerContext ctx, HttpObject request) {
if (decoder == null) {
try {
this.request = (HttpRequest) request;
// always save data on disk
CustomHttpDataFactory dataFactory = new CustomHttpDataFactory();
decoder = new HttpCustomPostRequestDecoder(dataFactory, this.request);
} catch (Exception e) {
log.error("Can't decode message", e);
sendError(ctx, BAD_REQUEST, createError(INVALID_REQUEST, "Can't decode request"));
}
}
}
private void reset() {
log.debug("Release all resources from decoder started");
if (errorRequest) {
cleanFiles();
}
request = null;
decoder.destroy();
decoder = null;
log.debug("Release all resources from decoder completed");
}
private void cleanFiles() {
decoder.cleanFilesOnError();
}
private void readHttpDataChunkByChunk(ChannelHandlerContext ctx) {
try {
while (decoder.hasNext()) {
InterfaceHttpData data = decoder.next();
if (data != null) {
// new value
writeHttpData(ctx, data);
}
}
} catch (HttpPostRequestDecoder.EndOfDataDecoderException e1) {
// FIXME: send error?
log.debug("End of a request!");
}
}
private void writeHttpData(ChannelHandlerContext ctx, InterfaceHttpData data) {
if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.Attribute) {
Attribute attribute = (Attribute) data;
String value;
try {
value = attribute.getValue();
log.info("Api call: {}", value);
handleApiRequest(ctx, value);
requestIsSent = true;
return;
} catch (IOException e) {
log.error(format("Can't read data, attribute name is %s", attribute.getHttpDataType().name()), e);
sendError(ctx, BAD_REQUEST, createError(INVALID_REQUEST, "Can't read data"));
return;
}
} else {
if (data.getHttpDataType() == FileUpload) {
requestIsSent = true;
if (!clusterMembershipService.isMaster()) {
log.error("Node is not a master");
sendError(ctx, METHOD_NOT_ALLOWED, createError(NOT_A_MASTER, "Node is not a master"));
return;
}
String uri = request.getUri();
long id = getId(uri);
int type = getType(uri);
FileUpload fileUpload = (FileUpload) data;
if (fileUpload.isCompleted()) {
if (id == HttpUtils.NOT_FOUND || type == HttpUtils.NOT_FOUND) {
id = isGeneratorService.generate(DEFAULT_SHARD_ID);
type = DEFAULT_TYPE;
log.info("New id : type is {} : {}", id, type);
} else {
log.info("Given file : type is {} : {}", id, type);
if (null != indexService.getById(id, type)) {
log.error("File {} : {} is already present");
sendError(ctx, CONFLICT, createError(ALREADY_EXISTS, "Object already exists"));
return;
}
}
log.trace("Started copying a file to the write queue");
StorageFile storageFile = buildStorageFile(fileUpload)
.id(id)
.type(type)
.headers(MetadataParser.getHeaders(request))
.build();
addTempIndex(storageFile);
// add file to the upload queue
if (!writerTaskQueue.offer(storageFile)) {
String errorMessage = "Upload failed";
log.error(errorMessage);
sendError(ctx, BAD_GATEWAY, createError(SERVICE_ERROR, errorMessage));
} else {
log.info("File {} added to the upload queue", id);
writeResponse(ctx, new ReturnValue<>(new Result(id)), request);
return;
}
} else {
String errorMessage = "Upload file to be continued but should not!";
log.error(errorMessage);
sendError(ctx, BAD_REQUEST, createError(INVALID_REQUEST, errorMessage));
}
}
}
if (!requestIsSent) {
String errorMessage = "There is no upload file in the request";
log.error(errorMessage);
sendError(ctx, BAD_REQUEST, createError(INVALID_REQUEST, errorMessage));
}
}
/**
* Save a temp index for recovering
*/
private void addTempIndex(StorageFile storageFile) {
IndexTempElt indexElt = new IndexTempElt.IndexTempEltBuilder()
.id(storageFile.getId())
.type(storageFile.getType())
.created(now(UTC).getMillis())
.metadata(storageFile.getMetadata())
.file(storageFile.getFile().getAbsolutePath())
.build();
indexService.add(indexElt);
}
}