Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/chung step5 #12

Merged
merged 24 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
dbdd8e9
feat : Add shareLink
kochungcheon May 13, 2024
4df23ee
test: Add create shareLink
kochungcheon May 13, 2024
fd17400
feat: Add downloadShareFile
kochungcheon May 13, 2024
916c153
refacot: FileUtil.download
kochungcheon May 13, 2024
4f3be8a
test: Add download shareFile
kochungcheon May 13, 2024
3335d28
test : 공유파일 다운로드 실패 테스트
kochungcheon May 16, 2024
28c28d2
feat : Add 파일공유링크 삭제 스케줄러
kochungcheon May 17, 2024
4432e2c
test : 링크삭제 스케줄러
kochungcheon May 17, 2024
fc21cc3
refactor : 파일 공유 다운로드 에러코드 정정
kochungcheon May 17, 2024
733c6ad
chore : 코드스멜 제거
kochungcheon May 17, 2024
dc7a49f
chore : 코드스멜 제거
kochungcheon May 17, 2024
ada9402
refactor : PageRequest -> limit
kochungcheon May 18, 2024
4d1e597
refactor : Duration.ofHours -> duration.toHours()
kochungcheon May 18, 2024
b53fe7c
refactor : requestBody -> modelAttribute
kochungcheon May 18, 2024
d87cae5
refactor : createAt index
kochungcheon May 18, 2024
3949129
chore : code smell
kochungcheon May 18, 2024
f3fd490
refactor : 파일 공유 Response Dto에 유효성 검사 추가
kochungcheon May 19, 2024
7c43fed
chore : remove line
kochungcheon May 20, 2024
70010f1
refactor : mapping 형식 변경
kochungcheon May 22, 2024
a3be9b6
refactor : shareLink 생성 방법 변경
kochungcheon May 22, 2024
85002ab
refactor : cron 매일 3시간 단위로 수정
kochungcheon May 22, 2024
26be5ed
refactor : findExpirations -> deleteByExpirations
kochungcheon May 22, 2024
38ed73f
refactor : DeleteLinkSchedler 3시간 단위 삭제 -> 5분 단위 삭제
kochungcheon May 22, 2024
32737ab
refactor : deleteByExpirations 도입에 따른 deleteById 삭제
kochungcheon May 22, 2024
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
50 changes: 27 additions & 23 deletions src/main/java/com/c4cometrue/mystorage/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,41 @@
@Getter
@AllArgsConstructor
public enum ErrorCode {
UNAUTHORIZED_FILE_ACCESS(HttpStatus.FORBIDDEN, "비정상적인 요청입니다."),
UNAUTHORIZED_FILE_ACCESS(HttpStatus.FORBIDDEN, "비정상적인 요청입니다."),

CANNOT_FOUND_FILE(HttpStatus.NOT_FOUND, "해당 파일을 찾을 수 없습니다."),
CANNOT_FOUND_FILE(HttpStatus.NOT_FOUND, "해당 파일을 찾을 수 없습니다."),

FILE_COPY_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 복사 중 오류가 발생했습니다."),
FILE_COPY_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 복사 중 오류가 발생했습니다."),

FILE_DELETE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 삭제 중 오류가 발생했습니다."),
FILE_DELETE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 삭제 중 오류가 발생했습니다."),

DUPLICATE_FILE_NAME(HttpStatus.BAD_REQUEST, "파일 업로드에 중복이 발생 했습니다"),
DUPLICATE_FILE_NAME(HttpStatus.BAD_REQUEST, "파일 업로드에 중복이 발생 했습니다"),

FOLDER_CREATE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "폴더 생성 중 오류가 발생했습니다"),
UNAUTHORIZED_FOLDER_ACCESS(HttpStatus.FORBIDDEN, "비정상적인 요청입니다."),
DUPLICATE_FOLDER_NAME(HttpStatus.BAD_REQUEST, "폴더 업로드에 중복이 발생 했습니다"),
DUPLICATE_SERVER_FOLDER_NAME(HttpStatus.BAD_REQUEST, "폴더 UUID 중복이 발생 했습니다"),
CANNOT_FOUND_FOLDER(HttpStatus.NOT_FOUND, "해당 폴더를 찾을 수 없습니다."),
FOLDER_CREATE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "폴더 생성 중 오류가 발생했습니다"),
UNAUTHORIZED_FOLDER_ACCESS(HttpStatus.FORBIDDEN, "비정상적인 요청입니다."),
DUPLICATE_FOLDER_NAME(HttpStatus.BAD_REQUEST, "폴더 업로드에 중복이 발생 했습니다"),
DUPLICATE_SERVER_FOLDER_NAME(HttpStatus.BAD_REQUEST, "폴더 UUID 중복이 발생 했습니다"),
CANNOT_FOUND_FOLDER(HttpStatus.NOT_FOUND, "해당 폴더를 찾을 수 없습니다."),

DUPLICATE_BASE_PATH(HttpStatus.BAD_REQUEST, "기본 경로 생성에 중복이 발생했습니다"),
DUPLICATE_BASE_PATH(HttpStatus.BAD_REQUEST, "기본 경로 생성에 중복이 발생했습니다"),

MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 맴버를 찾지 못했습니다"),
EXCEEDED_CAPACITY(HttpStatus.INSUFFICIENT_STORAGE, "더 이상 업로드 할 수 없습니다"),
INVALID_OPERATION(HttpStatus.BAD_REQUEST, "사용 중인 공간보다 많은 공간은 해제할 수 없습니다"),
VALIDATION_ERROR(HttpStatus.BAD_REQUEST, "유효하지 않은 요청입니다.");
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 맴버를 찾지 못했습니다"),
EXCEEDED_CAPACITY(HttpStatus.INSUFFICIENT_STORAGE, "더 이상 업로드 할 수 없습니다"),
INVALID_OPERATION(HttpStatus.BAD_REQUEST, "사용 중인 공간보다 많은 공간은 해제할 수 없습니다"),
VALIDATION_ERROR(HttpStatus.BAD_REQUEST, "유효하지 않은 요청입니다."),

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

공유된 이슈로 개행 문제가 있습니다.
아래 세 개가 이번 수정 사항에 반영된 것입니다.

  • DUPLICATE_SHARE_LINK
  • NOT_FOUND_SHARE_LINK
  • NOT_FRESH_LINK

private final HttpStatus httpStatus;
private final String message;
DUPLICATE_SHARE_LINK(HttpStatus.INTERNAL_SERVER_ERROR, "링크 생성에 중복이 발생했습니다"),
NOT_FOUND_SHARE_LINK(HttpStatus.NOT_FOUND, "링크를 찾을 수 없습니다"),
NOT_FRESH_LINK(HttpStatus.BAD_REQUEST, "만료된 링크입니다.");

public ServiceException serviceException() {
return new ServiceException(this.name(), message);
}
private final HttpStatus httpStatus;
private final String message;

public ServiceException serviceException(String debugMessage, Object... debugMessageArgs) {
return new ServiceException(this.name(), message, String.format(debugMessage, debugMessageArgs));
}
public ServiceException serviceException() {
return new ServiceException(this.name(), message);
}

public ServiceException serviceException(String debugMessage, Object... debugMessageArgs) {
return new ServiceException(this.name(), message, String.format(debugMessage, debugMessageArgs));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,50 +13,55 @@
@Service
@RequiredArgsConstructor
public class FileDataHandlerService {
private final FileRepository fileRepository;

@Transactional
public void deleteBy(Long fileId) {
existBy(fileId);
fileRepository.deleteById(fileId);
}

private void existBy(Long fileId) {
if (!fileRepository.existsById(fileId)) {
throw ErrorCode.CANNOT_FOUND_FILE.serviceException("fileId : {}", fileId);
}
}

public FileMetadata findBy(Long fileId, Long userId) {
return fileRepository.findByIdAndUploaderId(fileId, userId)
.orElseThrow(() -> ErrorCode.CANNOT_FOUND_FILE.serviceException("fileId : {}, userId : {}", fileId,
userId));
}

public void persist(FileMetadata fileMetadata) {
fileRepository.save(fileMetadata);
}

public void duplicateBy(Long parentId, String fileName) {
if (fileRepository.existsByParentIdAndOriginalFileName(parentId, fileName)) {
throw ErrorCode.DUPLICATE_FILE_NAME.serviceException("fileName : {}", fileName);
}
}

public List<FileMetadata> getFileList(Long parentId, Long cursorId, Long userId, Pageable page) {
return cursorId == null ? fileRepository.findAllByParentIdAndUploaderIdOrderByIdDesc(parentId, userId, page)
: fileRepository.findByParentIdAndUploaderIdAndIdLessThanOrderByIdDesc(parentId, cursorId, userId, page);
}

public Boolean hashNext(Long parentId, Long userId, Long lastIdOfList) {
return fileRepository.existsByParentIdAndUploaderIdAndIdLessThan(parentId, userId, lastIdOfList);
}

public List<FileMetadata> findAllBy(Long parentId) {
return fileRepository.findAllByParentId(parentId);
}

public void deleteAll(List<FileMetadata> fileMetadataList) {
fileRepository.deleteAll(fileMetadataList);
}
private final FileRepository fileRepository;

@Transactional
public void deleteBy(Long fileId) {
existBy(fileId);
fileRepository.deleteById(fileId);
}

private void existBy(Long fileId) {
if (!fileRepository.existsById(fileId)) {
throw ErrorCode.CANNOT_FOUND_FILE.serviceException("fileId : {}", fileId);
}
}

public FileMetadata findBy(Long fileId, Long userId) {
return fileRepository.findByIdAndUploaderId(fileId, userId)
.orElseThrow(() -> ErrorCode.CANNOT_FOUND_FILE.serviceException("fileId : {}, userId : {}", fileId,
userId));
}

public void persist(FileMetadata fileMetadata) {
fileRepository.save(fileMetadata);
}

public void duplicateBy(Long parentId, String fileName) {
if (fileRepository.existsByParentIdAndOriginalFileName(parentId, fileName)) {
throw ErrorCode.DUPLICATE_FILE_NAME.serviceException("fileName : {}", fileName);
}
}

public List<FileMetadata> getFileList(Long parentId, Long cursorId, Long userId, Pageable page) {
return cursorId == null ? fileRepository.findAllByParentIdAndUploaderIdOrderByIdDesc(parentId, userId, page)
: fileRepository.findByParentIdAndUploaderIdAndIdLessThanOrderByIdDesc(parentId, cursorId, userId, page);
}

public Boolean hashNext(Long parentId, Long userId, Long lastIdOfList) {
return fileRepository.existsByParentIdAndUploaderIdAndIdLessThan(parentId, userId, lastIdOfList);
}

public List<FileMetadata> findAllBy(Long parentId) {
return fileRepository.findAllByParentId(parentId);
}

public void deleteAll(List<FileMetadata> fileMetadataList) {
fileRepository.deleteAll(fileMetadataList);
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

공유된 이슈로 개행 문제가 있습니다.
아래 findBy가 이번에 추가된 사항입니다.

// 파일이 삭제된 경우에는 파일 공유 링크로 다운 받을 수 없다.
public FileMetadata findBy(Long fileId) {
return fileRepository.findById(fileId).orElseThrow(ErrorCode.CANNOT_FOUND_FILE::serviceException);
}
}
19 changes: 9 additions & 10 deletions src/main/java/com/c4cometrue/mystorage/file/FileRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,22 @@

import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개행 수정했습니다. 로직 상 수정 사항은 없습니다.

public interface FileRepository extends JpaRepository<FileMetadata, Long> {
Optional<FileMetadata> findByIdAndUploaderId(Long id, Long uploaderId);
Optional<FileMetadata> findByIdAndUploaderId(Long id, Long uploaderId);

boolean existsByParentIdAndOriginalFileName(Long parentId, String fileName);
boolean existsByParentIdAndOriginalFileName(Long parentId, String fileName);

List<FileMetadata> findByParentIdAndUploaderId(Long parentId, Long userId);
List<FileMetadata> findByParentIdAndUploaderId(Long parentId, Long userId);

Boolean existsByIdAndUploaderId(Long parentId, Long userId);
Boolean existsByIdAndUploaderId(Long parentId, Long userId);

List<FileMetadata> findAllByParentIdAndUploaderIdOrderByIdDesc(Long parentId, Long uploaderId, Pageable page);
List<FileMetadata> findAllByParentIdAndUploaderIdOrderByIdDesc(Long parentId, Long uploaderId, Pageable page);

List<FileMetadata> findByParentIdAndUploaderIdAndIdLessThanOrderByIdDesc(Long parentId, Long userId, Long cursorId,
Pageable pageable);
List<FileMetadata> findByParentIdAndUploaderIdAndIdLessThanOrderByIdDesc(Long parentId, Long userId, Long cursorId,
Pageable pageable);

Boolean existsByParentIdAndUploaderIdAndIdLessThan(Long parentId, Long uploaderId, Long id);
Boolean existsByParentIdAndUploaderIdAndIdLessThan(Long parentId, Long uploaderId, Long id);

List<FileMetadata> findAllByParentId(Long parentId);
List<FileMetadata> findAllByParentId(Long parentId);
}
5 changes: 1 addition & 4 deletions src/main/java/com/c4cometrue/mystorage/file/FileService.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,7 @@ protected void uploadFile(Long userId, Long rootId, BigDecimal fileSize, FileMet

public void downloadFile(Long fileId, String userPath, Long userId) {
FileMetadata fileMetadata = fileDataHandlerService.findBy(fileId, userId);
Path originalPath = Paths.get(fileMetadata.getFilePath());
Path userDesignatedPath = Paths.get(userPath).resolve(fileMetadata.getOriginalFileName()).normalize();

FileUtil.download(originalPath, userDesignatedPath, bufferSize);
FileUtil.download(fileMetadata, userPath);
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.c4cometrue.mystorage.fileshare;

import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.List;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class DeleteLinkScheduler {
private static final int DELETE_SIZE = 100;

private final FileShareRepository fileShareRepository;

@Scheduled(cron = "0 0 3 * * ?")
public void deleteExpiredLinks() {
ZonedDateTime expirationTime = ZonedDateTime.now(ZoneOffset.UTC).minusHours(3);
List<Long> expirationLinkIds;

do {
expirationLinkIds = fileShareRepository.findExpirations(expirationTime, DELETE_SIZE);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 바로 삭제하지 않는 의도가 있을까요?
(delete 계열은 long을 반환할 수 있습니다.)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 바로 삭제하지 않는 의도가 있을까요? (delete 계열은 long을 반환할 수 있습니다.)

앗...!!!!! 덕분에 하나 배워 갑니다 감사합니다! 수정했습니다 💯

if (!expirationLinkIds.isEmpty()) {
fileShareRepository.deleteByIds(expirationLinkIds);
}
} while (expirationLinkIds.size() == DELETE_SIZE);
}
}
46 changes: 46 additions & 0 deletions src/main/java/com/c4cometrue/mystorage/fileshare/FileShare.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.c4cometrue.mystorage.fileshare;

import java.time.ZoneOffset;
import java.time.ZonedDateTime;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "file_share", indexes = {
@Index(name = "idx_share_link", columnList = "shareLink"),
@Index(name = "idx_share_created_at", columnList = "createAt")
})
public class FileShare {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long fileId;
@Column(nullable = false, unique = true)
private String shareLink;
@Column(updatable = false)
private ZonedDateTime createdAt;

@Builder
public FileShare(Long fileId, String shareLink) {
this.fileId = fileId;
this.shareLink = shareLink;
}

@PrePersist
public void prePersist() {
createdAt = ZonedDateTime.now(ZoneOffset.UTC);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.c4cometrue.mystorage.fileshare;

import org.springframework.http.ResponseEntity;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.c4cometrue.mystorage.fileshare.dto.CreateShareFileReq;
import com.c4cometrue.mystorage.fileshare.dto.CreateShareFileRes;
import com.c4cometrue.mystorage.fileshare.dto.DownloadShareFileReq;
import com.c4cometrue.mystorage.fileshare.dto.DownloadShareFileRes;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/files/share")
@RequiredArgsConstructor
public class FileShareController {
private final FileShareService fileShareService;

@PostMapping
public ResponseEntity<CreateShareFileRes> createShareFile(@Valid @RequestBody CreateShareFileReq req) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 수정하였습니다

CreateShareFileRes res = fileShareService.createShareFileLink(req.fileId(), req.userId());
return ResponseEntity.ok(res);
}

@GetMapping("/download")
public ResponseEntity<DownloadShareFileRes> downloadShareFile(@Valid @ModelAttribute DownloadShareFileReq req) {
DownloadShareFileRes res = fileShareService.downloadShareFile(req.shareLink(), req.userPath());
return ResponseEntity.ok(res);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.c4cometrue.mystorage.fileshare;

import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface FileShareRepository extends JpaRepository<FileShare, Long> {
boolean existsByShareLink(String shareLink);

Optional<FileShare> findByShareLink(String shareLink);

@Query(value = "SELECT fs.id FROM FileShare fs WHERE fs.createdAt < :expirationTime LIMIT :limit",
nativeQuery = true)
List<Long> findExpirations(@Param("expirationTime") ZonedDateTime expirationTime, @Param("limit") int limit);

@Query("DELETE FROM FileShare fs WHERE fs.id IN :ids")
void deleteByIds(@Param("ids") List<Long> ids);
}
Loading
Loading