diff --git a/src/main/java/org/example/crimearchive/DTO/ReportResponse.java b/src/main/java/org/example/crimearchive/DTO/ReportResponse.java new file mode 100644 index 0000000..b7c24e8 --- /dev/null +++ b/src/main/java/org/example/crimearchive/DTO/ReportResponse.java @@ -0,0 +1,9 @@ +package org.example.crimearchive.DTO; + +import java.util.UUID; + +public record ReportResponse( + UUID uuid, + String name, + String event +) {} \ No newline at end of file diff --git a/src/main/java/org/example/crimearchive/bevis/Report.java b/src/main/java/org/example/crimearchive/bevis/Report.java index 49a4cef..d6b51b8 100644 --- a/src/main/java/org/example/crimearchive/bevis/Report.java +++ b/src/main/java/org/example/crimearchive/bevis/Report.java @@ -13,15 +13,19 @@ public class Report { private UUID uuid; private String name; private String event; + private String s3KeyPdf; + private String s3KeyFile; public Report() { } - public Report(UUID id, String name, String event) { + public Report(UUID id, String name, String event, String s3KeyPdf, String s3KeyFile) { this.uuid = id; this.name = name; this.event = event; + this.s3KeyPdf = s3KeyPdf; + this.s3KeyFile = s3KeyFile; } public UUID getUuid() { @@ -48,6 +52,21 @@ public void setEvent(String event) { this.event = event; } + public String getS3KeyPdf() { + return s3KeyPdf; + } + + public void setS3KeyPdf(String s3KeyPdf) { + this.s3KeyPdf = s3KeyPdf; + } + public String getS3KeyFile() { + return s3KeyFile; + } + + public void setS3KeyFile(String s3KeyFile) { + this.s3KeyFile = s3KeyFile; + } + @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; diff --git a/src/main/java/org/example/crimearchive/config/S3Config.java b/src/main/java/org/example/crimearchive/config/S3Config.java new file mode 100644 index 0000000..b70d9a4 --- /dev/null +++ b/src/main/java/org/example/crimearchive/config/S3Config.java @@ -0,0 +1,64 @@ +package org.example.crimearchive.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.CreateBucketRequest; +import software.amazon.awssdk.services.s3.model.HeadBucketRequest; +import software.amazon.awssdk.services.s3.model.S3Exception; + +import java.net.URI; + +@Configuration +public class S3Config { + + @Value("${minio.endpoint}") + private String endpoint; + + @Value("${minio.access-key}") + private String accessKey; + + @Value("${minio.secret-key}") + private String secretKey; + + @Value("${minio.bucket}") + private String bucket; + + @Bean + public S3Client s3Client() { + return S3Client.builder() + .endpointOverride(URI.create(endpoint)) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKey, secretKey) + )) + .region(Region.US_EAST_1) + .forcePathStyle(true) + .build(); + } + + @Bean + public ApplicationRunner createBucket(S3Client s3Client) { + return args -> { + try { + s3Client.headBucket(HeadBucketRequest.builder() + .bucket(bucket) + .build()); + System.out.println("Bucket finns redan: " + bucket); + } catch (S3Exception e) { + if (e.statusCode() == 404) { + s3Client.createBucket(CreateBucketRequest.builder() + .bucket(bucket) + .build()); + System.out.println("Bucket skapad: " + bucket); + } else { + throw new RuntimeException("Fel vid kontroll av bucket: " + e.getMessage(), e); + } + } + }; + } +} \ No newline at end of file diff --git a/src/main/java/org/example/crimearchive/controllers/HomeController.java b/src/main/java/org/example/crimearchive/controllers/HomeController.java index fad066c..0994acb 100644 --- a/src/main/java/org/example/crimearchive/controllers/HomeController.java +++ b/src/main/java/org/example/crimearchive/controllers/HomeController.java @@ -2,17 +2,22 @@ import jakarta.validation.Valid; import org.example.crimearchive.DTO.CreateReport; +import org.example.crimearchive.DTO.ReportResponse; import org.example.crimearchive.service.ReportService; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; @Controller public class HomeController { - ReportService reportService; + private final ReportService reportService; public HomeController(ReportService reportService) { this.reportService = reportService; @@ -20,7 +25,6 @@ public HomeController(ReportService reportService) { @GetMapping("/") public String indexPage(Model model) { - //model.addAttribute("reports", reportService.getAllReports()); return "index"; } @@ -30,9 +34,33 @@ public String privatePage(Model model) { return "private"; } - @PostMapping("/reports/add") - public String saveReport(@ModelAttribute("newReport") @Valid CreateReport newReport) { - reportService.saveReport(newReport); - return "redirect:/"; + @PostMapping("/reports/add") + public String saveReport( + @ModelAttribute("newReport") @Valid CreateReport newReport, + @RequestParam(value = "file", required = false) MultipartFile file, + Model model) { + try { + reportService.saveReport(newReport, file); + return "redirect:/"; + } catch (IOException e) { + model.addAttribute("error", "Kunde inte spara rapporten. Försök igen."); + model.addAttribute("newReport", newReport); + return "private"; + } + } + + @GetMapping("/reports") + public ResponseEntity> getAllReports() { + return ResponseEntity.ok(reportService.getAllReportResponses()); + } + + @GetMapping("/reports/{uuid}/download/pdf") + public ResponseEntity downloadPdf(@PathVariable UUID uuid) { + return reportService.downloadPdf(uuid); + } + + @GetMapping("/reports/{uuid}/download/file") + public ResponseEntity downloadFile(@PathVariable UUID uuid) { + return reportService.downloadFile(uuid); } -} +} \ No newline at end of file diff --git a/src/main/java/org/example/crimearchive/mapper/ReportMapper.java b/src/main/java/org/example/crimearchive/mapper/ReportMapper.java index 2576c86..0577044 100644 --- a/src/main/java/org/example/crimearchive/mapper/ReportMapper.java +++ b/src/main/java/org/example/crimearchive/mapper/ReportMapper.java @@ -7,11 +7,14 @@ public class ReportMapper { - public static Report toEntity(CreateReport report) { + public static Report toEntity(CreateReport report, String s3KeyPdf, String s3KeyFile) { return new Report( UUID.randomUUID(), report.name(), - report.event() + report.event(), + s3KeyPdf, + s3KeyFile + ); } diff --git a/src/main/java/org/example/crimearchive/service/ReportService.java b/src/main/java/org/example/crimearchive/service/ReportService.java index 1316204..3817a46 100644 --- a/src/main/java/org/example/crimearchive/service/ReportService.java +++ b/src/main/java/org/example/crimearchive/service/ReportService.java @@ -1,28 +1,205 @@ package org.example.crimearchive.service; +import com.itextpdf.text.Image; +import org.example.crimearchive.DTO.ReportResponse; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import com.itextpdf.text.Document; +import com.itextpdf.text.Paragraph; +import com.itextpdf.text.pdf.PdfWriter; import org.example.crimearchive.DTO.CreateReport; import org.example.crimearchive.bevis.Report; import org.example.crimearchive.mapper.ReportMapper; import org.example.crimearchive.repository.SimpleRepository; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.LocalDateTime; import java.util.List; +import java.util.UUID; @Service public class ReportService { private final SimpleRepository simpleRepository; + private final S3Client s3Client; + @Value("${minio.bucket}") + private String bucket; - public ReportService(SimpleRepository simpleRepository) { + public ReportService(SimpleRepository simpleRepository, S3Client s3Client) { this.simpleRepository = simpleRepository; + this.s3Client = s3Client; } - public void saveReport(CreateReport report) { - simpleRepository.save(ReportMapper.toEntity(report)); + @Transactional + public void saveReport(CreateReport report, MultipartFile file) throws IOException { + String s3KeyPdf = null; + String s3KeyFile = null; + + try { + ByteArrayOutputStream pdfStream = new ByteArrayOutputStream(); + Document document = new Document(); + PdfWriter.getInstance(document, pdfStream); + document.open(); + + + document.add(new Paragraph("Brottsanmälan")); + document.add(new Paragraph("Namn: " + report.name())); + document.add(new Paragraph("Brottstyp: " + report.event())); + document.add(new Paragraph("Datum: " + LocalDateTime.now())); + + + if (file != null && !file.isEmpty()) { + document.newPage(); + + if (isImage(file)) { + document.add(new Paragraph("Bifogat bevisfoto:")); + Image image = Image.getInstance(file.getBytes()); + image.scaleToFit(500, 700); + document.add(image); + + } else if (isPdf(file)) { + document.add(new Paragraph("Bifogat dokument (PDF):")); + document.add(new Paragraph(file.getOriginalFilename())); + document.add(new Paragraph("Se separat bifogad fil för fullständigt dokument.")); + + } else if (isWord(file)) { + document.add(new Paragraph("Bifogat dokument (Word):")); + document.add(new Paragraph(file.getOriginalFilename())); + document.add(new Paragraph("Se separat bifogad fil för fullständigt dokument.")); + + } else { + document.add(new Paragraph("Bifogad fil: " + file.getOriginalFilename())); + } + } + + document.close(); + + byte[] pdfBytes = pdfStream.toByteArray(); + s3KeyPdf = "reports/pdf/" + UUID.randomUUID() + ".pdf"; + + s3Client.putObject( + PutObjectRequest.builder() + .bucket(bucket) + .key(s3KeyPdf) + .contentType("application/pdf") + .build(), + RequestBody.fromBytes(pdfBytes) + ); + + if (file != null && !file.isEmpty()) { + s3KeyFile = "reports/files/" + UUID.randomUUID(); + + s3Client.putObject( + PutObjectRequest.builder() + .bucket(bucket) + .key(s3KeyFile) + .contentType(file.getContentType()) + .build(), + RequestBody.fromInputStream(file.getInputStream(), file.getSize()) + ); + } + + simpleRepository.save(ReportMapper.toEntity(report, s3KeyPdf, s3KeyFile)); + + } catch (Exception e) { + try { + if (s3KeyPdf != null) { + s3Client.deleteObject(DeleteObjectRequest.builder() + .bucket(bucket).key(s3KeyPdf).build()); + } + } catch (Exception cleanupEx) {} + try { + if (s3KeyFile != null) { + s3Client.deleteObject(DeleteObjectRequest.builder() + .bucket(bucket).key(s3KeyFile).build()); + } + } catch (Exception cleanupEx) {} + throw new IOException("Kunde inte spara rapport: " + e.getMessage()); + } + } + + private boolean isImage(MultipartFile file) { + String contentType = file.getContentType(); + return contentType != null && contentType.startsWith("image/"); + } + + private boolean isPdf(MultipartFile file) { + String contentType = file.getContentType(); + return contentType != null && contentType.equals("application/pdf"); + } + + private boolean isWord(MultipartFile file) { + String contentType = file.getContentType(); + return contentType != null && ( + contentType.equals("application/msword") || + contentType.equals("application/vnd.openxmlformats-officedocument.wordprocessingml.document") + ); + } + + public ResponseEntity downloadPdf(UUID uuid) { + Report report = simpleRepository.findById(uuid) + .orElseThrow(() -> new ResponseStatusException( + HttpStatus.NOT_FOUND, "Rapporten hittades inte: " + uuid)); + + if (report.getS3KeyPdf() == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ingen PDF finns för denna rapport"); + } + + ResponseBytes objectBytes = s3Client.getObjectAsBytes( + GetObjectRequest.builder() + .bucket(bucket) + .key(report.getS3KeyPdf()) + .build() + ); + + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_PDF) + .header("Content-Disposition", "attachment; filename=rapport.pdf") + .body(objectBytes.asByteArray()); + } + + public ResponseEntity downloadFile(UUID uuid) { + Report report = simpleRepository.findById(uuid) + .orElseThrow(() -> new ResponseStatusException( + HttpStatus.NOT_FOUND, "Rapporten hittades inte: " + uuid)); + + if (report.getS3KeyFile() == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Ingen bifogad fil finns för denna rapport"); + } + + ResponseBytes objectBytes = s3Client.getObjectAsBytes( + GetObjectRequest.builder() + .bucket(bucket) + .key(report.getS3KeyFile()) + .build() + ); + + return ResponseEntity.ok() + .header("Content-Disposition", "attachment; filename=fil") + .body(objectBytes.asByteArray()); + } + public List getAllReportResponses() { + return simpleRepository.findAll().stream() + .map(r -> new ReportResponse(r.getUuid(), r.getName(), r.getEvent())) + .toList(); } public List getAllReports() { return simpleRepository.findAll(); } -} +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index cfa9736..d498dd0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,3 +3,7 @@ spring.application.name=crimeArchive spring.jpa.show-sql=true #create table automatic, understand flyway operartion and change this to validate spring.jpa.hibernate.ddl-auto=update +minio.endpoint=http://localhost:9000 +minio.access-key=minioadmin +minio.secret-key=minioadmin +minio.bucket=crime-archive diff --git a/src/main/resources/templates/private.html b/src/main/resources/templates/private.html index ad01f87..dcb7570 100644 --- a/src/main/resources/templates/private.html +++ b/src/main/resources/templates/private.html @@ -5,13 +5,18 @@ Title -
+
+ - +
+ + +
+
- + \ No newline at end of file