文件服务器 file service 增加:ftp、ceph、aliyun oss 初版
This commit is contained in:
parent
7e72849d05
commit
046c27d71c
|
|
@ -65,7 +65,60 @@
|
|||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-swagger</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- apache commons net ftp工具类; hutool 里面 scope 是 compile -->
|
||||
<!--用来ftp上传-->
|
||||
<dependency>
|
||||
<groupId>commons-net</groupId>
|
||||
<artifactId>commons-net</artifactId>
|
||||
<version>3.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.7.3</version>
|
||||
</dependency>
|
||||
|
||||
<!--阿里云OSS库-->
|
||||
<dependency>
|
||||
<groupId>com.aliyun.oss</groupId>
|
||||
<artifactId>aliyun-sdk-oss</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<exclusions>
|
||||
<!--排除 sdk-core 存在的包-->
|
||||
<exclusion>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpcore</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!--ceph upload start -->
|
||||
<!--文件上传 ceph 形式,一般项目都用不上,注释掉; 不要删-->
|
||||
<!-- ceph实现s3 文件上传-->
|
||||
<dependency>
|
||||
<groupId>com.ceph</groupId>
|
||||
<artifactId>libcephfs</artifactId>
|
||||
<version>0.80.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-s3</artifactId>
|
||||
<version>1.11.415</version>
|
||||
</dependency>
|
||||
<!--ceph upload end -->
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
package com.ruoyi.file.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* aliyun oss https://help.aliyun.com/learn/learningpath/oss.html ,需要购买
|
||||
*
|
||||
* @author dazer
|
||||
*/
|
||||
@RefreshScope
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "aliyun.oss")
|
||||
public class AliyunOssConfig {
|
||||
/**
|
||||
* aliyun oss相关配置
|
||||
* ACCESS_KEY_SECRET
|
||||
* AccessKeyId eg:LTAI4GFov2QymkmPf9cXdH5z
|
||||
* AccessKeySecret eg:ap8nmIvD1TctcCLsADS4JbkOoXOluW
|
||||
* BucketName eg:yuebaoxiao
|
||||
* Endpoint eg:oss-cn-shenzhen.aliyuncs.com
|
||||
*/
|
||||
private String accessKeyId;
|
||||
private String accessKeySecret;
|
||||
private String ossBucketName;
|
||||
private String ossEndpoint;
|
||||
|
||||
public String getAccessKeyId() {
|
||||
return accessKeyId;
|
||||
}
|
||||
|
||||
public void setAccessKeyId(String accessKeyId) {
|
||||
this.accessKeyId = accessKeyId;
|
||||
}
|
||||
|
||||
public String getAccessKeySecret() {
|
||||
return accessKeySecret;
|
||||
}
|
||||
|
||||
public void setAccessKeySecret(String accessKeySecret) {
|
||||
this.accessKeySecret = accessKeySecret;
|
||||
}
|
||||
|
||||
public String getOssBucketName() {
|
||||
return ossBucketName;
|
||||
}
|
||||
|
||||
public void setOssBucketName(String ossBucketName) {
|
||||
this.ossBucketName = ossBucketName;
|
||||
}
|
||||
|
||||
public String getOssEndpoint() {
|
||||
return ossEndpoint;
|
||||
}
|
||||
|
||||
public void setOssEndpoint(String ossEndpoint) {
|
||||
this.ossEndpoint = ossEndpoint;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.ruoyi.file.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* ftp config
|
||||
* vsftpd 服务器搭建、ftp客户端filezilla使、ceph分布式文件系统
|
||||
* https://blog.csdn.net/ab601026460/article/details/105928311
|
||||
* @author dazer
|
||||
*/
|
||||
@RefreshScope
|
||||
@Configuration
|
||||
@ConfigurationProperties(
|
||||
prefix = "ftp"
|
||||
)
|
||||
public class FtpConfig {
|
||||
/**
|
||||
* ftp访问地址
|
||||
* eg1: www.ourslook.com
|
||||
* eg2: 192.168.0.1
|
||||
*/
|
||||
private String hostName;
|
||||
/**
|
||||
* ftp端口,默认21
|
||||
*/
|
||||
private Integer port = 21;
|
||||
/**
|
||||
* ftp访问密码
|
||||
* 有可能是匿名,就不需要账号和密码
|
||||
* eg: ftpuser
|
||||
*/
|
||||
private String userName;
|
||||
/**
|
||||
* ftp账号密码
|
||||
* eg: ftpx123.pwd..1
|
||||
*/
|
||||
private String password;
|
||||
/**
|
||||
* filezila server: 就是空
|
||||
* vsftpd:使用的 系统用户的目录,这里往往都是不是根目录,如:/home/ftpuser/
|
||||
*/
|
||||
private String rootFtpPath = "";
|
||||
|
||||
public String getHostName() {
|
||||
return hostName;
|
||||
}
|
||||
|
||||
public void setHostName(String hostName) {
|
||||
this.hostName = hostName;
|
||||
}
|
||||
|
||||
public Integer getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public void setPort(Integer port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public String getUserName() {
|
||||
return userName;
|
||||
}
|
||||
|
||||
public void setUserName(String userName) {
|
||||
this.userName = userName;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getRootFtpPath() {
|
||||
return rootFtpPath;
|
||||
}
|
||||
|
||||
public void setRootFtpPath(String rootFtpPath) {
|
||||
this.rootFtpPath = rootFtpPath;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.ruoyi.file.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import io.minio.MinioClient;
|
||||
|
|
@ -10,27 +11,32 @@ import io.minio.MinioClient;
|
|||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RefreshScope
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "minio")
|
||||
public class MinioConfig
|
||||
{
|
||||
/**
|
||||
* 服务地址
|
||||
* eg: http://192.168.254.100:9900
|
||||
*/
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
* eg: D998GE6ZTQXSATTJWX35
|
||||
*/
|
||||
private String accessKey;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
* eg: QZVQGnhIQQE734UYSUFlGOZViE6+ZlDEfUG3NjXJ
|
||||
*/
|
||||
private String secretKey;
|
||||
|
||||
/**
|
||||
* 存储桶名称
|
||||
* eg: mall
|
||||
*/
|
||||
private String bucketName;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.ruoyi.file.config;
|
|||
|
||||
import java.io.File;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
|
@ -11,17 +12,20 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RefreshScope
|
||||
@Configuration
|
||||
public class ResourcesConfig implements WebMvcConfigurer
|
||||
{
|
||||
/**
|
||||
* 上传文件存储在本地的根路径
|
||||
* eg: D:/ruoyi/uploadPath
|
||||
*/
|
||||
@Value("${file.path}")
|
||||
private String localFilePath;
|
||||
|
||||
/**
|
||||
* 资源映射路径 前缀
|
||||
* eg: /statics
|
||||
*/
|
||||
@Value("${file.prefix}")
|
||||
public String localFilePrefix;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,564 @@
|
|||
package com.ruoyi.file.service;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.aliyun.oss.*;
|
||||
import com.aliyun.oss.model.*;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import com.ruoyi.file.config.AliyunOssConfig;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* aliyun oss 文件存储 实现类
|
||||
* https://help.aliyun.com/learn/learningpath/oss.html
|
||||
* 代码核心实现,来自aliyun oss 官方demo
|
||||
* https://github.com/aliyun/aliyun-oss-java-sdk
|
||||
* <p>
|
||||
* 对象存储可以对图片进行处理
|
||||
* 图片处理 https://help.aliyun.com/document_detail/47505.html
|
||||
* 图片样式 https://help.aliyun.com/document_detail/48884.html
|
||||
* </p>
|
||||
* 对象存储鉴权:阿里云临时安全令牌(Security Token Service,STS) https://help.aliyun.com/document_detail/28756.htm?spm=a2c4g.11186623.2.4.3fba6d13JdXEzJ#reference-ong-5nv-xdb
|
||||
* 对象存储oss docs 授权访问 使用STS进行临时授权 使用签名URL进行临时授权 https://help.aliyun.com/document_detail/32016.html?spm=a2c4g.11186623.6.992.7a943b4aPjkyTA#title-pu8-5o8-x7j
|
||||
* 搜索 【sts】
|
||||
*
|
||||
* @author yabo dazer
|
||||
* @date 2019/8/6 19:02
|
||||
* //@see AliyunMsgUtil
|
||||
*/
|
||||
@Service
|
||||
public class AliyunOssFileServiceImpl implements ISysFileService {
|
||||
private static final Logger log = LoggerFactory.getLogger(AliyunOssFileServiceImpl.class);
|
||||
@Autowired
|
||||
private AliyunOssConfig aliyunOssConfig;
|
||||
/**
|
||||
* 这些阿里云访问id
|
||||
*/
|
||||
private String ACCESS_KEY_ID;
|
||||
private String ACCESS_KEY_SECRET;
|
||||
private String BUCKET_NAME;
|
||||
/**
|
||||
* https://oss.console.aliyun.com/
|
||||
* 这些阿里云 oss 参数都需要替换
|
||||
* <p>
|
||||
* 如果是内网的话,访问速度肯定更快,内网不限制速度。
|
||||
*/
|
||||
private String ENDPOINT = "oss-cn-shenzhen.aliyuncs.com";
|
||||
private String ENDPOINT_INTERNAL = ENDPOINT.replace(".aliyuncs.com", "-internal.aliyuncs.com");
|
||||
|
||||
/**
|
||||
* 域名绑定
|
||||
* USER_DOMAIN_NAME: 域名名称, oss 访问路径绑定的用户自定义域名; 如果没有,就设置为null
|
||||
* hostHttps: 是否开启了https, 需要在控制台配置
|
||||
* https://oss.console.aliyun.com/bucket/oss-cn-shanghai/hiber2019/domain
|
||||
* <p>
|
||||
* private static final String USER_DOMAIN_NAME = "image.jl-media.cn";
|
||||
*/
|
||||
private static final String USER_DOMAIN_NAME = null;
|
||||
private static final boolean HOST_HTTPS = true;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
ACCESS_KEY_ID = aliyunOssConfig.getAccessKeyId();
|
||||
ACCESS_KEY_SECRET = aliyunOssConfig.getAccessKeySecret();
|
||||
BUCKET_NAME = aliyunOssConfig.getOssBucketName();
|
||||
ENDPOINT = aliyunOssConfig.getOssEndpoint();
|
||||
ENDPOINT_INTERNAL = ENDPOINT.replace(".aliyuncs.com", "-internal.aliyuncs.com");
|
||||
}
|
||||
|
||||
/**
|
||||
* demo 地址 https://help.aliyun.com/learn/learningpath/oss.html
|
||||
* <p>
|
||||
* 简单demo https://github.com/aliyun/aliyun-oss-java-sdk/blob/master/src/samples/UploadSample.java
|
||||
* 分片上传demo(大文件) https://github.com/aliyun/aliyun-oss-java-sdk/blob/master/src/samples/MultipartUploadSample.java 分片上传,在oss上面能看到碎片记录
|
||||
*
|
||||
* @return eg: https://hiber2019.oss-cn-shanghai.aliyuncs.com/upload/default/20190806202208849_jvs5g.png
|
||||
*/
|
||||
@Override
|
||||
public String uploadFile(MultipartFile file) throws Exception {
|
||||
return this.uploadFile(file, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uploadFile(MultipartFile file, String modules) throws Exception {
|
||||
//key: 这里不能以/开头
|
||||
String newName = validateModule(file, null);
|
||||
//key: 这里不能以/开头
|
||||
String requestKey = "upload/" + "/" + newName;
|
||||
//这里增加一个前缀区分一下是测试环境还是正式环境
|
||||
boolean isProd = "prod".equalsIgnoreCase(SpringUtil.getActiveProfile());
|
||||
if (!isProd) {
|
||||
requestKey = SpringUtil.getActiveProfile() + "/" + requestKey;
|
||||
}
|
||||
|
||||
long mb5 = 5 * 1024 * 1024L;
|
||||
if (file.getSize() > mb5) {
|
||||
//大于5mb,我们就分片上传
|
||||
this.ossUploadFileBigMultiable(isProd ? ENDPOINT_INTERNAL : ENDPOINT, requestKey, file);
|
||||
} else {
|
||||
//否则,我们常规上传
|
||||
this.ossUploadFileSmall(isProd ? ENDPOINT_INTERNAL : ENDPOINT, requestKey, file);
|
||||
}
|
||||
|
||||
// 解析结果
|
||||
// 注意,这里可能 需要 replace
|
||||
String accessPath;
|
||||
if (StringUtils.isNotBlank(USER_DOMAIN_NAME)) {
|
||||
if (HOST_HTTPS) {
|
||||
accessPath = "https://" + USER_DOMAIN_NAME + "/" + requestKey;
|
||||
} else {
|
||||
accessPath = "http://" + USER_DOMAIN_NAME + "/" + requestKey;
|
||||
}
|
||||
} else {
|
||||
accessPath = "https://" + BUCKET_NAME + "." + ENDPOINT + "/" + requestKey;
|
||||
}
|
||||
return accessPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* demo 地址 https://help.aliyun.com/learn/learningpath/oss.html
|
||||
* https://github.com/aliyun/aliyun-oss-java-sdk/blob/master/src/samples/DeleteObjectsSample.java
|
||||
*/
|
||||
@Override
|
||||
public boolean deleteFile(String fileUrl) {
|
||||
/*
|
||||
* Constructs a client instance with your account for accessing OSS
|
||||
*/
|
||||
OSS client = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
|
||||
String storePath = getStorePath(fileUrl);
|
||||
List<String> keys = new ArrayList<>();
|
||||
keys.add(storePath);
|
||||
|
||||
try {
|
||||
/*
|
||||
* Delete all objects uploaded recently under the bucket
|
||||
*/
|
||||
log.info("\nDeleting all objects:");
|
||||
DeleteObjectsResult deleteObjectsResult = client.deleteObjects(
|
||||
new DeleteObjectsRequest(BUCKET_NAME).withKeys(keys));
|
||||
List<String> deletedObjects = deleteObjectsResult.getDeletedObjects();
|
||||
for (String object : deletedObjects) {
|
||||
log.info("\t" + object);
|
||||
}
|
||||
return true;
|
||||
} catch (OSSException oe) {
|
||||
log.error("Caught an OSSException, which means your request made it to OSS, "
|
||||
+ "but was rejected with an error response for some reason.");
|
||||
log.error("Error Message: " + oe.getErrorCode());
|
||||
log.error("Error Code: " + oe.getErrorCode());
|
||||
log.error("Request ID: " + oe.getRequestId());
|
||||
log.error("Host ID: " + oe.getHostId());
|
||||
} catch (ClientException ce) {
|
||||
log.error("Caught an ClientException, which means the client encountered "
|
||||
+ "a serious internal problem while trying to communicate with OSS, "
|
||||
+ "such as not being able to access the network.");
|
||||
log.error("Error Message: " + ce.getMessage());
|
||||
} finally {
|
||||
/*
|
||||
* Do not forget to shut down the client finally to release all allocated resources.
|
||||
*/
|
||||
client.shutdown();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换url
|
||||
*
|
||||
* @param filePath https://hiber2019.oss-cn-shanghai.aliyuncs.com/upload/default/20190806202208849_jvs5g.png
|
||||
* @return upload/default/20190806202208849_jvs5g.png
|
||||
*/
|
||||
private String getStorePath(String filePath) {
|
||||
String publicPath1 = "https://" + BUCKET_NAME + "." + ENDPOINT + "/";
|
||||
String publicPath2 = "http://" + BUCKET_NAME + "." + ENDPOINT + "/";
|
||||
String publicPath3 = "https://" + USER_DOMAIN_NAME + "/";
|
||||
String publicPath4 = "http://" + USER_DOMAIN_NAME + "/";
|
||||
//String publicPath5 = ServletCacheUtils.getInstance().getHttpRootPath();
|
||||
|
||||
|
||||
filePath = filePath.replace(publicPath1, "");
|
||||
filePath = filePath.replace(publicPath2, "");
|
||||
filePath = filePath.replace(publicPath3, "");
|
||||
filePath = filePath.replace(publicPath4, "");
|
||||
//filePath = filePath.replace(publicPath5, "");
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* oss上传方式1:小文件
|
||||
*
|
||||
* @param picturePath 文件的访问路径,access url
|
||||
* @param file 等待上传的文件
|
||||
* @return picturePath 上传文件的相对路径
|
||||
* demo https://github.com/aliyun/aliyun-oss-java-sdk/blob/master/src/samples/UploadSample.java
|
||||
*/
|
||||
private String ossUploadFileSmall(String endpoint, String picturePath, MultipartFile file) throws IOException {
|
||||
OSS ossClient = new OSSClientBuilder().build(endpoint, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
|
||||
|
||||
try {
|
||||
// 上传文件 (上传文件流的形式)
|
||||
PutObjectResult putResult = ossClient.putObject(BUCKET_NAME, picturePath, file.getInputStream());
|
||||
} catch (OSSException oe) {
|
||||
log.error("Caught an OSSException, which means your request made it to OSS, "
|
||||
+ "but was rejected with an error response for some reason.");
|
||||
log.error("Error Message: {}", oe.getErrorMessage());
|
||||
log.error("Error Code {}: ", oe.getErrorCode());
|
||||
log.error("Request ID {}: ", oe.getRequestId());
|
||||
log.error("Host ID {}: ", oe.getHostId());
|
||||
log.error("");
|
||||
|
||||
log.error("图片上传失败(OSS)", oe);
|
||||
oe.printStackTrace();
|
||||
throw new IOException("图片上传失败(OSS)");
|
||||
} catch (ClientException ce) {
|
||||
log.error("Caught an ClientException, which means the client encountered "
|
||||
+ "a serious internal problem while trying to communicate with OSS, "
|
||||
+ "such as not being able to access the network.");
|
||||
log.error("Error Message: " + ce.getMessage());
|
||||
log.error("");
|
||||
|
||||
log.error("图片上传失败(Client)", ce);
|
||||
ce.printStackTrace();
|
||||
throw new IOException("图片上传失败(Client)");
|
||||
} catch (Throwable e) {
|
||||
log.error("图片上传失败(Throwable)", e);
|
||||
e.printStackTrace();
|
||||
|
||||
/* try {
|
||||
//保存错误日志
|
||||
SysLogService sysLogService = SpringContextUtils.getBean(SysLogService.class);
|
||||
sysLogService.saveExceptionLog(e, "图片上传失败", SysLogEntity.OPERATION_WARN_LEVEL_1);
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}*/
|
||||
|
||||
throw new IOException("图片上传失败(Throwable)");
|
||||
} finally {
|
||||
ossClient.shutdown();
|
||||
}
|
||||
return picturePath;
|
||||
}
|
||||
|
||||
// ================================= 一下全是大文件上传的代码 ===============================================
|
||||
|
||||
/**
|
||||
* oss上传方式2:大文件(分片上传)
|
||||
*
|
||||
* @param picturePath 文件的访问路径,access url, 也是 oss中的唯一存放地址
|
||||
* @param file 等待上传的文件
|
||||
* @return picturePath 上传文件的相对路径
|
||||
* demo https://github.com/aliyun/aliyun-oss-java-sdk/blob/master/src/samples/MultipartUploadSample.java
|
||||
* <p>
|
||||
* 除了通过PUT Object接口上传文件到OSS以外,OSS还提供了另外一种上传模式——Multipart Upload。 https://help.aliyun.com/document_detail/31991.html
|
||||
*/
|
||||
private String ossUploadFileBigMultiable(String endpoint, String picturePath, MultipartFile file) throws IOException {
|
||||
String requestKey = String.valueOf(picturePath);
|
||||
List<PartETag> partETags = Collections.synchronizedList(new ArrayList<>());
|
||||
/**
|
||||
* 线程池
|
||||
* Executors.newFixedThreadPool(10);
|
||||
* 这里不限制大小,线程会按照最大能力,开启,限制了大小,比如:10, 就一共只开启这么多线程
|
||||
* <p>
|
||||
* private static ExecutorService executorService = Executors.newCachedThreadPool();
|
||||
*/
|
||||
ExecutorService executorService = new ThreadPoolExecutor(50, 1000, 60L, TimeUnit.SECONDS,
|
||||
new SynchronousQueue<>(), new ThreadFactoryBuilder().setNameFormat("aliyun-oss-upload(" + requestKey + ")-thread-pool-%d").build());
|
||||
|
||||
/*
|
||||
* Constructs a client instance with your account for accessing OSS
|
||||
*/
|
||||
ClientBuilderConfiguration conf = new ClientBuilderConfiguration();
|
||||
conf.setIdleConnectionTime(1000);
|
||||
OSS client = new OSSClientBuilder().build(endpoint, ACCESS_KEY_ID, ACCESS_KEY_SECRET, conf);
|
||||
|
||||
try {
|
||||
/*
|
||||
* Claim a upload id firstly
|
||||
*/
|
||||
InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(BUCKET_NAME, requestKey);
|
||||
InitiateMultipartUploadResult result = client.initiateMultipartUpload(request);
|
||||
String uploadId = result.getUploadId();
|
||||
|
||||
/*
|
||||
* Calculate how many parts to be divided
|
||||
*/
|
||||
final long partSize = 5 * 1024 * 1024L; // 5MB
|
||||
long fileLength = file.getSize();
|
||||
int partCount = (int) (fileLength / partSize);
|
||||
if (fileLength % partSize != 0) {
|
||||
partCount++;
|
||||
}
|
||||
if (partCount > 10000) {
|
||||
throw new RuntimeException("Total parts count should not exceed 10000");
|
||||
}
|
||||
/*
|
||||
* Upload multiparts to your bucket
|
||||
*/
|
||||
if (log.isInfoEnabled()) {
|
||||
log.info("Begin to upload multiparts to OSS from a file\n");
|
||||
}
|
||||
for (int i = 0; i < partCount; i++) {
|
||||
long startPos = i * partSize;
|
||||
long curPartSize = (i + 1 == partCount) ? (fileLength - startPos) : partSize;
|
||||
executorService.execute(new PartUploader(client, requestKey, partETags, file, startPos, curPartSize, i + 1, uploadId));
|
||||
}
|
||||
|
||||
/*
|
||||
* Waiting for all parts finished
|
||||
*/
|
||||
executorService.shutdown();
|
||||
while (!executorService.isTerminated()) {
|
||||
try {
|
||||
executorService.awaitTermination(5, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
/* try {
|
||||
//等待所有任务执行完毕, 这里 不限定20分钟必须执行完毕
|
||||
countDownLatch.await(60 * 20, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
log.error("所有线程都已经执行完毕...., 一共开启线程数量: {}", partCount);
|
||||
}*/
|
||||
|
||||
/*
|
||||
* Verify whether all parts are finished
|
||||
*/
|
||||
if (partETags.size() != partCount) {
|
||||
throw new RuntimeException("Upload multiparts fail due to some parts are not finished yet");
|
||||
} else {
|
||||
if (log.isInfoEnabled()) {
|
||||
log.info("Succeed to complete multiparts into an object named {} \n", requestKey);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* View all parts uploaded recently
|
||||
*/
|
||||
listAllParts(client, requestKey, uploadId);
|
||||
|
||||
/*
|
||||
* Complete to upload multiparts
|
||||
*/
|
||||
completeMultipartUpload(client, partETags, requestKey, uploadId);
|
||||
|
||||
/*
|
||||
* Fetch the object that newly created at the step below.
|
||||
*/
|
||||
if (log.isInfoEnabled()) {
|
||||
log.info("Fetching an object");
|
||||
}
|
||||
OSSObject ossObject = client.getObject(new GetObjectRequest(BUCKET_NAME, requestKey));
|
||||
if (log.isInfoEnabled()) {
|
||||
log.info(ossObject.getKey());
|
||||
}
|
||||
} catch (OSSException oe) {
|
||||
log.error("Caught an OSSException, which means your request made it to OSS, "
|
||||
+ "but was rejected with an error response for some reason.");
|
||||
log.error("Error Message: {}", oe.getErrorMessage());
|
||||
log.error("Error Code {}: ", oe.getErrorCode());
|
||||
log.error("Request ID {}: ", oe.getRequestId());
|
||||
log.error("Host ID {}: ", oe.getHostId());
|
||||
log.error("");
|
||||
|
||||
log.error("图片上传失败(OSS)", oe);
|
||||
oe.printStackTrace();
|
||||
throw new IOException("图片上传失败(OSS Multipart)");
|
||||
} catch (ClientException ce) {
|
||||
log.error("Caught an ClientException, which means the client encountered "
|
||||
+ "a serious internal problem while trying to communicate with OSS, "
|
||||
+ "such as not being able to access the network.");
|
||||
log.error("Error Message: " + ce.getMessage());
|
||||
log.error("");
|
||||
|
||||
log.error("图片上传失败(Client)", ce);
|
||||
ce.printStackTrace();
|
||||
throw new IOException("图片上传失败(Client Multipart)");
|
||||
} finally {
|
||||
/*
|
||||
* Do not forget to shut down the client finally to release all allocated resources.
|
||||
*/
|
||||
if (client != null) {
|
||||
client.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
return picturePath;
|
||||
}
|
||||
|
||||
private class PartUploader implements Runnable {
|
||||
private MultipartFile file;
|
||||
private long startPos;
|
||||
|
||||
private long partSize;
|
||||
private int partNumber;
|
||||
private String uploadId;
|
||||
|
||||
private OSS client;
|
||||
private String requestKey;
|
||||
private final List<PartETag> partETags;
|
||||
|
||||
private PartUploader(OSS client, String requestKey, List<PartETag> partETags, MultipartFile file, long startPos, long partSize, int partNumber, String uploadId) {
|
||||
this.client = client;
|
||||
this.requestKey = requestKey;
|
||||
this.partETags = partETags;
|
||||
|
||||
this.file = file;
|
||||
this.startPos = startPos;
|
||||
this.partSize = partSize;
|
||||
this.partNumber = partNumber;
|
||||
this.uploadId = uploadId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
InputStream instream = null;
|
||||
try {
|
||||
instream = file.getInputStream();
|
||||
instream.skip(this.startPos);
|
||||
|
||||
UploadPartRequest uploadPartRequest = new UploadPartRequest();
|
||||
uploadPartRequest.setBucketName(BUCKET_NAME);
|
||||
uploadPartRequest.setKey(this.requestKey);
|
||||
uploadPartRequest.setUploadId(this.uploadId);
|
||||
uploadPartRequest.setInputStream(instream);
|
||||
uploadPartRequest.setPartSize(this.partSize);
|
||||
uploadPartRequest.setPartNumber(this.partNumber);
|
||||
|
||||
UploadPartResult uploadPartResult = client.uploadPart(uploadPartRequest);
|
||||
if (log.isInfoEnabled()) {
|
||||
log.info("Part {} done\n", this.partNumber);
|
||||
}
|
||||
|
||||
synchronized (partETags) {
|
||||
partETags.add(uploadPartResult.getPartETag());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (instream != null) {
|
||||
try {
|
||||
instream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void completeMultipartUpload(OSS client, List<PartETag> partETags, String requestKey, String uploadId) {
|
||||
// Make part numbers in ascending order
|
||||
partETags.sort(Comparator.comparingInt(PartETag::getPartNumber));
|
||||
|
||||
if (log.isInfoEnabled()) {
|
||||
log.info("Completing to upload multiparts\n");
|
||||
}
|
||||
|
||||
CompleteMultipartUploadRequest completeMultipartUploadRequest =
|
||||
new CompleteMultipartUploadRequest(BUCKET_NAME, requestKey, uploadId, partETags);
|
||||
client.completeMultipartUpload(completeMultipartUploadRequest);
|
||||
}
|
||||
|
||||
private void listAllParts(OSS client, String requestKey, String uploadId) {
|
||||
if (log.isInfoEnabled()) {
|
||||
log.info("Listing all parts......");
|
||||
}
|
||||
ListPartsRequest listPartsRequest = new ListPartsRequest(BUCKET_NAME, requestKey, uploadId);
|
||||
PartListing partListing = client.listParts(listPartsRequest);
|
||||
|
||||
int partCount = partListing.getParts().size();
|
||||
for (int i = 0; i < partCount; i++) {
|
||||
PartSummary partSummary = partListing.getParts().get(i);
|
||||
if (log.isInfoEnabled()) {
|
||||
log.info("\tPart# {} , ETag={}", partSummary.getPartNumber(), partSummary.getETag());
|
||||
}
|
||||
}
|
||||
if (log.isInfoEnabled()) {
|
||||
log.info("\n");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String listObject() {
|
||||
OSS client = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
|
||||
final int maxKeys = 200;
|
||||
String nextMarker = null;
|
||||
ObjectListing objectListing;
|
||||
|
||||
long size = 0L;
|
||||
String result = "";
|
||||
|
||||
do
|
||||
{
|
||||
objectListing = client.listObjects(new ListObjectsRequest(BUCKET_NAME).withMarker(nextMarker).withMaxKeys(maxKeys));
|
||||
|
||||
List<OSSObjectSummary> sums = objectListing.getObjectSummaries();
|
||||
for (OSSObjectSummary s : sums) {
|
||||
size += s.getSize() / 1024;
|
||||
}
|
||||
nextMarker = objectListing.getNextMarker();
|
||||
|
||||
} while (objectListing.isTruncated());
|
||||
client.shutdown();
|
||||
|
||||
if (size > (1024 * 1024)) {
|
||||
result = (new BigDecimal((double) size / 1024 / 1024)).setScale(2, BigDecimal.ROUND_HALF_UP) + "GB";
|
||||
} else if (size > 1024) {
|
||||
result = (new BigDecimal((double) size / 1024).setScale(2, BigDecimal.ROUND_HALF_UP)) + "MB";
|
||||
} else {
|
||||
result = size + "KB";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 阿里云 对象存储 oss 使用签名URL进行临时授权
|
||||
* https://help.aliyun.com/document_detail/32016.html?spm=a2c4g.11186623.6.992.7a943b4aPjkyTA#title-pu8-5o8-x7j
|
||||
*
|
||||
* @param objectName 完成的url, filePath
|
||||
* @return 返回url签名之后的url
|
||||
*/
|
||||
public String getStsURL(String objectName) {
|
||||
if (StringUtils.isBlank(objectName)) {
|
||||
return objectName;
|
||||
}
|
||||
try {
|
||||
objectName = new URL(objectName).getPath();
|
||||
if (StringUtils.isBlank(objectName)) {
|
||||
return objectName;
|
||||
}
|
||||
if (objectName.startsWith("/")) {
|
||||
objectName = objectName.replaceFirst("/", ""); // 不能以/ 开头。例如 /dev/upload/123.jpg,需要转为 dev/upload/123.jpg
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
// 忽略
|
||||
}
|
||||
OSS ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
|
||||
// 设置URL过期时间为12小时,最大值就是43200
|
||||
Date expiration = new Date(System.currentTimeMillis() + (43200 * 1000));
|
||||
// 生成以GET方法访问的签名URL,访客可以直接通过浏览器访问相关内容。
|
||||
URL url = ossClient.generatePresignedUrl(BUCKET_NAME, objectName, expiration);
|
||||
// 关闭OSSClient。
|
||||
ossClient.shutdown();
|
||||
return url.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
package com.ruoyi.file.service;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.amazonaws.ClientConfiguration;
|
||||
import com.amazonaws.Protocol;
|
||||
import com.amazonaws.auth.AWSCredentials;
|
||||
import com.amazonaws.auth.BasicAWSCredentials;
|
||||
import com.amazonaws.services.s3.AmazonS3;
|
||||
import com.amazonaws.services.s3.AmazonS3Client;
|
||||
import com.amazonaws.services.s3.model.*;
|
||||
import com.ruoyi.common.core.utils.SpringUtils;
|
||||
import com.ruoyi.file.utils.FileUploadUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author dazer
|
||||
* @date 2020-4-4
|
||||
* ceph实现s3文件上传
|
||||
* 上传文件相关
|
||||
* 参考网址:https://blog.csdn.net/qq_32524177/article/details/76226257
|
||||
* ceph储存的S3接口实现 或者支持文件挂载的方式(NFS NAS挂载)
|
||||
* https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/dev/RetrievingObjectUsingJava.html
|
||||
* 参考:Amazonaws S3 java SDK连接初探 https://www.cnblogs.com/zmdd/p/9342510.html
|
||||
* =========================================================================================================================
|
||||
* 部署:
|
||||
* 1:https://ceph.com/planet/%E5%9F%BA%E4%BA%8Edocker%E9%83%A8%E7%BD%B2ceph%E4%BB%A5%E5%8F%8A%E4%BF%AE%E6%94%B9docker-image/
|
||||
* 基于docker部署ceph以及修改docker image
|
||||
* 2:ceph存储,使用docker部署 https://www.cnblogs.com/bladeyul/p/10649049.html
|
||||
* 3:使用docker 搭建 ceph 开发环境,使用aws sdk 存储数据 https://blog.csdn.net/freewebsys/article/details/79553386
|
||||
*/
|
||||
public class CephSysFileServiceImpl implements ISysFileService {
|
||||
private static final Logger log = LoggerFactory.getLogger(AliyunOssFileServiceImpl.class);
|
||||
|
||||
protected static AmazonS3 amazonS3 = null;
|
||||
/**
|
||||
* s3 提供的 accessKey secretKey
|
||||
* BUCKET_NAME: 概念和阿里云 oss 一模一样
|
||||
*/
|
||||
private static String ACCESS_KEY = "XPVF8TESA1X4SFU*****";
|
||||
private static String SECRET_KEY = "hBBEFpV3qsyI7HAdCBzA2ZdAhuANJFRIUz****";
|
||||
private static String HOST = "127.0.0.1";
|
||||
private static String BUCKET_NAME = "dfwwbook";
|
||||
/**
|
||||
* 域名绑定
|
||||
* USER_DOMAIN_NAME: 域名名称, oss 访问路径绑定的用户自定义域名; 如果没有,就设置为null
|
||||
* hostHttps: 是否开启了https, 需要在控制台配置
|
||||
* https://oss.console.aliyun.com/bucket/oss-cn-shanghai/hiber2019/domain
|
||||
* <p>
|
||||
* private static final String USER_DOMAIN_NAME = "image.jl-media.cn";
|
||||
*/
|
||||
private static final String USER_DOMAIN_NAME = null;
|
||||
private static final boolean HOST_HTTPS = true;
|
||||
|
||||
/**
|
||||
* ceph配置初始化
|
||||
*/
|
||||
static {
|
||||
log.info("开始初始化ceph配置");
|
||||
AWSCredentials credentials = new BasicAWSCredentials(ACCESS_KEY, SECRET_KEY);
|
||||
ClientConfiguration clientConfiguration = new ClientConfiguration();
|
||||
clientConfiguration.setProtocol(Protocol.HTTP);
|
||||
amazonS3 = new AmazonS3Client(credentials, clientConfiguration);
|
||||
amazonS3.setEndpoint(HOST);
|
||||
log.info("ceph配置初始化成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用s3 api 创建 ceph bucket
|
||||
*
|
||||
* @param bucketname
|
||||
* @return
|
||||
*/
|
||||
public Bucket createBucket(String bucketname) {
|
||||
Bucket bucket = amazonS3.createBucket(bucketname);
|
||||
log.info("bucket name is {}", bucket);
|
||||
return bucket;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 bucketName 和 filename 获取指定文件返回输入流
|
||||
*/
|
||||
public S3ObjectInputStream getObject(String bucketName, String filename) {
|
||||
S3Object s3Object = amazonS3.getObject(new GetObjectRequest(bucketName, filename));
|
||||
S3ObjectInputStream s3ObjectInputStream = s3Object.getObjectContent();
|
||||
return s3ObjectInputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uploadFile(MultipartFile file) throws Exception {
|
||||
return this.uploadFile(file, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uploadFile(MultipartFile file, String modules) throws Exception {
|
||||
//key: 这里不能以/开头
|
||||
String newName = validateModule(file, null);
|
||||
//key: 这里不能以/开头
|
||||
String requestKey = "upload/" + newName;
|
||||
//这里增加一个前缀区分一下是测试环境还是正式环境
|
||||
boolean isProd = "prod".equalsIgnoreCase(SpringUtil.getActiveProfile());
|
||||
if (!isProd) {
|
||||
requestKey = SpringUtil.getActiveProfile() + "/" + requestKey;
|
||||
}
|
||||
|
||||
// long mb5 = 5 * 1024 * 1024L;
|
||||
//大于5mb,我们就分片上传
|
||||
PutObjectResult result = amazonS3.putObject(BUCKET_NAME, requestKey, file.getInputStream(), new ObjectMetadata());
|
||||
// 上传成功
|
||||
if (result.isRequesterCharged()) {
|
||||
// 解析结果
|
||||
// 注意,这里可能 需要 replace
|
||||
String accessPath;
|
||||
if (StringUtils.isNotBlank(USER_DOMAIN_NAME)) {
|
||||
if (HOST_HTTPS) {
|
||||
accessPath = "https://" + USER_DOMAIN_NAME + "/" + requestKey;
|
||||
} else {
|
||||
accessPath = "http://" + USER_DOMAIN_NAME + "/" + requestKey;
|
||||
}
|
||||
} else {
|
||||
accessPath = "https://" + BUCKET_NAME + "/" + requestKey;
|
||||
}
|
||||
return accessPath;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteFile(String fileUrl) {
|
||||
if (StringUtils.isEmpty(fileUrl)) {
|
||||
return false;
|
||||
}
|
||||
String storePath = getStorePath(fileUrl);
|
||||
amazonS3.deleteObject(BUCKET_NAME, storePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String listObject() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换url
|
||||
*
|
||||
* @param filePath https://hiber2019.oss-cn-shanghai.aliyuncs.com/upload/default/20190806202208849_jvs5g.png
|
||||
* @return upload/default/20190806202208849_jvs5g.png
|
||||
*/
|
||||
private String getStorePath(String filePath) {
|
||||
String publicPath1 = "https://" + BUCKET_NAME + "/";
|
||||
String publicPath2 = "http://" + BUCKET_NAME + "/";
|
||||
String publicPath3 = "https://" + USER_DOMAIN_NAME + "/";
|
||||
String publicPath4 = "http://" + USER_DOMAIN_NAME + "/";
|
||||
//String publicPath5 = ServletCacheUtils.getInstance().getHttpRootPath();
|
||||
|
||||
filePath = filePath.replace(publicPath1, "");
|
||||
filePath = filePath.replace(publicPath2, "");
|
||||
filePath = filePath.replace(publicPath3, "");
|
||||
filePath = filePath.replace(publicPath4, "");
|
||||
//filePath = filePath.replace(publicPath5, "");
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
package com.ruoyi.file.service;
|
||||
|
||||
import com.github.tobato.fastdfs.domain.conn.PooledConnectionFactory;
|
||||
import com.github.tobato.fastdfs.exception.FdfsUnsupportStorePathException;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
|
@ -10,14 +15,15 @@ import com.github.tobato.fastdfs.service.FastFileStorageClient;
|
|||
|
||||
/**
|
||||
* FastDFS 文件存储
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Service
|
||||
public class FastDfsSysFileServiceImpl implements ISysFileService
|
||||
{
|
||||
private final Logger logger = LoggerFactory.getLogger(FastDfsSysFileServiceImpl.class);
|
||||
/**
|
||||
* 域名或本机访问地址
|
||||
* FastDFS配置 其他参数见:{@link PooledConnectionFactory}
|
||||
*/
|
||||
@Value("${fdfs.domain}")
|
||||
public String domain;
|
||||
|
|
@ -35,8 +41,38 @@ public class FastDfsSysFileServiceImpl implements ISysFileService
|
|||
@Override
|
||||
public String uploadFile(MultipartFile file) throws Exception
|
||||
{
|
||||
return this.uploadFile(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uploadFile(MultipartFile file, String modules) throws Exception {
|
||||
// fastdsf 这里的 modules 没用
|
||||
validateModule(file, modules);
|
||||
|
||||
StorePath storePath = storageClient.uploadFile(file.getInputStream(), file.getSize(),
|
||||
FilenameUtils.getExtension(file.getOriginalFilename()), null);
|
||||
|
||||
/// fileUrl = "http://127.0.0.1:22122/" + storePath.getFullPath();
|
||||
return domain + "/" + storePath.getFullPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteFile(String fileUrl) {
|
||||
if (StringUtils.isEmpty(fileUrl)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
StorePath storePath = StorePath.parseFromUrl(fileUrl);
|
||||
storageClient.deleteFile(storePath.getGroup(), storePath.getPath());
|
||||
return true;
|
||||
} catch (FdfsUnsupportStorePathException e) {
|
||||
logger.warn(e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String listObject() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
package com.ruoyi.file.service;
|
||||
|
||||
import cn.hutool.extra.ftp.Ftp;
|
||||
import cn.hutool.extra.ftp.FtpMode;
|
||||
import com.ruoyi.file.config.FtpConfig;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* ftp目录用来上传文件;
|
||||
* ftp, 如:iis、linux ftp、vsftpd、FileZilla Server,需要自己搭建服务
|
||||
* @author dazer
|
||||
*/
|
||||
@Service
|
||||
public class FtpFileServiceImpl implements ISysFileService {
|
||||
@Autowired
|
||||
private FtpConfig ftpConfig;
|
||||
public static final String ACCESS_PREFIX = "";
|
||||
|
||||
@Override
|
||||
public String uploadFile(MultipartFile file) throws Exception {
|
||||
return this.uploadFile(file, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uploadFile(MultipartFile file, String modules) throws Exception {
|
||||
String fileName = "upload/" + validateModule(file, modules);
|
||||
|
||||
Ftp ftp = null;
|
||||
try {
|
||||
ftp = new Ftp(ftpConfig.getHostName(), ftpConfig.getPort(), ftpConfig.getUserName(), ftpConfig.getPassword());
|
||||
ftp.cd("");
|
||||
ftp.setMode(FtpMode.Passive);
|
||||
ftp.upload("", fileName, file.getInputStream());
|
||||
} finally {
|
||||
if (ftp != null) {
|
||||
ftp.close();
|
||||
}
|
||||
}
|
||||
return ftpConfig.getHostName() + "/" + fileName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteFile(String fileUrl) {
|
||||
Ftp ftp = null;
|
||||
try {
|
||||
ftp = new Ftp(ftpConfig.getHostName(), ftpConfig.getPort(), ftpConfig.getUserName(), ftpConfig.getPassword());
|
||||
String storePath = getStorePath(fileUrl);
|
||||
return ftp.delFile(storePath);
|
||||
} finally {
|
||||
if (ftp != null) {
|
||||
try {
|
||||
ftp.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String listObject() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getStorePath(String filePath) {
|
||||
int groupStartPos = -1;
|
||||
if ((groupStartPos = filePath.indexOf(ACCESS_PREFIX) + ACCESS_PREFIX.length()) + 1 == 0) {
|
||||
groupStartPos = 0;
|
||||
//throw new RrException("解析文件路径错误,被解析路径url没有" + Constant.SERVIER_NAME_SUFFIX + ",当前解析路径为".concat(filePath));
|
||||
}
|
||||
// 获取group起始位置
|
||||
String groupAndPath = filePath.substring(groupStartPos);
|
||||
return groupAndPath + "";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,44 @@
|
|||
package com.ruoyi.file.service;
|
||||
|
||||
import com.ruoyi.common.core.exception.file.FileNameLengthLimitExceededException;
|
||||
import com.ruoyi.common.core.exception.file.InvalidExtensionException;
|
||||
import com.ruoyi.common.core.utils.file.MimeTypeUtils;
|
||||
import com.ruoyi.file.utils.FileUploadUtils;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.ruoyi.file.utils.FileUploadUtils.assertAllowed;
|
||||
import static com.ruoyi.file.utils.FileUploadUtils.extractFilename;
|
||||
|
||||
/**
|
||||
* 文件上传接口
|
||||
*
|
||||
* 1: default: 最原始的java文件上传
|
||||
* 2: ftp 使用ftp模拟文件服务器; 如:iis、linux ftp、vsftpd、FileZilla Server,需要自己搭建服务
|
||||
* 3: FastDfs 是淘宝开源的分布式文件系统; 淘宝 开发的分布式 dfs, 需要自己搭建服务 (FastDFS)
|
||||
* 4: minio 轻量级分布式文件系统; 类似一个阿里云oss、腾讯COS的一个开源、轻量级别的对象存储付;
|
||||
* 5: aliyun oss; aliyun oss https://help.aliyun.com/learn/learningpath/oss.html ,需要购买
|
||||
* 6: CEPH 分布式大数据文件存储系统 http://docs.ceph.org.cn/
|
||||
* @author ruoyi
|
||||
*/
|
||||
public interface ISysFileService
|
||||
{
|
||||
/**
|
||||
* 允许上传文件存放的目录
|
||||
* 不同项目,这里可能做不同的修改;不过不想区分,就default;
|
||||
* 项目稍微大一些,如果不区分目录,后期要做删除 or 迁移就很麻烦;
|
||||
*/
|
||||
String[] DEFAULT_MODULES_NAME = {
|
||||
// 图片
|
||||
"default", "banner", "product", "images", "music",
|
||||
// pdf
|
||||
"pdf" };
|
||||
|
||||
/**
|
||||
* 文件上传接口
|
||||
*
|
||||
|
|
@ -16,5 +46,52 @@ public interface ISysFileService
|
|||
* @return 访问地址
|
||||
* @throws Exception
|
||||
*/
|
||||
public String uploadFile(MultipartFile file) throws Exception;
|
||||
String uploadFile(MultipartFile file) throws Exception;
|
||||
|
||||
String uploadFile(MultipartFile file, String modules) throws Exception;
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*
|
||||
* @param fileUrl 文件访问地址,全路径或者不是全路径都可以
|
||||
* @return
|
||||
*/
|
||||
boolean deleteFile(String fileUrl);
|
||||
|
||||
/**
|
||||
* 获取文件占用空间
|
||||
* 别名:objectsCapacity
|
||||
* @return 文件大小字符串,eg: 100MB、2G
|
||||
*/
|
||||
String listObject();
|
||||
|
||||
/**
|
||||
* 校验文件名称长度 & 校验文件大小 & 校验上传的目录是否是项目中注册了的 & 返回新的文件名称
|
||||
*
|
||||
* @param file 文件
|
||||
* @param modules 模块,这里作为上传的文件夹使用;eg: 项目中有banner、video、music、txt、product、default 多个模块,不同模块存放到不同文件夹中;
|
||||
* @return 新的系统生成的文件名称
|
||||
* @throws InvalidExtensionException
|
||||
*/
|
||||
default String validateModule(MultipartFile file, String modules) throws InvalidExtensionException {
|
||||
Objects.requireNonNull(file, "文件不能为空!");
|
||||
modules = StringUtils.defaultString(modules, "default");
|
||||
|
||||
//1、这里校验上传文件的模块,如果没有注册,直接报错
|
||||
if (!Arrays.stream(DEFAULT_MODULES_NAME).collect(Collectors.toList()).contains(modules)) {
|
||||
throw new RuntimeException("上传模块" + modules + "不存在,请现在 'FolderPath.UploadModules'中注册. 枚举值,请参见Home接口:api/getFolderPath");
|
||||
}
|
||||
|
||||
// 2、校验文件名称长度
|
||||
/// String ext = (String.valueOf(file.getOriginalFilename()).substring(String.valueOf(file.getOriginalFilename()).lastIndexOf("."))).toLowerCase();
|
||||
int fileNamelength = file.getOriginalFilename().length();
|
||||
if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH)
|
||||
{
|
||||
throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
|
||||
}
|
||||
|
||||
// 3、文件大小校验
|
||||
assertAllowed(file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
|
||||
return modules + "/" + extractFilename(file);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.ruoyi.file.service;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
|
@ -17,18 +18,21 @@ public class LocalSysFileServiceImpl implements ISysFileService
|
|||
{
|
||||
/**
|
||||
* 资源映射路径 前缀
|
||||
* eg: eg: /statics
|
||||
*/
|
||||
@Value("${file.prefix}")
|
||||
public String localFilePrefix;
|
||||
|
||||
/**
|
||||
* 域名或本机访问地址
|
||||
* eg: http://127.0.0.1:9300
|
||||
*/
|
||||
@Value("${file.domain}")
|
||||
public String domain;
|
||||
|
||||
/**
|
||||
* 上传文件存储在本地的根路径
|
||||
* eg: D:/ruoyi/uploadPath
|
||||
*/
|
||||
@Value("${file.path}")
|
||||
private String localFilePath;
|
||||
|
|
@ -43,8 +47,23 @@ public class LocalSysFileServiceImpl implements ISysFileService
|
|||
@Override
|
||||
public String uploadFile(MultipartFile file) throws Exception
|
||||
{
|
||||
String name = FileUploadUtils.upload(localFilePath, file);
|
||||
return this.uploadFile(file, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uploadFile(MultipartFile file, String modules) throws Exception {
|
||||
String name = FileUploadUtils.upload(localFilePath + "/" + StringUtils.defaultString(modules, ""), file);
|
||||
String url = domain + localFilePrefix + name;
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteFile(String fileUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String listObject() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package com.ruoyi.file.service;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import io.minio.RemoveObjectArgs;
|
||||
import io.minio.errors.*;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
|
@ -8,6 +11,10 @@ import com.ruoyi.file.utils.FileUploadUtils;
|
|||
import io.minio.MinioClient;
|
||||
import io.minio.PutObjectArgs;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* Minio 文件存储
|
||||
*
|
||||
|
|
@ -32,7 +39,17 @@ public class MinioSysFileServiceImpl implements ISysFileService
|
|||
@Override
|
||||
public String uploadFile(MultipartFile file) throws Exception
|
||||
{
|
||||
String fileName = FileUploadUtils.extractFilename(file);
|
||||
return this.uploadFile(file, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uploadFile(MultipartFile file, String modules) throws Exception {
|
||||
String fileName = validateModule(file ,modules);
|
||||
boolean isProd = "prod".equalsIgnoreCase(SpringUtil.getActiveProfile());
|
||||
if (!isProd) {
|
||||
fileName = SpringUtil.getActiveProfile() + "/" + fileName;
|
||||
}
|
||||
|
||||
PutObjectArgs args = PutObjectArgs.builder()
|
||||
.bucket(minioConfig.getBucketName())
|
||||
.object(fileName)
|
||||
|
|
@ -42,4 +59,32 @@ public class MinioSysFileServiceImpl implements ISysFileService
|
|||
client.putObject(args);
|
||||
return minioConfig.getUrl() + "/" + minioConfig.getBucketName() + "/" + fileName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteFile(String fileUrl) {
|
||||
RemoveObjectArgs args = RemoveObjectArgs.builder().
|
||||
bucket(minioConfig.getBucketName()).
|
||||
object(fileUrl).
|
||||
build();
|
||||
try {
|
||||
client.removeObject(args);
|
||||
return true;
|
||||
} catch (ErrorResponseException |
|
||||
InsufficientDataException |
|
||||
InternalException |
|
||||
InvalidKeyException |
|
||||
InvalidResponseException |
|
||||
IOException |
|
||||
NoSuchAlgorithmException |
|
||||
ServerException |
|
||||
XmlParserException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String listObject() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue