package jnpf.fileStorage.dms;
|
|
import jakarta.servlet.http.HttpServletResponse;
|
import jnpf.dms.DmsTicketLifecycleApi;
|
import jnpf.dms.DmsTicketLifecycleChecks;
|
import jnpf.dmsEntity.download.model.DmsFileDownloadTicket;
|
import jnpf.dmsEntity.preview.enums.DmsWatermarkTargetType;
|
import jnpf.exception.DataException;
|
import jnpf.base.ActionResult;
|
import jnpf.onlyoffice.DmsDownloadPdfApi;
|
import jnpf.onlyoffice.DmsDownloadPdfRequest;
|
import jnpf.util.TicketUtil;
|
import org.dromara.x.file.storage.core.FileInfo;
|
import org.dromara.x.file.storage.core.FileStorageService;
|
import org.dromara.x.file.storage.core.platform.MinioFileStorage;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.http.ContentDisposition;
|
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.MediaType;
|
import org.springframework.stereotype.Service;
|
|
import java.io.ByteArrayOutputStream;
|
import java.nio.charset.StandardCharsets;
|
import java.util.Locale;
|
import java.util.Objects;
|
import java.util.UUID;
|
|
@Service
|
public class DmsFileDownloadStorageService {
|
private static final String DMS_STORAGE_PLATFORM = "seaweedfs-1";
|
private static final long MAX_WATERMARK_SOURCE_BYTES = 100L * 1024L * 1024L;
|
|
private final FileStorageService fileStorageService;
|
private final TicketReader ticketReader;
|
private final DmsFileWatermarkService watermarkService;
|
private final DmsDownloadPdfApi pdfRenderer;
|
private final DmsPersistentPdfRecoveryReporter recoveryReporter;
|
|
@Autowired
|
public DmsFileDownloadStorageService(FileStorageService fileStorageService,
|
DmsFileWatermarkService watermarkService,
|
DmsDownloadPdfApi pdfRenderer,
|
DmsPersistentPdfRecoveryReporter recoveryReporter, DmsTicketLifecycleApi lifecycleApi) {
|
this(fileStorageService, ticket -> DmsTicketLifecycleChecks.download(TicketUtil.parseTicket(ticket), lifecycleApi, "jnpf-file"), watermarkService, pdfRenderer,
|
recoveryReporter);
|
}
|
|
DmsFileDownloadStorageService(FileStorageService fileStorageService, TicketReader ticketReader,
|
DmsFileWatermarkService watermarkService, DmsDownloadPdfApi pdfRenderer) {
|
this(fileStorageService, ticketReader, watermarkService, pdfRenderer, null);
|
}
|
|
DmsFileDownloadStorageService(FileStorageService fileStorageService, TicketReader ticketReader,
|
DmsFileWatermarkService watermarkService, DmsDownloadPdfApi pdfRenderer,
|
DmsPersistentPdfRecoveryReporter recoveryReporter) {
|
this.fileStorageService = fileStorageService;
|
this.ticketReader = ticketReader;
|
this.watermarkService = watermarkService;
|
this.pdfRenderer = pdfRenderer;
|
this.recoveryReporter = recoveryReporter;
|
}
|
|
public void stream(UUID fileVersionId, String accessTicket, HttpServletResponse response) {
|
stream(fileVersionId, accessTicket, null, response);
|
}
|
|
public void stream(UUID fileVersionId, String accessTicket, String format, HttpServletResponse response) {
|
DmsFileDownloadTicket payload = parse(accessTicket);
|
if (!fileVersionId.equals(payload.getFileVersionId())) {
|
throw new DataException("DMS 下载票据与文件版本不匹配");
|
}
|
if (!DMS_STORAGE_PLATFORM.equals(payload.getStoragePlatform())) {
|
throw new DataException("DMS 下载票据的存储平台无效");
|
}
|
boolean pdf = !isBlank(format);
|
if (pdf && !"pdf".equalsIgnoreCase(format.trim())) {
|
throw new DataException("不支持的下载格式,format 仅支持 pdf");
|
}
|
String lowerName = payload.getFileName().toLowerCase(Locale.ROOT);
|
if (pdf && !lowerName.endsWith(".doc") && !lowerName.endsWith(".docx")
|
&& !lowerName.endsWith(".pdf")) {
|
throw new DataException("仅支持将 Word 文件转换为 PDF 下载");
|
}
|
|
MinioFileStorage storage = fileStorageService.getFileStorageVerify(payload.getStoragePlatform());
|
if (!Objects.equals(storage.getBucketName(), payload.getBucketName())) {
|
throw new DataException("DMS 下载票据的存储桶与当前配置不匹配");
|
}
|
|
if (pdf && (lowerName.endsWith(".doc") || lowerName.endsWith(".docx"))
|
&& reusablePersistentPdf(payload)) {
|
if (streamPersistentPdf(payload, response)) return;
|
}
|
if (pdf && (lowerName.endsWith(".doc") || lowerName.endsWith(".docx"))) {
|
streamPdf(payload, accessTicket, response);
|
return;
|
}
|
|
FileInfo fileInfo = new FileInfo()
|
.setPlatform(payload.getStoragePlatform())
|
.setBasePath(empty(payload.getBasePath()))
|
.setPath(payload.getObjectPath())
|
.setFilename(payload.getObjectName())
|
.setOriginalFilename(payload.getFileName())
|
.setContentType(payload.getMimeType())
|
.setSize(payload.getSizeBytes());
|
response.setContentType(pdf ? MediaType.APPLICATION_PDF_VALUE : isBlank(payload.getMimeType())
|
? MediaType.APPLICATION_OCTET_STREAM_VALUE : payload.getMimeType());
|
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment()
|
.filename(payload.getFileName(), StandardCharsets.UTF_8).build().toString());
|
response.setHeader(HttpHeaders.CACHE_CONTROL, "no-store");
|
try {
|
if (payload.getWatermarks() == null || payload.getWatermarks().isEmpty()) {
|
if (payload.getSizeBytes() != null && payload.getSizeBytes() >= 0) {
|
response.setContentLengthLong(payload.getSizeBytes());
|
}
|
fileStorageService.download(fileInfo).outputStream(response.getOutputStream());
|
return;
|
}
|
DmsWatermarkTargetType targetType = DmsWatermarkTargetType.from(
|
payload.getFileName(), payload.getMimeType());
|
if (targetType == DmsWatermarkTargetType.UNSUPPORTED) {
|
throw new DataException("该文件格式不支持动态水印");
|
}
|
if (targetType == DmsWatermarkTargetType.IMAGE) {
|
response.setContentType(DmsImageWatermarkService.contentType(
|
payload.getFileName(), payload.getMimeType()));
|
}
|
long sourceLimit = targetType == DmsWatermarkTargetType.IMAGE
|
? DmsImageWatermarkService.MAX_SOURCE_BYTES : MAX_WATERMARK_SOURCE_BYTES;
|
if (payload.getSizeBytes() != null
|
&& payload.getSizeBytes() > sourceLimit) {
|
throw new DataException(targetType == DmsWatermarkTargetType.IMAGE
|
? "图片超过动态水印处理上限(50 MB)"
|
: "DOCX 文件超过动态水印处理上限(100 MB)");
|
}
|
byte[] result = watermarkService.loadAndApply(() -> {
|
DmsBoundedByteArrayOutputStream source = new DmsBoundedByteArrayOutputStream(sourceLimit,
|
targetType == DmsWatermarkTargetType.IMAGE
|
? "图片超过动态水印处理上限(50 MB)"
|
: "DOCX 文件超过动态水印处理上限(100 MB)");
|
fileStorageService.download(fileInfo).outputStream(source);
|
return source.toByteArrayAndRelease();
|
}, payload);
|
response.setContentLengthLong(result.length);
|
response.getOutputStream().write(result);
|
} catch (DataException exception) {
|
if (!response.isCommitted()) response.reset();
|
throw exception;
|
} catch (Exception exception) {
|
if (!response.isCommitted()) response.reset();
|
throw new DataException("下载 DMS 文件失败:" + exception.getMessage());
|
}
|
}
|
|
private void streamPdf(DmsFileDownloadTicket payload, String accessTicket, HttpServletResponse response) {
|
try {
|
ActionResult<byte[]> result = pdfRenderer.render(
|
new DmsDownloadPdfRequest(payload.getFileVersionId(), accessTicket));
|
if (result == null || result.getCode() != 200 || result.getData() == null
|
|| result.getData().length == 0) {
|
throw new DataException(result == null || isBlank(result.getMsg())
|
? "DOCX 转换为 PDF 失败" : result.getMsg());
|
}
|
byte[] bytes = result.getData();
|
String fileName = payload.getFileName();
|
fileName = fileName.substring(0, fileName.lastIndexOf('.')) + ".pdf";
|
// Only commit download headers once conversion has completed successfully.
|
response.setContentType(MediaType.APPLICATION_PDF_VALUE);
|
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment()
|
.filename(fileName, StandardCharsets.UTF_8).build().toString());
|
response.setHeader(HttpHeaders.CACHE_CONTROL, "no-store");
|
response.setContentLengthLong(bytes.length);
|
response.getOutputStream().write(bytes);
|
} catch (DataException exception) {
|
throw exception;
|
} catch (Exception exception) {
|
throw new DataException("DOCX 转换为 PDF 下载失败,请稍后重试");
|
}
|
}
|
|
private boolean reusablePersistentPdf(DmsFileDownloadTicket payload) {
|
return (payload.getWatermarks() == null || payload.getWatermarks().isEmpty())
|
&& payload.getPdfSource() != null
|
&& payload.getPdfSource().getObjectKey() != null
|
&& payload.getPdfSource().getObjectKey().startsWith("dms-derived/pdf/")
|
&& !payload.getPdfSource().getObjectKey().contains("..")
|
&& payload.getPdfSource().getSizeBytes() != null
|
&& payload.getPdfSource().getSizeBytes() > 0;
|
}
|
|
private boolean streamPersistentPdf(DmsFileDownloadTicket payload, HttpServletResponse response) {
|
String key = payload.getPdfSource().getObjectKey();
|
int slash = key.lastIndexOf('/');
|
if (slash < 0 || slash == key.length() - 1) return false;
|
String original = payload.getFileName();
|
int dot = original.lastIndexOf('.');
|
String fileName = (dot > 0 ? original.substring(0, dot) : original) + ".pdf";
|
FileInfo fileInfo = new FileInfo()
|
.setPlatform(payload.getStoragePlatform())
|
.setBasePath(empty(payload.getBasePath()))
|
.setPath(key.substring(0, slash + 1))
|
.setFilename(key.substring(slash + 1))
|
.setOriginalFilename(fileName)
|
.setContentType(MediaType.APPLICATION_PDF_VALUE)
|
.setSize(payload.getPdfSource().getSizeBytes());
|
try {
|
if (!fileStorageService.exists(fileInfo)) {
|
reportInvalid(payload, "持久化 PDF 对象不存在");
|
return false;
|
}
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
fileStorageService.download(fileInfo).outputStream(output);
|
byte[] bytes = output.toByteArray();
|
if (bytes.length == 0 || bytes.length != payload.getPdfSource().getSizeBytes()) {
|
reportInvalid(payload, "持久化 PDF 对象大小不匹配");
|
return false;
|
}
|
response.setContentType(MediaType.APPLICATION_PDF_VALUE);
|
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment()
|
.filename(fileName, StandardCharsets.UTF_8).build().toString());
|
response.setHeader(HttpHeaders.CACHE_CONTROL, "no-store");
|
response.setContentLengthLong(bytes.length);
|
response.getOutputStream().write(bytes);
|
return true;
|
} catch (Exception exception) {
|
if (!response.isCommitted()) response.reset();
|
reportInvalid(payload, "持久化 PDF 对象读取失败");
|
return false;
|
}
|
}
|
|
private void reportInvalid(DmsFileDownloadTicket payload, String reason) {
|
if (recoveryReporter != null) recoveryReporter.report(payload, reason);
|
}
|
|
DmsFileDownloadTicket parse(String accessTicket) {
|
if (isBlank(accessTicket)) {
|
throw new DataException("缺少 DMS 下载票据");
|
}
|
try {
|
return DmsFileDownloadTicket.fromRaw(ticketReader.read(accessTicket));
|
} catch (RuntimeException exception) {
|
throw new DataException("DMS 下载票据无效或已过期");
|
}
|
}
|
|
private String empty(String value) {
|
return value == null ? "" : value;
|
}
|
|
private boolean isBlank(String value) {
|
return value == null || value.trim().isEmpty();
|
}
|
|
@FunctionalInterface
|
interface TicketReader {
|
Object read(String ticket);
|
}
|
}
|