add TempFileService

This commit is contained in:
Timi
2025-11-06 14:46:26 +08:00
parent 323e038e86
commit c270ae177d
9 changed files with 294 additions and 17 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
/data /data
/logs /logs
/target /target
/temp
multilingualField/ multilingualField/
!.mvn/wrapper/maven-wrapper.jar !.mvn/wrapper/maven-wrapper.jar

1
.idea/.gitignore generated vendored
View File

@ -2,3 +2,4 @@
/shelf/ /shelf/
/workspace.xml /workspace.xml
CopilotChatHistory.xml CopilotChatHistory.xml
developer-tools.xml

View File

@ -32,6 +32,8 @@ public enum SettingKey {
/** 启用灰色滤镜 */ /** 启用灰色滤镜 */
ENABLE_GRAY_FILTER, ENABLE_GRAY_FILTER,
TEMP_FILE_PATH,
// ---------- ICP 备案号 ---------- // ---------- ICP 备案号 ----------
ICP_IMYEYU_COM, ICP_IMYEYU_COM,
@ -129,6 +131,16 @@ public enum SettingKey {
MUSIC_CONTROLLER_URI, MUSIC_CONTROLLER_URI,
// ---------- ----------
JOURNAL_KEY,
JOURNAL_APP_ID,
JOURNAL_APP_SECRET,
JOURNAL_TRAVEL,
// ---------- 系统 ---------- // ---------- 系统 ----------
SYSTEM_FILE_BASE, SYSTEM_FILE_BASE,

View File

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

View File

@ -2,12 +2,6 @@ package com.imyeyu.api.modules.common.controller;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken; 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.bean.CaptchaFrom;
import com.imyeyu.api.modules.common.bean.ImageType; import com.imyeyu.api.modules.common.bean.ImageType;
import com.imyeyu.api.modules.common.bean.SettingKey; 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.FeedbackService;
import com.imyeyu.api.modules.common.service.SettingService; import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.common.service.TaskService; 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.TemplateService;
import com.imyeyu.api.modules.common.service.VersionService; import com.imyeyu.api.modules.common.service.VersionService;
import com.imyeyu.api.modules.common.vo.FeedbackRequest; 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.common.vo.attachment.AttachmentView;
import com.imyeyu.api.modules.system.util.ResourceHandler; import com.imyeyu.api.modules.system.util.ResourceHandler;
import com.imyeyu.api.util.CaptchaManager; 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.TimiSpring;
import com.imyeyu.spring.annotation.AOPLog; import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.IgnoreGlobalReturn; import com.imyeyu.spring.annotation.IgnoreGlobalReturn;
import com.imyeyu.spring.annotation.RequestRateLimit; import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.CaptchaData; import com.imyeyu.spring.bean.CaptchaData;
import com.imyeyu.spring.bean.RequestRange;
import com.mongodb.client.gridfs.GridFSBucket; import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSDownloadStream; import com.mongodb.client.gridfs.GridFSDownloadStream;
import com.mongodb.client.gridfs.model.GridFSFile; import com.mongodb.client.gridfs.model.GridFSFile;
@ -39,6 +42,7 @@ import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.tika.Tika; 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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
@ -58,8 +63,12 @@ import java.awt.Graphics2D;
import java.awt.RenderingHints; import java.awt.RenderingHints;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -83,6 +92,7 @@ public class CommonController {
private final SettingService settingService; private final SettingService settingService;
private final FeedbackService feedbackService; private final FeedbackService feedbackService;
private final TemplateService templateService; private final TemplateService templateService;
private final TempFileService tempFileService;
private final AttachmentService attachmentService; private final AttachmentService attachmentService;
private final Gson gson; private final Gson gson;
@ -171,7 +181,7 @@ public class CommonController {
@RequestRateLimit @RequestRateLimit
@PostMapping("/feedback") @PostMapping("/feedback")
public void createFeedback(@Valid @NotNull @RequestBody CaptchaData<FeedbackRequest> request) { 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()); feedbackService.create(request.getData());
} }
@ -250,12 +260,6 @@ public class CommonController {
return result.stream().collect(Collectors.toMap(Setting::getKey, Setting::getValue)); return result.stream().collect(Collectors.toMap(Setting::getKey, Setting::getValue));
} }
@RequestRateLimit
@GetMapping("/setting/flushCache")
public void settingFlushCache() {
settingService.flushCache();
}
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@GetMapping("/attachment/{mongoId}") @GetMapping("/attachment/{mongoId}")
@ -264,7 +268,6 @@ public class CommonController {
} }
@AOPLog @AOPLog
@RequestRateLimit
@IgnoreGlobalReturn @IgnoreGlobalReturn
@GetMapping("/attachment/read/{mongoId}") @GetMapping("/attachment/read/{mongoId}")
public void readAttachment( public void readAttachment(
@ -372,4 +375,72 @@ public class CommonController {
resp.setCharacterEncoding(StandardCharsets.UTF_8.toString()); 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);
}
}
} }

View File

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

View File

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

View File

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

View File

@ -1,13 +1,13 @@
package com.imyeyu.api.modules.system.controller; 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.service.AttachmentService;
import com.imyeyu.api.modules.common.vo.attachment.AttachmentRequest; import com.imyeyu.api.modules.common.vo.attachment.AttachmentRequest;
import com.imyeyu.api.modules.system.bean.ServerStatus; import com.imyeyu.api.modules.system.bean.ServerStatus;
import com.imyeyu.api.modules.system.service.SystemService; import com.imyeyu.api.modules.system.service.SystemService;
import com.imyeyu.api.modules.system.vo.TempAttachRequest; 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 com.imyeyu.spring.annotation.AOPLog;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;