mirror of
https://github.com/booklore-app/booklore.git
synced 2026-02-18 00:17:53 +01:00
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:
@@ -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 -> {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user