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 paramThreadLocal = ThreadLocal.withInitial(() -> new Param()); ThreadLocal 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 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; } /** * 在保存方法之前执行 * 设置保存路径,执行该方法后,setKeepFilename,setFilename,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); } /** *
   * - 根据存放规则和文件名生成存放的URI
   * - 支持
   * 	- {yyyy}/{MM}/{dd}/{HH}/{mm}/{ss} 年月日时分秒
   * 	- {UUID} 32位的唯一标志
   *  - {i} 自增id
   * 	- {id} 当日int类型的唯一id,防止重复建议+年月日路径
   * 	- {id16} 当日int类型的唯一id16进制表示,防止重复建议+年月日路径
   * 	- {id36} 当日int类型的唯一id36进制表示,防止重复建议+年月日路径
   * 	- {filename} 文件基础名称
   * 	- {ext} 扩展名
   * 
* * @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; } }