|
|
@@ -21,20 +21,20 @@ import org.springframework.scheduling.annotation.Async;
|
|
|
import org.springframework.util.LinkedMultiValueMap;
|
|
|
import org.springframework.util.MultiValueMap;
|
|
|
import org.springframework.util.NumberUtils;
|
|
|
-import org.springframework.web.bind.annotation.GetMapping;
|
|
|
-import org.springframework.web.bind.annotation.PostMapping;
|
|
|
-import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
-import org.springframework.web.bind.annotation.RestController;
|
|
|
+import org.springframework.web.bind.annotation.*;
|
|
|
|
|
|
import javax.print.DocFlavor;
|
|
|
import javax.servlet.http.HttpServletRequest;
|
|
|
import javax.servlet.http.HttpServletResponse;
|
|
|
-import java.io.File;
|
|
|
-import java.io.FileInputStream;
|
|
|
-import java.io.IOException;
|
|
|
-import java.io.UnsupportedEncodingException;
|
|
|
+import java.io.*;
|
|
|
+import java.net.URLConnection;
|
|
|
import java.net.URLDecoder;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.Path;
|
|
|
+import java.nio.file.Paths;
|
|
|
import java.util.*;
|
|
|
+import java.util.regex.Matcher;
|
|
|
+import java.util.regex.Pattern;
|
|
|
|
|
|
/**
|
|
|
* Decription:
|
|
|
@@ -2626,7 +2626,7 @@ public class YiDaController {
|
|
|
* @param filePathName
|
|
|
* @param response
|
|
|
*/
|
|
|
- @GetMapping("preview")
|
|
|
+ @GetMapping("previewOld")
|
|
|
public void preview(String filePathName, HttpServletResponse response, HttpServletRequest request) {
|
|
|
String queryString = request.getQueryString();
|
|
|
String[] params = queryString.split("filePathName=");
|
|
|
@@ -2675,6 +2675,196 @@ public class YiDaController {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private static final int BUFFER_SIZE = 16 * 1024;
|
|
|
+ private static final Pattern RANGE_PATTERN = Pattern.compile("^bytes=(\\d*)-(\\d*)$");
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 浏览器文件预览,支持图片、PDF、音视频及 HTTP Range 请求。
|
|
|
+ *
|
|
|
+ * @param filePathName 文件完整路径
|
|
|
+ * @param request HTTP 请求
|
|
|
+ * @param response HTTP 响应
|
|
|
+ */
|
|
|
+ @GetMapping("preview")
|
|
|
+ public void preview(@RequestParam("filePathName") String filePathName,
|
|
|
+ HttpServletRequest request,
|
|
|
+ HttpServletResponse response) {
|
|
|
+ if (filePathName == null || filePathName.trim().isEmpty()) {
|
|
|
+ sendError(response, HttpServletResponse.SC_BAD_REQUEST, "filePathName不能为空");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Path filePath;
|
|
|
+ try {
|
|
|
+ filePath = Paths.get(filePathName).normalize();
|
|
|
+ } catch (Exception e) {
|
|
|
+ sendError(response, HttpServletResponse.SC_BAD_REQUEST, "文件路径格式错误");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!Files.exists(filePath)) {
|
|
|
+ sendError(response, HttpServletResponse.SC_NOT_FOUND, "文件不存在");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!Files.isRegularFile(filePath)) {
|
|
|
+ sendError(response, HttpServletResponse.SC_BAD_REQUEST, "目标不是普通文件");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!Files.isReadable(filePath)) {
|
|
|
+ sendError(response, HttpServletResponse.SC_FORBIDDEN, "文件不可读取");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ long fileLength = Files.size(filePath);
|
|
|
+ String contentType = Files.probeContentType(filePath);
|
|
|
+ if (contentType == null) {
|
|
|
+ contentType = URLConnection.guessContentTypeFromName(filePath.getFileName().toString());
|
|
|
+ }
|
|
|
+ if (contentType == null) {
|
|
|
+ contentType = "application/octet-stream";
|
|
|
+ }
|
|
|
+
|
|
|
+ response.reset();
|
|
|
+ response.setContentType(contentType);
|
|
|
+ response.setHeader("Content-Disposition", "inline");
|
|
|
+ response.setHeader("Accept-Ranges", "bytes");
|
|
|
+
|
|
|
+ String rangeHeader = request.getHeader("Range");
|
|
|
+ if (rangeHeader == null || rangeHeader.trim().isEmpty()) {
|
|
|
+ response.setStatus(HttpServletResponse.SC_OK);
|
|
|
+ response.setContentLengthLong(fileLength);
|
|
|
+ writeFile(filePath, 0, fileLength, response);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ long[] range = parseRange(rangeHeader, fileLength);
|
|
|
+ if (range == null) {
|
|
|
+ response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
|
|
+ response.setHeader("Content-Range", "bytes */" + fileLength);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ long start = range[0];
|
|
|
+ long end = range[1];
|
|
|
+ long contentLength = end - start + 1;
|
|
|
+
|
|
|
+ response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
|
|
+ response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileLength);
|
|
|
+ response.setContentLengthLong(contentLength);
|
|
|
+ writeFile(filePath, start, contentLength, response);
|
|
|
+ } catch (IOException e) {
|
|
|
+ sendError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "文件预览失败");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析单段 HTTP Range 请求。
|
|
|
+ *
|
|
|
+ * @param rangeHeader Range 请求头
|
|
|
+ * @param fileLength 文件总长度
|
|
|
+ * @return 起止字节下标;非法范围返回 null
|
|
|
+ */
|
|
|
+ private long[] parseRange(String rangeHeader, long fileLength) {
|
|
|
+ if (fileLength <= 0 || rangeHeader.contains(",")) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ Matcher matcher = RANGE_PATTERN.matcher(rangeHeader.trim());
|
|
|
+ if (!matcher.matches()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ String startText = matcher.group(1);
|
|
|
+ String endText = matcher.group(2);
|
|
|
+ if (startText.isEmpty() && endText.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ long start;
|
|
|
+ long end;
|
|
|
+
|
|
|
+ if (startText.isEmpty()) {
|
|
|
+ long suffixLength = Long.parseLong(endText);
|
|
|
+ if (suffixLength <= 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ start = Math.max(0, fileLength - suffixLength);
|
|
|
+ end = fileLength - 1;
|
|
|
+ } else {
|
|
|
+ start = Long.parseLong(startText);
|
|
|
+ end = endText.isEmpty() ? fileLength - 1 : Long.parseLong(endText);
|
|
|
+
|
|
|
+ if (start >= fileLength) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ end = Math.min(end, fileLength - 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (start < 0 || end < start) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return new long[]{start, end};
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从指定字节位置开始流式写出文件内容。
|
|
|
+ *
|
|
|
+ * @param filePath 文件路径
|
|
|
+ * @param start 起始字节位置
|
|
|
+ * @param length 输出长度
|
|
|
+ * @param response HTTP 响应
|
|
|
+ * @throws IOException 文件读取或响应写出失败
|
|
|
+ */
|
|
|
+ private void writeFile(Path filePath, long start, long length,
|
|
|
+ HttpServletResponse response) throws IOException {
|
|
|
+ byte[] buffer = new byte[BUFFER_SIZE];
|
|
|
+ long remaining = length;
|
|
|
+
|
|
|
+ try (RandomAccessFile input = new RandomAccessFile(filePath.toFile(), "r")) {
|
|
|
+ input.seek(start);
|
|
|
+
|
|
|
+ while (remaining > 0) {
|
|
|
+ int bytesToRead = (int) Math.min(buffer.length, remaining);
|
|
|
+ int bytesRead = input.read(buffer, 0, bytesToRead);
|
|
|
+ if (bytesRead == -1) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ response.getOutputStream().write(buffer, 0, bytesRead);
|
|
|
+ remaining -= bytesRead;
|
|
|
+ }
|
|
|
+
|
|
|
+ response.getOutputStream().flush();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 在响应未提交前返回错误状态。
|
|
|
+ *
|
|
|
+ * @param response HTTP 响应
|
|
|
+ * @param status HTTP 状态码
|
|
|
+ * @param message 错误信息
|
|
|
+ */
|
|
|
+ private void sendError(HttpServletResponse response, int status, String message) {
|
|
|
+ if (!response.isCommitted()) {
|
|
|
+ try {
|
|
|
+ response.sendError(status, message);
|
|
|
+ } catch (IOException ignored) {
|
|
|
+ // 响应通道异常时无法继续向客户端写入错误信息。
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
@Autowired
|
|
|
private ReadExcel readExcel;
|
|
|
|