初始化
This commit is contained in:
58
easyflow-commons/easyflow-common-file-storage/pom.xml
Normal file
58
easyflow-commons/easyflow-common-file-storage/pom.xml
Normal file
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-commons</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<name>easyflow-common-file-storage</name>
|
||||
<artifactId>easyflow-common-file-storage</artifactId>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-base</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara.x-file-storage</groupId>
|
||||
<artifactId>x-file-storage-spring</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.aliyun.oss</groupId>
|
||||
<artifactId>aliyun-sdk-oss</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,55 @@
|
||||
package tech.easyflow.common.filestorage;
|
||||
|
||||
import tech.easyflow.common.filestorage.xFileStorage.StorageConfig;
|
||||
import tech.easyflow.common.util.SpringContextUtil;
|
||||
import tech.easyflow.common.filestorage.impl.LocalFileStorageServiceImpl;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
@Component("default")
|
||||
public class FileStorageManager implements FileStorageService {
|
||||
|
||||
@Override
|
||||
public String save(MultipartFile file) {
|
||||
return getService().save(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String save(MultipartFile file,String prePath) {
|
||||
return getService().save(file,prePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) {
|
||||
getService().delete(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String save(File file, String prePath) {
|
||||
return getService().save(file, prePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream readStream(String path) throws IOException {
|
||||
return getService().readStream(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFileSize(String path) {
|
||||
return getService().getFileSize(path);
|
||||
}
|
||||
|
||||
private FileStorageService getService() {
|
||||
String type = StorageConfig.getInstance().getType();
|
||||
if (!StringUtils.hasText(type)) {
|
||||
return SpringContextUtil.getBean(LocalFileStorageServiceImpl.class);
|
||||
} else {
|
||||
return SpringContextUtil.getBean(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package tech.easyflow.common.filestorage;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public interface FileStorageService {
|
||||
|
||||
|
||||
String save(MultipartFile file);
|
||||
|
||||
|
||||
void delete(String path);
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param file 文件
|
||||
* @param prePath 存储桶和文件名中间的路径(不用加斜杠)
|
||||
* @return 文件url
|
||||
*/
|
||||
default String save(MultipartFile file, String prePath){
|
||||
return "";
|
||||
}
|
||||
|
||||
default String save(File file, String prePath){
|
||||
return "";
|
||||
}
|
||||
|
||||
InputStream readStream(String path) throws IOException;
|
||||
|
||||
/**
|
||||
* 获取文件大小
|
||||
* @param path
|
||||
* @return 文件大小 单位字节
|
||||
*/
|
||||
public long getFileSize(String path);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package tech.easyflow.common.filestorage.impl;
|
||||
|
||||
import org.dromara.x.file.storage.core.FileInfo;
|
||||
import org.dromara.x.file.storage.core.recorder.FileRecorder;
|
||||
import org.dromara.x.file.storage.core.upload.FilePartInfo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 文件记录器
|
||||
*/
|
||||
@Service
|
||||
public class FileRecoderImpl implements FileRecorder {
|
||||
@Override
|
||||
public boolean save(FileInfo fileInfo) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(FileInfo fileInfo) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileInfo getByUrl(String url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(String url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveFilePart(FilePartInfo filePartInfo) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteFilePartByUploadId(String uploadId) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package tech.easyflow.common.filestorage.impl;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.common.filestorage.FileStorageService;
|
||||
import tech.easyflow.common.filestorage.utils.PathGeneratorUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
|
||||
@Component("local")
|
||||
public class LocalFileStorageServiceImpl implements FileStorageService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(LocalFileStorageServiceImpl.class);
|
||||
|
||||
|
||||
@Value("${easyflow.storage.local.root:}")
|
||||
private String root;
|
||||
@Value("${easyflow.storage.local.prefix}")
|
||||
private String prefix;
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void init() {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String save(MultipartFile file) {
|
||||
try {
|
||||
String path = PathGeneratorUtil.generateUserPath(file.getOriginalFilename());
|
||||
File target = getLocalFile(path);
|
||||
if (!target.getParentFile().exists() && !target.getParentFile().mkdirs()) {
|
||||
LOG.error("创建文件失败: {} ", target.getParentFile());
|
||||
}
|
||||
file.transferTo(target);
|
||||
return prefix + path;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream readStream(String path) throws IOException {
|
||||
File target = getLocalFile(path);
|
||||
return Files.newInputStream(target.toPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFileSize(String path) {
|
||||
File target = null;
|
||||
try {
|
||||
target = getLocalFile(path);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("获取文件大小出错", e);
|
||||
}
|
||||
if (target.exists()) {
|
||||
return target.length();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) {
|
||||
try {
|
||||
File file = getLocalFile(path);
|
||||
Files.delete(file.toPath());
|
||||
} catch (IOException e) {
|
||||
LOG.error("删除本地文件出错: {}", path, e);
|
||||
throw new RuntimeException("删除本地文件出错:",e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归删除文件或目录(支持删除非空目录)
|
||||
* @param file 要删除的文件或目录
|
||||
*/
|
||||
private void deleteRecursively(File file) throws Exception {
|
||||
if (file == null || !file.exists()) {
|
||||
LOG.warn("文件/目录不存在: {}", file);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是目录,先递归删除子文件和子目录
|
||||
if (file.isDirectory()) {
|
||||
File[] children = file.listFiles();
|
||||
if (children != null) { // 防止null(目录可能被其他进程修改)
|
||||
for (File child : children) {
|
||||
deleteRecursively(child); // 递归删除子内容
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除当前文件或空目录
|
||||
boolean deleted = file.delete();
|
||||
if (!deleted) {
|
||||
throw new Exception("无法删除文件/目录: " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private File getLocalFile(String path) throws IOException {
|
||||
if (this.root == null || this.root.isEmpty()) {
|
||||
throw new RuntimeException("请指定存储根目录");
|
||||
}
|
||||
return new File(this.root, path.replace(prefix, ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String save(MultipartFile file, String prePath) {
|
||||
return save(file);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package tech.easyflow.common.filestorage.impl;
|
||||
|
||||
import org.dromara.x.file.storage.core.FileInfo;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.common.filestorage.FileStorageService;
|
||||
import tech.easyflow.common.filestorage.utils.PathGeneratorUtil;
|
||||
import tech.easyflow.common.util.OkHttpUtil;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
@Component("xFileStorage")
|
||||
public class XFIleStorageServiceImpl implements FileStorageService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XFIleStorageServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private org.dromara.x.file.storage.core.FileStorageService fileStorageService;
|
||||
|
||||
@Override
|
||||
public String save(MultipartFile file) {
|
||||
FileInfo fileInfo = fileStorageService.of(file)
|
||||
.setPath(PathGeneratorUtil.generateUserPath(""))
|
||||
.setSaveFilename(file.getOriginalFilename())
|
||||
.setContentType(getFileContentType(file))
|
||||
.upload();
|
||||
return fileInfo == null ? "上传失败!" : fileInfo.getUrl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) {
|
||||
fileStorageService.delete(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream readStream(String fileUrl) {
|
||||
return OkHttpUtil.getInputStream(fileUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取S3中文件的大小(单位:字节)
|
||||
* @param path 文件路径
|
||||
* @return 文件大小(字节)
|
||||
*/
|
||||
@Override
|
||||
public long getFileSize(String path) {
|
||||
try {
|
||||
return OkHttpUtil.getFileSize(path);
|
||||
} catch (Exception e) {
|
||||
LOG.error("获取文件大小失败", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件的 Content-Type
|
||||
*/
|
||||
public static String getFileContentType(MultipartFile file) {
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
String contentType = null;
|
||||
if (originalFilename != null && originalFilename.toLowerCase().endsWith(".txt")) {
|
||||
contentType = "text/plain; charset=utf-8";
|
||||
} else {
|
||||
// 其他类型文件可以按需设置
|
||||
contentType = file.getContentType();
|
||||
}
|
||||
return contentType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package tech.easyflow.common.filestorage.utils;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import tech.easyflow.common.util.StringUtil;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 路径生成工具类:年/月/日分别作为独立目录,格式为 "/年/月/日/{uuid}/文件名"
|
||||
*/
|
||||
public class PathGeneratorUtil {
|
||||
|
||||
public static String generateUserPath(String fileName) {
|
||||
return "/" + getAccountLoginIdOrCommons() + PathGeneratorUtil.generatePath(fileName);
|
||||
}
|
||||
|
||||
private static String getAccountLoginIdOrCommons() {
|
||||
try {
|
||||
String loginIdAsString = StpUtil.getLoginIdAsString();
|
||||
return StringUtil.hasText(loginIdAsString) ? loginIdAsString : "commons";
|
||||
} catch (Exception e) {
|
||||
return "commons";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 使用当前日期 + 自动生成UUID + 自定义文件名,生成路径
|
||||
*
|
||||
* @param fileName 文件名(含后缀,如 "video.mp4")
|
||||
* @return 示例:"/2024/10/15/1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed/video.mp4"
|
||||
*/
|
||||
public static String generatePath(String fileName) {
|
||||
LocalDate currentDate = LocalDate.now();
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
return buildPath(currentDate, uuid, fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用当前日期 - 1天
|
||||
*
|
||||
* @return 示例:"yyyy/MM/dd"(如 2024/10/19)
|
||||
*/
|
||||
public static String generatePrePath() {
|
||||
LocalDate yesterday = LocalDate.now().minusDays(2);
|
||||
// 自定义格式为 "yyyy/MM/dd"(如 2024/10/19)
|
||||
return yesterday.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部拼接路径核心方法(年/月/日独立目录)
|
||||
*/
|
||||
private static String buildPath(LocalDate date, String uuid, String fileName) {
|
||||
int year = date.getYear(); // 年(如 2024)
|
||||
int month = date.getMonthValue(); // 月(如 10,1-12)
|
||||
int day = date.getDayOfMonth(); // 日(如 15)
|
||||
|
||||
// 处理文件名(剔除可能包含的路径,只保留纯文件名)
|
||||
String pureFileName = getPureFileName(fileName);
|
||||
|
||||
// 拼接格式:/年/月/日/uuid/文件名
|
||||
return String.format("/%d/%d/%d/%s/%s", year, month, day, uuid, pureFileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取纯文件名(避免文件名包含路径分隔符)
|
||||
*/
|
||||
public static String getPureFileName(String fileName) {
|
||||
if (fileName == null) {
|
||||
return "";
|
||||
}
|
||||
// 处理 Windows(\)和 Linux(/)的路径分隔符
|
||||
int lastBackslash = fileName.lastIndexOf("\\");
|
||||
int lastSlash = fileName.lastIndexOf("/");
|
||||
int lastSeparator = Math.max(lastBackslash, lastSlash);
|
||||
return lastSeparator == -1 ? fileName : fileName.substring(lastSeparator + 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package tech.easyflow.common.filestorage.xFileStorage;
|
||||
|
||||
import tech.easyflow.common.util.SpringContextUtil;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "easyflow.storage")
|
||||
public class StorageConfig {
|
||||
|
||||
//支持 local、s3、xfile...
|
||||
private String type;
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public static StorageConfig getInstance() {
|
||||
return SpringContextUtil.getBean(StorageConfig.class);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user