feat(attachment): 新增通用文件上传下载模块

This commit is contained in:
verto 2024-02-01 22:34:29 +08:00
parent 22bb6a47c5
commit f57d6a5078
10 changed files with 483 additions and 0 deletions

15
pom.xml
View File

@ -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>

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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>{
}

View File

@ -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);
}

View File

@ -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;
}
}