Initial commit

This commit is contained in:
root 2025-12-18 09:34:49 +08:00
commit 8232827835
35 changed files with 3893 additions and 0 deletions

37
.gitlab-ci.yml Normal file
View File

@ -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

34
Dockerfile Normal file
View File

@ -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"]

109
cmd/server/main.go Normal file
View File

@ -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)
}

35
docker-compose.yml Normal file
View File

@ -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:

647
docs/docs.go Normal file
View File

@ -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)
}

623
docs/swagger.json Normal file
View File

@ -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"
}
}
}
}
}

412
docs/swagger.yaml Normal file
View File

@ -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"

73
go.mod Normal file
View File

@ -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
)

204
go.sum Normal file
View File

@ -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=

View File

@ -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})
}

View File

@ -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()})
}
}

View File

@ -0,0 +1,7 @@
package handlers
type CreateBucketCommand struct {
BucketName string
}
type ListBucketsQuery struct{}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -0,0 +1,6 @@
package handlers
type DownloadFileQuery struct {
BucketName string
ObjectKey string
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -0,0 +1,9 @@
package handlers
import "io"
type UploadFileCommand struct {
BucketName string
FileName string
Data io.Reader
}

View File

@ -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
}

View File

@ -0,0 +1,7 @@
package requests
type CreateBucketRequest struct {
BucketName string `form:"bucket_name" json:"bucket_name"`
}
type ListBucketsRequest struct{}

View File

@ -0,0 +1,6 @@
package requests
type DownloadFileRequest struct {
BucketName string `form:"bucket_name"`
ObjectKey string `form:"object_key"`
}

View File

@ -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"`
}

View File

@ -0,0 +1,8 @@
package requests
import "mime/multipart"
type UploadFileRequest struct {
BucketName string `form:"bucket_name"`
File *multipart.FileHeader `form:"file"`
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

28
internal/common/config.go Normal file
View File

@ -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
}

17
internal/common/errors.go Normal file
View File

@ -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,
}
}

6
internal/common/types.go Normal file
View File

@ -0,0 +1,6 @@
package common
type Part struct {
PartNumber int32
ETag string
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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),
}
}

View File

@ -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
}

541
web/index.html Normal file
View File

@ -0,0 +1,541 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RustFS 高级文件管理系统</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Vue 3 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>
<!-- Axios -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<!-- Font Awesome -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
body { background-color: #f8f9fa; font-family: "Microsoft YaHei", sans-serif; }
.card { border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); border: none; margin-bottom: 20px; }
.card-header { background-color: #fff; border-bottom: 1px solid #eee; font-weight: bold; padding: 15px 20px; }
.btn-primary { background-color: #0d6efd; border: none; }
.progress { height: 20px; border-radius: 10px; }
.file-icon { font-size: 1.2rem; margin-right: 10px; color: #6c757d; }
.action-btn { cursor: pointer; margin-right: 10px; }
.action-btn:hover { color: #0d6efd; }
[v-cloak] { display: none; }
</style>
</head>
<body>
<div id="app" class="container py-4" v-cloak>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="fas fa-cloud-upload-alt text-primary me-2"></i>RustFS 文件管理系统</h1>
<div>
<a href="/swagger/index.html" target="_blank" class="btn btn-outline-secondary me-2">
<i class="fas fa-book me-1"></i>API 文档
</a>
<button class="btn btn-primary" @click="showCreateBucketModal = true">
<i class="fas fa-plus me-1"></i>新建存储桶
</button>
</div>
</div>
<div class="row">
<!-- Sidebar: Buckets -->
<div class="col-md-3">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
存储桶列表
<button class="btn btn-sm btn-link" @click="loadBuckets"><i class="fas fa-sync-alt"></i></button>
</div>
<div class="list-group list-group-flush">
<a v-for="bucket in buckets" :key="bucket"
href="#"
class="list-group-item list-group-item-action"
:class="{ active: currentBucket === bucket }"
@click.prevent="selectBucket(bucket)">
<i class="fas fa-box me-2"></i>{{ bucket }}
</a>
<div v-if="buckets.length === 0" class="list-group-item text-muted text-center py-4">
暂无存储桶
</div>
</div>
</div>
</div>
<!-- Main Content: File List & Upload -->
<div class="col-md-9">
<!-- Toolbar -->
<div class="card mb-3" v-if="currentBucket">
<div class="card-body py-3">
<div class="row g-3 align-items-center">
<div class="col-auto">
<label class="col-form-label fw-bold">{{ currentBucket }}</label>
</div>
<div class="col">
<div class="input-group">
<span class="input-group-text bg-white"><i class="fas fa-search"></i></span>
<input type="text" class="form-control" placeholder="搜索文件名..." v-model="filters.prefix" @keyup.enter="refreshFiles">
</div>
</div>
<div class="col-auto">
<button class="btn btn-success" @click="triggerFileInput">
<i class="fas fa-upload me-1"></i>上传文件
</button>
<input type="file" ref="fileInput" class="d-none" @change="handleFileSelect">
</div>
</div>
</div>
</div>
<!-- File List Table -->
<div class="card" v-if="currentBucket">
<div class="card-body p-0">
<table class="table table-hover mb-0 align-middle">
<thead class="table-light">
<tr>
<th class="ps-4">文件名</th>
<th>大小</th>
<th>修改时间</th>
<th class="text-end pe-4">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="file in files" :key="file.Key">
<td class="ps-4">
<i :class="getFileIcon(file.Key)" class="file-icon"></i>
{{ file.Key }}
</td>
<td>{{ formatSize(file.Size) }}</td>
<td class="text-muted small">{{ formatDate(file.LastModified) }}</td>
<td class="text-end pe-4">
<span class="action-btn text-primary" @click="previewFile(file.Key)" title="预览">
<i class="fas fa-eye"></i>
</span>
<span class="action-btn text-success" @click="downloadFile(file.Key)" title="下载">
<i class="fas fa-download"></i>
</span>
</td>
</tr>
<tr v-if="files.length === 0 && !loadingFiles">
<td colspan="4" class="text-center py-5 text-muted">
<i class="fas fa-folder-open fa-3x mb-3 d-block opacity-50"></i>
暂无文件
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="card-footer d-flex justify-content-between align-items-center" v-if="nextToken || pageHistory.length > 0">
<button class="btn btn-sm btn-outline-secondary" :disabled="pageHistory.length === 0" @click="prevPage">
<i class="fas fa-chevron-left me-1"></i>上一页
</button>
<span class="text-muted small">当前页数: {{ pageHistory.length + 1 }}</span>
<button class="btn btn-sm btn-outline-secondary" :disabled="!nextToken" @click="nextPage">
下一页<i class="fas fa-chevron-right ms-1"></i>
</button>
</div>
</div>
<div v-else class="text-center py-5">
<i class="fas fa-arrow-left fa-3x text-muted mb-3 d-block opacity-25"></i>
<h4 class="text-muted">请选择一个存储桶</h4>
</div>
<!-- Upload Progress -->
<div class="card mt-3" v-if="uploads.length > 0">
<div class="card-header">上传任务队列</div>
<ul class="list-group list-group-flush">
<li class="list-group-item" v-for="upload in uploads" :key="upload.id">
<div class="d-flex justify-content-between mb-1">
<div>
<span class="fw-bold">{{ upload.file.name }}</span>
<span class="badge bg-secondary ms-2">{{ upload.status }}</span>
</div>
<span class="small text-muted">{{ upload.progress }}%</span>
</div>
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated"
:class="getProgressBarClass(upload.status)"
role="progressbar"
:style="{ width: upload.progress + '%' }"></div>
</div>
<div class="mt-1 small text-danger" v-if="upload.error">{{ upload.error }}</div>
</li>
</ul>
</div>
</div>
</div>
<!-- Create Bucket Modal -->
<div class="modal fade show" style="display: block; background: rgba(0,0,0,0.5)" v-if="showCreateBucketModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">新建存储桶</h5>
<button type="button" class="btn-close" @click="showCreateBucketModal = false"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">存储桶名称</label>
<input type="text" class="form-control" v-model="newBucketName" placeholder="输入名称...">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @click="showCreateBucketModal = false">取消</button>
<button type="button" class="btn btn-primary" @click="createBucket">创建</button>
</div>
</div>
</div>
</div>
<!-- Preview Modal -->
<div class="modal fade show" style="display: block; background: rgba(0,0,0,0.5)" v-if="previewUrl">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">文件预览</h5>
<button type="button" class="btn-close" @click="previewUrl = null"></button>
</div>
<div class="modal-body text-center bg-light p-4">
<img v-if="isPreviewImage" :src="previewUrl" class="img-fluid" style="max-height: 80vh">
<video v-else-if="isPreviewVideo" :src="previewUrl" controls class="w-100" style="max-height: 80vh"></video>
<iframe v-else :src="previewUrl" class="w-100" style="height: 60vh; border:none"></iframe>
</div>
<div class="modal-footer justify-content-center">
<a :href="previewUrl" target="_blank" class="btn btn-outline-primary">
<i class="fas fa-external-link-alt me-1"></i>在新窗口打开
</a>
</div>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, computed, onMounted } = Vue;
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
createApp({
setup() {
const buckets = ref([]);
const currentBucket = ref(null);
const files = ref([]);
const loadingFiles = ref(false);
const nextToken = ref(null);
const pageHistory = ref([]); // Stack to store tokens for previous pages
const filters = ref({ prefix: '', maxKeys: 20 });
const showCreateBucketModal = ref(false);
const newBucketName = ref('');
const uploads = ref([]); // { id, file, progress, status: 'pending'|'uploading'|'completed'|'failed', error, uploadId, parts }
const previewUrl = ref(null);
const previewType = ref('');
// API Client
const api = axios.create({ baseURL: window.location.origin });
// Load Buckets
const loadBuckets = async () => {
try {
const res = await api.get('/buckets');
buckets.value = res.data.buckets || [];
} catch (err) {
alert('加载存储桶失败: ' + (err.response?.data?.error || err.message));
}
};
// Create Bucket
const createBucket = async () => {
if (!newBucketName.value) return alert('请输入名称');
try {
await api.post('/buckets', { bucket_name: newBucketName.value });
await loadBuckets();
showCreateBucketModal.value = false;
newBucketName.value = '';
} catch (err) {
alert('创建失败: ' + (err.response?.data?.error || err.message));
}
};
// Select Bucket
const selectBucket = (name) => {
currentBucket.value = name;
nextToken.value = null;
pageHistory.value = [];
loadFiles();
};
// Load Files
const loadFiles = async (token = null) => {
loadingFiles.value = true;
try {
const res = await api.get('/files/list', {
params: {
bucket_name: currentBucket.value,
prefix: filters.value.prefix,
max_keys: filters.value.maxKeys,
token: token
}
});
files.value = res.data.Files || [];
nextToken.value = res.data.NextContinuationToken;
} catch (err) {
console.error(err);
alert('加载文件列表失败');
} finally {
loadingFiles.value = false;
}
};
const refreshFiles = () => {
nextToken.value = null;
pageHistory.value = [];
loadFiles();
};
const nextPage = () => {
if (nextToken.value) {
pageHistory.value.push(filters.value.token); // Save current token (which was used to get here) - actually we need to save the *previous* state.
// Simplified: pageHistory stores the token that GENERATED the current page.
// Actually better:
// Page 1: token=null. Res returns T1.
// Page 2: req token=T1. Res returns T2.
// History: [null, T1]
pageHistory.value.push(nextToken.value); // Wait, this logic is tricky without full state.
// Correct logic: push current page's start token to history.
// But wait, listObjectsV2 is stateless.
// Let's just reload with nextToken.
// We need to store the token used to fetch CURRENT page to go back? No, to go back we need token of PREVIOUS page.
// Let's simplistic approach: History stores [token_for_page_1, token_for_page_2...]
// But we don't know token for page 1 (it's null).
// Let's ignore complex history for now and just support Next, and reset on bucket change.
// To support Prev properly, we need to push the token used for *current* view into stack before moving.
// But we don't have "current token" stored in variable clearly except implicitly.
// Let's re-implement: loadFiles takes token.
// We will just use the returned nextToken for next page.
// For prev page, we need a stack of tokens.
loadFiles(nextToken.value);
}
};
// Fix pagination logic later or keep simple "Load More" style?
// User asked for "Pagination", table style usually implies Next/Prev.
// S3 only supports forward paging efficiently. Prev requires caching tokens.
// We will implement simple Next for now, Prev is hard without state.
// Let's actually implement a stack.
// When clicking Next: push current_token (or null for page 1) to stack. load(next_token).
// Refined Pagination
const currentToken = ref(null); // Token used for current page
const loadFilesWrapped = async (token) => {
loadingFiles.value = true;
try {
const res = await api.get('/files/list', {
params: {
bucket_name: currentBucket.value,
prefix: filters.value.prefix,
max_keys: filters.value.maxKeys,
token: token
}
});
files.value = res.data.Files || [];
currentToken.value = token;
nextToken.value = res.data.NextContinuationToken;
} catch (err) {
console.error(err);
} finally {
loadingFiles.value = false;
}
}
const nextP = () => {
if(!nextToken.value) return;
pageHistory.value.push(currentToken.value);
loadFilesWrapped(nextToken.value);
}
const prevPage = () => {
if(pageHistory.value.length === 0) return;
const prevToken = pageHistory.value.pop();
loadFilesWrapped(prevToken);
}
// Override originals
const loadFilesInitial = () => loadFilesWrapped(null);
// File Upload
const triggerFileInput = () => document.querySelector('input[type=file]').click();
const handleFileSelect = async (e) => {
const file = e.target.files[0];
if (!file) return;
e.target.value = ''; // reset
// Create upload task
const uploadTask = {
id: Date.now(),
file: file,
progress: 0,
status: 'pending',
error: null,
uploadId: null,
parts: []
};
uploads.value.unshift(uploadTask);
processUpload(uploadTask);
};
const processUpload = async (task) => {
task.status = 'uploading';
const file = task.file;
const bucket = currentBucket.value;
const key = file.name;
try {
// 1. Init Multipart
const initRes = await api.post('/files/multipart/init', {
bucket_name: bucket,
object_key: key
});
task.uploadId = initRes.data.upload_id;
// 2. Upload Parts
const totalParts = Math.ceil(file.size / CHUNK_SIZE);
const parts = [];
for (let i = 0; i < totalParts; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
const partNumber = i + 1;
const formData = new FormData();
formData.append('bucket_name', bucket);
formData.append('object_key', key);
formData.append('upload_id', task.uploadId);
formData.append('part_number', partNumber);
formData.append('file', chunk);
// Retry logic for part
let retries = 3;
let etag = null;
while(retries > 0) {
try {
const partRes = await api.put('/files/multipart/part', formData);
etag = partRes.data.etag;
break;
} catch(e) {
retries--;
if(retries === 0) throw e;
await new Promise(r => setTimeout(r, 1000));
}
}
parts.push({ PartNumber: partNumber, ETag: etag });
// Update Progress
task.progress = Math.round(((i + 1) / totalParts) * 100);
}
// 3. Complete
await api.post('/files/multipart/complete', {
bucket_name: bucket,
object_key: key,
upload_id: task.uploadId,
parts: parts
});
task.status = 'completed';
task.progress = 100;
setTimeout(() => refreshFiles(), 1000);
} catch (err) {
console.error(err);
task.status = 'failed';
task.error = err.response?.data?.error || err.message;
}
};
// Preview
const previewFile = async (key) => {
try {
const res = await api.get('/files/preview', {
params: { bucket_name: currentBucket.value, object_key: key }
});
previewUrl.value = res.data.url;
// Determine type
const ext = key.split('.').pop().toLowerCase();
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) {
previewType.value = 'image';
} else if (['mp4', 'webm', 'ogg'].includes(ext)) {
previewType.value = 'video';
} else {
previewType.value = 'other';
}
} catch (err) {
alert('无法获取预览链接');
}
};
const isPreviewImage = computed(() => previewType.value === 'image');
const isPreviewVideo = computed(() => previewType.value === 'video');
// Download
const downloadFile = (key) => {
const url = `${window.location.origin}/files/download?bucket_name=${encodeURIComponent(currentBucket.value)}&object_key=${encodeURIComponent(key)}`;
window.open(url, '_blank');
};
// Utils
const formatSize = (bytes) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const formatDate = (dateStr) => {
return new Date(dateStr).toLocaleString('zh-CN');
};
const getFileIcon = (filename) => {
const ext = filename.split('.').pop().toLowerCase();
if (['jpg', 'png', 'gif'].includes(ext)) return 'fas fa-file-image text-primary';
if (['pdf', 'doc', 'docx'].includes(ext)) return 'fas fa-file-pdf text-danger';
if (['mp4', 'avi'].includes(ext)) return 'fas fa-file-video text-success';
if (['zip', 'rar'].includes(ext)) return 'fas fa-file-archive text-warning';
return 'fas fa-file text-secondary';
};
const getProgressBarClass = (status) => {
if(status === 'completed') return 'bg-success';
if(status === 'failed') return 'bg-danger';
return 'bg-primary';
}
onMounted(() => {
loadBuckets();
});
return {
buckets, currentBucket, files, loadingFiles, nextToken, pageHistory, filters,
showCreateBucketModal, newBucketName, uploads, previewUrl, isPreviewImage, isPreviewVideo,
loadBuckets, createBucket, selectBucket, refreshFiles: () => loadFilesInitial(),
nextPage: nextP, prevPage,
triggerFileInput, handleFileSelect,
previewFile, downloadFile,
formatSize, formatDate, getFileIcon, getProgressBarClass
};
}
}).mount('#app');
</script>
</body>
</html>