添加文件删除功能

This commit is contained in:
root 2025-12-19 16:32:20 +08:00
parent c565eb25af
commit 71a5ea5f41
9 changed files with 111 additions and 5 deletions

2
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,2 @@
{
}

View File

@ -41,6 +41,7 @@ func main() {
initMultipartHandler := handlers.NewInitMultipartHandler(s3Repo)
uploadPartHandler := handlers.NewUploadPartHandler(s3Repo)
completeMultipartHandler := handlers.NewCompleteMultipartHandler(s3Repo)
deleteFileHandler := handlers.NewDeleteFileHandler(s3Repo)
// Register Handlers
mediator.Register[handlers.UploadFileCommand, string](m, uploadHandler)
@ -53,6 +54,7 @@ func main() {
mediator.Register[handlers.InitMultipartCommand, string](m, initMultipartHandler)
mediator.Register[handlers.UploadPartCommand, string](m, uploadPartHandler)
mediator.Register[handlers.CompleteMultipartCommand, string](m, completeMultipartHandler)
mediator.Register[handlers.DeleteFileCommand, string](m, deleteFileHandler)
// Validators
uploadValidator := validators.NewUploadFileValidator()
@ -72,7 +74,7 @@ func main() {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
@ -87,6 +89,9 @@ func main() {
r.GET("/files/list", fileEndpoint.ListFiles)
r.GET("/files/preview", fileEndpoint.GetPreviewURL)
// Delete file
r.DELETE("/files/delete", fileEndpoint.DeleteFile)
// Multipart Upload
r.POST("/files/multipart/init", fileEndpoint.InitMultipart)
r.PUT("/files/multipart/part", fileEndpoint.UploadPart)

View File

@ -321,6 +321,42 @@ func (e *FileEndpoint) CompleteMultipart(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"location": result})
}
// DeleteFile godoc
// @Summary 删除文件
// @Description 从指定的存储桶删除文件
// @Tags 文件操作
// @Accept json
// @Produce json
// @Param request body requests.DeleteFileRequest true "请求参数"
// @Success 200 {object} map[string]string "删除成功消息"
// @Failure 400 {object} map[string]string "参数错误"
// @Failure 500 {object} map[string]string "服务器内部错误"
// @Router /files/delete [delete]
func (e *FileEndpoint) DeleteFile(c *gin.Context) {
var req requests.DeleteFileRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := e.NewFeaturesValidator.ValidateDeleteFile(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
cmd := handlers.DeleteFileCommand{
BucketName: req.BucketName,
ObjectKey: req.ObjectKey,
}
result, err := mediator.Send[handlers.DeleteFileCommand, string](e.Mediator, c.Request.Context(), cmd)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": result})
}
func handleError(c *gin.Context, err error) {
if be, ok := err.(*common.BusinessException); ok {
c.JSON(be.Code, gin.H{"error": be.Message})

View File

@ -2,9 +2,10 @@ package handlers
import (
"context"
"file-system/internal/common"
"file-sys
"file-system/internal/domain/repository"
"io"
"time"
"file-system/internal/common"
)
@ -67,3 +68,25 @@ func NewGetFilePreviewHandler(repo repository.FileRepository) *GetFilePreviewHan
func (h *GetFilePreviewHandler) Handle(ctx context.Context, q GetFilePreviewQuery) (string, error) {
return h.Repo.GeneratePresignedURL(ctx, q.BucketName, q.ObjectKey, q.Expiry)
}
// DeleteFileCommand 删除文件命令
type DeleteFileCommand struct {
BucketName string
ObjectKey string
}
type DeleteFileHandler struct {
Repo repository.FileRepository
}
func NewDeleteFileHandler(repo repository.FileRepository) *DeleteFileHandler {
return &DeleteFileHandler{Repo: repo}
}
func (h *DeleteFileHandler) Handle(ctx context.Context, cmd DeleteFileCommand) (string, error) {
err := h.Repo.DeleteFile(ctx, cmd.BucketName, cmd.ObjectKey)
if err != nil {
return "", err
}
return "File deleted successfully", nil
}

View File

@ -36,3 +36,8 @@ type CompleteMultipartRequest struct {
UploadId string `json:"upload_id"`
Parts []common.Part `json:"parts"`
}
type DeleteFileRequest struct {
BucketName string `json:"bucket_name"`
ObjectKey string `json:"object_key"`
}

View File

@ -48,3 +48,10 @@ func (v *NewFeaturesValidator) ValidateCompleteMultipart(req *requests.CompleteM
}
return nil
}
func (v *NewFeaturesValidator) ValidateDeleteFile(req *requests.DeleteFileRequest) error {
if req.BucketName == "" || req.ObjectKey == "" {
return common.NewBusinessException("Bucket name and Object key are required")
}
return nil
}

View File

@ -15,7 +15,7 @@ type FileInfo struct {
}
type ListFilesResult struct {
Files []FileInfo
Files []FileInfo
NextContinuationToken *string
}
@ -30,7 +30,10 @@ type FileRepository interface {
// 新增功能
ListObjectsV2(ctx context.Context, bucketName string, prefix string, maxKeys int32, continuationToken *string) (*ListFilesResult, error)
GeneratePresignedURL(ctx context.Context, bucketName string, objectKey string, expiry time.Duration) (string, error)
// 删除文件
DeleteFile(ctx context.Context, bucketName string, objectKey string) error
// 分片上传
CreateMultipartUpload(ctx context.Context, bucketName string, objectKey string) (string, error)
UploadPart(ctx context.Context, bucketName string, objectKey string, uploadId string, partNumber int32, data io.Reader) (string, error)

View File

@ -85,6 +85,15 @@ func (r *S3FileRepository) ListObjects(ctx context.Context, bucketName string) (
return objects, nil
}
// DeleteFile 删除文件
func (r *S3FileRepository) DeleteFile(ctx context.Context, bucketName string, objectKey string) error {
_, err := r.client.Client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
})
return err
}
// ListObjectsV2 分页列出文件
func (r *S3FileRepository) ListObjectsV2(ctx context.Context, bucketName string, prefix string, maxKeys int32, continuationToken *string) (*repository.ListFilesResult, error) {
input := &s3.ListObjectsV2Input{

View File

@ -113,6 +113,9 @@
<span class="action-btn text-success" @click="downloadFile(file.Key)" title="下载">
<i class="fas fa-download"></i>
</span>
<span class="action-btn text-danger" @click="deleteFile(file.Key)" title="删除">
<i class="fas fa-trash-alt"></i>
</span>
</td>
</tr>
<tr v-if="files.length === 0 && !loadingFiles">
@ -493,6 +496,19 @@
window.open(url, '_blank');
};
// Delete
const deleteFile = async (key) => {
if (!confirm(`确定要删除文件 "${key}" 吗?此操作不可恢复!`)) return;
try {
await api.delete('/files/delete', {
data: { bucket_name: currentBucket.value, object_key: key }
});
loadFilesWrapped(currentToken.value);
} catch (err) {
alert('删除失败: ' + (err.response?.data?.error || err.message));
}
};
// Utils
const formatSize = (bytes) => {
if (bytes === 0) return '0 B';
@ -531,7 +547,7 @@
loadBuckets, createBucket, selectBucket, refreshFiles: () => loadFilesInitial(),
nextPage: nextP, prevPage,
triggerFileInput, handleFileSelect,
previewFile, downloadFile,
previewFile, downloadFile, deleteFile,
formatSize, formatDate, getFileIcon, getProgressBarClass
};
}