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