Initial commit
This commit is contained in:
commit
8232827835
37
.gitlab-ci.yml
Normal file
37
.gitlab-ci.yml
Normal 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
34
Dockerfile
Normal 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
109
cmd/server/main.go
Normal 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
35
docker-compose.yml
Normal 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
647
docs/docs.go
Normal 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
623
docs/swagger.json
Normal 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
412
docs/swagger.yaml
Normal 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
73
go.mod
Normal 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
204
go.sum
Normal 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=
|
||||
81
internal/api/endpoints/bucket_endpoint.go
Normal file
81
internal/api/endpoints/bucket_endpoint.go
Normal 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})
|
||||
}
|
||||
330
internal/api/endpoints/file_endpoint.go
Normal file
330
internal/api/endpoints/file_endpoint.go
Normal 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()})
|
||||
}
|
||||
}
|
||||
7
internal/api/handlers/bucket_commands.go
Normal file
7
internal/api/handlers/bucket_commands.go
Normal file
@ -0,0 +1,7 @@
|
||||
package handlers
|
||||
|
||||
type CreateBucketCommand struct {
|
||||
BucketName string
|
||||
}
|
||||
|
||||
type ListBucketsQuery struct{}
|
||||
34
internal/api/handlers/bucket_handlers.go
Normal file
34
internal/api/handlers/bucket_handlers.go
Normal 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)
|
||||
}
|
||||
19
internal/api/handlers/download_file_handler.go
Normal file
19
internal/api/handlers/download_file_handler.go
Normal 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)
|
||||
}
|
||||
6
internal/api/handlers/download_file_query.go
Normal file
6
internal/api/handlers/download_file_query.go
Normal file
@ -0,0 +1,6 @@
|
||||
package handlers
|
||||
|
||||
type DownloadFileQuery struct {
|
||||
BucketName string
|
||||
ObjectKey string
|
||||
}
|
||||
45
internal/api/handlers/multipart_handlers_split.go
Normal file
45
internal/api/handlers/multipart_handlers_split.go
Normal 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)
|
||||
}
|
||||
69
internal/api/handlers/query_handlers.go
Normal file
69
internal/api/handlers/query_handlers.go
Normal 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)
|
||||
}
|
||||
9
internal/api/handlers/upload_file_command.go
Normal file
9
internal/api/handlers/upload_file_command.go
Normal file
@ -0,0 +1,9 @@
|
||||
package handlers
|
||||
|
||||
import "io"
|
||||
|
||||
type UploadFileCommand struct {
|
||||
BucketName string
|
||||
FileName string
|
||||
Data io.Reader
|
||||
}
|
||||
25
internal/api/handlers/upload_file_handler.go
Normal file
25
internal/api/handlers/upload_file_handler.go
Normal 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
|
||||
}
|
||||
7
internal/api/requests/bucket_requests.go
Normal file
7
internal/api/requests/bucket_requests.go
Normal file
@ -0,0 +1,7 @@
|
||||
package requests
|
||||
|
||||
type CreateBucketRequest struct {
|
||||
BucketName string `form:"bucket_name" json:"bucket_name"`
|
||||
}
|
||||
|
||||
type ListBucketsRequest struct{}
|
||||
6
internal/api/requests/download_file_request.go
Normal file
6
internal/api/requests/download_file_request.go
Normal file
@ -0,0 +1,6 @@
|
||||
package requests
|
||||
|
||||
type DownloadFileRequest struct {
|
||||
BucketName string `form:"bucket_name"`
|
||||
ObjectKey string `form:"object_key"`
|
||||
}
|
||||
38
internal/api/requests/new_features_requests.go
Normal file
38
internal/api/requests/new_features_requests.go
Normal 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"`
|
||||
}
|
||||
8
internal/api/requests/upload_file_request.go
Normal file
8
internal/api/requests/upload_file_request.go
Normal file
@ -0,0 +1,8 @@
|
||||
package requests
|
||||
|
||||
import "mime/multipart"
|
||||
|
||||
type UploadFileRequest struct {
|
||||
BucketName string `form:"bucket_name"`
|
||||
File *multipart.FileHeader `form:"file"`
|
||||
}
|
||||
19
internal/api/validators/bucket_validators.go
Normal file
19
internal/api/validators/bucket_validators.go
Normal 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
|
||||
}
|
||||
22
internal/api/validators/download_file_validator.go
Normal file
22
internal/api/validators/download_file_validator.go
Normal 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
|
||||
}
|
||||
50
internal/api/validators/new_features_validators.go
Normal file
50
internal/api/validators/new_features_validators.go
Normal 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
|
||||
}
|
||||
22
internal/api/validators/upload_file_validator.go
Normal file
22
internal/api/validators/upload_file_validator.go
Normal 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
28
internal/common/config.go
Normal 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
17
internal/common/errors.go
Normal 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
6
internal/common/types.go
Normal file
@ -0,0 +1,6 @@
|
||||
package common
|
||||
|
||||
type Part struct {
|
||||
PartNumber int32
|
||||
ETag string
|
||||
}
|
||||
39
internal/domain/repository/file_repository.go
Normal file
39
internal/domain/repository/file_repository.go
Normal 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
|
||||
}
|
||||
44
internal/infrastructure/mediator/mediator.go
Normal file
44
internal/infrastructure/mediator/mediator.go
Normal 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)
|
||||
}
|
||||
49
internal/infrastructure/s3/client.go
Normal file
49
internal/infrastructure/s3/client.go
Normal 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),
|
||||
}
|
||||
}
|
||||
198
internal/infrastructure/s3/file_repository_impl.go
Normal file
198
internal/infrastructure/s3/file_repository_impl.go
Normal 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
541
web/index.html
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user