添加文件删除功能

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) initMultipartHandler := handlers.NewInitMultipartHandler(s3Repo)
uploadPartHandler := handlers.NewUploadPartHandler(s3Repo) uploadPartHandler := handlers.NewUploadPartHandler(s3Repo)
completeMultipartHandler := handlers.NewCompleteMultipartHandler(s3Repo) completeMultipartHandler := handlers.NewCompleteMultipartHandler(s3Repo)
deleteFileHandler := handlers.NewDeleteFileHandler(s3Repo)
// Register Handlers // Register Handlers
mediator.Register[handlers.UploadFileCommand, string](m, uploadHandler) mediator.Register[handlers.UploadFileCommand, string](m, uploadHandler)
@ -53,6 +54,7 @@ func main() {
mediator.Register[handlers.InitMultipartCommand, string](m, initMultipartHandler) mediator.Register[handlers.InitMultipartCommand, string](m, initMultipartHandler)
mediator.Register[handlers.UploadPartCommand, string](m, uploadPartHandler) mediator.Register[handlers.UploadPartCommand, string](m, uploadPartHandler)
mediator.Register[handlers.CompleteMultipartCommand, string](m, completeMultipartHandler) mediator.Register[handlers.CompleteMultipartCommand, string](m, completeMultipartHandler)
mediator.Register[handlers.DeleteFileCommand, string](m, deleteFileHandler)
// Validators // Validators
uploadValidator := validators.NewUploadFileValidator() uploadValidator := validators.NewUploadFileValidator()
@ -72,7 +74,7 @@ func main() {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*") c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") 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-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" { if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204) c.AbortWithStatus(204)
return return
@ -87,6 +89,9 @@ func main() {
r.GET("/files/list", fileEndpoint.ListFiles) r.GET("/files/list", fileEndpoint.ListFiles)
r.GET("/files/preview", fileEndpoint.GetPreviewURL) r.GET("/files/preview", fileEndpoint.GetPreviewURL)
// Delete file
r.DELETE("/files/delete", fileEndpoint.DeleteFile)
// Multipart Upload // Multipart Upload
r.POST("/files/multipart/init", fileEndpoint.InitMultipart) r.POST("/files/multipart/init", fileEndpoint.InitMultipart)
r.PUT("/files/multipart/part", fileEndpoint.UploadPart) 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}) 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) { func handleError(c *gin.Context, err error) {
if be, ok := err.(*common.BusinessException); ok { if be, ok := err.(*common.BusinessException); ok {
c.JSON(be.Code, gin.H{"error": be.Message}) c.JSON(be.Code, gin.H{"error": be.Message})

View File

@ -2,9 +2,10 @@ package handlers
import ( import (
"context" "context"
"file-system/internal/common"
"file-sys
"file-system/internal/domain/repository" "file-system/internal/domain/repository"
"io" "io"
"time"
"file-system/internal/common" "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) { func (h *GetFilePreviewHandler) Handle(ctx context.Context, q GetFilePreviewQuery) (string, error) {
return h.Repo.GeneratePresignedURL(ctx, q.BucketName, q.ObjectKey, q.Expiry) 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"` UploadId string `json:"upload_id"`
Parts []common.Part `json:"parts"` 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 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 { type ListFilesResult struct {
Files []FileInfo Files []FileInfo
NextContinuationToken *string NextContinuationToken *string
} }
@ -30,7 +30,10 @@ type FileRepository interface {
// 新增功能 // 新增功能
ListObjectsV2(ctx context.Context, bucketName string, prefix string, maxKeys int32, continuationToken *string) (*ListFilesResult, error) 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) 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) 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) 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 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 分页列出文件 // ListObjectsV2 分页列出文件
func (r *S3FileRepository) ListObjectsV2(ctx context.Context, bucketName string, prefix string, maxKeys int32, continuationToken *string) (*repository.ListFilesResult, error) { func (r *S3FileRepository) ListObjectsV2(ctx context.Context, bucketName string, prefix string, maxKeys int32, continuationToken *string) (*repository.ListFilesResult, error) {
input := &s3.ListObjectsV2Input{ input := &s3.ListObjectsV2Input{

View File

@ -113,6 +113,9 @@
<span class="action-btn text-success" @click="downloadFile(file.Key)" title="下载"> <span class="action-btn text-success" @click="downloadFile(file.Key)" title="下载">
<i class="fas fa-download"></i> <i class="fas fa-download"></i>
</span> </span>
<span class="action-btn text-danger" @click="deleteFile(file.Key)" title="删除">
<i class="fas fa-trash-alt"></i>
</span>
</td> </td>
</tr> </tr>
<tr v-if="files.length === 0 && !loadingFiles"> <tr v-if="files.length === 0 && !loadingFiles">
@ -493,6 +496,19 @@
window.open(url, '_blank'); 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 // Utils
const formatSize = (bytes) => { const formatSize = (bytes) => {
if (bytes === 0) return '0 B'; if (bytes === 0) return '0 B';
@ -531,7 +547,7 @@
loadBuckets, createBucket, selectBucket, refreshFiles: () => loadFilesInitial(), loadBuckets, createBucket, selectBucket, refreshFiles: () => loadFilesInitial(),
nextPage: nextP, prevPage, nextPage: nextP, prevPage,
triggerFileInput, handleFileSelect, triggerFileInput, handleFileSelect,
previewFile, downloadFile, previewFile, downloadFile, deleteFile,
formatSize, formatDate, getFileIcon, getProgressBarClass formatSize, formatDate, getFileIcon, getProgressBarClass
}; };
} }