Commit 4ae3f6b2c9883978837638c14e3d18419819eeb0

Authored by YunaiV
1 parent e4401175

fix(infra): 加强文件上传路径安全校验

- 为 App 文件上传入口启用参数校验,保留 PermitAll
- 统一校验文件名、目录和相对路径,拒绝目录穿越路径
- LocalFileClient 限制最终文件路径必须位于 basePath 内
- 补充文件路径安全相关单元测试
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java
1 package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file; 1 package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file;
2 2
3 -import cn.hutool.core.util.StrUtil; 3 +import cn.iocoder.yudao.module.infra.framework.file.core.utils.FilePathUtils;
4 import com.fasterxml.jackson.annotation.JsonIgnore; 4 import com.fasterxml.jackson.annotation.JsonIgnore;
5 import io.swagger.v3.oas.annotations.media.Schema; 5 import io.swagger.v3.oas.annotations.media.Schema;
6 import jakarta.validation.constraints.AssertTrue; 6 import jakarta.validation.constraints.AssertTrue;
@@ -26,10 +26,7 @@ public class FileUploadReqVO { @@ -26,10 +26,7 @@ public class FileUploadReqVO {
26 } 26 }
27 27
28 public static boolean isDirectoryValid(String directory) { 28 public static boolean isDirectoryValid(String directory) {
29 - // 1. 不能包含 .. 防止目录穿越  
30 - // 2. 不能以 / 或 \ 开头,防止上传到根目录  
31 - return !StrUtil.contains(directory, "..")  
32 - && !StrUtil.startWithAny(directory, "/", "\\"); 29 + return FilePathUtils.isDirectoryValid(directory);
33 } 30 }
34 31
35 } 32 }
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java
@@ -36,7 +36,7 @@ public class AppFileController { @@ -36,7 +36,7 @@ public class AppFileController {
36 @Parameter(name = "file", description = "文件附件", required = true, 36 @Parameter(name = "file", description = "文件附件", required = true,
37 schema = @Schema(type = "string", format = "binary")) 37 schema = @Schema(type = "string", format = "binary"))
38 @PermitAll 38 @PermitAll
39 - public CommonResult<String> uploadFile(AppFileUploadReqVO uploadReqVO) throws Exception { 39 + public CommonResult<String> uploadFile(@Valid AppFileUploadReqVO uploadReqVO) throws Exception {
40 MultipartFile file = uploadReqVO.getFile(); 40 MultipartFile file = uploadReqVO.getFile();
41 byte[] content = IoUtil.readBytes(file.getInputStream()); 41 byte[] content = IoUtil.readBytes(file.getInputStream());
42 return success(fileService.createFile(content, file.getOriginalFilename(), 42 return success(fileService.createFile(content, file.getOriginalFilename(),
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java
@@ -33,6 +33,7 @@ public interface ErrorCodeConstants { @@ -33,6 +33,7 @@ public interface ErrorCodeConstants {
33 ErrorCode FILE_PATH_EXISTS = new ErrorCode(1_001_003_000, "文件路径已存在"); 33 ErrorCode FILE_PATH_EXISTS = new ErrorCode(1_001_003_000, "文件路径已存在");
34 ErrorCode FILE_NOT_EXISTS = new ErrorCode(1_001_003_001, "文件不存在"); 34 ErrorCode FILE_NOT_EXISTS = new ErrorCode(1_001_003_001, "文件不存在");
35 ErrorCode FILE_IS_EMPTY = new ErrorCode(1_001_003_002, "文件为空"); 35 ErrorCode FILE_IS_EMPTY = new ErrorCode(1_001_003_002, "文件为空");
  36 + ErrorCode FILE_PATH_INVALID = new ErrorCode(1_001_003_003, "文件路径不正确");
36 37
37 // ========== 代码生成器 1-001-004-000 ========== 38 // ========== 代码生成器 1-001-004-000 ==========
38 ErrorCode CODEGEN_TABLE_EXISTS = new ErrorCode(1_001_004_002, "表定义已经存在"); 39 ErrorCode CODEGEN_TABLE_EXISTS = new ErrorCode(1_001_004_002, "表定义已经存在");
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java
@@ -3,8 +3,12 @@ package cn.iocoder.yudao.module.infra.framework.file.core.client.local; @@ -3,8 +3,12 @@ package cn.iocoder.yudao.module.infra.framework.file.core.client.local;
3 import cn.hutool.core.io.FileUtil; 3 import cn.hutool.core.io.FileUtil;
4 import cn.hutool.core.io.IORuntimeException; 4 import cn.hutool.core.io.IORuntimeException;
5 import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient; 5 import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient;
  6 +import cn.iocoder.yudao.module.infra.framework.file.core.utils.FilePathUtils;
6 7
7 -import java.io.File; 8 +import java.nio.file.Path;
  9 +
  10 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  11 +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_PATH_INVALID;
8 12
9 /** 13 /**
10 * 本地文件客户端 14 * 本地文件客户端
@@ -50,7 +54,13 @@ public class LocalFileClient extends AbstractFileClient&lt;LocalFileClientConfig&gt; { @@ -50,7 +54,13 @@ public class LocalFileClient extends AbstractFileClient&lt;LocalFileClientConfig&gt; {
50 } 54 }
51 55
52 private String getFilePath(String path) { 56 private String getFilePath(String path) {
53 - return config.getBasePath() + File.separator + path; 57 + FilePathUtils.validatePath(path);
  58 + Path basePath = Path.of(config.getBasePath()).toAbsolutePath().normalize();
  59 + Path filePath = basePath.resolve(path).normalize();
  60 + if (!filePath.startsWith(basePath)) {
  61 + throw exception(FILE_PATH_INVALID);
  62 + }
  63 + return filePath.toString();
54 } 64 }
55 65
56 } 66 }
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FilePathUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.framework.file.core.utils;
  2 +
  3 +import cn.hutool.core.io.FileUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +
  6 +import java.nio.file.InvalidPathException;
  7 +import java.nio.file.Path;
  8 +
  9 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  10 +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_PATH_INVALID;
  11 +
  12 +/**
  13 + * 文件路径工具类
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +public class FilePathUtils {
  18 +
  19 + private FilePathUtils() {
  20 + }
  21 +
  22 + /**
  23 + * 校验文件名是否合法,禁止携带目录路径。
  24 + *
  25 + * @param name 文件名
  26 + * @return 文件名
  27 + */
  28 + public static String validateFileName(String name) {
  29 + if (StrUtil.isEmpty(name)) {
  30 + return name;
  31 + }
  32 + if (!isPathValid(name) || StrUtil.contains(name, StrUtil.SLASH) || !StrUtil.equals(name, FileUtil.getName(name))) {
  33 + throw exception(FILE_PATH_INVALID);
  34 + }
  35 + return name;
  36 + }
  37 +
  38 + /**
  39 + * 校验文件目录是否合法。
  40 + *
  41 + * @param directory 文件目录,允许为空
  42 + * @return 是否合法
  43 + */
  44 + public static boolean isDirectoryValid(String directory) {
  45 + return StrUtil.isEmpty(directory) || isPathValid(directory);
  46 + }
  47 +
  48 + /**
  49 + * 校验文件目录是否合法,不合法时抛出业务异常。
  50 + *
  51 + * @param directory 文件目录,允许为空
  52 + */
  53 + public static void validateDirectory(String directory) {
  54 + if (!isDirectoryValid(directory)) {
  55 + throw exception(FILE_PATH_INVALID);
  56 + }
  57 + }
  58 +
  59 + /**
  60 + * 校验文件相对路径是否合法,不合法时抛出业务异常。
  61 + *
  62 + * @param path 文件相对路径
  63 + */
  64 + public static void validatePath(String path) {
  65 + if (StrUtil.isEmpty(path) || !isPathValid(path)) {
  66 + throw exception(FILE_PATH_INVALID);
  67 + }
  68 + }
  69 +
  70 + /**
  71 + * 校验路径是否为安全的相对路径,禁止绝对路径、Windows 盘符、反斜杠、空路径段和目录穿越。
  72 + *
  73 + * @param path 路径
  74 + * @return 是否合法
  75 + */
  76 + private static boolean isPathValid(String path) {
  77 + // 不能以 / 或 \ 开头,避免传入绝对路径
  78 + if (StrUtil.startWithAny(path, StrUtil.SLASH, "\\")) {
  79 + return false;
  80 + }
  81 + // 不能包含反斜杠或空字符,避免绕过不同系统的路径解析
  82 + if (StrUtil.contains(path, "\\") || path.indexOf('\0') >= 0) {
  83 + return false;
  84 + }
  85 + // 不能是 Windows 盘符路径,例如 C:/test.jpg
  86 + if (path.length() >= 2 && Character.isLetter(path.charAt(0)) && path.charAt(1) == ':') {
  87 + return false;
  88 + }
  89 + try {
  90 + // 使用 JDK Path 再兜底判断一次绝对路径
  91 + if (Path.of(path).isAbsolute()) {
  92 + return false;
  93 + }
  94 + } catch (InvalidPathException ex) {
  95 + return false;
  96 + }
  97 + // 不能包含空路径段、当前目录或上级目录,避免目录穿越
  98 + for (String segment : path.split(StrUtil.SLASH, -1)) {
  99 + if (StrUtil.isEmpty(segment) || ".".equals(segment) || "..".equals(segment)) {
  100 + return false;
  101 + }
  102 + }
  103 + return true;
  104 + }
  105 +
  106 +}
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java
@@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresigned @@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresigned
15 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; 15 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
16 import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; 16 import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper;
17 import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient; 17 import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
  18 +import cn.iocoder.yudao.module.infra.framework.file.core.utils.FilePathUtils;
18 import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils; 19 import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils;
19 import com.google.common.annotations.VisibleForTesting; 20 import com.google.common.annotations.VisibleForTesting;
20 import jakarta.annotation.Resource; 21 import jakarta.annotation.Resource;
@@ -70,11 +71,14 @@ public class FileServiceImpl implements FileService { @@ -70,11 +71,14 @@ public class FileServiceImpl implements FileService {
70 @Override 71 @Override
71 @SneakyThrows 72 @SneakyThrows
72 public String createFile(byte[] content, String name, String directory, String type) { 73 public String createFile(byte[] content, String name, String directory, String type) {
73 - // 1.1 处理 type 为空的情况 74 + // 1.1 处理 name 的合法性,禁止携带目录路径
  75 + name = FilePathUtils.validateFileName(name);
  76 +
  77 + // 1.2.1 处理 type 为空的情况
74 if (StrUtil.isEmpty(type)) { 78 if (StrUtil.isEmpty(type)) {
75 type = FileTypeUtils.getMineType(content, name); 79 type = FileTypeUtils.getMineType(content, name);
76 } 80 }
77 - // 1.2 处理 name 为空的情况 81 + // 1.2.2 处理 name 为空的情况
78 if (StrUtil.isEmpty(name)) { 82 if (StrUtil.isEmpty(name)) {
79 name = DigestUtil.sha256Hex(content); 83 name = DigestUtil.sha256Hex(content);
80 } 84 }
@@ -102,7 +106,11 @@ public class FileServiceImpl implements FileService { @@ -102,7 +106,11 @@ public class FileServiceImpl implements FileService {
102 106
103 @VisibleForTesting 107 @VisibleForTesting
104 String generateUploadPath(String name, String directory) { 108 String generateUploadPath(String name, String directory) {
105 - // 1. 生成前缀、后缀 109 + // 1.1 处理 name 和 directory 的合法性
  110 + name = FilePathUtils.validateFileName(name);
  111 + FilePathUtils.validatePath(name);
  112 + FilePathUtils.validateDirectory(directory);
  113 + // 1.2 生成前缀、后缀
106 String prefix = null; 114 String prefix = null;
107 if (PATH_PREFIX_DATE_ENABLE) { 115 if (PATH_PREFIX_DATE_ENABLE) {
108 prefix = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), PURE_DATE_PATTERN); 116 prefix = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), PURE_DATE_PATTERN);
@@ -159,7 +167,13 @@ public class FileServiceImpl implements FileService { @@ -159,7 +167,13 @@ public class FileServiceImpl implements FileService {
159 167
160 @Override 168 @Override
161 public Long createFile(FileCreateReqVO createReqVO) { 169 public Long createFile(FileCreateReqVO createReqVO) {
  170 + // 1.1 校验参数的合法性
  171 + FilePathUtils.validatePath(createReqVO.getPath());
  172 + createReqVO.setName(FilePathUtils.validateFileName(createReqVO.getName()));
  173 + // 1.2 处理 URL 的合法性,移除 URL 中的查询参数(例如签名参数),保证 URL 的唯一性
162 createReqVO.setUrl(HttpUtils.removeUrlQuery(createReqVO.getUrl())); // 目的:移除私有桶情况下,URL 的签名参数 174 createReqVO.setUrl(HttpUtils.removeUrlQuery(createReqVO.getUrl())); // 目的:移除私有桶情况下,URL 的签名参数
  175 +
  176 + // 2. 保存到数据库
163 FileDO file = BeanUtils.toBean(createReqVO, FileDO.class); 177 FileDO file = BeanUtils.toBean(createReqVO, FileDO.class);
164 fileMapper.insert(file); 178 fileMapper.insert(file);
165 return file.getId(); 179 return file.getId();
@@ -172,15 +186,17 @@ public class FileServiceImpl implements FileService { @@ -172,15 +186,17 @@ public class FileServiceImpl implements FileService {
172 186
173 @Override 187 @Override
174 public void deleteFile(Long id) throws Exception { 188 public void deleteFile(Long id) throws Exception {
175 - // 校验存在 189 + // 1.1 校验存在
176 FileDO file = validateFileExists(id); 190 FileDO file = validateFileExists(id);
  191 + // 1.2 校验路径合法性,避免误删文件存储器中的其他文件
  192 + FilePathUtils.validatePath(file.getPath());
177 193
178 - // 从文件存储器中删除 194 + // 2.1 从文件存储器中删除
179 FileClient client = fileConfigService.getFileClient(file.getConfigId()); 195 FileClient client = fileConfigService.getFileClient(file.getConfigId());
180 Assert.notNull(client, "客户端({}) 不能为空", file.getConfigId()); 196 Assert.notNull(client, "客户端({}) 不能为空", file.getConfigId());
181 client.delete(file.getPath()); 197 client.delete(file.getPath());
182 198
183 - // 删除记录 199 + // 2.2 删除记录
184 fileMapper.deleteById(id); 200 fileMapper.deleteById(id);
185 } 201 }
186 202
@@ -190,6 +206,7 @@ public class FileServiceImpl implements FileService { @@ -190,6 +206,7 @@ public class FileServiceImpl implements FileService {
190 // 删除文件 206 // 删除文件
191 List<FileDO> files = fileMapper.selectByIds(ids); 207 List<FileDO> files = fileMapper.selectByIds(ids);
192 for (FileDO file : files) { 208 for (FileDO file : files) {
  209 + FilePathUtils.validatePath(file.getPath());
193 // 获取客户端 210 // 获取客户端
194 FileClient client = fileConfigService.getFileClient(file.getConfigId()); 211 FileClient client = fileConfigService.getFileClient(file.getConfigId());
195 Assert.notNull(client, "客户端({}) 不能为空", file.getPath()); 212 Assert.notNull(client, "客户端({}) 不能为空", file.getPath());
@@ -211,8 +228,13 @@ public class FileServiceImpl implements FileService { @@ -211,8 +228,13 @@ public class FileServiceImpl implements FileService {
211 228
212 @Override 229 @Override
213 public byte[] getFileContent(Long configId, String path) throws Exception { 230 public byte[] getFileContent(Long configId, String path) throws Exception {
  231 + // 1. 校验路径合法性
  232 + FilePathUtils.validatePath(path);
  233 +
  234 + // 2.1 获取客户端
214 FileClient client = fileConfigService.getFileClient(configId); 235 FileClient client = fileConfigService.getFileClient(configId);
215 Assert.notNull(client, "客户端({}) 不能为空", configId); 236 Assert.notNull(client, "客户端({}) 不能为空", configId);
  237 + // 2.2 获取文件内容
216 return client.getContent(path); 238 return client.getContent(path);
217 } 239 }
218 240
yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/local/LocalFileClientTest.java
1 package cn.iocoder.yudao.module.infra.framework.file.core.local; 1 package cn.iocoder.yudao.module.infra.framework.file.core.local;
2 2
  3 +import cn.hutool.core.io.FileUtil;
3 import cn.hutool.core.io.resource.ResourceUtil; 4 import cn.hutool.core.io.resource.ResourceUtil;
4 import cn.hutool.core.util.IdUtil; 5 import cn.hutool.core.util.IdUtil;
  6 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
5 import cn.iocoder.yudao.module.infra.framework.file.core.client.local.LocalFileClient; 7 import cn.iocoder.yudao.module.infra.framework.file.core.client.local.LocalFileClient;
6 import cn.iocoder.yudao.module.infra.framework.file.core.client.local.LocalFileClientConfig; 8 import cn.iocoder.yudao.module.infra.framework.file.core.client.local.LocalFileClientConfig;
7 import org.junit.jupiter.api.Disabled; 9 import org.junit.jupiter.api.Disabled;
8 import org.junit.jupiter.api.Test; 10 import org.junit.jupiter.api.Test;
  11 +import org.junit.jupiter.api.io.TempDir;
  12 +
  13 +import java.io.File;
  14 +import java.nio.charset.StandardCharsets;
9 15
10 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; 16 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
  17 +import static org.junit.jupiter.api.Assertions.*;
11 18
12 public class LocalFileClientTest { 19 public class LocalFileClientTest {
13 20
  21 + @TempDir
  22 + public File tempDir;
  23 +
  24 + @Test
  25 + public void testUpload_success() {
  26 + // 准备参数
  27 + LocalFileClient client = createClient();
  28 + byte[] content = "test".getBytes(StandardCharsets.UTF_8);
  29 + String path = "avatar/test.txt";
  30 +
  31 + // 调用
  32 + String url = client.upload(content, path, "text/plain");
  33 +
  34 + // 断言
  35 + assertEquals("http://127.0.0.1:48080/admin-api/infra/file/0/get/avatar/test.txt", url);
  36 + assertArrayEquals(content, FileUtil.readBytes(new File(tempDir, path)));
  37 + assertArrayEquals(content, client.getContent(path));
  38 +
  39 + // 删除
  40 + client.delete(path);
  41 + assertFalse(FileUtil.exist(new File(tempDir, path)));
  42 + }
  43 +
  44 + @Test
  45 + public void testUpload_pathInvalid() {
  46 + // 准备参数
  47 + LocalFileClient client = createClient();
  48 + byte[] content = "test".getBytes(StandardCharsets.UTF_8);
  49 +
  50 + // 调用,并断言异常
  51 + assertThrows(ServiceException.class, () -> client.upload(content, "../test.txt", "text/plain"));
  52 + assertFalse(FileUtil.exist(new File(tempDir.getParentFile(), "test.txt")));
  53 + }
  54 +
14 @Test 55 @Test
15 @Disabled 56 @Disabled
16 public void test() { 57 public void test() {
@@ -42,4 +83,13 @@ public class LocalFileClientTest { @@ -42,4 +83,13 @@ public class LocalFileClientTest {
42 System.out.println(); 83 System.out.println();
43 } 84 }
44 85
  86 + private LocalFileClient createClient() {
  87 + LocalFileClientConfig config = new LocalFileClientConfig();
  88 + config.setDomain("http://127.0.0.1:48080");
  89 + config.setBasePath(tempDir.getAbsolutePath());
  90 + LocalFileClient client = new LocalFileClient(0L, config);
  91 + client.init();
  92 + return client;
  93 + }
  94 +
45 } 95 }
yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImplTest.java
@@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; 5 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
6 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; 6 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
7 import cn.iocoder.yudao.framework.test.core.util.AssertUtils; 7 import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
  8 +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
8 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; 9 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
9 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; 10 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
10 import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; 11 import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper;
@@ -22,6 +23,7 @@ import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.bui @@ -22,6 +23,7 @@ import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.bui
22 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; 23 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
23 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; 24 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
24 import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS; 25 import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS;
  26 +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_PATH_INVALID;
25 import static org.junit.jupiter.api.Assertions.*; 27 import static org.junit.jupiter.api.Assertions.*;
26 import static org.mockito.ArgumentMatchers.same; 28 import static org.mockito.ArgumentMatchers.same;
27 import static org.mockito.Mockito.*; 29 import static org.mockito.Mockito.*;
@@ -173,6 +175,16 @@ public class FileServiceImplTest extends BaseDbUnitTest { @@ -173,6 +175,16 @@ public class FileServiceImplTest extends BaseDbUnitTest {
173 } 175 }
174 176
175 @Test 177 @Test
  178 + public void testDeleteFile_pathInvalid() {
  179 + // mock 数据
  180 + FileDO dbFile = randomPojo(FileDO.class, o -> o.setConfigId(10L).setPath("../tudou.jpg"));
  181 + fileMapper.insert(dbFile);
  182 +
  183 + // 调用,并断言异常
  184 + assertServiceException(() -> fileService.deleteFile(dbFile.getId()), FILE_PATH_INVALID);
  185 + }
  186 +
  187 + @Test
176 public void testGetFileContent() throws Exception { 188 public void testGetFileContent() throws Exception {
177 // 准备参数 189 // 准备参数
178 Long configId = 10L; 190 Long configId = 10L;
@@ -190,6 +202,59 @@ public class FileServiceImplTest extends BaseDbUnitTest { @@ -190,6 +202,59 @@ public class FileServiceImplTest extends BaseDbUnitTest {
190 } 202 }
191 203
192 @Test 204 @Test
  205 + public void testGetFileContent_pathInvalid() {
  206 + // 准备参数
  207 + Long configId = 10L;
  208 + String path = "../tudou.jpg";
  209 +
  210 + // 调用,并断言异常
  211 + assertServiceException(() -> fileService.getFileContent(configId, path), FILE_PATH_INVALID);
  212 + }
  213 +
  214 + @Test
  215 + public void testCreateFileByPresignedPath_success() {
  216 + // 准备参数
  217 + FileCreateReqVO reqVO = randomPojo(FileCreateReqVO.class, o -> {
  218 + o.setPath("avatar/test.jpg");
  219 + o.setName("test.jpg");
  220 + o.setUrl("https://www.iocoder.cn/test.jpg?token=123");
  221 + });
  222 +
  223 + // 调用
  224 + Long fileId = fileService.createFile(reqVO);
  225 +
  226 + // 断言
  227 + FileDO file = fileMapper.selectById(fileId);
  228 + assertEquals("avatar/test.jpg", file.getPath());
  229 + assertEquals("test.jpg", file.getName());
  230 + assertEquals("https://www.iocoder.cn/test.jpg", file.getUrl());
  231 + }
  232 +
  233 + @Test
  234 + public void testCreateFileByPresignedPath_nameInvalid() {
  235 + // 准备参数
  236 + FileCreateReqVO reqVO = randomPojo(FileCreateReqVO.class, o -> {
  237 + o.setPath("avatar/test.jpg");
  238 + o.setName("../test.jpg");
  239 + });
  240 +
  241 + // 调用,并断言异常
  242 + assertServiceException(() -> fileService.createFile(reqVO), FILE_PATH_INVALID);
  243 + }
  244 +
  245 + @Test
  246 + public void testCreateFileByPresignedPath_pathInvalid() {
  247 + // 准备参数
  248 + FileCreateReqVO reqVO = randomPojo(FileCreateReqVO.class, o -> {
  249 + o.setPath("../test.jpg");
  250 + o.setName("test.jpg");
  251 + });
  252 +
  253 + // 调用,并断言异常
  254 + assertServiceException(() -> fileService.createFile(reqVO), FILE_PATH_INVALID);
  255 + }
  256 +
  257 + @Test
193 public void testGenerateUploadPath_AllEnabled() { 258 public void testGenerateUploadPath_AllEnabled() {
194 // 准备参数 259 // 准备参数
195 String name = "test.jpg"; 260 String name = "test.jpg";
@@ -343,6 +408,28 @@ public class FileServiceImplTest extends BaseDbUnitTest { @@ -343,6 +408,28 @@ public class FileServiceImplTest extends BaseDbUnitTest {
343 } 408 }
344 409
345 @Test 410 @Test
  411 + public void testGenerateUploadPath_FileNameInvalid() {
  412 + // 准备参数
  413 + String name = "../test.jpg";
  414 + String directory = "avatar";
  415 + FileServiceImpl.PATH_PREFIX_DATE_ENABLE = false;
  416 + FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = false;
  417 +
  418 + // 调用,并断言异常
  419 + assertServiceException(() -> fileService.generateUploadPath(name, directory), FILE_PATH_INVALID);
  420 + }
  421 +
  422 + @Test
  423 + public void testGenerateUploadPath_DirectoryInvalid() {
  424 + // 准备参数
  425 + String name = "test.jpg";
  426 + String directory = "../avatar";
  427 +
  428 + // 调用,并断言异常
  429 + assertServiceException(() -> fileService.generateUploadPath(name, directory), FILE_PATH_INVALID);
  430 + }
  431 +
  432 + @Test
346 public void testGenerateUploadPath_DirectoryEmpty() { 433 public void testGenerateUploadPath_DirectoryEmpty() {
347 // 准备参数 434 // 准备参数
348 String name = "test.jpg"; 435 String name = "test.jpg";