You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

719 lines
16 KiB
Java

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package com.ruoyi.file;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.img.Img;
import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.ruoyi.common.utils.IdUtils;
import lombok.Data;
import lombok.SneakyThrows;
import org.apache.tika.Tika;
import org.dromara.x.file.storage.core.Downloader;
import org.dromara.x.file.storage.core.FileInfo;
import org.dromara.x.file.storage.core.hash.HashInfo;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
public interface FileService {
/**
* 生成保留文件名的规则
*/
String RULE_KEEP_FILENAME = "/{yyyy}/{MM}/{dd}/{id36}/{filename}.{ext}";
/**
* 生成随机文件名的规则
*/
String RULE_RANDOM_FILENAME = "/{yyyy}/{MM}/{dd}/{id36}.{ext}";
/**
* 默认前缀
*/
String DEFAULT_PREFIX = "default";
/**
* 默认图片文件名
*/
String DEFAULT_IMAGE_FILENAME = "a.webp";
/**
* 缩略图的扩展名
*/
String THUMBNAIL_EXT = ".min.webp";
/**
* 源文件的扩展名
*/
String SRC_EXT = ".zip";
/**
* webp图片格式扩展名
*/
String IMAGE_WEBP = "webp";
ThreadLocal<Param> paramThreadLocal = ThreadLocal.withInitial(() -> new Param());
ThreadLocal<HashInfo> hashInfoThreadLocal = new ThreadLocal<>();
/**
* 获取文件上传完成后计算的md5值
* @return
*/
default String getMd5() {
return hashInfoThreadLocal.get()==null?null:hashInfoThreadLocal.get().getMd5();
}
/**
* 获取文件上传完成后计算的sha256值
* @return
*/
default String getSha256() {
return hashInfoThreadLocal.get()==null?null:hashInfoThreadLocal.get().getSha256();
}
/**
* 初始化分片上传
*
* @return 分配上传编号
*/
String multipartUploadInit();
/**
* 分片上传
*
* @param uploadId
* @param partNumber
* @param inputStream
*/
void multipartUpload(String uploadId, Integer partNumber, InputStream inputStream);
/**
* 分片上传完成
*
* @param uploadId
* @return
*/
String multipartUploadComplete(String uploadId);
/**
* 分片上传取消
*
* @param uploadId
*/
void multipartUploadAbort(String uploadId);
List<Integer> multipartUploadListParts(String uploadId);
/**
* 删除
*
* @param url
*/
void delete(String url);
/**
* 下载
*
* @param url
* @return
*/
Downloader download(String url);
/**
* 下载文件
* 1.支持预签名的跳转
* 2.本地模式的读取下载
* 3.否则,直接读取下载
*
* @param url url
* @param exp 过期时间
* @param request 请求
* @param response 响应
*/
void download(String url, Duration exp, HttpServletRequest request, HttpServletResponse response);
/**
* 生成预签名URL
*
* @param url 原始URL
* @param exp 过期时间
* @return
*/
String generatePresignedUrl(String url, Duration exp);
/**
* 在保存方法之前执行
* 设置平台
*
* @param platform 平台
* @return
*/
default FileService setPlatform(String platform) {
paramThreadLocal.get().setPlatform(platform);
return this;
}
/**
* 设置计算md5
* @return
*/
default FileService setMd5() {
paramThreadLocal.get().setMd5(true);
return this;
}
/**
* 设置是否计算md5
* @param md5
* @return
*/
default FileService setMd5(boolean md5) {
paramThreadLocal.get().setMd5(md5);
return this;
}
/**
* 设置计算Sha256
* @return
*/
default FileService setSha256() {
paramThreadLocal.get().setSha256(true);
return this;
}
/**
* 设置是否计算Sha256
* @return
*/
default FileService setSha256(boolean sha256) {
paramThreadLocal.get().setSha256(sha256);
return this;
}
/**
* 在保存方法之前执行
* 设置路径前缀
*
* @param prefix 前缀
* @return
*/
default FileService setPrefix(String prefix) {
if (StrUtil.isBlank(prefix)) {
return this;
}
if (!prefix.matches("^[\\w\\-]+$")) {
throw new RuntimeException("路径前缀规则错误");
}
paramThreadLocal.get().setPrefix(prefix);
return this;
}
/**
* 在保存方法之前执行
* 设置文件名
* 保存inputstream流需要设置
*
* @param filename
* @return
*/
default FileService setFilename(String filename) {
paramThreadLocal.get().setFilename(filename);
return this;
}
/**
* 在保存方法之前执行
* 使用RULE_KEEP_FILENAME规则生成保存路径
* 不执行使用RULE_RANDOM_FILENAME规则生成保存路径
*
* @return
*/
default FileService setKeepFilename() {
return setKeepFilename(true);
}
/**
* 在保存方法之前执行
* 使用RULE_KEEP_FILENAME规则生成保存路径
* 不执行使用RULE_RANDOM_FILENAME规则生成保存路径
*
* @return
*/
default FileService setKeepFilename(Boolean keepFilename) {
paramThreadLocal.get().setKeepFilename(keepFilename);
return this;
}
/**
* 在保存方法之前执行
* 设置保存路径执行该方法后setKeepFilenamesetFilename,setRule将无效
*
* @param uri 保存路径
* @return
*/
default FileService setUri(String uri) {
paramThreadLocal.get().setUri(uri);
return this;
}
/**
* 在保存方法之前执行
* 使用rule规则生成保存路径
*
* @param rule
* @return
*/
default FileService setRule(String rule) {
paramThreadLocal.get().setRule(rule);
return this;
}
/**
* 在saveImage方法之前执行
* 调整图片大小
*
* @param maxWidth
* @param maxHeight
* @return
*/
default FileService setSize(int maxWidth, int maxHeight) {
paramThreadLocal.get().setMaxWidth(maxWidth);
paramThreadLocal.get().setMaxHeight(maxHeight);
return this;
}
/**
* 在saveImage方法之前执行
* 调整图片大小(默认值)
*
* @return
*/
FileService setSize();
/**
* 在saveImage方法之前执行
* 设置水印
*
* @param watermark
* @return
*/
default FileService setWatermark(BufferedImage watermark) {
paramThreadLocal.get().setWatermark(watermark);
return this;
}
/**
* 是否添加默认水印
*
* @param watermark
* @return
*/
default FileService setWatermark(Boolean watermark) {
if (watermark) {
this.setWatermark();
} else {
paramThreadLocal.get().setWatermark(null);
}
return this;
}
/**
* 在saveImage方法之前执行
* 设置默认水印
*
* @return
*/
FileService setWatermark();
/**
* 在saveImage方法之前执行
* 设置同时生成缩略图
*
* @return
*/
default FileService setThumbnail() {
return setThumbnail(true);
}
/**
* 在saveImage方法之前执行
* 设置是否同时生成缩略图
*
* @return
*/
FileService setThumbnail(boolean thumbnail);
default FileService setSaveSrc() {
return setSaveSrc(true);
}
default FileService setSaveSrc(boolean saveSrc) {
paramThreadLocal.get().setSaveSrc(saveSrc);
return this;
}
/**
* 在saveImage方法之前执行
* 设置同时生成缩略图,并指定尺寸
*
* @param width
* @param height
* @return
*/
default FileService setThumbnail(int width, int height) {
paramThreadLocal.get().setThWidth(width);
paramThreadLocal.get().setThHeight(height);
return this;
}
/**
* 保存文件
*
* @param in
* @return
*/
String save(InputStream in);
/**
* 保存文件
*
* @param file
* @return
*/
@SneakyThrows
default String save(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new RuntimeException("文件不能为空");
}
return setFilename(file.getOriginalFilename()).save(file.getInputStream());
}
/**
* 保存图片
*
* @param in
* @return
*/
String saveImage(InputStream in);
/**
* 保存图片
*
* @param image 图片
* @return
*/
default String saveImage(Image image) {
return saveImage(ImgUtil.toStream(image, "png"));
}
/**
* 保存图片
*
* @param file
* @return
*/
@SneakyThrows
default String saveImage(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new RuntimeException("文件不能为空");
}
return setFilename(file.getOriginalFilename()).saveImage(file.getInputStream());
}
/**
* 获取类型提取工具
*
* @return
*/
Tika getTika();
/**
* 获取文件类型
*
* @param in
* @return
*/
@SneakyThrows
default String getContentType(InputStream in) {
return getTika().detect(in);
}
/**
* 是否是图片文件
*
* @param in
* @return
*/
@SneakyThrows
default boolean isImage(InputStream in) {
return getContentType(in).startsWith("image/");
}
default InputStream formatImage(MultipartFile file, int maxWidth, int maxHeight,
BufferedImage watermark) {
try {
if (file == null || file.isEmpty()) {
throw new RuntimeException("上传图片不能为空");
}
if (!file.getContentType().startsWith("image/")) {
throw new RuntimeException("上传的文件不是图片");
}
return formatImage(file.getInputStream(), maxWidth, maxHeight, watermark);
} catch (IOException e) {
throw new RuntimeException("处理图片错误", e);
}
}
default InputStream formatImage(Image img, int maxWidth, int maxHeight, BufferedImage watermark) {
if (img == null) {
throw new RuntimeException("图片不能为空");
}
return formatImage(Img.from(img), maxWidth, maxHeight, watermark);
}
default InputStream formatImage(InputStream in, int maxWidth, int maxHeight, BufferedImage watermark) {
return formatImage(Img.from(in), maxWidth, maxHeight, watermark);
}
default InputStream formatImage(Img img, int maxWidth, int maxHeight, BufferedImage watermark) {
try {
int w = img.getImg().getWidth(null);
int h = img.getImg().getHeight(null);
if (maxWidth > 0 && maxHeight > 0) {
if (w > maxWidth || h > maxHeight) {
int outWidth = 0;
int outHeight = 0;
outHeight = maxWidth * h / w;
if (outHeight > maxHeight) {
outHeight = maxHeight;
outWidth = outHeight * w / h;
} else {
outWidth = maxWidth;
}
img = img.scale(outWidth, outHeight);
w = outWidth;
h = outHeight;
}
}
if (watermark != null) {
int ww = watermark.getWidth(null);
int wh = watermark.getHeight(null);
if (w > ww && h > wh) {
img = img.pressImage(watermark, 0, 0, 1f);
}
}
ByteArrayOutputStream pout = new ByteArrayOutputStream();
// img.setTargetImageType(IMAGE_WEBP).write(pout);
ImageIO.write(ImgUtil.toBufferedImage(img.getImg()), IMAGE_WEBP, pout);
ByteArrayInputStream pin = new ByteArrayInputStream(pout.toByteArray());
pout.close();
pout = null;
return pin;
} catch (Exception e) {
throw new RuntimeException("处理图片错误", e);
}
}
/**
* 生成存放的URI并保留文件名
*
* @param filename 文件名
* @param prefix 前缀
* @return
*/
default String generateURIKeepFilename(String prefix, String filename) {
return generateURI(prefix, RULE_KEEP_FILENAME, filename);
}
/**
* 生成存放的URI并保留文件名
*
* @param filename 文件名
* @return
*/
default String generateURIKeepFilename(String filename) {
return generateURI(DEFAULT_PREFIX, RULE_KEEP_FILENAME, filename);
}
/**
* 生成存放的URI并随机文件名
*
* @param filename 文件名
* @param prefix 前缀
* @return
*/
default String generateURIRandomFilename(String prefix, String filename) {
return generateURI(prefix, RULE_RANDOM_FILENAME, filename);
}
/**
* 生成存放的URI并随机文件名
*
* @param filename 文件名
* @return
*/
default String generateURIRandomFilename(String filename) {
return generateURI(DEFAULT_PREFIX, RULE_RANDOM_FILENAME, filename);
}
/**
* <pre>
* - 根据存放规则和文件名生成存放的URI
* - 支持
* - {yyyy}/{MM}/{dd}/{HH}/{mm}/{ss} 年月日时分秒
* - {UUID} 32位的唯一标志
* - {i} 自增id
* - {id} 当日int类型的唯一id,防止重复建议+年月日路径
* - {id16} 当日int类型的唯一id16进制表示,防止重复建议+年月日路径
* - {id36} 当日int类型的唯一id36进制表示,防止重复建议+年月日路径
* - {filename} 文件基础名称
* - {ext} 扩展名
* </pre>
*
* @param prefix 前缀
* @param rule 规则
* @param filename 文件名
* @return
*/
default String generateURI(String prefix, String rule, String filename) {
if (StrUtil.isBlank(prefix)) {
prefix = DEFAULT_PREFIX;
}
LocalDateTime now = LocalDateTime.now();
if (rule.contains("{yyyy}")) {
rule = rule.replace("{yyyy}", "" + now.getYear());
}
if (rule.contains("{MM}")) {
rule = rule.replace("{MM}", String.format("%02d", now.getMonthValue()));
}
if (rule.contains("{dd}")) {
rule = rule.replace("{dd}", String.format("%02d", now.getDayOfMonth()));
}
if (rule.contains("{HH}")) {
rule = rule.replace("{HH}", String.format("%02d", now.getHour()));
}
if (rule.contains("{mm}")) {
rule = rule.replace("{mm}", String.format("%02d", now.getMinute()));
}
if (rule.contains("{ss}")) {
rule = rule.replace("{ss}", String.format("%02d", now.getSecond()));
}
if (rule.contains("{UUID}")) {
rule = rule.replace("{UUID}", UUID.fastUUID().toString(true));
}
if (rule.contains("{i}")) {
rule = rule.replace("{i}", IdUtils.nextId(Id.groupName).toString());
}
if (rule.contains("{id}")) {
rule = rule.replace("{id}", Long.toString(id.nextId()));
}
if (rule.contains("{id16}")) {
rule = rule.replace("{id16}", Long.toString(id.nextId(), 16));
}
if (rule.contains("{id36}")) {
rule = rule.replace("{id36}", Long.toString(id.nextId(), 36));
}
if (rule.contains("{filename}")) {
String temp = null;
if (filename.contains(".")) {
temp = filename.substring(0, filename.lastIndexOf("."));
} else {
temp = filename;
}
rule = rule.replace("{filename}", temp);
}
if (rule.contains("{ext}")) {
String temp = null;
if (filename.contains(".")) {
temp = filename.substring(filename.lastIndexOf(".") + 1);
} else {
temp = "";
}
rule = rule.replace("{ext}", temp.toLowerCase());
}
return prefix + rule;
}
Id id = new Id();
static class Id {
private static final String groupName = "file:id";
private SymmetricCrypto crypto;
private String today = DateUtil.today();
public synchronized Long nextId() {
if (crypto == null || !today.equals(DateUtil.today())) {
today = DateUtil.today();
byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue(), today.getBytes()).getEncoded();
crypto = new SymmetricCrypto(SymmetricAlgorithm.DES, key);
}
ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES);
buffer.putInt(IdUtils.nextDayId(groupName).intValue());
return Math.abs(ByteBuffer.wrap(crypto.encrypt(buffer.array())).getLong());
}
}
@Data
public static class Param {
private String platform;
private String prefix = FileService.DEFAULT_PREFIX;
private String filename;
private boolean keepFilename = false;
private boolean saveSrc = false;
private String rule;
private String uri;
private int maxWidth = 0;
private int maxHeight = 0;
@JsonIgnore
private BufferedImage watermark;
private int thWidth = 0;
private int thHeight = 0;
private boolean md5 = false;
private boolean sha256 = false;
}
}