feat(admin): add blob share and task admin apis

This commit is contained in:
yoyuzh
2026-04-11 14:09:31 +08:00
parent 12005cc606
commit f59515f5dd
17 changed files with 2118 additions and 22 deletions

View File

@@ -5,7 +5,11 @@ import com.yoyuzh.auth.CustomUserDetailsService;
import com.yoyuzh.auth.User;
import com.yoyuzh.common.ApiResponse;
import com.yoyuzh.common.PageResponse;
import com.yoyuzh.files.core.FileEntityType;
import com.yoyuzh.files.tasks.BackgroundTask;
import com.yoyuzh.files.tasks.BackgroundTaskFailureCategory;
import com.yoyuzh.files.tasks.BackgroundTaskStatus;
import com.yoyuzh.files.tasks.BackgroundTaskType;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -61,6 +65,49 @@ public class AdminController {
return ApiResponse.success(adminService.listFiles(page, size, query, ownerQuery));
}
@GetMapping("/file-blobs")
public ApiResponse<PageResponse<AdminFileBlobResponse>> fileBlobs(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "") String userQuery,
@RequestParam(required = false) Long storagePolicyId,
@RequestParam(defaultValue = "") String objectKey,
@RequestParam(required = false) FileEntityType entityType) {
return ApiResponse.success(adminService.listFileBlobs(page, size, userQuery, storagePolicyId, objectKey, entityType));
}
@GetMapping("/shares")
public ApiResponse<PageResponse<AdminShareResponse>> shares(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "") String userQuery,
@RequestParam(defaultValue = "") String fileName,
@RequestParam(defaultValue = "") String token,
@RequestParam(required = false) Boolean passwordProtected,
@RequestParam(required = false) Boolean expired) {
return ApiResponse.success(adminService.listShares(page, size, userQuery, fileName, token, passwordProtected, expired));
}
@DeleteMapping("/shares/{shareId}")
public ApiResponse<Void> deleteShare(@PathVariable Long shareId) {
adminService.deleteShare(shareId);
return ApiResponse.success();
}
@GetMapping("/tasks")
public ApiResponse<PageResponse<AdminTaskResponse>> tasks(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "") String userQuery,
@RequestParam(required = false) BackgroundTaskType type,
@RequestParam(required = false) BackgroundTaskStatus status,
@RequestParam(required = false) BackgroundTaskFailureCategory failureCategory,
@RequestParam(required = false) AdminTaskLeaseState leaseState) {
return ApiResponse.success(adminService.listTasks(page, size, userQuery, type, status, failureCategory, leaseState));
}
@GetMapping("/tasks/{taskId}")
public ApiResponse<AdminTaskResponse> task(@PathVariable Long taskId) {
return ApiResponse.success(adminService.getTask(taskId));
}
@GetMapping("/storage-policies")
public ApiResponse<List<AdminStoragePolicyResponse>> storagePolicies() {
return ApiResponse.success(adminService.listStoragePolicies());

View File

@@ -0,0 +1,28 @@
package com.yoyuzh.admin;
import com.yoyuzh.files.core.FileEntityType;
import java.time.LocalDateTime;
public record AdminFileBlobResponse(
Long entityId,
Long blobId,
String objectKey,
FileEntityType entityType,
Long storagePolicyId,
Long size,
String contentType,
Integer referenceCount,
long linkedStoredFileCount,
long linkedOwnerCount,
String sampleOwnerUsername,
String sampleOwnerEmail,
Long createdByUserId,
String createdByUsername,
LocalDateTime createdAt,
LocalDateTime blobCreatedAt,
boolean blobMissing,
boolean orphanRisk,
boolean referenceMismatch
) {
}

View File

@@ -1,29 +1,42 @@
package com.yoyuzh.admin;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yoyuzh.auth.AuthTokenInvalidationService;
import com.yoyuzh.auth.PasswordPolicy;
import com.yoyuzh.auth.RefreshTokenService;
import com.yoyuzh.auth.RegistrationInviteService;
import com.yoyuzh.auth.User;
import com.yoyuzh.auth.UserRole;
import com.yoyuzh.auth.UserRepository;
import com.yoyuzh.auth.RefreshTokenService;
import com.yoyuzh.auth.UserRole;
import com.yoyuzh.common.BusinessException;
import com.yoyuzh.common.ErrorCode;
import com.yoyuzh.common.PageResponse;
import com.yoyuzh.files.core.FileBlobRepository;
import com.yoyuzh.config.RedisCacheNames;
import com.yoyuzh.files.core.FileEntity;
import com.yoyuzh.files.core.FileEntityRepository;
import com.yoyuzh.files.core.FileEntityType;
import com.yoyuzh.files.core.FileService;
import com.yoyuzh.files.core.StoredFile;
import com.yoyuzh.files.core.StoredFileEntityRepository;
import com.yoyuzh.files.core.StoredFileRepository;
import com.yoyuzh.files.core.FileBlobRepository;
import com.yoyuzh.files.policy.StoragePolicy;
import com.yoyuzh.files.policy.StoragePolicyRepository;
import com.yoyuzh.files.policy.StoragePolicyService;
import com.yoyuzh.files.share.FileShareLink;
import com.yoyuzh.files.share.FileShareLinkRepository;
import com.yoyuzh.files.tasks.BackgroundTask;
import com.yoyuzh.files.tasks.BackgroundTaskFailureCategory;
import com.yoyuzh.files.tasks.BackgroundTaskRepository;
import com.yoyuzh.files.tasks.BackgroundTaskService;
import com.yoyuzh.files.tasks.BackgroundTaskStatus;
import com.yoyuzh.files.tasks.BackgroundTaskType;
import com.yoyuzh.transfer.OfflineTransferSessionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
@@ -34,8 +47,12 @@ import org.springframework.util.StringUtils;
import java.security.SecureRandom;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@@ -47,6 +64,7 @@ public class AdminService {
private final FileService fileService;
private final PasswordEncoder passwordEncoder;
private final RefreshTokenService refreshTokenService;
private final AuthTokenInvalidationService authTokenInvalidationService;
private final RegistrationInviteService registrationInviteService;
private final OfflineTransferSessionRepository offlineTransferSessionRepository;
private final AdminMetricsService adminMetricsService;
@@ -54,7 +72,10 @@ public class AdminService {
private final StoragePolicyService storagePolicyService;
private final FileEntityRepository fileEntityRepository;
private final StoredFileEntityRepository storedFileEntityRepository;
private final BackgroundTaskRepository backgroundTaskRepository;
private final BackgroundTaskService backgroundTaskService;
private final FileShareLinkRepository fileShareLinkRepository;
private final ObjectMapper objectMapper;
private final SecureRandom secureRandom = new SecureRandom();
public AdminSummaryResponse getSummary() {
@@ -98,6 +119,92 @@ public class AdminService {
return new PageResponse<>(items, result.getTotalElements(), page, size);
}
public PageResponse<AdminFileBlobResponse> listFileBlobs(int page,
int size,
String userQuery,
Long storagePolicyId,
String objectKey,
FileEntityType entityType) {
Page<FileEntity> result = fileEntityRepository.searchAdminEntities(
normalizeQuery(userQuery),
storagePolicyId,
normalizeQuery(objectKey),
entityType,
PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"))
);
List<AdminFileBlobResponse> items = result.getContent().stream()
.map(this::toFileBlobResponse)
.toList();
return new PageResponse<>(items, result.getTotalElements(), page, size);
}
public PageResponse<AdminShareResponse> listShares(int page,
int size,
String userQuery,
String fileName,
String token,
Boolean passwordProtected,
Boolean expired) {
Page<FileShareLink> result = fileShareLinkRepository.searchAdminShares(
normalizeQuery(userQuery),
normalizeQuery(fileName),
normalizeQuery(token),
passwordProtected,
expired,
LocalDateTime.now(),
PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"))
);
List<AdminShareResponse> items = result.getContent().stream()
.map(this::toAdminShareResponse)
.toList();
return new PageResponse<>(items, result.getTotalElements(), page, size);
}
@Transactional
public void deleteShare(Long shareId) {
FileShareLink shareLink = fileShareLinkRepository.findById(shareId)
.orElseThrow(() -> new BusinessException(ErrorCode.FILE_NOT_FOUND, "share not found"));
fileShareLinkRepository.delete(shareLink);
}
public PageResponse<AdminTaskResponse> listTasks(int page,
int size,
String userQuery,
BackgroundTaskType type,
BackgroundTaskStatus status,
BackgroundTaskFailureCategory failureCategory,
AdminTaskLeaseState leaseState) {
String failureCategoryPattern = failureCategory == null
? null
: "\"failureCategory\":\"" + failureCategory.name() + "\"";
Page<BackgroundTask> result = backgroundTaskRepository.searchAdminTasks(
normalizeQuery(userQuery),
type,
status,
failureCategoryPattern,
leaseState == null ? null : leaseState.name(),
LocalDateTime.now(),
PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"))
);
Map<Long, User> ownerById = userRepository.findAllById(result.getContent().stream()
.map(BackgroundTask::getUserId)
.collect(Collectors.toSet()))
.stream()
.collect(Collectors.toMap(User::getId, user -> user));
List<AdminTaskResponse> items = result.getContent().stream()
.map(task -> toAdminTaskResponse(task, ownerById.get(task.getUserId())))
.toList();
return new PageResponse<>(items, result.getTotalElements(), page, size);
}
public AdminTaskResponse getTask(Long taskId) {
BackgroundTask task = backgroundTaskRepository.findById(taskId)
.orElseThrow(() -> new BusinessException(ErrorCode.FILE_NOT_FOUND, "task not found"));
User owner = userRepository.findById(task.getUserId()).orElse(null);
return toAdminTaskResponse(task, owner);
}
@Cacheable(cacheNames = RedisCacheNames.STORAGE_POLICIES, key = "'all'")
public List<AdminStoragePolicyResponse> listStoragePolicies() {
return storagePolicyRepository.findAll(Sort.by(Sort.Direction.DESC, "defaultPolicy")
.and(Sort.by(Sort.Direction.DESC, "enabled"))
@@ -108,6 +215,7 @@ public class AdminService {
}
@Transactional
@CacheEvict(cacheNames = RedisCacheNames.STORAGE_POLICIES, allEntries = true)
public AdminStoragePolicyResponse createStoragePolicy(AdminStoragePolicyUpsertRequest request) {
StoragePolicy policy = new StoragePolicy();
policy.setDefaultPolicy(false);
@@ -116,6 +224,7 @@ public class AdminService {
}
@Transactional
@CacheEvict(cacheNames = RedisCacheNames.STORAGE_POLICIES, allEntries = true)
public AdminStoragePolicyResponse updateStoragePolicy(Long policyId, AdminStoragePolicyUpsertRequest request) {
StoragePolicy policy = getRequiredStoragePolicy(policyId);
applyStoragePolicyUpsert(policy, request);
@@ -123,10 +232,11 @@ public class AdminService {
}
@Transactional
@CacheEvict(cacheNames = RedisCacheNames.STORAGE_POLICIES, allEntries = true)
public AdminStoragePolicyResponse updateStoragePolicyStatus(Long policyId, boolean enabled) {
StoragePolicy policy = getRequiredStoragePolicy(policyId);
if (policy.isDefaultPolicy() && !enabled) {
throw new BusinessException(ErrorCode.UNKNOWN, "默认存储策略不能停用");
throw new BusinessException(ErrorCode.UNKNOWN, "榛樿瀛樺偍绛栫暐涓嶈兘鍋滅敤");
}
policy.setEnabled(enabled);
return toStoragePolicyResponse(storagePolicyRepository.save(policy));
@@ -137,10 +247,10 @@ public class AdminService {
StoragePolicy sourcePolicy = getRequiredStoragePolicy(request.sourcePolicyId());
StoragePolicy targetPolicy = getRequiredStoragePolicy(request.targetPolicyId());
if (sourcePolicy.getId().equals(targetPolicy.getId())) {
throw new BusinessException(ErrorCode.UNKNOWN, "源存储策略和目标存储策略不能相同");
throw new BusinessException(ErrorCode.UNKNOWN, "婧愬瓨鍌ㄧ瓥鐣ュ拰鐩爣瀛樺偍绛栫暐涓嶈兘鐩稿悓");
}
if (!targetPolicy.isEnabled()) {
throw new BusinessException(ErrorCode.UNKNOWN, "目标存储策略必须处于启用状态");
throw new BusinessException(ErrorCode.UNKNOWN, "target storage policy must be enabled");
}
long candidateEntityCount = fileEntityRepository.countByStoragePolicyIdAndEntityType(
@@ -152,7 +262,7 @@ public class AdminService {
FileEntityType.VERSION
);
java.util.Map<String, Object> state = new java.util.LinkedHashMap<>();
Map<String, Object> state = new LinkedHashMap<>();
state.put("sourcePolicyId", sourcePolicy.getId());
state.put("sourcePolicyName", sourcePolicy.getName());
state.put("targetPolicyId", targetPolicy.getId());
@@ -164,7 +274,7 @@ public class AdminService {
state.put("entityType", FileEntityType.VERSION.name());
state.put("message", "storage policy migration skeleton queued; worker will validate and recount candidates without moving object data");
java.util.Map<String, Object> privateState = new java.util.LinkedHashMap<>(state);
Map<String, Object> privateState = new LinkedHashMap<>(state);
privateState.put("taskType", BackgroundTaskType.STORAGE_POLICY_MIGRATION.name());
return backgroundTaskService.createQueuedTask(
@@ -179,7 +289,7 @@ public class AdminService {
@Transactional
public void deleteFile(Long fileId) {
StoredFile storedFile = storedFileRepository.findById(fileId)
.orElseThrow(() -> new BusinessException(ErrorCode.FILE_NOT_FOUND, "文件不存在"));
.orElseThrow(() -> new BusinessException(ErrorCode.FILE_NOT_FOUND, "file not found"));
fileService.delete(storedFile.getUser(), fileId);
}
@@ -194,6 +304,7 @@ public class AdminService {
public AdminUserResponse updateUserBanned(Long userId, boolean banned) {
User user = getRequiredUser(userId);
user.setBanned(banned);
authTokenInvalidationService.revokeAccessTokensForUser(user.getId());
user.setActiveSessionId(UUID.randomUUID().toString());
user.setDesktopActiveSessionId(UUID.randomUUID().toString());
user.setMobileActiveSessionId(UUID.randomUUID().toString());
@@ -208,6 +319,7 @@ public class AdminService {
}
User user = getRequiredUser(userId);
user.setPasswordHash(passwordEncoder.encode(newPassword));
authTokenInvalidationService.revokeAccessTokensForUser(user.getId());
user.setActiveSessionId(UUID.randomUUID().toString());
user.setDesktopActiveSessionId(UUID.randomUUID().toString());
user.setMobileActiveSessionId(UUID.randomUUID().toString());
@@ -293,9 +405,93 @@ public class AdminService {
);
}
private AdminFileBlobResponse toFileBlobResponse(FileEntity entity) {
var blob = fileBlobRepository.findByObjectKey(entity.getObjectKey()).orElse(null);
long linkedStoredFileCount = storedFileEntityRepository.countByFileEntityId(entity.getId());
long linkedOwnerCount = storedFileEntityRepository.countDistinctOwnersByFileEntityId(entity.getId());
return new AdminFileBlobResponse(
entity.getId(),
blob == null ? null : blob.getId(),
entity.getObjectKey(),
entity.getEntityType(),
entity.getStoragePolicyId(),
entity.getSize(),
StringUtils.hasText(entity.getContentType()) ? entity.getContentType() : blob == null ? null : blob.getContentType(),
entity.getReferenceCount(),
linkedStoredFileCount,
linkedOwnerCount,
storedFileEntityRepository.findSampleOwnerUsernameByFileEntityId(entity.getId()),
storedFileEntityRepository.findSampleOwnerEmailByFileEntityId(entity.getId()),
entity.getCreatedBy() == null ? null : entity.getCreatedBy().getId(),
entity.getCreatedBy() == null ? null : entity.getCreatedBy().getUsername(),
entity.getCreatedAt(),
blob == null ? null : blob.getCreatedAt(),
blob == null,
linkedStoredFileCount == 0,
entity.getReferenceCount() == null || entity.getReferenceCount() != linkedStoredFileCount
);
}
private AdminShareResponse toAdminShareResponse(FileShareLink shareLink) {
StoredFile file = shareLink.getFile();
User owner = shareLink.getOwner();
boolean expired = shareLink.getExpiresAt() != null && shareLink.getExpiresAt().isBefore(LocalDateTime.now());
return new AdminShareResponse(
shareLink.getId(),
shareLink.getToken(),
shareLink.getShareNameOrDefault(),
shareLink.hasPassword(),
expired,
shareLink.getCreatedAt(),
shareLink.getExpiresAt(),
shareLink.getMaxDownloads(),
shareLink.getDownloadCountOrZero(),
shareLink.getViewCountOrZero(),
shareLink.isAllowImportEnabled(),
shareLink.isAllowDownloadEnabled(),
owner.getId(),
owner.getUsername(),
owner.getEmail(),
file.getId(),
file.getFilename(),
file.getPath(),
file.getContentType(),
file.getSize(),
file.isDirectory()
);
}
private AdminTaskResponse toAdminTaskResponse(BackgroundTask task, User owner) {
Map<String, Object> state = parseState(task.getPublicStateJson());
return new AdminTaskResponse(
task.getId(),
task.getType(),
task.getStatus(),
task.getUserId(),
owner == null ? null : owner.getUsername(),
owner == null ? null : owner.getEmail(),
task.getPublicStateJson(),
task.getCorrelationId(),
task.getErrorMessage(),
task.getAttemptCount(),
task.getMaxAttempts(),
task.getNextRunAt(),
task.getLeaseOwner(),
task.getLeaseExpiresAt(),
task.getHeartbeatAt(),
task.getCreatedAt(),
task.getUpdatedAt(),
task.getFinishedAt(),
readStringState(state, "failureCategory"),
readBooleanState(state, "retryScheduled"),
readStringState(state, "workerOwner"),
resolveLeaseState(task)
);
}
private void applyStoragePolicyUpsert(StoragePolicy policy, AdminStoragePolicyUpsertRequest request) {
if (policy.isDefaultPolicy() && !request.enabled()) {
throw new BusinessException(ErrorCode.UNKNOWN, "默认存储策略不能停用");
throw new BusinessException(ErrorCode.UNKNOWN, "榛樿瀛樺偍绛栫暐涓嶈兘鍋滅敤");
}
validateStoragePolicyRequest(request);
policy.setName(request.name().trim());
@@ -313,12 +509,12 @@ public class AdminService {
private User getRequiredUser(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.UNKNOWN, "用户不存在"));
.orElseThrow(() -> new BusinessException(ErrorCode.UNKNOWN, "user not found"));
}
private StoragePolicy getRequiredStoragePolicy(Long policyId) {
return storagePolicyRepository.findById(policyId)
.orElseThrow(() -> new BusinessException(ErrorCode.UNKNOWN, "存储策略不存在"));
.orElseThrow(() -> new BusinessException(ErrorCode.UNKNOWN, "storage policy not found"));
}
private String normalizeQuery(String query) {
@@ -342,14 +538,51 @@ public class AdminService {
return prefix.trim();
}
private Map<String, Object> parseState(String json) {
if (!StringUtils.hasText(json)) {
return Map.of();
}
try {
return objectMapper.readValue(json, new TypeReference<LinkedHashMap<String, Object>>() {
});
} catch (JsonProcessingException ex) {
return Map.of();
}
}
private String readStringState(Map<String, Object> state, String key) {
Object value = state.get(key);
return value == null ? null : String.valueOf(value);
}
private Boolean readBooleanState(Map<String, Object> state, String key) {
Object value = state.get(key);
if (value instanceof Boolean boolValue) {
return boolValue;
}
if (value instanceof String stringValue) {
return Boolean.parseBoolean(stringValue);
}
return null;
}
private AdminTaskLeaseState resolveLeaseState(BackgroundTask task) {
if (!StringUtils.hasText(task.getLeaseOwner()) || task.getLeaseExpiresAt() == null) {
return AdminTaskLeaseState.NONE;
}
return task.getLeaseExpiresAt().isBefore(LocalDateTime.now())
? AdminTaskLeaseState.EXPIRED
: AdminTaskLeaseState.ACTIVE;
}
private void validateStoragePolicyRequest(AdminStoragePolicyUpsertRequest request) {
if (request.type() == com.yoyuzh.files.policy.StoragePolicyType.LOCAL
&& request.credentialMode() != com.yoyuzh.files.policy.StoragePolicyCredentialMode.NONE) {
throw new BusinessException(ErrorCode.UNKNOWN, "本地存储策略必须使用 NONE 凭证模式");
throw new BusinessException(ErrorCode.UNKNOWN, "鏈湴瀛樺偍绛栫暐蹇呴』浣跨敤 NONE 鍑瘉妯″紡");
}
if (request.type() == com.yoyuzh.files.policy.StoragePolicyType.S3_COMPATIBLE
&& !StringUtils.hasText(request.bucketName())) {
throw new BusinessException(ErrorCode.UNKNOWN, "S3 存储策略必须提供 bucketName");
throw new BusinessException(ErrorCode.UNKNOWN, "S3 瀛樺偍绛栫暐蹇呴』鎻愪緵 bucketName");
}
}

View File

@@ -0,0 +1,28 @@
package com.yoyuzh.admin;
import java.time.LocalDateTime;
public record AdminShareResponse(
Long id,
String token,
String shareName,
boolean passwordProtected,
boolean expired,
LocalDateTime createdAt,
LocalDateTime expiresAt,
Integer maxDownloads,
long downloadCount,
long viewCount,
boolean allowImport,
boolean allowDownload,
Long ownerId,
String ownerUsername,
String ownerEmail,
Long fileId,
String fileName,
String filePath,
String fileContentType,
long fileSize,
boolean directory
) {
}

View File

@@ -0,0 +1,7 @@
package com.yoyuzh.admin;
public enum AdminTaskLeaseState {
ACTIVE,
EXPIRED,
NONE
}

View File

@@ -0,0 +1,32 @@
package com.yoyuzh.admin;
import com.yoyuzh.files.tasks.BackgroundTaskStatus;
import com.yoyuzh.files.tasks.BackgroundTaskType;
import java.time.LocalDateTime;
public record AdminTaskResponse(
Long id,
BackgroundTaskType type,
BackgroundTaskStatus status,
Long userId,
String ownerUsername,
String ownerEmail,
String publicStateJson,
String correlationId,
String errorMessage,
Integer attemptCount,
Integer maxAttempts,
LocalDateTime nextRunAt,
String leaseOwner,
LocalDateTime leaseExpiresAt,
LocalDateTime heartbeatAt,
LocalDateTime createdAt,
LocalDateTime updatedAt,
LocalDateTime finishedAt,
String failureCategory,
Boolean retryScheduled,
String workerOwner,
AdminTaskLeaseState leaseState
) {
}

View File

@@ -1,6 +1,11 @@
package com.yoyuzh.files.core;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;
@@ -12,4 +17,28 @@ public interface FileEntityRepository extends JpaRepository<FileEntity, Long> {
long countByStoragePolicyIdAndEntityType(Long storagePolicyId, FileEntityType entityType);
List<FileEntity> findByStoragePolicyIdAndEntityTypeOrderByIdAsc(Long storagePolicyId, FileEntityType entityType);
@EntityGraph(attributePaths = {"createdBy"})
@Query("""
select entity from FileEntity entity
where (:storagePolicyId is null or entity.storagePolicyId = :storagePolicyId)
and (:entityType is null or entity.entityType = :entityType)
and (:objectKey is null or :objectKey = ''
or lower(entity.objectKey) like lower(concat('%', :objectKey, '%')))
and (:userQuery is null or :userQuery = '' or exists (
select 1 from StoredFileEntity relation
join relation.storedFile storedFile
join storedFile.user owner
where relation.fileEntity = entity
and (
lower(owner.username) like lower(concat('%', :userQuery, '%'))
or lower(owner.email) like lower(concat('%', :userQuery, '%'))
)
))
""")
Page<FileEntity> searchAdminEntities(@Param("userQuery") String userQuery,
@Param("storagePolicyId") Long storagePolicyId,
@Param("objectKey") String objectKey,
@Param("entityType") FileEntityType entityType,
Pageable pageable);
}

View File

@@ -14,4 +14,31 @@ public interface StoredFileEntityRepository extends JpaRepository<StoredFileEnti
""")
long countDistinctStoredFilesByStoragePolicyIdAndEntityType(@Param("storagePolicyId") Long storagePolicyId,
@Param("entityType") FileEntityType entityType);
long countByFileEntityId(Long fileEntityId);
@Query("""
select count(distinct relation.storedFile.user.id)
from StoredFileEntity relation
where relation.fileEntity.id = :fileEntityId
""")
long countDistinctOwnersByFileEntityId(@Param("fileEntityId") Long fileEntityId);
@Query("""
select min(owner.username)
from StoredFileEntity relation
join relation.storedFile storedFile
join storedFile.user owner
where relation.fileEntity.id = :fileEntityId
""")
String findSampleOwnerUsernameByFileEntityId(@Param("fileEntityId") Long fileEntityId);
@Query("""
select min(owner.email)
from StoredFileEntity relation
join relation.storedFile storedFile
join storedFile.user owner
where relation.fileEntity.id = :fileEntityId
""")
String findSampleOwnerEmailByFileEntityId(@Param("fileEntityId") Long fileEntityId);
}

View File

@@ -4,6 +4,10 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.time.LocalDateTime;
import java.util.Optional;
@@ -17,4 +21,31 @@ public interface FileShareLinkRepository extends JpaRepository<FileShareLink, Lo
@EntityGraph(attributePaths = {"owner", "file", "file.user", "file.blob"})
Optional<FileShareLink> findByIdAndOwnerId(Long id, Long ownerId);
@EntityGraph(attributePaths = {"owner", "file", "file.user", "file.primaryEntity", "file.blob"})
@Query("""
select share from FileShareLink share
join share.owner owner
join share.file file
where (:userQuery is null or :userQuery = ''
or lower(owner.username) like lower(concat('%', :userQuery, '%'))
or lower(owner.email) like lower(concat('%', :userQuery, '%')))
and (:fileName is null or :fileName = ''
or lower(file.filename) like lower(concat('%', :fileName, '%')))
and (:token is null or :token = ''
or lower(share.token) like lower(concat('%', :token, '%')))
and (:passwordProtected is null
or (:passwordProtected = true and share.passwordHash is not null and share.passwordHash <> '')
or (:passwordProtected = false and (share.passwordHash is null or share.passwordHash = '')))
and (:expired is null
or (:expired = true and share.expiresAt is not null and share.expiresAt < :now)
or (:expired = false and (share.expiresAt is null or share.expiresAt >= :now)))
""")
Page<FileShareLink> searchAdminShares(@Param("userQuery") String userQuery,
@Param("fileName") String fileName,
@Param("token") String token,
@Param("passwordProtected") Boolean passwordProtected,
@Param("expired") Boolean expired,
@Param("now") LocalDateTime now,
Pageable pageable);
}

View File

@@ -15,8 +15,36 @@ public interface BackgroundTaskRepository extends JpaRepository<BackgroundTask,
Page<BackgroundTask> findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable);
@Query("""
select task from BackgroundTask task
where (:userQuery is null or :userQuery = '' or exists (
select 1 from User owner
where owner.id = task.userId
and (
lower(owner.username) like lower(concat('%', :userQuery, '%'))
or lower(owner.email) like lower(concat('%', :userQuery, '%'))
)
))
and (:type is null or task.type = :type)
and (:status is null or task.status = :status)
and (:failureCategoryPattern is null or lower(task.publicStateJson) like lower(concat('%', :failureCategoryPattern, '%')))
and (:leaseState is null
or (:leaseState = 'ACTIVE' and task.leaseOwner is not null and task.leaseExpiresAt is not null and task.leaseExpiresAt > :now)
or (:leaseState = 'EXPIRED' and task.leaseOwner is not null and task.leaseExpiresAt is not null and task.leaseExpiresAt <= :now)
or (:leaseState = 'NONE' and (task.leaseOwner is null or task.leaseExpiresAt is null)))
""")
Page<BackgroundTask> searchAdminTasks(@Param("userQuery") String userQuery,
@Param("type") BackgroundTaskType type,
@Param("status") BackgroundTaskStatus status,
@Param("failureCategoryPattern") String failureCategoryPattern,
@Param("leaseState") String leaseState,
@Param("now") LocalDateTime now,
Pageable pageable);
Optional<BackgroundTask> findByIdAndUserId(Long id, Long userId);
boolean existsByCorrelationId(String correlationId);
List<BackgroundTask> findByStatusOrderByCreatedAtAsc(BackgroundTaskStatus status, Pageable pageable);
List<BackgroundTask> findByStatusOrderByUpdatedAtAsc(BackgroundTaskStatus status);