ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Implementing HTTP byte-range requests in Spring MVC 파일 이어받기 서버측 구현
    프로그래밍/JAVA 2015.10.15 14:19

    물론 아래 소스를 복사하여 붙여넣은뒤, 자신의 시스템에 맞게 커스텀마이징 해주셔야 합니다...




    /**
    * net/balusc/webapp/FileServlet.java
    *
    * Copyright (C) 2009 BalusC
    *
    * This program 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 3
    * of the License, or (at your option) any later version.
    *
    * This library 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 library.
    * If not, see .
    */

    import org.springframework.web.servlet.view.AbstractView;

    import javax.servlet.ServletOutputStream;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.*;
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.List;
    import java.util.Map;

    /**
    * LGPL code from http://balusc.blogspot.in/2009/02/fileservlet-supporting-resume-and.html adopted
    * as Spring View for streaming binary files.
    *
    * @author Atul M Dambalkar (Adapted for Spring View)
    */
    public class StreamingViewRenderer extends AbstractView {

        // Constants ----------------------------------------------------------------------------------

        private static final int DEFAULT_BUFFER_SIZE = 20480; // ..bytes = 20KB.
        private static final long DEFAULT_EXPIRE_TIME = 604800000L; // ..ms = 1 week.
        private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES";

        @Override
        protected void renderMergedOutputModel(Map objectMap,
                HttpServletRequest request, HttpServletResponse response)
                throws Exception {

            InputStream dataStream =  (InputStream) objectMap.get(DownloadConstants.INPUT_STREAM);
           
            if (dataStream == null) {
                response.sendError(HttpServletResponse.SC_NOT_FOUND);
                return;
            }
            long length = (Long) objectMap.get(DownloadConstants.CONTENT_LENGTH);
            String fileName = (String) objectMap.get(DownloadConstants.FILENAME);
            Date lastModifiedObj = (Date)objectMap.get(DownloadConstants.LAST_MODIFIED);

            if (StringUtils.isEmpty(fileName) || lastModifiedObj == null) {
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                return;
            }
            long lastModified = lastModifiedObj.getTime();
            String contentType = (String) objectMap.get(DownloadConstants.CONTENT_TYPE);

            // Validate request headers for caching ---------------------------------------------------

            // If-None-Match header should contain "*" or ETag. If so, then return 304.
            String ifNoneMatch = request.getHeader("If-None-Match");
            if (ifNoneMatch != null && HttpUtils.matches(ifNoneMatch, fileName)) {
                response.setHeader("ETag", fileName); // Required in 304.
                response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
                return;
            }

            // If-Modified-Since header should be greater than LastModified. If so, then return 304.
            // This header is ignored if any If-None-Match header is specified.
            long ifModifiedSince = request.getDateHeader("If-Modified-Since");
            if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) {
                response.setHeader("ETag", fileName); // Required in 304.
                response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
                return;
            }

            // Validate request headers for resume ----------------------------------------------------

            // If-Match header should contain "*" or ETag. If not, then return 412.
            String ifMatch = request.getHeader("If-Match");
            if (ifMatch != null && !HttpUtils.matches(ifMatch, fileName)) {
                response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
                return;
            }

            // If-Unmodified-Since header should be greater than LastModified. If not, then return 412.
            long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since");
            if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified) {
                response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
                return;
            }
           
            // Validate and process range -------------------------------------------------------------

            // Prepare some variables. The full Range represents the complete file.
            Range full = new Range(0, length - 1, length);
            List ranges = new ArrayList();

            // Validate and process Range and If-Range headers.
            String range = request.getHeader("Range");
            if (range != null) {

                // Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416.
                if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
                    response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
                    response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                    return;
                }

                String ifRange = request.getHeader("If-Range");
                if (ifRange != null && !ifRange.equals(fileName)) {
                    try {
                        long ifRangeTime = request.getDateHeader("If-Range"); // Throws IAE if invalid.
                        if (ifRangeTime != -1) {
                            ranges.add(full);
                        }
                    } catch (IllegalArgumentException ignore) {
                        ranges.add(full);
                    }
                }

                // If any valid If-Range header, then process each part of byte range.
                if (ranges.isEmpty()) {
                    for (String part : range.substring(6).split(",")) {
                        // Assuming a file with length of 100, the following examples returns bytes at:
                        // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
                        long start = StringUtils.sublong(part, 0, part.indexOf("-"));
                        long end = StringUtils.sublong(part, part.indexOf("-") + 1, part.length());

                        if (start == -1) {
                            start = length - end;
                            end = length - 1;
                        } else if (end == -1 || end > length - 1) {
                            end = length - 1;
                        }

                        // Check if Range is syntactically valid. If not, then return 416.
                        if (start > end) {
                            response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
                            response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                            return;
                        }

                        // Add range.                   
                        ranges.add(new Range(start, end, length));
                    }
                }
            }

            // Prepare and initialize response --------------------------------------------------------

            // Get content type by file name and set content disposition.
            String disposition = "inline";

            // If content type is unknown, then set the default value.
            // For all content types, see: http://www.w3schools.com/media/media_mimeref.asp
            // To add new content types, add new mime-mapping entry in web.xml.
            if (contentType == null) {
                contentType = "application/octet-stream";
            } else if (!contentType.startsWith("image")) {
                // Else, expect for images, determine content disposition. If content type is supported by
                // the browser, then set to inline, else attachment which will pop a 'save as' dialogue.
                String accept = request.getHeader("Accept");
                disposition = accept != null && HttpUtils.accepts(accept, contentType)
                                        ? "inline" : "attachment";
            }

            // Initialize response.
            response.reset();
            response.setBufferSize(DEFAULT_BUFFER_SIZE);
            response.setHeader("Content-Disposition", disposition + ";filename=\"" + fileName + "\"");
            response.setHeader("Accept-Ranges", "bytes");
            response.setHeader("ETag", fileName);
            response.setDateHeader("Last-Modified", lastModified);
            response.setDateHeader("Expires", System.currentTimeMillis() + DEFAULT_EXPIRE_TIME);

            // Send requested file (part(s)) to client ------------------------------------------------

            // Prepare streams.
            InputStream input = null;
            OutputStream output = null;

            try {
                // Open streams.
                input = new BufferedInputStream(dataStream);
                output = response.getOutputStream();

                if (ranges.isEmpty() || ranges.get(0) == full) {

                    // Return full file.
                    Range r = full;
                    response.setContentType(contentType);
                    response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
                    response.setHeader("Content-Length", String.valueOf(r.length));
                    copy(input, output, length, r.start, r.length);
                   
                } else if (ranges.size() == 1) {

                    // Return single part of file.
                    Range r = ranges.get(0);
                    response.setContentType(contentType);
                    response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
                    response.setHeader("Content-Length", String.valueOf(r.length));
                    response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

                    // Copy single part range.
                    copy(input, output, length, r.start, r.length);

                } else {

                    // Return multiple parts of file.
                    response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY);
                    response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

                    // Cast back to ServletOutputStream to get the easy println methods.
                    ServletOutputStream sos = (ServletOutputStream) output;

                    // Copy multi part range.
                    for (Range r : ranges) {
                        // Add multipart boundary and header fields for every range.
                        sos.println();
                        sos.println("--" + MULTIPART_BOUNDARY);
                        sos.println("Content-Type: " + contentType);
                        sos.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total);

                        // Copy single part range of multi part range.
                        copy(input, output, length, r.start, r.length);
                    }

                    // End with multipart boundary.
                    sos.println();
                    sos.println("--" + MULTIPART_BOUNDARY + "--");
                }
            } finally {
                // Gently close streams.
                close(output);
                close(input);
                close(dataStream);
            }

        }

        /**
         * Close the given resource.
         * @param resource The resource to be closed.
         */
        private static void close(Closeable resource) {
            if (resource != null) {
                try {
                    resource.close();
                } catch (IOException ignore) {
                    // Ignore IOException. If you want to handle this anyway, it might be useful to know
                    // that this will generally only be thrown when the client aborted the request.
                }
            }
        }
       
        /**
         * Copy the given byte range of the given input to the given output.
         * @param input The input to copy the given range to the given output for.
         * @param output The output to copy the given range from the given input for.
         * @param start Start of the byte range.
         * @param length Length of the byte range.
         * @throws IOException If something fails at I/O level.
         */
        private static void copy(InputStream input, OutputStream output,
                                 long inputSize, long start, long length)
            throws IOException
        {
            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
            int read;

            if (inputSize == length) {
                // Write full range.
                while ((read = input.read(buffer)) > 0) {
                    output.write(buffer, 0, read);
                    output.flush();
                }
            } else {
                input.skip(start);
                long toRead = length;

                while ((read = input.read(buffer)) > 0) {
                    if ((toRead -= read) > 0) {
                        output.write(buffer, 0, read);
                        output.flush();
                    } else {
                        output.write(buffer, 0, (int) toRead + read);
                        output.flush();
                        break;
                    }
                }
            }
        }

        // Inner classes ------------------------------------------------------------------------------

        /**
         * This class represents a byte range.
         */
        protected class Range {
            long start;
            long end;
            long length;
            long total;

            /**
             * Construct a byte range.
             * @param start Start of the byte range.
             * @param end End of the byte range.
             * @param total Total length of the byte source.
             */
            public Range(long start, long end, long total) {
                this.start = start;
                this.end = end;
                this.length = end - start + 1;
                this.total = total;
            }
        }
    }

    public class DownloadConstants {

    public static final String INPUT_STREAM = "inputStream";
    public static final String CONTENT_TYPE = "contentType";

    public static final String CONTENT_LENGTH = "contentLength";
    public static final String FILENAME = "filename";
        public static final String LAST_MODIFIED = "lastModified";
    }

    public class HttpUtils {
       
        /**
         * Returns true if the given accept header accepts the given value.
         * @param acceptHeader The accept header.
         * @param toAccept The value to be accepted.
         * @return True if the given accept header accepts the given value.
         */
        public static boolean accepts(String acceptHeader, String toAccept) {
            String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*");
            Arrays.sort(acceptValues);
            return Arrays.binarySearch(acceptValues, toAccept) > -1
                || Arrays.binarySearch(acceptValues, toAccept.replaceAll("/.*$", "/*")) > -1
                || Arrays.binarySearch(acceptValues, "*/*") > -1;
        }

        /**
         * Returns true if the given match header matches the given value.
         * @param matchHeader The match header.
         * @param toMatch The value to be matched.
         * @return True if the given match header matches the given value.
         */
        public static boolean matches(String matchHeader, String toMatch) {
            String[] matchValues = matchHeader.split("\\s*,\\s*");
            Arrays.sort(matchValues);
            return Arrays.binarySearch(matchValues, toMatch) > -1
                || Arrays.binarySearch(matchValues, "*") > -1;
        }
    }

    public class StringUtils {

        /**
         * Returns a substring of the given string value from the given begin index to the given end
         * index as a long. If the substring is empty, then -1 will be returned
         * @param value The string value to return a substring as long for.
         * @param beginIndex The begin index of the substring to be returned as long.
         * @param endIndex The end index of the substring to be returned as long.
         * @return A substring of the given string value as long or -1 if substring is empty.
         */
        public static long sublong(String value, int beginIndex, int endIndex) {
            String substring = value.substring(beginIndex, endIndex);
            return (substring.length() > 0) ? Long.parseLong(substring) : -1;
        }   
       
    }

    댓글 0

Designed by Tistory.