feat(attachment): 新增通用文件上传下载模块
This commit is contained in:
parent
22bb6a47c5
commit
f57d6a5078
15
pom.xml
15
pom.xml
@ -96,7 +96,22 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.12.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>1.15</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.tika</groupId>
|
||||
<artifactId>tika-core</artifactId>
|
||||
<version>2.5.0</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
@ -0,0 +1,25 @@
|
||||
package com.zsc.edu.bill.framework.storage;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author harry_yao
|
||||
*/
|
||||
@ConfigurationProperties("storage")
|
||||
@Component
|
||||
public class StorageProperties {
|
||||
|
||||
/**
|
||||
* 附件存储路径
|
||||
*/
|
||||
@Value("${storage.attachment}")
|
||||
public String attachment;
|
||||
|
||||
/**
|
||||
* 临时文件存储路径
|
||||
*/
|
||||
@Value("${storage.temp}")
|
||||
public String temp;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package com.zsc.edu.bill.framework.storage.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
/**
|
||||
* @author harry_yao
|
||||
*/
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public class StorageException extends RuntimeException {
|
||||
|
||||
public StorageException() {
|
||||
super("文件存储出错");
|
||||
}
|
||||
|
||||
public StorageException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public StorageException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package com.zsc.edu.bill.framework.storage.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
/**
|
||||
* @author harry_yao
|
||||
*/
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public class StorageFileEmptyException extends StorageException {
|
||||
|
||||
public StorageFileEmptyException() {
|
||||
super("存储的是空文件!");
|
||||
}
|
||||
|
||||
public StorageFileEmptyException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public StorageFileEmptyException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package com.zsc.edu.bill.framework.storage.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
/**
|
||||
* @author harry_yao
|
||||
*/
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public class StorageFileNotFoundException extends StorageException {
|
||||
|
||||
public StorageFileNotFoundException() {
|
||||
super("文件不存在!");
|
||||
}
|
||||
|
||||
public StorageFileNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public StorageFileNotFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package com.zsc.edu.bill.modules.file.controller;
|
||||
|
||||
import com.zsc.edu.bill.exception.StorageException;
|
||||
import com.zsc.edu.bill.modules.file.entity.Attachment;
|
||||
import com.zsc.edu.bill.modules.file.service.AttachmentService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 附件Controller
|
||||
*
|
||||
* @author harry_yao
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("api/rest/attachment")
|
||||
public class AttachmentController {
|
||||
|
||||
private final AttachmentService service;
|
||||
|
||||
/**
|
||||
* 上传附件
|
||||
*
|
||||
* @param type 附件功能类型
|
||||
* @param file 文件
|
||||
* @return 附件信息
|
||||
*/
|
||||
@PostMapping()
|
||||
public Attachment upload(
|
||||
@RequestParam(required = false) Attachment.Type type,
|
||||
@RequestParam("file") MultipartFile file
|
||||
) {
|
||||
try {
|
||||
if (type == null) {
|
||||
type = Attachment.Type.其他;
|
||||
}
|
||||
return service.store(type, file);
|
||||
} catch (IOException e) {
|
||||
throw new StorageException("文件上传出错");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载附件
|
||||
*
|
||||
* @param id 附件ID
|
||||
* @return 附件文件内容
|
||||
*/
|
||||
@GetMapping("{id}")
|
||||
public ResponseEntity<Resource> download(
|
||||
@PathVariable("id") String id
|
||||
) {
|
||||
Attachment.Wrapper wrapper = service.loadAsWrapper(id);
|
||||
if (wrapper.attachment.filename != null) {
|
||||
ContentDisposition contentDisposition = ContentDisposition.builder("attachment").filename(wrapper.attachment.filename, StandardCharsets.UTF_8).build();
|
||||
return ResponseEntity.ok().
|
||||
header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString()).
|
||||
header(HttpHeaders.CONTENT_TYPE, wrapper.attachment.mimeType).
|
||||
body(wrapper.resource);
|
||||
}
|
||||
return ResponseEntity.ok(wrapper.resource);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
package com.zsc.edu.bill.modules.file.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 附件
|
||||
*
|
||||
* @author harry_yao
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName(value ="attach_file")
|
||||
public class Attachment implements Serializable {
|
||||
|
||||
/**
|
||||
* ID,用文件和文件名的SHA-1值生成
|
||||
*/
|
||||
@TableId
|
||||
public String id;
|
||||
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
public String filename;
|
||||
|
||||
/**
|
||||
* 附件作用类型
|
||||
*/
|
||||
public String mimeType;
|
||||
|
||||
/**
|
||||
* 附件功能类型
|
||||
*/
|
||||
// @Column(nullable = false)
|
||||
// @Enumerated(EnumType.STRING)
|
||||
// public Type type = Type.其他;
|
||||
|
||||
/**
|
||||
* 文件上传时间
|
||||
*/
|
||||
public LocalDateTime uploadTime;
|
||||
|
||||
/**
|
||||
* 文件下载链接
|
||||
*/
|
||||
@JsonSerialize
|
||||
@TableField(exist = false)
|
||||
public String url;
|
||||
|
||||
public Attachment(String id, String filename, String mimeType, Type type, LocalDateTime uploadTime) {
|
||||
this.id = id;
|
||||
this.filename = filename;
|
||||
this.mimeType = mimeType;
|
||||
// this.type = type;
|
||||
this.uploadTime = uploadTime;
|
||||
this.url = "/api/rest/attachment/" + id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
this.url = "/api/rest/attachment/" + id;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return "/api/rest/attachment/" + id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 枚举类:附件功能类型
|
||||
*/
|
||||
public enum Type {
|
||||
其他,
|
||||
头像
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
public static final class Wrapper {
|
||||
|
||||
public Attachment attachment;
|
||||
|
||||
public FileSystemResource resource;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.zsc.edu.bill.modules.file.repo;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.zsc.edu.bill.modules.file.entity.AttachFile;
|
||||
import com.zsc.edu.bill.modules.file.entity.Attachment;
|
||||
|
||||
/**
|
||||
* @author ftz
|
||||
* 创建时间:29/1/2024 上午9:55
|
||||
* 描述: TODO
|
||||
*/
|
||||
public interface AttachmentRepository extends BaseMapper<Attachment>{
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.zsc.edu.bill.modules.file.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.zsc.edu.bill.modules.file.entity.Attachment;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author fantianzhi
|
||||
* @description 针对表【attach_file(票据附件表)】的数据库操作Service
|
||||
* @createDate 2024-01-28 19:48:22
|
||||
*/
|
||||
public interface AttachmentService {
|
||||
|
||||
Attachment store(Attachment.Type type, MultipartFile file) throws IOException;
|
||||
|
||||
Attachment.Wrapper loadAsWrapper(String id);
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
package com.zsc.edu.bill.modules.file.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.zsc.edu.bill.framework.storage.StorageProperties;
|
||||
import com.zsc.edu.bill.framework.storage.exception.StorageFileEmptyException;
|
||||
import com.zsc.edu.bill.framework.storage.exception.StorageFileNotFoundException;
|
||||
import com.zsc.edu.bill.modules.file.entity.Attachment;
|
||||
import com.zsc.edu.bill.modules.file.repo.AttachmentRepository;
|
||||
import com.zsc.edu.bill.modules.file.service.AttachmentService;
|
||||
import org.apache.tika.Tika;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 附件Service
|
||||
*
|
||||
* @author harry_yao
|
||||
*/
|
||||
@Service
|
||||
public class AttachmentServiceImpl implements AttachmentService {
|
||||
|
||||
final static int[] illegalChars = {34, 60, 62, 124, 0, 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, 58, 42, 63, 92, 47};
|
||||
|
||||
private final AttachmentRepository repo;
|
||||
private final Path attachmentPath;
|
||||
private final Path tempPath;
|
||||
|
||||
public AttachmentServiceImpl(AttachmentRepository repo, StorageProperties storageProperties) {
|
||||
this.repo = repo;
|
||||
this.attachmentPath = Paths.get(storageProperties.attachment);
|
||||
this.tempPath = Paths.get(storageProperties.temp);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() throws IOException {
|
||||
if (Files.notExists(attachmentPath)) {
|
||||
Files.createDirectories(attachmentPath);
|
||||
}
|
||||
if (Files.notExists(tempPath)) {
|
||||
Files.createDirectories(tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
public Attachment store(Attachment.Type type, MultipartFile file) throws IOException {
|
||||
if (file.isEmpty()) {
|
||||
throw new StorageFileEmptyException();
|
||||
}
|
||||
MessageDigest digest = DigestUtils.getSha1Digest();
|
||||
String filename = file.getOriginalFilename();
|
||||
if (filename != null) {
|
||||
digest.update(filename.getBytes());
|
||||
}
|
||||
Path temp = tempPath.resolve(String.valueOf(System.nanoTime()));
|
||||
byte[] fileContent = file.getBytes();
|
||||
ByteArrayInputStream input = new ByteArrayInputStream(fileContent);
|
||||
Tika tika = new Tika();
|
||||
String mimeType = tika.detect(input, filename);
|
||||
OutputStream output = Files.newOutputStream(temp);
|
||||
digest.update(fileContent);
|
||||
output.write(fileContent);
|
||||
input.close();
|
||||
output.flush();
|
||||
output.close();
|
||||
String sha1 = Hex.encodeHexString(digest.digest());
|
||||
return save(temp, sha1, filename, mimeType, type);
|
||||
}
|
||||
|
||||
public Attachment store(Attachment.Type type, File file) throws IOException {
|
||||
MessageDigest digest = DigestUtils.getSha1Digest();
|
||||
String filename = file.getName();
|
||||
if (filename != null) {
|
||||
digest.update(filename.getBytes());
|
||||
}
|
||||
Tika tika = new Tika();
|
||||
String mimeType = tika.detect(file);
|
||||
String sha1 = Hex.encodeHexString(digest.digest());
|
||||
return save(file.toPath(), sha1, filename, mimeType, type);
|
||||
}
|
||||
|
||||
public Attachment store(Attachment.Type type, Path file) throws IOException {
|
||||
String filename = file.getFileName().toString();
|
||||
MessageDigest digest = DigestUtils.getSha1Digest();
|
||||
if (filename != null) {
|
||||
digest.update(filename.getBytes());
|
||||
}
|
||||
Tika tika = new Tika();
|
||||
String mimeType = tika.detect(file);
|
||||
InputStream input = Files.newInputStream(file);
|
||||
byte[] buf = new byte[8192];
|
||||
int n;
|
||||
while ((n = input.read(buf)) > 0) {
|
||||
digest.update(buf, 0, n);
|
||||
}
|
||||
String sha1 = Hex.encodeHexString(digest.digest());
|
||||
return save(file, sha1, filename, mimeType, type);
|
||||
}
|
||||
|
||||
public Resource loadAsResource(String id) {
|
||||
Path file = attachmentPath.resolve(id);
|
||||
FileSystemResource resource = new FileSystemResource(file);
|
||||
if (resource.exists() || resource.isReadable()) {
|
||||
return resource;
|
||||
} else {
|
||||
throw new StorageFileNotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
public Attachment.Wrapper loadAsWrapper(String id) {
|
||||
Path file = attachmentPath.resolve(id);
|
||||
FileSystemResource resource = new FileSystemResource(file);
|
||||
if (!resource.exists() || !resource.isReadable()) {
|
||||
throw new StorageFileNotFoundException();
|
||||
}
|
||||
Attachment attachment = repo.selectById(id); //.orElseThrow(NotExistException::new);
|
||||
return new Attachment.Wrapper(attachment, resource);
|
||||
}
|
||||
|
||||
public Attachment findById(String id) {
|
||||
return repo.selectById(id); //.orElseThrow(NotExistException::new);
|
||||
}
|
||||
|
||||
public List<Attachment> findAllById(Collection<String> ids) {
|
||||
return (ids != null && !ids.isEmpty()) ? repo.selectList(new LambdaQueryWrapper<Attachment>().in(Attachment::getId, ids)) : new ArrayList<>();
|
||||
}
|
||||
|
||||
public Path convertToTempPath(String fileName) {
|
||||
fileName = fileName.replace("/", "");
|
||||
Path path;
|
||||
try {
|
||||
path = tempPath.resolve(fileName);
|
||||
} catch (Exception e) {
|
||||
StringBuilder cleanName = new StringBuilder();
|
||||
for (int i = 0; i < fileName.length(); i++) {
|
||||
int c = fileName.charAt(i);
|
||||
if (Arrays.binarySearch(illegalChars, c) < 0) {
|
||||
cleanName.append((char) c);
|
||||
}
|
||||
}
|
||||
path = tempPath.resolve(cleanName.toString());
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private Attachment save(Path temp, String id, String filename, String mimeType, Attachment.Type type) throws IOException {
|
||||
Path dest = attachmentPath.resolve(id);
|
||||
if (Files.exists(dest)) {
|
||||
return findById(id);
|
||||
}
|
||||
Files.move(temp, dest);
|
||||
Attachment attachment = new Attachment(id, filename, mimeType, type, LocalDateTime.now());
|
||||
repo.insert(attachment);
|
||||
return attachment;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user