diff --git a/src/main/java/com/newyetai/component/file/service/impl/FileServiceImpl.java b/src/main/java/com/newyetai/component/file/service/impl/FileServiceImpl.java index 370b40d..7481b91 100644 --- a/src/main/java/com/newyetai/component/file/service/impl/FileServiceImpl.java +++ b/src/main/java/com/newyetai/component/file/service/impl/FileServiceImpl.java @@ -95,21 +95,149 @@ public class FileServiceImpl implements FileService { if (Objects.nonNull(request) && StringUtils.isNotBlank(request.getParameter("filename"))) { filename = URLDecoder.decode(request.getParameter("filename"),"utf-8"); } - response.setContentType(session.getServletContext().getMimeType(filename)); + + String mimeType = session.getServletContext().getMimeType(filename); + response.setContentType(mimeType); String encodFilename = URLEncoder.encode(filename, "utf-8"); response.setHeader("code", "200"); response.setHeader("Cache-Control", "max-age=31556926"); + + // 检查是否为视频文件,如果是则支持Range请求 + boolean isVideo = mimeType != null && mimeType.startsWith("video/"); + if (!Boolean.TRUE.equals(display)) { response.setHeader("Content-Disposition", String.format("attachment; filename=\"%1$s\"; filename*=utf-8''%1$s", encodFilename)); } - try (OutputStream outputStream = response.getOutputStream()) { - fileStorage.download(fileRecord.getFilePath(), outputStream, fileRecord.getFileSize()); + + Long fileSize = fileRecord.getFileSize(); + + // 对于视频文件,支持Range请求 + if (isVideo) { + handleVideoDownloadWithRange(fileRecord, request, response, fileSize); + } else { + // 普通文件下载 + try (OutputStream outputStream = response.getOutputStream()) { + fileStorage.download(fileRecord.getFilePath(), outputStream, fileSize); + } catch (ClientAbortException e) { + log.warn("客户端主动断开连接: {}", e.getMessage()); + } + } + } + + /** + * 处理支持Range请求的视频文件下载 + */ + private void handleVideoDownloadWithRange(FileRecord fileRecord, HttpServletRequest request, + HttpServletResponse response, Long fileSize) throws IOException { + + // 设置支持Range请求的头部 + response.setHeader("Accept-Ranges", "bytes"); + + if (fileSize != null) { + response.setHeader("Content-Length", String.valueOf(fileSize)); + } + + String rangeHeader = request.getHeader("Range"); + + if (rangeHeader != null && rangeHeader.startsWith("bytes=")) { + // 处理Range请求 + handleRangeRequest(fileRecord, response, rangeHeader, fileSize); + } else { + // 普通请求,但设置正确的头部 + if (fileSize != null) { + response.setContentLengthLong(fileSize); + } + try (OutputStream outputStream = response.getOutputStream()) { + fileStorage.download(fileRecord.getFilePath(), outputStream, fileSize); + } catch (ClientAbortException e) { + log.warn("客户端主动断开连接: {}", e.getMessage()); + } + } + } + + /** + * 处理HTTP Range请求 + */ + private void handleRangeRequest(FileRecord fileRecord, HttpServletResponse response, + String rangeHeader, Long fileSize) throws IOException { + + if (fileSize == null) { + // 如果没有文件大小信息,降级为普通下载 + try (OutputStream outputStream = response.getOutputStream()) { + fileStorage.download(fileRecord.getFilePath(), outputStream, fileSize); + } catch (ClientAbortException e) { + log.warn("客户端主动断开连接: {}", e.getMessage()); + } + return; + } + + // 解析Range头部,格式:bytes=start-end + String range = rangeHeader.substring(6); // 去掉 "bytes=" + String[] ranges = range.split("-"); + + long start = 0; + long end = fileSize - 1; + + try { + if (ranges.length >= 1 && !ranges[0].isEmpty()) { + start = Long.parseLong(ranges[0]); + } + if (ranges.length >= 2 && !ranges[1].isEmpty()) { + end = Long.parseLong(ranges[1]); + } + + // 确保范围有效 + if (start < 0) start = 0; + if (end >= fileSize) end = fileSize - 1; + if (start > end) { + response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + return; + } + + long contentLength = end - start + 1; + + // 设置206 Partial Content响应 + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + response.setHeader("Content-Range", String.format("bytes %d-%d/%d", start, end, fileSize)); + response.setHeader("Content-Length", String.valueOf(contentLength)); + + // 下载指定范围的内容 + downloadRange(fileRecord.getFilePath(), response.getOutputStream(), start, contentLength); + + } catch (NumberFormatException e) { + log.warn("无效的Range请求: {}", rangeHeader); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); } catch (ClientAbortException e) { log.warn("客户端主动断开连接: {}", e.getMessage()); } } + /** + * 下载文件的指定范围 + */ + private void downloadRange(String filePath, OutputStream outputStream, long start, long length) throws IOException { + try (InputStream inputStream = fileStorage.download(filePath)) { + // 跳过开始位置之前的字节 + long skipped = inputStream.skip(start); + if (skipped != start) { + log.warn("跳过字节数不匹配,期望: {}, 实际: {}", start, skipped); + } + + // 读取指定长度的数据 + byte[] buffer = new byte[8192]; + long remaining = length; + int bytesRead; + + while (remaining > 0 && (bytesRead = inputStream.read(buffer, 0, (int) Math.min(buffer.length, remaining))) != -1) { + outputStream.write(buffer, 0, bytesRead); + remaining -= bytesRead; + } + + outputStream.flush(); + } + } + @Override