媒体下载优化
This commit is contained in:
parent
e28d4aa921
commit
b61dbb1028
|
|
@ -8,7 +8,6 @@ import com.multictrl.common.constant.Constant;
|
||||||
import com.multictrl.common.page.PageData;
|
import com.multictrl.common.page.PageData;
|
||||||
import com.multictrl.common.utils.Result;
|
import com.multictrl.common.utils.Result;
|
||||||
import com.multictrl.common.validator.AssertUtils;
|
import com.multictrl.common.validator.AssertUtils;
|
||||||
import com.multictrl.modules.business.dto.FlightTaskDTO;
|
|
||||||
import com.multictrl.modules.business.dto.MediaFileDTO;
|
import com.multictrl.modules.business.dto.MediaFileDTO;
|
||||||
import com.multictrl.modules.business.service.MediaFileService;
|
import com.multictrl.modules.business.service.MediaFileService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
|
@ -17,9 +16,10 @@ import io.swagger.v3.oas.annotations.Parameters;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
import org.springframework.core.io.InputStreamResource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
@ -85,9 +85,9 @@ public class MediaFileController {
|
||||||
|
|
||||||
@Operation(summary = "下载媒体文件")
|
@Operation(summary = "下载媒体文件")
|
||||||
@LogOperation("下载媒体文件")
|
@LogOperation("下载媒体文件")
|
||||||
@GetMapping("/downloadMedia/{id}")
|
@GetMapping("/downloadMedia")
|
||||||
@RequiresPermissions("bus:media:download")
|
@RequiresPermissions("bus:media:download")
|
||||||
public ResponseEntity<InputStreamResource> downloadMedia(@PathVariable Long id) {
|
public ResponseEntity<Resource> downloadMedia(@RequestParam Long id) {
|
||||||
|
|
||||||
return mediaFileService.downloadMedia(id);
|
return mediaFileService.downloadMedia(id);
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +103,7 @@ public class MediaFileController {
|
||||||
@Parameter(name = "endTime", description = "结束时间")
|
@Parameter(name = "endTime", description = "结束时间")
|
||||||
})
|
})
|
||||||
@DataFilter(tableAlias = "t")
|
@DataFilter(tableAlias = "t")
|
||||||
public ResponseEntity<InputStreamResource> conditionDownloadMedia(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
public ResponseEntity<StreamingResponseBody> conditionDownloadMedia(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
|
||||||
return mediaFileService.downloadMediaByZip(params);
|
return mediaFileService.downloadMediaByZip(params);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,9 @@ import com.multictrl.common.service.CrudService;
|
||||||
import com.multictrl.modules.business.dao.MediaFileDao;
|
import com.multictrl.modules.business.dao.MediaFileDao;
|
||||||
import com.multictrl.modules.business.dto.MediaFileDTO;
|
import com.multictrl.modules.business.dto.MediaFileDTO;
|
||||||
import com.multictrl.modules.business.entity.MediaFileEntity;
|
import com.multictrl.modules.business.entity.MediaFileEntity;
|
||||||
import org.springframework.core.io.InputStreamResource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
@ -30,10 +31,10 @@ public interface MediaFileService extends CrudService<MediaFileEntity, MediaFile
|
||||||
void deleteMediaFile(Long[] ids);
|
void deleteMediaFile(Long[] ids);
|
||||||
|
|
||||||
//下载媒体文件
|
//下载媒体文件
|
||||||
ResponseEntity<InputStreamResource> downloadMedia(Long id);
|
ResponseEntity<Resource> downloadMedia(Long id);
|
||||||
|
|
||||||
//下载媒体zip
|
//下载媒体zip
|
||||||
ResponseEntity<InputStreamResource> downloadMediaByZip(Map<String, Object> params);
|
ResponseEntity<StreamingResponseBody> downloadMediaByZip(Map<String, Object> params);
|
||||||
|
|
||||||
//下载文件地址
|
//下载文件地址
|
||||||
Map<String, List<String>> oneClickGetDownloadPath(Map<String, Object> params);
|
Map<String, List<String>> oneClickGetDownloadPath(Map<String, Object> params);
|
||||||
|
|
|
||||||
|
|
@ -22,17 +22,20 @@ import com.multictrl.modules.security.user.SecurityUser;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
import org.springframework.core.io.InputStreamResource;
|
import org.springframework.core.io.FileSystemResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
|
|
@ -122,30 +125,26 @@ public class MediaFileServiceImpl extends CrudServiceImpl<MediaFileDao, MediaFil
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<InputStreamResource> downloadMedia(Long id) {
|
public ResponseEntity<Resource> downloadMedia(Long id) {
|
||||||
try {
|
try {
|
||||||
MediaFileDTO dto = get(id);
|
MediaFileDTO dto = get(id);
|
||||||
String filePath = minioConfig.getOther().getDataPath() + BusinessConstant.DOCK_MEDIA_BUCKET + "/" + dto.getObjectKey();
|
// 构建文件路径(建议用 Paths.get 更安全)
|
||||||
String fileName = dto.getName();
|
Path filePath = Paths.get(minioConfig.getOther().getDataPath(), BusinessConstant.DOCK_MEDIA_BUCKET, dto.getObjectKey());
|
||||||
File file = new File(filePath);
|
File file = filePath.toFile();
|
||||||
|
|
||||||
// 检查文件是否存在
|
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置 Content-Disposition 头
|
String fileName = dto.getName();
|
||||||
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20");
|
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8)
|
||||||
String contentDisposition = "attachment; filename=" + encodedFileName;
|
.replace("+", "%20");
|
||||||
|
String contentDisposition = "attachment; filename*=UTF-8''" + encodedFileName;
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
return ResponseEntity.ok()
|
||||||
headers.setContentType(MediaType.parseMediaType(MediaType.APPLICATION_OCTET_STREAM_VALUE));
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
headers.set(HttpHeaders.CONTENT_DISPOSITION, contentDisposition);
|
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
|
||||||
headers.setContentLength((int) Files.size(file.toPath()));
|
.contentLength(Files.size(filePath)) // 直接传 long,安全
|
||||||
|
.body(new FileSystemResource(file)); // 自动管理流
|
||||||
// 使用 InputStreamResource 支持大文件流式传输
|
|
||||||
InputStream inputStream = Files.newInputStream(file.toPath());
|
|
||||||
return new ResponseEntity<>(new InputStreamResource(inputStream), headers, HttpStatus.OK);
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("媒体文件下载失败:{}", ExceptionUtils.getStackTrace(e));
|
log.error("媒体文件下载失败:{}", ExceptionUtils.getStackTrace(e));
|
||||||
|
|
@ -154,64 +153,54 @@ public class MediaFileServiceImpl extends CrudServiceImpl<MediaFileDao, MediaFil
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<InputStreamResource> downloadMediaByZip(Map<String, Object> params) {
|
public ResponseEntity<StreamingResponseBody> downloadMediaByZip(Map<String, Object> params) {
|
||||||
ZipOutputStream zipOut = null;
|
List<MediaFileEntity> list = baseDao.list(getWrapperBy(params));
|
||||||
ByteArrayOutputStream baos = null;
|
if (list.isEmpty()) {
|
||||||
try {
|
return ResponseEntity.notFound().build();
|
||||||
baos = new ByteArrayOutputStream();
|
|
||||||
zipOut = new ZipOutputStream(baos);
|
|
||||||
|
|
||||||
// 遍历所有ID,将文件添加到ZIP中
|
|
||||||
List<MediaFileEntity> list = baseDao.list(getWrapperBy(params));
|
|
||||||
for (MediaFileEntity entity : list) {
|
|
||||||
try {
|
|
||||||
String filePath = minioConfig.getOther().getDataPath() + BusinessConstant.DOCK_MEDIA_BUCKET + "/" + entity.getObjectKey();
|
|
||||||
String fileName = entity.getName();
|
|
||||||
File file = new File(filePath);
|
|
||||||
|
|
||||||
// 创建ZIP条目
|
|
||||||
ZipEntry zipEntry = new ZipEntry(fileName);
|
|
||||||
zipOut.putNextEntry(zipEntry);
|
|
||||||
|
|
||||||
// 将文件内容写入ZIP
|
|
||||||
Files.copy(file.toPath(), zipOut);
|
|
||||||
|
|
||||||
zipOut.closeEntry();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
// 记录错误但继续处理其他文件
|
|
||||||
log.error("处理文件ID {} 时出错: {}", entity.getId(), e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭ZIP流
|
|
||||||
zipOut.close();
|
|
||||||
baos.close();
|
|
||||||
|
|
||||||
// 设置响应头
|
|
||||||
String zipFileName = "media_" + System.currentTimeMillis() + ".zip";
|
|
||||||
String encodedFileName = URLEncoder.encode(zipFileName, StandardCharsets.UTF_8).replace("+", "%20");
|
|
||||||
String contentDisposition = "attachment; filename=" + encodedFileName;
|
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.setContentType(MediaType.parseMediaType("application/zip"));
|
|
||||||
headers.set(HttpHeaders.CONTENT_DISPOSITION, contentDisposition);
|
|
||||||
headers.setContentLength(baos.size());
|
|
||||||
|
|
||||||
// 创建输入流资源
|
|
||||||
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
|
|
||||||
return new ResponseEntity<>(new InputStreamResource(bais), headers, HttpStatus.OK);
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
// 关闭流
|
|
||||||
try {
|
|
||||||
zipOut.close();
|
|
||||||
baos.close();
|
|
||||||
} catch (IOException ex) {
|
|
||||||
log.error("媒体文件(zip)下载失败:{}", ExceptionUtils.getStackTrace(e));
|
|
||||||
}
|
|
||||||
throw new RenException(ErrorCode.INTERNAL_SERVER_ERROR);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String zipFileName = "media_" + System.currentTimeMillis() + ".zip";
|
||||||
|
String encodedFileName = URLEncoder.encode(zipFileName, StandardCharsets.UTF_8)
|
||||||
|
.replace("+", "%20");
|
||||||
|
// 使用 RFC 5987 标准编码避免乱码
|
||||||
|
String contentDisposition = "attachment; filename*=UTF-8''" + encodedFileName;
|
||||||
|
|
||||||
|
StreamingResponseBody streamingBody = outputStream -> {
|
||||||
|
// 使用 try-with-resources 确保 zip 流关闭
|
||||||
|
try (ZipOutputStream zipOut = new ZipOutputStream(outputStream)) {
|
||||||
|
for (MediaFileEntity entity : list) {
|
||||||
|
try {
|
||||||
|
Path filePath = Paths.get(
|
||||||
|
minioConfig.getOther().getDataPath(),
|
||||||
|
BusinessConstant.DOCK_MEDIA_BUCKET,
|
||||||
|
entity.getObjectKey()
|
||||||
|
);
|
||||||
|
// 避免路径遍历攻击:检查最终路径是否在基础目录内(可选但推荐)
|
||||||
|
File file = filePath.toFile();
|
||||||
|
if (!file.exists()) {
|
||||||
|
log.warn("文件不存在,跳过: {}", filePath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String fileName = entity.getName();
|
||||||
|
ZipEntry zipEntry = new ZipEntry(fileName);
|
||||||
|
zipOut.putNextEntry(zipEntry);
|
||||||
|
Files.copy(filePath, zipOut);
|
||||||
|
zipOut.closeEntry();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 记录错误但继续打包其他文件
|
||||||
|
log.error("添加文件到ZIP失败,ID: {}", entity.getId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("ZIP打包过程中发生IO异常", e);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.parseMediaType("application/zip"))
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
|
||||||
|
.body(streamingBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue