mirror of
https://github.com/booklore-app/booklore.git
synced 2026-05-06 11:06:39 -04:00
Vendor unrar binaries for Alpine 3.23 compatibility (#2950)
This commit is contained in:
+5
-1
@@ -56,9 +56,13 @@ LABEL org.opencontainers.image.title="BookLore" \
|
||||
|
||||
ENV JAVA_TOOL_OPTIONS="-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+UseStringDeduplication -XX:+UseContainerSupport -XX:+UseCompactObjectHeaders -XX:MaxRAMPercentage=75.0"
|
||||
|
||||
RUN apk update && apk add --no-cache su-exec unrar && \
|
||||
ARG TARGETARCH
|
||||
RUN apk update && apk add --no-cache su-exec libstdc++ libgcc && \
|
||||
mkdir -p /bookdrop
|
||||
|
||||
COPY docker/unrar/unrar-${TARGETARCH} /usr/local/bin/unrar
|
||||
RUN chmod 755 /usr/local/bin/unrar
|
||||
|
||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
COPY --from=springboot-build /springboot-app/build/libs/booklore-api-0.0.1-SNAPSHOT.jar /app/app.jar
|
||||
|
||||
+5
-1
@@ -18,7 +18,11 @@ LABEL org.opencontainers.image.title="BookLore" \
|
||||
|
||||
ENV JAVA_TOOL_OPTIONS="-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+UseStringDeduplication -XX:+UseContainerSupport -XX:+UseCompactObjectHeaders -XX:MaxRAMPercentage=75.0"
|
||||
|
||||
RUN apk update && apk add --no-cache su-exec unrar
|
||||
ARG TARGETARCH
|
||||
RUN apk update && apk add --no-cache su-exec libstdc++ libgcc
|
||||
|
||||
COPY docker/unrar/unrar-${TARGETARCH} /usr/local/bin/unrar
|
||||
RUN chmod 755 /usr/local/bin/unrar
|
||||
|
||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
package org.booklore.service;
|
||||
|
||||
import org.booklore.model.dto.BookMetadata;
|
||||
import org.booklore.model.entity.BookEntity;
|
||||
import org.booklore.model.entity.BookMetadataEntity;
|
||||
import org.booklore.service.kobo.CbxConversionService;
|
||||
import org.booklore.service.metadata.extractor.CbxMetadataExtractor;
|
||||
import org.booklore.service.metadata.writer.CbxMetadataWriter;
|
||||
import org.booklore.service.reader.CbxReaderService;
|
||||
import org.booklore.repository.BookRepository;
|
||||
import org.booklore.service.appsettings.AppSettingService;
|
||||
import org.booklore.util.UnrarHelper;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assumptions.assumeThat;
|
||||
|
||||
/**
|
||||
* Integration tests that feed a real RAR5 archive into the service layer
|
||||
* and verify junrar fails then the unrar CLI fallback kicks in.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class Rar5FallbackIntegrationTest {
|
||||
|
||||
private static final Path RAR5_CBR = Path.of("src/test/resources/cbx/test-rar5.cbr");
|
||||
|
||||
@BeforeAll
|
||||
static void checkUnrarAvailable() {
|
||||
assumeThat(UnrarHelper.isAvailable())
|
||||
.as("unrar binary must be on PATH to run these tests")
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
// -- CbxMetadataExtractor: extractMetadata fallback --
|
||||
|
||||
@Test
|
||||
void metadataExtractor_extractsComicInfoFromRar5(@TempDir Path tempDir) throws Exception {
|
||||
Path cbrCopy = tempDir.resolve("test.cbr");
|
||||
Files.copy(RAR5_CBR, cbrCopy);
|
||||
|
||||
CbxMetadataExtractor extractor = new CbxMetadataExtractor();
|
||||
BookMetadata metadata = extractor.extractMetadata(cbrCopy.toFile());
|
||||
|
||||
assertThat(metadata.getTitle()).isEqualTo("Test RAR5 Comic");
|
||||
assertThat(metadata.getSeriesName()).isEqualTo("RAR5 Test Series");
|
||||
assertThat(metadata.getSeriesNumber()).isEqualTo(1.0f);
|
||||
assertThat(metadata.getAuthors()).contains("Test Author");
|
||||
}
|
||||
|
||||
// -- CbxReaderService: getImageEntriesFromRar + streamEntryFromRar fallback --
|
||||
|
||||
@Test
|
||||
void readerService_listsImagePagesFromRar5(@TempDir Path tempDir) throws Exception {
|
||||
Path cbrCopy = tempDir.resolve("test.cbr");
|
||||
Files.copy(RAR5_CBR, cbrCopy);
|
||||
|
||||
BookEntity book = new BookEntity();
|
||||
book.setId(99L);
|
||||
BookRepository mockRepo = org.mockito.Mockito.mock(BookRepository.class);
|
||||
org.mockito.Mockito.when(mockRepo.findById(99L)).thenReturn(java.util.Optional.of(book));
|
||||
|
||||
try (var fileUtilsStatic = org.mockito.Mockito.mockStatic(org.booklore.util.FileUtils.class)) {
|
||||
fileUtilsStatic.when(() -> org.booklore.util.FileUtils.getBookFullPath(book))
|
||||
.thenReturn(cbrCopy.toString());
|
||||
|
||||
CbxReaderService readerService = new CbxReaderService(mockRepo);
|
||||
List<Integer> pages = readerService.getAvailablePages(99L);
|
||||
|
||||
assertThat(pages).hasSize(3);
|
||||
assertThat(pages).containsExactly(1, 2, 3);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void readerService_streamsImageFromRar5(@TempDir Path tempDir) throws Exception {
|
||||
Path cbrCopy = tempDir.resolve("test.cbr");
|
||||
Files.copy(RAR5_CBR, cbrCopy);
|
||||
|
||||
BookEntity book = new BookEntity();
|
||||
book.setId(99L);
|
||||
BookRepository mockRepo = org.mockito.Mockito.mock(BookRepository.class);
|
||||
org.mockito.Mockito.when(mockRepo.findById(99L)).thenReturn(java.util.Optional.of(book));
|
||||
|
||||
try (var fileUtilsStatic = org.mockito.Mockito.mockStatic(org.booklore.util.FileUtils.class)) {
|
||||
fileUtilsStatic.when(() -> org.booklore.util.FileUtils.getBookFullPath(book))
|
||||
.thenReturn(cbrCopy.toString());
|
||||
|
||||
CbxReaderService readerService = new CbxReaderService(mockRepo);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
readerService.streamPageImage(99L, 1, out);
|
||||
|
||||
byte[] imageBytes = out.toByteArray();
|
||||
assertThat(imageBytes).hasSizeGreaterThan(0);
|
||||
assertThat(imageBytes[0]).isEqualTo((byte) 0xFF);
|
||||
assertThat(imageBytes[1]).isEqualTo((byte) 0xD8);
|
||||
}
|
||||
}
|
||||
|
||||
// -- CbxConversionService: extractImagesFromRar fallback --
|
||||
|
||||
@Test
|
||||
void conversionService_extractsImagesFromRar5(@TempDir Path tempDir) throws Exception {
|
||||
Path cbrCopy = tempDir.resolve("test.cbr");
|
||||
Files.copy(RAR5_CBR, cbrCopy);
|
||||
|
||||
BookEntity book = new BookEntity();
|
||||
book.setId(99L);
|
||||
BookMetadataEntity meta = new BookMetadataEntity();
|
||||
meta.setTitle("Test RAR5 Comic");
|
||||
book.setMetadata(meta);
|
||||
|
||||
CbxConversionService conversionService = new CbxConversionService();
|
||||
File epub = conversionService.convertCbxToEpub(cbrCopy.toFile(), tempDir.toFile(), book, 85);
|
||||
|
||||
assertThat(epub).exists();
|
||||
assertThat(epub.length()).isGreaterThan(0);
|
||||
|
||||
try (ZipFile epubZip = new ZipFile(epub)) {
|
||||
ZipEntry mimetype = epubZip.getEntry("mimetype");
|
||||
assertThat(mimetype).isNotNull();
|
||||
|
||||
List<String> imageEntries = epubZip.stream()
|
||||
.map(ZipEntry::getName)
|
||||
.filter(name -> name.startsWith("OEBPS/Images/page-"))
|
||||
.toList();
|
||||
assertThat(imageEntries).hasSize(3);
|
||||
}
|
||||
}
|
||||
|
||||
// -- CbxMetadataWriter: loadFromRar + convertRarToZipArchive fallback --
|
||||
|
||||
@Test
|
||||
void metadataWriter_convertsRar5ToCbzViaFallback(@TempDir Path tempDir) throws Exception {
|
||||
Path cbrCopy = tempDir.resolve("test.cbr");
|
||||
Files.copy(RAR5_CBR, cbrCopy);
|
||||
|
||||
AppSettingService mockSettings = org.mockito.Mockito.mock(AppSettingService.class);
|
||||
var appSettings = new org.booklore.model.dto.settings.AppSettings();
|
||||
var persistenceSettings = new org.booklore.model.dto.settings.MetadataPersistenceSettings();
|
||||
var saveToFile = new org.booklore.model.dto.settings.MetadataPersistenceSettings.SaveToOriginalFile();
|
||||
var cbxSettings = new org.booklore.model.dto.settings.MetadataPersistenceSettings.FormatSettings();
|
||||
cbxSettings.setEnabled(true);
|
||||
cbxSettings.setMaxFileSizeInMb(500);
|
||||
saveToFile.setCbx(cbxSettings);
|
||||
persistenceSettings.setSaveToOriginalFile(saveToFile);
|
||||
appSettings.setMetadataPersistenceSettings(persistenceSettings);
|
||||
org.mockito.Mockito.when(mockSettings.getAppSettings()).thenReturn(appSettings);
|
||||
|
||||
CbxMetadataWriter writer = new CbxMetadataWriter(mockSettings);
|
||||
|
||||
BookMetadataEntity metadata = new BookMetadataEntity();
|
||||
metadata.setTitle("Updated RAR5 Title");
|
||||
|
||||
writer.saveMetadataToFile(cbrCopy.toFile(), metadata, null, null);
|
||||
|
||||
Path cbzPath = tempDir.resolve("test.cbz");
|
||||
assertThat(cbzPath).exists();
|
||||
|
||||
try (ZipFile resultZip = new ZipFile(cbzPath.toFile())) {
|
||||
ZipEntry comicInfo = resultZip.getEntry("ComicInfo.xml");
|
||||
assertThat(comicInfo).isNotNull();
|
||||
|
||||
String xml = new String(resultZip.getInputStream(comicInfo).readAllBytes());
|
||||
assertThat(xml).contains("Updated RAR5 Title");
|
||||
|
||||
long imageCount = resultZip.stream()
|
||||
.map(ZipEntry::getName)
|
||||
.filter(name -> name.endsWith(".jpg"))
|
||||
.count();
|
||||
assertThat(imageCount).isEqualTo(3);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package org.booklore.util;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.assertj.core.api.Assumptions.assumeThat;
|
||||
|
||||
class UnrarHelperIntegrationTest {
|
||||
|
||||
private static final Path RAR5_CBR = Path.of("src/test/resources/cbx/test-rar5.cbr");
|
||||
|
||||
@BeforeAll
|
||||
static void checkUnrarAvailable() {
|
||||
assumeThat(UnrarHelper.isAvailable())
|
||||
.as("unrar binary must be on PATH to run these tests")
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void listEntries_returnsAllEntriesFromRar5Archive() throws IOException {
|
||||
List<String> entries = UnrarHelper.listEntries(RAR5_CBR);
|
||||
|
||||
assertThat(entries).containsExactly(
|
||||
"ComicInfo.xml",
|
||||
"page_001.jpg",
|
||||
"page_002.jpg",
|
||||
"page_003.jpg"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractEntryBytes_extractsComicInfoXml() throws IOException {
|
||||
byte[] bytes = UnrarHelper.extractEntryBytes(RAR5_CBR, "ComicInfo.xml");
|
||||
String xml = new String(bytes);
|
||||
|
||||
assertThat(xml).contains("<Title>Test RAR5 Comic</Title>");
|
||||
assertThat(xml).contains("<Series>RAR5 Test Series</Series>");
|
||||
assertThat(xml).contains("<Writer>Test Author</Writer>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractEntry_streamsImageToOutputStream() throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
UnrarHelper.extractEntry(RAR5_CBR, "page_001.jpg", out);
|
||||
|
||||
byte[] imageBytes = out.toByteArray();
|
||||
assertThat(imageBytes).hasSizeGreaterThan(0);
|
||||
assertThat(imageBytes[0]).isEqualTo((byte) 0xFF);
|
||||
assertThat(imageBytes[1]).isEqualTo((byte) 0xD8);
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractAll_extractsAllFilesToDirectory(@TempDir Path tempDir) throws IOException {
|
||||
UnrarHelper.extractAll(RAR5_CBR, tempDir);
|
||||
|
||||
assertThat(tempDir.resolve("ComicInfo.xml")).exists();
|
||||
assertThat(tempDir.resolve("page_001.jpg")).exists();
|
||||
assertThat(tempDir.resolve("page_002.jpg")).exists();
|
||||
assertThat(tempDir.resolve("page_003.jpg")).exists();
|
||||
|
||||
String xml = Files.readString(tempDir.resolve("ComicInfo.xml"));
|
||||
assertThat(xml).contains("<Title>Test RAR5 Comic</Title>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void listEntries_throwsForNonExistentFile() {
|
||||
Path bogus = Path.of("/tmp/does-not-exist.cbr");
|
||||
|
||||
assertThatThrownBy(() -> UnrarHelper.listEntries(bogus))
|
||||
.isInstanceOf(IOException.class)
|
||||
.hasMessageContaining("unrar list failed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractEntryBytes_throwsForNonExistentEntry() {
|
||||
assertThatThrownBy(() -> UnrarHelper.extractEntryBytes(RAR5_CBR, "no-such-file.jpg"))
|
||||
.isInstanceOf(IOException.class)
|
||||
.hasMessageContaining("unrar extract failed");
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user