mirror of
https://github.com/halo-dev/plugin-s3.git
synced 2025-10-19 09:07:33 +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 访问风格
|
### Endpoint 访问风格
|
||||||
|
|
||||||
请根据下方表格中的兼容访问风格选择,若您的服务商不在表格中,请自行查看服务商的 s3 兼容性文档或自行尝试。
|
请根据下方表格中的兼容访问风格选择,若您的服务商不在表格中,请自行查看服务商的 s3 兼容性文档或自行尝试。
|
||||||
@@ -44,14 +52,6 @@
|
|||||||
|
|
||||||
与服务商自己 API 的 Access Key 和 Access Secret 相同,详情查看对应服务商的文档。
|
与服务商自己 API 的 Access Key 和 Access Secret 相同,详情查看对应服务商的文档。
|
||||||
|
|
||||||
### Bucket 桶名称
|
|
||||||
|
|
||||||
一般与服务商控制台中的空间名称一致。
|
|
||||||
|
|
||||||
> 注意部分服务商 s3 空间名 ≠ 空间名称,若出现“Access Denied”报错可检查 Bucket 是否正确。
|
|
||||||
>
|
|
||||||
> 可通过 S3Browser 查看桶列表,七牛云也可在“开发者平台-对象存储-空间概览-s3域名”中查看 s3 空间名。
|
|
||||||
|
|
||||||
### Region
|
### Region
|
||||||
|
|
||||||
一般留空即可。
|
一般留空即可。
|
||||||
@@ -60,15 +60,79 @@
|
|||||||
>
|
>
|
||||||
> Cloudflare 需要填写均为小写字母的 `auto`。
|
> 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 {
|
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'
|
compileOnly 'run.halo.app:api'
|
||||||
|
|
||||||
implementation platform('software.amazon.awssdk:bom:2.19.8')
|
implementation platform('software.amazon.awssdk:bom:2.19.8')
|
||||||
|
@@ -1,95 +1,130 @@
|
|||||||
package run.halo.s3os;
|
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 com.google.common.io.Files;
|
||||||
import org.apache.commons.lang3.RandomStringUtils;
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public final class FileNameUtils {
|
public final class FileNameUtils {
|
||||||
|
|
||||||
private FileNameUtils() {
|
private FileNameUtils() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String removeFileExtension(String filename, boolean removeAllExtensions) {
|
/**
|
||||||
if (filename == null || filename.isEmpty()) {
|
* Replace placeholders in filename. No duplicate handling.
|
||||||
return filename;
|
*
|
||||||
}
|
* @param filename filename
|
||||||
var extPattern = "(?<!^)[.]" + (removeAllExtensions ? ".*" : "[^.]*$");
|
* @param mode random filename mode
|
||||||
return filename.replaceAll(extPattern, "");
|
* @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 getRandomFilename(String filename, Integer length, String mode) {
|
*/
|
||||||
return switch (mode) {
|
public static String replaceFilename(String filename, RandomFilenameMode mode,
|
||||||
// case "none" -> filename;
|
Integer randomStringLength, String customTemplate) {
|
||||||
case "withString" -> randomFilenameWithString(filename, length);
|
var extension = Files.getFileExtension(filename);
|
||||||
case "dateWithString" -> randomDateWithString(filename, length);
|
var filenameWithoutExtension = Files.getNameWithoutExtension(filename);
|
||||||
case "datetimeWithString" -> randomDatetimeWithString(filename, length);
|
var replaced = replaceFilenameByMode(filenameWithoutExtension, mode, randomStringLength,
|
||||||
case "string" -> randomString(filename, length);
|
customTemplate);
|
||||||
case "uuid" -> randomUuid(filename);
|
return replaced + (StringUtils.isBlank(extension) ? "" : "." + extension);
|
||||||
default -> filename;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Append random string after file name.
|
* Replace placeholders in filename with duplicate handling.
|
||||||
* <pre>
|
* <pre>
|
||||||
* Case 1: halo.run -> halo-xyz.run
|
* Case 1: halo.run -> halo-xyz.run
|
||||||
* Case 2: .run -> xyz.run
|
* Case 2: .run -> xyz.run
|
||||||
* Case 3: halo -> halo-xyz
|
* Case 3: halo -> halo-xyz
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
* @param filename is name of file.
|
* @param filename filename
|
||||||
* @param length is for generating random string with specific length.
|
* @param mode random filename mode
|
||||||
* @return File name with random string.
|
* @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) {
|
public static String replaceFilenameWithDuplicateHandling(String filename,
|
||||||
String random = RandomStringUtils.randomAlphabetic(length).toLowerCase();
|
RandomFilenameMode mode,
|
||||||
return randomFilename(filename, random, true);
|
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) {
|
private static String getDuplicateFilenameSuffix(
|
||||||
String random = LocalDate.now() + "-" + RandomStringUtils.randomAlphabetic(length).toLowerCase();
|
S3OsProperties.DuplicateFilenameHandling duplicateFilenameHandling) {
|
||||||
return randomFilename(filename, random, false);
|
if (duplicateFilenameHandling == null) {
|
||||||
|
return RandomStringUtils.randomAlphabetic(4).toLowerCase();
|
||||||
|
}
|
||||||
|
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 randomDatetimeWithString(String filename, Integer length) {
|
private static String replaceFilenameByMode(String filenameWithoutExtension,
|
||||||
String random = LocalDateTime.now() + "-" + RandomStringUtils.randomAlphabetic(length).toLowerCase();
|
S3OsProperties.RandomFilenameMode mode,
|
||||||
return randomFilename(filename, random, false);
|
Integer randomStringLength,
|
||||||
|
String customTemplate) {
|
||||||
|
if (mode == null) {
|
||||||
|
return filenameWithoutExtension;
|
||||||
}
|
}
|
||||||
|
// default length is 8
|
||||||
|
Integer length = randomStringLength == null ? 8 : randomStringLength;
|
||||||
|
|
||||||
private static String randomString(String filename, Integer length) {
|
return switch (mode) {
|
||||||
String random = RandomStringUtils.randomAlphabetic(length).toLowerCase();
|
case custom -> {
|
||||||
return randomFilename(filename, random, false);
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (extensionIsEmpty) {
|
|
||||||
return random;
|
|
||||||
}
|
|
||||||
return random + "." + extension;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,23 +1,11 @@
|
|||||||
package run.halo.s3os;
|
package run.halo.s3os;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
public class FilePathUtils {
|
public class FilePathUtils {
|
||||||
private FilePathUtils() {
|
|
||||||
|
|
||||||
}
|
public static String getFilePathByPlaceholder(String filePath) {
|
||||||
|
return PlaceholderReplacer.replacePlaceholders(filePath, "");
|
||||||
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())
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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;
|
this.originalFileName = fileName;
|
||||||
|
|
||||||
if (needRandomJudge) {
|
if (needRandomJudge) {
|
||||||
fileName = FileNameUtils.getRandomFilename(fileName,
|
fileName =
|
||||||
properties.getRandomStringLength(), properties.getRandomFilenameMode());
|
FileNameUtils.replaceFilename(fileName, properties.getRandomFilenameMode(),
|
||||||
|
properties.getRandomStringLength(), properties.getCustomTemplate());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fileName = fileName;
|
this.fileName = fileName;
|
||||||
@@ -525,7 +526,10 @@ public class S3OsAttachmentHandler implements AttachmentHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void randomDuplicateFileName() {
|
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);
|
this.objectKey = properties.getObjectName(fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -29,7 +29,11 @@ class S3OsProperties {
|
|||||||
*/
|
*/
|
||||||
private String location;
|
private String location;
|
||||||
|
|
||||||
private String randomFilenameMode = "none";
|
private RandomFilenameMode randomFilenameMode;
|
||||||
|
|
||||||
|
private String customTemplate;
|
||||||
|
|
||||||
|
private DuplicateFilenameHandling duplicateFilenameHandling;
|
||||||
|
|
||||||
private Integer randomStringLength = 8;
|
private Integer randomStringLength = 8;
|
||||||
|
|
||||||
@@ -53,6 +57,14 @@ class S3OsProperties {
|
|||||||
private String urlSuffix;
|
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) {
|
public String getObjectName(String filename) {
|
||||||
var objectName = filename;
|
var objectName = filename;
|
||||||
var finalName = FilePathUtils.getFilePathByPlaceholder(getLocation());
|
var finalName = FilePathUtils.getFilePathByPlaceholder(getLocation());
|
||||||
@@ -62,7 +74,7 @@ class S3OsProperties {
|
|||||||
return objectName;
|
return objectName;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Protocol {
|
public enum Protocol {
|
||||||
http, https
|
http, https
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -62,30 +62,54 @@ spec:
|
|||||||
name: location
|
name: location
|
||||||
label: 上传目录
|
label: 上传目录
|
||||||
placeholder: 如不填写,则默认上传到根目录
|
placeholder: 如不填写,则默认上传到根目录
|
||||||
help: 支持使用 ${year} ${month} ${day} 占位符
|
help: 支持的占位符请查阅:https://github.com/halo-dev/plugin-s3#上传目录
|
||||||
- $formkit: select
|
- $formkit: select
|
||||||
name: randomFilenameMode
|
name: randomFilenameMode
|
||||||
label: 上传时重命名文件方式
|
label: 上传时重命名文件方式
|
||||||
options:
|
options:
|
||||||
- label: 保留原文件名
|
- label: 保留原文件名
|
||||||
value: none
|
value: none
|
||||||
- label: 使用原文件名 + 随机字符串
|
- label: 自定义(请在下方输入自定义模板)
|
||||||
value: withString
|
value: custom
|
||||||
- label: 使用日期 + 随机字符串
|
|
||||||
value: dateWithString
|
|
||||||
- label: 使用日期时间 + 随机字符串
|
|
||||||
value: datetimeWithString
|
|
||||||
- label: 使用随机字符串
|
|
||||||
value: string
|
|
||||||
- label: 使用UUID
|
- label: 使用UUID
|
||||||
value: uuid
|
value: uuid
|
||||||
|
- label: 使用毫秒时间戳
|
||||||
|
value: timestampMs
|
||||||
|
- label: 使用原文件名 + 随机字母
|
||||||
|
value: withString
|
||||||
|
- label: 使用日期 + 随机字母
|
||||||
|
value: dateWithString
|
||||||
|
- label: 使用日期时间 + 随机字母
|
||||||
|
value: datetimeWithString
|
||||||
|
- label: 使用随机字母
|
||||||
|
value: string
|
||||||
validation: required
|
validation: required
|
||||||
- $formkit: number
|
- $formkit: number
|
||||||
name: randomStringLength
|
name: randomStringLength
|
||||||
label: 随机字符串长度
|
key: randomStringLength
|
||||||
|
label: 随机字母长度
|
||||||
min: 4
|
min: 4
|
||||||
max: 16
|
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
|
- $formkit: select
|
||||||
name: protocol
|
name: protocol
|
||||||
label: 绑定域名协议
|
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