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; } }