完善 yudao-spring-boot-starter-file 组件,支持 S3 对接云存储、local、ftp、sftp 等协议

This commit is contained in:
YunaiV 2022-03-14 22:09:41 +08:00
parent ed53ca3de9
commit 3d40fc81dd
15 changed files with 514 additions and 10 deletions

View File

@ -22,13 +22,40 @@ public class FileUtils {
*/
@SneakyThrows
public static File createTempFile(String data) {
// 创建文件通过 UUID 保证唯一
File file = File.createTempFile(IdUtil.simpleUUID(), null);
// 标记 JVM 退出时自动删除
file.deleteOnExit();
File file = createTempFile();
// 写入内容
FileUtil.writeUtf8String(data, file);
return file;
}
/**
* 创建临时文件
* 该文件会在 JVM 退出时进行删除
*
* @param data 文件内容
* @return 文件
*/
@SneakyThrows
public static File createTempFile(byte[] data) {
File file = createTempFile();
// 写入内容
FileUtil.writeBytes(data, file);
return file;
}
/**
* 创建临时文件无内容
* 该文件会在 JVM 退出时进行删除
*
* @return 文件
*/
@SneakyThrows
public static File createTempFile() {
// 创建文件通过 UUID 保证唯一
File file = File.createTempFile(IdUtil.simpleUUID(), null);
// 标记 JVM 退出时自动删除
file.deleteOnExit();
return file;
}
}

View File

@ -51,6 +51,18 @@
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId> <!-- 解决 ftp 连接 -->
<version>3.8.0</version>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId> <!-- 解决 sftp 连接 -->
<version>0.1.55</version>
</dependency>
<!-- 三方云服务相关 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.file.core.client.impl;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.file.core.client.FileClient;
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
import lombok.extern.slf4j.Slf4j;
@ -55,4 +56,16 @@ public abstract class AbstractFileClient<Config extends FileClientConfig> implem
return id;
}
/**
* 格式化文件的 URL 访问地址
* 使用场景localftpdb通过 FileController getFile 来获取文件内容
*
* @param domain 自定义域名
* @param path 文件路径
* @return URL 访问地址
*/
protected String formatFileUrl(String domain, String path) {
return StrUtil.format("{}/system-api/{}/get/{}", domain, getId(), path);
}
}

View File

@ -0,0 +1,73 @@
package cn.iocoder.yudao.framework.file.core.client.impl.ftp;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.ftp.Ftp;
import cn.hutool.extra.ftp.FtpException;
import cn.hutool.extra.ftp.FtpMode;
import cn.iocoder.yudao.framework.file.core.client.impl.AbstractFileClient;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
/**
* Ftp 文件客户端
*
* @author 芋道源码
*/
public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> {
private Ftp ftp;
public FtpFileClient(Long id, FtpFileClientConfig config) {
super(id, config);
}
@Override
protected void doInit() {
// 补全风格例如说 Linux /Windows \
if (!config.getBasePath().endsWith(File.separator)) {
config.setBasePath(config.getBasePath() + File.separator);
}
// 初始化 Ftp 对象
this.ftp = new Ftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(),
CharsetUtil.CHARSET_UTF_8, null, null, FtpMode.valueOf(config.getMode()));
}
@Override
public String upload(byte[] content, String path) {
// 执行写入
String filePath = getFilePath(path);
String fileName = FileUtil.getName(filePath);
String dir = StrUtil.removeSuffix(filePath, fileName);
boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content));
if (!success) {
throw new FtpException(StrUtil.format("上海文件到目标目录 ({}) 失败", filePath));
}
// 拼接返回路径
return super.formatFileUrl(config.getDomain(), path);
}
@Override
public void delete(String path) {
String filePath = getFilePath(path);
ftp.delFile(filePath);
}
@Override
public byte[] getContent(String path) {
String filePath = getFilePath(path);
String fileName = FileUtil.getName(filePath);
String dir = StrUtil.removeSuffix(path, fileName);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ftp.download(dir, fileName, out);
return out.toByteArray();
}
private String getFilePath(String path) {
return config.getBasePath() + path;
}
}

View File

@ -0,0 +1,59 @@
package cn.iocoder.yudao.framework.file.core.client.impl.ftp;
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
* Ftp 文件客户端的配置类
*
* @author 芋道源码
*/
@Data
public class FtpFileClientConfig implements FileClientConfig {
/**
* 基础路径
*/
@NotEmpty(message = "基础路径不能为空")
private String basePath;
/**
* 自定义域名
*/
@NotEmpty(message = "domain 不能为空")
@URL(message = "domain 必须是 URL 格式")
private String domain;
/**
* 主机地址
*/
@NotEmpty(message = "host 不能为空")
private String host;
/**
* 主机端口
*/
@NotNull(message = "port 不能为空")
private Integer port;
/**
* 用户名
*/
@NotEmpty(message = "用户名不能为空")
private String username;
/**
* 密码
*/
@NotEmpty(message = "密码不能为空")
private String password;
/**
* 连接模式
*
* 使用 {@link cn.hutool.extra.ftp.FtpMode} 对应的字符串
*/
@NotEmpty(message = "连接模式不能为空")
private String mode;
}

View File

@ -0,0 +1,52 @@
package cn.iocoder.yudao.framework.file.core.client.impl.local;
import cn.hutool.core.io.FileUtil;
import cn.iocoder.yudao.framework.file.core.client.impl.AbstractFileClient;
import java.io.File;
/**
* 本地文件客户端
*
* @author 芋道源码
*/
public class LocalFileClient extends AbstractFileClient<LocalFileClientConfig> {
public LocalFileClient(Long id, LocalFileClientConfig config) {
super(id, config);
}
@Override
protected void doInit() {
// 补全风格例如说 Linux /Windows \
if (!config.getBasePath().endsWith(File.separator)) {
config.setBasePath(config.getBasePath() + File.separator);
}
}
@Override
public String upload(byte[] content, String path) {
// 执行写入
String filePath = getFilePath(path);
FileUtil.writeBytes(content, filePath);
// 拼接返回路径
return super.formatFileUrl(config.getDomain(), path);
}
@Override
public void delete(String path) {
String filePath = getFilePath(path);
FileUtil.del(filePath);
}
@Override
public byte[] getContent(String path) {
String filePath = getFilePath(path);
return FileUtil.readBytes(filePath);
}
private String getFilePath(String path) {
return config.getBasePath() + path;
}
}

View File

@ -0,0 +1,30 @@
package cn.iocoder.yudao.framework.file.core.client.impl.local;
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.NotEmpty;
/**
* 本地文件客户端的配置类
*
* @author 芋道源码
*/
@Data
public class LocalFileClientConfig implements FileClientConfig {
/**
* 基础路径
*/
@NotEmpty(message = "基础路径不能为空")
private String basePath;
/**
* 自定义域名
*/
@NotEmpty(message = "domain 不能为空")
@URL(message = "domain 必须是 URL 格式")
private String domain;
}

View File

@ -7,6 +7,8 @@ import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import java.net.URI;
@ -14,7 +16,7 @@ import java.net.URI;
import static cn.iocoder.yudao.framework.file.core.client.impl.s3.S3FileClientConfig.ENDPOINT_QINIU;
/**
* 基于 S3 协议实现 MinIO阿里云腾讯云七牛云华为云等云服务
* 基于 S3 协议的文件客户端实现 MinIO阿里云腾讯云七牛云华为云等云服务
*
* S3 协议的客户端采用亚马逊提供的 software.amazon.awssdk.s3
*
@ -84,12 +86,18 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
@Override
public void delete(String path) {
DeleteObjectRequest.Builder request = DeleteObjectRequest.builder()
.bucket(config.getBucket()) // bucket 必须传递
.key(path); // 相对路径作为 key
client.deleteObject(request.build());
}
@Override
public byte[] getContent(String path) {
return new byte[0];
GetObjectRequest.Builder request = GetObjectRequest.builder()
.bucket(config.getBucket()) // bucket 必须传递
.key(path); // 相对路径作为 key
return client.getObjectAsBytes(request.build()).asByteArray();
}
}

View File

@ -0,0 +1,61 @@
package cn.iocoder.yudao.framework.file.core.client.impl.sftp;
import cn.hutool.core.io.FileUtil;
import cn.hutool.extra.ssh.Sftp;
import cn.iocoder.yudao.framework.common.util.io.FileUtils;
import cn.iocoder.yudao.framework.file.core.client.impl.AbstractFileClient;
import java.io.File;
/**
* Sftp 文件客户端
*
* @author 芋道源码
*/
public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> {
private Sftp sftp;
public SftpFileClient(Long id, SftpFileClientConfig config) {
super(id, config);
}
@Override
protected void doInit() {
// 补全风格例如说 Linux /Windows \
if (!config.getBasePath().endsWith(File.separator)) {
config.setBasePath(config.getBasePath() + File.separator);
}
// 初始化 Ftp 对象
this.sftp = new Sftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword());
}
@Override
public String upload(byte[] content, String path) {
// 执行写入
String filePath = getFilePath(path);
File file = FileUtils.createTempFile(content);
sftp.upload(filePath, file);
// 拼接返回路径
return super.formatFileUrl(config.getDomain(), path);
}
@Override
public void delete(String path) {
String filePath = getFilePath(path);
sftp.delFile(filePath);
}
@Override
public byte[] getContent(String path) {
String filePath = getFilePath(path);
File destFile = FileUtils.createTempFile();
sftp.download(filePath, destFile);
return FileUtil.readBytes(destFile);
}
private String getFilePath(String path) {
return config.getBasePath() + path;
}
}

View File

@ -0,0 +1,52 @@
package cn.iocoder.yudao.framework.file.core.client.impl.sftp;
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
* Sftp 文件客户端的配置类
*
* @author 芋道源码
*/
@Data
public class SftpFileClientConfig implements FileClientConfig {
/**
* 基础路径
*/
@NotEmpty(message = "基础路径不能为空")
private String basePath;
/**
* 自定义域名
*/
@NotEmpty(message = "domain 不能为空")
@URL(message = "domain 必须是 URL 格式")
private String domain;
/**
* 主机地址
*/
@NotEmpty(message = "host 不能为空")
private String host;
/**
* 主机端口
*/
@NotNull(message = "port 不能为空")
private Integer port;
/**
* 用户名
*/
@NotEmpty(message = "用户名不能为空")
private String username;
/**
* 密码
*/
@NotEmpty(message = "密码不能为空")
private String password;
}

View File

@ -0,0 +1,39 @@
package cn.iocoder.yudao.framework.file.core.client.ftp;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.extra.ftp.FtpMode;
import cn.iocoder.yudao.framework.file.core.client.impl.ftp.FtpFileClient;
import cn.iocoder.yudao.framework.file.core.client.impl.ftp.FtpFileClientConfig;
import org.junit.jupiter.api.Test;
public class FtpFileClientTest {
@Test
public void test() {
// 创建客户端
FtpFileClientConfig config = new FtpFileClientConfig();
config.setDomain("http://127.0.0.1:48080");
config.setBasePath("/home/ftp");
config.setHost("kanchai.club");
config.setPort(221);
config.setUsername("");
config.setPassword("");
config.setMode(FtpMode.Passive.name());
FtpFileClient client = new FtpFileClient(0L, config);
client.init();
// 上传文件
String path = IdUtil.fastSimpleUUID() + ".jpg";
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
String fullPath = client.upload(content, path);
System.out.println("访问地址:" + fullPath);
if (false) {
byte[] bytes = client.getContent(path);
System.out.println("文件内容:" + bytes);
}
if (false) {
client.delete(path);
}
}
}

View File

@ -0,0 +1,27 @@
package cn.iocoder.yudao.framework.file.core.client.local;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.framework.file.core.client.impl.local.LocalFileClient;
import cn.iocoder.yudao.framework.file.core.client.impl.local.LocalFileClientConfig;
import org.junit.jupiter.api.Test;
public class LocalFileClientTest {
@Test
public void test() {
// 创建客户端
LocalFileClientConfig config = new LocalFileClientConfig();
config.setDomain("http://127.0.0.1:48080");
config.setBasePath("/Users/yunai/file_test");
LocalFileClient client = new LocalFileClient(0L, config);
client.init();
// 上传文件
String path = IdUtil.fastSimpleUUID() + ".jpg";
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
String fullPath = client.upload(content, path);
System.out.println("访问地址:" + fullPath);
client.delete(path);
}
}

View File

@ -76,6 +76,15 @@ public class S3FileClientTest {
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
String fullPath = client.upload(content, path);
System.out.println("访问地址:" + fullPath);
// 读取文件
if (false) {
byte[] bytes = client.getContent(path);
System.out.println("文件内容:" + bytes);
}
// 删除文件
if (false) {
client.delete(path);
}
}
}

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.framework.file.core.client.sftp;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.framework.file.core.client.impl.sftp.SftpFileClient;
import cn.iocoder.yudao.framework.file.core.client.impl.sftp.SftpFileClientConfig;
import org.junit.jupiter.api.Test;
public class SftpFileClientTest {
@Test
public void test() {
// 创建客户端
SftpFileClientConfig config = new SftpFileClientConfig();
config.setDomain("http://127.0.0.1:48080");
config.setBasePath("/home/ftp");
config.setHost("kanchai.club");
config.setPort(222);
config.setUsername("");
config.setPassword("");
SftpFileClient client = new SftpFileClient(0L, config);
client.init();
// 上传文件
String path = IdUtil.fastSimpleUUID() + ".jpg";
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
String fullPath = client.upload(content, path);
System.out.println("访问地址:" + fullPath);
if (false) {
byte[] bytes = client.getContent(path);
System.out.println("文件内容:" + bytes);
}
if (false) {
client.delete(path);
}
}
}

View File

@ -57,10 +57,15 @@ public class FileController {
return success(true);
}
@GetMapping("/get/{path}")
@GetMapping("/{configId}/get/{path}")
@ApiOperation("下载文件")
@ApiImplicitParam(name = "path", value = "文件附件", required = true, dataTypeClass = MultipartFile.class)
public void getFile(HttpServletResponse response, @PathVariable("path") String path) throws IOException {
@ApiImplicitParams({
@ApiImplicitParam(name = "configId", value = "配置编号", required = true, dataTypeClass = String.class),
@ApiImplicitParam(name = "path", value = "文件路径", required = true, dataTypeClass = String.class)
})
public void getFile(HttpServletResponse response,
@PathVariable("configId") String configId,
@PathVariable("path") String path) throws IOException {
FileDO file = fileService.getFile(path);
if (file == null) {
log.warn("[getFile][path({}) 文件不存在]", path);