发布于 

实现通用文件上传,三方云通过s3协议实现

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
└─file
│ pom.xml
└─src
├─main
│ ├─java
│ │ └─common
│ │ └─file
│ │ ├─config
│ │ │ FileAutoConfiguration.java
│ │ │ FileConfigConfiguration.java
│ │ │ FileConfigDTO.java
│ │ │
│ │ └─core
│ │ ├─client
│ │ │ │ AbstractFileClient.java
│ │ │ │ FileClient.java
│ │ │ │ FileClientConfig.java
│ │ │ │ FileClientFactory.java
│ │ │ │ FileClientFactoryImpl.java
│ │ │ │
│ │ │ ├─db
│ │ │ │ DBFileClient.java
│ │ │ │ DBFileClientConfig.java
│ │ │ │ DBFileContentFrameworkDAO.java
│ │ │ │
│ │ │ ├─ftp
│ │ │ │ FtpFileClient.java
│ │ │ │ FtpFileClientConfig.java
│ │ │ │
│ │ │ ├─local
│ │ │ │ LocalFileClient.java
│ │ │ │ LocalFileClientConfig.java
│ │ │ │
│ │ │ ├─s3
│ │ │ │ S3FileClient.java
│ │ │ │ S3FileClientConfig.java
│ │ │ │
│ │ │ └─sftp
│ │ │ SftpFileClient.java
│ │ │ SftpFileClientConfig.java
│ │ │
│ │ ├─enums
│ │ │ FileStorageEnum.java
│ │ │
│ │ └─utils
│ │ FileClientUtils.java
│ │ FileTypeUtils.java
│ │ FileUtils.java
│ │
│ └─resources
│ └─META-INF
│ └─spring
│ org.springframework.boot.autoconfigure.AutoConfiguration.imports

└─test
├─java
│ └─common
│ └─file
│ ├─config
│ └─core
│ ├─client
│ │ ├─ftp
│ │ │ FtpFileClientTest.java
│ │ │
│ │ ├─local
│ │ │ LocalFileClientTest.java
│ │ │
│ │ ├─s3
│ │ │ S3FileClientTest.java
│ │ │
│ │ └─sftp
│ │ SftpFileClientTest.java
│ │
│ └─enums
└─resources
└─file
test.png

项目源码地址

https://github.com/1875586605/common-file

项目pom文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<?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>



<description>
1. file:本地磁盘
2. ftp:FTP 服务器
2. sftp:SFTP 服务器
4. db:数据库
5. s3:支持 S3 协议的云存储服务,例如说 MinIO、阿里云、华为云、腾讯云、七牛云等等
</description>

<groupId>demo</groupId>
<artifactId>file</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
</dependency>

<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId> <!-- 解决 sftp 连接 -->
<version>0.1.55</version>
</dependency>

<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId> <!-- 文件类型的识别 -->
<version>2.7.0</version>
</dependency>

<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>


<!-- 三方云服务相关 -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.2</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.7.10</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.7.10</version>
<scope>test</scope>
</dependency>

</dependencies>

</project>

配置类代码

自动注入配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package common.file.config;

import common.file.core.client.FileClientFactory;
import common.file.core.client.FileClientFactoryImpl;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;

/**
* 文件配置类
*/
@AutoConfiguration
public class FileAutoConfiguration {

@Bean
public FileClientFactory fileClientFactory() {
return new FileClientFactoryImpl();
}

}

文件配置类数据模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package common.file.config;

import common.file.core.client.FileClientConfig;
import common.file.core.enums.FileStorageEnum;
import lombok.Data;

/**
* @author cjb
* @date 2023/5/9 9:48
* @describe
*/
@Data
public class FileConfigDTO {

/**
* 配置名
*/
private String name;


/**
* 存储器
* <p>
* 枚举 {@link FileStorageEnum}
*/
private String storage;

/**
* 是否为主配置
* <p>
* 由于我们可以配置多个文件配置,默认情况下,使用主配置进行文件的上传
*/
private Boolean master;

/**
* 配置文件详情
*/
private FileClientConfig config;


}

配置参数读取注入类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package common.file.config;

import cn.hutool.core.bean.BeanDesc;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.PropDesc;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.spring.SpringUtil;
import common.file.core.client.FileClientConfig;
import common.file.core.enums.FileStorageEnum;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.*;

/**
* @author cjb
* @date 2023/5/9 9:38
* @describe 文件配置类
*/
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "file.conf")
public class FileConfigConfiguration {

private Map<String, FileConfigDTO> confMap;


@PostConstruct
public void init() {
log.info("初始化数据文件配置,进行多态配置");
for (String key : confMap.keySet()) {
FileConfigDTO fileConfigDTO = confMap.get(key);
FileStorageEnum storageEnum = FileStorageEnum.getByStorage(fileConfigDTO.getStorage());
String propertyKeyPrefix = "file.conf.confMap." + fileConfigDTO.getName() + ".config.";
HashMap<String, Object> beanMap = new HashMap<>();
BeanDesc beanDesc = BeanUtil.getBeanDesc(storageEnum.getConfigClass());
Collection<PropDesc> props = beanDesc.getProps();

for (PropDesc prop : props) {
String fieldName = prop.getFieldName();
String propertyKey = propertyKeyPrefix + fieldName;
String fieldValue = SpringUtil.getProperty(propertyKey);
if (ObjectUtil.isNotEmpty(fieldValue)) {
beanMap.put(fieldName, fieldValue);
}
}
FileClientConfig bean = BeanUtil.toBean(beanMap, storageEnum.getConfigClass());
fileConfigDTO.setConfig(bean);
}
log.info("初始化数据文件配置,多态配置完成");

}

public FileConfigDTO getConfByKey(String key) {
return this.confMap.get(key);
}

public List<FileConfigDTO> getFileConfList() {
ArrayList<FileConfigDTO> fileConfigDTOS = new ArrayList<>();
for (String key : this.confMap.keySet()) {
fileConfigDTOS.add(this.confMap.get(key));
}
return fileConfigDTOS;

}

}

配置文件示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
file: 
conf:
confMap:
# 配置名称
aliyunPreA:
name: aliyunPreA
# 存储器类型
storage: s3
# 是否为主配置
master: false
# 详细参数配置
config:
# 节点地址
endpoint: oss-cn-hangzhou.aliyuncs.com
# 自定义域名
domain: xxx.oss-cn-hangzhou.aliyuncs.com
# 存储桶名称
bucket: xxx
# 访问 Key
accessKey: xxxxxxxxx
# 访问 Secret
accessSecret: xxxxxxxxx
aliyunPreB:
name: aliyunPreB
storage: s3
master: true
config:
endpoint: oss-cn-hangzhou.aliyuncs.com
domain: xxxxxx.oss-cn-hangzhou.aliyuncs.com
bucket: xxxxxx
accessKey: xxxxxxxxx
accessSecret: xxxxxxxxx

枚举代码

文件存储器枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package common.file.core.enums;

import cn.hutool.core.util.ArrayUtil;
import common.file.core.client.FileClient;
import common.file.core.client.FileClientConfig;
import common.file.core.client.db.DBFileClient;
import common.file.core.client.db.DBFileClientConfig;
import common.file.core.client.ftp.FtpFileClient;
import common.file.core.client.ftp.FtpFileClientConfig;
import common.file.core.client.local.LocalFileClient;
import common.file.core.client.local.LocalFileClientConfig;
import common.file.core.client.s3.S3FileClient;
import common.file.core.client.s3.S3FileClientConfig;
import common.file.core.client.sftp.SftpFileClient;
import common.file.core.client.sftp.SftpFileClientConfig;
import lombok.AllArgsConstructor;
import lombok.Getter;

/**
* 文件存储器枚举
*
* @author
*/
@AllArgsConstructor
@Getter
public enum FileStorageEnum {

DB("db", DBFileClientConfig.class, DBFileClient.class),

LOCAL("local", LocalFileClientConfig.class, LocalFileClient.class),
FTP("ftp", FtpFileClientConfig.class, FtpFileClient.class),
SFTP("sftp", SftpFileClientConfig.class, SftpFileClient.class),

S3("s3", S3FileClientConfig.class, S3FileClient.class),
;

/**
* 存储器
*/
private final String storage;

/**
* 配置类
*/
private final Class<? extends FileClientConfig> configClass;
/**
* 客户端类
*/
private final Class<? extends FileClient> clientClass;

public static FileStorageEnum getByStorage(String storage) {
return ArrayUtil.firstMatch(o -> o.getStorage().equals(storage), values());
}

}

上传核心代码

通用接口定义相关代码

文件客户端的抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package common.file.core.client;

import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;

/**
* 文件客户端的抽象类,提供模板方法,减少子类的冗余代码
*/
@Slf4j
public abstract class AbstractFileClient<Config extends FileClientConfig> implements FileClient {

/**
* 配置编号
*/
private final String confKey;
/**
* 文件配置
*/
protected Config config;

public AbstractFileClient(String confKey, Config config) {
this.confKey = confKey;
this.config = config;
}

/**
* 初始化
*/
public final void init() {
doInit();
log.info("[init][配置({}) 初始化完成]", config);
}

/**
* 自定义初始化
*/
protected abstract void doInit();

public final void refresh(Config config) {
// 判断是否更新
if (config.equals(this.config)) {
return;
}
log.info("[refresh][配置({})发生变化,重新初始化]", config);
this.config = config;
// 初始化
this.init();
}

@Override
public String getConfKey() {
return confKey;
}

/**
* 格式化文件的 URL 访问地址
* 使用场景:local、ftp、db,通过 FileController 的 getFile 来获取文件内容
*
* @param domain 自定义域名
* @param path 文件路径
* @return URL 访问地址
*/
protected String formatFileUrl(String domain, String path) {
return StrUtil.format("{}/admin-api/infra/file/{}/get/{}", domain, getConfKey(), path);
}

}

文件客户端接口定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package common.file.core.client;

/**
* 文件客户端
*/
public interface FileClient {

/**
* 获得客户端编号
*
* @return 客户端编号
*/
String getConfKey();

/**
* 上传文件
*
* @param content 文件流
* @param path 相对路径
* @return 完整路径,即 HTTP 访问地址
* @throws Exception 上传文件时,抛出 Exception 异常
*/
String upload(byte[] content, String path, String type) throws Exception;

/**
* 删除文件
*
* @param path 相对路径
* @throws Exception 删除文件时,抛出 Exception 异常
*/
void delete(String path) throws Exception;

/**
* 获得文件的内容
*
* @param path 相对路径
* @return 文件的内容
*/
byte[] getContent(String path) throws Exception;

}

文件客户端的配置接口

1
2
3
4
5
6
7
8
9
10
package common.file.core.client;


/**
* 文件客户端的配置
* 不同实现的客户端,需要不同的配置,通过子类来定义
*/
public interface FileClientConfig {
}

文件客户端获取工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package common.file.core.client;

public interface FileClientFactory {

/**
* 获得文件客户端
*
* @param configKey 配置编号
* @return 文件客户端
*/
FileClient getFileClient(String configKey);

/**
* 创建文件客户端
*
* @param confKey 配置key
* @param storage 存储器的枚举 {@link FileClientFactoryImpl}
* @param config 文件配置
*/
<Config extends FileClientConfig> void createOrUpdateFileClient(String confKey, String storage, Config config);

}

文件客户端的工厂实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package common.file.core.client;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ReflectUtil;
import common.file.core.enums.FileStorageEnum;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
* 文件客户端的工厂实现类
*/
@Slf4j
public class FileClientFactoryImpl implements FileClientFactory {

/**
* 文件客户端 Map
* key:配置编号
*/
private final ConcurrentMap<String, AbstractFileClient<?>> clients = new ConcurrentHashMap<>();

@Override
public FileClient getFileClient(String configKey) {
AbstractFileClient<?> client = clients.get(configKey);
if (client == null) {
log.error("[getFileClient][配置编号({}) 找不到客户端]", configKey);
}
return client;
}

@Override
@SuppressWarnings("unchecked")
public <Config extends FileClientConfig> void createOrUpdateFileClient(String confKey, String storage, Config config) {
AbstractFileClient<Config> client = (AbstractFileClient<Config>) clients.get(confKey);
if (client == null) {
client = this.createFileClient(confKey, storage, config);
client.init();
clients.put(client.getConfKey(), client);
} else {
client.refresh(config);
}
}

@SuppressWarnings("unchecked")
private <Config extends FileClientConfig> AbstractFileClient<Config> createFileClient(
String confKey, String storage, Config config) {
FileStorageEnum storageEnum = FileStorageEnum.getByStorage(storage);
Assert.notNull(storageEnum, String.format("文件配置(%s) 为空", storageEnum));
// 创建客户端
return (AbstractFileClient<Config>) ReflectUtil.newInstance(storageEnum.getClientClass(), confKey, config);
}

}

db上传客户端

基于 DB 存储的文件客户端的配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package common.file.core.client.db;

import cn.hutool.extra.spring.SpringUtil;
import common.file.core.client.AbstractFileClient;

/**
* 基于 DB 存储的文件客户端的配置类
*/
public class DBFileClient extends AbstractFileClient<DBFileClientConfig> {

private DBFileContentFrameworkDAO dao;

public DBFileClient(String confKey, DBFileClientConfig config) {
super(confKey, config);
}

@Override
protected void doInit() {
}

@Override
public String upload(byte[] content, String path, String type) {
getDao().insert(getConfKey(), path, content);
// 拼接返回路径
return super.formatFileUrl(config.getDomain(), path);
}

@Override
public void delete(String path) {
getDao().delete(getConfKey(), path);
}

@Override
public byte[] getContent(String path) {
return getDao().selectContent(getConfKey(), path);
}

private DBFileContentFrameworkDAO getDao() {
// 延迟获取,因为 SpringUtil 初始化太慢
if (dao == null) {
dao = SpringUtil.getBean(DBFileContentFrameworkDAO.class);
}
return dao;
}

}

基于 DB 存储的文件客户端的配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package common.file.core.client.db;

import common.file.core.client.FileClientConfig;
import lombok.Data;
import org.hibernate.validator.constraints.URL;

import javax.validation.constraints.NotEmpty;

/**
* 基于 DB 存储的文件客户端的配置类
*/
@Data
public class DBFileClientConfig implements FileClientConfig {

/**
* 自定义域名
*/
@NotEmpty(message = "domain 不能为空")
@URL(message = "domain 必须是 URL 格式")
private String domain;

}

文件内容 Framework DAO 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package common.file.core.client.db;

/**
* 文件内容 Framework DAO 接口
*/
public interface DBFileContentFrameworkDAO {

/**
* 插入文件内容
*
* @param confKey 配置编号
* @param path 路径
* @param content 内容
*/
void insert(String confKey, String path, byte[] content);

/**
* 删除文件内容
*
* @param confKey 配置编号
* @param path 路径
*/
void delete(String confKey, String path);

/**
* 获得文件内容
*
* @param confKey 配置编号
* @param path 路径
* @return 内容
*/
byte[] selectContent(String confKey, String path);

}

FTP上传客户端

FTP 文件客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package common.file.core.client.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 common.file.core.client.AbstractFileClient;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;

/**
* Ftp 文件客户端
*/
public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> {

private Ftp ftp;

public FtpFileClient(String confKey, FtpFileClientConfig config) {
super(confKey, config);
}

@Override
protected void doInit() {
// 把配置的 \ 替换成 /, 如果路径配置 \a\test, 替换成 /a/test, 替换方法已经处理 null 情况
config.setBasePath(StrUtil.replace(config.getBasePath(), StrUtil.BACKSLASH, StrUtil.SLASH));
// ftp的路径是 / 结尾
if (!config.getBasePath().endsWith(StrUtil.SLASH)) {
config.setBasePath(config.getBasePath() + StrUtil.SLASH);
}
// 初始化 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 type) {
// 执行写入
String filePath = getFilePath(path);
String fileName = FileUtil.getName(filePath);
String dir = StrUtil.removeSuffix(filePath, fileName);
ftp.reconnectIfTimeout();
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.reconnectIfTimeout();
ftp.delFile(filePath);
}

@Override
public byte[] getContent(String path) {
String filePath = getFilePath(path);
String fileName = FileUtil.getName(filePath);
String dir = StrUtil.removeSuffix(filePath, fileName);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ftp.reconnectIfTimeout();
ftp.download(dir, fileName, out);
return out.toByteArray();
}

private String getFilePath(String path) {
return config.getBasePath() + path;
}

}

FTP 文件客户端的配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package common.file.core.client.ftp;

import common.file.core.client.FileClientConfig;
import lombok.Data;
import org.hibernate.validator.constraints.URL;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

/**
* Ftp 文件客户端的配置类
*/
@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;
/**
* 连接模式
* <p>
* 使用 {@link cn.hutool.extra.ftp.FtpMode} 对应的字符串
*/
@NotEmpty(message = "连接模式不能为空")
private String mode;

}

基于本地的上传

本地文件客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package common.file.core.client.local;

import cn.hutool.core.io.FileUtil;
import common.file.core.client.AbstractFileClient;

import java.io.File;

/**
* 本地文件客户端
*/
public class LocalFileClient extends AbstractFileClient<LocalFileClientConfig> {

public LocalFileClient(String confKey, LocalFileClientConfig config) {
super(confKey, 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 type) {
// 执行写入
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;
}

}

本地文件客户端的配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package common.file.core.client.local;

import common.file.core.client.FileClientConfig;
import lombok.Data;
import org.hibernate.validator.constraints.URL;

import javax.validation.constraints.NotEmpty;

/**
* 本地文件客户端的配置类
*/
@Data
public class LocalFileClientConfig implements FileClientConfig {

/**
* 基础路径
*/
@NotEmpty(message = "基础路径不能为空")
private String basePath;

/**
* 自定义域名
*/
@NotEmpty(message = "domain 不能为空")
@URL(message = "domain 必须是 URL 格式")
private String domain;

}

SFTP上传

SFTP 文件客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package common.file.core.client.sftp;

import cn.hutool.core.io.FileUtil;
import cn.hutool.extra.ssh.Sftp;
import common.file.core.client.AbstractFileClient;
import common.file.core.utils.FileUtils;


import java.io.File;

/**
* Sftp 文件客户端
*
*/
public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> {

private Sftp sftp;

public SftpFileClient(String confKey, SftpFileClientConfig config) {
super(confKey, 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 type) {
// 执行写入
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;
}

}

SFTP 文件客户端的配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package common.file.core.client.sftp;

import common.file.core.client.FileClientConfig;
import lombok.Data;
import org.hibernate.validator.constraints.URL;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

/**
* Sftp 文件客户端的配置类
*/
@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;

}

基于s3协议上传文件 实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务

基于 S3 协议的文件客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
package common.file.core.client.s3;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import common.file.core.client.AbstractFileClient;
import io.minio.*;

import java.io.ByteArrayInputStream;

import static common.file.core.client.s3.S3FileClientConfig.ENDPOINT_ALIYUN;
import static common.file.core.client.s3.S3FileClientConfig.ENDPOINT_TENCENT;


/**
* 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务
* <p>
* S3 协议的客户端,采用亚马逊提供的 software.amazon.awssdk.s3 库
*/
public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {

private MinioClient client;

public S3FileClient(String confKey, S3FileClientConfig config) {
super(confKey, config);
}

@Override
protected void doInit() {
// 补全 domain
if (StrUtil.isEmpty(config.getDomain())) {
config.setDomain(buildDomain());
}
// 初始化客户端
client = MinioClient.builder()
.endpoint(buildEndpointURL()) // Endpoint URL
.region(buildRegion()) // Region
.credentials(config.getAccessKey(), config.getAccessSecret()) // 认证密钥
.build();
}

/**
* 基于 endpoint 构建调用云服务的 URL 地址
*
* @return URI 地址
*/
private String buildEndpointURL() {
// 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO
if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
return config.getEndpoint();
}
return StrUtil.format("https://{}", config.getEndpoint());
}

/**
* 基于 bucket + endpoint 构建访问的 Domain 地址
*
* @return Domain 地址
*/
private String buildDomain() {
// 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO
if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket());
}
// 阿里云、腾讯云、华为云都适合。七牛云比较特殊,必须有自定义域名
return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
}

/**
* 基于 bucket 构建 region 地区
*
* @return region 地区
*/
private String buildRegion() {
// 阿里云必须有 region,否则会报错
if (config.getEndpoint().contains(ENDPOINT_ALIYUN)) {
return StrUtil.subBefore(config.getEndpoint(), '.', false)
.replaceAll("-internal", "")// 去除内网 Endpoint 的后缀
.replaceAll("https://", "");
}
// 腾讯云必须有 region,否则会报错
if (config.getEndpoint().contains(ENDPOINT_TENCENT)) {
return StrUtil.subAfter(config.getEndpoint(), ".cos.", false)
.replaceAll("." + ENDPOINT_TENCENT, ""); // 去除 Endpoint
}
return null;
}

@Override
public String upload(byte[] content, String path, String type) throws Exception {
// 执行上传
client.putObject(PutObjectArgs.builder()
.bucket(config.getBucket()) // bucket 必须传递
.contentType(type)
.object(path) // 相对路径作为 key
.stream(new ByteArrayInputStream(content), content.length, -1) // 文件内容
.build());
// 拼接返回路径
return config.getDomain() + "/" + path;
}

@Override
public void delete(String path) throws Exception {
client.removeObject(RemoveObjectArgs.builder()
.bucket(config.getBucket()) // bucket 必须传递
.object(path) // 相对路径作为 key
.build());
}

@Override
public byte[] getContent(String path) throws Exception {
GetObjectResponse response = client.getObject(GetObjectArgs.builder()
.bucket(config.getBucket()) // bucket 必须传递
.object(path) // 相对路径作为 key
.build());
return IoUtil.readBytes(response);
}

}

S3 文件客户端的配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package common.file.core.client.s3;

import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import common.file.core.client.FileClientConfig;
import lombok.Data;
import org.hibernate.validator.constraints.URL;

import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotNull;

/**
* S3 文件客户端的配置类
*/
@Data
public class S3FileClientConfig implements FileClientConfig {

public static final String ENDPOINT_QINIU = "qiniucs.com";
public static final String ENDPOINT_ALIYUN = "aliyuncs.com";
public static final String ENDPOINT_TENCENT = "myqcloud.com";

/**
* 节点地址
* 1. MinIO:https://www.iocoder.cn/Spring-Boot/MinIO 。例如说,http://127.0.0.1:9000
* 2. 阿里云:https://help.aliyun.com/document_detail/31837.html
* 3. 腾讯云:https://cloud.tencent.com/document/product/436/6224
* 4. 七牛云:https://developer.qiniu.com/kodo/4088/s3-access-domainname
* 5. 华为云:https://developer.huaweicloud.com/endpoint?OBS
*/
@NotNull(message = "endpoint 不能为空")
private String endpoint;
/**
* 自定义域名
* 1. MinIO:通过 Nginx 配置
* 2. 阿里云:https://help.aliyun.com/document_detail/31836.html
* 3. 腾讯云:https://cloud.tencent.com/document/product/436/11142
* 4. 七牛云:https://developer.qiniu.com/kodo/8556/set-the-custom-source-domain-name
* 5. 华为云:https://support.huaweicloud.com/usermanual-obs/obs_03_0032.html
*/
@URL(message = "domain 必须是 URL 格式")
private String domain;
/**
* 存储 Bucket
*/
@NotNull(message = "bucket 不能为空")
private String bucket;

/**
* 访问 Key
* 1. MinIO:https://www.iocoder.cn/Spring-Boot/MinIO
* 2. 阿里云:https://ram.console.aliyun.com/manage/ak
* 3. 腾讯云:https://console.cloud.tencent.com/cam/capi
* 4. 七牛云:https://portal.qiniu.com/user/key
* 5. 华为云:https://support.huaweicloud.com/qs-obs/obs_qs_0005.html
*/
@NotNull(message = "accessKey 不能为空")
private String accessKey;
/**
* 访问 Secret
*/
@NotNull(message = "accessSecret 不能为空")
private String accessSecret;

@SuppressWarnings("RedundantIfStatement")
@AssertTrue(message = "domain 不能为空")
@JsonIgnore
public boolean isDomainValid() {
// 如果是七牛,必须带有 domain
if (StrUtil.contains(endpoint, ENDPOINT_QINIU) && StrUtil.isEmpty(domain)) {
return false;
}
return true;
}

}

上传文件工具

文件上传连接工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
package common.file.core.utils;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import common.file.config.FileConfigConfiguration;
import common.file.config.FileConfigDTO;
import common.file.core.client.FileClient;
import common.file.core.client.FileClientFactory;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.InputStream;
import java.util.List;

/**
* @author cjb
* @date 2023/5/9 10:17
* @describe 文件上传连接工具
*/
@Slf4j
@Service
public class FileClientUtils {

/**
* Master FileClient 对象,有且仅有一个
*/
private FileClient masterFileClient;

private static FileClientUtils fileClientUtils;


@Resource
private FileClientFactory fileClientFactory;

@Resource
private FileConfigConfiguration fileConfigConfiguration;

@PostConstruct
public void init() {
// 获取所有配置
List<FileConfigDTO> configs = fileConfigConfiguration.getFileConfList();
// 加载所有配置

// 第二步:构建缓存:创建或更新文件 Client
configs.forEach(config -> {
fileClientFactory.createOrUpdateFileClient(config.getName(), config.getStorage(), config.getConfig());
// 如果是 master,进行设置
if (Boolean.TRUE.equals(config.getMaster())) {
masterFileClient = fileClientFactory.getFileClient(config.getName());
}
});
log.info("[initLocalCache][缓存文件配置,数量为:{}]", configs.size());

// 初始化静态工具
fileClientUtils = this;
fileClientUtils.fileClientFactory = this.fileClientFactory;
fileClientUtils.fileConfigConfiguration = this.fileConfigConfiguration;
}

/**
* 获得 Master 文件客户端
*
* @return 文件客户端
*/
public static FileClient getMasterFileClient() {
return fileClientUtils.masterFileClient;
}


/**
* 获得指定编号的文件客户端
*
* @param confKey 配置编号
* @return 链接对象
*/
public static FileClient getFileClient(String confKey) {
return fileClientUtils.fileClientFactory.getFileClient(confKey);
}

/**
* 上传文件
* @param inputStream 文件流
* @return
*/
public static String uploadFile(InputStream inputStream) {
return uploadFile(IoUtil.readBytes(inputStream));
}

/**
* 上传文件返回文件路径
*
* @param name 文件名称
* @param inputStream 文件流
* @return
*/
public static String uploadFile(String name,InputStream inputStream) {
return uploadFile(name, IoUtil.readBytes(inputStream));
}

/**
* 上传文件
* @param content 文件byte
* @return
*/
public static String uploadFile(byte[] content) {
return uploadFile(null, null, content);
}

/**
* 上传文件返回文件路径
*
* @param name 文件名称
* @param content 文件字节对象
* @return
*/
public static String uploadFile(String name,byte[] content) {
return uploadFile(name,null, content);
}

/**
* 上传文件
* @param name 文件名称
* @param content 文件byte
* @param confKey 链接配置key
* @return
*/
public static String uploadFile(String name,byte[] content,String confKey) {
return uploadFile(name, null, content,confKey);
}


/**
* 上传文件
* @param content 文件byte
* @param confKey 链接配置key
* @return
*/
public static String uploadFile(byte[] content,String confKey) {
return uploadFile(null, null, content,confKey);
}




/**
* 上传文件返回文件路径
*
* @param name 文件名称
* @param path 文件存储路径
* @param inputStream 文件输入流
* @return
*/
public static String uploadFile(String name, String path, InputStream inputStream) {
return uploadFile(name, path, IoUtil.readBytes(inputStream));
}

/**
* 上传文件返回文件路径
*
* @param name 文件名称
* @param path 文件存储路径
* @param inputStream 文件输入流
* @param confKey 链接对象配置key
* @return
*/
public static String uploadFile(String name, String path, InputStream inputStream, String confKey) {
FileClient fileClient = getFileClient(confKey);
return uploadFile(name, path, IoUtil.readBytes(inputStream), fileClient);
}

public static String uploadFile(String name, String path, byte[] content, String confKey) {
FileClient fileClient = getFileClient(confKey);
return uploadFile(name, path, content, fileClient);
}

/**
* 上传文件返回文件路径
*
* @param name 文件名称
* @param path 文件存储路径
* @param content 文件字节对象
* @return
*/
@SneakyThrows
public static String uploadFile(String name, String path, byte[] content) {
// 计算默认的 path 名
String type = FileTypeUtils.getMineType(content, name);
if (StrUtil.isEmpty(path)) {
path = FileUtils.generatePath(content, name);
}
FileClient client = getMasterFileClient();
Assert.notNull(client, "客户端(master) 不能为空");
return client.upload(content, path, type);
}

/**
* 上传文件返回文件路径
*
* @param name 文件名称
* @param path 文件存储路径
* @param content 文件字节对象
* @param client 指定链接对象
* @return
*/
@SneakyThrows
public static String uploadFile(String name, String path, byte[] content, FileClient client) {
// 计算默认的 path 名
String type = FileTypeUtils.getMineType(content, name);
if (StrUtil.isEmpty(path)) {
path = FileUtils.generatePath(content, name);
}
Assert.notNull(client, "客户端({}) 不能为空", client.getConfKey());
return client.upload(content, path, type);
}


/**
* 删除文件
*
* @param path 文件路径
* @return
*/
@SneakyThrows
public static void deleteFile(String path) {
FileClient client = getMasterFileClient();
Assert.notNull(client, "客户端(master) 不能为空");
client.delete(path);
}

/**
* 删除文件
*
* @param path 文件路径
* @param confKey 链接配置key
* @return
*/
@SneakyThrows
public static void deleteFile(String path, String confKey) {
FileClient client = getFileClient(confKey);
Assert.notNull(client, "客户端({}) 不能为空", client.getConfKey());
client.delete(path);
}

/**
* 下载文件
*
* @param path 文件路径
* @return
*/
@SneakyThrows
public static byte[] getFileContent(String path) {
FileClient client = getMasterFileClient();
Assert.notNull(client, "客户端(master) 不能为空");
return client.getContent(path);
}

/**
* 下载文件
*
* @param path 文件路径
* @return
*/
@SneakyThrows
public static byte[] getFileContent(String path, String confKey) {
FileClient client = getFileClient(confKey);
Assert.notNull(client, "客户端({}) 不能为空", client.getConfKey());
return client.getContent(path);
}

}

文件类型工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package common.file.core.utils;

import cn.hutool.core.util.StrUtil;
import com.alibaba.ttl.TransmittableThreadLocal;
import lombok.SneakyThrows;
import org.apache.tika.Tika;

/**
* 文件类型 Utils
*
* @author
*/
public class FileTypeUtils {

private static final ThreadLocal<Tika> TIKA = TransmittableThreadLocal.withInitial(Tika::new);

/**
* 获得文件的 mineType,对于doc,jar等文件会有误差
*
* @param data 文件内容
* @return mineType 无法识别时会返回“application/octet-stream”
*/
@SneakyThrows
public static String getMineType(byte[] data) {
return TIKA.get().detect(data);
}

/**
* 已知文件名,获取文件类型,在某些情况下比通过字节数组准确,例如使用jar文件时,通过名字更为准确
*
* @param name 文件名
* @return mineType 无法识别时会返回“application/octet-stream”
*/
public static String getMineType(String name) {
return TIKA.get().detect(name);
}

/**
* 在拥有文件和数据的情况下,最好使用此方法,最为准确
*
* @param data 文件内容
* @param name 文件名
* @return mineType 无法识别时会返回“application/octet-stream”
*/
public static String getMineType(byte[] data, String name) {
return TIKA.get().detect(data, name);
}

/**
* 判断文件类型是否为视屏
*
* @param mineType 文件类型
* @return
*/
public static boolean isVideo(String mineType) {
if (StrUtil.startWith(mineType, "video")) {
return true;
}
return false;
}

/**
* 判断文件类型是否为图片
*
* @param mineType 文件类型
* @return
*/
public static boolean isImage(String mineType) {
if (StrUtil.startWith(mineType, "image")) {
return true;
}
return false;
}
}

文件工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package common.file.core.utils;

import cn.hutool.core.io.FileTypeUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import lombok.SneakyThrows;

import java.io.ByteArrayInputStream;
import java.io.File;

/**
* 文件工具类
*/
public class FileUtils {

/**
* 创建临时文件
* 该文件会在 JVM 退出时,进行删除
*
* @param data 文件内容
* @return 文件
*/
@SneakyThrows
public static File createTempFile(String data) {
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;
}

/**
* 生成文件路径
*
* @param content 文件内容
* @param originalName 原始文件名
* @return path,唯一不可重复
*/
public static String generatePath(byte[] content, String originalName) {
String sha256Hex = DigestUtil.sha256Hex(content);
// 情况一:如果存在 name,则优先使用 name 的后缀
if (StrUtil.isNotBlank(originalName)) {
String extName = FileNameUtil.extName(originalName);
return StrUtil.isBlank(extName) ? sha256Hex : sha256Hex + "." + extName;
}
// 情况二:基于 content 计算
return sha256Hex + '.' + FileTypeUtil.getType(new ByteArrayInputStream(content));
}

}

上传测试类

FTP 上传测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package common.file.core.client.ftp;

import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.extra.ftp.FtpMode;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

public class FtpFileClientTest {

@Test
@Disabled
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, "image/jpeg");
System.out.println("访问地址:" + fullPath);
if (false) {
byte[] bytes = client.getContent(path);
System.out.println("文件内容:" + bytes);
}
if (false) {
client.delete(path);
}
}

}

local上传测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package common.file.core.client.local;

import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

public class LocalFileClientTest {

@Test
@Disabled
public void test() {
// 创建客户端
LocalFileClientConfig config = new LocalFileClientConfig();
config.setDomain("http://127.0.0.1:48080");
config.setBasePath("/Users/test/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, "image/jpeg");
System.out.println("访问地址:" + fullPath);
client.delete(path);
}

}

sftp 上传测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package common.file.core.client.sftp;

import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

public class SftpFileClientTest {

@Test
@Disabled
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, "image/jpeg");
System.out.println("访问地址:" + fullPath);
if (false) {
byte[] bytes = client.getContent(path);
System.out.println("文件内容:" + bytes);
}
if (false) {
client.delete(path);
}
}

}

s3 上传测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
package common.file.core.client.s3;

import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import javax.validation.Validation;

public class S3FileClientTest {

@Test
@Disabled // MinIO,如果要集成测试,可以注释本行
public void testMinIO() throws Exception {
S3FileClientConfig config = new S3FileClientConfig();
// 配置成你自己的
config.setAccessKey("admin");
config.setAccessSecret("password");
config.setBucket("test");
config.setDomain(null);
// 默认 9000 endpoint
config.setEndpoint("http://127.0.0.1:9000");

// 执行上传
testExecuteUpload(config);
}

@Test
@Disabled // 阿里云 OSS,如果要集成测试,可以注释本行
public void testAliyun() throws Exception {
S3FileClientConfig config = new S3FileClientConfig();
// 配置成你自己的
config.setAccessKey("ALIYUN_ACCESS_KEY");
config.setAccessSecret("ALIYUN_SECRET_KEY");
config.setBucket("test");
config.setDomain("test"); // 如果有自定义域名,则可以设置。http://ali-oss.iocoder.cn
// 默认上海的 endpoint
config.setEndpoint("cos.ap-shanghai.myqcloud.com");

// 执行上传
testExecuteUpload(config);
}

@Test
@Disabled // 腾讯云 COS,如果要集成测试,可以注释本行
public void testQCloud() throws Exception {
S3FileClientConfig config = new S3FileClientConfig();
// 配置成你自己的
config.setAccessKey(System.getenv("QCLOUD_ACCESS_KEY"));
config.setAccessSecret(System.getenv("QCLOUD_SECRET_KEY"));
config.setBucket("aoteman-1255880240");
config.setDomain(null); // 如果有自定义域名,则可以设置。http://tengxun-oss.iocoder.cn
// 默认上海的 endpoint
config.setEndpoint("cos.ap-shanghai.myqcloud.com");

// 执行上传
testExecuteUpload(config);
}

@Test
@Disabled // 七牛云存储,如果要集成测试,可以注释本行
public void testQiniu() throws Exception {
S3FileClientConfig config = new S3FileClientConfig();
// 配置成你自己的
config.setAccessKey(System.getenv("QINIU_ACCESS_KEY"));
config.setAccessSecret(System.getenv("QINIU_SECRET_KEY"));
// config.setAccessKey("b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8");
// config.setAccessSecret("kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP");
config.setBucket("test");
config.setDomain("http://test.test.iocoder.cn"); // 如果有自定义域名,则可以设置。
// 默认上海的 endpoint
config.setEndpoint("s3-cn-south-1.qiniucs.com");

// 执行上传
testExecuteUpload(config);
}

@Test
@Disabled // 华为云存储,如果要集成测试,可以注释本行
public void testHuaweiCloud() throws Exception {
S3FileClientConfig config = new S3FileClientConfig();
// 配置成你自己的
// config.setAccessKey(System.getenv("HUAWEI_CLOUD_ACCESS_KEY"));
// config.setAccessSecret(System.getenv("HUAWEI_CLOUD_SECRET_KEY"));
config.setBucket("test");
config.setDomain(null); // 如果有自定义域名,则可以设置。
// 默认上海的 endpoint
config.setEndpoint("obs.cn-east-3.myhuaweicloud.com");

// 执行上传
testExecuteUpload(config);
}

private void testExecuteUpload(S3FileClientConfig config) throws Exception {
// 创建 Client
S3FileClient client = new S3FileClient("test", config);
client.init();
// 上传文件
String path = IdUtil.fastSimpleUUID() + ".png";
byte[] content = ResourceUtil.readBytes("file/test.png");
String fullPath = client.upload(content, path, "image/png");
System.out.println("访问地址:" + fullPath);
// 读取文件
if (true) {
byte[] bytes = client.getContent(path);
System.out.println("文件内容:" + bytes.length);
}
// 删除文件
if (false) {
client.delete(path);
}
}

}


本站由 @binvv 使用 Stellar 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。