feat/manual-import #52
12
pom.xml
12
pom.xml
@ -131,10 +131,14 @@
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.junrar</groupId>
|
||||
<artifactId>junrar</artifactId>
|
||||
<version>7.5.8</version>
|
||||
<scope>compile</scope>
|
||||
<groupId>net.sf.sevenzipjbinding</groupId>
|
||||
<artifactId>sevenzipjbinding</artifactId>
|
||||
<version>16.02-2.01</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.sf.sevenzipjbinding</groupId>
|
||||
<artifactId>sevenzipjbinding-all-platforms</artifactId>
|
||||
<version>16.02-2.01</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@ -120,4 +120,16 @@ public class ContentController {
|
||||
@Parameter(hidden = true) @ParameterObject @PageableDefault Pageable pageable) {
|
||||
return DefaultResponseDTO.ok(contentImportService.getImportJobs(filter, pageable));
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Retry a failed import job",
|
||||
description =
|
||||
"Resets a failed import job to PENDING so it will be re-processed by the scanner.",
|
||||
tags = {"Content"},
|
||||
operationId = "retryImportJob")
|
||||
@PostMapping(value = "/import/jobs/{jobId}/retry")
|
||||
public DefaultResponseDTO<Void> retryImportJob(@PathVariable Long jobId) {
|
||||
contentImportService.retryImportJob(jobId);
|
||||
return DefaultResponseDTO.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
import com.magamochi.catalog.model.specification.MangaImportJobSpecification;
|
||||
import com.magamochi.catalog.service.MangaContentProviderService;
|
||||
import com.magamochi.catalog.service.MangaResolutionService;
|
||||
import com.magamochi.common.exception.NotFoundException;
|
||||
import com.magamochi.common.exception.UnprocessableException;
|
||||
import com.magamochi.common.model.enumeration.ContentType;
|
||||
import com.magamochi.content.model.dto.MangaImportJobDTO;
|
||||
@ -213,6 +214,28 @@ public class ContentImportService {
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void retryImportJob(Long jobId) {
|
||||
var job =
|
||||
mangaImportJobRepository
|
||||
.findById(jobId)
|
||||
.orElseThrow(() -> new NotFoundException("Import job not found: " + jobId));
|
||||
|
||||
if (job.getStatus() != ImportJobStatus.FAILED) {
|
||||
throw new UnprocessableException(
|
||||
"Only FAILED jobs can be retried. Current status: " + job.getStatus());
|
||||
}
|
||||
|
||||
if (!s3Service.objectExists(job.getS3FileKey())) {
|
||||
throw new UnprocessableException("The source file no longer exists in S3. Cannot retry.");
|
||||
}
|
||||
|
||||
job.setStatus(ImportJobStatus.PENDING);
|
||||
job.setErrorMessage(null);
|
||||
job.setErrorStacktrace(null);
|
||||
mangaImportJobRepository.save(job);
|
||||
}
|
||||
|
||||
public Optional<MangaImportJob> findImportJob(Long id) {
|
||||
return mangaImportJobRepository.findById(id);
|
||||
}
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
package com.magamochi.content.service.archive;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
import static java.util.Objects.nonNull;
|
||||
|
||||
import com.github.junrar.Archive;
|
||||
import com.github.junrar.rarfile.FileHeader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class RarExtractor implements ArchiveExtractor {
|
||||
|
||||
@Override
|
||||
public boolean supports(String mimeType) {
|
||||
if (isNull(mimeType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return mimeType.startsWith("application/x-rar-compressed")
|
||||
|| mimeType.startsWith("application/rar");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, byte[]> extract(InputStream stream) throws Exception {
|
||||
Map<String, byte[]> entryMap =
|
||||
new TreeMap<>(CaseInsensitiveSimpleNaturalComparator.getInstance());
|
||||
|
||||
var tempFile = Files.createTempFile("manga_import_rar_", ".rar");
|
||||
|
||||
try {
|
||||
Files.copy(stream, tempFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
try (var archive = new Archive(tempFile.toFile())) {
|
||||
FileHeader fileHeader;
|
||||
while (nonNull(fileHeader = archive.nextFileHeader())) {
|
||||
if (fileHeader.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var os = new ByteArrayOutputStream();
|
||||
archive.extractFile(fileHeader, os);
|
||||
entryMap.put(fileHeader.getFileName(), os.toByteArray());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Files.deleteIfExists(tempFile);
|
||||
}
|
||||
|
||||
return entryMap;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,130 @@
|
||||
package com.magamochi.content.service.archive;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
import static java.util.Objects.nonNull;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator;
|
||||
import net.sf.sevenzipjbinding.ExtractAskMode;
|
||||
import net.sf.sevenzipjbinding.ExtractOperationResult;
|
||||
import net.sf.sevenzipjbinding.IArchiveExtractCallback;
|
||||
import net.sf.sevenzipjbinding.IInArchive;
|
||||
import net.sf.sevenzipjbinding.ISequentialOutStream;
|
||||
import net.sf.sevenzipjbinding.PropID;
|
||||
import net.sf.sevenzipjbinding.SevenZip;
|
||||
import net.sf.sevenzipjbinding.SevenZipException;
|
||||
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SevenZipExtractor implements ArchiveExtractor {
|
||||
public SevenZipExtractor() {
|
||||
try {
|
||||
SevenZip.initSevenZipFromPlatformJAR();
|
||||
log.info("Successfully initialized SevenZipJBinding for multi-platform archive extraction.");
|
||||
} catch (net.sf.sevenzipjbinding.SevenZipNativeInitializationException e) {
|
||||
log.error(
|
||||
"Failed to initialize SevenZipJBinding. Native bindings may be missing or incompatible.",
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String mimeType) {
|
||||
if (isNull(mimeType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return mimeType.startsWith("application/zip")
|
||||
|| mimeType.startsWith("application/x-rar-compressed")
|
||||
|| mimeType.startsWith("application/rar")
|
||||
|| mimeType.startsWith("application/x-7z-compressed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, byte[]> extract(InputStream stream) throws Exception {
|
||||
Map<String, byte[]> entryMap =
|
||||
new TreeMap<>(CaseInsensitiveSimpleNaturalComparator.getInstance());
|
||||
var tempFile = Files.createTempFile("manga_import_archive_", ".tmp");
|
||||
|
||||
try {
|
||||
Files.copy(stream, tempFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
try (var randomAccessFile = new RandomAccessFile(tempFile.toFile(), "r");
|
||||
var inStream = new RandomAccessFileInStream(randomAccessFile);
|
||||
var inArchive = SevenZip.openInArchive(null, inStream)) {
|
||||
|
||||
inArchive.extract(null, false, new ExtractCallback(inArchive, entryMap));
|
||||
}
|
||||
} finally {
|
||||
Files.deleteIfExists(tempFile);
|
||||
}
|
||||
|
||||
return entryMap;
|
||||
}
|
||||
|
||||
private static class ExtractCallback implements IArchiveExtractCallback {
|
||||
private final IInArchive inArchive;
|
||||
private final Map<String, byte[]> entryMap;
|
||||
private int currentIndex;
|
||||
private ByteArrayOutputStream currentOs;
|
||||
|
||||
public ExtractCallback(IInArchive inArchive, Map<String, byte[]> entryMap) {
|
||||
this.inArchive = inArchive;
|
||||
this.entryMap = entryMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ISequentialOutStream getStream(int index, ExtractAskMode extractAskMode)
|
||||
throws SevenZipException {
|
||||
this.currentIndex = index;
|
||||
|
||||
if (!extractAskMode.equals(ExtractAskMode.EXTRACT)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var isFolder = (Boolean) inArchive.getProperty(index, PropID.IS_FOLDER);
|
||||
if (nonNull(isFolder) && isFolder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
currentOs = new ByteArrayOutputStream();
|
||||
return data -> {
|
||||
try {
|
||||
currentOs.write(data);
|
||||
return data.length;
|
||||
} catch (Exception e) {
|
||||
throw new SevenZipException("Error writing to stream", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareOperation(ExtractAskMode extractAskMode) {}
|
||||
|
||||
@Override
|
||||
public void setOperationResult(ExtractOperationResult extractOperationResult)
|
||||
throws SevenZipException {
|
||||
if (extractOperationResult.equals(ExtractOperationResult.OK) && nonNull(currentOs)) {
|
||||
var path = (String) inArchive.getProperty(currentIndex, PropID.PATH);
|
||||
entryMap.put(path, currentOs.toByteArray());
|
||||
}
|
||||
|
||||
currentOs = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTotal(long total) {}
|
||||
|
||||
@Override
|
||||
public void setCompleted(long complete) {}
|
||||
}
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
package com.magamochi.content.service.archive;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
import static java.util.Objects.nonNull;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ZipExtractor implements ArchiveExtractor {
|
||||
@Override
|
||||
public boolean supports(String mimeType) {
|
||||
if (isNull(mimeType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return mimeType.startsWith("application/zip");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, byte[]> extract(InputStream stream) throws Exception {
|
||||
Map<String, byte[]> entryMap =
|
||||
new TreeMap<>(CaseInsensitiveSimpleNaturalComparator.getInstance());
|
||||
|
||||
try (var zis = new ZipInputStream(stream)) {
|
||||
ZipEntry entry;
|
||||
while (nonNull(entry = zis.getNextEntry())) {
|
||||
if (entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var os = new ByteArrayOutputStream();
|
||||
zis.transferTo(os);
|
||||
entryMap.put(entry.getName(), os.toByteArray());
|
||||
zis.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
return entryMap;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user