mirror of
https://github.com/halo-dev/plugin-s3.git
synced 2025-10-17 16:08:25 +00:00
feat: add support for custom template in automatic renaming during upload (#115)
```release-note 上传自动重命名支持自定义模板,支持更多占位符 ``` fixes #110 fixes #98
This commit is contained in:
96
README.md
96
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`。
|
||||
|
||||
> **示例**:<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 错误。
|
||||
|
||||
## 部分对象存储服务商兼容性
|
||||
|
||||
|
@@ -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')
|
||||
|
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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, "");
|
||||
}
|
||||
}
|
||||
|
184
src/main/java/run/halo/s3os/PlaceholderReplacer.java
Normal file
184
src/main/java/run/halo/s3os/PlaceholderReplacer.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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: 绑定域名协议
|
||||
|
28
src/test/java/run/halo/s3os/FileNameUtilsTest.java
Normal file
28
src/test/java/run/halo/s3os/FileNameUtilsTest.java
Normal 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}"));
|
||||
}
|
||||
}
|
80
src/test/java/run/halo/s3os/PlaceholderReplacerTest.java
Normal file
80
src/test/java/run/halo/s3os/PlaceholderReplacerTest.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user