package jnpf.bizcommon.onlyoffice.service.impl;
|
|
import cn.hutool.core.net.URLEncodeUtil;
|
import cn.hutool.crypto.digest.DigestUtil;
|
import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeProperties;
|
import jnpf.constant.GlobalConst;
|
import jnpf.exception.DataException;
|
import jnpf.file.FileApi;
|
import jnpf.file.FileUploadApi;
|
import jnpf.util.UploaderUtil;
|
import lombok.RequiredArgsConstructor;
|
import org.dromara.x.file.storage.core.FileInfo;
|
import org.springframework.stereotype.Service;
|
import org.springframework.web.multipart.MultipartFile;
|
|
import java.io.ByteArrayInputStream;
|
import java.io.File;
|
import java.io.IOException;
|
import java.io.InputStream;
|
import java.nio.file.Files;
|
import java.util.LinkedHashMap;
|
import java.util.Map;
|
|
/** 所有编辑产物另存至专用目录;不调用 replaceFile,不触及源文件。 */
|
@Service
|
@RequiredArgsConstructor
|
public class UploadEditStorage {
|
private final OnlyOfficeProperties properties;
|
private final FileApi fileApi;
|
private final FileUploadApi fileUploadApi;
|
|
public Map<String, Object> save(String tenantId, String leaseId, String fileName, byte[] content) {
|
if (content == null || content.length == 0) throw new DataException("编辑产物为空");
|
String prefix = relativeDirectory(properties.getUploadFile().getTempDirectory(), tenantId, leaseId);
|
String base = fileApi.getPath("annex");
|
if (base == null || base.isBlank()) throw new DataException("无法解析附件存储目录");
|
String path = base.replace('\\', '/').replaceAll("/+$", "") + "/" + prefix;
|
FileInfo stored = fileUploadApi.uploadFile(new Content(fileName, content), path, fileName);
|
if (stored == null || stored.getFilename() == null || stored.getFilename().isBlank()) {
|
throw new DataException("在线编辑临时文件另存失败");
|
}
|
if (stored.getFilename().contains("/") || stored.getFilename().contains("\\") || stored.getFilename().contains(",")) {
|
throw new DataException("文件服务返回了无效的编辑产物标识");
|
}
|
// 平台原生自定义上传目录以逗号编码路径;查看、下载仍走原附件接口。
|
String fileId = prefix.replace('/', ',') + stored.getFilename();
|
Map<String, Object> file = new LinkedHashMap<>();
|
file.put("fileId", fileId);
|
file.put("contentSavedAt", System.currentTimeMillis());
|
file.put("name", fileName);
|
file.put("fileSize", stored.getSize());
|
file.put("fileExtension", fileName.toLowerCase(java.util.Locale.ROOT).endsWith(".doc") ? "doc" : "docx");
|
file.put("url", UploaderUtil.uploaderFile(fileId + "#annex") + "&name="
|
+ URLEncodeUtil.encode(fileName, GlobalConst.DEFAULT_CHARSET));
|
return file;
|
}
|
|
static String relativeDirectory(String directory, String tenantId, String leaseId) {
|
if (directory == null || !directory.matches("[A-Za-z0-9][A-Za-z0-9_-]{0,63}")) {
|
throw new DataException("在线编辑临时目录必须是单个安全目录名");
|
}
|
if (tenantId == null || tenantId.isBlank() || leaseId == null || !leaseId.matches("(?:review_)?[a-f0-9]{32}")) {
|
throw new DataException("在线编辑临时文件缺少有效归属");
|
}
|
return directory + "/" + DigestUtil.sha256Hex(tenantId).substring(0, 32) + "/" + leaseId + "/";
|
}
|
|
private static class Content implements MultipartFile {
|
private final String fileName;
|
private final byte[] content;
|
Content(String fileName, byte[] content) { this.fileName = fileName; this.content = content; }
|
@Override public String getName() { return "multipartFile"; }
|
@Override public String getOriginalFilename() { return fileName; }
|
@Override public String getContentType() {
|
return fileName.toLowerCase(java.util.Locale.ROOT).endsWith(".doc") ? "application/msword"
|
: "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
}
|
@Override public boolean isEmpty() { return content.length == 0; }
|
@Override public long getSize() { return content.length; }
|
@Override public byte[] getBytes() { return content; }
|
@Override public InputStream getInputStream() { return new ByteArrayInputStream(content); }
|
@Override public void transferTo(File dest) throws IOException { Files.write(dest.toPath(), content); }
|
}
|
}
|