feat: add support for custom template in automatic renaming during upload (#115)

```release-note
上传自动重命名支持自定义模板,支持更多占位符
```

fixes #110
fixes #98
This commit is contained in:
longjuan
2024-02-29 21:53:38 +08:00
committed by GitHub
parent 9efa4b97e5
commit a16bbde9dd
10 changed files with 535 additions and 116 deletions

View File

@@ -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`
> **示例**<br/>
> * `${year}/${month}/${day}/${random-alphabetic:1}`会放在`2023/12/01/a`。<br/>
> * `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`
> **示例**<br/>
> 当原始文件名为`image.png`时<br/>
> * `${origin-filename}-${uuid-with-dash}`会生成`image-123E4567-E89B-12D3-A456-426614174000.png`。<br/>
> * `${year}-${month}-${day}T${hour}:${minute}:${second}-${random-alphanumeric:5}`会生成`2023-12-01T09:30:01-abc12.png`。<br/>
> * `${uuid-no-dash}_file_${random-alphabetic:5}`会生成`123E4567E89B12D3A456426614174000_file_abcde.png`。<br/>
> * `halo_${origin-filename}_${random-num:3}`会生成`halo_image_123.png`。
### 重复文件名处理方式
* **加随机字母数字后缀:** 如遇重名会在文件名后加上4位的随机字母数字后缀例如`image.png`会变成`image_abc1.png`
* **加随机字母后缀:** 如遇重名会在文件名后加上4位的随机字母后缀例如`image.png`会变成`image_abcd.png`
* **报错不上传** 如遇重名,会放弃上传,并在用户界面提示 Duplicate filename 错误。
## 部分对象存储服务商兼容性

View File

@@ -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')

View File

@@ -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 = "(?<!^)[.]" + (removeAllExtensions ? ".*" : "[^.]*$");
return filename.replaceAll(extPattern, "");
}
public static String getRandomFilename(String filename, Integer length, String mode) {
return switch (mode) {
// case "none" -> 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.
* <pre>
* Case 1: halo.run -> halo-xyz.run
* Case 2: .run -> xyz.run
* Case 3: halo -> halo-xyz
* </pre>
*
* @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;
};
}
/**

View File

@@ -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, "");
}
}

View File

@@ -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<String, String> reusableParams) {
}
private static final PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}");
private static final Map<String, Function<PlaceholderFunctionInput, String>>
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<String, String> reusableParams) {
LocalDateTime time = LocalDateTime.parse(reusableParams.get("time"));
return String.valueOf(time.getNano() / 1000000);
}
private static String currentSecond(Map<String, String> reusableParams) {
LocalDateTime time = LocalDateTime.parse(reusableParams.get("time"));
return String.format("%02d", time.getSecond());
}
private static String currentMinute(Map<String, String> reusableParams) {
LocalDateTime time = LocalDateTime.parse(reusableParams.get("time"));
return String.format("%02d", time.getMinute());
}
private static String currentHour(Map<String, String> reusableParams) {
LocalDateTime time = LocalDateTime.parse(reusableParams.get("time"));
return String.format("%02d", time.getHour());
}
private static String currentWeekday(Map<String, String> reusableParams) {
LocalDateTime time = LocalDateTime.parse(reusableParams.get("time"));
return String.valueOf(time.getDayOfWeek().getValue());
}
private static String currentDay(Map<String, String> reusableParams) {
LocalDateTime time = LocalDateTime.parse(reusableParams.get("time"));
return String.format("%02d", time.getDayOfMonth());
}
private static String currentMonth(Map<String, String> reusableParams) {
LocalDateTime time = LocalDateTime.parse(reusableParams.get("time"));
return String.format("%02d", time.getMonthValue());
}
private static String currentYear(Map<String, String> reusableParams) {
LocalDateTime time = LocalDateTime.parse(reusableParams.get("time"));
return String.valueOf(time.getYear());
}
private static String currentMillisecondsTimestamp(Map<String, String> reusableParams) {
LocalDateTime time = LocalDateTime.parse(reusableParams.get("time"));
return String.valueOf(
time.toInstant(ZoneOffset.systemDefault().getRules().getOffset(time)).toEpochMilli());
}
private static String currentSecondsTimestamp(Map<String, String> 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<String, String> 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<String, String> 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<PlaceholderFunctionInput, String> 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;
}
}
}

View File

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

View File

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

View File

@@ -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: 绑定域名协议

View File

@@ -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}"));
}
}

View File

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