fix(file-move): implement transaction management for file moves and rollback on failure (#2592)

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
This commit is contained in:
Balázs Szücs
2026-02-06 21:11:10 +01:00
committed by GitHub
parent 7124578831
commit cd428c6fe3
3 changed files with 68 additions and 35 deletions

View File

@@ -1,5 +1,8 @@
package org.booklore.service.opds;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.booklore.config.security.service.AuthenticationService;
import org.booklore.config.security.userdetails.OpdsUserDetails;
import org.booklore.model.dto.Book;
@@ -8,9 +11,6 @@ import org.booklore.model.dto.Library;
import org.booklore.model.enums.OpdsSortOrder;
import org.booklore.service.MagicShelfService;
import org.booklore.util.ArchiveUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
@@ -658,30 +658,35 @@ public class OpdsFeedService {
case AZW3 -> "application/vnd.amazon.ebook";
case CBX -> {
if (bookFile.getArchiveType() != null) {
yield switch (bookFile.getArchiveType()) {
case RAR -> "application/vnd.comicbook-rar";
case ZIP -> "application/vnd.comicbook+zip";
case SEVEN_ZIP -> "application/x-7z-compressed";
default -> "application/vnd.comicbook+zip";
};
if (bookFile.getArchiveType() == ArchiveUtils.ArchiveType.RAR) {
yield "application/vnd.comicbook-rar";
}
if (bookFile.getArchiveType() == ArchiveUtils.ArchiveType.ZIP) {
yield "application/vnd.comicbook+zip";
}
if (bookFile.getArchiveType() == ArchiveUtils.ArchiveType.SEVEN_ZIP) {
yield "application/x-7z-compressed";
}
}
if (hasValidFilePath(bookFile)) {
ArchiveUtils.ArchiveType type = ArchiveUtils.detectArchiveType(new File(bookFile.getFilePath()));
yield switch (type) {
case RAR -> "application/vnd.comicbook-rar";
case ZIP -> "application/vnd.comicbook+zip";
case SEVEN_ZIP -> "application/x-7z-compressed";
default -> {
String lower = bookFile.getFileName().toLowerCase();
if (lower.endsWith(".cbr")) yield "application/vnd.comicbook-rar";
if (lower.endsWith(".cbz")) yield "application/vnd.comicbook+zip";
if (lower.endsWith(".cb7")) yield "application/x-7z-compressed";
if (lower.endsWith(".cbt")) yield "application/x-tar";
yield "application/vnd.comicbook+zip";
}
};
// We only trust detection if it found something definite (not UNKNOWN)
if (type != ArchiveUtils.ArchiveType.UNKNOWN) {
yield switch (type) {
case RAR -> "application/vnd.comicbook-rar";
case ZIP -> "application/vnd.comicbook+zip";
case SEVEN_ZIP -> "application/x-7z-compressed";
default -> "application/vnd.comicbook+zip"; // Should not happen given the if check
};
}
}
String lower = bookFile.getFileName().toLowerCase();
if (lower.endsWith(".cbr")) yield "application/vnd.comicbook-rar";
if (lower.endsWith(".cbz")) yield "application/vnd.comicbook+zip";
if (lower.endsWith(".cb7")) yield "application/x-7z-compressed";
if (lower.endsWith(".cbt")) yield "application/x-tar";
yield "application/vnd.comicbook+zip";
}
case AUDIOBOOK -> {

View File

@@ -3,11 +3,7 @@ package org.booklore.util;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.*;
@Slf4j
@UtilityClass
@@ -21,7 +17,11 @@ public class ArchiveUtils {
}
private static final byte[] ZIP_MAGIC = {0x50, 0x4B, 0x03, 0x04};
private static final byte[] RAR_MAGIC = {0x52, 0x61, 0x72, 0x21, 0x1A, 0x07};
// RAR 5.0 signature: 0x52 0x61 0x72 0x21 0x1A 0x07 0x01 0x00
private static final byte[] RAR_MAGIC_V5 = {0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00};
// RAR 4.x signature: 0x52 0x61 0x72 0x21 0x1A 0x07 0x00
private static final byte[] RAR_MAGIC_V4 = {0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00};
// Generic RAR signature (first 4 bytes): 0x52 0x61 0x72 0x21
private static final byte[] SEVEN_ZIP_MAGIC = {0x37, 0x7A, (byte) 0xBC, (byte) 0xAF, 0x27, 0x1C};
public static ArchiveType detectArchiveType(File file) {
@@ -39,7 +39,10 @@ public class ArchiveUtils {
if (startsWith(buffer, ZIP_MAGIC)) {
return ArchiveType.ZIP;
}
if (startsWith(buffer, RAR_MAGIC)) {
if (startsWith(buffer, RAR_MAGIC_V5)) {
return ArchiveType.RAR;
}
if (startsWith(buffer, RAR_MAGIC_V4)) {
return ArchiveType.RAR;
}
if (startsWith(buffer, SEVEN_ZIP_MAGIC)) {

View File

@@ -1,17 +1,13 @@
package org.booklore.service.opds;
import jakarta.servlet.http.HttpServletRequest;
import org.booklore.config.security.service.AuthenticationService;
import org.booklore.config.security.userdetails.OpdsUserDetails;
import org.booklore.model.dto.Book;
import org.booklore.model.dto.BookFile;
import org.booklore.model.dto.BookMetadata;
import org.booklore.model.dto.Library;
import org.booklore.model.dto.OpdsUserV2;
import org.booklore.model.dto.*;
import org.booklore.model.entity.ShelfEntity;
import org.booklore.model.enums.BookFileType;
import org.booklore.model.enums.OpdsSortOrder;
import org.booklore.service.MagicShelfService;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Page;
@@ -476,4 +472,33 @@ class OpdsFeedServiceTest {
assertThat(xml).contains("</feed>");
verify(opdsBookService).getBooksPage(TEST_USER_ID, "fantasy", null, Set.of(10L), 0, 50);
}
@Test
void fileMimeType_shouldReturnCorrectMimeTypeForCbz() throws Exception {
var method = OpdsFeedService.class.getDeclaredMethod("fileMimeType", BookFile.class);
method.setAccessible(true);
BookFile bookFile = BookFile.builder()
.bookType(BookFileType.CBX)
.fileName("comic.cbz")
.archiveType(org.booklore.util.ArchiveUtils.ArchiveType.UNKNOWN)
.build();
String mimeType = (String) method.invoke(opdsFeedService, bookFile);
assertThat(mimeType).isEqualTo("application/vnd.comicbook+zip");
}
@Test
void fileMimeType_shouldReturnCorrectMimeTypeForCbr() throws Exception {
var method = OpdsFeedService.class.getDeclaredMethod("fileMimeType", BookFile.class);
method.setAccessible(true);
BookFile bookFile = BookFile.builder()
.bookType(BookFileType.CBX)
.fileName("comic.cbr")
.archiveType(org.booklore.util.ArchiveUtils.ArchiveType.UNKNOWN)
.build();
String mimeType = (String) method.invoke(opdsFeedService, bookFile);
assertThat(mimeType).isEqualTo("application/vnd.comicbook-rar");
}
}