Skip to content

Commit

Permalink
Attachment for entities (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
haiphucnguyen authored Jan 12, 2025
1 parent 268bfa0 commit 6c58421
Show file tree
Hide file tree
Showing 40 changed files with 444 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.flowinquiry.modules.collab.web.rest;
package io.flowinquiry.modules.collab.controller;

import io.flowinquiry.modules.collab.domain.EntityType;
import io.flowinquiry.modules.collab.service.ActivityLogService;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.flowinquiry.modules.collab.web.rest;
package io.flowinquiry.modules.collab.controller;

import io.flowinquiry.modules.collab.domain.Comment;
import io.flowinquiry.modules.collab.domain.EntityType;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.flowinquiry.modules.collab.web.rest;
package io.flowinquiry.modules.collab.controller;

import io.flowinquiry.modules.collab.service.NotificationService;
import io.flowinquiry.modules.collab.service.dto.NotificationDTO;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package io.flowinquiry.modules.fss.controller;

import io.flowinquiry.modules.fss.domain.EntityAttachment;
import io.flowinquiry.modules.fss.service.EntityAttachmentService;
import io.flowinquiry.modules.fss.service.dto.EntityAttachmentDTO;
import java.util.List;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/api/entity-attachments")
public class EntityAttachmentController {

private final EntityAttachmentService attachmentService;

public EntityAttachmentController(EntityAttachmentService attachmentService) {
this.attachmentService = attachmentService;
}

/**
* Upload multiple attachments and associate them with a specific entity.
*
* @param entityType The type of entity (e.g., "team_request", "comment").
* @param entityId The ID of the entity.
* @param files The list of files to upload.
* @return A list of saved attachment entities.
*/
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<List<EntityAttachment>> uploadAttachments(
@RequestParam("entityType") String entityType,
@RequestParam("entityId") Long entityId,
@RequestPart("files") MultipartFile[] files)
throws Exception {
List<EntityAttachment> attachments =
attachmentService.uploadAttachments(entityType, entityId, files);
return ResponseEntity.ok(attachments);
}

/**
* Retrieve all attachments for a specific entity.
*
* @param entityType The type of entity (e.g., "team_request", "comment").
* @param entityId The ID of the entity.
* @return A list of attachments for the specified entity.
*/
@GetMapping
public ResponseEntity<List<EntityAttachmentDTO>> getAttachments(
@RequestParam("entityType") String entityType,
@RequestParam("entityId") Long entityId) {
List<EntityAttachmentDTO> attachments =
attachmentService.getAttachments(entityType, entityId);
return ResponseEntity.ok(attachments);
}

/**
* Deletes an attachment by its ID.
*
* @param attachmentId The ID of the attachment to delete.
* @return A ResponseEntity indicating the result of the deletion.
*/
@DeleteMapping("/{attachmentId}")
public ResponseEntity<Void> deleteAttachment(@PathVariable("attachmentId") Long attachmentId) {
attachmentService.deleteAttachment(attachmentId);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.flowinquiry.modules.fss.web.rest;
package io.flowinquiry.modules.fss.controller;

import io.flowinquiry.modules.fss.service.StorageService;
import jakarta.servlet.http.HttpServletRequest;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package io.flowinquiry.modules.fss.web.rest;
package io.flowinquiry.modules.fss.controller;

import io.flowinquiry.modules.fss.service.StorageService;
import io.flowinquiry.modules.usermanagement.service.dto.UserKey;
import io.flowinquiry.security.SecurityUtils;
import jakarta.json.Json;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -22,15 +20,6 @@ public class FileUploadController {

private static final Logger LOG = LoggerFactory.getLogger(FileUploadController.class);

private static final String AVATAR_TYPE = "avatar";

private final Map<String, String> typeRelativePaths =
new HashMap<>() {
{
put(AVATAR_TYPE, AVATAR_TYPE);
}
};

private final StorageService storageService;

public FileUploadController(StorageService storageService) {
Expand All @@ -49,10 +38,10 @@ public ResponseEntity<String> submit(
currentUser,
file.getOriginalFilename(),
type);

if (!typeRelativePaths.containsKey(type))
String prefixPath = storageService.getRelativePathByType(type);
if (prefixPath == null) {
return ResponseEntity.badRequest().body("Not support upload with type " + type);
String prefixPath = typeRelativePaths.get(type);
}

String path =
storageService.uploadFile(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.flowinquiry.modules.fss.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Table(
name = "fw_entity_attachment",
uniqueConstraints =
@UniqueConstraint(columnNames = {"entity_type", "entity_id", "file_url"}))
@Getter
@Setter
@Builder
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@NoArgsConstructor
@AllArgsConstructor
public class EntityAttachment {

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

@EqualsAndHashCode.Include
@Column(name = "entity_type", nullable = false, length = 50)
private String entityType;

@EqualsAndHashCode.Include
@Column(name = "entity_id", nullable = false)
private Long entityId;

@Column(name = "file_name", nullable = false, length = 255)
private String fileName;

@Column(name = "file_type", length = 100)
private String fileType;

@Column(name = "file_size")
private Long fileSize;

@EqualsAndHashCode.Include
@Column(name = "file_url", nullable = false, columnDefinition = "TEXT")
private String fileUrl;

@Column(name = "uploaded_at", nullable = false, updatable = false)
private LocalDateTime uploadedAt;

@PrePersist
public void prePersist() {
if (this.uploadedAt == null) {
this.uploadedAt = LocalDateTime.now();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.flowinquiry.modules.fss.repository;

import io.flowinquiry.modules.fss.domain.EntityAttachment;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface EntityAttachmentRepository extends JpaRepository<EntityAttachment, Long> {

/**
* Finds all attachments for a specific entity type and entity ID.
*
* @param entityType The type of entity (e.g., "team_request", "comment").
* @param entityId The ID of the entity.
* @return A list of attachments for the specified entity.
*/
List<EntityAttachment> findByEntityTypeAndEntityId(String entityType, Long entityId);

/**
* Deletes all attachments for a specific entity type and entity ID.
*
* @param entityType The type of entity (e.g., "team_request", "comment").
* @param entityId The ID of the entity.
*/
void deleteByEntityTypeAndEntityId(String entityType, Long entityId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package io.flowinquiry.modules.fss.service;

import io.flowinquiry.modules.fss.domain.EntityAttachment;
import io.flowinquiry.modules.fss.repository.EntityAttachmentRepository;
import io.flowinquiry.modules.fss.service.dto.EntityAttachmentDTO;
import io.flowinquiry.modules.fss.service.mapper.EntityAttachmentMapper;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

@Service
public class EntityAttachmentService {

private static final Logger LOG = LoggerFactory.getLogger(EntityAttachmentService.class);

private final EntityAttachmentRepository entityAttachmentRepository;
private final EntityAttachmentMapper entityAttachmentMapper;
private final StorageService storageService;

public EntityAttachmentService(
EntityAttachmentRepository entityAttachmentRepository,
EntityAttachmentMapper entityAttachmentMapper,
StorageService storageService) {
this.entityAttachmentRepository = entityAttachmentRepository;
this.entityAttachmentMapper = entityAttachmentMapper;
this.storageService = storageService;
}

/**
* Uploads a single attachment and returns an unsaved EntityAttachment object.
*
* @param entityType The type of entity (e.g., "team_request", "comment").
* @param entityId The ID of the entity.
* @param file The file to upload.
* @return An unsaved EntityAttachment object.
* @throws Exception If file storage fails or the file is empty.
*/
private EntityAttachment createAttachment(String entityType, Long entityId, MultipartFile file)
throws Exception {
if (file.isEmpty()) {
throw new IllegalArgumentException("File cannot be empty.");
}

// Store the file and get its URL
String fileUrl =
storageService.uploadFile(
StorageService.ATTACHMENTS,
file.getOriginalFilename(),
file.getInputStream());

// Create the attachment entity
EntityAttachment attachment = new EntityAttachment();
attachment.setEntityType(entityType);
attachment.setEntityId(entityId);
attachment.setFileName(file.getOriginalFilename());
attachment.setFileType(file.getContentType());
attachment.setFileSize(file.getSize());
attachment.setFileUrl(fileUrl);
attachment.setUploadedAt(LocalDateTime.now());

return attachment;
}

/**
* Uploads multiple attachments and associates them with a specific entity using batch insert.
*
* @param entityType The type of entity (e.g., "team_request", "comment").
* @param entityId The ID of the entity.
* @param files The list of files to upload.
* @return A list of saved EntityAttachment objects.
* @throws Exception If any file storage operation fails.
*/
@Transactional
public List<EntityAttachment> uploadAttachments(
String entityType, Long entityId, MultipartFile[] files) throws Exception {
if (files == null || files.length == 0) {
throw new IllegalArgumentException("File list cannot be empty.");
}

List<EntityAttachment> attachments = new ArrayList<>();
for (MultipartFile file : files) {
attachments.add(createAttachment(entityType, entityId, file));
}

// Perform batch insert for all attachments
return entityAttachmentRepository.saveAll(attachments);
}

/**
* Retrieves all attachments associated with a specific entity.
*
* @param entityType The type of entity (e.g., "team_request", "comment").
* @param entityId The ID of the entity.
* @return A list of attachments for the entity.
*/
public List<EntityAttachmentDTO> getAttachments(String entityType, Long entityId) {
return entityAttachmentMapper.toDtoList(
entityAttachmentRepository.findByEntityTypeAndEntityId(entityType, entityId));
}

/**
* Deletes all attachments associated with a specific entity.
*
* @param entityType The type of entity (e.g., "team_request", "comment").
* @param entityId The ID of the entity.
*/
public void deleteAttachments(String entityType, Long entityId) throws Exception {
List<EntityAttachment> attachments =
entityAttachmentRepository.findByEntityTypeAndEntityId(entityType, entityId);

for (EntityAttachment attachment : attachments) {
if (attachment.getFileUrl() != null) {
storageService.deleteFile(attachment.getFileUrl());
}
}

// Delete the attachment records from the database
entityAttachmentRepository.deleteAll(attachments);
}

/**
* Deletes an attachment by its ID. If the attachment does not exist, it silently ignores the
* operation.
*
* @param attachmentId The ID of the attachment to delete.
*/
@Transactional
public void deleteAttachment(Long attachmentId) {
entityAttachmentRepository
.findById(attachmentId)
.ifPresent(
attachment -> {
if (attachment.getFileUrl() != null) {
try {
storageService.deleteFile(attachment.getFileUrl());
} catch (Exception e) {
LOG.error("Can not delete file {}", attachment.getFileUrl(), e);
}
}

entityAttachmentRepository.deleteById(attachmentId);
});
}
}
Loading

0 comments on commit 6c58421

Please sign in to comment.