Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 15 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 78 additions & 14 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`。

### 上传时重命名文件方式
longjuan marked this conversation as resolved.
Show resolved Hide resolved

> 所有随机字符串的长度可在`随机字符串长度`中设置。
* **加随机字母数字后缀:** 如遇重名,会在文件名后加上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
Loading