Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
dd1af2c
refactor(epub): make epub extraction / creation code reusable
imnotjames Sep 14, 2026
6635d7d
build: drop kepubify from docker image
imnotjames Jul 14, 2026
91fa29a
feat: initial implementation of kepub conversion
imnotjames Jul 13, 2026
8a7e7ad
fix: handle null media type
imnotjames Jul 18, 2026
b3658f8
style: drop unused comments
imnotjames Jul 18, 2026
a980f99
fix: handle null cover image
imnotjames Jul 19, 2026
71659cc
chore: mark isIncludedResource as private
imnotjames Jul 19, 2026
d19ce94
fix: actually use temp dir
imnotjames Jul 19, 2026
e8d4edd
refactor: transform only on html
imnotjames Jul 19, 2026
ca4cd93
fix: include text nodes and comments in wrapper
imnotjames Jul 19, 2026
73d22e3
refactor: split HTML conversion from kepub conversion
imnotjames Jul 19, 2026
543a8d8
refactor: mark some items as static final
imnotjames Jul 19, 2026
fe09eec
test: add initial tests for conversion
imnotjames Jul 19, 2026
d73f474
test: add more behavior tests
imnotjames Jul 26, 2026
030779c
refactor: only construct epubwriter when needed
imnotjames Jul 26, 2026
5c64b17
fix: handle edge cases
imnotjames Jul 27, 2026
c14f289
test: add validation of img wrapping
imnotjames Jul 27, 2026
5292b50
chore: remove old comment
imnotjames Jul 27, 2026
e1c46d3
refactor: use nodefilter instead to skip children
imnotjames Jul 27, 2026
f9ee455
fix: handle edge cases in kepub HTML conversion
imnotjames Sep 13, 2026
47973bf
refactor: avoid epubreader
imnotjames Sep 14, 2026
30b879c
refactor: clean up tests too
imnotjames Sep 14, 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
23 changes: 0 additions & 23 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -50,28 +50,6 @@ RUN set -eux; \

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

FROM scratch AS kepubify-layer-amd64

ARG KEPUBIFY_VERSION="4.0.4"
ARG KEPUBIFY_AMD64_CHECKSUM="sha256:37d7628d26c5c906f607f24b36f781f306075e7073a6fe7820a751bb60431fc5"

ADD \
--checksum="${KEPUBIFY_AMD64_CHECKSUM}" \
--chmod=755 \
https://github.com/pgaskin/kepubify/releases/download/v${KEPUBIFY_VERSION}/kepubify-linux-64bit /kepubify

FROM scratch AS kepubify-layer-arm64

ARG KEPUBIFY_VERSION="4.0.4"
ARG KEPUBIFY_ARM64_CHECKSUM="sha256:5a15b8f6f6a96216c69330601bca29638cfee50f7bf48712795cff88ae2d03a3"

ADD \
--checksum="${KEPUBIFY_ARM64_CHECKSUM}" \
--chmod=755 \
https://github.com/pgaskin/kepubify/releases/download/v${KEPUBIFY_VERSION}/kepubify-linux-arm64 /kepubify

FROM kepubify-layer-${TARGETARCH} AS kepubify-layer

FROM eclipse-temurin:25-jre-alpine

ENV JAVA_TOOL_OPTIONS="-XX:+UseShenandoahGC \
Expand Down Expand Up @@ -102,7 +80,6 @@ COPY packaging/docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

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

COPY --from=backend-build /workspace/backend/app.jar /app/app.jar

Expand Down
38 changes: 36 additions & 2 deletions backend/src/main/java/org/booklore/service/ArchiveService.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
Expand Down Expand Up @@ -247,15 +249,15 @@ static final class LimitReachedException extends IOException {
}
}

public long extractEntryToPath(Path path, String entryName, Path outputPath) throws IOException {
public void extractEntryToPath(Path path, String entryName, Path outputPath) throws IOException {
ReentrantLock lock = getFileLock(path);
lock.lock();

boolean hasCreatedFile = false;
try (OutputStream outputStream = Files.newOutputStream(outputPath, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
hasCreatedFile = true;

return transferEntryTo(path, entryName, outputStream);
transferEntryTo(path, entryName, outputStream);
} catch (Exception e) {
if (hasCreatedFile) {
try {
Expand All @@ -270,4 +272,36 @@ public long extractEntryToPath(Path path, String entryName, Path outputPath) thr
lock.unlock();
}
}

public List<Path> extractToDirectory(Path path, Path outputPath) throws IOException {
return extractToDirectory(path, outputPath, null);
}

public List<Path> extractToDirectory(Path path, Path outputPath, Predicate<Entry> predicate) throws IOException {
List<Path> extractedPaths = new ArrayList<>();

for (var entry : getEntries(path)) {
if (predicate != null && !predicate.test(entry)) {
continue;
}

Path entryPath = outputPath.resolve(entry.name).normalize();
if (!entryPath.startsWith(outputPath)) {
log.warn("Archive entry outside target directory: {}", entry.name);
throw new IOException("Archive entry outside target directory");
}

if (Files.exists(entryPath)) {
log.warn("Entry already exists, skipping: {}", entry.name);
continue;
}

Files.createDirectories(entryPath.getParent());
extractEntryToPath(path, entry.name, entryPath);

extractedPaths.add(entryPath);
}

return extractedPaths;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,42 +185,28 @@ private Configuration initializeFreemarkerConfiguration() {
}

private List<Path> extractImagesFromCbx(File cbxFile, Path extractedImagesDir) throws IOException {
List<Path> imagePaths = new ArrayList<>();

for (ArchiveService.Entry entry : archiveService.getEntries(cbxFile.toPath())) {
if (!isImageFile(entry.name())) {
continue;
}

validateImageSize(entry.name(), entry.size());

try {
Path outputPath = extractedImagesDir.resolve(extractFileName(entry.name()));
var imagePaths = archiveService.extractToDirectory(
cbxFile.toPath(),
extractedImagesDir,
(entry) -> {
if (!isImageFile(entry.name())) {
return false;
}

archiveService.extractEntryToPath(cbxFile.toPath(), entry.name(), outputPath);
if (entry.size() > MAX_IMAGE_SIZE_BYTES) {
log.warn("Image too large, skipping: {} ({})", entry.name(), entry.size());
return false;
}

imagePaths.add(outputPath);
} catch (Exception e) {
log.warn("Error extracting image {}: {}", entry.name(), e.getMessage());
}
}
return true;
}
);

log.debug("Found {} image entries in CBR file", imagePaths.size());
imagePaths.sort(Comparator.comparing(path -> path.getFileName().toString().toLowerCase()));
return imagePaths;
}

private String extractFileName(String entryPath) {
return Path.of(entryPath).getFileName().toString();
}

private void validateImageSize(String imageName, long size) throws IOException {
if (size > MAX_IMAGE_SIZE_BYTES) {
throw new IOException(String.format("Image '%s' exceeds maximum size limit: %d bytes (max: %d bytes)",
imageName, size, MAX_IMAGE_SIZE_BYTES));
}
}

private boolean isImageFile(String fileName) {
if (shouldIgnoreEntry(fileName)) {
return false;
Expand Down
Loading
Loading