package com.vincent.rsf.server.system.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.vincent.rsf.framework.common.R; import com.vincent.rsf.framework.exception.CoolException; import com.vincent.rsf.server.common.utils.FileServerUtil; import com.vincent.rsf.server.system.entity.Config; import com.vincent.rsf.server.system.enums.StatusType; import com.vincent.rsf.server.system.service.ConfigService; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; @RestController public class ProjectLogoController extends BaseController { private static final String PROJECT_LOGO_FLAG = "PROJECT_LOGO"; private static final String PROJECT_COPYRIGHT_FLAG = "PROJECT_COPYRIGHT"; private static final Set ALLOWED_IMAGE_TYPES = Set.of( "image/png", "image/jpeg", "image/jpg", "image/gif", "image/bmp", "image/webp", "image/svg+xml", "image/x-icon", "image/vnd.microsoft.icon" ); private static final Set ALLOWED_IMAGE_EXTENSIONS = Set.of( ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".svg", ".ico" ); private static final Path PROJECT_LOGO_ROOT = Paths.get( System.getProperty("user.home"), ".rsf", "uploads", "logo" ).toAbsolutePath().normalize(); @Autowired private ConfigService configService; @GetMapping("/config/public/project-logo") public R getProjectLogoConfig() { return getEnabledConfigByFlag(PROJECT_LOGO_FLAG); } @GetMapping("/config/public/project-copyright") public R getProjectCopyrightConfig() { return getEnabledConfigByFlag(PROJECT_COPYRIGHT_FLAG); } private R getEnabledConfigByFlag(String flag) { List configs = configService.list(new LambdaQueryWrapper() .eq(Config::getFlag, flag) .eq(Config::getStatus, StatusType.ENABLE.val) .last("limit 1")); return R.ok().add(configs.stream().findFirst().orElse(null)); } @PreAuthorize("hasAnyAuthority('system:config:save','system:config:update')") @PostMapping("/config/logo/upload") public R uploadProjectLogo(@RequestParam("file") MultipartFile file, HttpServletRequest request) throws IOException { validateImageFile(file); File savedFile = FileServerUtil.upload(file, PROJECT_LOGO_ROOT.toString(), true); String relativePath = PROJECT_LOGO_ROOT.relativize(savedFile.toPath().toAbsolutePath().normalize()) .toString() .replace(File.separatorChar, '/'); String url = request.getContextPath() + "/file/logo?path=" + relativePath; Map payload = new HashMap<>(); payload.put("name", savedFile.getName()); payload.put("path", relativePath); payload.put("url", url); return R.ok().add(payload); } @GetMapping("/file/logo") public void previewProjectLogo(@RequestParam("path") String path, HttpServletRequest request, HttpServletResponse response) throws IOException { File file = resolveLogoPath(path).toFile(); if (!file.exists() || !file.isFile()) { response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } response.setContentType(resolveImageContentType(file.getName())); response.setHeader("Cache-Control", "public, max-age=86400"); response.setContentLengthLong(file.length()); try (FileInputStream inputStream = new FileInputStream(file); OutputStream outputStream = response.getOutputStream()) { inputStream.transferTo(outputStream); outputStream.flush(); } } private void validateImageFile(MultipartFile file) { if (file == null || file.isEmpty()) { throw new CoolException("上传文件不能为空"); } if (!isAllowedImageType(file.getContentType()) && !isAllowedImageExtension(file.getOriginalFilename())) { throw new CoolException("只支持上传图片文件"); } } private boolean isAllowedImageType(String contentType) { if (!StringUtils.hasText(contentType)) { return false; } return ALLOWED_IMAGE_TYPES.contains(contentType.toLowerCase(Locale.ROOT)); } private boolean isAllowedImageExtension(String fileName) { if (!StringUtils.hasText(fileName) || !fileName.contains(".")) { return false; } String extension = fileName.substring(fileName.lastIndexOf(".")).toLowerCase(Locale.ROOT); return ALLOWED_IMAGE_EXTENSIONS.contains(extension); } private Path resolveLogoPath(String relativePath) { if (!StringUtils.hasText(relativePath)) { throw new CoolException("文件路径不能为空"); } Path resolvedPath = PROJECT_LOGO_ROOT.resolve(relativePath).normalize(); if (!resolvedPath.startsWith(PROJECT_LOGO_ROOT)) { throw new CoolException("非法文件路径"); } return resolvedPath; } private String resolveImageContentType(String fileName) { String normalizedName = StringUtils.hasText(fileName) ? fileName.toLowerCase(Locale.ROOT) : ""; if (normalizedName.endsWith(".png")) { return "image/png"; } if (normalizedName.endsWith(".jpg") || normalizedName.endsWith(".jpeg")) { return "image/jpeg"; } if (normalizedName.endsWith(".gif")) { return "image/gif"; } if (normalizedName.endsWith(".bmp")) { return "image/bmp"; } if (normalizedName.endsWith(".webp")) { return "image/webp"; } if (normalizedName.endsWith(".svg")) { return "image/svg+xml"; } if (normalizedName.endsWith(".ico")) { return "image/x-icon"; } return "application/octet-stream"; } }