Skip to content

Commit

Permalink
feat: add support for custom template in automatic renaming during up…
Browse files Browse the repository at this point in the history
…load (#115)

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

fixes #110
fixes #98
  • Loading branch information
longjuan authored Feb 29, 2024
1 parent 9efa4b9 commit a16bbde
Show file tree
Hide file tree
Showing 10 changed files with 528 additions and 109 deletions.
94 changes: 79 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@

## 配置指南

### Bucket 桶名称

一般与服务商控制台中的空间名称一致。

> 注意部分服务商 s3 空间名 ≠ 空间名称,若出现“Access Denied”报错可检查 Bucket 是否正确。
>
> 可通过 S3Browser 查看桶列表,七牛云也可在“开发者平台-对象存储-空间概览-s3域名”中查看 s3 空间名。
### Endpoint 访问风格

请根据下方表格中的兼容访问风格选择,若您的服务商不在表格中,请自行查看服务商的 s3 兼容性文档或自行尝试。
Expand All @@ -44,14 +52,6 @@

与服务商自己 API 的 Access Key 和 Access Secret 相同,详情查看对应服务商的文档。

### Bucket 桶名称

一般与服务商控制台中的空间名称一致。

> 注意部分服务商 s3 空间名 ≠ 空间名称,若出现“Access Denied”报错可检查 Bucket 是否正确。
>
> 可通过 S3Browser 查看桶列表,七牛云也可在“开发者平台-对象存储-空间概览-s3域名”中查看 s3 空间名。
### Region

一般留空即可。
Expand All @@ -60,15 +60,79 @@
>
> Cloudflare 需要填写均为小写字母的 `auto`
### 上传目录

上传到对象存储的目录,前后`/`可省略,例如`/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`
### 上传时重命名文件方式
* **保留原文件名:** 默认使用上传时的文件名,如遇文件名冲突会自动使用`使用原文件名 + 随机字符串` 模式重命名。
* **使用原文件名 + 随机字符串:** 上传时会自动重命名为原文件名 + 随机的小写英文字母,长度请在`随机字符串长度`中设置。
* **使用日期 + 随机字符串:** 上传时会自动重命名为日期 + 随机的小写英文字母,例如 `2023-12-01-abcdefgh.png`
* **使用日期时间 + 随机字符串:** 上传时会自动重命名为日期时间 + 随机的小写英文字母,例如 `2023-12-01T09:30:01.123456789-abcdef.png`
* **使用随机字符串:** 上传时会自动重命名为随机的小写英文字母,长度请在`随机字符串长度`中设置。
* **保留原文件名:** 使用上传时的文件名。
* **自定义:** 使用`自定义文件名模板`中填写的模板,上传时替换相应占位符作后作为文件名。
* **使用 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 错误。

## 部分对象存储服务商兼容性

Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
161 changes: 98 additions & 63 deletions src/main/java/run/halo/s3os/FileNameUtils.java
Original file line number Diff line number Diff line change
@@ -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 getDuplicateFilenameSuffix(
S3OsProperties.DuplicateFilenameHandling duplicateFilenameHandling) {
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 randomUuid(String filename) {
String random = UUID.randomUUID().toString().toUpperCase();
return randomFilename(filename, random, false);
}
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;

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;
return switch (mode) {
case custom -> {
if (StringUtils.isBlank(customTemplate)) {
yield filenameWithoutExtension;
}
yield PlaceholderReplacer.replacePlaceholders(customTemplate,
filenameWithoutExtension);
}
if (extensionIsEmpty) {
return nameWithoutExtension + "-" + random;
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);
}
return nameWithoutExtension + "-" + random + "." + extension;
}
else {
if (extensionIsEmpty) {
return random;
case datetimeWithString -> {
String datetimeWithStringTemplate = String.format(
"${year}-${month}-${day}T${hour}:${minute}:${second}-${random-alphabetic:%d}",
length);
yield PlaceholderReplacer.replacePlaceholders(datetimeWithStringTemplate,
filenameWithoutExtension);
}
return random + "." + extension;
}
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;
};

}

/**
Expand Down
20 changes: 4 additions & 16 deletions src/main/java/run/halo/s3os/FilePathUtils.java
Original file line number Diff line number Diff line change
@@ -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, "");
}
}
Loading

0 comments on commit a16bbde

Please sign in to comment.