Add file upload functionality

This commit is contained in:
aditya.chandel
2024-12-16 16:50:49 -07:00
parent 7973d309de
commit 696649a24f
34 changed files with 508 additions and 141 deletions

View File

@@ -0,0 +1,24 @@
package com.adityachandel.booklore.controller;
import com.adityachandel.booklore.service.FileUploadService;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@AllArgsConstructor
@RestController
@RequestMapping("/api/v1/files")
public class FileUploadController {
private final FileUploadService fileUploadService;
@PostMapping("/upload")
public ResponseEntity<Void> uploadFile(@RequestParam("file") MultipartFile file, @RequestParam("libraryId") long libraryId, @RequestParam("filePath") String filePath) {
fileUploadService.uploadFile(file, libraryId, filePath);
return ResponseEntity.noContent().build();
}
}

View File

@@ -23,6 +23,16 @@ public class LibraryController {
private LibraryService libraryService;
private BooksService booksService;
@GetMapping("/{libraryId}")
public ResponseEntity<LibraryDTO> getLibrary(@PathVariable long libraryId) {
return ResponseEntity.ok(libraryService.getLibrary(libraryId));
}
@GetMapping
public ResponseEntity<Page<LibraryDTO>> getLibraries(@RequestParam(defaultValue = "0") @Min(0) int page, @RequestParam(defaultValue = "25") @Min(1) @Max(100) int size) {
return ResponseEntity.ok(libraryService.getLibraries(page, size));
}
@PostMapping
public ResponseEntity<LibraryDTO> createLibraryNew(@RequestBody CreateLibraryRequest request) {
return ResponseEntity.ok(libraryService.createLibrary(request));
@@ -33,10 +43,7 @@ public class LibraryController {
return libraryService.parseLibraryBooks(libraryId, force);
}
@GetMapping("/{libraryId}")
public ResponseEntity<LibraryDTO> getLibrary(@PathVariable long libraryId) {
return ResponseEntity.ok(libraryService.getLibrary(libraryId));
}
@DeleteMapping("/{libraryId}")
public ResponseEntity<?> deleteLibrary(@PathVariable long libraryId) {
@@ -44,10 +51,7 @@ public class LibraryController {
return ResponseEntity.noContent().build();
}
@GetMapping
public ResponseEntity<Page<LibraryDTO>> getLibraries(@RequestParam(defaultValue = "0") @Min(0) int page, @RequestParam(defaultValue = "25") @Min(1) @Max(100) int size) {
return ResponseEntity.ok(libraryService.getLibraries(page, size));
}
@GetMapping("/{libraryId}/book/{bookId}/withNeighbors")
public ResponseEntity<BookWithNeighborsDTO> getBookWithNeighbours(@PathVariable long libraryId, @PathVariable long bookId) {

View File

@@ -5,20 +5,24 @@ import org.springframework.http.HttpStatus;
@Getter
public enum ErrorCode {
public enum ApiError {
AUTHOR_NOT_FOUND(HttpStatus.NOT_FOUND, "Author not found with ID: %d"),
BOOK_NOT_FOUND(HttpStatus.NOT_FOUND, "Book not found with ID: %d"),
FILE_READ_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Error reading files from path"),
IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "Image not found or not readable"),
INVALID_FILE_FORMAT(HttpStatus.BAD_REQUEST, "Invalid file format"),
INVALID_FILE_FORMAT(HttpStatus.BAD_REQUEST, "Invalid file format, only pdf and epub are supported"),
LIBRARY_NOT_FOUND(HttpStatus.NOT_FOUND, "Library not found with ID: %d"),
BAD_REQUEST(HttpStatus.BAD_REQUEST, "Bad request"),
FILE_TOO_LARGE(HttpStatus.BAD_REQUEST, "File size exceeds the limit: 100 MB"),
DIRECTORY_CREATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to create the directory: %s"),
INVALID_LIBRARY_PATH(HttpStatus.BAD_REQUEST, "Invalid library path"),
FILE_ALREADY_EXISTS(HttpStatus.CONFLICT, "File already exists"),
INVALID_QUERY_PARAMETERS(HttpStatus.BAD_REQUEST, "Query parameters are required for the search.");
private final HttpStatus status;
private final String message;
ErrorCode(HttpStatus status, String message) {
ApiError(HttpStatus status, String message) {
this.status = status;
this.message = message;
}

View File

@@ -2,8 +2,10 @@ package com.adityachandel.booklore.model;
import com.adityachandel.booklore.model.entity.Library;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@Builder
@Data
@AllArgsConstructor
public class LibraryFile {

View File

@@ -4,11 +4,14 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Builder
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class LibraryDTO {
private Long id;
private String name;
private List<String> paths;
}

View File

@@ -0,0 +1,11 @@
package com.adityachandel.booklore.model.dto.request;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Data
public class FileUploadRequest {
private String libraryId;
private String filePath;
private MultipartFile file;
}

View File

@@ -0,0 +1,10 @@
package com.adityachandel.booklore.model.dto.response;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class ApiResponseMessage {
private String message;
}

View File

@@ -2,7 +2,7 @@ package com.adityachandel.booklore.service;
import com.adityachandel.booklore.model.dto.AuthorDTO;
import com.adityachandel.booklore.model.entity.Author;
import com.adityachandel.booklore.exception.ErrorCode;
import com.adityachandel.booklore.exception.ApiError;
import com.adityachandel.booklore.repository.AuthorRepository;
import com.adityachandel.booklore.repository.BookRepository;
import com.adityachandel.booklore.transformer.AuthorTransformer;
@@ -19,12 +19,12 @@ public class AuthorService {
private final BookRepository bookRepository;
public AuthorDTO getAuthorById(Long id) {
Author author = authorRepository.findById(id).orElseThrow(() -> ErrorCode.AUTHOR_NOT_FOUND.createException(id));
Author author = authorRepository.findById(id).orElseThrow(() -> ApiError.AUTHOR_NOT_FOUND.createException(id));
return AuthorTransformer.toAuthorDTO(author);
}
public List<AuthorDTO> getAuthorsByBookId(Long bookId) {
bookRepository.findById(bookId).orElseThrow(() -> ErrorCode.BOOK_NOT_FOUND.createException(bookId));
bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
List<Author> authors = authorRepository.findAuthorsByBookId(bookId);
return authors.stream().map(AuthorTransformer::toAuthorDTO).toList();
}

View File

@@ -44,23 +44,21 @@ public class BookCreatorService {
}
public void addAuthorsToBook(Set<String> authors, Book book) {
for (String authorSrt : authors) {
Optional<Author> authorOptional = authorRepository.findByName(authorSrt);
for (String authorStr : authors) {
Optional<Author> authorOptional = authorRepository.findByName(authorStr);
Author author;
if (authorOptional.isPresent()) {
author = authorOptional.get();
if(book.getMetadata().getAuthors() == null) {
book.getMetadata().setAuthors(new ArrayList<>());
}
book.getMetadata().getAuthors().add(author);
} else {
author = Author.builder()
.name(authorSrt)
.name(authorStr)
.build();
author.setBookMetadataList(new ArrayList<>());
book.getMetadata().setAuthors(new ArrayList<>());
book.getMetadata().getAuthors().add(author);
author = authorRepository.save(author);
}
if (book.getMetadata().getAuthors() == null) {
book.getMetadata().setAuthors(new ArrayList<>());
}
book.getMetadata().getAuthors().add(author);
}
}

View File

@@ -6,7 +6,7 @@ import com.adityachandel.booklore.model.dto.BookWithNeighborsDTO;
import com.adityachandel.booklore.model.dto.response.GoogleBooksMetadata;
import com.adityachandel.booklore.model.dto.BookViewerSettingDTO;
import com.adityachandel.booklore.model.dto.request.SetMetadataRequest;
import com.adityachandel.booklore.exception.ErrorCode;
import com.adityachandel.booklore.exception.ApiError;
import com.adityachandel.booklore.model.entity.*;
import com.adityachandel.booklore.repository.*;
import com.adityachandel.booklore.transformer.BookSettingTransformer;
@@ -52,7 +52,7 @@ public class BooksService {
public BookDTO getBook(long bookId) {
Book book = bookRepository.findById(bookId).orElseThrow(() -> ErrorCode.BOOK_NOT_FOUND.createException(bookId));
Book book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
return BookTransformer.convertToBookDTO(book);
}
@@ -72,7 +72,7 @@ public class BooksService {
}
public void saveBookViewerSetting(long bookId, BookViewerSettingDTO bookViewerSettingDTO) {
BookViewerSetting bookViewerSetting = bookViewerSettingRepository.findById(bookId).orElseThrow(() -> ErrorCode.BOOK_NOT_FOUND.createException(bookId));
BookViewerSetting bookViewerSetting = bookViewerSettingRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
bookViewerSetting.setPageNumber(bookViewerSettingDTO.getPageNumber());
bookViewerSetting.setZoom(bookViewerSettingDTO.getZoom());
bookViewerSetting.setSpread(bookViewerSettingDTO.getSpread());
@@ -81,7 +81,7 @@ public class BooksService {
}
public Resource getBookCover(long bookId) {
Book book = bookRepository.findById(bookId).orElseThrow(() -> ErrorCode.BOOK_NOT_FOUND.createException(bookId));
Book book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
String thumbPath = appProperties.getPathConfig() + "/thumbs/" + getFileNameWithoutExtension(book.getFileName()) + ".jpg";
Path filePath = Paths.get(thumbPath);
try {
@@ -89,15 +89,15 @@ public class BooksService {
if (resource.exists() && resource.isReadable()) {
return resource;
} else {
throw ErrorCode.IMAGE_NOT_FOUND.createException(thumbPath);
throw ApiError.IMAGE_NOT_FOUND.createException(thumbPath);
}
} catch (IOException e) {
throw ErrorCode.IMAGE_NOT_FOUND.createException(thumbPath);
throw ApiError.IMAGE_NOT_FOUND.createException(thumbPath);
}
}
public ResponseEntity<byte[]> getBookData(long bookId) throws IOException {
Book book = bookRepository.findById(bookId).orElseThrow(() -> ErrorCode.BOOK_NOT_FOUND.createException(bookId));
Book book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
byte[] pdfBytes = Files.readAllBytes(new File(book.getPath()).toPath());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, "application/pdf")
@@ -110,18 +110,18 @@ public class BooksService {
}
public BookViewerSettingDTO getBookViewerSetting(long bookId) {
BookViewerSetting bookViewerSetting = bookViewerSettingRepository.findById(bookId).orElseThrow(() -> ErrorCode.BOOK_NOT_FOUND.createException(bookId));
BookViewerSetting bookViewerSetting = bookViewerSettingRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
return BookSettingTransformer.convertToDTO(bookViewerSetting);
}
public void updateLastReadTime(long bookId) {
Book book = bookRepository.findById(bookId).orElseThrow(() -> ErrorCode.BOOK_NOT_FOUND.createException(bookId));
Book book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
book.setLastReadTime(Instant.now());
bookRepository.save(book);
}
public List<GoogleBooksMetadata> fetchProspectiveMetadataListByBookId(long bookId) {
Book book = bookRepository.findById(bookId).orElseThrow(() -> ErrorCode.BOOK_NOT_FOUND.createException(bookId));
Book book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
StringBuilder searchString = new StringBuilder();
if (!book.getMetadata().getTitle().isEmpty()) {
searchString.append(book.getMetadata().getTitle());
@@ -145,7 +145,7 @@ public class BooksService {
}
public void setMetadata(SetMetadataRequest setMetadataRequest, long bookId) {
Book book = bookRepository.findById(bookId).orElseThrow(() -> ErrorCode.BOOK_NOT_FOUND.createException(bookId));
Book book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
GoogleBooksMetadata gMetadata = googleBookMetadataService.getByGoogleBookId(setMetadataRequest.getGoogleBookId());
BookMetadata metadata = book.getMetadata();
metadata.setGoogleBookId(gMetadata.getGoogleBookId());
@@ -214,8 +214,8 @@ public class BooksService {
}
public BookWithNeighborsDTO getBookWithNeighbours(long libraryId, long bookId) {
libraryRepository.findById(libraryId).orElseThrow(() -> ErrorCode.LIBRARY_NOT_FOUND.createException(libraryId));
Book book = bookRepository.findById(bookId).orElseThrow(() -> ErrorCode.BOOK_NOT_FOUND.createException(bookId));
libraryRepository.findById(libraryId).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId));
Book book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
Book previousBook = bookRepository.findFirstByLibraryIdAndIdLessThanOrderByIdDesc(libraryId, bookId).orElse(null);
Book nextBook = bookRepository.findFirstByLibraryIdAndIdGreaterThanOrderByIdAsc(libraryId, bookId).orElse(null);
return BookWithNeighborsDTO.builder()

View File

@@ -0,0 +1,66 @@
package com.adityachandel.booklore.service;
import com.adityachandel.booklore.exception.ApiError;
import com.adityachandel.booklore.model.LibraryFile;
import com.adityachandel.booklore.model.entity.Library;
import com.adityachandel.booklore.repository.LibraryRepository;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
@AllArgsConstructor
@Service
@Slf4j
public class FileUploadService {
private final LibraryRepository libraryRepository;
private final PdfFileProcessor fileProcessor;
public void uploadFile(MultipartFile file, long libraryId, String filePath) {
Library library = libraryRepository.findById(libraryId).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId));
if (!library.getPaths().contains(filePath)) {
throw ApiError.INVALID_LIBRARY_PATH.createException();
}
String fileType = file.getContentType();
if (!"application/pdf".equals(fileType) && !"application/epub+zip".equals(fileType)) {
throw ApiError.INVALID_FILE_FORMAT.createException();
}
if (file.getSize() > 100 * 1024 * 1024) {
throw ApiError.FILE_TOO_LARGE.createException();
}
try {
Path storagePath = Paths.get(filePath, file.getOriginalFilename());
File storageFile = storagePath.toFile();
if (storageFile.exists()) {
throw ApiError.FILE_ALREADY_EXISTS.createException();
}
file.transferTo(storageFile);
LibraryFile libraryFile = LibraryFile.builder()
.library(library)
.fileType(getFileType(fileType))
.filePath(storageFile.getAbsolutePath())
.build();
fileProcessor.processFile(libraryFile, false);
log.info("File uploaded successfully: {}", storageFile.getAbsolutePath());
} catch (IOException e) {
throw ApiError.FILE_READ_ERROR.createException(e.getMessage());
}
}
private String getFileType(String f) {
if (f.equalsIgnoreCase("application/pdf")) {
return "pdf";
} else if (f.equalsIgnoreCase("application/epub+zip")) {
return "epub";
} else {
return null;
}
}
}

View File

@@ -8,7 +8,7 @@ import com.adityachandel.booklore.model.dto.*;
import com.adityachandel.booklore.model.dto.request.CreateLibraryRequest;
import com.adityachandel.booklore.model.entity.Book;
import com.adityachandel.booklore.model.entity.Library;
import com.adityachandel.booklore.exception.ErrorCode;
import com.adityachandel.booklore.exception.ApiError;
import com.adityachandel.booklore.repository.*;
import com.adityachandel.booklore.transformer.BookTransformer;
import com.adityachandel.booklore.transformer.LibraryTransformer;
@@ -45,7 +45,7 @@ public class LibraryService {
}
public LibraryDTO getLibrary(long libraryId) {
Library library = libraryRepository.findById(libraryId).orElseThrow(() -> ErrorCode.LIBRARY_NOT_FOUND.createException(libraryId));
Library library = libraryRepository.findById(libraryId).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId));
return LibraryTransformer.convertToLibraryDTO(library);
}
@@ -56,25 +56,25 @@ public class LibraryService {
}
public void deleteLibrary(long id) {
libraryRepository.findById(id).orElseThrow(() -> ErrorCode.LIBRARY_NOT_FOUND.createException(id));
libraryRepository.findById(id).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(id));
libraryRepository.deleteById(id);
}
public BookDTO getBook(long libraryId, long bookId) {
libraryRepository.findById(libraryId).orElseThrow(() -> ErrorCode.LIBRARY_NOT_FOUND.createException(libraryId));
Book book = bookRepository.findBookByIdAndLibraryId(bookId, libraryId).orElseThrow(() -> ErrorCode.BOOK_NOT_FOUND.createException(bookId));
libraryRepository.findById(libraryId).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId));
Book book = bookRepository.findBookByIdAndLibraryId(bookId, libraryId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
return BookTransformer.convertToBookDTO(book);
}
public Page<BookDTO> getBooks(long libraryId, int page, int size) {
libraryRepository.findById(libraryId).orElseThrow(() -> ErrorCode.LIBRARY_NOT_FOUND.createException(libraryId));
libraryRepository.findById(libraryId).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId));
PageRequest pageRequest = PageRequest.of(page, size);
Page<Book> bookPage = bookRepository.findBooksByLibraryId(libraryId, pageRequest);
return bookPage.map(BookTransformer::convertToBookDTO);
}
public SseEmitter parseLibraryBooks(long libraryId, boolean force) {
Library library = libraryRepository.findById(libraryId).orElseThrow(() -> ErrorCode.LIBRARY_NOT_FOUND.createException(libraryId));
Library library = libraryRepository.findById(libraryId).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId));
SseEmitter emitter = new SseEmitter();
ExecutorService sseMvcExecutor = Executors.newSingleThreadExecutor();
sseMvcExecutor.execute(() -> {

View File

@@ -10,6 +10,7 @@ public class LibraryTransformer {
return LibraryDTO.builder()
.id(library.getId())
.name(library.getName())
.paths(library.getPaths())
.build();
}
}

View File

@@ -3,6 +3,11 @@ app:
path-config: '/Users/aditya.chandel/my-library-temp/config'
spring:
servlet:
multipart:
enabled: true
max-file-size: 100MB
max-request-size: 100MB
mvc:
async:
request-timeout: 600000