// Copyright (C) 2015 anduo // All rights reserved package com.anduo.nz.server; import com.alibaba.fastjson.JSONObject; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.handler.codec.http.*; import io.netty.handler.codec.http.multipart.*; import io.netty.util.CharsetUtil; import net.coobird.thumbnailator.Thumbnails; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Level; import javax.activation.MimetypesFileTypeMap; import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.net.URI; import java.util.List; import java.util.Map; import java.util.UUID; import static io.netty.handler.codec.http.HttpHeaders.Names.CONNECTION; import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; import static io.netty.handler.codec.http.HttpHeaders.isKeepAlive; import static io.netty.handler.codec.http.HttpHeaders.setContentLength; import static io.netty.handler.codec.http.HttpResponseStatus.*; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; /** * Summary: HttpStaticFileServerHandler * Author : anduo@qq.com * Version: 1.0 * Date : 15/7/1 * time : 23:33 */ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> { private static final Logger LOGGER = LogManager.getLogger(HttpStaticFileServerHandler.class); // where to store the files private static final String BASE_PATH = "uploads/"; // query param used to download a file private static final String FILE_QUERY_PARAM = "file"; private HttpPostRequestDecoder decoder; private static final HttpDataFactory factory = new DefaultHttpDataFactory(true); private boolean readingChunks; private static final int THUMB_MAX_WIDTH = 100; private static final int THUMB_MAX_HEIGHT = 100; @Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { // get the URL URI uri = new URI(request.getUri()); String uriStr = uri.getPath(); System.out.println(request.getMethod() + " request received"); if (request.getMethod() == HttpMethod.GET) { serveFile(ctx, request); // user requested a file, serve it } else if (request.getMethod() == HttpMethod.POST) { uploadFile(ctx, request); // user requested to upload file, handle request } else { // unknown request, send error message System.out.println(request.getMethod() + " request received, sending 405"); sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED); } } //当绑定到服务端的时候触发 @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println("hello this is server!"); } private void serveFile(ChannelHandlerContext ctx, FullHttpRequest request) { // decode the query string QueryStringDecoder decoderQuery = new QueryStringDecoder(request.getUri()); Map<String, List<String>> uriAttributes = decoderQuery.parameters(); // get the requested file name String fileName = ""; try { fileName = uriAttributes.get(FILE_QUERY_PARAM).get(0); } catch (Exception e) { sendError(ctx, HttpResponseStatus.BAD_REQUEST, FILE_QUERY_PARAM + " query param not found"); return; } // start serving the requested file sendFile(ctx, fileName, request); } /** * This method reads the requested file from disk and sends it as response. * It also sets proper content-type of the response header * * @param fileName name of the requested file */ private void sendFile(ChannelHandlerContext ctx, String fileName, FullHttpRequest request) { File file = new File(BASE_PATH + fileName); if (file.isDirectory() || file.isHidden() || !file.exists()) { sendError(ctx, NOT_FOUND); return; } if (!file.isFile()) { sendError(ctx, FORBIDDEN); return; } RandomAccessFile raf; try { raf = new RandomAccessFile(file, "r"); } catch (FileNotFoundException fnfe) { sendError(ctx, NOT_FOUND); return; } long fileLength = 0; try { fileLength = raf.length(); } catch (IOException ex) { LOGGER.log(Level.ERROR, "fileLength error", ex); } HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK); setContentLength(response, fileLength); setContentTypeHeader(response, file); //setDateAndCacheHeaders(response, file); if (isKeepAlive(request)) { response.headers().set(CONNECTION, HttpHeaders.Values.KEEP_ALIVE); } // Write the initial line and the header. ctx.write(response); // Write the content. ChannelFuture sendFileFuture; DefaultFileRegion defaultRegion = new DefaultFileRegion(raf.getChannel(), 0, fileLength); sendFileFuture = ctx.write(defaultRegion); // Write the end marker ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); // Decide whether to close the connection or not. if (!isKeepAlive(request)) { // Close the connection when the whole content is written out. lastContentFuture.addListener(ChannelFutureListener.CLOSE); } } /** * This will set the content types of files. If you want to support any * files add the content type and corresponding file extension here. * * @param response * @param file */ private static void setContentTypeHeader(HttpResponse response, File file) { MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap(); mimeTypesMap.addMimeTypes("image png tif jpg jpeg bmp"); mimeTypesMap.addMimeTypes("text/plain txt"); mimeTypesMap.addMimeTypes("application/pdf pdf"); String mimeType = mimeTypesMap.getContentType(file); response.headers().set(CONTENT_TYPE, mimeType); } private void uploadFile(ChannelHandlerContext ctx, FullHttpRequest request) { // test comment try { decoder = new HttpPostRequestDecoder(factory, request); //System.out.println("decoder created"); } catch (HttpPostRequestDecoder.ErrorDataDecoderException e1) { e1.printStackTrace(); sendError(ctx, HttpResponseStatus.BAD_REQUEST, "Failed to decode file data"); return; } readingChunks = HttpHeaders.isTransferEncodingChunked(request); if (decoder != null) { if (request instanceof HttpContent) { // New chunk is received HttpContent chunk = (HttpContent) request; try { decoder.offer(chunk); } catch (HttpPostRequestDecoder.ErrorDataDecoderException e1) { e1.printStackTrace(); sendError(ctx, HttpResponseStatus.BAD_REQUEST, "Failed to decode file data"); return; } readHttpDataChunkByChunk(ctx); // example of reading only if at the end if (chunk instanceof LastHttpContent) { readingChunks = false; reset(); } } else { sendError(ctx, HttpResponseStatus.BAD_REQUEST, "Not a http request"); } } else { sendError(ctx, HttpResponseStatus.BAD_REQUEST, "Failed to decode file data"); } } private void sendOptionsRequestResponse(ChannelHandlerContext ctx) { HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK); ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } private void sendResponse(ChannelHandlerContext ctx, String responseString, String contentType, HttpResponseStatus status) { FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(responseString, CharsetUtil.UTF_8)); response.headers().set(CONTENT_TYPE, contentType); response.headers().add("Access-Control-Allow-Origin", "*"); // Close the connection as soon as the error message is sent. ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } private void sendUploadedFileName(JSONObject fileName, ChannelHandlerContext ctx) { JSONObject jsonObj = new JSONObject(); String msg = "Unexpected error occurred"; String contentType = "application/json; charset=UTF-8"; HttpResponseStatus status = HttpResponseStatus.OK; if (fileName != null) { msg = fileName.toString(); } else { LOGGER.error("uploaded file names are blank"); status = HttpResponseStatus.BAD_REQUEST; contentType = "text/plain; charset=UTF-8"; } sendResponse(ctx, msg, contentType, status); } private void reset() { //request = null; // destroy the decoder to release all resources decoder.destroy(); decoder = null; } /** * Example of reading request by chunk and getting values from chunk to * chunk */ private void readHttpDataChunkByChunk(ChannelHandlerContext ctx) { //decoder.isMultipart(); if (decoder.isMultipart()) { try { while (decoder.hasNext()) { //System.out.println("decoder has next"); InterfaceHttpData data = decoder.next(); if (data != null) { writeHttpData(data, ctx); data.release(); } } } catch (Exception e) { //e.printStackTrace(); } } else { sendError(ctx, HttpResponseStatus.BAD_REQUEST, "Not a multipart request"); } //System.out.println("decoder has no next"); } private void writeHttpData(InterfaceHttpData data, ChannelHandlerContext ctx) { if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.FileUpload) { FileUpload fileUpload = (FileUpload) data; if (fileUpload.isCompleted()) { JSONObject json = saveFileToDisk(fileUpload); sendUploadedFileName(json, ctx); } else { //responseContent.append("\tFile to be continued but should not!\r\n"); sendError(ctx, HttpResponseStatus.BAD_REQUEST, "Unknown error occurred"); } } } /** * generates and returns a unique string that'll be used to save an uploaded * file to disk * * @return generated unique string */ private String getUniqueId() { UUID uniqueId = UUID.randomUUID(); return uniqueId.toString(); } /** * Saves the uploaded file to disk. * * @param fileUpload FileUpload object that'll be saved * @return name of the saved file. null if error occurred */ private JSONObject saveFileToDisk(FileUpload fileUpload) { JSONObject responseJson = new JSONObject(); String filePath = null; // full path of the new file to be saved String upoadedFileName = fileUpload.getFilename(); // get the extension of the uploaded file String extension = ""; int i = upoadedFileName.lastIndexOf('.'); if (i > 0) { // get extension including the "." extension = upoadedFileName.substring(i); } String uniqueBaseName = getUniqueId(); String fileName = uniqueBaseName + extension; try { filePath = BASE_PATH + fileName; fileUpload.renameTo(new File(filePath)); // enable to move into another responseJson.put("file", fileName); if (isImageExtension(extension)) { String thumbname = createThumbnail(filePath, uniqueBaseName, extension); responseJson.put("thumb", thumbname); } } catch (Exception ex) { LOGGER.error(ex); responseJson = null; } return responseJson; } /** * Creates a thumbnail of an image file * * @param fileFullPath full path of the source image * @param fileNameBase Base name of the file i.e without extension * @param extension extension of the file */ private String createThumbnail(String fileFullPath, String fileNameBase, String extension) { String thumbImgName = fileNameBase + "_thumb" + extension; // thumbnail image base name String thumbImageFullPath = BASE_PATH + thumbImgName; // all thumbs are jpg files try { Thumbnails.of(new File(fileFullPath)).size(100, 100).toFile(new File(thumbImageFullPath)); } catch (IOException ex) { LOGGER.error(ex); thumbImgName = ""; } return thumbImgName; } private static boolean isImageExtension(String extension) { boolean isImageFile = false; String extensionInLowerCase = extension.toLowerCase(); isImageFile |= extensionInLowerCase.equals(".jpg"); isImageFile |= extensionInLowerCase.equals(".png"); isImageFile |= extensionInLowerCase.equals(".jpeg"); isImageFile |= extensionInLowerCase.equals(".gif"); return isImageFile; } private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status, String msg) { FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8)); response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); // Close the connection as soon as the error message is sent. ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { sendError(ctx, status, "Failure: " + status.toString() + "\r\n"); } private static BufferedImage resizeImage(BufferedImage originalImage, int type) { BufferedImage resizedImage = new BufferedImage(THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT, type); Graphics2D g = resizedImage.createGraphics(); g.drawImage(originalImage, 0, 0, THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT, null); g.dispose(); return resizedImage; } }