mirror of https://github.com/halo-dev/halo
Support randomizing local attachment filename (#7301)
#### What type of PR is this? /kind feature /area core /milestone 2.20.x #### What this PR does / why we need it: This PR allows users to upload local attachment always with a random filename to simply prevent resource leak. Please see the configuration and the uploaded result below:  ```json { "spec": { "displayName": "halo.run-ykfswxmokpjopvkqwybghazloxeovgae.cer", "policyName": "attachment-policy-XVdDK", "ownerName": "admin", "mediaType": "application/pkix-cert", "size": 1803 }, "status": { "permalink": "/upload/random/halo.run-ykfswxmokpjopvkqwybghazloxeovgae.cer" }, "apiVersion": "storage.halo.run/v1alpha1", "kind": "Attachment", "metadata": { "finalizers": [ "attachment-manager" ], "name": "44b4c8de-0d3b-4bbb-acc2-4af50175a2b5", "annotations": { "storage.halo.run/local-relative-path": "upload/random/halo.run-ykfswxmokpjopvkqwybghazloxeovgae.cer", "storage.halo.run/uri": "/upload/random/halo.run-ykfswxmokpjopvkqwybghazloxeovgae.cer" }, "version": 2, "creationTimestamp": "2025-03-18T15:53:11.817541483Z" } } ``` #### Does this PR introduce a user-facing change? ```release-note 支持上传附件至本地时总是随机命名文件名 ```pull/7313/head v2.20.18
parent
39f6f09dcc
commit
e2fd9ba60b
|
@ -12,6 +12,7 @@ import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.FileAlreadyExistsException;
|
import java.nio.file.FileAlreadyExistsException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.time.Clock;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -21,6 +22,7 @@ import java.util.UUID;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
import org.reactivestreams.Publisher;
|
import org.reactivestreams.Publisher;
|
||||||
import org.springframework.core.io.buffer.DataBuffer;
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||||
|
@ -52,6 +54,7 @@ import run.halo.app.infra.FileCategoryMatcher;
|
||||||
import run.halo.app.infra.exception.AttachmentAlreadyExistsException;
|
import run.halo.app.infra.exception.AttachmentAlreadyExistsException;
|
||||||
import run.halo.app.infra.exception.FileSizeExceededException;
|
import run.halo.app.infra.exception.FileSizeExceededException;
|
||||||
import run.halo.app.infra.exception.FileTypeNotAllowedException;
|
import run.halo.app.infra.exception.FileTypeNotAllowedException;
|
||||||
|
import run.halo.app.infra.utils.FileNameUtils;
|
||||||
import run.halo.app.infra.utils.FileTypeDetectUtils;
|
import run.halo.app.infra.utils.FileTypeDetectUtils;
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
|
||||||
|
@ -63,30 +66,45 @@ class LocalAttachmentUploadHandler implements AttachmentHandler {
|
||||||
|
|
||||||
private final ExternalUrlSupplier externalUrl;
|
private final ExternalUrlSupplier externalUrl;
|
||||||
|
|
||||||
|
private Clock clock = Clock.systemUTC();
|
||||||
|
|
||||||
public LocalAttachmentUploadHandler(AttachmentRootGetter attachmentDirGetter,
|
public LocalAttachmentUploadHandler(AttachmentRootGetter attachmentDirGetter,
|
||||||
ExternalUrlSupplier externalUrl) {
|
ExternalUrlSupplier externalUrl) {
|
||||||
this.attachmentDirGetter = attachmentDirGetter;
|
this.attachmentDirGetter = attachmentDirGetter;
|
||||||
this.externalUrl = externalUrl;
|
this.externalUrl = externalUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set clock for test.
|
||||||
|
*
|
||||||
|
* @param clock new clock
|
||||||
|
*/
|
||||||
|
void setClock(Clock clock) {
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Attachment> upload(UploadContext uploadOption) {
|
public Mono<Attachment> upload(UploadContext uploadOption) {
|
||||||
return Mono.just(uploadOption)
|
return Mono.just(uploadOption)
|
||||||
.filter(option -> this.shouldHandle(option.policy()))
|
.filter(option -> this.shouldHandle(option.policy()))
|
||||||
.flatMap(option -> {
|
.flatMap(option -> {
|
||||||
var configMap = option.configMap();
|
var configMap = option.configMap();
|
||||||
var settingJson = configMap.getData().getOrDefault("default", "{}");
|
var setting = Optional.ofNullable(configMap)
|
||||||
var setting = JsonUtils.jsonToObject(settingJson, PolicySetting.class);
|
.map(ConfigMap::getData)
|
||||||
|
.map(data -> data.get("default"))
|
||||||
|
.map(json -> JsonUtils.jsonToObject(json, PolicySetting.class))
|
||||||
|
.orElseGet(PolicySetting::new);
|
||||||
|
|
||||||
final var attachmentsRoot = attachmentDirGetter.get();
|
final var attachmentsRoot = attachmentDirGetter.get();
|
||||||
final var uploadRoot = attachmentsRoot.resolve("upload");
|
final var uploadRoot = attachmentsRoot.resolve("upload");
|
||||||
final var file = option.file();
|
final var file = option.file();
|
||||||
final Path attachmentPath;
|
final Path attachmentPath;
|
||||||
|
final String filename = getFilename(file.filename(), setting);
|
||||||
if (StringUtils.hasText(setting.getLocation())) {
|
if (StringUtils.hasText(setting.getLocation())) {
|
||||||
attachmentPath =
|
attachmentPath =
|
||||||
uploadRoot.resolve(setting.getLocation()).resolve(file.filename());
|
uploadRoot.resolve(setting.getLocation()).resolve(filename);
|
||||||
} else {
|
} else {
|
||||||
attachmentPath = uploadRoot.resolve(file.filename());
|
attachmentPath = uploadRoot.resolve(filename);
|
||||||
}
|
}
|
||||||
checkDirectoryTraversal(uploadRoot, attachmentPath);
|
checkDirectoryTraversal(uploadRoot, attachmentPath);
|
||||||
|
|
||||||
|
@ -102,7 +120,7 @@ class LocalAttachmentUploadHandler implements AttachmentHandler {
|
||||||
.subscribeOn(Schedulers.boundedElastic())
|
.subscribeOn(Schedulers.boundedElastic())
|
||||||
.then(writeContent(file.content(), attachmentPath, true))
|
.then(writeContent(file.content(), attachmentPath, true))
|
||||||
.map(path -> {
|
.map(path -> {
|
||||||
log.info("Wrote attachment {} into {}", file.filename(), path);
|
log.info("Wrote attachment {} into {}", filename, path);
|
||||||
// TODO check the file extension
|
// TODO check the file extension
|
||||||
var metadata = new Metadata();
|
var metadata = new Metadata();
|
||||||
metadata.setName(UUID.randomUUID().toString());
|
metadata.setName(UUID.randomUUID().toString());
|
||||||
|
@ -305,6 +323,56 @@ class LocalAttachmentUploadHandler implements AttachmentHandler {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getFilename(String filename, PolicySetting setting) {
|
||||||
|
if (!setting.isAlwaysRenameFilename()) {
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
var renameStrategy = setting.getRenameStrategy();
|
||||||
|
if (renameStrategy == null) {
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
var renameMethod = renameStrategy.getMethod();
|
||||||
|
if (renameMethod == null) {
|
||||||
|
renameMethod = RenameMethod.RANDOM;
|
||||||
|
}
|
||||||
|
var excludeOriginalFilename = renameStrategy.isExcludeOriginalFilename();
|
||||||
|
switch (renameMethod) {
|
||||||
|
case TIMESTAMP -> {
|
||||||
|
return FileNameUtils.renameFilename(
|
||||||
|
filename,
|
||||||
|
() -> {
|
||||||
|
var now = clock.instant();
|
||||||
|
return now.toEpochMilli() + "";
|
||||||
|
},
|
||||||
|
excludeOriginalFilename);
|
||||||
|
}
|
||||||
|
case UUID -> {
|
||||||
|
return FileNameUtils.renameFilename(
|
||||||
|
filename,
|
||||||
|
() -> UUID.randomUUID().toString(),
|
||||||
|
excludeOriginalFilename
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
return FileNameUtils.renameFilename(
|
||||||
|
filename,
|
||||||
|
() -> {
|
||||||
|
var length = renameStrategy.getRandomLength();
|
||||||
|
if (length < 8) {
|
||||||
|
length = 8;
|
||||||
|
} else if (length > 64) {
|
||||||
|
// The max filename length is 256, so we limit the random length to 64
|
||||||
|
// for most cases.
|
||||||
|
length = 64;
|
||||||
|
}
|
||||||
|
return RandomStringUtils.secure().nextAlphabetic(length);
|
||||||
|
},
|
||||||
|
excludeOriginalFilename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class PolicySetting {
|
public static class PolicySetting {
|
||||||
|
|
||||||
|
@ -314,6 +382,10 @@ class LocalAttachmentUploadHandler implements AttachmentHandler {
|
||||||
|
|
||||||
private Set<String> allowedFileTypes;
|
private Set<String> allowedFileTypes;
|
||||||
|
|
||||||
|
private boolean alwaysRenameFilename;
|
||||||
|
|
||||||
|
private RenameStrategy renameStrategy;
|
||||||
|
|
||||||
public void setMaxFileSize(String maxFileSize) {
|
public void setMaxFileSize(String maxFileSize) {
|
||||||
if (!StringUtils.hasText(maxFileSize)) {
|
if (!StringUtils.hasText(maxFileSize)) {
|
||||||
return;
|
return;
|
||||||
|
@ -321,4 +393,22 @@ class LocalAttachmentUploadHandler implements AttachmentHandler {
|
||||||
this.maxFileSize = DataSize.parse(maxFileSize);
|
this.maxFileSize = DataSize.parse(maxFileSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum RenameMethod {
|
||||||
|
RANDOM,
|
||||||
|
UUID,
|
||||||
|
TIMESTAMP
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class RenameStrategy {
|
||||||
|
|
||||||
|
private RenameMethod method;
|
||||||
|
|
||||||
|
private int randomLength = 32;
|
||||||
|
|
||||||
|
private boolean excludeOriginalFilename;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package run.halo.app.infra.utils;
|
package run.halo.app.infra.utils;
|
||||||
|
|
||||||
import com.google.common.io.Files;
|
import com.google.common.io.Files;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import org.apache.commons.lang3.RandomStringUtils;
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
@ -45,15 +46,30 @@ public final class FileNameUtils {
|
||||||
* @return File name with random string.
|
* @return File name with random string.
|
||||||
*/
|
*/
|
||||||
public static String randomFileName(String filename, int length) {
|
public static String randomFileName(String filename, int length) {
|
||||||
|
return renameFilename(
|
||||||
|
filename, () -> RandomStringUtils.secure().nextAlphabetic(length), false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String renameFilename(
|
||||||
|
String filename,
|
||||||
|
Supplier<String> renameSupplier,
|
||||||
|
boolean excludeBasename) {
|
||||||
var nameWithoutExt = Files.getNameWithoutExtension(filename);
|
var nameWithoutExt = Files.getNameWithoutExtension(filename);
|
||||||
var ext = Files.getFileExtension(filename);
|
var ext = Files.getFileExtension(filename);
|
||||||
var random = RandomStringUtils.randomAlphabetic(length).toLowerCase();
|
var rename = renameSupplier.get();
|
||||||
if (StringUtils.isBlank(nameWithoutExt)) {
|
if (StringUtils.isBlank(nameWithoutExt)) {
|
||||||
return random + "." + ext;
|
return rename + "." + ext;
|
||||||
}
|
}
|
||||||
if (StringUtils.isBlank(ext)) {
|
if (StringUtils.isBlank(ext)) {
|
||||||
return nameWithoutExt + "-" + random;
|
if (excludeBasename) {
|
||||||
|
return rename;
|
||||||
|
}
|
||||||
|
return nameWithoutExt + "-" + rename;
|
||||||
}
|
}
|
||||||
return nameWithoutExt + "-" + random + "." + ext;
|
if (excludeBasename) {
|
||||||
|
return rename + "." + ext;
|
||||||
|
}
|
||||||
|
return nameWithoutExt + "-" + rename + "." + ext;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,39 @@ spec:
|
||||||
value: DOCUMENT
|
value: DOCUMENT
|
||||||
- label: 压缩包
|
- label: 压缩包
|
||||||
value: ARCHIVE
|
value: ARCHIVE
|
||||||
|
- $formkit: checkbox
|
||||||
|
name: alwaysRenameFilename
|
||||||
|
label: 是否总是重命名文件名
|
||||||
|
help: 勾选后上传后的文件名将被重命名
|
||||||
|
- $formkit: group
|
||||||
|
if: $alwaysRenameFilename
|
||||||
|
name: renameStrategy
|
||||||
|
label: 重命名策略
|
||||||
|
children:
|
||||||
|
- $formkit: radio
|
||||||
|
name: method
|
||||||
|
label: 重命名方法
|
||||||
|
options:
|
||||||
|
- label: 随机字符串
|
||||||
|
value: RANDOM
|
||||||
|
- label: UUID
|
||||||
|
value: UUID
|
||||||
|
- label: 时间戳(毫秒级)
|
||||||
|
value: TIMESTAMP
|
||||||
|
- $formkit: number
|
||||||
|
number: integer
|
||||||
|
if: $renameStrategy.renameMethod === RANDOM
|
||||||
|
name: randomLength
|
||||||
|
label: 随机文件名长度
|
||||||
|
help: 默认值为 32。因为文件名的长度限制,随机文件名的长度范围为 [8, 64]。
|
||||||
|
validation: "between:8,64"
|
||||||
|
validation-visibility: live
|
||||||
|
min: 8
|
||||||
|
max: 64
|
||||||
|
- $formkit: checkbox
|
||||||
|
name: excludeOriginalFilename
|
||||||
|
label: 是否排除原始文件名
|
||||||
|
help: 勾选后重命名后的文件名将不包含原始文件名
|
||||||
---
|
---
|
||||||
apiVersion: storage.halo.run/v1alpha1
|
apiVersion: storage.halo.run/v1alpha1
|
||||||
kind: Group
|
kind: Group
|
||||||
|
|
|
@ -0,0 +1,202 @@
|
||||||
|
package run.halo.app.core.attachment.endpoint;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.junit.jupiter.params.provider.Arguments.arguments;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
import run.halo.app.core.attachment.AttachmentRootGetter;
|
||||||
|
import run.halo.app.core.extension.attachment.Attachment;
|
||||||
|
import run.halo.app.core.extension.attachment.Policy;
|
||||||
|
import run.halo.app.core.extension.attachment.endpoint.UploadOption;
|
||||||
|
import run.halo.app.extension.ConfigMap;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class LocalAttachmentUploadHandlerTest {
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
LocalAttachmentUploadHandler uploadHandler;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
AttachmentRootGetter attachmentRootGetter;
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
static Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
uploadHandler.setClock(clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Stream<Arguments> testUploadWithRenameStrategy() {
|
||||||
|
return Stream.of(arguments(
|
||||||
|
"Random file name with length 10",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"alwaysRenameFilename": true,
|
||||||
|
"renameStrategy": {
|
||||||
|
"method": "RANDOM",
|
||||||
|
"randomLength": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
(Consumer<Attachment>) attachment -> {
|
||||||
|
var displayName = attachment.getSpec().getDisplayName();
|
||||||
|
assertTrue(displayName.startsWith("halo-"));
|
||||||
|
assertTrue(displayName.endsWith(".png"));
|
||||||
|
// halo-xxxxxx.png
|
||||||
|
assertEquals(4 + 10 + 5, displayName.length());
|
||||||
|
// fake-content
|
||||||
|
assertEquals(12L, attachment.getSpec().getSize());
|
||||||
|
}),
|
||||||
|
arguments(
|
||||||
|
"Random file name with length 10 but without original filename",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"alwaysRenameFilename": true,
|
||||||
|
"renameStrategy": {
|
||||||
|
"method": "RANDOM",
|
||||||
|
"randomLength": 10,
|
||||||
|
"excludeOriginalFilename": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
(Consumer<Attachment>) attachment -> {
|
||||||
|
var displayName = attachment.getSpec().getDisplayName();
|
||||||
|
assertFalse(displayName.startsWith("halo-"));
|
||||||
|
assertTrue(displayName.endsWith(".png"));
|
||||||
|
// halo-xxxxxx.png
|
||||||
|
assertEquals(10 + 4, displayName.length());
|
||||||
|
// fake-content
|
||||||
|
assertEquals(12L, attachment.getSpec().getSize());
|
||||||
|
}),
|
||||||
|
arguments(
|
||||||
|
"Rename filename with UUID but exclude original filename",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"alwaysRenameFilename": true,
|
||||||
|
"renameStrategy": {
|
||||||
|
"method": "UUID",
|
||||||
|
"excludeOriginalFilename": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
(Consumer<Attachment>) attachment -> {
|
||||||
|
var displayName = attachment.getSpec().getDisplayName();
|
||||||
|
assertFalse(displayName.startsWith("halo-"));
|
||||||
|
assertTrue(displayName.endsWith(".png"));
|
||||||
|
// xxxxxx.png
|
||||||
|
assertEquals(36 + 4, displayName.length());
|
||||||
|
// fake-content
|
||||||
|
assertEquals(12L, attachment.getSpec().getSize());
|
||||||
|
}
|
||||||
|
),
|
||||||
|
arguments(
|
||||||
|
"Rename filename with UUID",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"alwaysRenameFilename": true,
|
||||||
|
"renameStrategy": {
|
||||||
|
"method": "UUID",
|
||||||
|
"excludeOriginalFilename": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
(Consumer<Attachment>) attachment -> {
|
||||||
|
var displayName = attachment.getSpec().getDisplayName();
|
||||||
|
assertTrue(displayName.startsWith("halo-"));
|
||||||
|
assertTrue(displayName.endsWith(".png"));
|
||||||
|
// xxxxxx.png
|
||||||
|
assertEquals(5 + 36 + 4, displayName.length());
|
||||||
|
// fake-content
|
||||||
|
assertEquals(12L, attachment.getSpec().getSize());
|
||||||
|
}
|
||||||
|
),
|
||||||
|
arguments(
|
||||||
|
"Rename filename with timestamp but without original filename",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"alwaysRenameFilename": true,
|
||||||
|
"renameStrategy": {
|
||||||
|
"method": "TIMESTAMP",
|
||||||
|
"excludeOriginalFilename": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
(Consumer<Attachment>) attachment -> {
|
||||||
|
var expect = clock.instant().toEpochMilli() + ".png";
|
||||||
|
assertEquals(expect, attachment.getSpec().getDisplayName());
|
||||||
|
}
|
||||||
|
),
|
||||||
|
arguments(
|
||||||
|
"Rename filename with timestamp",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"alwaysRenameFilename": true,
|
||||||
|
"renameStrategy": {
|
||||||
|
"method": "TIMESTAMP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
(Consumer<Attachment>) attachment -> {
|
||||||
|
var expect = "halo-" + clock.instant().toEpochMilli() + ".png";
|
||||||
|
assertEquals(expect, attachment.getSpec().getDisplayName());
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0}")
|
||||||
|
@MethodSource
|
||||||
|
void testUploadWithRenameStrategy(String name, String config, Consumer<Attachment> assertion) {
|
||||||
|
assertNotNull(uploadHandler);
|
||||||
|
var dataBufferFactory = new DefaultDataBufferFactory();
|
||||||
|
var dataBuffer = dataBufferFactory.allocateBuffer(1024);
|
||||||
|
dataBuffer.write("fake content".getBytes(StandardCharsets.UTF_8));
|
||||||
|
var content = Flux.<DataBuffer>just(dataBuffer);
|
||||||
|
|
||||||
|
var policy = new Policy();
|
||||||
|
var policySpec = new Policy.PolicySpec();
|
||||||
|
policy.setSpec(policySpec);
|
||||||
|
policySpec.setTemplateName("local");
|
||||||
|
|
||||||
|
var configMap = new ConfigMap();
|
||||||
|
configMap.setData(Map.of("default", config));
|
||||||
|
|
||||||
|
var uploadOption =
|
||||||
|
UploadOption.from("halo.png", content, MediaType.IMAGE_PNG, policy, configMap);
|
||||||
|
|
||||||
|
when(attachmentRootGetter.get()).thenReturn(tempDir);
|
||||||
|
uploadHandler.upload(uploadOption)
|
||||||
|
.as(StepVerifier::create)
|
||||||
|
.assertNext(assertion)
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue