feat: upload local music directories

This commit is contained in:
2026-07-21 19:25:20 +08:00
parent b21a5d0ed1
commit 59cf2ea67c
15 changed files with 1024 additions and 29 deletions
@@ -0,0 +1,14 @@
package com.music.common;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class Constants {
public static final Set<String> SUPPORTED_AUDIO_EXTENSIONS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
"flac", "mp3", "m4a", "aac", "ogg", "opus", "wma", "wav", "ape", "aiff", "aif", "wv", "tta"
)));
public static final String EXTENSION_LRC = "lrc";
}
@@ -11,9 +11,21 @@ import com.music.service.IngestTaskStore;
import com.music.service.ProgressStore;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.beans.factory.annotation.Value;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 一键导入控制器
@@ -28,20 +40,51 @@ public class IngestController {
private final ConfigService configService;
private final IngestTaskStore taskStore;
// Removed ALLOWED_EXTENSIONS, use com.music.common.Constants
private final Path tempDir;
/** 防止并发执行的生命周期锁,由 IngestService 在 finally 中释放 */
private final AtomicBoolean running = new AtomicBoolean(false);
private final AtomicInteger activeUploads = new AtomicInteger(0);
/** 当前/最近一次任务的 taskId,用于 GET /api/ingest/status 恢复 */
private volatile String currentTaskId;
private final Object lifecycleLock = new Object();
public IngestController(IngestService ingestService,
ProgressStore progressStore,
ConfigService configService,
IngestTaskStore taskStore) {
IngestTaskStore taskStore,
@Value("${spring.servlet.multipart.location:/tmp/mangtool-upload-tmp}") String tempLocation) {
this.ingestService = ingestService;
this.progressStore = progressStore;
this.configService = configService;
this.taskStore = taskStore;
this.tempDir = Paths.get(tempLocation);
}
@PostConstruct
public void init() {
try {
if (!Files.exists(tempDir)) {
Files.createDirectories(tempDir);
}
// Clean stale .part files
try (java.util.stream.Stream<Path> stream = Files.list(tempDir)) {
stream.filter(p -> p.toString().endsWith(".part"))
.forEach(p -> {
try {
Files.deleteIfExists(p);
} catch (IOException ignored) {}
});
}
} catch (IOException e) {
throw new RuntimeException("Failed to initialize temp directory for uploads", e);
}
}
/**
@@ -54,8 +97,14 @@ public class IngestController {
throw new BusinessException(400, "请先配置工作根目录");
}
if (!running.compareAndSet(false, true)) {
throw new BusinessException(409, "导入任务已在运行中,请等待完成");
synchronized (lifecycleLock) {
if (activeUploads.get() > 0) {
throw new BusinessException(409, "有文件正在上传,请等待完成后再试");
}
if (!running.compareAndSet(false, true)) {
throw new BusinessException(409, "导入任务已在运行中,请等待完成");
}
}
String taskId = UUID.randomUUID().toString();
@@ -190,6 +239,127 @@ public class IngestController {
throw new BusinessException(404, "未找到该任务的报告");
}
@PostMapping("/upload")
public Result<com.music.dto.UploadFileResponse> upload(@RequestParam("file") MultipartFile file,
@RequestParam("relativePath") String relativePath) {
synchronized (lifecycleLock) {
if (running.get()) {
throw new BusinessException(409, "导入任务正在运行中,无法上传文件");
}
activeUploads.incrementAndGet();
}
try {
String inputDir = configService.getInputDir();
if (inputDir == null || inputDir.trim().isEmpty()) {
throw new BusinessException(400, "请先配置工作根目录");
}
if (file.isEmpty()) {
throw new BusinessException(400, "文件为空");
}
if (relativePath == null || relativePath.trim().isEmpty()) {
throw new BusinessException(400, "相对路径不能为空");
}
if (relativePath.contains("\\")) {
throw new BusinessException(400, "路径中不能包含反斜杠");
}
// Path Security
if (relativePath.startsWith("/") || relativePath.contains(":") ||
relativePath.contains("..") || relativePath.contains("./") ||
relativePath.endsWith("/.")) {
throw new BusinessException(400, "非法的相对路径");
}
String ext = "";
int dotIdx = relativePath.lastIndexOf('.');
int slashIdx = relativePath.lastIndexOf('/');
if (dotIdx > slashIdx && dotIdx < relativePath.length() - 1) {
ext = relativePath.substring(dotIdx + 1).toLowerCase();
}
if (!com.music.common.Constants.SUPPORTED_AUDIO_EXTENSIONS.contains(ext) && !com.music.common.Constants.EXTENSION_LRC.equals(ext)) {
throw new BusinessException(400, "不支持的文件类型: " + ext);
}
String normalizedRelativePath = relativePath;
Path targetBase = Paths.get(inputDir).normalize();
Path targetPath = targetBase.resolve(normalizedRelativePath).normalize();
// 检查是否逃出 inputDir,或者发生路径遍历
if (!targetPath.startsWith(targetBase)) {
throw new BusinessException(400, "非法的相对路径,禁止目录遍历");
}
try {
Path current = targetBase;
String[] parts = relativePath.split("/");
for (String part : parts) {
if (part.isEmpty()) continue;
current = current.resolve(part);
if (Files.exists(current, java.nio.file.LinkOption.NOFOLLOW_LINKS) && Files.isSymbolicLink(current)) {
throw new BusinessException(400, "路径中不允许存在符号链接");
}
}
} catch (SecurityException e) {
throw new BusinessException(500, "安全检查失败: " + e.getMessage());
}
try {
Files.createDirectories(targetPath.getParent());
if (!Files.exists(tempDir)) {
Files.createDirectories(tempDir);
}
} catch (IOException e) {
throw new BusinessException(500, "无法创建目录: " + e.getMessage());
}
Path partFile = null;
try {
try {
if (Files.getFileStore(tempDir).equals(Files.getFileStore(targetPath.getParent()))) {
partFile = tempDir.resolve(UUID.randomUUID().toString() + ".part");
} else {
partFile = targetPath.getParent().resolve(UUID.randomUUID().toString() + ".part");
}
} catch (IOException e) {
partFile = targetPath.getParent().resolve(UUID.randomUUID().toString() + ".part");
}
if (partFile.getParent().toFile().getUsableSpace() < file.getSize()) {
throw new BusinessException(507, "磁盘空间不足");
}
file.transferTo(partFile.toFile());
synchronized (lifecycleLock) {
if (Files.exists(targetPath)) {
return Result.success(new com.music.dto.UploadFileResponse("skipped", normalizedRelativePath, targetPath.toFile().length()));
}
try {
Files.move(partFile, targetPath, StandardCopyOption.ATOMIC_MOVE);
} catch (java.nio.file.AtomicMoveNotSupportedException e) {
Files.move(partFile, targetPath);
}
}
return Result.success(new com.music.dto.UploadFileResponse("uploaded", normalizedRelativePath, targetPath.toFile().length()));
} catch (IOException e) {
throw new BusinessException(500, "文件上传失败: " + e.getMessage());
} finally {
if (partFile != null) {
try {
Files.deleteIfExists(partFile);
} catch (IOException ignored) {}
}
}
} finally {
activeUploads.decrementAndGet();
}
}
private long lastModified(java.nio.file.Path p) {
try {
return java.nio.file.Files.getLastModifiedTime(p).toMillis();
@@ -0,0 +1,40 @@
package com.music.dto;
public class UploadFileResponse {
private String status; // "uploaded" or "skipped"
private String relativePath;
private long size;
public UploadFileResponse() {
}
public UploadFileResponse(String status, String relativePath, long size) {
this.status = status;
this.relativePath = relativePath;
this.size = size;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getRelativePath() {
return relativePath;
}
public void setRelativePath(String relativePath) {
this.relativePath = relativePath;
}
public long getSize() {
return size;
}
public void setSize(long size) {
this.size = size;
}
}
@@ -23,6 +23,10 @@ public class GlobalExceptionHandler {
public Result<Void> handleValidationException(Exception ex) {
return Result.failure(400, "请求参数错误:" + ex.getMessage());
}
@ExceptionHandler(org.springframework.web.multipart.MaxUploadSizeExceededException.class)
public Result<Void> handleMaxUploadSizeExceededException(org.springframework.web.multipart.MaxUploadSizeExceededException ex) {
return Result.failure(413, "文件大小超出限制");
}
@ExceptionHandler(Exception.class)
public Result<Void> handleOther(Exception ex) {
@@ -57,19 +57,6 @@ public class IngestService {
"wav", "ape", "aiff", "aif", "wv", "tta"
));
/** 可直接保留的格式 */
private static final Set<String> COMPATIBLE_EXTENSIONS = new HashSet<>(Arrays.asList(
"flac", "mp3", "m4a", "aac", "ogg", "opus", "wma"
));
/** 所有支持的音频格式 */
private static final Set<String> ALL_AUDIO_EXTENSIONS = new HashSet<>();
static {
ALL_AUDIO_EXTENSIONS.addAll(LOSSLESS_EXTENSIONS);
ALL_AUDIO_EXTENSIONS.addAll(COMPATIBLE_EXTENSIONS);
}
/** 需要繁简转换的标签字段 */
private static final FieldKey[] TEXT_FIELDS = {
FieldKey.TITLE, FieldKey.ARTIST, FieldKey.ALBUM, FieldKey.ALBUM_ARTIST
@@ -1501,7 +1488,7 @@ public class IngestService {
private boolean isAudioFile(Path file) {
String ext = getExtension(file.getFileName().toString());
return ext != null && ALL_AUDIO_EXTENSIONS.contains(ext);
return ext != null && com.music.common.Constants.SUPPORTED_AUDIO_EXTENSIONS.contains(ext);
}
private boolean isLosslessFormat(Path file) {
@@ -9,6 +9,12 @@ spring:
serialization:
write-dates-as-timestamps: false
servlet:
multipart:
max-file-size: 4096MB
max-request-size: 4096MB
location: ${MANGTOOL_UPLOAD_TEMP:/tmp/mangtool-upload-tmp}
logging:
level:
root: INFO
@@ -0,0 +1,366 @@
package com.music;
import com.music.common.Constants;
import com.music.controller.IngestController;
import com.music.exception.BusinessException;
import com.music.dto.IngestRequest;
import com.music.service.ConfigService;
import com.music.service.IngestService;
import com.music.service.IngestTaskStore;
import com.music.service.ProgressStore;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.mock.web.MockMultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
import static org.mockito.ArgumentMatchers.any;
public class IngestUploadTest {
@Mock
private IngestService ingestService;
@Mock
private ProgressStore progressStore;
@Mock
private ConfigService configService;
@Mock
private IngestTaskStore taskStore;
private IngestController ingestController;
private Path tempDir;
private Path inputDir;
@BeforeEach
public void setUp() throws Exception {
MockitoAnnotations.openMocks(this);
tempDir = Files.createTempDirectory("upload-tmp");
inputDir = Files.createTempDirectory("music-input");
ingestController = new IngestController(ingestService, progressStore, configService, taskStore, tempDir.toString());
when(configService.getInputDir()).thenReturn(inputDir.toString());
when(configService.getBasePath()).thenReturn(inputDir.toString());
}
@AfterEach
public void tearDown() throws Exception {
if (Files.exists(tempDir)) {
try (java.util.stream.Stream<Path> s = Files.walk(tempDir)) {
s.sorted(java.util.Comparator.reverseOrder()).forEach(p -> {
try { Files.delete(p); } catch (IOException ignored) {}
});
}
}
if (Files.exists(inputDir)) {
try (java.util.stream.Stream<Path> s = Files.walk(inputDir)) {
s.sorted(java.util.Comparator.reverseOrder()).forEach(p -> {
try { Files.delete(p); } catch (IOException ignored) {}
});
}
}
}
@Test
public void testInitCleansStalePartFiles() throws Exception {
Path part1 = tempDir.resolve("test1.part");
Path part2 = tempDir.resolve("test2.txt");
Files.write(part1, "test".getBytes());
Files.write(part2, "test".getBytes());
ingestController.init();
assertFalse(Files.exists(part1), "Stale .part file should be cleaned");
assertTrue(Files.exists(part2), "Non-.part file should not be cleaned");
}
@Test
public void testUploadSuccess() throws Exception {
MockMultipartFile file = new MockMultipartFile("file", "test.mp3", "audio/mpeg", "testdata".getBytes());
com.music.common.Result<com.music.dto.UploadFileResponse> result = ingestController.upload(file, "album/test.mp3");
assertEquals(0, result.getCode());
assertEquals("uploaded", result.getData().getStatus());
assertEquals("album/test.mp3", result.getData().getRelativePath());
assertTrue(result.getData().getSize() > 0);
Path targetPath = inputDir.resolve("album/test.mp3");
assertTrue(Files.exists(targetPath));
assertEquals("testdata", new String(Files.readAllBytes(targetPath)));
}
@Test
public void testUploadExtensions() throws Exception {
for (String ext : Constants.SUPPORTED_AUDIO_EXTENSIONS) {
MockMultipartFile file = new MockMultipartFile("file", "test." + ext, "audio/mpeg", "testdata".getBytes());
com.music.common.Result<com.music.dto.UploadFileResponse> result = ingestController.upload(file, "album/test." + ext);
assertEquals(0, result.getCode());
}
MockMultipartFile file = new MockMultipartFile("file", "test.lrc", "text/plain", "testdata".getBytes());
com.music.common.Result<com.music.dto.UploadFileResponse> result = ingestController.upload(file, "album/test.lrc");
assertEquals(0, result.getCode());
}
@Test
public void testUploadSkipExisting() throws Exception {
Path targetPath = inputDir.resolve("album/test.mp3");
Files.createDirectories(targetPath.getParent());
Files.write(targetPath, "existing".getBytes());
MockMultipartFile file = new MockMultipartFile("file", "test.mp3", "audio/mpeg", "testdata".getBytes());
com.music.common.Result<com.music.dto.UploadFileResponse> result = ingestController.upload(file, "album/test.mp3");
assertEquals(0, result.getCode());
assertEquals("skipped", result.getData().getStatus());
// Assert content not overwritten
assertEquals("existing", new String(Files.readAllBytes(targetPath)));
}
@Test
public void testUploadInvalidPaths() {
MockMultipartFile file = new MockMultipartFile("file", "test.mp3", "audio/mpeg", "testdata".getBytes());
String[] badPaths = { "../test.mp3", "/etc/passwd", "C:\\test.mp3", "album\\test.mp3", "./test.mp3", "album/.", "C:/test.mp3" };
for (String path : badPaths) {
BusinessException ex = assertThrows(BusinessException.class, () -> {
ingestController.upload(file, path);
});
assertEquals(400, ex.getCode());
}
}
@Test
public void testUploadEmptyFile() {
MockMultipartFile file = new MockMultipartFile("file", "empty.mp3", "audio/mpeg", new byte[0]);
BusinessException ex = assertThrows(BusinessException.class, () -> {
ingestController.upload(file, "album/empty.mp3");
});
assertEquals(400, ex.getCode());
assertTrue(ex.getMessage().contains("文件为空"));
}
@Test
public void testUploadUnsupportedExtension() {
MockMultipartFile file = new MockMultipartFile("file", "test.exe", "application/octet-stream", "testdata".getBytes());
BusinessException ex = assertThrows(BusinessException.class, () -> {
ingestController.upload(file, "test.exe");
});
assertEquals(400, ex.getCode());
assertTrue(ex.getMessage().contains("不支持"));
}
@Test
public void testUploadSymlinkRejected() throws Exception {
Path targetDir = inputDir.resolve("album");
Files.createDirectories(targetDir);
Path realFile = tempDir.resolve("real.txt");
Files.write(realFile, "test".getBytes());
Path symlink = targetDir.resolve("link");
try {
Files.createSymbolicLink(symlink, realFile);
} catch (UnsupportedOperationException | IOException e) {
// Ignore on platforms without symlink support
return;
}
MockMultipartFile file = new MockMultipartFile("file", "test.mp3", "audio/mpeg", "testdata".getBytes());
BusinessException ex = assertThrows(BusinessException.class, () -> {
ingestController.upload(file, "album/link/test.mp3");
});
assertEquals(400, ex.getCode());
assertTrue(ex.getMessage().contains("符号链接"));
}
@Test
public void testUploadExtensionFromRelativePath() throws Exception {
MockMultipartFile file = new MockMultipartFile("file", "test.txt", "audio/mpeg", "testdata".getBytes());
com.music.common.Result<com.music.dto.UploadFileResponse> result = ingestController.upload(file, "album/test.mp3");
assertEquals(0, result.getCode());
assertEquals("uploaded", result.getData().getStatus());
}
static class BlockingMultipartFile extends MockMultipartFile {
private final CountDownLatch startLatch;
private final CountDownLatch endLatch;
public BlockingMultipartFile(String name, String originalFilename, String contentType, byte[] content, CountDownLatch startLatch, CountDownLatch endLatch) {
super(name, originalFilename, contentType, content);
this.startLatch = startLatch;
this.endLatch = endLatch;
}
@Override
public byte[] getBytes() throws IOException {
block();
return super.getBytes();
}
@Override
public java.io.InputStream getInputStream() throws IOException {
block();
return super.getInputStream();
}
@Override
public void transferTo(java.io.File dest) throws IOException, IllegalStateException {
block();
super.transferTo(dest);
}
@Override
public void transferTo(Path dest) throws IOException, IllegalStateException {
block();
super.transferTo(dest);
}
private void block() {
startLatch.countDown();
try {
endLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
@Test
public void testConcurrencyStartUpload() throws InterruptedException, Exception {
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(1);
ExecutorService executor = Executors.newFixedThreadPool(1);
BlockingMultipartFile blockingFile = new BlockingMultipartFile(
"file", "blocking.mp3", "audio/mpeg", "data".getBytes(), startLatch, endLatch);
executor.submit(() -> {
try {
ingestController.upload(blockingFile, "blocking.mp3");
} catch (Exception ignored) {
}
});
assertTrue(startLatch.await(5, java.util.concurrent.TimeUnit.SECONDS));
BusinessException ex = assertThrows(BusinessException.class, () -> {
ingestController.start(new IngestRequest());
});
assertEquals(409, ex.getCode());
endLatch.countDown();
executor.shutdown();
executor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS);
com.music.common.Result<?> res = ingestController.start(new IngestRequest());
assertEquals(0, res.getCode());
}
@Test
public void testEarlyFailureDoesNotBlockStart() throws Exception {
// missing config
when(configService.getInputDir()).thenReturn(null);
MockMultipartFile file = new MockMultipartFile("file", "test.mp3", "audio/mpeg", "testdata".getBytes());
assertThrows(BusinessException.class, () -> ingestController.upload(file, "album/test.mp3"));
when(configService.getInputDir()).thenReturn(inputDir.toString());
// empty file
MockMultipartFile emptyFile = new MockMultipartFile("file", "test.mp3", "audio/mpeg", new byte[0]);
assertThrows(BusinessException.class, () -> ingestController.upload(emptyFile, "album/test.mp3"));
// invalid path
assertThrows(BusinessException.class, () -> ingestController.upload(file, "../test.mp3"));
// symlink rejected
Path targetDir = inputDir.resolve("album");
Files.createDirectories(targetDir);
Path realFile = tempDir.resolve("real.txt");
Files.write(realFile, "test".getBytes());
Path symlink = targetDir.resolve("link");
try {
Files.createSymbolicLink(symlink, realFile);
assertThrows(BusinessException.class, () -> ingestController.upload(file, "album/link/test.mp3"));
} catch (UnsupportedOperationException | IOException ignored) {
}
// skipped existing
Path targetPath = inputDir.resolve("album/test.mp3");
Files.createDirectories(targetPath.getParent());
Files.write(targetPath, "existing".getBytes());
ingestController.upload(file, "album/test.mp3");
// Verify start is not blocked
com.music.common.Result<?> res = ingestController.start(new IngestRequest());
assertEquals(0, res.getCode());
}
@Test
public void testConcurrentUploadNoOverwrite() throws Exception {
CountDownLatch transferLatch = new CountDownLatch(2);
CountDownLatch releaseLatch = new CountDownLatch(1);
class SyncFile extends MockMultipartFile {
public SyncFile(String name, String originalFilename, String contentType, byte[] content) {
super(name, originalFilename, contentType, content);
}
@Override
public void transferTo(java.io.File dest) throws IOException, IllegalStateException {
super.transferTo(dest);
transferLatch.countDown();
try { releaseLatch.await(); } catch (InterruptedException e) {}
}
@Override
public void transferTo(Path dest) throws IOException, IllegalStateException {
super.transferTo(dest);
transferLatch.countDown();
try { releaseLatch.await(); } catch (InterruptedException e) {}
}
}
SyncFile file1 = new SyncFile("file", "test.mp3", "audio/mpeg", "content_A".getBytes());
SyncFile file2 = new SyncFile("file", "test.mp3", "audio/mpeg", "content_B".getBytes());
ExecutorService executor = Executors.newFixedThreadPool(2);
java.util.concurrent.Future<com.music.common.Result<com.music.dto.UploadFileResponse>> f1 = executor.submit(() -> ingestController.upload(file1, "concurrent/test.mp3"));
java.util.concurrent.Future<com.music.common.Result<com.music.dto.UploadFileResponse>> f2 = executor.submit(() -> ingestController.upload(file2, "concurrent/test.mp3"));
assertTrue(transferLatch.await(5, java.util.concurrent.TimeUnit.SECONDS));
releaseLatch.countDown();
com.music.common.Result<com.music.dto.UploadFileResponse> res1 = f1.get();
com.music.common.Result<com.music.dto.UploadFileResponse> res2 = f2.get();
executor.shutdown();
boolean r1Uploaded = "uploaded".equals(res1.getData().getStatus());
boolean r2Uploaded = "uploaded".equals(res2.getData().getStatus());
assertTrue(r1Uploaded ^ r2Uploaded, "Exactly one upload should succeed");
Path targetPath = inputDir.resolve("concurrent/test.mp3");
String content = new String(Files.readAllBytes(targetPath));
if (r1Uploaded) assertEquals("content_A", content);
else assertEquals("content_B", content);
}
}
@@ -114,7 +114,8 @@ class ProgressMessageMappingTest {
mock(IngestService.class),
new ProgressStore(),
cfgService,
mock(com.music.service.IngestTaskStore.class));
mock(com.music.service.IngestTaskStore.class),
"/tmp");
// 模拟配置已设置
com.music.dto.ConfigResponse cfgResp = new com.music.dto.ConfigResponse();
@@ -148,7 +149,8 @@ class ProgressMessageMappingTest {
mock(IngestService.class),
store,
cfgService,
mock(com.music.service.IngestTaskStore.class));
mock(com.music.service.IngestTaskStore.class),
"/tmp");
java.lang.reflect.Field runningField = com.music.controller.IngestController.class.getDeclaredField("running");
runningField.setAccessible(true);
@@ -169,7 +171,7 @@ class ProgressMessageMappingTest {
ProgressStore store = new ProgressStore();
com.music.controller.IngestController controller = new com.music.controller.IngestController(
mock(IngestService.class), store, cfgService,
mock(com.music.service.IngestTaskStore.class));
mock(com.music.service.IngestTaskStore.class), "/tmp");
com.music.common.Result<com.music.dto.IngestStatusResponse> result =
controller.statusById("expired-task");
@@ -203,8 +205,7 @@ class ProgressMessageMappingTest {
// 新进程的控制器:ProgressStore 为空(内存进度已丢失),currentTaskId 为 null
ProgressStore store = new ProgressStore();
com.music.controller.IngestController controller = new com.music.controller.IngestController(
mock(IngestService.class), store, cfgService, fresh);
com.music.controller.IngestController controller = new com.music.controller.IngestController(mock(IngestService.class), store, cfgService, fresh, "/tmp");
com.music.common.Result<com.music.dto.IngestStatusResponse> result = controller.statusCurrent();
com.music.dto.IngestStatusResponse resp = result.getData();