Files
my_site/backend/src/main/java/com/yoyuzh/files/ShareV2Service.java

173 lines
7.5 KiB
Java

package com.yoyuzh.files;
import com.yoyuzh.api.v2.ApiV2ErrorCode;
import com.yoyuzh.api.v2.ApiV2Exception;
import com.yoyuzh.api.v2.shares.CreateShareV2Request;
import com.yoyuzh.api.v2.shares.ImportShareV2Request;
import com.yoyuzh.api.v2.shares.ShareV2Response;
import com.yoyuzh.api.v2.shares.VerifySharePasswordV2Request;
import com.yoyuzh.auth.User;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.UUID;
@Service
@RequiredArgsConstructor
public class ShareV2Service {
private final StoredFileRepository storedFileRepository;
private final FileShareLinkRepository fileShareLinkRepository;
private final FileService fileService;
private final PasswordEncoder passwordEncoder;
@Transactional
public ShareV2Response createShare(User user, CreateShareV2Request request) {
StoredFile file = storedFileRepository.findByIdAndUserIdAndDeletedAtIsNull(request.fileId(), user.getId())
.orElseThrow(() -> new ApiV2Exception(ApiV2ErrorCode.FILE_NOT_FOUND, "file not found"));
if (file.isDirectory()) {
throw new ApiV2Exception(ApiV2ErrorCode.BAD_REQUEST, "directories are not supported");
}
validateSharePolicy(request.expiresAt(), request.maxDownloads());
FileShareLink shareLink = new FileShareLink();
shareLink.setOwner(user);
shareLink.setFile(file);
shareLink.setToken(UUID.randomUUID().toString().replace("-", ""));
shareLink.setShareName(StringUtils.hasText(request.shareName()) ? request.shareName().trim() : file.getFilename());
shareLink.setPasswordHash(StringUtils.hasText(request.password()) ? passwordEncoder.encode(request.password()) : null);
shareLink.setExpiresAt(request.expiresAt());
shareLink.setMaxDownloads(request.maxDownloads());
shareLink.setAllowImport(request.allowImport() == null ? true : request.allowImport());
shareLink.setAllowDownload(request.allowDownload() == null ? true : request.allowDownload());
FileShareLink saved = fileShareLinkRepository.save(shareLink);
return toResponse(saved, true, true);
}
@Transactional
public ShareV2Response getShare(String token) {
FileShareLink shareLink = getShareLink(token);
ensureShareNotExpired(shareLink);
shareLink.setViewCount(shareLink.getViewCountOrZero() + 1);
boolean passwordRequired = shareLink.hasPassword();
return toResponse(shareLink, !passwordRequired, !passwordRequired);
}
@Transactional
public ShareV2Response verifyPassword(String token, VerifySharePasswordV2Request request) {
FileShareLink shareLink = getShareLink(token);
ensureShareNotExpired(shareLink);
if (shareLink.hasPassword()) {
if (!StringUtils.hasText(request.password()) || !passwordEncoder.matches(request.password(), shareLink.getPasswordHash())) {
throw new ApiV2Exception(ApiV2ErrorCode.BAD_REQUEST, "invalid password");
}
}
shareLink.setViewCount(shareLink.getViewCountOrZero() + 1);
return toResponse(shareLink, true, true);
}
@Transactional
public FileMetadataResponse importSharedFile(User recipient, String token, ImportShareV2Request request) {
FileShareLink shareLink = getShareLink(token);
ensureShareNotExpired(shareLink);
ensureImportAllowed(shareLink);
ensurePasswordAccepted(shareLink, request.password());
FileMetadataResponse importedFile = fileService.importSharedFile(recipient, token, request.path());
shareLink.setDownloadCount(shareLink.getDownloadCountOrZero() + 1);
return importedFile;
}
@Transactional
public Page<ShareV2Response> listOwnedShares(User user, Pageable pageable) {
return fileShareLinkRepository.findByOwnerIdOrderByCreatedAtDesc(user.getId(), pageable)
.map(shareLink -> toResponse(shareLink, true, true));
}
@Transactional
public void deleteOwnedShare(User user, Long id) {
FileShareLink shareLink = fileShareLinkRepository.findByIdAndOwnerId(id, user.getId())
.orElseThrow(() -> new ApiV2Exception(ApiV2ErrorCode.FILE_NOT_FOUND, "share not found"));
fileShareLinkRepository.delete(shareLink);
}
private FileShareLink getShareLink(String token) {
return fileShareLinkRepository.findByToken(token)
.orElseThrow(() -> new ApiV2Exception(ApiV2ErrorCode.FILE_NOT_FOUND, "share not found"));
}
private void ensureShareNotExpired(FileShareLink shareLink) {
if (shareLink.getExpiresAt() != null && !LocalDateTime.now().isBefore(shareLink.getExpiresAt())) {
throw new ApiV2Exception(ApiV2ErrorCode.FILE_NOT_FOUND, "share not found");
}
}
private void ensureImportAllowed(FileShareLink shareLink) {
if (!shareLink.isAllowImportEnabled()) {
throw new ApiV2Exception(ApiV2ErrorCode.PERMISSION_DENIED, "import disabled");
}
Integer maxDownloads = shareLink.getMaxDownloads();
if (maxDownloads != null && shareLink.getDownloadCountOrZero() >= maxDownloads) {
throw new ApiV2Exception(ApiV2ErrorCode.PERMISSION_DENIED, "share quota exceeded");
}
}
private void ensurePasswordAccepted(FileShareLink shareLink, String password) {
if (!shareLink.hasPassword()) {
return;
}
if (!StringUtils.hasText(password) || !passwordEncoder.matches(password, shareLink.getPasswordHash())) {
throw new ApiV2Exception(ApiV2ErrorCode.BAD_REQUEST, "invalid password");
}
}
private void validateSharePolicy(LocalDateTime expiresAt, Integer maxDownloads) {
if (expiresAt != null && !expiresAt.isAfter(LocalDateTime.now())) {
throw new ApiV2Exception(ApiV2ErrorCode.BAD_REQUEST, "expiresAt must be in the future");
}
if (maxDownloads != null && maxDownloads <= 0) {
throw new ApiV2Exception(ApiV2ErrorCode.BAD_REQUEST, "maxDownloads must be greater than 0");
}
}
private ShareV2Response toResponse(FileShareLink shareLink, boolean passwordVerified, boolean includeFile) {
return new ShareV2Response(
shareLink.getId(),
shareLink.getToken(),
shareLink.getShareNameOrDefault(),
shareLink.getOwner() == null ? null : shareLink.getOwner().getUsername(),
shareLink.hasPassword(),
passwordVerified,
shareLink.isAllowImportEnabled(),
shareLink.isAllowDownloadEnabled(),
shareLink.getMaxDownloads(),
shareLink.getDownloadCountOrZero(),
shareLink.getViewCountOrZero(),
shareLink.getExpiresAt(),
shareLink.getCreatedAt(),
includeFile && shareLink.getFile() != null ? toFileMetadataResponse(shareLink.getFile()) : null
);
}
private FileMetadataResponse toFileMetadataResponse(StoredFile file) {
return new FileMetadataResponse(
file.getId(),
file.getFilename(),
file.getPath(),
file.getSize(),
file.getContentType(),
file.isDirectory(),
file.getCreatedAt()
);
}
}