package jnpf.fileStorage.dms;
|
|
import jakarta.servlet.http.HttpServletResponse;
|
import jnpf.dms.DmsTicketLifecycleApi;
|
import jnpf.dms.DmsTicketLifecycleChecks;
|
import jnpf.dmsEntity.preview.enums.DmsWatermarkTargetType;
|
import jnpf.dmsEntity.preview.model.DmsFilePreviewTicket;
|
import jnpf.dmsEntity.preview.model.DmsWatermarkSnapshot;
|
import jnpf.exception.DataException;
|
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.nio.charset.StandardCharsets;
|
import java.util.Collections;
|
import java.util.Objects;
|
import java.util.UUID;
|
import java.util.stream.Collectors;
|
|
@Service
|
public class DmsOnlyOfficeFileService {
|
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 DmsPersistentPdfRecoveryReporter recoveryReporter;
|
|
@Autowired
|
public DmsOnlyOfficeFileService(FileStorageService fileStorageService,
|
DmsFileWatermarkService watermarkService,
|
DmsPersistentPdfRecoveryReporter recoveryReporter, DmsTicketLifecycleApi lifecycleApi) {
|
this(fileStorageService, ticket -> DmsTicketLifecycleChecks.preview(TicketUtil.parseTicket(ticket), lifecycleApi, "jnpf-file"), watermarkService, recoveryReporter);
|
}
|
|
DmsOnlyOfficeFileService(FileStorageService fileStorageService, TicketReader ticketReader,
|
DmsFileWatermarkService watermarkService) {
|
this(fileStorageService, ticketReader, watermarkService, null);
|
}
|
|
DmsOnlyOfficeFileService(FileStorageService fileStorageService, TicketReader ticketReader,
|
DmsFileWatermarkService watermarkService,
|
DmsPersistentPdfRecoveryReporter recoveryReporter) {
|
this.fileStorageService = fileStorageService;
|
this.ticketReader = ticketReader;
|
this.watermarkService = watermarkService;
|
this.recoveryReporter = recoveryReporter;
|
}
|
|
public void stream(UUID fileVersionId, String accessTicket, HttpServletResponse response) {
|
stream(fileVersionId, accessTicket, false, response);
|
}
|
|
public void stream(UUID fileVersionId, String accessTicket, boolean printBase,
|
HttpServletResponse response) {
|
DmsFilePreviewTicket payload = parse(accessTicket);
|
if (!fileVersionId.equals(payload.getFileVersionId())) {
|
throw new DataException("DMS 预览票据与文件版本不匹配");
|
}
|
if (!DMS_STORAGE_PLATFORM.equals(payload.getStoragePlatform())) {
|
throw new DataException("DMS 预览票据的存储平台无效");
|
}
|
if (printBase) {
|
if (!DmsFilePreviewTicket.PRINT_BASE_SCOPE.equals(payload.getScope())) {
|
throw new DataException("只有内部打印基础票据可以读取基础源文件");
|
}
|
DmsFilePreviewTicket base = new DmsFilePreviewTicket();
|
org.springframework.beans.BeanUtils.copyProperties(payload, base);
|
base.setWatermarks((payload.getWatermarks() == null
|
? Collections.<DmsWatermarkSnapshot>emptyList()
|
: payload.getWatermarks()).stream()
|
.filter(mark -> mark != null && !Boolean.TRUE.equals(mark.getCustomBarcode()))
|
.collect(Collectors.toList()));
|
payload = base;
|
} else if (DmsFilePreviewTicket.PRINT_BASE_SCOPE.equals(payload.getScope())) {
|
throw new DataException("内部打印基础票据只能读取基础源文件");
|
}
|
|
if (printBase && reusablePersistentPdf(payload)) {
|
DmsFilePreviewTicket persistent = persistentPdf(payload);
|
if (persistentPdfExists(persistent)) {
|
payload = persistent;
|
} else {
|
reportInvalid(payload, "持久化 PDF 对象不存在或检查失败");
|
}
|
}
|
|
MinioFileStorage storage = fileStorageService.getFileStorageVerify(payload.getStoragePlatform());
|
if (!Objects.equals(storage.getBucketName(), payload.getBucketName())) {
|
throw new DataException("DMS 预览票据的存储桶与当前配置不匹配");
|
}
|
|
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(isBlank(payload.getMimeType())
|
? MediaType.APPLICATION_OCTET_STREAM_VALUE : payload.getMimeType());
|
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.inline()
|
.filename(payload.getFileName(), StandardCharsets.UTF_8).build().toString());
|
response.setHeader(HttpHeaders.CACHE_CONTROL, "private, no-store");
|
response.setHeader("X-Content-Type-Options", "nosniff");
|
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 reportInvalid(DmsFilePreviewTicket ticket, String reason) {
|
if (recoveryReporter != null) recoveryReporter.report(ticket, reason);
|
}
|
|
DmsFilePreviewTicket parse(String accessTicket) {
|
if (isBlank(accessTicket)) {
|
throw new DataException("缺少 DMS 预览票据");
|
}
|
try {
|
return DmsFilePreviewTicket.fromRaw(ticketReader.read(accessTicket));
|
} catch (RuntimeException exception) {
|
throw new DataException("DMS 预览票据无效或已过期");
|
}
|
}
|
|
private boolean reusablePersistentPdf(DmsFilePreviewTicket ticket) {
|
if (ticket.getPdfSource() == null || ticket.getPdfSource().getObjectKey() == null
|
|| !ticket.getPdfSource().getObjectKey().startsWith("dms-derived/pdf/")
|
|| ticket.getPdfSource().getObjectKey().contains("..")) return false;
|
return ticket.getWatermarks() == null || ticket.getWatermarks().stream()
|
.allMatch(mark -> mark != null && Boolean.TRUE.equals(mark.getCustomBarcode()));
|
}
|
|
private DmsFilePreviewTicket persistentPdf(DmsFilePreviewTicket ticket) {
|
String key = ticket.getPdfSource().getObjectKey();
|
int slash = key.lastIndexOf('/');
|
if (slash < 0 || slash == key.length() - 1) throw new DataException("持久化 PDF 对象键无效");
|
DmsFilePreviewTicket result = new DmsFilePreviewTicket();
|
org.springframework.beans.BeanUtils.copyProperties(ticket, result);
|
result.setFileName(pdfName(ticket.getFileName()));
|
result.setMimeType(MediaType.APPLICATION_PDF_VALUE);
|
result.setSizeBytes(ticket.getPdfSource().getSizeBytes());
|
result.setObjectPath(key.substring(0, slash + 1));
|
result.setObjectName(key.substring(slash + 1));
|
result.setPdfSource(null);
|
result.setWatermarks(Collections.emptyList());
|
return result;
|
}
|
|
private boolean persistentPdfExists(DmsFilePreviewTicket ticket) {
|
FileInfo file = new FileInfo()
|
.setPlatform(ticket.getStoragePlatform())
|
.setBasePath(empty(ticket.getBasePath()))
|
.setPath(ticket.getObjectPath())
|
.setFilename(ticket.getObjectName());
|
try {
|
return fileStorageService.exists(file);
|
} catch (RuntimeException exception) {
|
return false;
|
}
|
}
|
|
private String pdfName(String fileName) {
|
int dot = fileName == null ? -1 : fileName.lastIndexOf('.');
|
return (dot > 0 ? fileName.substring(0, dot) : fileName) + ".pdf";
|
}
|
|
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);
|
}
|
}
|