Commit 916cc368 by guoxuejian Committed by gdj

Implement OSS browser features: add methods for listing folders, checking object…

Implement OSS browser features: add methods for listing folders, checking object existence, and copying objects; create OssBrowserController and FolderTreeDTO.
parent 81465cd5
......@@ -5,6 +5,8 @@ import com.dji.sdk.cloudapi.storage.OssTypeEnum;
import java.io.InputStream;
import java.net.URL;
import java.util.List;
import java.util.Map;
/**
* @author sean
......@@ -48,4 +50,37 @@ public interface IOssService {
void putObject(String bucket, String objectKey, InputStream input);
void createClient();
// ============= New methods for OSS browser =============
/**
* List folders at given prefix.
* @param bucket bucket name
* @param prefix prefix path
* @return list of folder info (name, prefix)
*/
default List<Map<String, String>> listFolders(String bucket, String prefix) {
throw new UnsupportedOperationException("Not implemented");
}
/**
* Check if object exists.
* @param bucket bucket name
* @param objectKey object key
* @return true if exists
*/
default Boolean objectExists(String bucket, String objectKey) {
throw new UnsupportedOperationException("Not implemented");
}
/**
* Copy object from source to destination.
* @param bucket bucket name
* @param sourceKey source object key
* @param destKey destination object key
* @return true if successful
*/
default Boolean copyObject(String bucket, String sourceKey, String destKey) {
throw new UnsupportedOperationException("Not implemented");
}
}
......@@ -5,6 +5,7 @@ import com.dji.sample.component.oss.service.IOssService;
import com.dji.sdk.cloudapi.storage.CredentialsToken;
import com.dji.sdk.cloudapi.storage.OssTypeEnum;
import io.minio.*;
import io.minio.messages.Item;
import io.minio.credentials.AssumeRoleProvider;
import io.minio.credentials.Credentials;
import io.minio.errors.*;
......@@ -18,7 +19,8 @@ import java.io.InputStream;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author sean
......@@ -120,4 +122,78 @@ public class MinIOServiceImpl implements IOssService {
.region(OssConfiguration.region)
.build();
}
// ============= New methods for OSS browser =============
@Override
public List<Map<String, String>> listFolders(String bucket, String prefix) {
List<Map<String, String>> folders = new ArrayList<>();
try {
// Normalize prefix
String normalizedPrefix = prefix == null ? "" : prefix;
if (!normalizedPrefix.isEmpty() && !normalizedPrefix.endsWith("/")) {
normalizedPrefix += "/";
}
Iterable<Result<Item>> results = client.listObjects(
ListObjectsArgs.builder()
.bucket(bucket)
.prefix(normalizedPrefix)
.delimiter("/")
.build()
);
for (Result<Item> result : results) {
Item item = result.get();
if (item.isDir()) {
String folderPath = item.objectName();
// Extract folder name from path
String folderName = folderPath.substring(normalizedPrefix.length());
if (folderName.endsWith("/")) {
folderName = folderName.substring(0, folderName.length() - 1);
}
Map<String, String> folderInfo = new HashMap<>();
folderInfo.put("name", folderName);
folderInfo.put("prefix", folderPath);
folders.add(folderInfo);
}
}
} catch (MinioException | NoSuchAlgorithmException | IOException | InvalidKeyException e) {
log.error("Failed to list folders: {}", e.getMessage());
}
return folders;
}
@Override
public Boolean objectExists(String bucket, String objectKey) {
try {
client.statObject(
StatObjectArgs.builder()
.bucket(bucket)
.object(objectKey)
.build()
);
return true;
} catch (Exception e) {
return false;
}
}
@Override
public Boolean copyObject(String bucket, String sourceKey, String destKey) {
try {
client.copyObject(
CopyObjectArgs.builder()
.source(CopySource.builder().bucket(bucket).object(sourceKey).build())
.bucket(bucket)
.object(destKey)
.build()
);
return true;
} catch (MinioException | NoSuchAlgorithmException | IOException | InvalidKeyException e) {
log.error("Failed to copy object: {}", e.getMessage());
return false;
}
}
}
......@@ -10,8 +10,7 @@ import org.springframework.util.StringUtils;
import java.io.InputStream;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.*;
/**
* @author sean
......@@ -65,4 +64,27 @@ public class OssServiceContext {
void createClient() {
this.ossService.createClient();
}
// ============= New methods for OSS browser =============
public List<Map<String, String>> listFolders(String bucket, String prefix) {
if (this.ossService == null) {
return new ArrayList<>();
}
return this.ossService.listFolders(bucket, prefix);
}
public Boolean objectExists(String bucket, String objectKey) {
if (this.ossService == null) {
return false;
}
return this.ossService.objectExists(bucket, objectKey);
}
public Boolean copyObject(String bucket, String sourceKey, String destKey) {
if (this.ossService == null) {
return false;
}
return this.ossService.copyObject(bucket, sourceKey, destKey);
}
}
package com.dji.sample.media.controller;
import com.dji.sample.common.error.CommonErrorEnum;
import com.dji.sample.media.model.FolderTreeDTO;
import com.dji.sample.media.model.MediaFileDTO;
import com.dji.sample.media.service.IFileService;
import com.dji.sdk.common.HttpResultResponse;
import com.dji.sdk.common.PaginationData;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URL;
import java.util.List;
/**
* Controller for OSS browser operations (files, folders, move, rename, delete).
* @author dji
* @version 1.0
* @date 2026/01/27
*/
@Slf4j
@RestController
@RequestMapping("${url.media.prefix}${url.media.version}/oss")
public class OssBrowserController {
@Autowired
private IFileService fileService;
/**
* Get files with pagination and filters.
* @param page page number
* @param pageSize page size
* @param jobId job id filter
* @param drone drone filter
* @param workspaceId workspace id
* @return paginated file list
*/
@GetMapping("/{workspace_id}/files")
public HttpResultResponse getFiles(
@RequestParam(defaultValue = "1") Long page,
@RequestParam(name = "page_size", defaultValue = "20") Long pageSize,
@RequestParam(name = "job_id", required = false) String jobId,
@RequestParam(required = false) String drone,
@PathVariable(name = "workspace_id") String workspaceId
) {
PaginationData<MediaFileDTO> data = fileService.getMediaFilesPaginationByWorkspaceId(
workspaceId, jobId, drone, page, pageSize
);
return HttpResultResponse.success(data);
}
/**
* Get folder tree for the folder picker.
* @param workspaceId workspace id
* @param prefix path prefix
* @return folder tree list
*/
@GetMapping("/{workspace_id}/folders-tree")
public HttpResultResponse getFolderTree(
@PathVariable(name = "workspace_id") String workspaceId,
@RequestParam(required = false, defaultValue = "") String prefix
) {
List<FolderTreeDTO> folders = fileService.getFolderTree(workspaceId, prefix);
return HttpResultResponse.success(folders);
}
/**
* Download a file by redirecting to presigned URL.
* @param workspaceId workspace id
* @param fileId file id
* @param response http response for redirect
*/
@GetMapping("/{workspace_id}/file/{file_id}/download")
public void downloadFile(
@PathVariable(name = "workspace_id") String workspaceId,
@PathVariable(name = "file_id") String fileId,
HttpServletResponse response
) {
try {
URL url = fileService.getObjectUrl(workspaceId, fileId);
response.sendRedirect(url.toString());
} catch (IOException e) {
log.error("Download redirect failed: {}", e.getMessage());
}
}
/**
* Delete a media file.
* @param workspaceId workspace id
* @param fileId file id
* @return success status
*/
@DeleteMapping("/{workspace_id}/file/{file_id}")
public HttpResultResponse deleteFile(
@PathVariable(name = "workspace_id") String workspaceId,
@PathVariable(name = "file_id") String fileId
) {
Boolean success = fileService.deleteMediaFile(workspaceId, fileId);
if (success) {
return HttpResultResponse.success(true);
} else {
return HttpResultResponse.error("删除失败");
}
}
/**
* Rename a media file.
* @param workspaceId workspace id
* @param fileId file id
* @param request rename request with new name
* @return success status
*/
@PutMapping("/{workspace_id}/file/{file_id}/rename")
public HttpResultResponse renameFile(
@PathVariable(name = "workspace_id") String workspaceId,
@PathVariable(name = "file_id") String fileId,
@RequestBody RenameRequest request
) {
Boolean success = fileService.renameMediaFile(workspaceId, fileId, request.getNewName());
if (success) {
return HttpResultResponse.success(true);
} else {
return HttpResultResponse.error("重命名失败,目标文件名可能已存在");
}
}
/**
* Move a media file to a new folder.
* @param workspaceId workspace id
* @param fileId file id
* @param request move request with target path
* @return success status
*/
@PutMapping("/{workspace_id}/file/{file_id}/move")
public HttpResultResponse moveFile(
@PathVariable(name = "workspace_id") String workspaceId,
@PathVariable(name = "file_id") String fileId,
@RequestBody MoveRequest request
) {
Boolean success = fileService.moveMediaFile(workspaceId, fileId, request.getTargetPath());
if (success) {
return HttpResultResponse.success(true);
} else {
return HttpResultResponse.error("移动失败,目标路径可能已存在同名文件");
}
}
// ============= Request DTOs =============
@Data
public static class RenameRequest {
private String newName;
}
@Data
public static class MoveRequest {
private String targetPath;
}
}
package com.dji.sample.media.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* Folder tree structure for OSS browser.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FolderTreeDTO {
/**
* Unique key for the folder (used as tree node key)
*/
private String key;
/**
* Display title (folder name)
*/
private String title;
/**
* Full path of the folder
*/
private String path;
/**
* Whether this folder has children (for lazy loading)
*/
private Boolean isLeaf;
/**
* Children folders (null for lazy loading)
*/
private List<FolderTreeDTO> children;
}
package com.dji.sample.media.service;
import com.dji.sample.media.model.MediaFileDTO;
import com.dji.sample.media.model.FolderTreeDTO;
import com.dji.sdk.cloudapi.media.MediaUploadCallbackRequest;
import com.dji.sdk.common.PaginationData;
......@@ -72,4 +73,40 @@ public interface IFileService {
* @return
*/
List<MediaFileDTO> getFilesByWorkspaceAndJobId(String workspaceId, String jobId);
// ============= New methods for OSS browser =============
/**
* Get folder tree structure from file paths.
* @param workspaceId workspace id
* @param prefix optional prefix to filter
* @return folder tree
*/
List<FolderTreeDTO> getFolderTree(String workspaceId, String prefix);
/**
* Delete a media file.
* @param workspaceId workspace id
* @param fileId file id
* @return success
*/
Boolean deleteMediaFile(String workspaceId, String fileId);
/**
* Rename a media file.
* @param workspaceId workspace id
* @param fileId file id
* @param newName new file name
* @return success
*/
Boolean renameMediaFile(String workspaceId, String fileId, String newName);
/**
* Move a media file to a new path.
* @param workspaceId workspace id
* @param fileId file id
* @param targetPath target folder path
* @return success
*/
Boolean moveMediaFile(String workspaceId, String fileId, String targetPath);
}
......@@ -9,6 +9,7 @@ import com.dji.sample.manage.service.IDeviceDictionaryService;
import com.dji.sample.media.dao.IFileMapper;
import com.dji.sample.media.model.MediaFileDTO;
import com.dji.sample.media.model.MediaFileEntity;
import com.dji.sample.media.model.FolderTreeDTO;
import com.dji.sample.media.service.IFileService;
import com.dji.sdk.cloudapi.device.DeviceEnum;
import com.dji.sdk.cloudapi.media.MediaSubFileTypeEnum;
......@@ -24,9 +25,7 @@ import java.net.URL;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.*;
import java.util.stream.Collectors;
/**
......@@ -184,4 +183,164 @@ public class FileServiceImpl implements IFileService {
return builder.build();
}
// ============= New methods for OSS browser =============
@Override
public List<FolderTreeDTO> getFolderTree(String workspaceId, String prefix) {
// Get all distinct file paths from database
List<MediaFileEntity> files = mapper.selectList(
new LambdaQueryWrapper<MediaFileEntity>()
.eq(MediaFileEntity::getWorkspaceId, workspaceId)
.select(MediaFileEntity::getFilePath)
);
// Normalize prefix
String normalizedPrefix = prefix == null ? "" : prefix;
if (!normalizedPrefix.isEmpty() && !normalizedPrefix.endsWith("/")) {
normalizedPrefix += "/";
}
// Extract unique folders at the current prefix level
Set<String> folderSet = new LinkedHashSet<>();
for (MediaFileEntity file : files) {
String filePath = file.getFilePath();
if (filePath == null) continue;
// Ensure path starts with prefix
if (!filePath.startsWith(normalizedPrefix)) continue;
// Get the remaining path after prefix
String remaining = filePath.substring(normalizedPrefix.length());
if (remaining.isEmpty()) continue;
// Get the first folder in the remaining path
int slashIndex = remaining.indexOf('/');
if (slashIndex > 0) {
String folderName = remaining.substring(0, slashIndex);
folderSet.add(folderName);
}
}
// Convert to FolderTreeDTO list
final String finalPrefix = normalizedPrefix;
return folderSet.stream()
.map(folderName -> {
String fullPath = finalPrefix + folderName + "/";
return FolderTreeDTO.builder()
.key(fullPath)
.title(folderName)
.path(fullPath)
.isLeaf(false) // Assume not leaf for lazy loading
.children(null)
.build();
})
.collect(Collectors.toList());
}
@Override
public Boolean deleteMediaFile(String workspaceId, String fileId) {
Optional<MediaFileEntity> mediaFileOpt = getMediaByFileId(workspaceId, fileId);
if (mediaFileOpt.isEmpty()) {
return false;
}
MediaFileEntity mediaFile = mediaFileOpt.get();
try {
// Delete from OSS
ossService.deleteObject(OssConfiguration.bucket, mediaFile.getObjectKey());
// Delete from database
mapper.delete(new LambdaQueryWrapper<MediaFileEntity>()
.eq(MediaFileEntity::getWorkspaceId, workspaceId)
.eq(MediaFileEntity::getFileId, fileId)
);
return true;
} catch (Exception e) {
return false;
}
}
@Override
public Boolean renameMediaFile(String workspaceId, String fileId, String newName) {
Optional<MediaFileEntity> mediaFileOpt = getMediaByFileId(workspaceId, fileId);
if (mediaFileOpt.isEmpty()) {
return false;
}
MediaFileEntity mediaFile = mediaFileOpt.get();
String oldObjectKey = mediaFile.getObjectKey();
// Extract folder and create new key
int lastSlash = oldObjectKey.lastIndexOf('/');
String folder = lastSlash >= 0 ? oldObjectKey.substring(0, lastSlash + 1) : "";
String newObjectKey = folder + newName;
// Check if target already exists
if (ossService.objectExists(OssConfiguration.bucket, newObjectKey)) {
return false; // File already exists
}
try {
// Copy to new name
ossService.copyObject(OssConfiguration.bucket, oldObjectKey, newObjectKey);
// Delete old file
ossService.deleteObject(OssConfiguration.bucket, oldObjectKey);
// Update database
MediaFileEntity updateEntity = new MediaFileEntity();
updateEntity.setObjectKey(newObjectKey);
updateEntity.setFileName(newName);
mapper.update(updateEntity, new LambdaQueryWrapper<MediaFileEntity>()
.eq(MediaFileEntity::getWorkspaceId, workspaceId)
.eq(MediaFileEntity::getFileId, fileId)
);
return true;
} catch (Exception e) {
return false;
}
}
@Override
public Boolean moveMediaFile(String workspaceId, String fileId, String targetPath) {
Optional<MediaFileEntity> mediaFileOpt = getMediaByFileId(workspaceId, fileId);
if (mediaFileOpt.isEmpty()) {
return false;
}
MediaFileEntity mediaFile = mediaFileOpt.get();
String oldObjectKey = mediaFile.getObjectKey();
String fileName = mediaFile.getFileName();
// Ensure target path ends with /
String normalizedTargetPath = targetPath;
if (!normalizedTargetPath.isEmpty() && !normalizedTargetPath.endsWith("/")) {
normalizedTargetPath += "/";
}
String newObjectKey = normalizedTargetPath + fileName;
// Check if target already exists
if (ossService.objectExists(OssConfiguration.bucket, newObjectKey)) {
return false; // File already exists at target
}
try {
// Copy to new location
ossService.copyObject(OssConfiguration.bucket, oldObjectKey, newObjectKey);
// Delete old file
ossService.deleteObject(OssConfiguration.bucket, oldObjectKey);
// Update database
MediaFileEntity updateEntity = new MediaFileEntity();
updateEntity.setObjectKey(newObjectKey);
updateEntity.setFilePath(normalizedTargetPath);
mapper.update(updateEntity, new LambdaQueryWrapper<MediaFileEntity>()
.eq(MediaFileEntity::getWorkspaceId, workspaceId)
.eq(MediaFileEntity::getFileId, fileId)
);
return true;
} catch (Exception e) {
return false;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment