From 82328278351c41e49b3b4d3a5af8a106d3de2902 Mon Sep 17 00:00:00 2001 From: root <1772105645@qq.com> Date: Thu, 18 Dec 2025 09:34:49 +0800 Subject: [PATCH] Initial commit --- .gitlab-ci.yml | 37 + Dockerfile | 34 + cmd/server/main.go | 109 +++ docker-compose.yml | 35 + docs/docs.go | 647 ++++++++++++++++++ docs/swagger.json | 623 +++++++++++++++++ docs/swagger.yaml | 412 +++++++++++ go.mod | 73 ++ go.sum | 204 ++++++ internal/api/endpoints/bucket_endpoint.go | 81 +++ internal/api/endpoints/file_endpoint.go | 330 +++++++++ internal/api/handlers/bucket_commands.go | 7 + internal/api/handlers/bucket_handlers.go | 34 + .../api/handlers/download_file_handler.go | 19 + internal/api/handlers/download_file_query.go | 6 + .../api/handlers/multipart_handlers_split.go | 45 ++ internal/api/handlers/query_handlers.go | 69 ++ internal/api/handlers/upload_file_command.go | 9 + internal/api/handlers/upload_file_handler.go | 25 + internal/api/requests/bucket_requests.go | 7 + .../api/requests/download_file_request.go | 6 + .../api/requests/new_features_requests.go | 38 + internal/api/requests/upload_file_request.go | 8 + internal/api/validators/bucket_validators.go | 19 + .../api/validators/download_file_validator.go | 22 + .../api/validators/new_features_validators.go | 50 ++ .../api/validators/upload_file_validator.go | 22 + internal/common/config.go | 28 + internal/common/errors.go | 17 + internal/common/types.go | 6 + internal/domain/repository/file_repository.go | 39 ++ internal/infrastructure/mediator/mediator.go | 44 ++ internal/infrastructure/s3/client.go | 49 ++ .../infrastructure/s3/file_repository_impl.go | 198 ++++++ web/index.html | 541 +++++++++++++++ 35 files changed, 3893 insertions(+) create mode 100644 .gitlab-ci.yml create mode 100644 Dockerfile create mode 100644 cmd/server/main.go create mode 100644 docker-compose.yml create mode 100644 docs/docs.go create mode 100644 docs/swagger.json create mode 100644 docs/swagger.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/api/endpoints/bucket_endpoint.go create mode 100644 internal/api/endpoints/file_endpoint.go create mode 100644 internal/api/handlers/bucket_commands.go create mode 100644 internal/api/handlers/bucket_handlers.go create mode 100644 internal/api/handlers/download_file_handler.go create mode 100644 internal/api/handlers/download_file_query.go create mode 100644 internal/api/handlers/multipart_handlers_split.go create mode 100644 internal/api/handlers/query_handlers.go create mode 100644 internal/api/handlers/upload_file_command.go create mode 100644 internal/api/handlers/upload_file_handler.go create mode 100644 internal/api/requests/bucket_requests.go create mode 100644 internal/api/requests/download_file_request.go create mode 100644 internal/api/requests/new_features_requests.go create mode 100644 internal/api/requests/upload_file_request.go create mode 100644 internal/api/validators/bucket_validators.go create mode 100644 internal/api/validators/download_file_validator.go create mode 100644 internal/api/validators/new_features_validators.go create mode 100644 internal/api/validators/upload_file_validator.go create mode 100644 internal/common/config.go create mode 100644 internal/common/errors.go create mode 100644 internal/common/types.go create mode 100644 internal/domain/repository/file_repository.go create mode 100644 internal/infrastructure/mediator/mediator.go create mode 100644 internal/infrastructure/s3/client.go create mode 100644 internal/infrastructure/s3/file_repository_impl.go create mode 100644 web/index.html diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..9f7dfde --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,37 @@ +stages: + - build + - deploy + +variables: + DOCKER_IMAGE_NAME: file-system-server + DOCKER_TAG: latest + +# 构建镜像 +build_image: + stage: build + image: docker:20.10.16 + services: + - docker:20.10.16-dind + script: + - docker build -t $DOCKER_IMAGE_NAME:$DOCKER_TAG . + # 如果有私有仓库,可以在这里 push + # - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + # - docker push $DOCKER_IMAGE_NAME:$DOCKER_TAG + only: + - main + +# 部署服务 +deploy_service: + stage: deploy + image: docker/compose:latest + script: + # 停止旧容器(如果存在) + - docker-compose down --remove-orphans || true + # 重新构建并启动服务 + - docker-compose up -d --build + # 清理未使用的镜像 + - docker image prune -f + tags: + - shell # 假设您的 GitLab Runner 是 Shell Executor,可以直接操作宿主机 Docker + only: + - main diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cafb773 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +# Build Stage +FROM golang:1.23-alpine AS builder + +WORKDIR /app + +# Copy go mod and sum files +COPY go.mod go.sum ./ + +# Download all dependencies. Dependencies will be cached if the go.mod and go.sum files are not changed +RUN go mod download + +# Copy the source from the current directory to the Working Directory inside the container +COPY . . + +# Build the Go app +RUN go build -o server ./cmd/server + +# Run Stage +FROM alpine:latest + +WORKDIR /app + +# Copy the Pre-built binary from the previous stage +COPY --from=builder /app/server . +# Copy web resources +COPY --from=builder /app/web ./web +# Copy docs +COPY --from=builder /app/docs ./docs + +# Expose port 8080 to the outside world +EXPOSE 8080 + +# Command to run the executable +CMD ["./server"] diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..95edbad --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,109 @@ +package main + +import ( + _ "file-system/docs" // Import generated docs + "file-system/internal/api/endpoints" + "file-system/internal/api/handlers" + "file-system/internal/api/validators" + "file-system/internal/common" + "file-system/internal/domain/repository" + "file-system/internal/infrastructure/mediator" + "file-system/internal/infrastructure/s3" + "io" + "net/http" + + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +// @title RustFS File System API +// @version 1.1 +// @description RustFS 文件存储系统 API,支持分片上传、文件预览、分页查询等高级功能。 +// @host localhost:8080 +// @BasePath / +func main() { + cfg := common.LoadConfig() + + // Infrastructure + rustfsClient := s3.NewRustFSClient(cfg) + s3Repo := s3.NewS3FileRepository(rustfsClient) + m := mediator.NewMediator() + + // Handlers + uploadHandler := handlers.NewUploadFileHandler(s3Repo) + downloadHandler := handlers.NewDownloadFileHandler(s3Repo) + createBucketHandler := handlers.NewCreateBucketHandler(s3Repo) + listBucketsHandler := handlers.NewListBucketsHandler(s3Repo) + // New Handlers + listFilesHandler := handlers.NewListFilesHandler(s3Repo) + previewHandler := handlers.NewGetFilePreviewHandler(s3Repo) + initMultipartHandler := handlers.NewInitMultipartHandler(s3Repo) + uploadPartHandler := handlers.NewUploadPartHandler(s3Repo) + completeMultipartHandler := handlers.NewCompleteMultipartHandler(s3Repo) + + // Register Handlers + mediator.Register[handlers.UploadFileCommand, string](m, uploadHandler) + mediator.Register[handlers.DownloadFileQuery, io.ReadCloser](m, downloadHandler) + mediator.Register[handlers.CreateBucketCommand, string](m, createBucketHandler) + mediator.Register[handlers.ListBucketsQuery, []string](m, listBucketsHandler) + // New Registrations + mediator.Register[handlers.ListFilesQuery, *repository.ListFilesResult](m, listFilesHandler) + mediator.Register[handlers.GetFilePreviewQuery, string](m, previewHandler) + mediator.Register[handlers.InitMultipartCommand, string](m, initMultipartHandler) + mediator.Register[handlers.UploadPartCommand, string](m, uploadPartHandler) + mediator.Register[handlers.CompleteMultipartCommand, string](m, completeMultipartHandler) + + // Validators + uploadValidator := validators.NewUploadFileValidator() + downloadValidator := validators.NewDownloadFileValidator() + createBucketValidator := validators.NewCreateBucketValidator() + newFeaturesValidator := validators.NewNewFeaturesValidator() + + // Endpoints + fileEndpoint := endpoints.NewFileEndpoint(m, uploadValidator, downloadValidator, newFeaturesValidator) + bucketEndpoint := endpoints.NewBucketEndpoint(m, createBucketValidator) + + // Router + r := gin.Default() + + // CORS + r.Use(func(c *gin.Context) { + 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") + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + c.Next() + }) + + // Routes + // File operations + r.POST("/files/upload", fileEndpoint.UploadFile) + r.GET("/files/download", fileEndpoint.DownloadFile) + r.GET("/files/list", fileEndpoint.ListFiles) + r.GET("/files/preview", fileEndpoint.GetPreviewURL) + + // Multipart Upload + r.POST("/files/multipart/init", fileEndpoint.InitMultipart) + r.PUT("/files/multipart/part", fileEndpoint.UploadPart) + r.POST("/files/multipart/complete", fileEndpoint.CompleteMultipart) + + // Bucket operations + r.POST("/buckets", bucketEndpoint.CreateBucket) + r.GET("/buckets", bucketEndpoint.ListBuckets) + + // Swagger + r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + + // Web UI + r.Static("/web", "./web") + r.GET("/", func(c *gin.Context) { + c.Redirect(http.StatusMovedPermanently, "/web") + }) + + r.Run(":" + cfg.ServerPort) +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..62f57ed --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,35 @@ +version: '3.8' + +services: + rustfs: + image: rustfs/rustfs:latest + container_name: rustfs + restart: unless-stopped + ports: + - "20060:9000" + - "20061:9001" + environment: + - TZ=${TZ:-Asia/Shanghai} + - RUSTFS_ACCESS_KEY=xiangning + - RUSTFS_SECRET_KEY=xn001624. + volumes: + - rustfs-data:/data + + server: + build: . + container_name: file-system-server + restart: unless-stopped + ports: + - "8080:8080" + environment: + - TZ=Asia/Shanghai + - RUSTFS_ENDPOINT_URL=http://rustfs:9000 + - RUSTFS_ACCESS_KEY_ID=xiangning + - RUSTFS_SECRET_ACCESS_KEY=xn001624. + - RUSTFS_REGION=us-east-1 + - SERVER_PORT=8080 + depends_on: + - rustfs + +volumes: + rustfs-data: diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..1a6b7ab --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,647 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/buckets": { + "get": { + "description": "列出所有可用的 S3 存储桶", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "存储桶管理" + ], + "summary": "获取存储桶列表", + "responses": { + "200": { + "description": "存储桶列表", + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "description": "创建一个新的 S3 存储桶", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "存储桶管理" + ], + "summary": "创建存储桶", + "parameters": [ + { + "description": "创建存储桶请求参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.CreateBucketRequest" + } + } + ], + "responses": { + "200": { + "description": "创建成功消息", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "参数错误", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/files/download": { + "get": { + "description": "从指定的存储桶下载文件", + "consumes": [ + "application/json" + ], + "produces": [ + "application/octet-stream" + ], + "tags": [ + "文件操作" + ], + "summary": "下载文件", + "parameters": [ + { + "type": "string", + "description": "存储桶名称", + "name": "bucket_name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "对象键(文件名)", + "name": "object_key", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "文件流", + "schema": { + "type": "file" + } + }, + "400": { + "description": "参数错误", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/files/list": { + "get": { + "description": "分页查询存储桶中的文件", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "文件操作" + ], + "summary": "文件列表 (分页)", + "parameters": [ + { + "type": "string", + "description": "存储桶名称", + "name": "bucket_name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "文件名前缀筛选", + "name": "prefix", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "max_keys", + "in": "query" + }, + { + "type": "string", + "description": "分页Token", + "name": "token", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/repository.ListFilesResult" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/files/multipart/complete": { + "post": { + "description": "合并所有分片完成上传", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "大文件上传" + ], + "summary": "完成分片上传", + "parameters": [ + { + "description": "请求参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.CompleteMultipartRequest" + } + } + ], + "responses": { + "200": { + "description": "返回文件位置", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/files/multipart/init": { + "post": { + "description": "开始一个新的大文件分片上传任务", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "大文件上传" + ], + "summary": "初始化分片上传", + "parameters": [ + { + "description": "请求参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.InitMultipartRequest" + } + } + ], + "responses": { + "200": { + "description": "返回 upload_id", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/files/multipart/part": { + "put": { + "description": "上传单个文件分片", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "大文件上传" + ], + "summary": "上传分片", + "parameters": [ + { + "type": "string", + "description": "存储桶名称", + "name": "bucket_name", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "对象键", + "name": "object_key", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "上传ID", + "name": "upload_id", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "分片序号 (从1开始)", + "name": "part_number", + "in": "formData", + "required": true + }, + { + "type": "file", + "description": "分片文件数据", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "返回 ETag", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/files/preview": { + "get": { + "description": "生成文件的临时预览链接 (24小时有效)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "文件操作" + ], + "summary": "获取预览链接", + "parameters": [ + { + "type": "string", + "description": "存储桶名称", + "name": "bucket_name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "对象键", + "name": "object_key", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/files/upload": { + "post": { + "description": "上传小文件到指定的存储桶", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "文件操作" + ], + "summary": "上传文件 (简单上传)", + "parameters": [ + { + "type": "string", + "description": "存储桶名称", + "name": "bucket_name", + "in": "formData", + "required": true + }, + { + "type": "file", + "description": "要上传的文件", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "上传成功消息", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "参数错误", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + }, + "definitions": { + "common.Part": { + "type": "object", + "properties": { + "etag": { + "type": "string" + }, + "partNumber": { + "type": "integer" + } + } + }, + "repository.FileInfo": { + "type": "object", + "properties": { + "etag": { + "type": "string" + }, + "key": { + "type": "string" + }, + "lastModified": { + "type": "string" + }, + "size": { + "type": "integer" + } + } + }, + "repository.ListFilesResult": { + "type": "object", + "properties": { + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/repository.FileInfo" + } + }, + "nextContinuationToken": { + "type": "string" + } + } + }, + "requests.CompleteMultipartRequest": { + "type": "object", + "properties": { + "bucket_name": { + "type": "string" + }, + "object_key": { + "type": "string" + }, + "parts": { + "type": "array", + "items": { + "$ref": "#/definitions/common.Part" + } + }, + "upload_id": { + "type": "string" + } + } + }, + "requests.CreateBucketRequest": { + "type": "object", + "properties": { + "bucket_name": { + "type": "string" + } + } + }, + "requests.InitMultipartRequest": { + "type": "object", + "properties": { + "bucket_name": { + "type": "string" + }, + "object_key": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.1", + Host: "localhost:8080", + BasePath: "/", + Schemes: []string{}, + Title: "RustFS File System API", + Description: "RustFS 文件存储系统 API,支持分片上传、文件预览、分页查询等高级功能。", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..8243be7 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,623 @@ +{ + "swagger": "2.0", + "info": { + "description": "RustFS 文件存储系统 API,支持分片上传、文件预览、分页查询等高级功能。", + "title": "RustFS File System API", + "contact": {}, + "version": "1.1" + }, + "host": "localhost:8080", + "basePath": "/", + "paths": { + "/buckets": { + "get": { + "description": "列出所有可用的 S3 存储桶", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "存储桶管理" + ], + "summary": "获取存储桶列表", + "responses": { + "200": { + "description": "存储桶列表", + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "description": "创建一个新的 S3 存储桶", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "存储桶管理" + ], + "summary": "创建存储桶", + "parameters": [ + { + "description": "创建存储桶请求参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.CreateBucketRequest" + } + } + ], + "responses": { + "200": { + "description": "创建成功消息", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "参数错误", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/files/download": { + "get": { + "description": "从指定的存储桶下载文件", + "consumes": [ + "application/json" + ], + "produces": [ + "application/octet-stream" + ], + "tags": [ + "文件操作" + ], + "summary": "下载文件", + "parameters": [ + { + "type": "string", + "description": "存储桶名称", + "name": "bucket_name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "对象键(文件名)", + "name": "object_key", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "文件流", + "schema": { + "type": "file" + } + }, + "400": { + "description": "参数错误", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/files/list": { + "get": { + "description": "分页查询存储桶中的文件", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "文件操作" + ], + "summary": "文件列表 (分页)", + "parameters": [ + { + "type": "string", + "description": "存储桶名称", + "name": "bucket_name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "文件名前缀筛选", + "name": "prefix", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "max_keys", + "in": "query" + }, + { + "type": "string", + "description": "分页Token", + "name": "token", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/repository.ListFilesResult" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/files/multipart/complete": { + "post": { + "description": "合并所有分片完成上传", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "大文件上传" + ], + "summary": "完成分片上传", + "parameters": [ + { + "description": "请求参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.CompleteMultipartRequest" + } + } + ], + "responses": { + "200": { + "description": "返回文件位置", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/files/multipart/init": { + "post": { + "description": "开始一个新的大文件分片上传任务", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "大文件上传" + ], + "summary": "初始化分片上传", + "parameters": [ + { + "description": "请求参数", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.InitMultipartRequest" + } + } + ], + "responses": { + "200": { + "description": "返回 upload_id", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/files/multipart/part": { + "put": { + "description": "上传单个文件分片", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "大文件上传" + ], + "summary": "上传分片", + "parameters": [ + { + "type": "string", + "description": "存储桶名称", + "name": "bucket_name", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "对象键", + "name": "object_key", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "上传ID", + "name": "upload_id", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "分片序号 (从1开始)", + "name": "part_number", + "in": "formData", + "required": true + }, + { + "type": "file", + "description": "分片文件数据", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "返回 ETag", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/files/preview": { + "get": { + "description": "生成文件的临时预览链接 (24小时有效)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "文件操作" + ], + "summary": "获取预览链接", + "parameters": [ + { + "type": "string", + "description": "存储桶名称", + "name": "bucket_name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "对象键", + "name": "object_key", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/files/upload": { + "post": { + "description": "上传小文件到指定的存储桶", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "文件操作" + ], + "summary": "上传文件 (简单上传)", + "parameters": [ + { + "type": "string", + "description": "存储桶名称", + "name": "bucket_name", + "in": "formData", + "required": true + }, + { + "type": "file", + "description": "要上传的文件", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "上传成功消息", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "参数错误", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + }, + "definitions": { + "common.Part": { + "type": "object", + "properties": { + "etag": { + "type": "string" + }, + "partNumber": { + "type": "integer" + } + } + }, + "repository.FileInfo": { + "type": "object", + "properties": { + "etag": { + "type": "string" + }, + "key": { + "type": "string" + }, + "lastModified": { + "type": "string" + }, + "size": { + "type": "integer" + } + } + }, + "repository.ListFilesResult": { + "type": "object", + "properties": { + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/repository.FileInfo" + } + }, + "nextContinuationToken": { + "type": "string" + } + } + }, + "requests.CompleteMultipartRequest": { + "type": "object", + "properties": { + "bucket_name": { + "type": "string" + }, + "object_key": { + "type": "string" + }, + "parts": { + "type": "array", + "items": { + "$ref": "#/definitions/common.Part" + } + }, + "upload_id": { + "type": "string" + } + } + }, + "requests.CreateBucketRequest": { + "type": "object", + "properties": { + "bucket_name": { + "type": "string" + } + } + }, + "requests.InitMultipartRequest": { + "type": "object", + "properties": { + "bucket_name": { + "type": "string" + }, + "object_key": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..b19abfa --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,412 @@ +basePath: / +definitions: + common.Part: + properties: + etag: + type: string + partNumber: + type: integer + type: object + repository.FileInfo: + properties: + etag: + type: string + key: + type: string + lastModified: + type: string + size: + type: integer + type: object + repository.ListFilesResult: + properties: + files: + items: + $ref: '#/definitions/repository.FileInfo' + type: array + nextContinuationToken: + type: string + type: object + requests.CompleteMultipartRequest: + properties: + bucket_name: + type: string + object_key: + type: string + parts: + items: + $ref: '#/definitions/common.Part' + type: array + upload_id: + type: string + type: object + requests.CreateBucketRequest: + properties: + bucket_name: + type: string + type: object + requests.InitMultipartRequest: + properties: + bucket_name: + type: string + object_key: + type: string + type: object +host: localhost:8080 +info: + contact: {} + description: RustFS 文件存储系统 API,支持分片上传、文件预览、分页查询等高级功能。 + title: RustFS File System API + version: "1.1" +paths: + /buckets: + get: + consumes: + - application/json + description: 列出所有可用的 S3 存储桶 + produces: + - application/json + responses: + "200": + description: 存储桶列表 + schema: + additionalProperties: + items: + type: string + type: array + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: + type: string + type: object + summary: 获取存储桶列表 + tags: + - 存储桶管理 + post: + consumes: + - application/json + description: 创建一个新的 S3 存储桶 + parameters: + - description: 创建存储桶请求参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/requests.CreateBucketRequest' + produces: + - application/json + responses: + "200": + description: 创建成功消息 + schema: + additionalProperties: + type: string + type: object + "400": + description: 参数错误 + schema: + additionalProperties: + type: string + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: + type: string + type: object + summary: 创建存储桶 + tags: + - 存储桶管理 + /files/download: + get: + consumes: + - application/json + description: 从指定的存储桶下载文件 + parameters: + - description: 存储桶名称 + in: query + name: bucket_name + required: true + type: string + - description: 对象键(文件名) + in: query + name: object_key + required: true + type: string + produces: + - application/octet-stream + responses: + "200": + description: 文件流 + schema: + type: file + "400": + description: 参数错误 + schema: + additionalProperties: + type: string + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: + type: string + type: object + summary: 下载文件 + tags: + - 文件操作 + /files/list: + get: + consumes: + - application/json + description: 分页查询存储桶中的文件 + parameters: + - description: 存储桶名称 + in: query + name: bucket_name + required: true + type: string + - description: 文件名前缀筛选 + in: query + name: prefix + type: string + - description: 每页数量 + in: query + name: max_keys + type: integer + - description: 分页Token + in: query + name: token + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/repository.ListFilesResult' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: 文件列表 (分页) + tags: + - 文件操作 + /files/multipart/complete: + post: + consumes: + - application/json + description: 合并所有分片完成上传 + parameters: + - description: 请求参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/requests.CompleteMultipartRequest' + produces: + - application/json + responses: + "200": + description: 返回文件位置 + schema: + additionalProperties: + type: string + type: object + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: 完成分片上传 + tags: + - 大文件上传 + /files/multipart/init: + post: + consumes: + - application/json + description: 开始一个新的大文件分片上传任务 + parameters: + - description: 请求参数 + in: body + name: request + required: true + schema: + $ref: '#/definitions/requests.InitMultipartRequest' + produces: + - application/json + responses: + "200": + description: 返回 upload_id + schema: + additionalProperties: + type: string + type: object + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: 初始化分片上传 + tags: + - 大文件上传 + /files/multipart/part: + put: + consumes: + - multipart/form-data + description: 上传单个文件分片 + parameters: + - description: 存储桶名称 + in: formData + name: bucket_name + required: true + type: string + - description: 对象键 + in: formData + name: object_key + required: true + type: string + - description: 上传ID + in: formData + name: upload_id + required: true + type: string + - description: 分片序号 (从1开始) + in: formData + name: part_number + required: true + type: integer + - description: 分片文件数据 + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: 返回 ETag + schema: + additionalProperties: + type: string + type: object + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: 上传分片 + tags: + - 大文件上传 + /files/preview: + get: + consumes: + - application/json + description: 生成文件的临时预览链接 (24小时有效) + parameters: + - description: 存储桶名称 + in: query + name: bucket_name + required: true + type: string + - description: 对象键 + in: query + name: object_key + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: 获取预览链接 + tags: + - 文件操作 + /files/upload: + post: + consumes: + - multipart/form-data + description: 上传小文件到指定的存储桶 + parameters: + - description: 存储桶名称 + in: formData + name: bucket_name + required: true + type: string + - description: 要上传的文件 + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: 上传成功消息 + schema: + additionalProperties: + type: string + type: object + "400": + description: 参数错误 + schema: + additionalProperties: + type: string + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: + type: string + type: object + summary: 上传文件 (简单上传) + tags: + - 文件操作 +swagger: "2.0" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..691a15e --- /dev/null +++ b/go.mod @@ -0,0 +1,73 @@ +module file-system + +go 1.23.2 + +require ( + github.com/aws/aws-sdk-go-v2 v1.41.0 + github.com/aws/aws-sdk-go-v2/config v1.32.6 + github.com/aws/aws-sdk-go-v2/credentials v1.19.6 + github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 + github.com/gin-gonic/gin v1.11.0 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.3 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect + github.com/aws/smithy-go v1.24.0 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cc7e08c --- /dev/null +++ b/go.sum @@ -0,0 +1,204 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= +github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= +github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8= +github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI= +github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A= +github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 h1:SWTxh/EcUCDVqi/0s26V6pVUq0BBG7kx0tDTmF/hCgA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w= +github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its= +github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= +github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/endpoints/bucket_endpoint.go b/internal/api/endpoints/bucket_endpoint.go new file mode 100644 index 0000000..428ed79 --- /dev/null +++ b/internal/api/endpoints/bucket_endpoint.go @@ -0,0 +1,81 @@ +package endpoints + +import ( + "file-system/internal/api/handlers" + "file-system/internal/api/requests" + "file-system/internal/api/validators" + "file-system/internal/common" + "file-system/internal/infrastructure/mediator" + "net/http" + + "github.com/gin-gonic/gin" +) + +type BucketEndpoint struct { + Mediator *mediator.Mediator + CreateBucketValidator *validators.CreateBucketValidator +} + +func NewBucketEndpoint(m *mediator.Mediator, cbv *validators.CreateBucketValidator) *BucketEndpoint { + return &BucketEndpoint{ + Mediator: m, + CreateBucketValidator: cbv, + } +} + +// CreateBucket godoc +// @Summary 创建存储桶 +// @Description 创建一个新的 S3 存储桶 +// @Tags 存储桶管理 +// @Accept json +// @Produce json +// @Param request body requests.CreateBucketRequest true "创建存储桶请求参数" +// @Success 200 {object} map[string]string "创建成功消息" +// @Failure 400 {object} map[string]string "参数错误" +// @Failure 500 {object} map[string]string "服务器内部错误" +// @Router /buckets [post] +func (e *BucketEndpoint) CreateBucket(c *gin.Context) { + var req requests.CreateBucketRequest + if err := c.ShouldBind(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := e.CreateBucketValidator.Validate(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + cmd := handlers.CreateBucketCommand{BucketName: req.BucketName} + + result, err := mediator.Send[handlers.CreateBucketCommand, string](e.Mediator, c.Request.Context(), cmd) + if err != nil { + if be, ok := err.(*common.BusinessException); ok { + c.JSON(be.Code, gin.H{"error": be.Message}) + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + } + return + } + + c.JSON(http.StatusOK, gin.H{"message": result}) +} + +// ListBuckets godoc +// @Summary 获取存储桶列表 +// @Description 列出所有可用的 S3 存储桶 +// @Tags 存储桶管理 +// @Accept json +// @Produce json +// @Success 200 {object} map[string][]string "存储桶列表" +// @Failure 500 {object} map[string]string "服务器内部错误" +// @Router /buckets [get] +func (e *BucketEndpoint) ListBuckets(c *gin.Context) { + query := handlers.ListBucketsQuery{} + result, err := mediator.Send[handlers.ListBucketsQuery, []string](e.Mediator, c.Request.Context(), query) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"buckets": result}) +} diff --git a/internal/api/endpoints/file_endpoint.go b/internal/api/endpoints/file_endpoint.go new file mode 100644 index 0000000..b7e628d --- /dev/null +++ b/internal/api/endpoints/file_endpoint.go @@ -0,0 +1,330 @@ +package endpoints + +import ( + "file-system/internal/api/handlers" + "file-system/internal/api/requests" + "file-system/internal/api/validators" + "file-system/internal/common" + "file-system/internal/domain/repository" + "file-system/internal/infrastructure/mediator" + "io" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +type FileEndpoint struct { + Mediator *mediator.Mediator + UploadValidator *validators.UploadFileValidator + DownloadValidator *validators.DownloadFileValidator + NewFeaturesValidator *validators.NewFeaturesValidator +} + +func NewFileEndpoint(m *mediator.Mediator, uv *validators.UploadFileValidator, dv *validators.DownloadFileValidator, nfv *validators.NewFeaturesValidator) *FileEndpoint { + return &FileEndpoint{ + Mediator: m, + UploadValidator: uv, + DownloadValidator: dv, + NewFeaturesValidator: nfv, + } +} + +// UploadFile godoc +// @Summary 上传文件 (简单上传) +// @Description 上传小文件到指定的存储桶 +// @Tags 文件操作 +// @Accept multipart/form-data +// @Produce json +// @Param bucket_name formData string true "存储桶名称" +// @Param file formData file true "要上传的文件" +// @Success 200 {object} map[string]string "上传成功消息" +// @Failure 400 {object} map[string]string "参数错误" +// @Failure 500 {object} map[string]string "服务器内部错误" +// @Router /files/upload [post] +func (e *FileEndpoint) UploadFile(c *gin.Context) { + var req requests.UploadFileRequest + // 绑定参数 + if err := c.ShouldBind(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 验证参数 + if err := e.UploadValidator.Validate(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 打开文件流 + file, err := req.File.Open() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to open file"}) + return + } + defer file.Close() + + // 构建 Command + cmd := handlers.UploadFileCommand{ + BucketName: req.BucketName, + FileName: req.File.Filename, + Data: file, + } + + // 调用 Mediator + result, err := mediator.Send[handlers.UploadFileCommand, string](e.Mediator, c.Request.Context(), cmd) + if err != nil { + handleError(c, err) + return + } + + c.JSON(http.StatusOK, gin.H{"message": result}) +} + +// DownloadFile godoc +// @Summary 下载文件 +// @Description 从指定的存储桶下载文件 +// @Tags 文件操作 +// @Accept json +// @Produce octet-stream +// @Param bucket_name query string true "存储桶名称" +// @Param object_key query string true "对象键(文件名)" +// @Success 200 {file} file "文件流" +// @Failure 400 {object} map[string]string "参数错误" +// @Failure 500 {object} map[string]string "服务器内部错误" +// @Router /files/download [get] +func (e *FileEndpoint) DownloadFile(c *gin.Context) { + var req requests.DownloadFileRequest + if err := c.ShouldBind(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := e.DownloadValidator.Validate(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + query := handlers.DownloadFileQuery{ + BucketName: req.BucketName, + ObjectKey: req.ObjectKey, + } + + result, err := mediator.Send[handlers.DownloadFileQuery, io.ReadCloser](e.Mediator, c.Request.Context(), query) + if err != nil { + handleError(c, err) + return + } + defer result.Close() + + c.Header("Content-Disposition", "attachment; filename="+req.ObjectKey) + c.Header("Content-Type", "application/octet-stream") + io.Copy(c.Writer, result) +} + +// ListFiles godoc +// @Summary 文件列表 (分页) +// @Description 分页查询存储桶中的文件 +// @Tags 文件操作 +// @Accept json +// @Produce json +// @Param bucket_name query string true "存储桶名称" +// @Param prefix query string false "文件名前缀筛选" +// @Param max_keys query int false "每页数量" +// @Param token query string false "分页Token" +// @Success 200 {object} repository.ListFilesResult +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /files/list [get] +func (e *FileEndpoint) ListFiles(c *gin.Context) { + var req requests.ListFilesRequest + if err := c.ShouldBind(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := e.NewFeaturesValidator.ValidateListFiles(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var token *string + if req.Token != "" { + token = &req.Token + } + + query := handlers.ListFilesQuery{ + BucketName: req.BucketName, + Prefix: req.Prefix, + MaxKeys: req.MaxKeys, + Token: token, + } + + result, err := mediator.Send[handlers.ListFilesQuery, *repository.ListFilesResult](e.Mediator, c.Request.Context(), query) + if err != nil { + handleError(c, err) + return + } + c.JSON(http.StatusOK, result) +} + +// GetPreviewURL godoc +// @Summary 获取预览链接 +// @Description 生成文件的临时预览链接 (24小时有效) +// @Tags 文件操作 +// @Accept json +// @Produce json +// @Param bucket_name query string true "存储桶名称" +// @Param object_key query string true "对象键" +// @Success 200 {object} map[string]string +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /files/preview [get] +func (e *FileEndpoint) GetPreviewURL(c *gin.Context) { + var req requests.GetFilePreviewRequest + if err := c.ShouldBind(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := e.NewFeaturesValidator.ValidatePreview(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + query := handlers.GetFilePreviewQuery{ + BucketName: req.BucketName, + ObjectKey: req.ObjectKey, + Expiry: 24 * time.Hour, + } + + result, err := mediator.Send[handlers.GetFilePreviewQuery, string](e.Mediator, c.Request.Context(), query) + if err != nil { + handleError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"url": result}) +} + +// InitMultipart godoc +// @Summary 初始化分片上传 +// @Description 开始一个新的大文件分片上传任务 +// @Tags 大文件上传 +// @Accept json +// @Produce json +// @Param request body requests.InitMultipartRequest true "请求参数" +// @Success 200 {object} map[string]string "返回 upload_id" +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /files/multipart/init [post] +func (e *FileEndpoint) InitMultipart(c *gin.Context) { + var req requests.InitMultipartRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := e.NewFeaturesValidator.ValidateInitMultipart(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + cmd := handlers.InitMultipartCommand{BucketName: req.BucketName, ObjectKey: req.ObjectKey} + result, err := mediator.Send[handlers.InitMultipartCommand, string](e.Mediator, c.Request.Context(), cmd) + if err != nil { + handleError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"upload_id": result}) +} + +// UploadPart godoc +// @Summary 上传分片 +// @Description 上传单个文件分片 +// @Tags 大文件上传 +// @Accept multipart/form-data +// @Produce json +// @Param bucket_name formData string true "存储桶名称" +// @Param object_key formData string true "对象键" +// @Param upload_id formData string true "上传ID" +// @Param part_number formData int true "分片序号 (从1开始)" +// @Param file formData file true "分片文件数据" +// @Success 200 {object} map[string]string "返回 ETag" +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /files/multipart/part [put] +func (e *FileEndpoint) UploadPart(c *gin.Context) { + var req requests.UploadPartRequest + if err := c.ShouldBind(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := e.NewFeaturesValidator.ValidateUploadPart(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + file, err := req.File.Open() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to open part file"}) + return + } + defer file.Close() + + cmd := handlers.UploadPartCommand{ + BucketName: req.BucketName, + ObjectKey: req.ObjectKey, + UploadId: req.UploadId, + PartNumber: req.PartNumber, + Data: file, + } + result, err := mediator.Send[handlers.UploadPartCommand, string](e.Mediator, c.Request.Context(), cmd) + if err != nil { + handleError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"etag": result}) +} + +// CompleteMultipart godoc +// @Summary 完成分片上传 +// @Description 合并所有分片完成上传 +// @Tags 大文件上传 +// @Accept json +// @Produce json +// @Param request body requests.CompleteMultipartRequest true "请求参数" +// @Success 200 {object} map[string]string "返回文件位置" +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /files/multipart/complete [post] +func (e *FileEndpoint) CompleteMultipart(c *gin.Context) { + var req requests.CompleteMultipartRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := e.NewFeaturesValidator.ValidateCompleteMultipart(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + cmd := handlers.CompleteMultipartCommand{ + BucketName: req.BucketName, + ObjectKey: req.ObjectKey, + UploadId: req.UploadId, + Parts: req.Parts, + } + result, err := mediator.Send[handlers.CompleteMultipartCommand, string](e.Mediator, c.Request.Context(), cmd) + if err != nil { + handleError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"location": result}) +} + +func handleError(c *gin.Context, err error) { + if be, ok := err.(*common.BusinessException); ok { + c.JSON(be.Code, gin.H{"error": be.Message}) + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + } +} diff --git a/internal/api/handlers/bucket_commands.go b/internal/api/handlers/bucket_commands.go new file mode 100644 index 0000000..94df480 --- /dev/null +++ b/internal/api/handlers/bucket_commands.go @@ -0,0 +1,7 @@ +package handlers + +type CreateBucketCommand struct { + BucketName string +} + +type ListBucketsQuery struct{} diff --git a/internal/api/handlers/bucket_handlers.go b/internal/api/handlers/bucket_handlers.go new file mode 100644 index 0000000..04e7f70 --- /dev/null +++ b/internal/api/handlers/bucket_handlers.go @@ -0,0 +1,34 @@ +package handlers + +import ( + "context" + "file-system/internal/domain/repository" +) + +type CreateBucketHandler struct { + Repo repository.FileRepository +} + +func NewCreateBucketHandler(repo repository.FileRepository) *CreateBucketHandler { + return &CreateBucketHandler{Repo: repo} +} + +func (h *CreateBucketHandler) Handle(ctx context.Context, cmd CreateBucketCommand) (string, error) { + err := h.Repo.CreateBucket(ctx, cmd.BucketName) + if err != nil { + return "", err + } + return "Bucket created successfully", nil +} + +type ListBucketsHandler struct { + Repo repository.FileRepository +} + +func NewListBucketsHandler(repo repository.FileRepository) *ListBucketsHandler { + return &ListBucketsHandler{Repo: repo} +} + +func (h *ListBucketsHandler) Handle(ctx context.Context, query ListBucketsQuery) ([]string, error) { + return h.Repo.ListBuckets(ctx) +} diff --git a/internal/api/handlers/download_file_handler.go b/internal/api/handlers/download_file_handler.go new file mode 100644 index 0000000..1fd69c9 --- /dev/null +++ b/internal/api/handlers/download_file_handler.go @@ -0,0 +1,19 @@ +package handlers + +import ( + "context" + "file-system/internal/domain/repository" + "io" +) + +type DownloadFileHandler struct { + Repo repository.FileRepository +} + +func NewDownloadFileHandler(repo repository.FileRepository) *DownloadFileHandler { + return &DownloadFileHandler{Repo: repo} +} + +func (h *DownloadFileHandler) Handle(ctx context.Context, query DownloadFileQuery) (io.ReadCloser, error) { + return h.Repo.DownloadFile(ctx, query.BucketName, query.ObjectKey) +} diff --git a/internal/api/handlers/download_file_query.go b/internal/api/handlers/download_file_query.go new file mode 100644 index 0000000..015abcc --- /dev/null +++ b/internal/api/handlers/download_file_query.go @@ -0,0 +1,6 @@ +package handlers + +type DownloadFileQuery struct { + BucketName string + ObjectKey string +} diff --git a/internal/api/handlers/multipart_handlers_split.go b/internal/api/handlers/multipart_handlers_split.go new file mode 100644 index 0000000..1c1c57f --- /dev/null +++ b/internal/api/handlers/multipart_handlers_split.go @@ -0,0 +1,45 @@ +package handlers + +import ( + "context" + "file-system/internal/domain/repository" +) + +// InitMultipartHandler +type InitMultipartHandler struct { + Repo repository.FileRepository +} + +func NewInitMultipartHandler(repo repository.FileRepository) *InitMultipartHandler { + return &InitMultipartHandler{Repo: repo} +} + +func (h *InitMultipartHandler) Handle(ctx context.Context, cmd InitMultipartCommand) (string, error) { + return h.Repo.CreateMultipartUpload(ctx, cmd.BucketName, cmd.ObjectKey) +} + +// UploadPartHandler +type UploadPartHandler struct { + Repo repository.FileRepository +} + +func NewUploadPartHandler(repo repository.FileRepository) *UploadPartHandler { + return &UploadPartHandler{Repo: repo} +} + +func (h *UploadPartHandler) Handle(ctx context.Context, cmd UploadPartCommand) (string, error) { + return h.Repo.UploadPart(ctx, cmd.BucketName, cmd.ObjectKey, cmd.UploadId, cmd.PartNumber, cmd.Data) +} + +// CompleteMultipartHandler +type CompleteMultipartHandler struct { + Repo repository.FileRepository +} + +func NewCompleteMultipartHandler(repo repository.FileRepository) *CompleteMultipartHandler { + return &CompleteMultipartHandler{Repo: repo} +} + +func (h *CompleteMultipartHandler) Handle(ctx context.Context, cmd CompleteMultipartCommand) (string, error) { + return h.Repo.CompleteMultipartUpload(ctx, cmd.BucketName, cmd.ObjectKey, cmd.UploadId, cmd.Parts) +} diff --git a/internal/api/handlers/query_handlers.go b/internal/api/handlers/query_handlers.go new file mode 100644 index 0000000..be864bc --- /dev/null +++ b/internal/api/handlers/query_handlers.go @@ -0,0 +1,69 @@ +package handlers + +import ( + "context" + "file-system/internal/domain/repository" + "io" + "time" + "file-system/internal/common" +) + +// Queries & Commands +type ListFilesQuery struct { + BucketName string + Prefix string + MaxKeys int32 + Token *string +} + +type GetFilePreviewQuery struct { + BucketName string + ObjectKey string + Expiry time.Duration +} + +type InitMultipartCommand struct { + BucketName string + ObjectKey string +} + +type UploadPartCommand struct { + BucketName string + ObjectKey string + UploadId string + PartNumber int32 + Data io.Reader +} + +type CompleteMultipartCommand struct { + BucketName string + ObjectKey string + UploadId string + Parts []common.Part +} + +// Handlers + +type ListFilesHandler struct { + Repo repository.FileRepository +} + +func NewListFilesHandler(repo repository.FileRepository) *ListFilesHandler { + return &ListFilesHandler{Repo: repo} +} + +func (h *ListFilesHandler) Handle(ctx context.Context, q ListFilesQuery) (*repository.ListFilesResult, error) { + return h.Repo.ListObjectsV2(ctx, q.BucketName, q.Prefix, q.MaxKeys, q.Token) +} + +type GetFilePreviewHandler struct { + Repo repository.FileRepository +} + +func NewGetFilePreviewHandler(repo repository.FileRepository) *GetFilePreviewHandler { + return &GetFilePreviewHandler{Repo: repo} +} + +func (h *GetFilePreviewHandler) Handle(ctx context.Context, q GetFilePreviewQuery) (string, error) { + return h.Repo.GeneratePresignedURL(ctx, q.BucketName, q.ObjectKey, q.Expiry) +} diff --git a/internal/api/handlers/upload_file_command.go b/internal/api/handlers/upload_file_command.go new file mode 100644 index 0000000..256c759 --- /dev/null +++ b/internal/api/handlers/upload_file_command.go @@ -0,0 +1,9 @@ +package handlers + +import "io" + +type UploadFileCommand struct { + BucketName string + FileName string + Data io.Reader +} diff --git a/internal/api/handlers/upload_file_handler.go b/internal/api/handlers/upload_file_handler.go new file mode 100644 index 0000000..0907582 --- /dev/null +++ b/internal/api/handlers/upload_file_handler.go @@ -0,0 +1,25 @@ +package handlers + +import ( + "context" + "file-system/internal/domain/repository" +) + +type UploadFileHandler struct { + Repo repository.FileRepository +} + +func NewUploadFileHandler(repo repository.FileRepository) *UploadFileHandler { + return &UploadFileHandler{Repo: repo} +} + +func (h *UploadFileHandler) Handle(ctx context.Context, cmd UploadFileCommand) (string, error) { + // 业务逻辑:上传文件 + // 调用 Repository + err := h.Repo.UploadFile(ctx, cmd.BucketName, cmd.FileName, cmd.Data) + if err != nil { + // 简单的错误处理,实际可能需要包装为 BusinessException + return "", err + } + return "File uploaded successfully", nil +} diff --git a/internal/api/requests/bucket_requests.go b/internal/api/requests/bucket_requests.go new file mode 100644 index 0000000..a9fe3c4 --- /dev/null +++ b/internal/api/requests/bucket_requests.go @@ -0,0 +1,7 @@ +package requests + +type CreateBucketRequest struct { + BucketName string `form:"bucket_name" json:"bucket_name"` +} + +type ListBucketsRequest struct{} diff --git a/internal/api/requests/download_file_request.go b/internal/api/requests/download_file_request.go new file mode 100644 index 0000000..d2fa56e --- /dev/null +++ b/internal/api/requests/download_file_request.go @@ -0,0 +1,6 @@ +package requests + +type DownloadFileRequest struct { + BucketName string `form:"bucket_name"` + ObjectKey string `form:"object_key"` +} diff --git a/internal/api/requests/new_features_requests.go b/internal/api/requests/new_features_requests.go new file mode 100644 index 0000000..4a6db39 --- /dev/null +++ b/internal/api/requests/new_features_requests.go @@ -0,0 +1,38 @@ +package requests + +import ( + "file-system/internal/common" + "mime/multipart" +) + +type ListFilesRequest struct { + BucketName string `form:"bucket_name"` + Prefix string `form:"prefix"` + MaxKeys int32 `form:"max_keys"` + Token string `form:"token"` +} + +type GetFilePreviewRequest struct { + BucketName string `form:"bucket_name"` + ObjectKey string `form:"object_key"` +} + +type InitMultipartRequest struct { + BucketName string `json:"bucket_name"` + ObjectKey string `json:"object_key"` +} + +type UploadPartRequest struct { + BucketName string `form:"bucket_name"` + ObjectKey string `form:"object_key"` + UploadId string `form:"upload_id"` + PartNumber int32 `form:"part_number"` + File *multipart.FileHeader `form:"file"` +} + +type CompleteMultipartRequest struct { + BucketName string `json:"bucket_name"` + ObjectKey string `json:"object_key"` + UploadId string `json:"upload_id"` + Parts []common.Part `json:"parts"` +} diff --git a/internal/api/requests/upload_file_request.go b/internal/api/requests/upload_file_request.go new file mode 100644 index 0000000..1966670 --- /dev/null +++ b/internal/api/requests/upload_file_request.go @@ -0,0 +1,8 @@ +package requests + +import "mime/multipart" + +type UploadFileRequest struct { + BucketName string `form:"bucket_name"` + File *multipart.FileHeader `form:"file"` +} diff --git a/internal/api/validators/bucket_validators.go b/internal/api/validators/bucket_validators.go new file mode 100644 index 0000000..bc3fd99 --- /dev/null +++ b/internal/api/validators/bucket_validators.go @@ -0,0 +1,19 @@ +package validators + +import ( + "file-system/internal/api/requests" + "file-system/internal/common" +) + +type CreateBucketValidator struct{} + +func NewCreateBucketValidator() *CreateBucketValidator { + return &CreateBucketValidator{} +} + +func (v *CreateBucketValidator) Validate(req *requests.CreateBucketRequest) error { + if req.BucketName == "" { + return common.NewBusinessException("Bucket name cannot be empty") + } + return nil +} diff --git a/internal/api/validators/download_file_validator.go b/internal/api/validators/download_file_validator.go new file mode 100644 index 0000000..db31469 --- /dev/null +++ b/internal/api/validators/download_file_validator.go @@ -0,0 +1,22 @@ +package validators + +import ( + "file-system/internal/api/requests" + "file-system/internal/common" +) + +type DownloadFileValidator struct{} + +func NewDownloadFileValidator() *DownloadFileValidator { + return &DownloadFileValidator{} +} + +func (v *DownloadFileValidator) Validate(req *requests.DownloadFileRequest) error { + if req.BucketName == "" { + return common.NewBusinessException("Bucket name cannot be empty") + } + if req.ObjectKey == "" { + return common.NewBusinessException("Object key cannot be empty") + } + return nil +} diff --git a/internal/api/validators/new_features_validators.go b/internal/api/validators/new_features_validators.go new file mode 100644 index 0000000..2978000 --- /dev/null +++ b/internal/api/validators/new_features_validators.go @@ -0,0 +1,50 @@ +package validators + +import ( + "file-system/internal/api/requests" + "file-system/internal/common" +) + +type NewFeaturesValidator struct{} + +func NewNewFeaturesValidator() *NewFeaturesValidator { + return &NewFeaturesValidator{} +} + +func (v *NewFeaturesValidator) ValidateListFiles(req *requests.ListFilesRequest) error { + if req.BucketName == "" { + return common.NewBusinessException("Bucket name is required") + } + if req.MaxKeys <= 0 { + req.MaxKeys = 10 // default + } + return nil +} + +func (v *NewFeaturesValidator) ValidatePreview(req *requests.GetFilePreviewRequest) error { + if req.BucketName == "" || req.ObjectKey == "" { + return common.NewBusinessException("Bucket name and Object key are required") + } + return nil +} + +func (v *NewFeaturesValidator) ValidateInitMultipart(req *requests.InitMultipartRequest) error { + if req.BucketName == "" || req.ObjectKey == "" { + return common.NewBusinessException("Bucket name and Object key are required") + } + return nil +} + +func (v *NewFeaturesValidator) ValidateUploadPart(req *requests.UploadPartRequest) error { + if req.BucketName == "" || req.ObjectKey == "" || req.UploadId == "" || req.PartNumber <= 0 || req.File == nil { + return common.NewBusinessException("Missing required fields for upload part") + } + return nil +} + +func (v *NewFeaturesValidator) ValidateCompleteMultipart(req *requests.CompleteMultipartRequest) error { + if req.BucketName == "" || req.ObjectKey == "" || req.UploadId == "" || len(req.Parts) == 0 { + return common.NewBusinessException("Missing required fields for completion") + } + return nil +} diff --git a/internal/api/validators/upload_file_validator.go b/internal/api/validators/upload_file_validator.go new file mode 100644 index 0000000..d2ea205 --- /dev/null +++ b/internal/api/validators/upload_file_validator.go @@ -0,0 +1,22 @@ +package validators + +import ( + "file-system/internal/api/requests" + "file-system/internal/common" +) + +type UploadFileValidator struct{} + +func NewUploadFileValidator() *UploadFileValidator { + return &UploadFileValidator{} +} + +func (v *UploadFileValidator) Validate(req *requests.UploadFileRequest) error { + if req.BucketName == "" { + return common.NewBusinessException("Bucket name cannot be empty") + } + if req.File == nil { + return common.NewBusinessException("File is required") + } + return nil +} diff --git a/internal/common/config.go b/internal/common/config.go new file mode 100644 index 0000000..8d28db1 --- /dev/null +++ b/internal/common/config.go @@ -0,0 +1,28 @@ +package common + +import "os" + +type Config struct { + RustFSEndpoint string + RustFSAccessKeyID string + RustFSSecretAccessKey string + RustFSRegion string + ServerPort string +} + +func LoadConfig() *Config { + return &Config{ + RustFSEndpoint: getEnv("RUSTFS_ENDPOINT_URL", "http://192.168.1.29:20060"), // Default to docker-compose port + RustFSAccessKeyID: getEnv("RUSTFS_ACCESS_KEY_ID", "xiangning"), // Default from user input + RustFSSecretAccessKey: getEnv("RUSTFS_SECRET_ACCESS_KEY", "xn001624."), // Default from user input + RustFSRegion: getEnv("RUSTFS_REGION", "us-east-1"), // Default region + ServerPort: getEnv("SERVER_PORT", "8080"), + } +} + +func getEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} diff --git a/internal/common/errors.go b/internal/common/errors.go new file mode 100644 index 0000000..3d09b0b --- /dev/null +++ b/internal/common/errors.go @@ -0,0 +1,17 @@ +package common + +type BusinessException struct { + Message string + Code int +} + +func (e *BusinessException) Error() string { + return e.Message +} + +func NewBusinessException(message string) *BusinessException { + return &BusinessException{ + Message: message, + Code: 400, + } +} diff --git a/internal/common/types.go b/internal/common/types.go new file mode 100644 index 0000000..39380c3 --- /dev/null +++ b/internal/common/types.go @@ -0,0 +1,6 @@ +package common + +type Part struct { + PartNumber int32 + ETag string +} diff --git a/internal/domain/repository/file_repository.go b/internal/domain/repository/file_repository.go new file mode 100644 index 0000000..1bb2859 --- /dev/null +++ b/internal/domain/repository/file_repository.go @@ -0,0 +1,39 @@ +package repository + +import ( + "context" + "file-system/internal/common" + "io" + "time" +) + +type FileInfo struct { + Key string + Size int64 + LastModified time.Time + ETag string +} + +type ListFilesResult struct { + Files []FileInfo + NextContinuationToken *string +} + +type FileRepository interface { + UploadFile(ctx context.Context, bucketName string, objectKey string, data io.Reader) error + DownloadFile(ctx context.Context, bucketName string, objectKey string) (io.ReadCloser, error) + ListBuckets(ctx context.Context) ([]string, error) + CreateBucket(ctx context.Context, bucketName string) error + DeleteBucket(ctx context.Context, bucketName string) error + ListObjects(ctx context.Context, bucketName string) ([]string, 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) + + // 分片上传 + 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) + CompleteMultipartUpload(ctx context.Context, bucketName string, objectKey string, uploadId string, parts []common.Part) (string, error) + AbortMultipartUpload(ctx context.Context, bucketName string, objectKey string, uploadId string) error +} diff --git a/internal/infrastructure/mediator/mediator.go b/internal/infrastructure/mediator/mediator.go new file mode 100644 index 0000000..82983a8 --- /dev/null +++ b/internal/infrastructure/mediator/mediator.go @@ -0,0 +1,44 @@ +package mediator + +import ( + "context" + "fmt" + "reflect" +) + +type RequestHandler[TRequest any, TResponse any] interface { + Handle(ctx context.Context, request TRequest) (TResponse, error) +} + +type Mediator struct { + handlers map[reflect.Type]interface{} +} + +func NewMediator() *Mediator { + return &Mediator{ + handlers: make(map[reflect.Type]interface{}), + } +} + +func Register[TRequest any, TResponse any](m *Mediator, handler RequestHandler[TRequest, TResponse]) { + var req TRequest + t := reflect.TypeOf(req) + m.handlers[t] = handler +} + +func Send[TRequest any, TResponse any](m *Mediator, ctx context.Context, request TRequest) (TResponse, error) { + t := reflect.TypeOf(request) + handler, ok := m.handlers[t] + if !ok { + var zero TResponse + return zero, fmt.Errorf("handler not found for %v", t) + } + + h, ok := handler.(RequestHandler[TRequest, TResponse]) + if !ok { + var zero TResponse + return zero, fmt.Errorf("handler type mismatch") + } + + return h.Handle(ctx, request) +} diff --git a/internal/infrastructure/s3/client.go b/internal/infrastructure/s3/client.go new file mode 100644 index 0000000..1652614 --- /dev/null +++ b/internal/infrastructure/s3/client.go @@ -0,0 +1,49 @@ +package s3 + +import ( + "context" + "file-system/internal/common" + "log" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +type RustFSClient struct { + Client *s3.Client + PresignClient *s3.PresignClient +} + +func NewRustFSClient(cfg *common.Config) *RustFSClient { + // Custom Endpoint Resolver + customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { + return aws.Endpoint{ + URL: cfg.RustFSEndpoint, + SigningRegion: cfg.RustFSRegion, + }, nil + }) + + awsCfg, err := config.LoadDefaultConfig(context.TODO(), + config.WithRegion(cfg.RustFSRegion), + config.WithEndpointResolverWithOptions(customResolver), + config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( + cfg.RustFSAccessKeyID, + cfg.RustFSSecretAccessKey, + "", + )), + ) + if err != nil { + log.Fatalf("unable to load SDK config, %v", err) + } + + client := s3.NewFromConfig(awsCfg, func(o *s3.Options) { + o.UsePathStyle = true + }) + + return &RustFSClient{ + Client: client, + PresignClient: s3.NewPresignClient(client), + } +} diff --git a/internal/infrastructure/s3/file_repository_impl.go b/internal/infrastructure/s3/file_repository_impl.go new file mode 100644 index 0000000..5c671b3 --- /dev/null +++ b/internal/infrastructure/s3/file_repository_impl.go @@ -0,0 +1,198 @@ +package s3 + +import ( + "context" + "file-system/internal/common" + "file-system/internal/domain/repository" + "io" + "sort" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3/types" +) + +type S3FileRepository struct { + client *RustFSClient +} + +func NewS3FileRepository(client *RustFSClient) repository.FileRepository { + return &S3FileRepository{client: client} +} + +func (r *S3FileRepository) UploadFile(ctx context.Context, bucketName string, objectKey string, data io.Reader) error { + _, err := r.client.Client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(objectKey), + Body: data, + }) + return err +} + +func (r *S3FileRepository) DownloadFile(ctx context.Context, bucketName string, objectKey string) (io.ReadCloser, error) { + resp, err := r.client.Client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(objectKey), + }) + if err != nil { + return nil, err + } + return resp.Body, nil +} + +func (r *S3FileRepository) ListBuckets(ctx context.Context) ([]string, error) { + resp, err := r.client.Client.ListBuckets(ctx, &s3.ListBucketsInput{}) + if err != nil { + return nil, err + } + var buckets []string + for _, b := range resp.Buckets { + if b.Name != nil { + buckets = append(buckets, *b.Name) + } + } + return buckets, nil +} + +func (r *S3FileRepository) CreateBucket(ctx context.Context, bucketName string) error { + _, err := r.client.Client.CreateBucket(ctx, &s3.CreateBucketInput{ + Bucket: aws.String(bucketName), + }) + return err +} + +func (r *S3FileRepository) DeleteBucket(ctx context.Context, bucketName string) error { + _, err := r.client.Client.DeleteBucket(ctx, &s3.DeleteBucketInput{ + Bucket: aws.String(bucketName), + }) + return err +} + +func (r *S3FileRepository) ListObjects(ctx context.Context, bucketName string) ([]string, error) { + resp, err := r.client.Client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ + Bucket: aws.String(bucketName), + }) + if err != nil { + return nil, err + } + var objects []string + for _, obj := range resp.Contents { + if obj.Key != nil { + objects = append(objects, *obj.Key) + } + } + return objects, nil +} + +// ListObjectsV2 分页列出文件 +func (r *S3FileRepository) ListObjectsV2(ctx context.Context, bucketName string, prefix string, maxKeys int32, continuationToken *string) (*repository.ListFilesResult, error) { + input := &s3.ListObjectsV2Input{ + Bucket: aws.String(bucketName), + Prefix: aws.String(prefix), + MaxKeys: aws.Int32(maxKeys), + } + if continuationToken != nil && *continuationToken != "" { + input.ContinuationToken = continuationToken + } + + resp, err := r.client.Client.ListObjectsV2(ctx, input) + if err != nil { + return nil, err + } + + files := make([]repository.FileInfo, 0, len(resp.Contents)) + for _, obj := range resp.Contents { + files = append(files, repository.FileInfo{ + Key: *obj.Key, + Size: *obj.Size, + LastModified: *obj.LastModified, + ETag: *obj.ETag, + }) + } + + return &repository.ListFilesResult{ + Files: files, + NextContinuationToken: resp.NextContinuationToken, + }, nil +} + +// GeneratePresignedURL 生成预签名链接 +func (r *S3FileRepository) GeneratePresignedURL(ctx context.Context, bucketName string, objectKey string, expiry time.Duration) (string, error) { + presignResult, err := r.client.PresignClient.PresignGetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(objectKey), + }, func(opts *s3.PresignOptions) { + opts.Expires = expiry + }) + if err != nil { + return "", err + } + return presignResult.URL, nil +} + +// CreateMultipartUpload 初始化分片上传 +func (r *S3FileRepository) CreateMultipartUpload(ctx context.Context, bucketName string, objectKey string) (string, error) { + resp, err := r.client.Client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{ + Bucket: aws.String(bucketName), + Key: aws.String(objectKey), + }) + if err != nil { + return "", err + } + return *resp.UploadId, nil +} + +// UploadPart 上传分片 +func (r *S3FileRepository) UploadPart(ctx context.Context, bucketName string, objectKey string, uploadId string, partNumber int32, data io.Reader) (string, error) { + resp, err := r.client.Client.UploadPart(ctx, &s3.UploadPartInput{ + Bucket: aws.String(bucketName), + Key: aws.String(objectKey), + UploadId: aws.String(uploadId), + PartNumber: aws.Int32(partNumber), + Body: data, + }) + if err != nil { + return "", err + } + return *resp.ETag, nil +} + +// CompleteMultipartUpload 完成分片上传 +func (r *S3FileRepository) CompleteMultipartUpload(ctx context.Context, bucketName string, objectKey string, uploadId string, parts []common.Part) (string, error) { + // 需要按 PartNumber 排序 + sort.Slice(parts, func(i, j int) bool { + return parts[i].PartNumber < parts[j].PartNumber + }) + + completedParts := make([]types.CompletedPart, len(parts)) + for i, p := range parts { + completedParts[i] = types.CompletedPart{ + ETag: aws.String(p.ETag), + PartNumber: aws.Int32(p.PartNumber), + } + } + + resp, err := r.client.Client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{ + Bucket: aws.String(bucketName), + Key: aws.String(objectKey), + UploadId: aws.String(uploadId), + MultipartUpload: &types.CompletedMultipartUpload{ + Parts: completedParts, + }, + }) + if err != nil { + return "", err + } + return *resp.Location, nil +} + +// AbortMultipartUpload 取消分片上传 +func (r *S3FileRepository) AbortMultipartUpload(ctx context.Context, bucketName string, objectKey string, uploadId string) error { + _, err := r.client.Client.AbortMultipartUpload(ctx, &s3.AbortMultipartUploadInput{ + Bucket: aws.String(bucketName), + Key: aws.String(objectKey), + UploadId: aws.String(uploadId), + }) + return err +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..bf36fa9 --- /dev/null +++ b/web/index.html @@ -0,0 +1,541 @@ + + +
+ + +| 文件名 | +大小 | +修改时间 | +操作 | +
|---|---|---|---|
| + + {{ file.Key }} + | +{{ formatSize(file.Size) }} | +{{ formatDate(file.LastModified) }} | ++ + + + + + + | +
| + + 暂无文件 + | +|||