Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,16 @@
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>5.1.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>logging-interceptor</artifactId>
<version>5.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/org/example/team6backend/config/MinioConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.example.team6backend.config;

import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MinioConfig {

@Value("${minio.url}")
private String url;

@Value("${minio.access-key}")
private String accessKey;

@Value("${minio.secret-key}")
private String secretKey;

@Bean
public MinioClient minioClient() {
return MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.example.team6backend.document.controller;

import org.example.team6backend.document.entity.Document;
import org.example.team6backend.document.service.DocumentService;
import org.example.team6backend.incident.entity.Incident;
import org.example.team6backend.incident.service.IncidentService;
import org.example.team6backend.security.CustomUserDetails;
import org.example.team6backend.user.entity.AppUser;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;
import java.util.List;

@Controller
@RequestMapping("/documents")
public class DocumentController {

private final DocumentService documentService;
private final IncidentService incidentService;

public DocumentController(DocumentService documentService, IncidentService incidentService) {
this.documentService = documentService;
this.incidentService = incidentService;
}

@PostMapping("/upload/{incidentId}")
public String uploadFile(@PathVariable Long incidentId, @RequestParam("files") List<MultipartFile> files,
@AuthenticationPrincipal CustomUserDetails userDetails) {

AppUser user = userDetails.getUser();
Incident incident = incidentService.getById(incidentId, user);

for (MultipartFile file : files) {
if (!file.isEmpty()) {
documentService.uploadFile(file, incident);
}
}
return "redirect:/incidents/" + incidentId;
}

@GetMapping("/download/{incidentId}")
public ResponseEntity<InputStreamResource> downloadFile(@PathVariable Long incidentId,
@AuthenticationPrincipal CustomUserDetails userDetails) {
AppUser user = userDetails.getUser();
Incident incident = incidentService.getById(incidentId, user);

List<Document> documents = documentService.getDocumentsByIncident(incident);
if (documents.isEmpty()) {
return ResponseEntity.notFound().build();
}
Document document = documents.get(0);

InputStream stream = documentService.downloadFile(document.getFileKey());

return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + document.getFileName() + "\"")
.contentType(MediaType.parseMediaType(document.getContentType())).body(new InputStreamResource(stream));

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.example.team6backend.document.entity;

import jakarta.persistence.*;
import org.example.team6backend.incident.entity.Incident;

@Entity
public class Document {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "file_name")
private String fileName;
@Column(name = "content_type")
private String contentType;
@Column(name = "file_key")
private String fileKey;
@Column(name = "file_size")
private Long fileSize;

@ManyToOne
@JoinColumn(name = "incident_id")
private Incident incident;

public Long getId() {
return id;
}

public String getFileName() {
return fileName;
}

public String getContentType() {
return contentType;
}

public String getFileKey() {
return fileKey;
}

public Long getFileSize() {
return fileSize;
}

public Incident getIncident() {
return incident;
}

public void setId(Long id) {
this.id = id;
}

public void setFileName(String fileName) {
this.fileName = fileName;
}

public void setContentType(String contentType) {
this.contentType = contentType;
}

public void setFileKey(String fileKey) {
this.fileKey = fileKey;
}

public void setFileSize(Long fileSize) {
this.fileSize = fileSize;
}

public void setIncident(Incident incident) {
this.incident = incident;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.example.team6backend.document.repository;

import org.example.team6backend.document.entity.Document;
import org.example.team6backend.incident.entity.Incident;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface DocumentRepository extends JpaRepository<Document, Long> {
List<Document> findByIncident(Incident incident);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package org.example.team6backend.document.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.team6backend.document.entity.Document;
import org.example.team6backend.document.repository.DocumentRepository;
import org.example.team6backend.incident.entity.Incident;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;

@Slf4j
@Service
@RequiredArgsConstructor
public class DocumentService {

private final S3Service s3Service;
private final DocumentRepository documentRepository;

/** Upload file */
public Document uploadFile(MultipartFile file, Incident incident) {

String fileKey = UUID.randomUUID() + "_" + file.getOriginalFilename();
boolean uploaded = false;

try {
s3Service.uploadFile(fileKey, file);
uploaded = true;

Document document = new Document();
document.setFileName(file.getOriginalFilename());
document.setContentType(file.getContentType());
document.setFileKey(fileKey);
document.setFileSize(file.getSize());
document.setIncident(incident);

return documentRepository.save(document);

} catch (Exception e) {
if (uploaded) {
try {
s3Service.deleteFile(fileKey);
} catch (Exception cleanupEx) {
log.warn("Failed to cleanup S3 file: {}", fileKey, cleanupEx);
}
}
throw new RuntimeException("File upload failed", e);
}
Comment thread
SandraNelj marked this conversation as resolved.
}

/** Download file */
public InputStream downloadFile(String objectKey) {
return s3Service.downloadFile(objectKey);
}

/** Delete file */
public void deleteFile(Document document) {
s3Service.deleteFile(document.getFileKey());
documentRepository.delete(document);
}

/** Fetch all files connected to one incident */
public List<Document> getDocumentsByIncident(Incident incidentId) {
return documentRepository.findByIncident(incidentId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.example.team6backend.document.service;

import io.minio.*;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;

@Service
@RequiredArgsConstructor
public class S3Service {

private final MinioClient minioClient;

@Value("${minio.bucket}")
private String bucketName;

@PostConstruct
public void init() {
try {
boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());

if (!exists) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
} catch (Exception e) {
throw new RuntimeException("Could not initialize minio service", e);
}
}

/** Upload file to MinIO */
public void uploadFile(String fileKey, MultipartFile file) {
try {
minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(fileKey)
.stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build());
} catch (Exception e) {
throw new RuntimeException("Failed to upload file " + fileKey, e);
}
}

/** Fetch file from MinIO */
public InputStream downloadFile(String fileKey) {
try {
return minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(fileKey).build());
} catch (Exception e) {
throw new RuntimeException("Failed to download file " + fileKey, e);
}
}

/** Delete file from MinIO */
public void deleteFile(String fileKey) {
try {
minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(fileKey).build());
} catch (Exception e) {
throw new RuntimeException("Failed to delete file " + fileKey, e);
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,12 @@ public IncidentController(IncidentService incidentService, UserService userServi
/** Create new incident */
@PostMapping
@PreAuthorize("hasAnyRole('RESIDENT', 'ADMIN')")
public IncidentResponse createIncident(@RequestBody @Valid IncidentRequest incidentRequest) {
Incident incident = new Incident();
incident.setSubject(incidentRequest.getSubject());
incident.setDescription(incidentRequest.getDescription());
incident.setIncidentCategory(incidentRequest.getIncidentCategory());
public IncidentResponse createIncident(@RequestBody @Valid IncidentRequest incidentRequest,
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
AppUser user = customUserDetails.getUser();

Incident saved = incidentService.createIncident(incidentRequest, null, user);

Incident saved = incidentService.createIncident(incident);
return IncidentResponse.fromEntity(saved);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package org.example.team6backend.incident.entity;

import jakarta.persistence.*;
import org.example.team6backend.document.entity.Document;
import org.example.team6backend.user.entity.AppUser;

import java.time.LocalDateTime;
import java.util.List;

@Entity
public class Incident {
Expand Down Expand Up @@ -40,6 +41,9 @@ public class Incident {
@Column(name = "updated_at")
private LocalDateTime updatedAt;

@OneToMany(mappedBy = "incident", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Document> documents;

@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
Expand Down Expand Up @@ -91,6 +95,10 @@ public LocalDateTime getUpdatedAt() {
return updatedAt;
}

public List<Document> getDocuments() {
return documents;
}

public void setId(Long id) {
this.id = id;
}
Expand Down Expand Up @@ -130,4 +138,8 @@ public void setCreatedAt(LocalDateTime createdAt) {
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}

public void setDocuments(List<Document> documents) {
this.documents = documents;
}
}
Loading
Loading