刘光辉
8 小时以前 06f314545ac3514d7ce28939920866ae8188d31c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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); }
    }
}