hejianhao
2026-08-18 3df055ea1038e0bbcbbbf80ccbeea43c82952932
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package com.tcm.training.admin.upload;
 
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
 
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
 
/**
 * 安装包本地存储服务。
 */
@Service
public class LocalAppPackageStorageService {
 
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd");
 
    private final LocalUploadProperties localUploadProperties;
 
    public LocalAppPackageStorageService(LocalUploadProperties localUploadProperties) {
        this.localUploadProperties = localUploadProperties;
    }
 
    /**
     * 将安装包写入服务器本地目录,并返回可下载地址。
     */
    public UploadFileResult store(MultipartFile file, String platform) throws IOException {
        validateConfig();
        String originalName = file.getOriginalFilename();
        String extension = resolveExtension(originalName);
        String relativePath = buildRelativePath(platform, extension);
        Path targetPath = resolveTargetPath(relativePath);
        Files.createDirectories(targetPath.getParent());
        file.transferTo(targetPath.toFile());
        return new UploadFileResult(buildPublicUrl(relativePath), originalName, relativePath);
    }
 
    /**
     * 解析安装包最终写入路径。
     */
    Path resolveTargetPath(String relativePath) {
        Path uploadRoot = resolveUploadRoot();
        String normalizedRelativePath = relativePath.replace("/", java.io.File.separator);
        return uploadRoot.resolve(normalizedRelativePath).normalize();
    }
 
    /**
     * 生成对外下载地址。
     */
    String buildPublicUrl(String relativePath) {
        String baseUrl = trimTrailingSlash(localUploadProperties.getPublicBaseUrl());
        String pathPrefix = ensureLeadingSlash(trimTrailingSlash(localUploadProperties.getPathPrefix()));
        return baseUrl + pathPrefix + "/" + relativePath;
    }
 
    /**
     * 组装安装包相对路径。
     */
    String buildRelativePath(String platform, String extension) {
        String datePath = LocalDate.now().format(DATE_FORMATTER);
        String fileName = UUID.randomUUID().toString().replace("-", "") + extension;
        return normalizePackageDir() + "/" + platform.toLowerCase() + "/" + datePath + "/" + fileName;
    }
 
    private Path resolveUploadRoot() {
        String uploadDir = localUploadProperties.getDir();
        if (!StringUtils.hasText(uploadDir)) {
            throw new IllegalArgumentException("本地上传根目录未配置");
        }
        Path uploadRoot = Paths.get(uploadDir).toAbsolutePath().normalize();
        Path storageRoot = uploadRoot.resolve(normalizePackageDir()).normalize();
        if (!storageRoot.startsWith(uploadRoot)) {
            throw new IllegalArgumentException("安装包存储目录必须位于上传根目录下");
        }
        return uploadRoot;
    }
 
    private void validateConfig() {
        if (!StringUtils.hasText(localUploadProperties.getPublicBaseUrl())) {
            throw new IllegalArgumentException("安装包公网下载地址未配置");
        }
        normalizePackageDir();
        resolveUploadRoot();
    }
 
    private String normalizePackageDir() {
        String appPackageDir = localUploadProperties.getAppPackageDir();
        if (!StringUtils.hasText(appPackageDir)) {
            throw new IllegalArgumentException("安装包存储目录未配置");
        }
        String normalized = trimSlash(appPackageDir.replace("\\", "/"));
        if (!StringUtils.hasText(normalized)) {
            throw new IllegalArgumentException("安装包存储目录未配置");
        }
        if (normalized.startsWith("..") || normalized.contains("/../")) {
            throw new IllegalArgumentException("安装包存储目录不合法");
        }
        return normalized;
    }
 
    private String resolveExtension(String originalName) {
        if (!StringUtils.hasText(originalName) || !originalName.contains(".")) {
            return "";
        }
        return originalName.substring(originalName.lastIndexOf(".")).toLowerCase();
    }
 
    private String trimTrailingSlash(String value) {
        String normalized = value == null ? "" : value.trim();
        while (normalized.endsWith("/")) {
            normalized = normalized.substring(0, normalized.length() - 1);
        }
        return normalized;
    }
 
    private String ensureLeadingSlash(String value) {
        return value.startsWith("/") ? value : "/" + value;
    }
 
    private String trimSlash(String value) {
        String normalized = value == null ? "" : value.trim();
        while (normalized.startsWith("/")) {
            normalized = normalized.substring(1);
        }
        while (normalized.endsWith("/")) {
            normalized = normalized.substring(0, normalized.length() - 1);
        }
        return normalized;
    }
}