From a16bbde9dd7f268d8b5c45d93eef96fd35931fc8 Mon Sep 17 00:00:00 2001 From: longjuan <769022681@qq.com> Date: Thu, 29 Feb 2024 21:53:38 +0800 Subject: [PATCH] feat: add support for custom template in automatic renaming during upload (#115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ```release-note 上传自动重命名支持自定义模板,支持更多占位符 ``` fixes #110 fixes #98 --- README.md | 96 +++++++-- build.gradle | 2 +- .../java/run/halo/s3os/FileNameUtils.java | 169 +++++++++------- .../java/run/halo/s3os/FilePathUtils.java | 20 +- .../run/halo/s3os/PlaceholderReplacer.java | 184 ++++++++++++++++++ .../run/halo/s3os/S3OsAttachmentHandler.java | 10 +- .../java/run/halo/s3os/S3OsProperties.java | 16 +- .../extensions/policy-template-s3os.yaml | 46 +++-- .../java/run/halo/s3os/FileNameUtilsTest.java | 28 +++ .../halo/s3os/PlaceholderReplacerTest.java | 80 ++++++++ 10 files changed, 535 insertions(+), 116 deletions(-) create mode 100644 src/main/java/run/halo/s3os/PlaceholderReplacer.java create mode 100644 src/test/java/run/halo/s3os/FileNameUtilsTest.java create mode 100644 src/test/java/run/halo/s3os/PlaceholderReplacerTest.java diff --git a/README.md b/README.md index 5e5a93e..6dc35ab 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,14 @@ ## 配置指南 +### Bucket 桶名称 + +一般与服务商控制台中的空间名称一致。 + +> 注意部分服务商 s3 空间名 ≠ 空间名称,若出现“Access Denied”报错可检查 Bucket 是否正确。 +> +> 可通过 S3Browser 查看桶列表,七牛云也可在“开发者平台-对象存储-空间概览-s3域名”中查看 s3 空间名。 + ### Endpoint 访问风格 请根据下方表格中的兼容访问风格选择,若您的服务商不在表格中,请自行查看服务商的 s3 兼容性文档或自行尝试。 @@ -44,14 +52,6 @@ 与服务商自己 API 的 Access Key 和 Access Secret 相同,详情查看对应服务商的文档。 -### Bucket 桶名称 - -一般与服务商控制台中的空间名称一致。 - -> 注意部分服务商 s3 空间名 ≠ 空间名称,若出现“Access Denied”报错可检查 Bucket 是否正确。 -> -> 可通过 S3Browser 查看桶列表,七牛云也可在“开发者平台-对象存储-空间概览-s3域名”中查看 s3 空间名。 - ### Region 一般留空即可。 @@ -60,15 +60,79 @@ > > Cloudflare 需要填写均为小写字母的 `auto`。 -### 上传时重命名文件方式 -* **保留原文件名:** 默认使用上传时的文件名,如遇文件名冲突会自动使用`使用原文件名 + 随机字符串` 模式重命名。 -* **使用原文件名 + 随机字符串:** 上传时会自动重命名为原文件名 + 随机的小写英文字母,长度请在`随机字符串长度`中设置。 -* **使用日期 + 随机字符串:** 上传时会自动重命名为日期 + 随机的小写英文字母,例如 `2023-12-01-abcdefgh.png`。 -* **使用日期时间 + 随机字符串:** 上传时会自动重命名为日期时间 + 随机的小写英文字母,例如 `2023-12-01T09:30:01.123456789-abcdef.png`。 -* **使用随机字符串:** 上传时会自动重命名为随机的小写英文字母,长度请在`随机字符串长度`中设置。 -* **使用 UUID:** 上传时会自动重命名为随机的 UUID。 +### 上传目录 -> 所有随机字符串的长度可在`随机字符串长度`中设置。 +上传到对象存储的目录,前后`/`可省略,例如`/halo`和`halo`是等价的。 + +支持的占位符有: +* `${uuid-with-dash}`:带有`-`的 UUID +* `${uuid-no-dash}`:不带`-`的 UUID +* `${timestamp-sec}`:秒时间戳(10位时间戳) +* `${timestamp-ms}`:毫秒时间戳(13位时间戳) +* `${year}`:年份 +* `${month}`:月份(两位数) +* `${day}`:日期(两位数) +* `${weekday}`:星期几,1-7 +* `${hour}`:小时(24小时制,两位数) +* `${minute}`:分钟(两位数) +* `${second}`:秒(两位数) +* `${millisecond}`:毫秒(三位数) +* `${random-alphabetic:X}`:随机的小写英文字母,长度为`X`,例如`${random-alphabetic:5}`会生成`abcde`。 +* `${random-num:X}`:随机的数字,长度为`X`,例如`${random-num:5}`会生成`12345`。 +* `${random-alphanumeric:X}`:随机的小写英文字母和数字,长度为`X`,例如`${random-alphanumeric:5}`会生成`abc12`。 + +> **示例**:
+> * `${year}/${month}/${day}/${random-alphabetic:1}`会放在`2023/12/01/a`。
+> * `halo/${uuid-no-dash}`会放在`halo/123E4567E89B12D3A456426614174000`。 + +### 上传时重命名文件方式 +* **保留原文件名:** 使用上传时的文件名。 +* **自定义:** 使用`自定义文件名模板`中填写的模板,上传时替换相应占位符作后作为文件名。 +* **使用 UUID:** 上传时会自动重命名为随机的 UUID。 +* **使用毫秒时间戳:** 上传时会自动重命名为毫秒时间戳(13位时间戳)。 +* **使使用原文件名 + 随机字母:** 上传时会自动重命名为原文件名 + 随机的小写英文字母,长度请在`随机字母长度`中设置。 +* **使用日期 + 随机字母:** 上传时会自动重命名为日期 + 随机的小写英文字母,例如 `2023-12-01-abcdefgh.png`。 +* **使用日期时间 + 随机字母:** 上传时会自动重命名为日期时间 + 随机的小写英文字母,例如 `2023-12-01T09:30:01-abcdef.png`。 +* **使用随机字母:** 上传时会自动重命名为随机的小写英文字母,长度请在`随机字母长度`中设置。 + +### 随机字母长度 + +仅当`上传时重命名文件方式`为`使用原文件名 + 随机字母`或`使用日期 + 随机字母`或`使用日期时间 + 随机字母`或`使用随机字母`时出现,用于设置随机字母的长度。 + +### 自定义文件名模板 + +仅当`上传时重命名文件方式`为`自定义`时出现,用于设置自定义文件名模板。 + +支持的占位符有: +* `${origin-filename}`:原文件名 +* `${uuid-with-dash}`:带有`-`的 UUID +* `${uuid-no-dash}`:不带`-`的 UUID +* `${timestamp-sec}`:秒时间戳(10位时间戳) +* `${timestamp-ms}`:毫秒时间戳(13位时间戳) +* `${year}`:年份 +* `${month}`:月份(两位数) +* `${day}`:日期(两位数) +* `${weekday}`:星期几,1-7 +* `${hour}`:小时(24小时制,两位数) +* `${minute}`:分钟(两位数) +* `${second}`:秒(两位数) +* `${millisecond}`:毫秒(三位数) +* `${random-alphabetic:X}`:随机的小写英文字母,长度为`X`,例如`${random-alphabetic:5}`会生成`abcde`。 +* `${random-num:X}`:随机的数字,长度为`X`,例如`${random-num:5}`会生成`12345`。 +* `${random-alphanumeric:X}`:随机的小写英文字母和数字,长度为`X`,例如`${random-alphanumeric:5}`会生成`abc12`。 + +> **示例**:
+> 当原始文件名为`image.png`时
+> * `${origin-filename}-${uuid-with-dash}`会生成`image-123E4567-E89B-12D3-A456-426614174000.png`。
+> * `${year}-${month}-${day}T${hour}:${minute}:${second}-${random-alphanumeric:5}`会生成`2023-12-01T09:30:01-abc12.png`。
+> * `${uuid-no-dash}_file_${random-alphabetic:5}`会生成`123E4567E89B12D3A456426614174000_file_abcde.png`。
+> * `halo_${origin-filename}_${random-num:3}`会生成`halo_image_123.png`。 + +### 重复文件名处理方式 + +* **加随机字母数字后缀:** 如遇重名,会在文件名后加上4位的随机字母数字后缀,例如`image.png`会变成`image_abc1.png`。 +* **加随机字母后缀:** 如遇重名,会在文件名后加上4位的随机字母后缀,例如`image.png`会变成`image_abcd.png`。 +* **报错不上传** 如遇重名,会放弃上传,并在用户界面提示 Duplicate filename 错误。 ## 部分对象存储服务商兼容性 diff --git a/build.gradle b/build.gradle index 8255b89..4cac596 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ repositories { } dependencies { - implementation platform('run.halo.tools.platform:plugin:2.10.0-SNAPSHOT') + implementation platform('run.halo.tools.platform:plugin:2.12.0-SNAPSHOT') compileOnly 'run.halo.app:api' implementation platform('software.amazon.awssdk:bom:2.19.8') diff --git a/src/main/java/run/halo/s3os/FileNameUtils.java b/src/main/java/run/halo/s3os/FileNameUtils.java index 32a1a6a..feff1be 100644 --- a/src/main/java/run/halo/s3os/FileNameUtils.java +++ b/src/main/java/run/halo/s3os/FileNameUtils.java @@ -1,95 +1,130 @@ package run.halo.s3os; +import static run.halo.s3os.S3OsProperties.DuplicateFilenameHandling; +import static run.halo.s3os.S3OsProperties.RandomFilenameMode; + import com.google.common.io.Files; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.UUID; +import org.springframework.web.server.ServerWebInputException; public final class FileNameUtils { private FileNameUtils() { } - public static String removeFileExtension(String filename, boolean removeAllExtensions) { - if (filename == null || filename.isEmpty()) { - return filename; - } - var extPattern = "(? filename; - case "withString" -> randomFilenameWithString(filename, length); - case "dateWithString" -> randomDateWithString(filename, length); - case "datetimeWithString" -> randomDatetimeWithString(filename, length); - case "string" -> randomString(filename, length); - case "uuid" -> randomUuid(filename); - default -> filename; - }; + /** + * Replace placeholders in filename. No duplicate handling. + * + * @param filename filename + * @param mode random filename mode + * @param randomStringLength random string length,when mode is withString or string + * @param customTemplate custom template,when mode is custom + * @return replaced filename + */ + public static String replaceFilename(String filename, RandomFilenameMode mode, + Integer randomStringLength, String customTemplate) { + var extension = Files.getFileExtension(filename); + var filenameWithoutExtension = Files.getNameWithoutExtension(filename); + var replaced = replaceFilenameByMode(filenameWithoutExtension, mode, randomStringLength, + customTemplate); + return replaced + (StringUtils.isBlank(extension) ? "" : "." + extension); } /** - * Append random string after file name. + * Replace placeholders in filename with duplicate handling. *
      * Case 1: halo.run -> halo-xyz.run
      * Case 2: .run -> xyz.run
      * Case 3: halo -> halo-xyz
      * 
* - * @param filename is name of file. - * @param length is for generating random string with specific length. - * @return File name with random string. + * @param filename filename + * @param mode random filename mode + * @param randomStringLength random string length,when mode is withString or string + * @param customTemplate custom template,when mode is custom + * @param handling duplicate filename handling + * @return replaced filename */ - public static String randomFilenameWithString(String filename, Integer length) { - String random = RandomStringUtils.randomAlphabetic(length).toLowerCase(); - return randomFilename(filename, random, true); + public static String replaceFilenameWithDuplicateHandling(String filename, + RandomFilenameMode mode, + Integer randomStringLength, + String customTemplate, + DuplicateFilenameHandling handling) { + var extension = Files.getFileExtension(filename); + var filenameWithoutExtension = Files.getNameWithoutExtension(filename); + var replaced = + replaceFilenameByMode(filenameWithoutExtension, mode, randomStringLength, + customTemplate); + var suffix = getDuplicateFilenameSuffix(handling); + return replaced + (StringUtils.isBlank(replaced) ? "" : "-") + suffix + + (StringUtils.isBlank(extension) ? "" : "." + extension); } - private static String randomDateWithString(String filename, Integer length) { - String random = LocalDate.now() + "-" + RandomStringUtils.randomAlphabetic(length).toLowerCase(); - return randomFilename(filename, random, false); - } - - private static String randomDatetimeWithString(String filename, Integer length) { - String random = LocalDateTime.now() + "-" + RandomStringUtils.randomAlphabetic(length).toLowerCase(); - return randomFilename(filename, random, false); - } - - private static String randomString(String filename, Integer length) { - String random = RandomStringUtils.randomAlphabetic(length).toLowerCase(); - return randomFilename(filename, random, false); - } - - private static String randomUuid(String filename) { - String random = UUID.randomUUID().toString().toUpperCase(); - return randomFilename(filename, random, false); - } - - private static String randomFilename(String filename, String random, Boolean needOriginalName) { - String nameWithoutExtension = Files.getNameWithoutExtension(filename); - String extension = Files.getFileExtension(filename); - boolean nameIsEmpty = StringUtils.isBlank(nameWithoutExtension); - boolean extensionIsEmpty = StringUtils.isBlank(extension); - if (needOriginalName) { - if (nameIsEmpty) { - return random + "." + extension; - } - if (extensionIsEmpty) { - return nameWithoutExtension + "-" + random; - } - return nameWithoutExtension + "-" + random + "." + extension; + private static String getDuplicateFilenameSuffix( + S3OsProperties.DuplicateFilenameHandling duplicateFilenameHandling) { + if (duplicateFilenameHandling == null) { + return RandomStringUtils.randomAlphabetic(4).toLowerCase(); } - else { - if (extensionIsEmpty) { - return random; - } - return random + "." + extension; + return switch (duplicateFilenameHandling) { + case randomAlphabetic -> RandomStringUtils.randomAlphabetic(4).toLowerCase(); + case exception -> throw new ServerWebInputException("Duplicate filename"); + // include "randomAlphanumeric" mode + default -> RandomStringUtils.randomAlphanumeric(4).toLowerCase(); + }; + } + + private static String replaceFilenameByMode(String filenameWithoutExtension, + S3OsProperties.RandomFilenameMode mode, + Integer randomStringLength, + String customTemplate) { + if (mode == null) { + return filenameWithoutExtension; } + // default length is 8 + Integer length = randomStringLength == null ? 8 : randomStringLength; + + return switch (mode) { + case custom -> { + if (StringUtils.isBlank(customTemplate)) { + yield filenameWithoutExtension; + } + yield PlaceholderReplacer.replacePlaceholders(customTemplate, + filenameWithoutExtension); + } + case uuid -> PlaceholderReplacer.replacePlaceholders("${uuid-with-dash}", + filenameWithoutExtension); + case timestampMs -> PlaceholderReplacer.replacePlaceholders("${timestamp-ms}", + filenameWithoutExtension); + case dateWithString -> { + String dateWithStringTemplate = + String.format("${year}-${month}-${day}-${random-alphabetic:%d}", length); + yield PlaceholderReplacer.replacePlaceholders(dateWithStringTemplate, + filenameWithoutExtension); + } + case datetimeWithString -> { + String datetimeWithStringTemplate = String.format( + "${year}-${month}-${day}T${hour}:${minute}:${second}-${random-alphabetic:%d}", + length); + yield PlaceholderReplacer.replacePlaceholders(datetimeWithStringTemplate, + filenameWithoutExtension); + } + case withString -> { + String withStringTemplate = + String.format("${origin-filename}-${random-alphabetic:%d}", length); + yield PlaceholderReplacer.replacePlaceholders(withStringTemplate, + filenameWithoutExtension); + } + case string -> { + String stringTemplate = String.format("${random-alphabetic:%d}", length); + yield PlaceholderReplacer.replacePlaceholders(stringTemplate, + filenameWithoutExtension); + } + default -> + // include "none" mode + filenameWithoutExtension; + }; + } /** diff --git a/src/main/java/run/halo/s3os/FilePathUtils.java b/src/main/java/run/halo/s3os/FilePathUtils.java index 6fb5155..042f948 100644 --- a/src/main/java/run/halo/s3os/FilePathUtils.java +++ b/src/main/java/run/halo/s3os/FilePathUtils.java @@ -1,23 +1,11 @@ package run.halo.s3os; -import org.apache.commons.lang3.StringUtils; - -import java.time.LocalDate; +import lombok.experimental.UtilityClass; +@UtilityClass public class FilePathUtils { - private FilePathUtils() { - } - - public static String getFilePathByPlaceholder(String filename) { - LocalDate localDate = LocalDate.now(); - return StringUtils.replaceEach(filename, - new String[] {"${year}","${month}","${day}"}, - new String[] { - String.valueOf(localDate.getYear()), - String.valueOf(localDate.getMonthValue()), - String.valueOf(localDate.getDayOfMonth()) - } - ); + public static String getFilePathByPlaceholder(String filePath) { + return PlaceholderReplacer.replacePlaceholders(filePath, ""); } } diff --git a/src/main/java/run/halo/s3os/PlaceholderReplacer.java b/src/main/java/run/halo/s3os/PlaceholderReplacer.java new file mode 100644 index 0000000..87e502a --- /dev/null +++ b/src/main/java/run/halo/s3os/PlaceholderReplacer.java @@ -0,0 +1,184 @@ +package run.halo.s3os; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; +import lombok.experimental.UtilityClass; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.util.PropertyPlaceholderHelper; + +@UtilityClass +public class PlaceholderReplacer { + record PlaceholderFunctionInput(String[] placeholderParams, + Map reusableParams) { + } + private static final PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}"); + + private static final Map> + placeholderFunctions = new HashMap<>(); + + static { + initializePlaceholderFunctions(); + } + + private static void initializePlaceholderFunctions() { + placeholderFunctions.put("origin-filename", input -> input.reusableParams.get("filename")); + placeholderFunctions.put("uuid-with-dash", input -> generateUUIDWithDash()); + placeholderFunctions.put("uuid-no-dash", input -> generateUUIDWithoutDash()); + placeholderFunctions.put("timestamp-sec", + input -> currentSecondsTimestamp(input.reusableParams)); + placeholderFunctions.put("timestamp-ms", + input -> currentMillisecondsTimestamp(input.reusableParams)); + placeholderFunctions.put("year", input -> currentYear(input.reusableParams)); + placeholderFunctions.put("month", input -> currentMonth(input.reusableParams)); + placeholderFunctions.put("day", input -> currentDay(input.reusableParams)); + placeholderFunctions.put("weekday", input -> currentWeekday(input.reusableParams)); + placeholderFunctions.put("hour", input -> currentHour(input.reusableParams)); + placeholderFunctions.put("minute", input -> currentMinute(input.reusableParams)); + placeholderFunctions.put("second", input -> currentSecond(input.reusableParams)); + placeholderFunctions.put("millisecond", input -> currentMillisecond(input.reusableParams)); + placeholderFunctions.put("random-alphabetic", + input -> generateRandomLetter(input.placeholderParams)); + placeholderFunctions.put("random-num", + input -> generateRandomNumber(input.placeholderParams)); + placeholderFunctions.put("random-alphanumeric", + input -> generateRandomAlphanumeric(input.placeholderParams)); + } + + private static String generateRandomAlphanumeric(String[] placeholderParams) { + try { + int length = Integer.parseInt(placeholderParams[0]); + return RandomStringUtils.randomAlphanumeric(length > 0 ? length : 8).toLowerCase(); + } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { + return RandomStringUtils.randomAlphanumeric(8).toLowerCase(); + } + } + + private static String generateRandomNumber(String[] placeholderParams) { + try { + int length = Integer.parseInt(placeholderParams[0]); + return RandomStringUtils.randomNumeric(length > 0 ? length : 8); + } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { + return RandomStringUtils.randomNumeric(8); + } + } + + private static String currentMillisecond(Map reusableParams) { + LocalDateTime time = LocalDateTime.parse(reusableParams.get("time")); + return String.valueOf(time.getNano() / 1000000); + } + + private static String currentSecond(Map reusableParams) { + LocalDateTime time = LocalDateTime.parse(reusableParams.get("time")); + return String.format("%02d", time.getSecond()); + } + + private static String currentMinute(Map reusableParams) { + LocalDateTime time = LocalDateTime.parse(reusableParams.get("time")); + return String.format("%02d", time.getMinute()); + } + + private static String currentHour(Map reusableParams) { + LocalDateTime time = LocalDateTime.parse(reusableParams.get("time")); + return String.format("%02d", time.getHour()); + } + + private static String currentWeekday(Map reusableParams) { + LocalDateTime time = LocalDateTime.parse(reusableParams.get("time")); + return String.valueOf(time.getDayOfWeek().getValue()); + } + + private static String currentDay(Map reusableParams) { + LocalDateTime time = LocalDateTime.parse(reusableParams.get("time")); + return String.format("%02d", time.getDayOfMonth()); + } + + private static String currentMonth(Map reusableParams) { + LocalDateTime time = LocalDateTime.parse(reusableParams.get("time")); + return String.format("%02d", time.getMonthValue()); + } + + private static String currentYear(Map reusableParams) { + LocalDateTime time = LocalDateTime.parse(reusableParams.get("time")); + return String.valueOf(time.getYear()); + } + + private static String currentMillisecondsTimestamp(Map reusableParams) { + LocalDateTime time = LocalDateTime.parse(reusableParams.get("time")); + return String.valueOf( + time.toInstant(ZoneOffset.systemDefault().getRules().getOffset(time)).toEpochMilli()); + } + + private static String currentSecondsTimestamp(Map reusableParams) { + LocalDateTime time = LocalDateTime.parse(reusableParams.get("time")); + return String.valueOf( + time.toInstant(ZoneOffset.systemDefault().getRules().getOffset(time)).getEpochSecond()); + } + + + private static String generateRandomLetter(String[] placeholderParams) { + try { + int length = Integer.parseInt(placeholderParams[0]); + return RandomStringUtils.randomAlphabetic(length > 0 ? length : 8).toLowerCase(); + } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { + return RandomStringUtils.randomAlphabetic(8).toLowerCase(); + } + } + + private static String generateUUIDWithoutDash() { + return UUID.randomUUID().toString().replace("-", "").toUpperCase(); + } + + private static String generateUUIDWithDash() { + return UUID.randomUUID().toString().toUpperCase(); + } + + /** + * Replace placeholders in the template with the provided filename. + * + * @param template The template to replace + * @param filename The filename without extension + * @return The replaced string + */ + public static String replacePlaceholders(String template, String filename) { + if (StringUtils.isBlank(template)) { + return filename; + } + Map reusableParams = new HashMap<>(); + + reusableParams.put("filename", filename); + reusableParams.put("time", LocalDateTime.now().toString()); + + return helper.replacePlaceholders(template, placeholder -> getPlaceholderValue(placeholder, reusableParams)); + } + + private static String getPlaceholderValue(String placeholderWithParam, + Map reusableParams) { + String[] parts = placeholderWithParam.split(":"); + String placeholder = parts[0]; + + String[] placeholderParams; + if (parts.length > 1) { + placeholderParams = new String[parts.length - 1]; + System.arraycopy(parts, 1, placeholderParams, 0, parts.length - 1); + } else { + placeholderParams = new String[0]; + } + + Function placeholderFunction = + placeholderFunctions.get(placeholder); + if (placeholderFunction != null) { + // Call the placeholder function with the provided map + return placeholderFunction.apply( + new PlaceholderFunctionInput(placeholderParams, reusableParams)); + } else { + // If placeholder not found, return null to keep the original placeholder string + return null; + } + } + +} diff --git a/src/main/java/run/halo/s3os/S3OsAttachmentHandler.java b/src/main/java/run/halo/s3os/S3OsAttachmentHandler.java index 5c5e321..ba3f3e9 100644 --- a/src/main/java/run/halo/s3os/S3OsAttachmentHandler.java +++ b/src/main/java/run/halo/s3os/S3OsAttachmentHandler.java @@ -510,8 +510,9 @@ public class S3OsAttachmentHandler implements AttachmentHandler { this.originalFileName = fileName; if (needRandomJudge) { - fileName = FileNameUtils.getRandomFilename(fileName, - properties.getRandomStringLength(), properties.getRandomFilenameMode()); + fileName = + FileNameUtils.replaceFilename(fileName, properties.getRandomFilenameMode(), + properties.getRandomStringLength(), properties.getCustomTemplate()); } this.fileName = fileName; @@ -525,7 +526,10 @@ public class S3OsAttachmentHandler implements AttachmentHandler { } public void randomDuplicateFileName() { - this.fileName = FileNameUtils.randomFilenameWithString(originalFileName, 4); + this.fileName = FileNameUtils.replaceFilenameWithDuplicateHandling(originalFileName, + properties.getRandomFilenameMode(), + properties.getRandomStringLength(), properties.getCustomTemplate(), + properties.getDuplicateFilenameHandling()); this.objectKey = properties.getObjectName(fileName); } } diff --git a/src/main/java/run/halo/s3os/S3OsProperties.java b/src/main/java/run/halo/s3os/S3OsProperties.java index 5df8add..634eea2 100644 --- a/src/main/java/run/halo/s3os/S3OsProperties.java +++ b/src/main/java/run/halo/s3os/S3OsProperties.java @@ -29,7 +29,11 @@ class S3OsProperties { */ private String location; - private String randomFilenameMode = "none"; + private RandomFilenameMode randomFilenameMode; + + private String customTemplate; + + private DuplicateFilenameHandling duplicateFilenameHandling; private Integer randomStringLength = 8; @@ -53,6 +57,14 @@ class S3OsProperties { private String urlSuffix; } + public enum DuplicateFilenameHandling { + randomAlphanumeric, randomAlphabetic, exception + } + + public enum RandomFilenameMode { + none, custom, uuid, timestampMs, dateWithString, datetimeWithString, withString, string, random_number + } + public String getObjectName(String filename) { var objectName = filename; var finalName = FilePathUtils.getFilePathByPlaceholder(getLocation()); @@ -62,7 +74,7 @@ class S3OsProperties { return objectName; } - enum Protocol { + public enum Protocol { http, https } diff --git a/src/main/resources/extensions/policy-template-s3os.yaml b/src/main/resources/extensions/policy-template-s3os.yaml index ced586f..9b9b663 100644 --- a/src/main/resources/extensions/policy-template-s3os.yaml +++ b/src/main/resources/extensions/policy-template-s3os.yaml @@ -62,30 +62,54 @@ spec: name: location label: 上传目录 placeholder: 如不填写,则默认上传到根目录 - help: 支持使用 ${year} ${month} ${day} 占位符 + help: 支持的占位符请查阅:https://github.com/halo-dev/plugin-s3#上传目录 - $formkit: select name: randomFilenameMode label: 上传时重命名文件方式 options: - label: 保留原文件名 value: none - - label: 使用原文件名 + 随机字符串 - value: withString - - label: 使用日期 + 随机字符串 - value: dateWithString - - label: 使用日期时间 + 随机字符串 - value: datetimeWithString - - label: 使用随机字符串 - value: string + - label: 自定义(请在下方输入自定义模板) + value: custom - label: 使用UUID value: uuid + - label: 使用毫秒时间戳 + value: timestampMs + - label: 使用原文件名 + 随机字母 + value: withString + - label: 使用日期 + 随机字母 + value: dateWithString + - label: 使用日期时间 + 随机字母 + value: datetimeWithString + - label: 使用随机字母 + value: string validation: required - $formkit: number name: randomStringLength - label: 随机字符串长度 + key: randomStringLength + label: 随机字母长度 min: 4 max: 16 - placeholder: 仅在重命名文件时需要随机字符串时填写(支持4~16位, 默认为8位) + if: "$randomFilenameMode == 'dateWithString' || $randomFilenameMode == 'datetimeWithString' || $randomFilenameMode == 'withString' || $randomFilenameMode == 'string'" + help: 支持4~16位, 默认为8位 + - $formkit: text + name: customTemplate + key: customTemplate + label: 自定义文件名模板 + if: "$randomFilenameMode == 'custom'" + value: "${origin-filename}" + help: 支持的占位符请查阅:https://github.com/halo-dev/plugin-s3#上传时重命名文件方式 + - $formkit: select + name: duplicateFilenameHandling + label: 重复文件名处理方式 + options: + - label: 加随机字母数字后缀 + value: randomAlphanumeric + - label: 加随机字母后缀 + value: randomAlphabetic + - label: 报错不上传 + value: exception + validation: required - $formkit: select name: protocol label: 绑定域名协议 diff --git a/src/test/java/run/halo/s3os/FileNameUtilsTest.java b/src/test/java/run/halo/s3os/FileNameUtilsTest.java new file mode 100644 index 0000000..6f7dfb2 --- /dev/null +++ b/src/test/java/run/halo/s3os/FileNameUtilsTest.java @@ -0,0 +1,28 @@ +package run.halo.s3os; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class FileNameUtilsTest { + @Test + void testReplaceFilenameWithDuplicateHandling() { + // Case 1: halo.run -> halo-xyz.run + var result = FileNameUtils.replaceFilenameWithDuplicateHandling( + "halo.run", S3OsProperties.RandomFilenameMode.none, null, + null, S3OsProperties.DuplicateFilenameHandling.randomAlphanumeric); + assertTrue(result.matches("halo-[a-z0-9]{4}.run")); + + // Case 2: .run -> xyz.run + result = FileNameUtils.replaceFilenameWithDuplicateHandling( + ".run", S3OsProperties.RandomFilenameMode.none, null, + null, S3OsProperties.DuplicateFilenameHandling.randomAlphanumeric); + assertTrue(result.matches("[a-z0-9]{4}.run")); + + // Case 3: halo -> halo-xyz + result = FileNameUtils.replaceFilenameWithDuplicateHandling( + "halo", S3OsProperties.RandomFilenameMode.none, null, + null, S3OsProperties.DuplicateFilenameHandling.randomAlphanumeric); + assertTrue(result.matches("halo-[a-z0-9]{4}")); + } +} \ No newline at end of file diff --git a/src/test/java/run/halo/s3os/PlaceholderReplacerTest.java b/src/test/java/run/halo/s3os/PlaceholderReplacerTest.java new file mode 100644 index 0000000..3a3ed29 --- /dev/null +++ b/src/test/java/run/halo/s3os/PlaceholderReplacerTest.java @@ -0,0 +1,80 @@ +package run.halo.s3os; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class PlaceholderReplacerTest { + + @Test + void testReplacePlaceholdersTemplateNull() { + String result1 = PlaceholderReplacer.replacePlaceholders(null, "test"); + assertEquals("test", result1); + String result2 = PlaceholderReplacer.replacePlaceholders("", "test"); + assertEquals("test", result2); + } + + @Test + void testReplacePlaceholdersAllPlaceholder() { + String template = "${origin-filename}-${uuid-with-dash}-${uuid-no-dash}-${timestamp-sec}-" + + "${timestamp-ms}-${year}-${month}-${day}-${weekday}-${hour}-${minute}-${second}-" + + "${millisecond}-${random-alphabetic:4}-${random-num:5}-${random-alphanumeric:6}"; + String result = PlaceholderReplacer.replacePlaceholders(template, "test"); + String regex = "test-" + + "[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}-" + + "[A-F0-9]{32}-" + + "[0-9]{10}-" + + "[0-9]{13}-[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{1}-[0-9]{2}-[0-9]{2}-[0-9]{2}-" + + "[0-9]{3}-[a-z]{4}-[0-9]{5}-[a-z0-9]{6}"; + assertTrue(result.matches(regex)); + } + + @Test + void testReplacePlaceholdersTimestamp() { + String template = + "${timestamp-sec}-${timestamp-sec}-${timestamp-ms}-${timestamp-ms}-${year}-${year}-" + + "${month}-${month}-${day}-${day}-${weekday}-${weekday}-${hour}-${hour}-" + + "${minute}-${minute}-${second}-${second}-${millisecond}-${millisecond}"; + String result = PlaceholderReplacer.replacePlaceholders(template, "test"); + String[] split = result.split("-"); + for (int i = 0; i < split.length; i += 2) { + assertEquals(split[i], split[i + 1]); + } + } + + @Test + void testReplacePlaceholdersRandomAlphabeticNoLength() { + String template = + "${random-alphabetic}_${random-alphabetic:}_${random-alphabetic:-1}_${random-alphabetic:0}"; + String result = PlaceholderReplacer.replacePlaceholders(template, "test"); + String regex = "[a-z]{8}_[a-z]{8}_[a-z]{8}_[a-z]{8}"; + assertTrue(result.matches(regex)); + + template = "${random-num}_${random-num:}_${random-num:-1}_${random-num:0}"; + result = PlaceholderReplacer.replacePlaceholders(template, "test"); + regex = "[0-9]{8}_[0-9]{8}_[0-9]{8}_[0-9]{8}"; + assertTrue(result.matches(regex)); + + template = + "${random-alphanumeric}_${random-alphanumeric:}_${random-alphanumeric:-1}_${random-alphanumeric:0}"; + result = PlaceholderReplacer.replacePlaceholders(template, "test"); + regex = "[a-z0-9]{8}_[a-z0-9]{8}_[a-z0-9]{8}_[a-z0-9]{8}"; + assertTrue(result.matches(regex)); + } + + @Test + void testReplacePlaceholdersInvalid() { + String template = "file_${not-exist}_test"; + String result = PlaceholderReplacer.replacePlaceholders(template, "test"); + assertEquals("file_${not-exist}_test", result); + + template = "file_${random-alphabetic_test"; + result = PlaceholderReplacer.replacePlaceholders(template, "test"); + assertEquals("file_${random-alphabetic_test", result); + + template = "file_random-alphabetic}_test"; + result = PlaceholderReplacer.replacePlaceholders(template, "test"); + assertEquals("file_random-alphabetic}_test", result); + } + +} \ No newline at end of file