Spring-Boot實現(xiàn)HTTP大文件斷點續(xù)傳分片下載-大視頻分段漸進式播放

服務(wù)端如何將一個大視頻文件做切分忘苛,分段響應(yīng)給客戶端碱蒙,讓瀏覽器可以漸進式地播放莹菱。

Spring Boot實現(xiàn)HTTP分片下載斷點續(xù)傳植捎,從而實現(xiàn)H5頁面的大視頻播放問題场晶,實現(xiàn)漸進式播放咆爽,每次只播放需要播放的內(nèi)容就可以了脆丁,不需要加載整個文件到內(nèi)存中音榜。

文件的斷點續(xù)傳蔚鸥、文件多線程并發(fā)下載(迅雷就是這么玩的)等惜论。

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-bom</artifactId>
        <version>5.8.18</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>cn.hutool</groupId>
      <artifactId>hutool-core</artifactId>
    </dependency>

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
  </dependencies>

代碼實現(xiàn)

ResourceController

package com.example.insurance.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

import com.example.insurance.common.ContentRange;
import com.example.insurance.common.MediaContentUtil;
import com.example.insurance.common.NioUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRange;
import org.springframework.http.HttpStatus;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StopWatch;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 內(nèi)容資源控制器
 */
@SuppressWarnings("unused")
@Slf4j
@RestController("resourceController")
@RequestMapping(path = "/resource")
public class ResourceController {

    /**
     * 獲取文件內(nèi)容
     *
     * @param fileName 內(nèi)容文件名稱
     * @param response 響應(yīng)對象
     */
    @GetMapping("/media/{fileName}")
    public void getMedia(@PathVariable String fileName, HttpServletRequest request, HttpServletResponse response,
                         @RequestHeader HttpHeaders headers) {
//        printRequestInfo(fileName, request, headers);

        String filePath = MediaContentUtil.filePath();
        try {
            this.download(fileName, filePath, request, response, headers);
        } catch (Exception e) {
            log.error("getMedia error, fileName={}", fileName, e);
        }
    }

    /**
     * 獲取封面內(nèi)容
     *
     * @param fileName 內(nèi)容封面名稱
     * @param response 響應(yīng)對象
     */
    @GetMapping("/cover/{fileName}")
    public void getCover(@PathVariable String fileName, HttpServletRequest request, HttpServletResponse response,
                         @RequestHeader HttpHeaders headers) {
//        printRequestInfo(fileName, request, headers);

        String filePath = MediaContentUtil.filePath();
        try {
            this.download(fileName, filePath, request, response, headers);
        } catch (Exception e) {
            log.error("getCover error, fileName={}", fileName, e);
        }
    }


    // ======= internal =======

    private static void printRequestInfo(String fileName, HttpServletRequest request, HttpHeaders headers) {
        String requestUri = request.getRequestURI();
        String queryString = request.getQueryString();
        log.debug("file={}, url={}?{}", fileName, requestUri, queryString);
        log.info("headers={}", headers);
    }

    /**
     * 設(shè)置請求響應(yīng)狀態(tài)、頭信息止喷、內(nèi)容類型與長度 等馆类。
     * <pre>
     * <a >
     *     HTTP/1.1 Range Requests</a>
     * 2. Range Units
     * 4. Responses to a Range Request
     *
     * <a >
     *     HTTP/1.1</a>
     * 10.2.7 206 Partial Content
     * 14.5 Accept-Ranges
     * 14.13 Content-Length
     * 14.16 Content-Range
     * 14.17 Content-Type
     * 19.5.1 Content-Disposition
     * 15.5 Content-Disposition Issues
     *
     * <a >
     *     Content-Disposition</a>
     * 2. The Content-Disposition Header Field
     * 2.1 The Inline Disposition Type
     * 2.3 The Filename Parameter
     * </pre>
     *
     * @param response     請求響應(yīng)對象
     * @param fileName     請求的文件名稱
     * @param contentType  內(nèi)容類型
     * @param contentRange 內(nèi)容范圍對象
     */
    private static void setResponse(
            HttpServletResponse response, String fileName, String contentType,
            ContentRange contentRange) {
        // http狀態(tài)碼要為206:表示獲取部分內(nèi)容
        response.setStatus(HttpStatus.PARTIAL_CONTENT.value());
        // 支持斷點續(xù)傳,獲取部分字節(jié)內(nèi)容
        // Accept-Ranges:bytes弹谁,表示支持Range請求
        response.setHeader(HttpHeaders.ACCEPT_RANGES, ContentRange.BYTES_STRING);
        // inline表示瀏覽器直接使用乾巧,attachment表示下載,fileName表示下載的文件名
        response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
                "inline;filename=" + MediaContentUtil.encode(fileName));
        // Content-Range预愤,格式為:[要下載的開始位置]-[結(jié)束位置]/[文件總大小]
        // Content-Range: bytes 0-10/3103沟于,格式為bytes 開始-結(jié)束/全部
        response.setHeader(HttpHeaders.CONTENT_RANGE, contentRange.toContentRange());

        response.setContentType(contentType);
        // Content-Length: 11,本次內(nèi)容的大小
        response.setContentLengthLong(contentRange.applyAsContentLength());
    }

    /**
     * <a href="http://www.reibang.com/p/08db5ba3bc95">
     *     Spring Boot 處理 HTTP Headers</a>
     */
    private void download(
            String fileName, String path, HttpServletRequest request, HttpServletResponse response,
            HttpHeaders headers)
            throws IOException {
        Path filePath = Paths.get(path + fileName);
        if (!Files.exists(filePath)) {
            log.warn("file not exist, filePath={}", filePath);
            return;
        }
        long fileLength = Files.size(filePath);
//        long fileLength2 = filePath.toFile().length() - 1;
//        // fileLength=1184856, fileLength2=1184855
//        log.info("fileLength={}, fileLength2={}", fileLength, fileLength2);

        // 內(nèi)容范圍
        ContentRange contentRange = applyAsContentRange(headers, fileLength, request);

        // 要下載的長度
        long contentLength = contentRange.applyAsContentLength();
        log.debug("contentRange={}, contentLength={}", contentRange, contentLength);

        // 文件類型
        String contentType = request.getServletContext().getMimeType(fileName);
        // mimeType=video/mp4, CONTENT_TYPE=null
        log.debug("mimeType={}, CONTENT_TYPE={}", contentType, request.getContentType());

        setResponse(response, fileName, contentType, contentRange);

        // 耗時指標統(tǒng)計
        StopWatch stopWatch = new StopWatch("downloadFile");
        stopWatch.start(fileName);
        try {
            // case-1.參考網(wǎng)上他人的實現(xiàn)
//            if (fileLength >= Integer.MAX_VALUE) {
//                NioUtils.copy(filePath, response, contentRange);
//            } else {
//                NioUtils.copyByChannelAndBuffer(filePath, response, contentRange);
//            }

            // case-2.使用現(xiàn)成API
            NioUtils.copyByBio(filePath, response, contentRange);
//            NioUtils.copyByNio(filePath, response, contentRange);

            // case-3.視頻分段漸進式播放
//            if (contentType.startsWith("video")) {
//                NioUtils.copyForBufferSize(filePath, response, contentRange);
//            } else {
//                // 圖片植康、PDF等文件
//                NioUtils.copyByBio(filePath, response, contentRange);
//            }
        } finally {
            stopWatch.stop();
            log.info("download file, fileName={}, time={} ms", fileName, stopWatch.getTotalTimeMillis());
        }
    }

    private static ContentRange applyAsContentRange(
            HttpHeaders headers, long fileLength, HttpServletRequest request) {
        /*
         * 3.1. Range - HTTP/1.1 Range Requests
         * https://www.rfc-editor.org/rfc/rfc7233#section-3.1
         * Range: "bytes" "=" first-byte-pos "-" [ last-byte-pos ]
         *
         * For example:
         * bytes=0-
         * bytes=0-499
         */
        // Range:告知服務(wù)端旷太,客戶端下載該文件想要從指定的位置開始下載
        List<HttpRange> httpRanges = headers.getRange();

        String range = request.getHeader(HttpHeaders.RANGE);
        // httpRanges=[], range=null
        // httpRanges=[448135688-], range=bytes=448135688-
        log.debug("httpRanges={}, range={}", httpRanges, range);

        // 開始下載位置
        long firstBytePos;
        // 結(jié)束下載位置
        long lastBytePos;
        if (CollectionUtils.isEmpty(httpRanges)) {
            firstBytePos = 0;
            lastBytePos = fileLength - 1;
        } else {
            HttpRange httpRange = httpRanges.get(0);
            firstBytePos = httpRange.getRangeStart(fileLength);
            lastBytePos = httpRange.getRangeEnd(fileLength);
        }
        return new ContentRange(firstBytePos, lastBytePos, fileLength);
    }
}

NioUtils

package com.example.insurance.common;

import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.NioUtil;
import cn.hutool.core.io.StreamProgress;
import cn.hutool.core.io.unit.DataSize;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;

/**
 * NIO相關(guān)工具封裝,主要針對Channel讀寫、拷貝等封裝
 */
@Slf4j
public final class NioUtils {

    /**
     * 緩沖區(qū)大小 16KB
     *
     * @see NioUtil#DEFAULT_BUFFER_SIZE
     * @see NioUtil#DEFAULT_LARGE_BUFFER_SIZE
     */
//    private static final int BUFFER_SIZE = NioUtil.DEFAULT_MIDDLE_BUFFER_SIZE;
    private static final int BUFFER_SIZE = (int) DataSize.ofKilobytes(16L).toBytes();

    /**
     * <pre>
     * <a >
     *     Java后端實現(xiàn)視頻分段漸進式播放</a>
     * 服務(wù)端如何將一個大的視頻文件做切分供璧,分段響應(yīng)給客戶端存崖,讓瀏覽器可以漸進式地播放。
     * 文件的斷點續(xù)傳睡毒、文件多線程并發(fā)下載(迅雷就是這么玩的)等来惧。
     *
     * <a >
     *     大文件分片上傳前后端實現(xiàn)</a>
     * </pre>
     */
    public static void copyForBufferSize(
            Path filePath, HttpServletResponse response, ContentRange contentRange) {
        String fileName = filePath.getFileName().toString();

        RandomAccessFile randomAccessFile = null;
        OutputStream outputStream = null;
        try {
            // 隨機讀文件
            randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");
            // 移動訪問指針到指定位置
            randomAccessFile.seek(contentRange.getStart());

            // 注意:緩沖區(qū)大小 2MB,視頻加載正常吕嘀;1MB時有部分視頻加載失敗
            int bufferSize = BUFFER_SIZE;

            //獲取響應(yīng)的輸出流
            outputStream = new BufferedOutputStream(response.getOutputStream(), bufferSize);

            // 每次請求只返回1MB的視頻流
            byte[] buffer = new byte[bufferSize];
            int len = randomAccessFile.read(buffer);
            //設(shè)置此次相應(yīng)返回的數(shù)據(jù)長度
            response.setContentLength(len);
            // 將這1MB的視頻流響應(yīng)給客戶端
            outputStream.write(buffer, 0, len);

            log.info("file download complete, fileName={}, contentRange={}",
                    fileName, contentRange.toContentRange());
        } catch (ClientAbortException | IORuntimeException e) {
            // 捕獲此異常表示用戶停止下載
            log.warn("client stop file download, fileName={}", fileName);
        } catch (Exception e) {
            log.error("file download error, fileName={}", fileName, e);
        } finally {
            IoUtil.close(outputStream);
            IoUtil.close(randomAccessFile);
        }
    }

    /**
     * 拷貝流违寞,拷貝后關(guān)閉流。
     *
     * @param filePath     源文件路徑
     * @param response     請求響應(yīng)
     * @param contentRange 內(nèi)容范圍
     */
    public static void copyByBio(
            Path filePath, HttpServletResponse response, ContentRange contentRange) {
        String fileName = filePath.getFileName().toString();

        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            RandomAccessFile randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");
            randomAccessFile.seek(contentRange.getStart());

            inputStream = Channels.newInputStream(randomAccessFile.getChannel());
            outputStream = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE);

            StreamProgress streamProgress = new StreamProgressImpl(fileName);

            long transmitted = IoUtil.copy(inputStream, outputStream, BUFFER_SIZE, streamProgress);
            log.info("file download complete, fileName={}, transmitted={}", fileName, transmitted);
        } catch (ClientAbortException | IORuntimeException e) {
            // 捕獲此異常表示用戶停止下載
            log.warn("client stop file download, fileName={}", fileName);
        } catch (Exception e) {
            log.error("file download error, fileName={}", fileName, e);
        } finally {
            IoUtil.close(outputStream);
            IoUtil.close(inputStream);
        }
    }

    /**
     * 拷貝流偶房,拷貝后關(guān)閉流趁曼。
     * <pre>
     * <a >
     *     Java NIO 學習筆記(一)----概述,Channel/Buffer</a>
     * </pre>
     *
     * @param filePath     源文件路徑
     * @param response     請求響應(yīng)
     * @param contentRange 內(nèi)容范圍
     */
    public static void copyByNio(
            Path filePath, HttpServletResponse response, ContentRange contentRange) {
        String fileName = filePath.getFileName().toString();

        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            RandomAccessFile randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");
            randomAccessFile.seek(contentRange.getStart());

            inputStream = Channels.newInputStream(randomAccessFile.getChannel());
            outputStream = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE);

            StreamProgress streamProgress = new StreamProgressImpl(fileName);

            long transmitted = NioUtil.copyByNIO(inputStream, outputStream,
                    BUFFER_SIZE, streamProgress);
            log.info("file download complete, fileName={}, transmitted={}", fileName, transmitted);
        } catch (ClientAbortException | IORuntimeException e) {
            // 捕獲此異常表示用戶停止下載
            log.warn("client stop file download, fileName={}", fileName);
        } catch (Exception e) {
            log.error("file download error, fileName={}", fileName, e);
        } finally {
            IoUtil.close(outputStream);
            IoUtil.close(inputStream);
        }
    }

    /**
     * <pre>
     * <a >
     *     SpringBoot Java實現(xiàn)Http方式分片下載斷點續(xù)傳+實現(xiàn)H5大視頻漸進式播放</a>
     * SpringBoot 實現(xiàn)Http分片下載斷點續(xù)傳棕洋,從而實現(xiàn)H5頁面的大視頻播放問題挡闰,實現(xiàn)漸進式播放,每次只播放需要播放的內(nèi)容就可以了掰盘,不需要加載整個文件到內(nèi)存中摄悯。
     * 二、Http分片下載斷點續(xù)傳實現(xiàn)
     * 四愧捕、緩存文件定時刪除任務(wù)
     * </pre>
     */
    public static void copy(Path filePath, HttpServletResponse response, ContentRange contentRange) {
        String fileName = filePath.getFileName().toString();
        // 要下載的長度
        long contentLength = contentRange.applyAsContentLength();

        BufferedOutputStream outputStream = null;
        RandomAccessFile randomAccessFile = null;
        // 已傳送數(shù)據(jù)大小
        long transmitted = 0;
        try {
            randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");
            randomAccessFile.seek(contentRange.getStart());
            outputStream = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE);
            // 把數(shù)據(jù)讀取到緩沖區(qū)中
            byte[] buffer = new byte[BUFFER_SIZE];

            int len = BUFFER_SIZE;
            //warning:判斷是否到了最后不足4096(buffer的length)個byte這個邏輯((transmitted + len) <= contentLength)要放前面
            //不然會會先讀取randomAccessFile奢驯,造成后面讀取位置出錯;
            while ((transmitted + len) <= contentLength && (len = randomAccessFile.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
                transmitted += len;

                log.info("fileName={}, transmitted={}", fileName, transmitted);
            }
            //處理不足buffer.length部分
            if (transmitted < contentLength) {
                len = randomAccessFile.read(buffer, 0, (int) (contentLength - transmitted));
                outputStream.write(buffer, 0, len);
                transmitted += len;

                log.info("fileName={}, transmitted={}", fileName, transmitted);
            }

            log.info("file download complete, fileName={}, transmitted={}", fileName, transmitted);
        } catch (ClientAbortException e) {
            // 捕獲此異常表示用戶停止下載
            log.warn("client stop file download, fileName={}, transmitted={}", fileName, transmitted);
        } catch (Exception e) {
            log.error("file download error, fileName={}, transmitted={}", fileName, transmitted, e);
        } finally {
            IoUtil.close(outputStream);
            IoUtil.close(randomAccessFile);
        }
    }

    /**
     * 通過數(shù)據(jù)傳輸通道和緩沖區(qū)讀取文件數(shù)據(jù)。
     * <pre>
     * 當文件長度超過{@link Integer#MAX_VALUE}時次绘,
     * 使用{@link FileChannel#map(FileChannel.MapMode, long, long)}報如下異常瘪阁。
     * java.lang.IllegalArgumentException: Size exceeds Integer.MAX_VALUE
     *   at sun.nio.ch.FileChannelImpl.map(FileChannelImpl.java:863)
     *   at com.example.insurance.controller.ResourceController.download(ResourceController.java:200)
     * </pre>
     *
     * @param filePath     源文件路徑
     * @param response     請求響應(yīng)
     * @param contentRange 內(nèi)容范圍
     */
    public static void copyByChannelAndBuffer(
            Path filePath, HttpServletResponse response, ContentRange contentRange) {
        String fileName = filePath.getFileName().toString();
        // 要下載的長度
        long contentLength = contentRange.applyAsContentLength();

        BufferedOutputStream outputStream = null;
        FileChannel inChannel = null;
        // 已傳送數(shù)據(jù)大小
        long transmitted = 0;
        long firstBytePos = contentRange.getStart();
        long fileLength = contentRange.getLength();
        try {
            inChannel = FileChannel.open(filePath, StandardOpenOption.READ, StandardOpenOption.WRITE);
            // 建立直接緩沖區(qū)
            MappedByteBuffer inMap = inChannel.map(FileChannel.MapMode.READ_ONLY, firstBytePos, fileLength);
            outputStream = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE);
            // 把數(shù)據(jù)讀取到緩沖區(qū)中
            byte[] buffer = new byte[BUFFER_SIZE];

            int len = BUFFER_SIZE;
            // warning:判斷是否到了最后不足4096(buffer的length)個byte這個邏輯((transmitted + len) <= contentLength)要放前面
            // 不然會會先讀取file,造成后面讀取位置出錯
            while ((transmitted + len) <= contentLength) {
                inMap.get(buffer);
                outputStream.write(buffer, 0, len);
                transmitted += len;

                log.info("fileName={}, transmitted={}", fileName, transmitted);
            }
            // 處理不足buffer.length部分
            if (transmitted < contentLength) {
                len = (int) (contentLength - transmitted);
                buffer = new byte[len];
                inMap.get(buffer);
                outputStream.write(buffer, 0, len);
                transmitted += len;

                log.info("fileName={}, transmitted={}", fileName, transmitted);
            }

            log.info("file download complete, fileName={}, transmitted={}", fileName, transmitted);
        } catch (ClientAbortException e) {
            // 捕獲此異常表示用戶停止下載
            log.warn("client stop file download, fileName={}, transmitted={}", fileName, transmitted);
        } catch (Exception e) {
            log.error("file download error, fileName={}, transmitted={}", fileName, transmitted, e);
        } finally {
            IoUtil.close(outputStream);
            IoUtil.close(inChannel);
        }
    }

}

ContentRange

package com.example.insurance.common;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 內(nèi)容范圍對象
 * <pre>
 * <a >
 *     4.2. Content-Range - HTTP/1.1 Range Requests</a>
 * Content-Range: "bytes" first-byte-pos "-" last-byte-pos  "/" complete-length
 *
 * For example:
 * Content-Range: bytes 0-499/1234
 * </pre>
 *
 * @see org.apache.catalina.servlets.DefaultServlet.Range
 */
@Getter
@AllArgsConstructor
public class ContentRange {

    /**
     * 第一個字節(jié)的位置
     */
    private final long start;
    /**
     * 最后一個字節(jié)的位置
     */
    private long end;
    /**
     * 內(nèi)容完整的長度/總長度
     */
    private final long length;

    public static final String BYTES_STRING = "bytes";

    /**
     * 組裝內(nèi)容范圍的響應(yīng)頭邮偎。
     * <pre>
     * <a >
     *     4.2. Content-Range - HTTP/1.1 Range Requests</a>
     * Content-Range: "bytes" first-byte-pos "-" last-byte-pos  "/" complete-length
     *
     * For example:
     * Content-Range: bytes 0-499/1234
     * </pre>
     *
     * @return 內(nèi)容范圍的響應(yīng)頭
     */
    public String toContentRange() {
        return BYTES_STRING + ' ' + start + '-' + end + '/' + length;
//        return "bytes " + start + "-" + end + "/" + length;
    }

    /**
     * 計算內(nèi)容完整的長度/總長度管跺。
     *
     * @return 內(nèi)容完整的長度/總長度
     */
    public long applyAsContentLength() {
        return end - start + 1;
    }

    /**
     * Validate range.
     *
     * @return true if the range is valid, otherwise false
     */
    public boolean validate() {
        if (end >= length) {
            end = length - 1;
        }
        return (start >= 0) && (end >= 0) && (start <= end) && (length > 0);
    }

    @Override
    public String toString() {
        return "firstBytePos=" + start +
                ", lastBytePos=" + end +
                ", fileLength=" + length;
    }
}

StreamProgressImpl

package com.example.insurance.common;

import cn.hutool.core.io.StreamProgress;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * 數(shù)據(jù)流進度條
 */
@Slf4j
@AllArgsConstructor
public class StreamProgressImpl implements StreamProgress {

    private final String fileName;

    @Override
    public void start() {
        log.info("start progress {}", fileName);
    }

    @Override
    public void progress(long total, long progressSize) {
        log.debug("progress {}, total={}, progressSize={}", fileName, total, progressSize);
    }

    @Override
    public void finish() {
        log.info("finish progress {}", fileName);
    }
}

MediaContentUtil

package com.example.insurance.common;

import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

/**
 * 文件內(nèi)容輔助方法集
 */
public final class MediaContentUtil {

    public static String filePath() {
        String osName = System.getProperty("os.name");
        String filePath = "/data/files/";
        if (osName.startsWith("Windows")) {
            filePath = "D:\" + filePath;
        }
//        else if (osName.startsWith("Linux")) {
//            filePath = MediaContentConstant.FILE_PATH;
//        }
        else if (osName.startsWith("Mac") || osName.startsWith("Linux")) {
            filePath = "/home/admin" + filePath;
        }
        return filePath;
    }

    public static String encode(String fileName) {
        return URLEncoder.encode(fileName, StandardCharsets.UTF_8);
    }

    public static String decode(String fileName) {
        return URLDecoder.decode(fileName, StandardCharsets.UTF_8);
    }
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市禾进,隨后出現(xiàn)的幾起案子豁跑,更是在濱河造成了極大的恐慌,老刑警劉巖泻云,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件艇拍,死亡現(xiàn)場離奇詭異,居然都是意外死亡宠纯,警方通過查閱死者的電腦和手機淑倾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來征椒,“玉大人,你說我怎么就攤上這事湃累〔龋” “怎么了碍讨?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蒙秒。 經(jīng)常有香客問我勃黍,道長,這世上最難降的妖魔是什么晕讲? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任覆获,我火速辦了婚禮,結(jié)果婚禮上瓢省,老公的妹妹穿的比我還像新娘弄息。我一直安慰自己,他們只是感情好勤婚,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布摹量。 她就那樣靜靜地躺著,像睡著了一般馒胆。 火紅的嫁衣襯著肌膚如雪缨称。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天祝迂,我揣著相機與錄音睦尽,去河邊找鬼。 笑死型雳,一個胖子當著我的面吹牛当凡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播四啰,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼宁玫,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了柑晒?” 一聲冷哼從身側(cè)響起欧瘪,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎匙赞,沒想到半個月后佛掖,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡涌庭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年芥被,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坐榆。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡拴魄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情匹中,我是刑警寧澤夏漱,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站顶捷,受9級特大地震影響挂绰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜服赎,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一葵蒂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧重虑,春花似錦践付、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至芽死,卻和暖如春乏梁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背关贵。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工遇骑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人揖曾。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓落萎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親炭剪。 傳聞我的和親對象是個殘疾皇子练链,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容