Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1bc8d4c
build: drop unrar from dockerfile
imnotjames Mar 22, 2026
08f6fe0
build: add libarchive to dockerfile
imnotjames Mar 22, 2026
73e5ad4
ci: add libarchive for tests
imnotjames Mar 22, 2026
581c967
build: try adding to the gradle build
imnotjames Mar 22, 2026
850fe4d
refactor: use nightcompress for reading CBX
imnotjames Mar 21, 2026
7a0c57a
test: disable tests using nightcompress when unavailable
imnotjames Mar 30, 2026
4ed3e6c
ci: drop showing java information
imnotjames Mar 30, 2026
44882c0
build: add libarchive to dev compose
imnotjames Mar 30, 2026
6381818
build: fix libarchive link in alpine
imnotjames Mar 30, 2026
21d2fe7
build: use expected order for allowing java to use native libs
imnotjames Mar 30, 2026
f9c82b7
fix: ensure we initialize NightCompress before using it
imnotjames Mar 31, 2026
700a79f
fix: properly find input stream
imnotjames Mar 31, 2026
2160950
fix: return amount of data copied
imnotjames Mar 31, 2026
b12e087
test: add helper to turn off tests in ArchiveService
imnotjames Mar 31, 2026
81e36a1
test: rewrite CbxMetadataExtractorTest to mock archiveService
imnotjames Mar 31, 2026
d7a899e
refactor: expose `Path` to make `CbxMetadataExtractor` easier to test
imnotjames Mar 31, 2026
b8a2a8a
test: drop now useless test
imnotjames Mar 31, 2026
f54c5f2
test: add archiveservice to rar5 integration
imnotjames Mar 31, 2026
69a198a
test: mock out archiveservice in cbxreader test
imnotjames Mar 31, 2026
141a122
test: use real archiveservice in cbxconverstionintegration
imnotjames Mar 31, 2026
f947545
file: the cbxconversionservicetest also seems to be integration
imnotjames Mar 31, 2026
9533626
test: remove unused imports from cbxmetadataextractortest
imnotjames Mar 31, 2026
cc6ff7d
test: cbxcomicinfocompliance is also an integration
imnotjames Mar 31, 2026
15908e1
test: cbxmetadatawriter is also integration
imnotjames Mar 31, 2026
51d946a
chore: drop unused properties
imnotjames Mar 31, 2026
6d84558
test: make cbxmetadataextractortest do something for covers
imnotjames Mar 31, 2026
8343bd1
fix: invert check
imnotjames Mar 31, 2026
5864b9b
chore: better comment
imnotjames Mar 31, 2026
055f745
test: fix more cbxmetadataextractor tests that never worked
imnotjames Mar 31, 2026
206b6f6
test: we only ever raise ioexception
imnotjames Mar 31, 2026
df91323
fix: handle `null` comicinfo when parsing
imnotjames Mar 31, 2026
bfc30b3
test: properly test for no files comicinfo
imnotjames Mar 31, 2026
56e4893
build: manual link of dev docker compose
imnotjames Mar 31, 2026
81f87d6
test: if you use any matchers, all must be
imnotjames Mar 31, 2026
273829f
build: drop unrar from dev compose
imnotjames Mar 31, 2026
54b51c9
test: if you use any matchers, all must be
imnotjames Mar 31, 2026
12c3179
test: always get a fresh stream
imnotjames Mar 31, 2026
ef588df
test: drop more toFile
imnotjames Mar 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/test-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ jobs:
restore-keys: |
gradle-${{ runner.os }}-
# Needed for NightCompress
- name: Install libarchive
run: |
sudo apt-get install libarchive-dev --yes
# Place libarchive somewhere that java looks for it.
sudo mkdir -p /usr/java/packages/lib/
sudo cp /usr/lib/x86_64-linux-gnu/libarchive.* /usr/java/packages/lib/
- name: Execute Backend Tests
id: backend_tests
run: |
Expand Down
10 changes: 5 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ RUN set -eux; \
jar_path="$(find build/libs -maxdepth 1 -name '*.jar' ! -name '*plain.jar' | head -n 1)"; \
cp "$jar_path" /workspace/booklore-api/app.jar

FROM linuxserver/unrar:7.1.10 AS unrar-layer

FROM mwader/static-ffmpeg:8.1 AS ffprobe-layer

FROM scratch AS kepubify-layer-amd64
Expand Down Expand Up @@ -70,13 +68,15 @@ ENV JAVA_TOOL_OPTIONS="-XX:+UseShenandoahGC \
-XX:InitialRAMPercentage=8.0 \
-XX:+ExitOnOutOfMemoryError"

RUN apk add --no-cache su-exec libstdc++ libgcc && \
RUN apk add --no-cache su-exec libstdc++ libgcc libarchive && \
mkdir -p /bookdrop

# Manually link `libarchive.so.13` so java and other libraries can see it
RUN ln -s /usr/lib/libarchive.so.13 /usr/lib/libarchive.so

COPY packaging/docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

COPY --from=unrar-layer /usr/bin/unrar-alpine /usr/local/bin/unrar
COPY --from=ffprobe-layer /ffprobe /usr/local/bin/ffprobe
COPY --from=kepubify-layer /kepubify /usr/local/bin/kepubify

Expand All @@ -102,4 +102,4 @@ ARG BOOKLORE_PORT=6060
EXPOSE ${BOOKLORE_PORT}

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["java", "-jar", "/app/app.jar"]
CMD ["java", "--enable-native-access=ALL-UNNAMED", "-jar", "/app/app.jar"]
6 changes: 3 additions & 3 deletions booklore-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ dependencies {
// --- Audio Metadata (Audiobook Support) ---
implementation("com.github.RouHim:jaudiotagger:2.0.19")

// --- UNRAR Support ---
implementation("com.github.junrar:junrar:7.5.8")
// --- Archive Support ---
implementation("com.github.gotson.nightcompress:nightcompress:1.1.1")

// --- JSON & Web Scraping ---
implementation("org.jsoup:jsoup:1.22.1")
Expand Down Expand Up @@ -139,7 +139,7 @@ hibernate {

tasks.named<Test>("test") {
useJUnitPlatform()
jvmArgs("-XX:+EnableDynamicAgentLoading")
jvmArgs("-XX:+EnableDynamicAgentLoading", "--enable-native-access=ALL-UNNAMED")
finalizedBy(tasks.named("jacocoTestReport"))
}

Expand Down
124 changes: 124 additions & 0 deletions booklore-api/src/main/java/org/booklore/service/ArchiveService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package org.booklore.service;

import com.github.gotson.nightcompress.Archive;
import com.github.gotson.nightcompress.ArchiveEntry;
import com.github.gotson.nightcompress.LibArchiveException;
import jakarta.annotation.PostConstruct;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;

@Slf4j
@Service
public class ArchiveService {
@PostConstruct
public void checkArchiveAvailable() {
try {
// We want to check the version early because it allows for
// NightCompress to preload the libarchive library in a safe
// thread. Loading native libraries is not a thread safe operation.
if (!Archive.isAvailable()) {
log.warn("LibArchive is not available");
}
} catch (Throwable e) {
log.error("LibArchive could not be loaded", e);
}
}

public static boolean isAvailable() {
return Archive.isAvailable();
}

public static class Entry {
private Entry() {}

@Getter
private String name;

@Getter
private long size;
}

private Entry getEntryFromArchiveEntry(ArchiveEntry archiveEntry) {
Entry entry = new Entry();

entry.name = archiveEntry.getName();
entry.size = archiveEntry.getSize();

return entry;
}

public List<Entry> getEntries(Path path) throws IOException {
return streamEntries(path).toList();
}

public Stream<Entry> streamEntries(Path path) throws IOException {
try {
return Archive.getEntries(path)
.stream()
.map(this::getEntryFromArchiveEntry);
} catch (LibArchiveException e) {
throw new IOException("Failed to read archive", e);
}
}

public List<String> getEntryNames(Path path) throws IOException {
return streamEntryNames(path).toList();
}

public Stream<String> streamEntryNames(Path path) throws IOException {
try {
return Archive.getEntries(path)
.stream()
.map(ArchiveEntry::getName);
} catch (LibArchiveException e) {
throw new IOException("Failed to read archive", e);
}
}

public long transferEntryTo(Path path, String entryName, OutputStream outputStream) throws IOException {
// We cannot directly use the NightCompress `InputStream` as it is limited
// in its implementation and will cause fatal errors. Instead, we can use
// the `transferTo` on an output stream to copy data around.
try (InputStream inputStream = Archive.getInputStream(path, entryName)) {
if (inputStream != null) {
return inputStream.transferTo(outputStream);
}
} catch (Exception e) {
throw new IOException("Failed to extract from archive: " + e.getMessage(), e);
}

throw new IOException("Entry not found in archive");
}

public byte[] getEntryBytes(Path path, String entryName) throws IOException {
try (
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
) {
transferEntryTo(path, entryName, outputStream);

return outputStream.toByteArray();
}
}

public long extractEntryToPath(Path path, String entryName, Path outputPath) throws IOException {
try (InputStream inputStream = Archive.getInputStream(path, entryName)) {
if (inputStream != null) {
return Files.copy(inputStream, outputPath);
}
} catch (Exception e) {
throw new IOException("Failed to extract from archive: " + e.getMessage(), e);
}

throw new IOException("Entry not found in archive");
}
}
Loading
Loading