add TempFileService
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
||||
/data
|
||||
/logs
|
||||
/target
|
||||
/temp
|
||||
|
||||
multilingualField/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
1
.idea/.gitignore
generated
vendored
1
.idea/.gitignore
generated
vendored
@ -2,3 +2,4 @@
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
CopilotChatHistory.xml
|
||||
developer-tools.xml
|
||||
@ -32,6 +32,8 @@ public enum SettingKey {
|
||||
/** 启用灰色滤镜 */
|
||||
ENABLE_GRAY_FILTER,
|
||||
|
||||
TEMP_FILE_PATH,
|
||||
|
||||
// ---------- ICP 备案号 ----------
|
||||
|
||||
ICP_IMYEYU_COM,
|
||||
@ -129,6 +131,16 @@ public enum SettingKey {
|
||||
|
||||
MUSIC_CONTROLLER_URI,
|
||||
|
||||
// ---------- ----------
|
||||
|
||||
JOURNAL_KEY,
|
||||
|
||||
JOURNAL_APP_ID,
|
||||
|
||||
JOURNAL_APP_SECRET,
|
||||
|
||||
JOURNAL_TRAVEL,
|
||||
|
||||
// ---------- 系统 ----------
|
||||
|
||||
SYSTEM_FILE_BASE,
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
package com.imyeyu.api.modules.common.bean;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* @author 夜雨
|
||||
* @since 2025-09-27 01:47
|
||||
*/
|
||||
@Data
|
||||
public class TempFileMetaData {
|
||||
|
||||
private String id;
|
||||
|
||||
private String name;
|
||||
|
||||
private String originalName;
|
||||
|
||||
private Path path;
|
||||
|
||||
private Long lastAccessAt;
|
||||
}
|
||||
@ -2,12 +2,6 @@ package com.imyeyu.api.modules.common.controller;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.imyeyu.io.IO;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.java.ref.Ref;
|
||||
import com.imyeyu.network.Network;
|
||||
import com.imyeyu.api.bean.CaptchaFrom;
|
||||
import com.imyeyu.api.modules.common.bean.ImageType;
|
||||
import com.imyeyu.api.modules.common.bean.SettingKey;
|
||||
@ -20,17 +14,26 @@ import com.imyeyu.api.modules.common.service.AttachmentService;
|
||||
import com.imyeyu.api.modules.common.service.FeedbackService;
|
||||
import com.imyeyu.api.modules.common.service.SettingService;
|
||||
import com.imyeyu.api.modules.common.service.TaskService;
|
||||
import com.imyeyu.api.modules.common.service.TempFileService;
|
||||
import com.imyeyu.api.modules.common.service.TemplateService;
|
||||
import com.imyeyu.api.modules.common.service.VersionService;
|
||||
import com.imyeyu.api.modules.common.vo.FeedbackRequest;
|
||||
import com.imyeyu.api.modules.common.vo.TempFileResponse;
|
||||
import com.imyeyu.api.modules.common.vo.attachment.AttachmentView;
|
||||
import com.imyeyu.api.modules.system.util.ResourceHandler;
|
||||
import com.imyeyu.api.util.CaptchaManager;
|
||||
import com.imyeyu.io.IO;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.java.ref.Ref;
|
||||
import com.imyeyu.network.Network;
|
||||
import com.imyeyu.spring.TimiSpring;
|
||||
import com.imyeyu.spring.annotation.AOPLog;
|
||||
import com.imyeyu.spring.annotation.IgnoreGlobalReturn;
|
||||
import com.imyeyu.spring.annotation.RequestRateLimit;
|
||||
import com.imyeyu.spring.bean.CaptchaData;
|
||||
import com.imyeyu.spring.bean.RequestRange;
|
||||
import com.mongodb.client.gridfs.GridFSBucket;
|
||||
import com.mongodb.client.gridfs.GridFSDownloadStream;
|
||||
import com.mongodb.client.gridfs.model.GridFSFile;
|
||||
@ -39,6 +42,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Cleanup;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.tika.Tika;
|
||||
@ -51,6 +55,7 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
@ -58,8 +63,12 @@ import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -83,6 +92,7 @@ public class CommonController {
|
||||
private final SettingService settingService;
|
||||
private final FeedbackService feedbackService;
|
||||
private final TemplateService templateService;
|
||||
private final TempFileService tempFileService;
|
||||
private final AttachmentService attachmentService;
|
||||
|
||||
private final Gson gson;
|
||||
@ -171,7 +181,7 @@ public class CommonController {
|
||||
@RequestRateLimit
|
||||
@PostMapping("/feedback")
|
||||
public void createFeedback(@Valid @NotNull @RequestBody CaptchaData<FeedbackRequest> request) {
|
||||
captchaManager.test(request.getCaptcha(), request.getFrom());
|
||||
captchaManager.test(request.getCaptcha(), request.getCaptchaId());
|
||||
feedbackService.create(request.getData());
|
||||
}
|
||||
|
||||
@ -250,12 +260,6 @@ public class CommonController {
|
||||
return result.stream().collect(Collectors.toMap(Setting::getKey, Setting::getValue));
|
||||
}
|
||||
|
||||
@RequestRateLimit
|
||||
@GetMapping("/setting/flushCache")
|
||||
public void settingFlushCache() {
|
||||
settingService.flushCache();
|
||||
}
|
||||
|
||||
@AOPLog
|
||||
@RequestRateLimit
|
||||
@GetMapping("/attachment/{mongoId}")
|
||||
@ -264,7 +268,6 @@ public class CommonController {
|
||||
}
|
||||
|
||||
@AOPLog
|
||||
@RequestRateLimit
|
||||
@IgnoreGlobalReturn
|
||||
@GetMapping("/attachment/read/{mongoId}")
|
||||
public void readAttachment(
|
||||
@ -372,4 +375,72 @@ public class CommonController {
|
||||
resp.setCharacterEncoding(StandardCharsets.UTF_8.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@AOPLog
|
||||
@PostMapping("/temp/file/upload")
|
||||
public List<TempFileResponse> uploadFile(@RequestParam("file") List<MultipartFile> files) {
|
||||
return tempFileService.store(files);
|
||||
}
|
||||
|
||||
@AOPLog
|
||||
@RequestRateLimit
|
||||
@IgnoreGlobalReturn
|
||||
@GetMapping("/temp/file/read/{fileId}")
|
||||
public void tempFileRead(@PathVariable String fileId, HttpServletRequest req, HttpServletResponse resp) {
|
||||
try {
|
||||
File file = tempFileService.get(fileId);
|
||||
if (TimiJava.isEmpty(file) && file.exists()) {
|
||||
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
Path filePath = file.toPath();
|
||||
resp.setContentLengthLong(Files.size(filePath));
|
||||
String mimeType = new Tika().detect(filePath);
|
||||
if (TimiJava.isNotEmpty(mimeType)) {
|
||||
resp.setContentType(mimeType);
|
||||
}
|
||||
req.setAttribute(ResourceHandler.ATTR_TYPE, ResourceHandler.Type.FILE);
|
||||
req.setAttribute(ResourceHandler.ATTR_VALUE, filePath);
|
||||
resourceHandler.handleRequest(req, resp);
|
||||
} catch (Exception e) {
|
||||
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
@AOPLog
|
||||
@RequestRateLimit
|
||||
@IgnoreGlobalReturn
|
||||
@RequestMapping("/temp/file/download/{fileId}")
|
||||
public void tempFileDownload(@PathVariable String fileId, HttpServletResponse resp) {
|
||||
try {
|
||||
File file = tempFileService.get(fileId);
|
||||
if (TimiJava.isEmpty(file) && file.exists()) {
|
||||
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
String mimeType = new Tika().detect(file);
|
||||
resp.setContentType(mimeType);
|
||||
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(file.getName()));
|
||||
resp.setHeader("Accept-Ranges", "bytes");
|
||||
|
||||
RequestRange range = TimiSpring.requestRange(file.length());
|
||||
if (range == null) {
|
||||
// 完整文件
|
||||
resp.setContentLengthLong(file.length());
|
||||
resp.setStatus(HttpServletResponse.SC_OK);
|
||||
IO.toOutputStream(resp.getOutputStream(), file);
|
||||
} else {
|
||||
// 分片文件
|
||||
resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
resp.setHeader("Content-Range", "bytes %s-%s/%s".formatted(range.getStart(), range.getEnd(), file.length()));
|
||||
resp.setContentLengthLong(range.getLength());
|
||||
|
||||
@Cleanup RandomAccessFile raf = new RandomAccessFile(file, "r");
|
||||
raf.seek(range.getStart());
|
||||
IO.toOutputStream(resp.getOutputStream(), raf, range.getStart(), range.getLength());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("download error", e);
|
||||
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
package com.imyeyu.api.modules.common.service;
|
||||
|
||||
import com.imyeyu.api.modules.common.bean.TempFileMetaData;
|
||||
import com.imyeyu.api.modules.common.vo.TempFileResponse;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author 夜雨
|
||||
* @since 2025-09-27 01:35
|
||||
*/
|
||||
public interface TempFileService {
|
||||
|
||||
List<TempFileResponse> store(List<MultipartFile> files) throws TimiException;
|
||||
|
||||
File get(String id) throws TimiException;
|
||||
|
||||
TempFileMetaData metadata(String id) throws TimiException;
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
package com.imyeyu.api.modules.common.service.implement;
|
||||
|
||||
import com.imyeyu.api.modules.common.bean.SettingKey;
|
||||
import com.imyeyu.api.modules.common.bean.TempFileMetaData;
|
||||
import com.imyeyu.api.modules.common.service.AttachmentService;
|
||||
import com.imyeyu.api.modules.common.service.SettingService;
|
||||
import com.imyeyu.api.modules.common.service.TempFileService;
|
||||
import com.imyeyu.api.modules.common.vo.TempFileResponse;
|
||||
import com.imyeyu.io.IO;
|
||||
import com.imyeyu.io.IOSize;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.network.Network;
|
||||
import com.imyeyu.spring.TimiSpring;
|
||||
import com.imyeyu.utils.Time;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* @author 夜雨
|
||||
* @since 2025-09-27 01:47
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TempFileServiceImplement implements TempFileService {
|
||||
|
||||
private static final Long TTL = Time.H * 6;
|
||||
private static final Long LIMIT = IOSize.GB * 10;
|
||||
|
||||
private final SettingService settingService;
|
||||
private final AttachmentService attachmentService;
|
||||
|
||||
private Path storagePath;
|
||||
|
||||
private final Map<String, TempFileMetaData> metadataMap = new ConcurrentHashMap<>();
|
||||
private final Map<String, Long> usageMap = new ConcurrentHashMap<>();
|
||||
|
||||
@PostConstruct
|
||||
public void init() throws IOException {
|
||||
storagePath = Paths.get(settingService.getAsString(SettingKey.TEMP_FILE_PATH));
|
||||
IO.destroy(storagePath.toFile());
|
||||
Files.createDirectories(storagePath);
|
||||
}
|
||||
|
||||
public List<TempFileResponse> store(List<MultipartFile> files) throws TimiException {
|
||||
long newFileSize = files.stream().mapToLong(MultipartFile::getSize).sum();
|
||||
long currentUsage = usageMap.getOrDefault(TimiSpring.getRequestIP(), 0L);
|
||||
TimiException.requiredTrue(currentUsage + newFileSize < LIMIT, "out of storage limit(10 GB)");
|
||||
|
||||
try {
|
||||
List<TempFileResponse> result = new ArrayList<>();
|
||||
for (int i = 0; i < files.size(); i++) {
|
||||
MultipartFile file = files.get(i);
|
||||
|
||||
String fileId = UUID.randomUUID().toString();
|
||||
String fileName = fileId;
|
||||
if (TimiJava.isNotEmpty(file.getOriginalFilename())) {
|
||||
fileName = fileId + "." + Network.uriFileExtension(file.getOriginalFilename());
|
||||
}
|
||||
Path filePath = storagePath.resolve(fileName);
|
||||
Files.copy(file.getInputStream(), filePath);
|
||||
|
||||
// 创建元数据
|
||||
TempFileMetaData metadata = new TempFileMetaData();
|
||||
metadata.setId(fileId);
|
||||
metadata.setPath(filePath);
|
||||
metadata.setName(fileName);
|
||||
metadata.setOriginalName(file.getOriginalFilename());
|
||||
metadata.setLastAccessAt(Time.now());
|
||||
metadataMap.put(metadata.getId(), metadata);
|
||||
|
||||
TempFileResponse resp = new TempFileResponse();
|
||||
resp.setId(metadata.getId());
|
||||
resp.setExpireAt(metadata.getLastAccessAt() + TTL);
|
||||
|
||||
usageMap.put(TimiSpring.getRequestIP(), currentUsage + newFileSize);
|
||||
result.add(resp);
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("store temp file error", e);
|
||||
throw new TimiException(TimiCode.ERROR, "store temp file error", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public File get(String id) throws TimiException {
|
||||
TempFileMetaData metaData = metadataMap.get(id);
|
||||
TimiException.required(metaData, "not found temp file");
|
||||
metaData.setLastAccessAt(Time.now());
|
||||
return metaData.getPath().toFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TempFileMetaData metadata(String id) throws TimiException {
|
||||
return metadataMap.get(id);
|
||||
}
|
||||
|
||||
@Scheduled(fixedRate = 3600000)
|
||||
public void cleanup() {
|
||||
List<String> expiredIds = metadataMap.values()
|
||||
.stream()
|
||||
.filter(metadata -> metadata.getLastAccessAt() + TTL < Time.now())
|
||||
.map(TempFileMetaData::getId)
|
||||
.toList();
|
||||
for (int i = 0; i < expiredIds.size(); i++) {
|
||||
TempFileMetaData removed = metadataMap.remove(expiredIds.get(i));
|
||||
if (TimiJava.isNotEmpty(removed)) {
|
||||
File file = removed.getPath().toFile();
|
||||
IO.destroy(file);
|
||||
usageMap.computeIfPresent(TimiSpring.getRequestIP(), (ip, usage) -> usage - file.length());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package com.imyeyu.api.modules.common.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author 夜雨
|
||||
* @since 2025-09-27 01:37
|
||||
*/
|
||||
@Data
|
||||
public class TempFileResponse {
|
||||
|
||||
private String id;
|
||||
|
||||
private Long expireAt;
|
||||
}
|
||||
@ -1,13 +1,13 @@
|
||||
package com.imyeyu.api.modules.system.controller;
|
||||
|
||||
import com.imyeyu.io.IO;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.api.modules.common.service.AttachmentService;
|
||||
import com.imyeyu.api.modules.common.vo.attachment.AttachmentRequest;
|
||||
import com.imyeyu.api.modules.system.bean.ServerStatus;
|
||||
import com.imyeyu.api.modules.system.service.SystemService;
|
||||
import com.imyeyu.api.modules.system.vo.TempAttachRequest;
|
||||
import com.imyeyu.io.IO;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.spring.annotation.AOPLog;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
Reference in New Issue
Block a user