refactor: the naming method of uploaded attachments (#1500)

* feat: Refactor the naming method of uploaded attachments

* refactor: attachment upload parameter

* refactor: 重构文件名称是否重复的判断方式

* refactor: the usage of file path descriptor

* fix: remove blank line

* feat: add more test case

* fix: file base name in FilePathDescriptor

* Revert "refactor: the usage of file path descriptor"

This reverts commit b46ff3b4
pull/1509/head
guqing 2021-10-29 19:36:37 +08:00 committed by GitHub
parent 44d740b760
commit 1d91450505
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 498 additions and 212 deletions

View File

@ -18,8 +18,8 @@ import run.halo.app.exception.FileOperationException;
import run.halo.app.model.enums.AttachmentType; import run.halo.app.model.enums.AttachmentType;
import run.halo.app.model.properties.AliOssProperties; import run.halo.app.model.properties.AliOssProperties;
import run.halo.app.model.support.UploadResult; import run.halo.app.model.support.UploadResult;
import run.halo.app.repository.AttachmentRepository;
import run.halo.app.service.OptionService; import run.halo.app.service.OptionService;
import run.halo.app.utils.FilenameUtils;
import run.halo.app.utils.ImageUtils; import run.halo.app.utils.ImageUtils;
/** /**
@ -27,6 +27,7 @@ import run.halo.app.utils.ImageUtils;
* *
* @author MyFaith * @author MyFaith
* @author ryanwang * @author ryanwang
* @author guqing
* @date 2019-04-04 * @date 2019-04-04
*/ */
@Slf4j @Slf4j
@ -34,9 +35,12 @@ import run.halo.app.utils.ImageUtils;
public class AliOssFileHandler implements FileHandler { public class AliOssFileHandler implements FileHandler {
private final OptionService optionService; private final OptionService optionService;
private final AttachmentRepository attachmentRepository;
public AliOssFileHandler(OptionService optionService) { public AliOssFileHandler(OptionService optionService,
AttachmentRepository attachmentRepository) {
this.optionService = optionService; this.optionService = optionService;
this.attachmentRepository = attachmentRepository;
} }
@Override @Override
@ -79,30 +83,21 @@ public class AliOssFileHandler implements FileHandler {
} }
try { try {
final String basename = FilePathDescriptor uploadFilePath = new FilePathDescriptor.Builder()
FilenameUtils.getBasename(Objects.requireNonNull(file.getOriginalFilename())); .setBasePath(basePath.toString())
final String extension = FilenameUtils.getExtension(file.getOriginalFilename()); .setSubPath(source)
final String timestamp = String.valueOf(System.currentTimeMillis()); .setAutomaticRename(true)
final StringBuilder upFilePath = new StringBuilder(); .setRenamePredicate(relativePath ->
attachmentRepository
if (StringUtils.isNotEmpty(source)) { .countByFileKeyAndType(relativePath, AttachmentType.ALIOSS) > 0)
upFilePath.append(source) .setOriginalName(file.getOriginalFilename())
.append(URL_SEPARATOR); .build();
}
upFilePath.append(basename)
.append("_")
.append(timestamp)
.append(".")
.append(extension);
String filePath = StringUtils.join(basePath.toString(), upFilePath.toString());
log.info(basePath.toString()); log.info(basePath.toString());
// Upload // Upload
final PutObjectResult putObjectResult = ossClient.putObject(bucketName, final PutObjectResult putObjectResult = ossClient.putObject(bucketName,
upFilePath.toString(), uploadFilePath.getRelativePath(),
file.getInputStream()); file.getInputStream());
if (putObjectResult == null) { if (putObjectResult == null) {
@ -111,21 +106,22 @@ public class AliOssFileHandler implements FileHandler {
// Response result // Response result
final UploadResult uploadResult = new UploadResult(); final UploadResult uploadResult = new UploadResult();
uploadResult.setFilename(basename); uploadResult.setFilename(uploadFilePath.getName());
String fullPath = uploadFilePath.getFullPath();
uploadResult uploadResult
.setFilePath(StringUtils.isBlank(styleRule) ? filePath : filePath + styleRule); .setFilePath(StringUtils.isBlank(styleRule) ? fullPath : fullPath + styleRule);
uploadResult.setKey(upFilePath.toString()); uploadResult.setKey(uploadFilePath.getRelativePath());
uploadResult uploadResult
.setMediaType(MediaType.valueOf(Objects.requireNonNull(file.getContentType()))); .setMediaType(MediaType.valueOf(Objects.requireNonNull(file.getContentType())));
uploadResult.setSuffix(extension); uploadResult.setSuffix(uploadFilePath.getExtension());
uploadResult.setSize(file.getSize()); uploadResult.setSize(file.getSize());
handleImageMetadata(file, uploadResult, () -> { handleImageMetadata(file, uploadResult, () -> {
if (ImageUtils.EXTENSION_ICO.equals(extension)) { if (ImageUtils.EXTENSION_ICO.equals(uploadFilePath.getExtension())) {
return filePath; return fullPath;
} else { } else {
return StringUtils.isBlank(thumbnailStyleRule) ? filePath : return StringUtils.isBlank(thumbnailStyleRule) ? fullPath :
filePath + thumbnailStyleRule; fullPath + thumbnailStyleRule;
} }
}); });

View File

@ -1,6 +1,5 @@
package run.halo.app.handler.file; package run.halo.app.handler.file;
import com.baidubce.auth.DefaultBceCredentials; import com.baidubce.auth.DefaultBceCredentials;
import com.baidubce.services.bos.BosClient; import com.baidubce.services.bos.BosClient;
import com.baidubce.services.bos.BosClientConfiguration; import com.baidubce.services.bos.BosClientConfiguration;
@ -16,8 +15,8 @@ import run.halo.app.exception.FileOperationException;
import run.halo.app.model.enums.AttachmentType; import run.halo.app.model.enums.AttachmentType;
import run.halo.app.model.properties.BaiduBosProperties; import run.halo.app.model.properties.BaiduBosProperties;
import run.halo.app.model.support.UploadResult; import run.halo.app.model.support.UploadResult;
import run.halo.app.repository.AttachmentRepository;
import run.halo.app.service.OptionService; import run.halo.app.service.OptionService;
import run.halo.app.utils.FilenameUtils;
import run.halo.app.utils.ImageUtils; import run.halo.app.utils.ImageUtils;
/** /**
@ -32,9 +31,12 @@ import run.halo.app.utils.ImageUtils;
public class BaiduBosFileHandler implements FileHandler { public class BaiduBosFileHandler implements FileHandler {
private final OptionService optionService; private final OptionService optionService;
private final AttachmentRepository attachmentRepository;
public BaiduBosFileHandler(OptionService optionService) { public BaiduBosFileHandler(OptionService optionService,
AttachmentRepository attachmentRepository) {
this.optionService = optionService; this.optionService = optionService;
this.attachmentRepository = attachmentRepository;
} }
@Override @Override
@ -69,40 +71,42 @@ public class BaiduBosFileHandler implements FileHandler {
domain = protocol + domain; domain = protocol + domain;
try { try {
String basename = FilePathDescriptor pathDescriptor = new FilePathDescriptor.Builder()
FilenameUtils.getBasename(Objects.requireNonNull(file.getOriginalFilename())); .setBasePath(domain)
String extension = FilenameUtils.getExtension(file.getOriginalFilename()); .setSubPath(source)
String timestamp = String.valueOf(System.currentTimeMillis()); .setAutomaticRename(true)
String upFilePath = StringUtils.join(basename, "_", timestamp, ".", extension); .setRenamePredicate(relativePath ->
String filePath = StringUtils.join( attachmentRepository
StringUtils.appendIfMissing(StringUtils.isNotBlank(domain) ? domain : source, "/"), .countByFileKeyAndType(relativePath, AttachmentType.BAIDUBOS) > 0)
upFilePath); .setOriginalName(file.getOriginalFilename())
.build();
// Upload // Upload
PutObjectResponse putObjectResponseFromInputStream = PutObjectResponse putObjectResponseFromInputStream =
client.putObject(bucketName, upFilePath, file.getInputStream()); client.putObject(bucketName, pathDescriptor.getFullName(), file.getInputStream());
if (putObjectResponseFromInputStream == null) { if (putObjectResponseFromInputStream == null) {
throw new FileOperationException("上传附件 " + file.getOriginalFilename() + " 到百度云失败 "); throw new FileOperationException("上传附件 " + file.getOriginalFilename() + " 到百度云失败 ");
} }
// Response result // Response result
UploadResult uploadResult = new UploadResult(); UploadResult uploadResult = new UploadResult();
uploadResult.setFilename(basename); uploadResult.setFilename(pathDescriptor.getFullName());
String fullPath = pathDescriptor.getFullPath();
uploadResult uploadResult
.setFilePath(StringUtils.isBlank(styleRule) ? filePath : filePath + styleRule); .setFilePath(StringUtils.isBlank(styleRule) ? fullPath : fullPath + styleRule);
uploadResult.setKey(upFilePath); uploadResult.setKey(pathDescriptor.getRelativePath());
uploadResult uploadResult
.setMediaType(MediaType.valueOf(Objects.requireNonNull(file.getContentType()))); .setMediaType(MediaType.valueOf(Objects.requireNonNull(file.getContentType())));
uploadResult.setSuffix(extension); uploadResult.setSuffix(pathDescriptor.getExtension());
uploadResult.setSize(file.getSize()); uploadResult.setSize(file.getSize());
// Handle thumbnail // Handle thumbnail
handleImageMetadata(file, uploadResult, () -> { handleImageMetadata(file, uploadResult, () -> {
if (ImageUtils.EXTENSION_ICO.equals(extension)) { if (ImageUtils.EXTENSION_ICO.equals(pathDescriptor.getExtension())) {
return filePath; return fullPath;
} else { } else {
return StringUtils.isBlank(thumbnailStyleRule) ? filePath : return StringUtils.isBlank(thumbnailStyleRule) ? fullPath :
filePath + thumbnailStyleRule; fullPath + thumbnailStyleRule;
} }
}); });

View File

@ -0,0 +1,189 @@
package run.halo.app.handler.file;
import java.util.function.Predicate;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Assert;
import run.halo.app.utils.FilenameUtils;
/**
* File path descriptor.
*
* @author guqing
* @since 2021-10-21
*/
@Slf4j
@Data
@Accessors(chain = true)
public final class FilePathDescriptor {
private String name;
private String extension;
private String relativePath;
private String basePath;
private String fullName;
private String fullPath;
private String subPath;
public static final class Builder {
private String name;
private String extension;
private String subPath;
private String basePath;
private String nameSuffix = StringUtils.EMPTY;
private String separator = "/";
private boolean automaticRename;
private Predicate<String> renamePredicate;
private String relativePath;
public Builder setSubPath(String subPath) {
this.subPath = StringUtils.removeEnd(subPath, separator);
return this;
}
public Builder setAutomaticRename(Boolean automaticRename) {
this.automaticRename = automaticRename != null && automaticRename;
return this;
}
public Builder setRenamePredicate(Predicate<String> predicate) {
this.renamePredicate = predicate;
return this;
}
/**
* Set path separator, <code>NULL</code> value is not allowed.
*
* @param separator path separator
* @return builder
*/
public Builder setSeparator(String separator) {
if (separator == null) {
throw new IllegalArgumentException("The separator must not be null.");
}
this.separator = separator;
return this;
}
/**
* Set original file name.
*
* @param originalFileName original file name
* @return file path builder
*/
public Builder setOriginalName(String originalFileName) {
Assert.notNull(originalFileName, "The originalFileName must not be null.");
this.name = FilenameUtils.getBasename(originalFileName);
this.extension = FilenameUtils.getExtension(originalFileName);
return this;
}
public Builder setBasePath(String basePath) {
this.basePath = StringUtils.removeEnd(basePath, separator);
return this;
}
/**
* Set file base name suffix.
*
* @param nameSuffix file base name suffix
* @return builder
*/
public Builder setNameSuffix(String nameSuffix) {
if (nameSuffix == null) {
nameSuffix = StringUtils.EMPTY;
}
this.nameSuffix = nameSuffix;
return this;
}
String buildName() {
StringBuilder sb = new StringBuilder()
.append(this.name);
if (shouldRename()) {
String timestamp = String.valueOf(System.currentTimeMillis());
sb.append('-').append(timestamp);
}
sb.append(this.nameSuffix);
return sb.toString();
}
String getFullName() {
// eg. hello.jpg -> hello-uuid-thumbnail.jpg
if (StringUtils.isNotBlank(this.extension)) {
return this.name + '.' + this.extension;
}
return this.name;
}
String getFullPath() {
if (StringUtils.isNotBlank(this.basePath)) {
return getPath(this.basePath, this.subPath, this.getFullName());
}
return getPath(this.subPath, this.getFullName());
}
String getRelativePath() {
return getPath(this.subPath, getFullName());
}
private boolean shouldRename() {
if (!automaticRename) {
return false;
}
// automaticRename is true
if (renamePredicate == null) {
return true;
}
// renamePredicate not null
return renamePredicate.test(this.relativePath);
}
private String getPath(String first, String... more) {
String path;
if (more.length == 0) {
path = first;
} else {
StringBuilder sb = new StringBuilder();
sb.append(first);
for (String segment : more) {
if (StringUtils.isNotBlank(segment)) {
if (sb.length() > 0) {
sb.append(separator);
}
sb.append(segment);
}
}
path = sb.toString();
}
return path;
}
/**
* build file path object.
*
* @return file path
*/
public FilePathDescriptor build() {
// build relative path first, used to determine if it needs to be renamed
this.relativePath = getRelativePath();
// then build name, returns a new name if the relative path exists
this.name = buildName();
FilePathDescriptor descriptor = new FilePathDescriptor()
.setBasePath(this.basePath)
.setSubPath(this.subPath)
// regenerate relative path
.setRelativePath(getRelativePath())
.setName(this.name)
.setExtension(extension)
.setFullPath(getFullPath())
.setFullName(getFullName());
log.info("FilePathDescriptor: [{}]", descriptor);
return descriptor;
}
}
}

View File

@ -17,14 +17,15 @@ import run.halo.app.exception.FileOperationException;
import run.halo.app.model.enums.AttachmentType; import run.halo.app.model.enums.AttachmentType;
import run.halo.app.model.properties.HuaweiObsProperties; import run.halo.app.model.properties.HuaweiObsProperties;
import run.halo.app.model.support.UploadResult; import run.halo.app.model.support.UploadResult;
import run.halo.app.repository.AttachmentRepository;
import run.halo.app.service.OptionService; import run.halo.app.service.OptionService;
import run.halo.app.utils.FilenameUtils;
import run.halo.app.utils.ImageUtils; import run.halo.app.utils.ImageUtils;
/** /**
* Huawei obs file handler. * Huawei obs file handler.
* *
* @author qilin * @author qilin
* @author guqing
* @date 2020-04-03 * @date 2020-04-03
*/ */
@Slf4j @Slf4j
@ -32,9 +33,12 @@ import run.halo.app.utils.ImageUtils;
public class HuaweiObsFileHandler implements FileHandler { public class HuaweiObsFileHandler implements FileHandler {
private final OptionService optionService; private final OptionService optionService;
private final AttachmentRepository attachmentRepository;
public HuaweiObsFileHandler(OptionService optionService) { public HuaweiObsFileHandler(OptionService optionService,
AttachmentRepository attachmentRepository) {
this.optionService = optionService; this.optionService = optionService;
this.attachmentRepository = attachmentRepository;
} }
@Override @Override
@ -77,51 +81,44 @@ public class HuaweiObsFileHandler implements FileHandler {
} }
try { try {
String basename = FilePathDescriptor pathDescriptor = new FilePathDescriptor.Builder()
FilenameUtils.getBasename(Objects.requireNonNull(file.getOriginalFilename())); .setBasePath(domain)
String extension = FilenameUtils.getExtension(file.getOriginalFilename()); .setSubPath(source)
String timestamp = String.valueOf(System.currentTimeMillis()); .setAutomaticRename(true)
StringBuilder upFilePath = new StringBuilder(); .setRenamePredicate(relativePath ->
attachmentRepository
if (StringUtils.isNotEmpty(source)) { .countByFileKeyAndType(relativePath, AttachmentType.HUAWEIOBS) > 0)
upFilePath.append(source) .setOriginalName(file.getOriginalFilename())
.append(URL_SEPARATOR); .build();
}
upFilePath.append(basename)
.append("_")
.append(timestamp)
.append(".")
.append(extension);
String filePath = StringUtils.join(basePath.toString(), upFilePath.toString());
log.info(basePath.toString()); log.info(basePath.toString());
// Upload // Upload
PutObjectResult putObjectResult = PutObjectResult putObjectResult =
obsClient.putObject(bucketName, upFilePath.toString(), file.getInputStream()); obsClient.putObject(bucketName, pathDescriptor.getRelativePath(),
file.getInputStream());
if (putObjectResult == null) { if (putObjectResult == null) {
throw new FileOperationException("上传附件 " + file.getOriginalFilename() + " 到华为云失败 "); throw new FileOperationException("上传附件 " + file.getOriginalFilename() + " 到华为云失败 ");
} }
// Response result // Response result
UploadResult uploadResult = new UploadResult(); UploadResult uploadResult = new UploadResult();
uploadResult.setFilename(basename); uploadResult.setFilename(pathDescriptor.getName());
String fullPath = pathDescriptor.getFullPath();
uploadResult uploadResult
.setFilePath(StringUtils.isBlank(styleRule) ? filePath : filePath + styleRule); .setFilePath(StringUtils.isBlank(styleRule) ? fullPath : fullPath + styleRule);
uploadResult.setKey(upFilePath.toString()); uploadResult.setKey(pathDescriptor.getRelativePath());
uploadResult uploadResult
.setMediaType(MediaType.valueOf(Objects.requireNonNull(file.getContentType()))); .setMediaType(MediaType.valueOf(Objects.requireNonNull(file.getContentType())));
uploadResult.setSuffix(extension); uploadResult.setSuffix(pathDescriptor.getExtension());
uploadResult.setSize(file.getSize()); uploadResult.setSize(file.getSize());
handleImageMetadata(file, uploadResult, () -> { handleImageMetadata(file, uploadResult, () -> {
if (ImageUtils.EXTENSION_ICO.equals(extension)) { if (ImageUtils.EXTENSION_ICO.equals(pathDescriptor.getExtension())) {
return filePath; return fullPath;
} else { } else {
return StringUtils.isBlank(thumbnailStyleRule) ? filePath : return StringUtils.isBlank(thumbnailStyleRule) ? fullPath :
filePath + thumbnailStyleRule; fullPath + thumbnailStyleRule;
} }
}); });

View File

@ -13,6 +13,7 @@ import java.util.Objects;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails; import net.coobird.thumbnailator.Thumbnails;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@ -20,9 +21,8 @@ import run.halo.app.config.properties.HaloProperties;
import run.halo.app.exception.FileOperationException; import run.halo.app.exception.FileOperationException;
import run.halo.app.model.enums.AttachmentType; import run.halo.app.model.enums.AttachmentType;
import run.halo.app.model.support.UploadResult; import run.halo.app.model.support.UploadResult;
import run.halo.app.service.OptionService; import run.halo.app.repository.AttachmentRepository;
import run.halo.app.utils.FilenameUtils; import run.halo.app.utils.FilenameUtils;
import run.halo.app.utils.HaloUtils;
import run.halo.app.utils.ImageUtils; import run.halo.app.utils.ImageUtils;
/** /**
@ -30,6 +30,7 @@ import run.halo.app.utils.ImageUtils;
* *
* @author johnniang * @author johnniang
* @author ryanwang * @author ryanwang
* @author guqing
* @date 2019-03-27 * @date 2019-03-27
*/ */
@Slf4j @Slf4j
@ -53,13 +54,13 @@ public class LocalFileHandler implements FileHandler {
*/ */
private static final int THUMB_HEIGHT = 256; private static final int THUMB_HEIGHT = 256;
private final OptionService optionService; private final AttachmentRepository attachmentRepository;
private final String workDir; private final String workDir;
public LocalFileHandler(OptionService optionService, public LocalFileHandler(AttachmentRepository attachmentRepository,
HaloProperties haloProperties) { HaloProperties haloProperties) {
this.optionService = optionService; this.attachmentRepository = attachmentRepository;
// Get work dir // Get work dir
workDir = FileHandler.normalizeDirectory(haloProperties.getWorkDir()); workDir = FileHandler.normalizeDirectory(haloProperties.getWorkDir());
@ -83,57 +84,39 @@ public class LocalFileHandler implements FileHandler {
} }
} }
@NonNull
@Override @Override
public UploadResult upload(MultipartFile file) { public UploadResult upload(@NonNull MultipartFile file) {
Assert.notNull(file, "Multipart file must not be null"); Assert.notNull(file, "Multipart file must not be null");
// Get current time FilePathDescriptor uploadFilePath = new FilePathDescriptor.Builder()
Calendar current = Calendar.getInstance(optionService.getLocale()); .setBasePath(workDir)
// Get month and day of month .setSubPath(generatePath())
int year = current.get(Calendar.YEAR); .setSeparator(FILE_SEPARATOR)
int month = current.get(Calendar.MONTH) + 1; .setAutomaticRename(true)
.setRenamePredicate(relativePath ->
String monthString = month < 10 ? "0" + month : String.valueOf(month); attachmentRepository
.countByFileKeyAndType(relativePath, AttachmentType.LOCAL) > 0)
// Build directory .setOriginalName(file.getOriginalFilename())
String subDir = UPLOAD_SUB_DIR + year + FILE_SEPARATOR + monthString + FILE_SEPARATOR; .build();
log.info("Uploading file: [{}] to directory: [{}]", file.getOriginalFilename(),
String originalBasename = uploadFilePath.getRelativePath());
FilenameUtils.getBasename(Objects.requireNonNull(file.getOriginalFilename())); Path localFileFullPath = Paths.get(uploadFilePath.getFullPath());
// Get basename
String basename = originalBasename + '-' + HaloUtils.randomUUIDWithoutDash();
// Get extension
String extension = FilenameUtils.getExtension(file.getOriginalFilename());
log.debug("Base name: [{}], extension: [{}] of original filename: [{}]", basename,
extension, file.getOriginalFilename());
// Build sub file path
String subFilePath = subDir + basename + '.' + extension;
// Get upload path
Path uploadPath = Paths.get(workDir, subFilePath);
log.info("Uploading file: [{}]to directory: [{}]", file.getOriginalFilename(),
uploadPath.toString());
try { try {
// TODO Synchronize here // TODO Synchronize here
// Create directory // Create directory
Files.createDirectories(uploadPath.getParent()); Files.createDirectories(localFileFullPath.getParent());
Files.createFile(uploadPath); Files.createFile(localFileFullPath);
// Upload this file // Upload this file
file.transferTo(uploadPath); file.transferTo(localFileFullPath);
// Build upload result // Build upload result
UploadResult uploadResult = new UploadResult(); UploadResult uploadResult = new UploadResult();
uploadResult.setFilename(originalBasename); uploadResult.setFilename(uploadFilePath.getName());
uploadResult.setFilePath(subFilePath); uploadResult.setFilePath(uploadFilePath.getRelativePath());
uploadResult.setKey(subFilePath); uploadResult.setKey(uploadFilePath.getRelativePath());
uploadResult.setSuffix(extension); uploadResult.setSuffix(uploadFilePath.getExtension());
uploadResult uploadResult
.setMediaType(MediaType.valueOf(Objects.requireNonNull(file.getContentType()))); .setMediaType(MediaType.valueOf(Objects.requireNonNull(file.getContentType())));
uploadResult.setSize(file.getSize()); uploadResult.setSize(file.getSize());
@ -141,28 +124,35 @@ public class LocalFileHandler implements FileHandler {
// TODO refactor this: if image is svg ext. extension // TODO refactor this: if image is svg ext. extension
handleImageMetadata(file, uploadResult, () -> { handleImageMetadata(file, uploadResult, () -> {
// Upload a thumbnail // Upload a thumbnail
final String thumbnailBasename = basename + THUMBNAIL_SUFFIX; FilePathDescriptor thumbnailFilePath = new FilePathDescriptor.Builder()
final String thumbnailSubFilePath = subDir + thumbnailBasename + '.' + extension; .setBasePath(workDir)
final Path thumbnailPath = Paths.get(workDir + thumbnailSubFilePath); .setSubPath(uploadFilePath.getSubPath())
.setSeparator(FILE_SEPARATOR)
.setOriginalName(uploadFilePath.getFullName())
.setNameSuffix(THUMBNAIL_SUFFIX)
.build();
final Path thumbnailPath = Paths.get(thumbnailFilePath.getFullPath());
try (InputStream is = file.getInputStream()) { try (InputStream is = file.getInputStream()) {
// Generate thumbnail // Generate thumbnail
BufferedImage originalImage = ImageUtils.getImageFromFile(is, extension); BufferedImage originalImage =
boolean result = generateThumbnail(originalImage, thumbnailPath, extension); ImageUtils.getImageFromFile(is, uploadFilePath.getExtension());
boolean result = generateThumbnail(originalImage, thumbnailPath,
uploadFilePath.getExtension());
if (result) { if (result) {
// Set thumb path // Set thumb path
return thumbnailSubFilePath; return thumbnailFilePath.getRelativePath();
} }
} catch (Throwable e) { } catch (Throwable e) {
log.warn("Failed to open image file.", e); log.warn("Failed to open image file.", e);
} }
return subFilePath; return uploadFilePath.getRelativePath();
}); });
log.info("Uploaded file: [{}] to directory: [{}] successfully", log.info("Uploaded file: [{}] to directory: [{}] successfully",
file.getOriginalFilename(), uploadPath.toString()); file.getOriginalFilename(), uploadFilePath.getFullPath());
return uploadResult; return uploadResult;
} catch (IOException e) { } catch (IOException e) {
throw new FileOperationException("上传附件失败").setErrorData(uploadPath); throw new FileOperationException("上传附件失败").setErrorData(uploadFilePath.getFullPath());
} }
} }
@ -205,6 +195,19 @@ public class LocalFileHandler implements FileHandler {
return AttachmentType.LOCAL; return AttachmentType.LOCAL;
} }
private String generatePath() {
// Get current time
Calendar current = Calendar.getInstance();
// Get month and day of month
int year = current.get(Calendar.YEAR);
int month = current.get(Calendar.MONTH) + 1;
String monthString = month < 10 ? "0" + month : String.valueOf(month);
// Build directory
return UPLOAD_SUB_DIR + year + FILE_SEPARATOR + monthString + FILE_SEPARATOR;
}
private boolean generateThumbnail(BufferedImage originalImage, Path thumbPath, private boolean generateThumbnail(BufferedImage originalImage, Path thumbPath,
String extension) { String extension) {
Assert.notNull(originalImage, "Image must not be null"); Assert.notNull(originalImage, "Image must not be null");
@ -215,11 +218,10 @@ public class LocalFileHandler implements FileHandler {
try { try {
Files.createFile(thumbPath); Files.createFile(thumbPath);
// Convert to thumbnail and copy the thumbnail // Convert to thumbnail and copy the thumbnail
log.debug("Trying to generate thumbnail: [{}]", thumbPath.toString()); log.debug("Trying to generate thumbnail: [{}]", thumbPath);
Thumbnails.of(originalImage).size(THUMB_WIDTH, THUMB_HEIGHT).keepAspectRatio(true) Thumbnails.of(originalImage).size(THUMB_WIDTH, THUMB_HEIGHT).keepAspectRatio(true)
.toFile(thumbPath.toFile()); .toFile(thumbPath.toFile());
log.info("Generated thumbnail image, and wrote the thumbnail to [{}]", log.info("Generated thumbnail image, and wrote the thumbnail to [{}]", thumbPath);
thumbPath.toString());
result = true; result = true;
} catch (Throwable t) { } catch (Throwable t) {
// Ignore the error // Ignore the error

View File

@ -16,14 +16,14 @@ import run.halo.app.model.enums.AttachmentType;
import run.halo.app.model.properties.MinioProperties; import run.halo.app.model.properties.MinioProperties;
import run.halo.app.model.support.HaloConst; import run.halo.app.model.support.HaloConst;
import run.halo.app.model.support.UploadResult; import run.halo.app.model.support.UploadResult;
import run.halo.app.repository.AttachmentRepository;
import run.halo.app.service.OptionService; import run.halo.app.service.OptionService;
import run.halo.app.utils.FilenameUtils;
/** /**
* MinIO file handler. * MinIO file handler.
* *
* @author Wh1te * @author Wh1te
* @author guqing
* @date 2020-10-03 * @date 2020-10-03
*/ */
@Slf4j @Slf4j
@ -31,9 +31,12 @@ import run.halo.app.utils.FilenameUtils;
public class MinioFileHandler implements FileHandler { public class MinioFileHandler implements FileHandler {
private final OptionService optionService; private final OptionService optionService;
private final AttachmentRepository attachmentRepository;
public MinioFileHandler(OptionService optionService) { public MinioFileHandler(OptionService optionService,
AttachmentRepository attachmentRepository) {
this.optionService = optionService; this.optionService = optionService;
this.attachmentRepository = attachmentRepository;
} }
@NonNull @NonNull
@ -62,36 +65,36 @@ public class MinioFileHandler implements FileHandler {
.build(); .build();
try { try {
String basename = FilePathDescriptor pathDescriptor = new FilePathDescriptor.Builder()
FilenameUtils.getBasename(Objects.requireNonNull(file.getOriginalFilename())); .setBasePath(endpoint + bucketName)
String extension = FilenameUtils.getExtension(file.getOriginalFilename()); .setSubPath(source)
String timestamp = String.valueOf(System.currentTimeMillis()); .setAutomaticRename(true)
String upFilePath = StringUtils .setRenamePredicate(relativePath ->
.join(StringUtils.isNotBlank(source) ? source + HaloConst.URL_SEPARATOR : "", attachmentRepository
basename, "_", timestamp, ".", extension); .countByFileKeyAndType(relativePath, AttachmentType.MINIO) > 0)
String filePath = .setOriginalName(file.getOriginalFilename())
StringUtils.join(endpoint, bucketName, HaloConst.URL_SEPARATOR, upFilePath); .build();
PutObjectArgs putObjectArgs = PutObjectArgs.builder() PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.contentType(file.getContentType()) .contentType(file.getContentType())
.bucket(bucketName) .bucket(bucketName)
.stream(file.getInputStream(), file.getSize(), -1) .stream(file.getInputStream(), file.getSize(), -1)
.object(upFilePath) .object(pathDescriptor.getRelativePath())
.build(); .build();
minioClient.ignoreCertCheck(); minioClient.ignoreCertCheck();
minioClient.putObject(putObjectArgs); minioClient.putObject(putObjectArgs);
UploadResult uploadResult = new UploadResult(); UploadResult uploadResult = new UploadResult();
uploadResult.setFilename(basename); uploadResult.setFilename(pathDescriptor.getName());
uploadResult.setFilePath(filePath); uploadResult.setFilePath(pathDescriptor.getFullPath());
uploadResult.setKey(upFilePath); uploadResult.setKey(pathDescriptor.getRelativePath());
uploadResult uploadResult
.setMediaType(MediaType.valueOf(Objects.requireNonNull(file.getContentType()))); .setMediaType(MediaType.valueOf(Objects.requireNonNull(file.getContentType())));
uploadResult.setSuffix(extension); uploadResult.setSuffix(pathDescriptor.getExtension());
uploadResult.setSize(file.getSize()); uploadResult.setSize(file.getSize());
// Handle thumbnail // Handle thumbnail
handleImageMetadata(file, uploadResult, () -> filePath); handleImageMetadata(file, uploadResult, pathDescriptor::getFullPath);
return uploadResult; return uploadResult;
} catch (Exception e) { } catch (Exception e) {

View File

@ -30,8 +30,8 @@ import run.halo.app.exception.FileOperationException;
import run.halo.app.model.enums.AttachmentType; import run.halo.app.model.enums.AttachmentType;
import run.halo.app.model.properties.QiniuOssProperties; import run.halo.app.model.properties.QiniuOssProperties;
import run.halo.app.model.support.UploadResult; import run.halo.app.model.support.UploadResult;
import run.halo.app.repository.AttachmentRepository;
import run.halo.app.service.OptionService; import run.halo.app.service.OptionService;
import run.halo.app.utils.FilenameUtils;
import run.halo.app.utils.ImageUtils; import run.halo.app.utils.ImageUtils;
import run.halo.app.utils.JsonUtils; import run.halo.app.utils.JsonUtils;
@ -40,6 +40,7 @@ import run.halo.app.utils.JsonUtils;
* *
* @author johnniang * @author johnniang
* @author ryanwang * @author ryanwang
* @author guqing
* @date 2019-03-27 * @date 2019-03-27
*/ */
@Slf4j @Slf4j
@ -47,9 +48,12 @@ import run.halo.app.utils.JsonUtils;
public class QiniuOssFileHandler implements FileHandler { public class QiniuOssFileHandler implements FileHandler {
private final OptionService optionService; private final OptionService optionService;
private final AttachmentRepository attachmentRepository;
public QiniuOssFileHandler(OptionService optionService) { public QiniuOssFileHandler(OptionService optionService,
AttachmentRepository attachmentRepository) {
this.optionService = optionService; this.optionService = optionService;
this.attachmentRepository = attachmentRepository;
} }
@Override @Override
@ -96,20 +100,15 @@ public class QiniuOssFileHandler implements FileHandler {
.append(URL_SEPARATOR); .append(URL_SEPARATOR);
try { try {
String basename = FilePathDescriptor pathDescriptor = new FilePathDescriptor.Builder()
FilenameUtils.getBasename(Objects.requireNonNull(file.getOriginalFilename())); .setBasePath(basePath.toString())
String extension = FilenameUtils.getExtension(file.getOriginalFilename()); .setSubPath(source)
String timestamp = String.valueOf(System.currentTimeMillis()); .setAutomaticRename(true)
StringBuilder upFilePath = new StringBuilder(); .setRenamePredicate(relativePath ->
if (StringUtils.isNotEmpty(source)) { attachmentRepository
upFilePath.append(source) .countByFileKeyAndType(relativePath, AttachmentType.QINIUOSS) > 0)
.append(URL_SEPARATOR); .setOriginalName(file.getOriginalFilename())
} .build();
upFilePath.append(basename)
.append("_")
.append(timestamp)
.append(".")
.append(extension);
// Get file recorder for temp directory // Get file recorder for temp directory
FileRecorder fileRecorder = new FileRecorder(tmpPath.toFile()); FileRecorder fileRecorder = new FileRecorder(tmpPath.toFile());
@ -117,7 +116,8 @@ public class QiniuOssFileHandler implements FileHandler {
UploadManager uploadManager = new UploadManager(configuration, fileRecorder); UploadManager uploadManager = new UploadManager(configuration, fileRecorder);
// Put the file // Put the file
Response response = uploadManager Response response = uploadManager
.put(file.getInputStream(), upFilePath.toString(), uploadToken, null, null); .put(file.getInputStream(), pathDescriptor.getRelativePath(), uploadToken, null,
null);
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Qiniu oss response: [{}]", response.toString()); log.debug("Qiniu oss response: [{}]", response.toString());
@ -128,25 +128,26 @@ public class QiniuOssFileHandler implements FileHandler {
PutSet putSet = JsonUtils.jsonToObject(response.bodyString(), PutSet.class); PutSet putSet = JsonUtils.jsonToObject(response.bodyString(), PutSet.class);
// Get file full path // Get file full path
String filePath = StringUtils.join(basePath.toString(), upFilePath.toString()); String fullPath = pathDescriptor.getFullPath();
// Build upload result // Build upload result
UploadResult result = new UploadResult(); UploadResult result = new UploadResult();
result.setFilename(basename); result.setFilename(pathDescriptor.getName());
result.setFilePath(StringUtils.isBlank(styleRule) ? filePath : filePath + styleRule);
result.setKey(upFilePath.toString()); result.setFilePath(StringUtils.isBlank(styleRule) ? fullPath : fullPath + styleRule);
result.setSuffix(extension); result.setKey(pathDescriptor.getRelativePath());
result.setSuffix(pathDescriptor.getExtension());
result.setWidth(putSet.getWidth()); result.setWidth(putSet.getWidth());
result.setHeight(putSet.getHeight()); result.setHeight(putSet.getHeight());
result.setMediaType(MediaType.valueOf(Objects.requireNonNull(file.getContentType()))); result.setMediaType(MediaType.valueOf(Objects.requireNonNull(file.getContentType())));
result.setSize(file.getSize()); result.setSize(file.getSize());
if (isImageType(file)) { if (isImageType(file)) {
if (ImageUtils.EXTENSION_ICO.equals(extension)) { if (ImageUtils.EXTENSION_ICO.equals(pathDescriptor.getExtension())) {
result.setThumbPath(filePath); result.setThumbPath(fullPath);
} else { } else {
result.setThumbPath(StringUtils.isBlank(thumbnailStyleRule) ? filePath : result.setThumbPath(StringUtils.isBlank(thumbnailStyleRule) ? fullPath :
filePath + thumbnailStyleRule); fullPath + thumbnailStyleRule);
} }
} }

View File

@ -113,7 +113,7 @@ public class SmmsFileHandler implements FileHandler {
// Check status // Check status
if (mapResponseEntity.getStatusCode().isError()) { if (mapResponseEntity.getStatusCode().isError()) {
log.error("Server response detail: [{}]", mapResponseEntity.toString()); log.error("Server response detail: [{}]", mapResponseEntity);
throw new FileOperationException( throw new FileOperationException(
"SM.MS 服务状态异常,状态码: " + mapResponseEntity.getStatusCodeValue()); "SM.MS 服务状态异常,状态码: " + mapResponseEntity.getStatusCodeValue());
} }

View File

@ -1,6 +1,5 @@
package run.halo.app.handler.file; package run.halo.app.handler.file;
import static run.halo.app.model.support.HaloConst.URL_SEPARATOR; import static run.halo.app.model.support.HaloConst.URL_SEPARATOR;
import com.qcloud.cos.COSClient; import com.qcloud.cos.COSClient;
@ -21,8 +20,8 @@ import run.halo.app.exception.FileOperationException;
import run.halo.app.model.enums.AttachmentType; import run.halo.app.model.enums.AttachmentType;
import run.halo.app.model.properties.TencentCosProperties; import run.halo.app.model.properties.TencentCosProperties;
import run.halo.app.model.support.UploadResult; import run.halo.app.model.support.UploadResult;
import run.halo.app.repository.AttachmentRepository;
import run.halo.app.service.OptionService; import run.halo.app.service.OptionService;
import run.halo.app.utils.FilenameUtils;
import run.halo.app.utils.ImageUtils; import run.halo.app.utils.ImageUtils;
/** /**
@ -37,9 +36,12 @@ import run.halo.app.utils.ImageUtils;
public class TencentCosFileHandler implements FileHandler { public class TencentCosFileHandler implements FileHandler {
private final OptionService optionService; private final OptionService optionService;
private final AttachmentRepository attachmentRepository;
public TencentCosFileHandler(OptionService optionService) { public TencentCosFileHandler(OptionService optionService,
AttachmentRepository attachmentRepository) {
this.optionService = optionService; this.optionService = optionService;
this.attachmentRepository = attachmentRepository;
} }
@Override @Override
@ -88,24 +90,15 @@ public class TencentCosFileHandler implements FileHandler {
} }
try { try {
String basename = FilePathDescriptor pathDescriptor = new FilePathDescriptor.Builder()
FilenameUtils.getBasename(Objects.requireNonNull(file.getOriginalFilename())); .setBasePath(basePath.toString())
String extension = FilenameUtils.getExtension(file.getOriginalFilename()); .setSubPath(source)
String timestamp = String.valueOf(System.currentTimeMillis()); .setAutomaticRename(true)
StringBuilder upFilePath = new StringBuilder(); .setRenamePredicate(relativePath ->
attachmentRepository
if (StringUtils.isNotEmpty(source)) { .countByFileKeyAndType(relativePath, AttachmentType.TENCENTCOS) > 0)
upFilePath.append(source) .setOriginalName(file.getOriginalFilename())
.append(URL_SEPARATOR); .build();
}
upFilePath.append(basename)
.append("_")
.append(timestamp)
.append(".")
.append(extension);
String filePath = StringUtils.join(basePath.toString(), upFilePath.toString());
// Upload // Upload
ObjectMetadata objectMetadata = new ObjectMetadata(); ObjectMetadata objectMetadata = new ObjectMetadata();
@ -114,31 +107,31 @@ public class TencentCosFileHandler implements FileHandler {
// 设置 Content type, 默认是 application/octet-stream // 设置 Content type, 默认是 application/octet-stream
objectMetadata.setContentType(file.getContentType()); objectMetadata.setContentType(file.getContentType());
PutObjectResult putObjectResponseFromInputStream = cosClient PutObjectResult putObjectResponseFromInputStream = cosClient
.putObject(bucketName, upFilePath.toString(), file.getInputStream(), .putObject(bucketName, pathDescriptor.getRelativePath(), file.getInputStream(),
objectMetadata); objectMetadata);
if (putObjectResponseFromInputStream == null) { if (putObjectResponseFromInputStream == null) {
throw new FileOperationException("上传附件 " + file.getOriginalFilename() + " 到腾讯云失败 "); throw new FileOperationException("上传附件 " + file.getOriginalFilename() + " 到腾讯云失败 ");
} }
String fullPath = pathDescriptor.getFullPath();
// Response result // Response result
UploadResult uploadResult = new UploadResult(); UploadResult uploadResult = new UploadResult();
uploadResult.setFilename(basename); uploadResult.setFilename(pathDescriptor.getName());
uploadResult uploadResult
.setFilePath(StringUtils.isBlank(styleRule) ? filePath : filePath + styleRule); .setFilePath(StringUtils.isBlank(styleRule) ? fullPath : fullPath + styleRule);
uploadResult.setKey(upFilePath.toString()); uploadResult.setKey(pathDescriptor.getRelativePath());
uploadResult uploadResult
.setMediaType(MediaType.valueOf(Objects.requireNonNull(file.getContentType()))); .setMediaType(MediaType.valueOf(Objects.requireNonNull(file.getContentType())));
uploadResult.setSuffix(extension); uploadResult.setSuffix(pathDescriptor.getExtension());
uploadResult.setSize(file.getSize()); uploadResult.setSize(file.getSize());
// Handle thumbnail // Handle thumbnail
handleImageMetadata(file, uploadResult, () -> { handleImageMetadata(file, uploadResult, () -> {
if (ImageUtils.EXTENSION_ICO.equals(extension)) { if (ImageUtils.EXTENSION_ICO.equals(pathDescriptor.getExtension())) {
uploadResult.setThumbPath(filePath); uploadResult.setThumbPath(fullPath);
return filePath; return fullPath;
} else { } else {
return StringUtils.isBlank(thumbnailStyleRule) ? filePath : return StringUtils.isBlank(thumbnailStyleRule) ? fullPath :
filePath + thumbnailStyleRule; fullPath + thumbnailStyleRule;
} }
}); });
return uploadResult; return uploadResult;

View File

@ -107,6 +107,7 @@ public class UpOssFileHandler implements FileHandler {
filePath + thumbnailStyleRule; filePath + thumbnailStyleRule;
} }
}); });
result.close();
return uploadResult; return uploadResult;
} catch (Exception e) { } catch (Exception e) {
throw new FileOperationException("上传附件 " + file.getOriginalFilename() + " 到又拍云失败", e); throw new FileOperationException("上传附件 " + file.getOriginalFilename() + " 到又拍云失败", e);

View File

@ -41,4 +41,13 @@ public interface AttachmentRepository
* @return count of the given path * @return count of the given path
*/ */
long countByPath(@NonNull String path); long countByPath(@NonNull String path);
/**
* Counts by attachment file key and type.
*
* @param fileKey attachment file key must not be blank
* @param type attachment type must not be null
* @return count of the given path and type
*/
long countByFileKeyAndType(@NonNull String fileKey, @NonNull AttachmentType type);
} }

View File

@ -0,0 +1,90 @@
package run.halo.app.handler.file;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static run.halo.app.model.support.HaloConst.FILE_SEPARATOR;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* File path descriptor test case.
*
* @author guqing
* @since 2021-10-21
*/
public class FilePathDescriptorTest {
private FilePathDescriptor.Builder descriptorBuilder;
@BeforeEach
void setUp() {
descriptorBuilder = new FilePathDescriptor.Builder()
.setBasePath("/home/halo")
.setSubPath("2021/10/")
.setSeparator(FILE_SEPARATOR)
.setAutomaticRename(false)
.setRenamePredicate(builder -> true)
.setOriginalName("hello.jpg");
}
@Test
public void build() {
FilePathDescriptor descriptor = descriptorBuilder.build();
assertEquals("/home/halo/2021/10/hello.jpg", descriptor.getFullPath());
assertEquals("2021/10/hello.jpg", descriptor.getRelativePath());
}
@Test
public void autoRename() {
FilePathDescriptor descriptor = descriptorBuilder.setAutomaticRename(true).build();
assertNotEquals("/home/halo/2021/10/hello.jpg", descriptor.getFullPath());
assertNotEquals("2021/10/hello.jpg", descriptor.getRelativePath());
}
@Test
public void autoRenameWithPredicate() {
FilePathDescriptor descriptor1 = descriptorBuilder.setAutomaticRename(true)
.setRenamePredicate(path -> false).build();
assertEquals("/home/halo/2021/10/hello.jpg", descriptor1.getFullPath());
assertEquals("2021/10/hello.jpg", descriptor1.getRelativePath());
FilePathDescriptor descriptor2 = descriptorBuilder.setAutomaticRename(true)
.setRenamePredicate(path -> true).build();
assertNotEquals("/home/halo/2021/10/hello.jpg", descriptor2.getFullPath());
assertNotEquals("2021/10/hello.jpg", descriptor2.getRelativePath());
}
@Test
public void separator() {
FilePathDescriptor descriptor = descriptorBuilder.setSeparator("->").build();
assertEquals("/home/halo->2021/10->hello.jpg", descriptor.getFullPath());
assertEquals("2021/10->hello.jpg", descriptor.getRelativePath());
}
@Test
public void nameSuffix() {
FilePathDescriptor descriptor = descriptorBuilder.setNameSuffix("_thumbnail").build();
assertEquals("/home/halo/2021/10/hello_thumbnail.jpg", descriptor.getFullPath());
assertEquals("hello_thumbnail", descriptor.getName());
assertEquals("hello_thumbnail.jpg", descriptor.getFullName());
}
@Test
public void withoutExtension() {
FilePathDescriptor descriptor = descriptorBuilder.setOriginalName("hello").build();
assertEquals("hello", descriptor.getName());
assertEquals("", descriptor.getExtension());
assertEquals("hello", descriptor.getFullName());
assertEquals("2021/10/hello", descriptor.getRelativePath());
}
@Test
public void otherName() {
FilePathDescriptor descriptor = descriptorBuilder.setOriginalName("1.4.9.png").build();
assertEquals("1.4.9", descriptor.getName());
assertEquals("1.4.9.png", descriptor.getFullName());
assertEquals("/home/halo/2021/10/1.4.9.png", descriptor.getFullPath());
assertEquals("2021/10/1.4.9.png", descriptor.getRelativePath());
}
}

View File

@ -26,6 +26,7 @@ class FilenameUtilsTest {
assertEquals("", FilenameUtils.getBasename("a/b/c/")); assertEquals("", FilenameUtils.getBasename("a/b/c/"));
assertEquals("o", FilenameUtils.getBasename("he/ll/o.tar.gz")); assertEquals("o", FilenameUtils.getBasename("he/ll/o.tar.gz"));
assertEquals("i", FilenameUtils.getBasename("h/i.tar.bz2")); assertEquals("i", FilenameUtils.getBasename("h/i.tar.bz2"));
assertEquals("1.4.9", FilenameUtils.getBasename("1.4.9.png"));
} }
// foo.txt --> "txt" // foo.txt --> "txt"