Images are working but still there is need to add isImageMain flag to images.

This commit is contained in:
2025-04-28 14:17:38 +02:00
parent 6b5dded7f8
commit 7f8f13b115
12 changed files with 251 additions and 174 deletions

View File

@@ -0,0 +1,35 @@
package _11.asktpk.artisanconnectbackend.config;
import _11.asktpk.artisanconnectbackend.dto.RequestResponseDTO;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
@Controller
public class CustomErrorController implements ErrorController {
@RequestMapping("/error")
public ResponseEntity<RequestResponseDTO> handleError(HttpServletRequest request) {
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if (status != null) {
int statusCode = Integer.parseInt(status.toString());
if (statusCode == HttpStatus.NOT_FOUND.value()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new RequestResponseDTO("Nie znaleziono zasobu. Sprawdź URL i spróbuj ponownie."));
} else if (statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new RequestResponseDTO("Wystąpił wewnętrzny błąd serwera. Spróbuj ponownie później."));
}
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new RequestResponseDTO("Wystąpił nieoczekiwany błąd."));
}
}

View File

@@ -0,0 +1,89 @@
package _11.asktpk.artisanconnectbackend.controller;
import _11.asktpk.artisanconnectbackend.dto.RequestResponseDTO;
import _11.asktpk.artisanconnectbackend.service.ImageService;
import _11.asktpk.artisanconnectbackend.service.NoticeService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.*;
@RestController
@RequestMapping("/api/v1/images")
public class ImageController {
private final ImageService imageService;
private final NoticeService noticeService;
ImageController(ImageService imageService, NoticeService noticeService) {
this.imageService = imageService;
this.noticeService = noticeService;
}
@Value("${file.upload-dir}")
private String uploadDir;
@PostMapping("/upload/{id}")
public ResponseEntity<RequestResponseDTO> uploadImage(@RequestParam("file") MultipartFile file, @PathVariable("id") Long noticeId) {
try {
if(file.isEmpty()) {
return ResponseEntity.badRequest().body(new RequestResponseDTO("File is empty"));
}
if(!Objects.equals(file.getContentType(), "image/jpeg") && !Objects.equals(file.getContentType(), "image/png")) {
return ResponseEntity.badRequest().body(new RequestResponseDTO("File must be a JPEG or PNG image."));
}
if(noticeId == null || !noticeService.noticeExists(noticeId)) {
return ResponseEntity.badRequest().body(new RequestResponseDTO("Notice ID is invalid or does not exist."));
}
String newImageName = imageService.saveImageToStorage(uploadDir, file);
imageService.addImageUrlToDB(newImageName, noticeId);
return ResponseEntity.ok(new RequestResponseDTO("Image uploaded successfully with new name: " + newImageName));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE).body(new RequestResponseDTO(e.getMessage()));
}
}
@GetMapping("/get/{filename}")
public ResponseEntity<Resource> getImage(@PathVariable String filename) {
try {
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_JPEG)
.body(imageService.getImage(uploadDir, filename));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
}
@GetMapping("/list/{id}")
public ResponseEntity<List<String>> getImagesNamesList(@PathVariable("id") Long noticeId) {
if(noticeId == null) {
return ResponseEntity.badRequest().body(Collections.singletonList("Notice ID is invalid or does not exist."));
}
List<String> result;
try {
result = imageService.getImagesList(noticeId);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Collections.singletonList(e.getMessage()));
}
return ResponseEntity.ok(result);
}
@DeleteMapping("/delete/{id}")
public ResponseEntity deleteImage(@PathVariable("id") String filename) {
try {
imageService.deleteImage(uploadDir, filename);
return new ResponseEntity<>(HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

View File

@@ -1,20 +1,13 @@
package _11.asktpk.artisanconnectbackend.controller;
import _11.asktpk.artisanconnectbackend.service.ClientService;
import _11.asktpk.artisanconnectbackend.service.ImageService;
import _11.asktpk.artisanconnectbackend.service.NoticeService;
import _11.asktpk.artisanconnectbackend.dto.NoticeDTO;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
@@ -23,12 +16,10 @@ import java.util.List;
public class NoticeController {
private final NoticeService noticeService;
private final ClientService clientService;
private final ImageService imageService;
public NoticeController(NoticeService noticeService, ClientService clientService, ImageService imageService) {
public NoticeController(NoticeService noticeService, ClientService clientService) {
this.noticeService = noticeService;
this.clientService = clientService;
this.imageService = imageService;
}
@GetMapping("/get/all")
@@ -53,6 +44,10 @@ public class NoticeController {
.body("Nie znaleziono klienta o ID: " + dto.getClientId());
}
if (dto.getCategory() == null) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Nie ma takiej kategorii");
}
dto.setPublishDate(java.time.LocalDateTime.now());
noticeService.addNotice(dto);
@@ -112,76 +107,4 @@ public class NoticeController {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
@PostMapping("/upload/{id}")
public ResponseEntity<String> uploadImage(@PathVariable("id") Long id, @RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Nie przesłano pliku.");
}
if (!noticeService.noticeExists(id)) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Nie znaleziono ogłoszenia o ID: " + id);
}
try {
String filePath = noticeService.saveImage("./app/images/notices/", id, file);
return ResponseEntity.ok("Zdjęcie zapisane pod ścieżką: " + filePath);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Błąd podczas zapisywania zdjęcia: " + e.getMessage());
}
}
@GetMapping("/images/{noticeId}/{imageIndex}")
public ResponseEntity<byte[]> getImage(@PathVariable Long noticeId, @PathVariable Integer imageIndex) {
try {
if (!noticeService.noticeExists(noticeId)) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
NoticeDTO notice = noticeService.getNoticeById(noticeId);
List<String> imagePaths = notice.getImages();
if (imagePaths == null || imagePaths.isEmpty() || imageIndex >= imagePaths.size() || imageIndex < 0) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
String imagePath = imagePaths.get(imageIndex);
byte[] imageBytes = imageService.getImageBytes(imagePath);
MediaType mediaType = imageService.getMediaTypeForImage(imagePath);
return ResponseEntity
.ok()
.contentType(mediaType)
.body(imageBytes);
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@GetMapping("/images/{id}")
public ResponseEntity<List<String>> getNoticeImageUrls(@PathVariable("id") Long id) {
try {
if (!noticeService.noticeExists(id)) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
NoticeDTO notice = noticeService.getNoticeById(id);
List<String> imagePaths = notice.getImages();
if (imagePaths == null || imagePaths.isEmpty()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
// Zamiast przesyłać bajty, zwracamy listę URL-i do obrazów
List<String> imageUrls = new ArrayList<>();
for (int i = 0; i < imagePaths.size(); i++) {
imageUrls.add("/api/v1/notices/images/" + id + "/" + i);
}
return ResponseEntity.ok(imageUrls);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}

View File

@@ -0,0 +1,9 @@
package _11.asktpk.artisanconnectbackend.dto;
import org.springframework.core.io.Resource;
public class ImageRequestDTO {
public Resource image;
public Long noticeId;
public boolean isMainImage;
}

View File

@@ -22,8 +22,6 @@ public class NoticeDTO {
private Enums.Category category;
private List<String> images;
private Enums.Status status;
private LocalDateTime publishDate;
@@ -33,19 +31,4 @@ public class NoticeDTO {
public NoticeDTO() {
}
public NoticeDTO(Long noticeId, String title, Long clientId, String description, Double price,
Enums.Category category, List<String> images, Enums.Status status,
LocalDateTime publishDate, List<AttributesNotice> attributesNotices) {
this.noticeId = noticeId;
this.title = title;
this.clientId = clientId;
this.description = description;
this.price = price;
this.category = category;
this.images = images;
this.status = status;
this.publishDate = publishDate;
this.attributesNotices = attributesNotices;
}
}

View File

@@ -0,0 +1,13 @@
package _11.asktpk.artisanconnectbackend.dto;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class RequestResponseDTO {
public String message;
public RequestResponseDTO(String message) {
this.message = message;
}
}

View File

@@ -0,0 +1,23 @@
package _11.asktpk.artisanconnectbackend.entities;
import jakarta.persistence.*;
import jdk.jfr.BooleanFlag;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "images")
@Getter @Setter
public class Image {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long noticeId;
private String imageName;
@BooleanFlag
private boolean isMainImage;
}

View File

@@ -30,9 +30,6 @@ public class Notice {
@Enumerated(EnumType.STRING)
private Category category;
@ElementCollection
private List<String> images;
@Enumerated(EnumType.STRING)
private Status status;

View File

@@ -0,0 +1,10 @@
package _11.asktpk.artisanconnectbackend.repository;
import _11.asktpk.artisanconnectbackend.entities.Image;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ImageRepository extends JpaRepository<Image, Long> {
List<Image> findByNoticeId(Long noticeId);
}

View File

@@ -1,38 +1,84 @@
package _11.asktpk.artisanconnectbackend.service;
import _11.asktpk.artisanconnectbackend.entities.Image;
import _11.asktpk.artisanconnectbackend.repository.ImageRepository;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
public class ImageService {
private final ImageRepository imageRepository;
public byte[] getImageBytes(String imagePath) throws IOException {
Path path = Paths.get(imagePath);
return Files.readAllBytes(path);
ImageService(ImageRepository imageRepository) {
this.imageRepository = imageRepository;
}
public Resource getImageAsResource(String imagePath) throws IOException {
Path path = Paths.get(imagePath);
Resource resource = new UrlResource(path.toUri());
public String saveImageToStorage(String uploadDirectory, MultipartFile imageFile) throws IOException {
String uniqueFileName = UUID.randomUUID() + imageFile.getOriginalFilename();
if(resource.exists() && resource.isReadable()) {
return resource;
Path uploadPath = Path.of(uploadDirectory);
Path filePath = uploadPath.resolve(uniqueFileName);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
Files.copy(imageFile.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
return uniqueFileName;
}
public void addImageUrlToDB(String filename, Long noticeId) {
Image image = new Image();
image.setImageName(filename);
image.setNoticeId(noticeId);
imageRepository.save(image);
}
public Resource getImage(String imageDirectory, String imageName) throws IOException {
Path filePath = Paths.get(imageDirectory).resolve(imageName);
Resource resource = new UrlResource(filePath.toUri());
if(imageName.isEmpty() || imageDirectory.isEmpty()) {
throw new IOException("Filename or folder is empty. Please check your request and try again.");
}
if (!resource.exists()) {
throw new IOException("File not found");
}
return resource;
}
public String deleteImage(String imageDirectory, String imageName) throws IOException {
Path imagePath = Path.of(imageDirectory, imageName);
if (Files.exists(imagePath)) {
Files.delete(imagePath);
return "Success";
} else {
throw new IOException("Nie można odczytać obrazu: " + imagePath);
return "Failed"; // Handle missing images
}
}
public MediaType getMediaTypeForImage(String imagePath) {
return MediaTypeFactory
.getMediaType(imagePath)
.orElse(MediaType.APPLICATION_OCTET_STREAM);
public List<String> getImagesList(Long noticeID) throws Exception {
List<Image> images = imageRepository.findByNoticeId(noticeID);
if (images.isEmpty()) {
throw new Exception("There is no images for this notice");
}
return images.stream()
.map(Image::getImageName)
.collect(Collectors.toList());
}
}

View File

@@ -7,13 +7,7 @@ import _11.asktpk.artisanconnectbackend.repository.NoticeRepository;
import _11.asktpk.artisanconnectbackend.dto.NoticeDTO;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
@@ -34,7 +28,6 @@ public class NoticeService {
notice.setDescription(dto.getDescription());
notice.setPrice(dto.getPrice());
notice.setCategory(dto.getCategory());
notice.setImages(dto.getImages());
notice.setStatus(dto.getStatus());
notice.setPublishDate(dto.getPublishDate());
notice.setAttributesNotices(dto.getAttributesNotices());
@@ -54,7 +47,6 @@ public class NoticeService {
dto.setDescription(notice.getDescription());
dto.setPrice(notice.getPrice());
dto.setCategory(notice.getCategory());
dto.setImages(notice.getImages());
dto.setStatus(notice.getStatus());
dto.setPublishDate(notice.getPublishDate());
dto.setAttributesNotices(notice.getAttributesNotices());
@@ -92,7 +84,6 @@ public class NoticeService {
existingNotice.setDescription(dto.getDescription());
existingNotice.setPrice(dto.getPrice());
existingNotice.setCategory(dto.getCategory());
existingNotice.setImages(dto.getImages());
existingNotice.setStatus(dto.getStatus());
existingNotice.setAttributesNotices(dto.getAttributesNotices());
@@ -112,50 +103,4 @@ public class NoticeService {
throw new EntityNotFoundException("Nie znaleziono ogłoszenia o ID: " + id);
}
}
public String saveImage(String uploadFolder, Long noticeId, MultipartFile file) throws IOException {
String uploadDir = uploadFolder + noticeId;
Path uploadPath = Paths.get(uploadDir);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
// szukanie nazwy pliku
String fileName = file.getOriginalFilename();
if (fileName != null) {
String extension = fileName.substring(fileName.lastIndexOf('.'));
String baseName = fileName.substring(0, fileName.lastIndexOf('.'));
List<Path> filesInDirectory = Files.list(uploadPath)
.filter(Files::isRegularFile)
.toList();
int maxNumber = filesInDirectory.stream()
.map(path -> path.getFileName().toString())
.filter(name -> name.startsWith(baseName) && name.endsWith(extension))
.map(name -> name.substring(baseName.length(), name.length() - extension.length()))
.filter(number -> number.matches("\\d+"))
.mapToInt(Integer::parseInt)
.max()
.orElse(0);
fileName = baseName + (maxNumber + 1) + extension;
} else {
throw new IOException("Nie można znaleźć nazwy pliku");
}
//koniec szukania nazwy pliku
Path filePath = uploadPath.resolve(fileName);
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
Notice notice = noticeRepository.findById(noticeId)
.orElseThrow(() -> new EntityNotFoundException("Nie znaleziono ogłoszenia o ID: " + noticeId));
List<String> images = notice.getImages();
images.add(filePath.toString());
notice.setImages(images);
noticeRepository.save(notice);
return filePath.toString();
}
}

View File

@@ -2,14 +2,18 @@ spring.application.name=ArtisanConnectBackend
## PostgreSQL
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.username=postgres
spring.datasource.password=postgres
#initial data for db injection
spring.sql.init.data-locations=classpath:sql/data.sql
spring.sql.init.mode=always
spring.jpa.defer-datasource-initialization=true
# create and drop table, good for testing, production set to none or comment it
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.ddl-auto=create-drop
spring.web.resources.static-locations=classpath:/static/,file:images/
file.upload-dir=/Users/andsol/Desktop/uploads
spring.servlet.multipart.max-file-size=5MB
spring.servlet.multipart.max-request-size=5MB