Compare commits
17 Commits
3a18ca0579
...
1cd46bc6db
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cd46bc6db | ||
|
|
11315fd00b | ||
|
|
3eb1a1839d | ||
|
|
95f76bbc70 | ||
|
|
4b35503b4f | ||
|
|
80507c0e18 | ||
|
|
42addaea7d | ||
|
|
dfaead4766 | ||
|
|
b9edb7b7de | ||
|
|
2647314fe7 | ||
|
|
4927de90cc | ||
|
|
bcd637387a | ||
|
|
7faddfed05 | ||
|
|
b9b5838938 | ||
|
|
1cfa43a33c | ||
|
|
ed47904a85 | ||
|
|
654b7d9bb6 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,6 +5,8 @@
|
||||
*.so
|
||||
*.dylib
|
||||
file-service
|
||||
/server
|
||||
bin/
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
188
CLAUDE.md
188
CLAUDE.md
@ -1,7 +1,5 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
> **Cross-repo rules** — see `/Users/wen/project/rag/CLAUDE.md` for full workspace conventions.
|
||||
> - .NET backends share JWT key: `RagJwtSecretKey2026MustBeAtLeast32CharsLong!`
|
||||
> - gRPC auth: call `rag-backend:50051` for token validation + permission checks
|
||||
@ -10,130 +8,138 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
## Build & Run
|
||||
|
||||
```bash
|
||||
go run cmd/server/main.go # HTTP :8080
|
||||
go test ./... # All tests
|
||||
docker compose up -d # Via private registry
|
||||
buf generate # Regenerate proto code
|
||||
go run ./cmd/server -conf configs/config.yaml # HTTP :8080, gRPC :9000
|
||||
make api # Regenerate proto code
|
||||
make wire # Regenerate Wire DI
|
||||
go test ./... # All tests
|
||||
buf generate # Regenerate proto
|
||||
docker compose up -d # Via local build
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
Go 1.25 microservice. Clean Architecture: `domain` ← `infrastructure` ← `api`.
|
||||
Go 1.25 microservice. Kratos framework + DDD four-layer + Watermill CQRS.
|
||||
|
||||
```
|
||||
cmd/server/main.go → Entry point, manual wiring
|
||||
internal/domain/repository/ → Pure interfaces (FileRepository)
|
||||
internal/infrastructure/
|
||||
s3/ → S3 adapter (AWS SDK v2, RustFS/MinIO-compatible)
|
||||
grpc/ → gRPC auth client (token validation via rag-backend)
|
||||
mediator/ → Generic CQRS mediator (Go generics + reflect)
|
||||
internal/api/
|
||||
endpoints/ → Gin HTTP handlers (thin controllers)
|
||||
handlers/ → CQRS command/query handlers (business logic)
|
||||
requests/ → Request DTOs (form/json struct tags)
|
||||
validators/ → Input validation + sanitization
|
||||
internal/middleware/ → Auth, CORS, logging, rate limit, request ID, timeout
|
||||
internal/common/ → Config (env-only), errors, logger, OTel, sanitization
|
||||
api/proto/ → Protobuf definitions + generated Go code
|
||||
cmd/server/main.go → Entry point, load config
|
||||
cmd/server/wire.go → Wire DI declarations
|
||||
cmd/server/wire_gen.go → Wire generated code
|
||||
api/file/v1/ → Proto definitions (HTTP+gRPC dual protocol)
|
||||
internal/conf/ → Config structs (proto-defined)
|
||||
internal/biz/ → Business logic layer (repo interface definitions + usecases)
|
||||
internal/data/ → Data access layer (GORM + S3, implements biz repo interfaces)
|
||||
internal/service/ → Service implementation (implements proto Service interface)
|
||||
internal/server/ → HTTP/gRPC server creation and middleware
|
||||
internal/watermark/ → Watermill CQRS (CommandBus + EventBus)
|
||||
internal/pkg/sanitize/ → Input sanitization utilities
|
||||
internal/pkg/s3errors/ → S3 error mapping
|
||||
configs/config.yaml → Local dev config
|
||||
```
|
||||
|
||||
## Layered Call Chain
|
||||
|
||||
```
|
||||
service (DTO conversion) → biz (business logic) → data (data access)
|
||||
↕
|
||||
watermark (CQRS commands/events)
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Kratos** — HTTP + gRPC framework, proto-first API definition
|
||||
- **Wire** — Compile-time dependency injection
|
||||
- **Watermill** — CQRS (CommandBus + EventBus), PGSQL as message store
|
||||
- **GORM** — Business data ORM (folders, files, share_links)
|
||||
- **AWS SDK v2** — S3 interface to RustFS/MinIO
|
||||
- **PostgreSQL** — Business data + Watermill message queue
|
||||
|
||||
## Code Patterns
|
||||
|
||||
### Generic Mediator (CQRS)
|
||||
### Wire ProviderSet Pattern
|
||||
|
||||
Each layer defines a ProviderSet:
|
||||
```go
|
||||
// Define command/query struct
|
||||
type UploadFileCommand struct { BucketName string; FileName string; Data io.Reader }
|
||||
// internal/data/data.go
|
||||
var ProviderSet = wire.NewSet(NewData, NewFileRepo, NewFolderRepo, NewFileMetaRepo, NewShareRepo)
|
||||
|
||||
// Handler implements generic interface
|
||||
type UploadFileHandler struct { Repo repository.FileRepository }
|
||||
func (h *UploadFileHandler) Handle(ctx context.Context, cmd UploadFileCommand) (string, error) { ... }
|
||||
// internal/biz/biz.go
|
||||
var ProviderSet = wire.NewSet(NewFileUsecase, NewBucketUsecase, NewFolderUsecase, NewShareUsecase)
|
||||
|
||||
// Register: mediator.Register[RequestType, ResponseType](m, handler)
|
||||
mediator.Register[UploadFileCommand, string](m, uploadHandler)
|
||||
// internal/service/service.go
|
||||
var ProviderSet = wire.NewSet(NewFileService)
|
||||
|
||||
// Dispatch: mediator.Send[RequestType, ResponseType](m, ctx, cmd)
|
||||
result, err := mediator.Send[UploadFileCommand, string](e.Mediator, c.Request.Context(), cmd)
|
||||
// internal/server/server.go
|
||||
var ProviderSet = wire.NewSet(NewHTTPServer, NewGRPCServer)
|
||||
```
|
||||
|
||||
Commands named `XxxCommand`, queries named `XxxQuery`. Split across files: `file_commands.go`, `file_queries.go`, `bucket_commands.go`.
|
||||
Wire bindings (in cmd/server/wire.go):
|
||||
- `biz.FileRepo` → `*data.FileRepo`
|
||||
- `biz.FolderRepo` → `*data.FolderRepo`
|
||||
- `biz.FileMetaRepo` → `*data.FileMetaRepo`
|
||||
- `biz.ShareRepo` → `*data.ShareRepo`
|
||||
|
||||
### Endpoint Pattern (4 steps)
|
||||
### GORM Transaction Management
|
||||
|
||||
```go
|
||||
func (e *FileEndpoint) UploadFile(c *gin.Context) {
|
||||
// 1. Bind request
|
||||
var req requests.UploadFileRequest
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
// 2. Validate
|
||||
if err := e.FileValidator.ValidateUpload(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
// 3. Send through mediator
|
||||
result, err := mediator.Send[XxxCommand, string](e.Mediator, ctx, cmd)
|
||||
if err != nil { handleError(c, err); return }
|
||||
// 4. Respond
|
||||
c.JSON(http.StatusOK, gin.H{"message": result})
|
||||
// biz layer defines interface
|
||||
type Transaction interface { InTx(ctx context.Context, fn func(ctx context.Context) error) error }
|
||||
|
||||
// data layer implements
|
||||
func (d *Data) DB(ctx context.Context) *gorm.DB {
|
||||
tx, ok := ctx.Value(contextTxKey{}).(*gorm.DB)
|
||||
if ok { return tx }
|
||||
return d.db.WithContext(ctx)
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
### Proto Error Definition
|
||||
|
||||
```go
|
||||
// Domain errors
|
||||
type BusinessException struct { Message string; Code int }
|
||||
func NewBusinessError(msg string) *BusinessException // 400
|
||||
func NewNotFoundError(msg string) *BusinessException // 404
|
||||
|
||||
// Endpoint error handler
|
||||
func handleError(c *gin.Context, err error) {
|
||||
if be, ok := err.(*common.BusinessException); ok {
|
||||
c.JSON(be.Code, gin.H{"error": be.Message})
|
||||
} else {
|
||||
common.Logger.Error("unhandled error", "error", err, "path", c.Request.URL.Path)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
|
||||
}
|
||||
Errors defined in `api/file/v1/error_reason.proto`:
|
||||
```protobuf
|
||||
enum ErrorReason {
|
||||
FILE_NOT_FOUND = 0 [(errors.code) = 404];
|
||||
INVALID_PARAMETER = 1 [(errors.code) = 400];
|
||||
}
|
||||
```
|
||||
|
||||
### Request Struct Tags
|
||||
Usage: `return nil, api.file.v1.ErrorFileNotFound("file %s not found", key)`
|
||||
|
||||
- GET query params: `form:"bucket_name"` (bound via `c.ShouldBind`)
|
||||
- JSON body: `json:"bucket_name"` (bound via `c.ShouldBindJSON`)
|
||||
- File upload: `form:"file"` with `*multipart.FileHeader`
|
||||
### GORM Model Naming
|
||||
|
||||
### Auth Middleware
|
||||
- Models suffixed with `PO`: `FolderPO`, `FileMetaPO`, `ShareLinkPO`
|
||||
- Table names via `TableName()` method with snake_case
|
||||
|
||||
Dual-mode (configured by `GRPC_AUTH_ADDR` env var):
|
||||
- **With gRPC**: `JWTAuthMiddleware` → calls rag-backend's `ValidateToken` RPC with token caching (2min TTL)
|
||||
- **Without gRPC**: `AuthMiddleware` → checks `X-API-Key` header against configured key
|
||||
### Service → Biz → Data Pattern
|
||||
|
||||
### Middleware Chain
|
||||
```go
|
||||
// service: proto request → usecase call → proto response
|
||||
func (s *FileService) UploadFile(ctx context.Context, req *pb.UploadFileRequest) (*pb.UploadFileResponse, error) {
|
||||
err := s.fileUC.UploadFile(ctx, req.BucketName, req.ObjectKey, bytes.NewReader(req.Data))
|
||||
return &pb.UploadFileResponse{Message: "uploaded"}, err
|
||||
}
|
||||
|
||||
```
|
||||
gin.Default (Logger + Recovery)
|
||||
→ OTel tracing
|
||||
→ RequestID (X-Request-ID)
|
||||
→ Logging (structured: method, path, status, duration_ms, request_id)
|
||||
→ Timeout (30s context deadline)
|
||||
→ CORS
|
||||
→ Auth (API key or JWT)
|
||||
// biz: business logic, repo interface calls
|
||||
func (uc *FileUsecase) UploadFile(ctx context.Context, bucket, key string, data io.Reader) error {
|
||||
return uc.repo.UploadFile(ctx, bucket, key, data)
|
||||
}
|
||||
|
||||
// data: concrete implementation
|
||||
func (r *FileRepo) UploadFile(ctx context.Context, bucket, key string, data io.Reader) error {
|
||||
_, err := r.client.PutObject(ctx, &s3.PutObjectInput{...})
|
||||
return s3errors.Wrap(err)
|
||||
}
|
||||
```
|
||||
|
||||
### Input Sanitization
|
||||
### Configuration
|
||||
|
||||
- `SanitizeObjectKey(key)` — blocks `..`, `//`, leading `/`
|
||||
- `SanitizeBucketName(name)` — regex: `^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$`
|
||||
- `SanitizeFilename(name)` — escapes quotes, strips CR/LF
|
||||
`configs/config.yaml` for local development. Environment variables via `${ENV_VAR}` placeholders.
|
||||
Loaded via Kratos config component with file source.
|
||||
|
||||
### Testing
|
||||
### Middleware Chain (Kratos)
|
||||
|
||||
Standard `testing` package (no testify). Table-driven tests for sanitization. `httptest.NewRecorder` + `gin.TestMode` for middleware tests.
|
||||
HTTP and gRPC share the same middleware:
|
||||
```
|
||||
recovery → tracing → logging
|
||||
```
|
||||
|
||||
### Config
|
||||
|
||||
Environment variables only (12-factor). No YAML/JSON config files. Loaded in `internal/common/config.go`.
|
||||
Configured in `internal/server/http.go` and `internal/server/grpc.go`.
|
||||
|
||||
34
Dockerfile
34
Dockerfile
@ -1,36 +1,14 @@
|
||||
# Build Stage - 使用预装 Go 的基础镜像
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV GOPROXY=https://goproxy.cn,direct
|
||||
ENV GOSUMDB=sum.golang.google.cn
|
||||
|
||||
# 复制依赖文件
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -o /bin/file-system ./cmd/server
|
||||
|
||||
# 编译 Go 应用
|
||||
RUN go build -v -o server ./cmd/server
|
||||
|
||||
# Run Stage
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk --no-cache add ca-certificates
|
||||
COPY --from=builder /bin/file-system /bin/file-system
|
||||
COPY configs/config.yaml /app/configs/config.yaml
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache wget file
|
||||
|
||||
COPY --from=builder /app/server .
|
||||
RUN chmod +x server
|
||||
|
||||
COPY --from=builder /app/docs ./docs
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
||||
|
||||
CMD ["./server"]
|
||||
EXPOSE 8080 9000
|
||||
CMD ["/bin/file-system", "-conf", "configs/config.yaml"]
|
||||
|
||||
24
Makefile
Normal file
24
Makefile
Normal file
@ -0,0 +1,24 @@
|
||||
.PHONY: api config wire build run test clean
|
||||
|
||||
api:
|
||||
buf generate
|
||||
|
||||
config:
|
||||
buf generate --path internal/conf/conf.proto --output /tmp/buf-gen/ && \
|
||||
cp /tmp/buf-gen/conf.pb.go internal/conf/conf.pb.go && \
|
||||
rm -rf /tmp/buf-gen/
|
||||
|
||||
wire:
|
||||
cd cmd/server && wire
|
||||
|
||||
build:
|
||||
go build -o ./bin/file-system ./cmd/server
|
||||
|
||||
run:
|
||||
go run ./cmd/server -conf configs/config.yaml
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
|
||||
clean:
|
||||
rm -rf bin/
|
||||
99
api/errors/errors.pb.go
Normal file
99
api/errors/errors.pb.go
Normal file
@ -0,0 +1,99 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: errors/errors.proto
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
descriptorpb "google.golang.org/protobuf/types/descriptorpb"
|
||||
reflect "reflect"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
var file_errors_errors_proto_extTypes = []protoimpl.ExtensionInfo{
|
||||
{
|
||||
ExtendedType: (*descriptorpb.EnumOptions)(nil),
|
||||
ExtensionType: (*int32)(nil),
|
||||
Field: 1108,
|
||||
Name: "errors.default_code",
|
||||
Tag: "varint,1108,opt,name=default_code",
|
||||
Filename: "errors/errors.proto",
|
||||
},
|
||||
{
|
||||
ExtendedType: (*descriptorpb.EnumValueOptions)(nil),
|
||||
ExtensionType: (*int32)(nil),
|
||||
Field: 1109,
|
||||
Name: "errors.code",
|
||||
Tag: "varint,1109,opt,name=code",
|
||||
Filename: "errors/errors.proto",
|
||||
},
|
||||
}
|
||||
|
||||
// Extension fields to descriptorpb.EnumOptions.
|
||||
var (
|
||||
// optional int32 default_code = 1108;
|
||||
E_DefaultCode = &file_errors_errors_proto_extTypes[0]
|
||||
)
|
||||
|
||||
// Extension fields to descriptorpb.EnumValueOptions.
|
||||
var (
|
||||
// optional int32 code = 1109;
|
||||
E_Code = &file_errors_errors_proto_extTypes[1]
|
||||
)
|
||||
|
||||
var File_errors_errors_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_errors_errors_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x13errors/errors.proto\x12\x06errors\x1a google/protobuf/descriptor.proto:@\n" +
|
||||
"\fdefault_code\x12\x1c.google.protobuf.EnumOptions\x18\xd4\b \x01(\x05R\vdefaultCode:6\n" +
|
||||
"\x04code\x12!.google.protobuf.EnumValueOptions\x18\xd5\b \x01(\x05R\x04codeB'Z%github.com/go-kratos/kratos/v2/errorsb\x06proto3"
|
||||
|
||||
var file_errors_errors_proto_goTypes = []any{
|
||||
(*descriptorpb.EnumOptions)(nil), // 0: google.protobuf.EnumOptions
|
||||
(*descriptorpb.EnumValueOptions)(nil), // 1: google.protobuf.EnumValueOptions
|
||||
}
|
||||
var file_errors_errors_proto_depIdxs = []int32{
|
||||
0, // 0: errors.default_code:extendee -> google.protobuf.EnumOptions
|
||||
1, // 1: errors.code:extendee -> google.protobuf.EnumValueOptions
|
||||
2, // [2:2] is the sub-list for method output_type
|
||||
2, // [2:2] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
0, // [0:2] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_errors_errors_proto_init() }
|
||||
func file_errors_errors_proto_init() {
|
||||
if File_errors_errors_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_errors_errors_proto_rawDesc), len(file_errors_errors_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 0,
|
||||
NumExtensions: 2,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_errors_errors_proto_goTypes,
|
||||
DependencyIndexes: file_errors_errors_proto_depIdxs,
|
||||
ExtensionInfos: file_errors_errors_proto_extTypes,
|
||||
}.Build()
|
||||
File_errors_errors_proto = out.File
|
||||
file_errors_errors_proto_goTypes = nil
|
||||
file_errors_errors_proto_depIdxs = nil
|
||||
}
|
||||
167
api/file/v1/error_reason.pb.go
Normal file
167
api/file/v1/error_reason.pb.go
Normal file
@ -0,0 +1,167 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: file/v1/error_reason.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
_ "github.com/go-kratos/kratos/v2/errors"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type ErrorReason int32
|
||||
|
||||
const (
|
||||
ErrorReason_BUCKET_NOT_FOUND ErrorReason = 0
|
||||
ErrorReason_FILE_NOT_FOUND ErrorReason = 1
|
||||
ErrorReason_FOLDER_NOT_FOUND ErrorReason = 2
|
||||
ErrorReason_SHARE_NOT_FOUND ErrorReason = 3
|
||||
ErrorReason_INVALID_PARAMETER ErrorReason = 4
|
||||
ErrorReason_PATH_TRAVERSAL_DETECTED ErrorReason = 5
|
||||
ErrorReason_INVALID_BUCKET_NAME ErrorReason = 6
|
||||
ErrorReason_STORAGE_OPERATION_FAILED ErrorReason = 7
|
||||
ErrorReason_SHARE_PASSWORD_REQUIRED ErrorReason = 8
|
||||
ErrorReason_SHARE_EXPIRED ErrorReason = 9
|
||||
ErrorReason_SHARE_DOWNLOAD_LIMIT_REACHED ErrorReason = 10
|
||||
ErrorReason_FOLDER_NAME_CONFLICT ErrorReason = 11
|
||||
)
|
||||
|
||||
// Enum value maps for ErrorReason.
|
||||
var (
|
||||
ErrorReason_name = map[int32]string{
|
||||
0: "BUCKET_NOT_FOUND",
|
||||
1: "FILE_NOT_FOUND",
|
||||
2: "FOLDER_NOT_FOUND",
|
||||
3: "SHARE_NOT_FOUND",
|
||||
4: "INVALID_PARAMETER",
|
||||
5: "PATH_TRAVERSAL_DETECTED",
|
||||
6: "INVALID_BUCKET_NAME",
|
||||
7: "STORAGE_OPERATION_FAILED",
|
||||
8: "SHARE_PASSWORD_REQUIRED",
|
||||
9: "SHARE_EXPIRED",
|
||||
10: "SHARE_DOWNLOAD_LIMIT_REACHED",
|
||||
11: "FOLDER_NAME_CONFLICT",
|
||||
}
|
||||
ErrorReason_value = map[string]int32{
|
||||
"BUCKET_NOT_FOUND": 0,
|
||||
"FILE_NOT_FOUND": 1,
|
||||
"FOLDER_NOT_FOUND": 2,
|
||||
"SHARE_NOT_FOUND": 3,
|
||||
"INVALID_PARAMETER": 4,
|
||||
"PATH_TRAVERSAL_DETECTED": 5,
|
||||
"INVALID_BUCKET_NAME": 6,
|
||||
"STORAGE_OPERATION_FAILED": 7,
|
||||
"SHARE_PASSWORD_REQUIRED": 8,
|
||||
"SHARE_EXPIRED": 9,
|
||||
"SHARE_DOWNLOAD_LIMIT_REACHED": 10,
|
||||
"FOLDER_NAME_CONFLICT": 11,
|
||||
}
|
||||
)
|
||||
|
||||
func (x ErrorReason) Enum() *ErrorReason {
|
||||
p := new(ErrorReason)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x ErrorReason) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (ErrorReason) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_file_v1_error_reason_proto_enumTypes[0].Descriptor()
|
||||
}
|
||||
|
||||
func (ErrorReason) Type() protoreflect.EnumType {
|
||||
return &file_file_v1_error_reason_proto_enumTypes[0]
|
||||
}
|
||||
|
||||
func (x ErrorReason) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ErrorReason.Descriptor instead.
|
||||
func (ErrorReason) EnumDescriptor() ([]byte, []int) {
|
||||
return file_file_v1_error_reason_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
var File_file_v1_error_reason_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_file_v1_error_reason_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x1afile/v1/error_reason.proto\x12\vapi.file.v1\x1a\x13errors/errors.proto*\x87\x03\n" +
|
||||
"\vErrorReason\x12\x1a\n" +
|
||||
"\x10BUCKET_NOT_FOUND\x10\x00\x1a\x04\xa8E\x94\x03\x12\x18\n" +
|
||||
"\x0eFILE_NOT_FOUND\x10\x01\x1a\x04\xa8E\x94\x03\x12\x1a\n" +
|
||||
"\x10FOLDER_NOT_FOUND\x10\x02\x1a\x04\xa8E\x94\x03\x12\x19\n" +
|
||||
"\x0fSHARE_NOT_FOUND\x10\x03\x1a\x04\xa8E\x94\x03\x12\x1b\n" +
|
||||
"\x11INVALID_PARAMETER\x10\x04\x1a\x04\xa8E\x90\x03\x12!\n" +
|
||||
"\x17PATH_TRAVERSAL_DETECTED\x10\x05\x1a\x04\xa8E\x90\x03\x12\x1d\n" +
|
||||
"\x13INVALID_BUCKET_NAME\x10\x06\x1a\x04\xa8E\x90\x03\x12\"\n" +
|
||||
"\x18STORAGE_OPERATION_FAILED\x10\a\x1a\x04\xa8E\xf4\x03\x12!\n" +
|
||||
"\x17SHARE_PASSWORD_REQUIRED\x10\b\x1a\x04\xa8E\x91\x03\x12\x17\n" +
|
||||
"\rSHARE_EXPIRED\x10\t\x1a\x04\xa8E\x9a\x03\x12&\n" +
|
||||
"\x1cSHARE_DOWNLOAD_LIMIT_REACHED\x10\n" +
|
||||
"\x1a\x04\xa8E\xad\x03\x12\x1e\n" +
|
||||
"\x14FOLDER_NAME_CONFLICT\x10\v\x1a\x04\xa8E\x99\x03\x1a\x04\xa0E\xf4\x03B\x1dZ\x1brag/file-system/api/file/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_file_v1_error_reason_proto_rawDescOnce sync.Once
|
||||
file_file_v1_error_reason_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_file_v1_error_reason_proto_rawDescGZIP() []byte {
|
||||
file_file_v1_error_reason_proto_rawDescOnce.Do(func() {
|
||||
file_file_v1_error_reason_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_file_v1_error_reason_proto_rawDesc), len(file_file_v1_error_reason_proto_rawDesc)))
|
||||
})
|
||||
return file_file_v1_error_reason_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_file_v1_error_reason_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_file_v1_error_reason_proto_goTypes = []any{
|
||||
(ErrorReason)(0), // 0: api.file.v1.ErrorReason
|
||||
}
|
||||
var file_file_v1_error_reason_proto_depIdxs = []int32{
|
||||
0, // [0:0] is the sub-list for method output_type
|
||||
0, // [0:0] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_file_v1_error_reason_proto_init() }
|
||||
func file_file_v1_error_reason_proto_init() {
|
||||
if File_file_v1_error_reason_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_file_v1_error_reason_proto_rawDesc), len(file_file_v1_error_reason_proto_rawDesc)),
|
||||
NumEnums: 1,
|
||||
NumMessages: 0,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_file_v1_error_reason_proto_goTypes,
|
||||
DependencyIndexes: file_file_v1_error_reason_proto_depIdxs,
|
||||
EnumInfos: file_file_v1_error_reason_proto_enumTypes,
|
||||
}.Build()
|
||||
File_file_v1_error_reason_proto = out.File
|
||||
file_file_v1_error_reason_proto_goTypes = nil
|
||||
file_file_v1_error_reason_proto_depIdxs = nil
|
||||
}
|
||||
24
api/file/v1/error_reason.proto
Normal file
24
api/file/v1/error_reason.proto
Normal file
@ -0,0 +1,24 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package api.file.v1;
|
||||
|
||||
option go_package = "rag/file-system/api/file/v1";
|
||||
|
||||
import "errors/errors.proto";
|
||||
|
||||
enum ErrorReason {
|
||||
option (errors.default_code) = 500;
|
||||
|
||||
BUCKET_NOT_FOUND = 0 [(errors.code) = 404];
|
||||
FILE_NOT_FOUND = 1 [(errors.code) = 404];
|
||||
FOLDER_NOT_FOUND = 2 [(errors.code) = 404];
|
||||
SHARE_NOT_FOUND = 3 [(errors.code) = 404];
|
||||
INVALID_PARAMETER = 4 [(errors.code) = 400];
|
||||
PATH_TRAVERSAL_DETECTED = 5 [(errors.code) = 400];
|
||||
INVALID_BUCKET_NAME = 6 [(errors.code) = 400];
|
||||
STORAGE_OPERATION_FAILED = 7 [(errors.code) = 500];
|
||||
SHARE_PASSWORD_REQUIRED = 8 [(errors.code) = 401];
|
||||
SHARE_EXPIRED = 9 [(errors.code) = 410];
|
||||
SHARE_DOWNLOAD_LIMIT_REACHED = 10 [(errors.code) = 429];
|
||||
FOLDER_NAME_CONFLICT = 11 [(errors.code) = 409];
|
||||
}
|
||||
2834
api/file/v1/file.pb.go
Normal file
2834
api/file/v1/file.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
339
api/file/v1/file.proto
Normal file
339
api/file/v1/file.proto
Normal file
@ -0,0 +1,339 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package api.file.v1;
|
||||
|
||||
option go_package = "rag/file-system/api/file/v1";
|
||||
|
||||
import "google/api/annotations.proto";
|
||||
import "google/protobuf/empty.proto";
|
||||
|
||||
service FileService {
|
||||
// File operations
|
||||
rpc UploadFile (UploadFileRequest) returns (UploadFileResponse) {
|
||||
option (google.api.http) = { post: "/files/upload" body: "*" };
|
||||
}
|
||||
rpc DownloadFile (DownloadFileRequest) returns (DownloadFileResponse) {
|
||||
option (google.api.http) = { get: "/files/download" };
|
||||
}
|
||||
rpc ListFiles (ListFilesRequest) returns (ListFilesResponse) {
|
||||
option (google.api.http) = { get: "/files/list" };
|
||||
}
|
||||
rpc GetFilePreview (GetFilePreviewRequest) returns (GetFilePreviewResponse) {
|
||||
option (google.api.http) = { get: "/files/preview" };
|
||||
}
|
||||
rpc GetFileContent (GetFileContentRequest) returns (GetFileContentResponse) {
|
||||
option (google.api.http) = { get: "/files/content" };
|
||||
}
|
||||
rpc DeleteFile (DeleteFileRequest) returns (google.protobuf.Empty) {
|
||||
option (google.api.http) = { delete: "/files/delete" };
|
||||
}
|
||||
|
||||
// Multipart upload
|
||||
rpc InitMultipartUpload (InitMultipartRequest) returns (InitMultipartResponse) {
|
||||
option (google.api.http) = { post: "/files/multipart/init" body: "*" };
|
||||
}
|
||||
rpc UploadPart (UploadPartRequest) returns (UploadPartResponse) {
|
||||
option (google.api.http) = { put: "/files/multipart/part" body: "*" };
|
||||
}
|
||||
rpc CompleteMultipartUpload (CompleteMultipartRequest) returns (CompleteMultipartResponse) {
|
||||
option (google.api.http) = { post: "/files/multipart/complete" body: "*" };
|
||||
}
|
||||
rpc AbortMultipartUpload (AbortMultipartRequest) returns (google.protobuf.Empty) {
|
||||
option (google.api.http) = { post: "/files/multipart/abort" body: "*" };
|
||||
}
|
||||
|
||||
// Bucket operations
|
||||
rpc CreateBucket (CreateBucketRequest) returns (google.protobuf.Empty) {
|
||||
option (google.api.http) = { post: "/buckets" body: "*" };
|
||||
}
|
||||
rpc ListBuckets (google.protobuf.Empty) returns (ListBucketsResponse) {
|
||||
option (google.api.http) = { get: "/buckets" };
|
||||
}
|
||||
rpc DeleteBucket (DeleteBucketRequest) returns (google.protobuf.Empty) {
|
||||
option (google.api.http) = { delete: "/buckets" };
|
||||
}
|
||||
|
||||
// Folder operations
|
||||
rpc CreateFolder (CreateFolderRequest) returns (Folder) {
|
||||
option (google.api.http) = { post: "/folders" body: "*" };
|
||||
}
|
||||
rpc GetFolderTree (GetFolderTreeRequest) returns (GetFolderTreeResponse) {
|
||||
option (google.api.http) = { get: "/folders/tree" };
|
||||
}
|
||||
rpc GetFolder (GetFolderRequest) returns (FolderWithChildren) {
|
||||
option (google.api.http) = { get: "/folders/{id}" };
|
||||
}
|
||||
rpc RenameFolder (RenameFolderRequest) returns (Folder) {
|
||||
option (google.api.http) = { put: "/folders/{id}" body: "*" };
|
||||
}
|
||||
rpc DeleteFolder (DeleteFolderRequest) returns (google.protobuf.Empty) {
|
||||
option (google.api.http) = { delete: "/folders/{id}" };
|
||||
}
|
||||
rpc UploadToFolder (UploadToFolderRequest) returns (FileMeta) {
|
||||
option (google.api.http) = { post: "/folders/{folder_id}/files" body: "*" };
|
||||
}
|
||||
rpc MoveFile (MoveFileRequest) returns (google.protobuf.Empty) {
|
||||
option (google.api.http) = { post: "/files/{id}/move" body: "*" };
|
||||
}
|
||||
|
||||
// Share operations
|
||||
rpc CreateShare (CreateShareRequest) returns (ShareLink) {
|
||||
option (google.api.http) = { post: "/share" body: "*" };
|
||||
}
|
||||
rpc DeleteShare (DeleteShareRequest) returns (google.protobuf.Empty) {
|
||||
option (google.api.http) = { delete: "/share/{id}" };
|
||||
}
|
||||
rpc GetShareInfo (GetShareInfoRequest) returns (ShareInfo) {
|
||||
option (google.api.http) = { get: "/share/{token}" };
|
||||
}
|
||||
rpc DownloadShare (DownloadShareRequest) returns (DownloadShareResponse) {
|
||||
option (google.api.http) = { post: "/share/{token}/download" body: "*" };
|
||||
}
|
||||
}
|
||||
|
||||
// File messages
|
||||
message UploadFileRequest {
|
||||
string bucket_name = 1;
|
||||
string object_key = 2;
|
||||
bytes data = 3;
|
||||
string content_type = 4;
|
||||
}
|
||||
|
||||
message UploadFileResponse {
|
||||
string message = 1;
|
||||
string object_key = 2;
|
||||
}
|
||||
|
||||
message DownloadFileRequest {
|
||||
string bucket_name = 1;
|
||||
string object_key = 2;
|
||||
}
|
||||
|
||||
message DownloadFileResponse {
|
||||
bytes data = 1;
|
||||
string content_type = 2;
|
||||
string file_name = 3;
|
||||
}
|
||||
|
||||
message ListFilesRequest {
|
||||
string bucket_name = 1;
|
||||
string prefix = 2;
|
||||
int32 max_keys = 3;
|
||||
string continuation_token = 4;
|
||||
}
|
||||
|
||||
message FileInfo {
|
||||
string key = 1;
|
||||
int64 size = 2;
|
||||
string last_modified = 3;
|
||||
string etag = 4;
|
||||
}
|
||||
|
||||
message ListFilesResponse {
|
||||
repeated FileInfo files = 1;
|
||||
string next_continuation_token = 2;
|
||||
}
|
||||
|
||||
message GetFilePreviewRequest {
|
||||
string bucket_name = 1;
|
||||
string object_key = 2;
|
||||
}
|
||||
|
||||
message GetFilePreviewResponse {
|
||||
string presigned_url = 1;
|
||||
}
|
||||
|
||||
message GetFileContentRequest {
|
||||
string bucket_name = 1;
|
||||
string object_key = 2;
|
||||
}
|
||||
|
||||
message GetFileContentResponse {
|
||||
string content = 1;
|
||||
}
|
||||
|
||||
message DeleteFileRequest {
|
||||
string bucket_name = 1;
|
||||
string object_key = 2;
|
||||
}
|
||||
|
||||
// Multipart messages
|
||||
message InitMultipartRequest {
|
||||
string bucket_name = 1;
|
||||
string object_key = 2;
|
||||
}
|
||||
|
||||
message InitMultipartResponse {
|
||||
string upload_id = 1;
|
||||
}
|
||||
|
||||
message UploadPartRequest {
|
||||
string bucket_name = 1;
|
||||
string object_key = 2;
|
||||
string upload_id = 3;
|
||||
int32 part_number = 4;
|
||||
bytes data = 5;
|
||||
}
|
||||
|
||||
message UploadPartResponse {
|
||||
string etag = 1;
|
||||
}
|
||||
|
||||
message CompletedPart {
|
||||
int32 part_number = 1;
|
||||
string etag = 2;
|
||||
}
|
||||
|
||||
message CompleteMultipartRequest {
|
||||
string bucket_name = 1;
|
||||
string object_key = 2;
|
||||
string upload_id = 3;
|
||||
repeated CompletedPart parts = 4;
|
||||
}
|
||||
|
||||
message CompleteMultipartResponse {
|
||||
string location = 1;
|
||||
}
|
||||
|
||||
message AbortMultipartRequest {
|
||||
string bucket_name = 1;
|
||||
string object_key = 2;
|
||||
string upload_id = 3;
|
||||
}
|
||||
|
||||
// Bucket messages
|
||||
message CreateBucketRequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message ListBucketsResponse {
|
||||
repeated string buckets = 1;
|
||||
}
|
||||
|
||||
message DeleteBucketRequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
// Folder messages
|
||||
message Folder {
|
||||
string id = 1;
|
||||
string parent_id = 2;
|
||||
string name = 3;
|
||||
string owner_id = 4;
|
||||
string created_at = 5;
|
||||
string updated_at = 6;
|
||||
}
|
||||
|
||||
message FolderWithChildren {
|
||||
Folder folder = 1;
|
||||
repeated Folder sub_folders = 2;
|
||||
repeated FileMeta files = 3;
|
||||
}
|
||||
|
||||
message CreateFolderRequest {
|
||||
string parent_id = 1;
|
||||
string name = 2;
|
||||
string owner_id = 3;
|
||||
}
|
||||
|
||||
message GetFolderTreeRequest {
|
||||
string owner_id = 1;
|
||||
}
|
||||
|
||||
message GetFolderTreeResponse {
|
||||
repeated Folder folders = 1;
|
||||
}
|
||||
|
||||
message GetFolderRequest {
|
||||
string id = 1;
|
||||
string owner_id = 2;
|
||||
}
|
||||
|
||||
message RenameFolderRequest {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
string owner_id = 3;
|
||||
}
|
||||
|
||||
message DeleteFolderRequest {
|
||||
string id = 1;
|
||||
string owner_id = 2;
|
||||
}
|
||||
|
||||
message FileMeta {
|
||||
string id = 1;
|
||||
string folder_id = 2;
|
||||
string name = 3;
|
||||
string s3_key = 4;
|
||||
string s3_bucket = 5;
|
||||
int64 size = 6;
|
||||
string content_type = 7;
|
||||
string owner_id = 8;
|
||||
string created_at = 9;
|
||||
string updated_at = 10;
|
||||
}
|
||||
|
||||
message UploadToFolderRequest {
|
||||
string folder_id = 1;
|
||||
string file_name = 2;
|
||||
bytes data = 3;
|
||||
string content_type = 4;
|
||||
string owner_id = 5;
|
||||
}
|
||||
|
||||
message MoveFileRequest {
|
||||
string id = 1;
|
||||
string target_folder_id = 2;
|
||||
string owner_id = 3;
|
||||
}
|
||||
|
||||
// Share messages
|
||||
message ShareLink {
|
||||
string id = 1;
|
||||
string resource_type = 2;
|
||||
string resource_id = 3;
|
||||
string token = 4;
|
||||
string password = 5;
|
||||
string expires_at = 6;
|
||||
int32 download_count = 7;
|
||||
int32 max_downloads = 8;
|
||||
string created_by = 9;
|
||||
string created_at = 10;
|
||||
}
|
||||
|
||||
message ShareInfo {
|
||||
string token = 1;
|
||||
string resource_type = 2;
|
||||
string file_name = 3;
|
||||
int64 file_size = 4;
|
||||
bool has_password = 5;
|
||||
string expires_at = 6;
|
||||
}
|
||||
|
||||
message CreateShareRequest {
|
||||
string resource_type = 1;
|
||||
string resource_id = 2;
|
||||
string password = 3;
|
||||
string expires_at = 4;
|
||||
int32 max_downloads = 5;
|
||||
string created_by = 6;
|
||||
}
|
||||
|
||||
message DeleteShareRequest {
|
||||
string id = 1;
|
||||
string created_by = 2;
|
||||
}
|
||||
|
||||
message GetShareInfoRequest {
|
||||
string token = 1;
|
||||
}
|
||||
|
||||
message DownloadShareRequest {
|
||||
string token = 1;
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message DownloadShareResponse {
|
||||
string presigned_url = 1;
|
||||
string file_name = 2;
|
||||
}
|
||||
1006
api/file/v1/file_grpc.pb.go
Normal file
1006
api/file/v1/file_grpc.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
979
api/file/v1/file_http.pb.go
Normal file
979
api/file/v1/file_http.pb.go
Normal file
@ -0,0 +1,979 @@
|
||||
// Code generated by protoc-gen-go-http. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-http v2.9.2
|
||||
// - protoc (unknown)
|
||||
// source: file/v1/file.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
context "context"
|
||||
http "github.com/go-kratos/kratos/v2/transport/http"
|
||||
binding "github.com/go-kratos/kratos/v2/transport/http/binding"
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the kratos package it is being compiled against.
|
||||
var _ = new(context.Context)
|
||||
var _ = binding.EncodeURL
|
||||
|
||||
const _ = http.SupportPackageIsVersion1
|
||||
|
||||
const OperationFileServiceAbortMultipartUpload = "/api.file.v1.FileService/AbortMultipartUpload"
|
||||
const OperationFileServiceCompleteMultipartUpload = "/api.file.v1.FileService/CompleteMultipartUpload"
|
||||
const OperationFileServiceCreateBucket = "/api.file.v1.FileService/CreateBucket"
|
||||
const OperationFileServiceCreateFolder = "/api.file.v1.FileService/CreateFolder"
|
||||
const OperationFileServiceCreateShare = "/api.file.v1.FileService/CreateShare"
|
||||
const OperationFileServiceDeleteBucket = "/api.file.v1.FileService/DeleteBucket"
|
||||
const OperationFileServiceDeleteFile = "/api.file.v1.FileService/DeleteFile"
|
||||
const OperationFileServiceDeleteFolder = "/api.file.v1.FileService/DeleteFolder"
|
||||
const OperationFileServiceDeleteShare = "/api.file.v1.FileService/DeleteShare"
|
||||
const OperationFileServiceDownloadFile = "/api.file.v1.FileService/DownloadFile"
|
||||
const OperationFileServiceDownloadShare = "/api.file.v1.FileService/DownloadShare"
|
||||
const OperationFileServiceGetFileContent = "/api.file.v1.FileService/GetFileContent"
|
||||
const OperationFileServiceGetFilePreview = "/api.file.v1.FileService/GetFilePreview"
|
||||
const OperationFileServiceGetFolder = "/api.file.v1.FileService/GetFolder"
|
||||
const OperationFileServiceGetFolderTree = "/api.file.v1.FileService/GetFolderTree"
|
||||
const OperationFileServiceGetShareInfo = "/api.file.v1.FileService/GetShareInfo"
|
||||
const OperationFileServiceInitMultipartUpload = "/api.file.v1.FileService/InitMultipartUpload"
|
||||
const OperationFileServiceListBuckets = "/api.file.v1.FileService/ListBuckets"
|
||||
const OperationFileServiceListFiles = "/api.file.v1.FileService/ListFiles"
|
||||
const OperationFileServiceMoveFile = "/api.file.v1.FileService/MoveFile"
|
||||
const OperationFileServiceRenameFolder = "/api.file.v1.FileService/RenameFolder"
|
||||
const OperationFileServiceUploadFile = "/api.file.v1.FileService/UploadFile"
|
||||
const OperationFileServiceUploadPart = "/api.file.v1.FileService/UploadPart"
|
||||
const OperationFileServiceUploadToFolder = "/api.file.v1.FileService/UploadToFolder"
|
||||
|
||||
type FileServiceHTTPServer interface {
|
||||
AbortMultipartUpload(context.Context, *AbortMultipartRequest) (*emptypb.Empty, error)
|
||||
CompleteMultipartUpload(context.Context, *CompleteMultipartRequest) (*CompleteMultipartResponse, error)
|
||||
// CreateBucket Bucket operations
|
||||
CreateBucket(context.Context, *CreateBucketRequest) (*emptypb.Empty, error)
|
||||
// CreateFolder Folder operations
|
||||
CreateFolder(context.Context, *CreateFolderRequest) (*Folder, error)
|
||||
// CreateShare Share operations
|
||||
CreateShare(context.Context, *CreateShareRequest) (*ShareLink, error)
|
||||
DeleteBucket(context.Context, *DeleteBucketRequest) (*emptypb.Empty, error)
|
||||
DeleteFile(context.Context, *DeleteFileRequest) (*emptypb.Empty, error)
|
||||
DeleteFolder(context.Context, *DeleteFolderRequest) (*emptypb.Empty, error)
|
||||
DeleteShare(context.Context, *DeleteShareRequest) (*emptypb.Empty, error)
|
||||
DownloadFile(context.Context, *DownloadFileRequest) (*DownloadFileResponse, error)
|
||||
DownloadShare(context.Context, *DownloadShareRequest) (*DownloadShareResponse, error)
|
||||
GetFileContent(context.Context, *GetFileContentRequest) (*GetFileContentResponse, error)
|
||||
GetFilePreview(context.Context, *GetFilePreviewRequest) (*GetFilePreviewResponse, error)
|
||||
GetFolder(context.Context, *GetFolderRequest) (*FolderWithChildren, error)
|
||||
GetFolderTree(context.Context, *GetFolderTreeRequest) (*GetFolderTreeResponse, error)
|
||||
GetShareInfo(context.Context, *GetShareInfoRequest) (*ShareInfo, error)
|
||||
// InitMultipartUpload Multipart upload
|
||||
InitMultipartUpload(context.Context, *InitMultipartRequest) (*InitMultipartResponse, error)
|
||||
ListBuckets(context.Context, *emptypb.Empty) (*ListBucketsResponse, error)
|
||||
ListFiles(context.Context, *ListFilesRequest) (*ListFilesResponse, error)
|
||||
MoveFile(context.Context, *MoveFileRequest) (*emptypb.Empty, error)
|
||||
RenameFolder(context.Context, *RenameFolderRequest) (*Folder, error)
|
||||
// UploadFile File operations
|
||||
UploadFile(context.Context, *UploadFileRequest) (*UploadFileResponse, error)
|
||||
UploadPart(context.Context, *UploadPartRequest) (*UploadPartResponse, error)
|
||||
UploadToFolder(context.Context, *UploadToFolderRequest) (*FileMeta, error)
|
||||
}
|
||||
|
||||
func RegisterFileServiceHTTPServer(s *http.Server, srv FileServiceHTTPServer) {
|
||||
r := s.Route("/")
|
||||
r.POST("/files/upload", _FileService_UploadFile0_HTTP_Handler(srv))
|
||||
r.GET("/files/download", _FileService_DownloadFile0_HTTP_Handler(srv))
|
||||
r.GET("/files/list", _FileService_ListFiles0_HTTP_Handler(srv))
|
||||
r.GET("/files/preview", _FileService_GetFilePreview0_HTTP_Handler(srv))
|
||||
r.GET("/files/content", _FileService_GetFileContent0_HTTP_Handler(srv))
|
||||
r.DELETE("/files/delete", _FileService_DeleteFile0_HTTP_Handler(srv))
|
||||
r.POST("/files/multipart/init", _FileService_InitMultipartUpload0_HTTP_Handler(srv))
|
||||
r.PUT("/files/multipart/part", _FileService_UploadPart0_HTTP_Handler(srv))
|
||||
r.POST("/files/multipart/complete", _FileService_CompleteMultipartUpload0_HTTP_Handler(srv))
|
||||
r.POST("/files/multipart/abort", _FileService_AbortMultipartUpload0_HTTP_Handler(srv))
|
||||
r.POST("/buckets", _FileService_CreateBucket0_HTTP_Handler(srv))
|
||||
r.GET("/buckets", _FileService_ListBuckets0_HTTP_Handler(srv))
|
||||
r.DELETE("/buckets", _FileService_DeleteBucket0_HTTP_Handler(srv))
|
||||
r.POST("/folders", _FileService_CreateFolder0_HTTP_Handler(srv))
|
||||
r.GET("/folders/tree", _FileService_GetFolderTree0_HTTP_Handler(srv))
|
||||
r.GET("/folders/{id}", _FileService_GetFolder0_HTTP_Handler(srv))
|
||||
r.PUT("/folders/{id}", _FileService_RenameFolder0_HTTP_Handler(srv))
|
||||
r.DELETE("/folders/{id}", _FileService_DeleteFolder0_HTTP_Handler(srv))
|
||||
r.POST("/folders/{folder_id}/files", _FileService_UploadToFolder0_HTTP_Handler(srv))
|
||||
r.POST("/files/{id}/move", _FileService_MoveFile0_HTTP_Handler(srv))
|
||||
r.POST("/share", _FileService_CreateShare0_HTTP_Handler(srv))
|
||||
r.DELETE("/share/{id}", _FileService_DeleteShare0_HTTP_Handler(srv))
|
||||
r.GET("/share/{token}", _FileService_GetShareInfo0_HTTP_Handler(srv))
|
||||
r.POST("/share/{token}/download", _FileService_DownloadShare0_HTTP_Handler(srv))
|
||||
}
|
||||
|
||||
func _FileService_UploadFile0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in UploadFileRequest
|
||||
if err := ctx.Bind(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceUploadFile)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.UploadFile(ctx, req.(*UploadFileRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*UploadFileResponse)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_DownloadFile0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in DownloadFileRequest
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceDownloadFile)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.DownloadFile(ctx, req.(*DownloadFileRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*DownloadFileResponse)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_ListFiles0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in ListFilesRequest
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceListFiles)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.ListFiles(ctx, req.(*ListFilesRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*ListFilesResponse)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_GetFilePreview0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in GetFilePreviewRequest
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceGetFilePreview)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.GetFilePreview(ctx, req.(*GetFilePreviewRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*GetFilePreviewResponse)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_GetFileContent0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in GetFileContentRequest
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceGetFileContent)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.GetFileContent(ctx, req.(*GetFileContentRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*GetFileContentResponse)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_DeleteFile0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in DeleteFileRequest
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceDeleteFile)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.DeleteFile(ctx, req.(*DeleteFileRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*emptypb.Empty)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_InitMultipartUpload0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in InitMultipartRequest
|
||||
if err := ctx.Bind(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceInitMultipartUpload)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.InitMultipartUpload(ctx, req.(*InitMultipartRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*InitMultipartResponse)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_UploadPart0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in UploadPartRequest
|
||||
if err := ctx.Bind(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceUploadPart)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.UploadPart(ctx, req.(*UploadPartRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*UploadPartResponse)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_CompleteMultipartUpload0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in CompleteMultipartRequest
|
||||
if err := ctx.Bind(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceCompleteMultipartUpload)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.CompleteMultipartUpload(ctx, req.(*CompleteMultipartRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*CompleteMultipartResponse)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_AbortMultipartUpload0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in AbortMultipartRequest
|
||||
if err := ctx.Bind(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceAbortMultipartUpload)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.AbortMultipartUpload(ctx, req.(*AbortMultipartRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*emptypb.Empty)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_CreateBucket0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in CreateBucketRequest
|
||||
if err := ctx.Bind(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceCreateBucket)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.CreateBucket(ctx, req.(*CreateBucketRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*emptypb.Empty)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_ListBuckets0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in emptypb.Empty
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceListBuckets)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.ListBuckets(ctx, req.(*emptypb.Empty))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*ListBucketsResponse)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_DeleteBucket0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in DeleteBucketRequest
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceDeleteBucket)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.DeleteBucket(ctx, req.(*DeleteBucketRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*emptypb.Empty)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_CreateFolder0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in CreateFolderRequest
|
||||
if err := ctx.Bind(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceCreateFolder)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.CreateFolder(ctx, req.(*CreateFolderRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*Folder)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_GetFolderTree0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in GetFolderTreeRequest
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceGetFolderTree)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.GetFolderTree(ctx, req.(*GetFolderTreeRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*GetFolderTreeResponse)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_GetFolder0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in GetFolderRequest
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindVars(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceGetFolder)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.GetFolder(ctx, req.(*GetFolderRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*FolderWithChildren)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_RenameFolder0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in RenameFolderRequest
|
||||
if err := ctx.Bind(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindVars(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceRenameFolder)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.RenameFolder(ctx, req.(*RenameFolderRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*Folder)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_DeleteFolder0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in DeleteFolderRequest
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindVars(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceDeleteFolder)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.DeleteFolder(ctx, req.(*DeleteFolderRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*emptypb.Empty)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_UploadToFolder0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in UploadToFolderRequest
|
||||
if err := ctx.Bind(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindVars(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceUploadToFolder)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.UploadToFolder(ctx, req.(*UploadToFolderRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*FileMeta)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_MoveFile0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in MoveFileRequest
|
||||
if err := ctx.Bind(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindVars(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceMoveFile)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.MoveFile(ctx, req.(*MoveFileRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*emptypb.Empty)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_CreateShare0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in CreateShareRequest
|
||||
if err := ctx.Bind(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceCreateShare)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.CreateShare(ctx, req.(*CreateShareRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*ShareLink)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_DeleteShare0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in DeleteShareRequest
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindVars(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceDeleteShare)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.DeleteShare(ctx, req.(*DeleteShareRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*emptypb.Empty)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_GetShareInfo0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in GetShareInfoRequest
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindVars(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceGetShareInfo)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.GetShareInfo(ctx, req.(*GetShareInfoRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*ShareInfo)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
func _FileService_DownloadShare0_HTTP_Handler(srv FileServiceHTTPServer) func(ctx http.Context) error {
|
||||
return func(ctx http.Context) error {
|
||||
var in DownloadShareRequest
|
||||
if err := ctx.Bind(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindQuery(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.BindVars(&in); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetOperation(ctx, OperationFileServiceDownloadShare)
|
||||
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.DownloadShare(ctx, req.(*DownloadShareRequest))
|
||||
})
|
||||
out, err := h(ctx, &in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply := out.(*DownloadShareResponse)
|
||||
return ctx.Result(200, reply)
|
||||
}
|
||||
}
|
||||
|
||||
type FileServiceHTTPClient interface {
|
||||
AbortMultipartUpload(ctx context.Context, req *AbortMultipartRequest, opts ...http.CallOption) (rsp *emptypb.Empty, err error)
|
||||
CompleteMultipartUpload(ctx context.Context, req *CompleteMultipartRequest, opts ...http.CallOption) (rsp *CompleteMultipartResponse, err error)
|
||||
// CreateBucket Bucket operations
|
||||
CreateBucket(ctx context.Context, req *CreateBucketRequest, opts ...http.CallOption) (rsp *emptypb.Empty, err error)
|
||||
// CreateFolder Folder operations
|
||||
CreateFolder(ctx context.Context, req *CreateFolderRequest, opts ...http.CallOption) (rsp *Folder, err error)
|
||||
// CreateShare Share operations
|
||||
CreateShare(ctx context.Context, req *CreateShareRequest, opts ...http.CallOption) (rsp *ShareLink, err error)
|
||||
DeleteBucket(ctx context.Context, req *DeleteBucketRequest, opts ...http.CallOption) (rsp *emptypb.Empty, err error)
|
||||
DeleteFile(ctx context.Context, req *DeleteFileRequest, opts ...http.CallOption) (rsp *emptypb.Empty, err error)
|
||||
DeleteFolder(ctx context.Context, req *DeleteFolderRequest, opts ...http.CallOption) (rsp *emptypb.Empty, err error)
|
||||
DeleteShare(ctx context.Context, req *DeleteShareRequest, opts ...http.CallOption) (rsp *emptypb.Empty, err error)
|
||||
DownloadFile(ctx context.Context, req *DownloadFileRequest, opts ...http.CallOption) (rsp *DownloadFileResponse, err error)
|
||||
DownloadShare(ctx context.Context, req *DownloadShareRequest, opts ...http.CallOption) (rsp *DownloadShareResponse, err error)
|
||||
GetFileContent(ctx context.Context, req *GetFileContentRequest, opts ...http.CallOption) (rsp *GetFileContentResponse, err error)
|
||||
GetFilePreview(ctx context.Context, req *GetFilePreviewRequest, opts ...http.CallOption) (rsp *GetFilePreviewResponse, err error)
|
||||
GetFolder(ctx context.Context, req *GetFolderRequest, opts ...http.CallOption) (rsp *FolderWithChildren, err error)
|
||||
GetFolderTree(ctx context.Context, req *GetFolderTreeRequest, opts ...http.CallOption) (rsp *GetFolderTreeResponse, err error)
|
||||
GetShareInfo(ctx context.Context, req *GetShareInfoRequest, opts ...http.CallOption) (rsp *ShareInfo, err error)
|
||||
// InitMultipartUpload Multipart upload
|
||||
InitMultipartUpload(ctx context.Context, req *InitMultipartRequest, opts ...http.CallOption) (rsp *InitMultipartResponse, err error)
|
||||
ListBuckets(ctx context.Context, req *emptypb.Empty, opts ...http.CallOption) (rsp *ListBucketsResponse, err error)
|
||||
ListFiles(ctx context.Context, req *ListFilesRequest, opts ...http.CallOption) (rsp *ListFilesResponse, err error)
|
||||
MoveFile(ctx context.Context, req *MoveFileRequest, opts ...http.CallOption) (rsp *emptypb.Empty, err error)
|
||||
RenameFolder(ctx context.Context, req *RenameFolderRequest, opts ...http.CallOption) (rsp *Folder, err error)
|
||||
// UploadFile File operations
|
||||
UploadFile(ctx context.Context, req *UploadFileRequest, opts ...http.CallOption) (rsp *UploadFileResponse, err error)
|
||||
UploadPart(ctx context.Context, req *UploadPartRequest, opts ...http.CallOption) (rsp *UploadPartResponse, err error)
|
||||
UploadToFolder(ctx context.Context, req *UploadToFolderRequest, opts ...http.CallOption) (rsp *FileMeta, err error)
|
||||
}
|
||||
|
||||
type FileServiceHTTPClientImpl struct {
|
||||
cc *http.Client
|
||||
}
|
||||
|
||||
func NewFileServiceHTTPClient(client *http.Client) FileServiceHTTPClient {
|
||||
return &FileServiceHTTPClientImpl{client}
|
||||
}
|
||||
|
||||
func (c *FileServiceHTTPClientImpl) AbortMultipartUpload(ctx context.Context, in *AbortMultipartRequest, opts ...http.CallOption) (*emptypb.Empty, error) {
|
||||
var out emptypb.Empty
|
||||
pattern := "/files/multipart/abort"
|
||||
path := binding.EncodeURL(pattern, in, false)
|
||||
opts = append(opts, http.Operation(OperationFileServiceAbortMultipartUpload))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "POST", path, in, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *FileServiceHTTPClientImpl) CompleteMultipartUpload(ctx context.Context, in *CompleteMultipartRequest, opts ...http.CallOption) (*CompleteMultipartResponse, error) {
|
||||
var out CompleteMultipartResponse
|
||||
pattern := "/files/multipart/complete"
|
||||
path := binding.EncodeURL(pattern, in, false)
|
||||
opts = append(opts, http.Operation(OperationFileServiceCompleteMultipartUpload))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "POST", path, in, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// CreateBucket Bucket operations
|
||||
func (c *FileServiceHTTPClientImpl) CreateBucket(ctx context.Context, in *CreateBucketRequest, opts ...http.CallOption) (*emptypb.Empty, error) {
|
||||
var out emptypb.Empty
|
||||
pattern := "/buckets"
|
||||
path := binding.EncodeURL(pattern, in, false)
|
||||
opts = append(opts, http.Operation(OperationFileServiceCreateBucket))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "POST", path, in, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// CreateFolder Folder operations
|
||||
func (c *FileServiceHTTPClientImpl) CreateFolder(ctx context.Context, in *CreateFolderRequest, opts ...http.CallOption) (*Folder, error) {
|
||||
var out Folder
|
||||
pattern := "/folders"
|
||||
path := binding.EncodeURL(pattern, in, false)
|
||||
opts = append(opts, http.Operation(OperationFileServiceCreateFolder))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "POST", path, in, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// CreateShare Share operations
|
||||
func (c *FileServiceHTTPClientImpl) CreateShare(ctx context.Context, in *CreateShareRequest, opts ...http.CallOption) (*ShareLink, error) {
|
||||
var out ShareLink
|
||||
pattern := "/share"
|
||||
path := binding.EncodeURL(pattern, in, false)
|
||||
opts = append(opts, http.Operation(OperationFileServiceCreateShare))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "POST", path, in, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *FileServiceHTTPClientImpl) DeleteBucket(ctx context.Context, in *DeleteBucketRequest, opts ...http.CallOption) (*emptypb.Empty, error) {
|
||||
var out emptypb.Empty
|
||||
pattern := "/buckets"
|
||||
path := binding.EncodeURL(pattern, in, true)
|
||||
opts = append(opts, http.Operation(OperationFileServiceDeleteBucket))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "DELETE", path, nil, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *FileServiceHTTPClientImpl) DeleteFile(ctx context.Context, in *DeleteFileRequest, opts ...http.CallOption) (*emptypb.Empty, error) {
|
||||
var out emptypb.Empty
|
||||
pattern := "/files/delete"
|
||||
path := binding.EncodeURL(pattern, in, true)
|
||||
opts = append(opts, http.Operation(OperationFileServiceDeleteFile))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "DELETE", path, nil, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *FileServiceHTTPClientImpl) DeleteFolder(ctx context.Context, in *DeleteFolderRequest, opts ...http.CallOption) (*emptypb.Empty, error) {
|
||||
var out emptypb.Empty
|
||||
pattern := "/folders/{id}"
|
||||
path := binding.EncodeURL(pattern, in, true)
|
||||
opts = append(opts, http.Operation(OperationFileServiceDeleteFolder))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "DELETE", path, nil, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *FileServiceHTTPClientImpl) DeleteShare(ctx context.Context, in *DeleteShareRequest, opts ...http.CallOption) (*emptypb.Empty, error) {
|
||||
var out emptypb.Empty
|
||||
pattern := "/share/{id}"
|
||||
path := binding.EncodeURL(pattern, in, true)
|
||||
opts = append(opts, http.Operation(OperationFileServiceDeleteShare))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "DELETE", path, nil, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *FileServiceHTTPClientImpl) DownloadFile(ctx context.Context, in *DownloadFileRequest, opts ...http.CallOption) (*DownloadFileResponse, error) {
|
||||
var out DownloadFileResponse
|
||||
pattern := "/files/download"
|
||||
path := binding.EncodeURL(pattern, in, true)
|
||||
opts = append(opts, http.Operation(OperationFileServiceDownloadFile))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "GET", path, nil, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *FileServiceHTTPClientImpl) DownloadShare(ctx context.Context, in *DownloadShareRequest, opts ...http.CallOption) (*DownloadShareResponse, error) {
|
||||
var out DownloadShareResponse
|
||||
pattern := "/share/{token}/download"
|
||||
path := binding.EncodeURL(pattern, in, false)
|
||||
opts = append(opts, http.Operation(OperationFileServiceDownloadShare))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "POST", path, in, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *FileServiceHTTPClientImpl) GetFileContent(ctx context.Context, in *GetFileContentRequest, opts ...http.CallOption) (*GetFileContentResponse, error) {
|
||||
var out GetFileContentResponse
|
||||
pattern := "/files/content"
|
||||
path := binding.EncodeURL(pattern, in, true)
|
||||
opts = append(opts, http.Operation(OperationFileServiceGetFileContent))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "GET", path, nil, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *FileServiceHTTPClientImpl) GetFilePreview(ctx context.Context, in *GetFilePreviewRequest, opts ...http.CallOption) (*GetFilePreviewResponse, error) {
|
||||
var out GetFilePreviewResponse
|
||||
pattern := "/files/preview"
|
||||
path := binding.EncodeURL(pattern, in, true)
|
||||
opts = append(opts, http.Operation(OperationFileServiceGetFilePreview))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "GET", path, nil, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *FileServiceHTTPClientImpl) GetFolder(ctx context.Context, in *GetFolderRequest, opts ...http.CallOption) (*FolderWithChildren, error) {
|
||||
var out FolderWithChildren
|
||||
pattern := "/folders/{id}"
|
||||
path := binding.EncodeURL(pattern, in, true)
|
||||
opts = append(opts, http.Operation(OperationFileServiceGetFolder))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "GET", path, nil, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *FileServiceHTTPClientImpl) GetFolderTree(ctx context.Context, in *GetFolderTreeRequest, opts ...http.CallOption) (*GetFolderTreeResponse, error) {
|
||||
var out GetFolderTreeResponse
|
||||
pattern := "/folders/tree"
|
||||
path := binding.EncodeURL(pattern, in, true)
|
||||
opts = append(opts, http.Operation(OperationFileServiceGetFolderTree))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "GET", path, nil, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *FileServiceHTTPClientImpl) GetShareInfo(ctx context.Context, in *GetShareInfoRequest, opts ...http.CallOption) (*ShareInfo, error) {
|
||||
var out ShareInfo
|
||||
pattern := "/share/{token}"
|
||||
path := binding.EncodeURL(pattern, in, true)
|
||||
opts = append(opts, http.Operation(OperationFileServiceGetShareInfo))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "GET", path, nil, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// InitMultipartUpload Multipart upload
|
||||
func (c *FileServiceHTTPClientImpl) InitMultipartUpload(ctx context.Context, in *InitMultipartRequest, opts ...http.CallOption) (*InitMultipartResponse, error) {
|
||||
var out InitMultipartResponse
|
||||
pattern := "/files/multipart/init"
|
||||
path := binding.EncodeURL(pattern, in, false)
|
||||
opts = append(opts, http.Operation(OperationFileServiceInitMultipartUpload))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "POST", path, in, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *FileServiceHTTPClientImpl) ListBuckets(ctx context.Context, in *emptypb.Empty, opts ...http.CallOption) (*ListBucketsResponse, error) {
|
||||
var out ListBucketsResponse
|
||||
pattern := "/buckets"
|
||||
path := binding.EncodeURL(pattern, in, true)
|
||||
opts = append(opts, http.Operation(OperationFileServiceListBuckets))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "GET", path, nil, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *FileServiceHTTPClientImpl) ListFiles(ctx context.Context, in *ListFilesRequest, opts ...http.CallOption) (*ListFilesResponse, error) {
|
||||
var out ListFilesResponse
|
||||
pattern := "/files/list"
|
||||
path := binding.EncodeURL(pattern, in, true)
|
||||
opts = append(opts, http.Operation(OperationFileServiceListFiles))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "GET", path, nil, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *FileServiceHTTPClientImpl) MoveFile(ctx context.Context, in *MoveFileRequest, opts ...http.CallOption) (*emptypb.Empty, error) {
|
||||
var out emptypb.Empty
|
||||
pattern := "/files/{id}/move"
|
||||
path := binding.EncodeURL(pattern, in, false)
|
||||
opts = append(opts, http.Operation(OperationFileServiceMoveFile))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "POST", path, in, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *FileServiceHTTPClientImpl) RenameFolder(ctx context.Context, in *RenameFolderRequest, opts ...http.CallOption) (*Folder, error) {
|
||||
var out Folder
|
||||
pattern := "/folders/{id}"
|
||||
path := binding.EncodeURL(pattern, in, false)
|
||||
opts = append(opts, http.Operation(OperationFileServiceRenameFolder))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "PUT", path, in, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// UploadFile File operations
|
||||
func (c *FileServiceHTTPClientImpl) UploadFile(ctx context.Context, in *UploadFileRequest, opts ...http.CallOption) (*UploadFileResponse, error) {
|
||||
var out UploadFileResponse
|
||||
pattern := "/files/upload"
|
||||
path := binding.EncodeURL(pattern, in, false)
|
||||
opts = append(opts, http.Operation(OperationFileServiceUploadFile))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "POST", path, in, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *FileServiceHTTPClientImpl) UploadPart(ctx context.Context, in *UploadPartRequest, opts ...http.CallOption) (*UploadPartResponse, error) {
|
||||
var out UploadPartResponse
|
||||
pattern := "/files/multipart/part"
|
||||
path := binding.EncodeURL(pattern, in, false)
|
||||
opts = append(opts, http.Operation(OperationFileServiceUploadPart))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "PUT", path, in, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *FileServiceHTTPClientImpl) UploadToFolder(ctx context.Context, in *UploadToFolderRequest, opts ...http.CallOption) (*FileMeta, error) {
|
||||
var out FileMeta
|
||||
pattern := "/folders/{folder_id}/files"
|
||||
path := binding.EncodeURL(pattern, in, false)
|
||||
opts = append(opts, http.Operation(OperationFileServiceUploadToFolder))
|
||||
opts = append(opts, http.PathTemplate(pattern))
|
||||
err := c.cc.Invoke(ctx, "POST", path, in, &out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
103
api/google/api/annotations.pb.go
Normal file
103
api/google/api/annotations.pb.go
Normal file
@ -0,0 +1,103 @@
|
||||
// Copyright 2015 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: google/api/annotations.proto
|
||||
|
||||
package annotations
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
descriptorpb "google.golang.org/protobuf/types/descriptorpb"
|
||||
reflect "reflect"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
var file_google_api_annotations_proto_extTypes = []protoimpl.ExtensionInfo{
|
||||
{
|
||||
ExtendedType: (*descriptorpb.MethodOptions)(nil),
|
||||
ExtensionType: (*HttpRule)(nil),
|
||||
Field: 72295728,
|
||||
Name: "google.api.http",
|
||||
Tag: "bytes,72295728,opt,name=http",
|
||||
Filename: "google/api/annotations.proto",
|
||||
},
|
||||
}
|
||||
|
||||
// Extension fields to descriptorpb.MethodOptions.
|
||||
var (
|
||||
// See `HttpRule`.
|
||||
//
|
||||
// optional google.api.HttpRule http = 72295728;
|
||||
E_Http = &file_google_api_annotations_proto_extTypes[0]
|
||||
)
|
||||
|
||||
var File_google_api_annotations_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_google_api_annotations_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x1cgoogle/api/annotations.proto\x12\n" +
|
||||
"google.api\x1a\x15google/api/http.proto\x1a google/protobuf/descriptor.proto:K\n" +
|
||||
"\x04http\x12\x1e.google.protobuf.MethodOptions\x18\xb0ʼ\" \x01(\v2\x14.google.api.HttpRuleR\x04httpBn\n" +
|
||||
"\x0ecom.google.apiB\x10AnnotationsProtoP\x01ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\xa2\x02\x04GAPIb\x06proto3"
|
||||
|
||||
var file_google_api_annotations_proto_goTypes = []any{
|
||||
(*descriptorpb.MethodOptions)(nil), // 0: google.protobuf.MethodOptions
|
||||
(*HttpRule)(nil), // 1: google.api.HttpRule
|
||||
}
|
||||
var file_google_api_annotations_proto_depIdxs = []int32{
|
||||
0, // 0: google.api.http:extendee -> google.protobuf.MethodOptions
|
||||
1, // 1: google.api.http:type_name -> google.api.HttpRule
|
||||
2, // [2:2] is the sub-list for method output_type
|
||||
2, // [2:2] is the sub-list for method input_type
|
||||
1, // [1:2] is the sub-list for extension type_name
|
||||
0, // [0:1] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_google_api_annotations_proto_init() }
|
||||
func file_google_api_annotations_proto_init() {
|
||||
if File_google_api_annotations_proto != nil {
|
||||
return
|
||||
}
|
||||
file_google_api_http_proto_init()
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_google_api_annotations_proto_rawDesc), len(file_google_api_annotations_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 0,
|
||||
NumExtensions: 1,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_google_api_annotations_proto_goTypes,
|
||||
DependencyIndexes: file_google_api_annotations_proto_depIdxs,
|
||||
ExtensionInfos: file_google_api_annotations_proto_extTypes,
|
||||
}.Build()
|
||||
File_google_api_annotations_proto = out.File
|
||||
file_google_api_annotations_proto_goTypes = nil
|
||||
file_google_api_annotations_proto_depIdxs = nil
|
||||
}
|
||||
405
api/google/api/http.pb.go
Normal file
405
api/google/api/http.pb.go
Normal file
@ -0,0 +1,405 @@
|
||||
// Copyright 2015 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: google/api/http.proto
|
||||
|
||||
package annotations
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type Http struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Rules []*HttpRule `protobuf:"bytes,1,rep,name=rules,proto3" json:"rules,omitempty"`
|
||||
FullyDecodeReservedExpansion bool `protobuf:"varint,2,opt,name=fully_decode_reserved_expansion,json=fullyDecodeReservedExpansion,proto3" json:"fully_decode_reserved_expansion,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Http) Reset() {
|
||||
*x = Http{}
|
||||
mi := &file_google_api_http_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Http) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Http) ProtoMessage() {}
|
||||
|
||||
func (x *Http) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_google_api_http_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Http.ProtoReflect.Descriptor instead.
|
||||
func (*Http) Descriptor() ([]byte, []int) {
|
||||
return file_google_api_http_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Http) GetRules() []*HttpRule {
|
||||
if x != nil {
|
||||
return x.Rules
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Http) GetFullyDecodeReservedExpansion() bool {
|
||||
if x != nil {
|
||||
return x.FullyDecodeReservedExpansion
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type HttpRule struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Selector string `protobuf:"bytes,1,opt,name=selector,proto3" json:"selector,omitempty"`
|
||||
// Types that are valid to be assigned to Pattern:
|
||||
//
|
||||
// *HttpRule_Get
|
||||
// *HttpRule_Put
|
||||
// *HttpRule_Post
|
||||
// *HttpRule_Delete
|
||||
// *HttpRule_Patch
|
||||
// *HttpRule_Custom
|
||||
Pattern isHttpRule_Pattern `protobuf_oneof:"pattern"`
|
||||
Body string `protobuf:"bytes,7,opt,name=body,proto3" json:"body,omitempty"`
|
||||
ResponseBody string `protobuf:"bytes,12,opt,name=response_body,json=responseBody,proto3" json:"response_body,omitempty"`
|
||||
AdditionalBindings []*HttpRule `protobuf:"bytes,11,rep,name=additional_bindings,json=additionalBindings,proto3" json:"additional_bindings,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *HttpRule) Reset() {
|
||||
*x = HttpRule{}
|
||||
mi := &file_google_api_http_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *HttpRule) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*HttpRule) ProtoMessage() {}
|
||||
|
||||
func (x *HttpRule) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_google_api_http_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use HttpRule.ProtoReflect.Descriptor instead.
|
||||
func (*HttpRule) Descriptor() ([]byte, []int) {
|
||||
return file_google_api_http_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *HttpRule) GetSelector() string {
|
||||
if x != nil {
|
||||
return x.Selector
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *HttpRule) GetPattern() isHttpRule_Pattern {
|
||||
if x != nil {
|
||||
return x.Pattern
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *HttpRule) GetGet() string {
|
||||
if x != nil {
|
||||
if x, ok := x.Pattern.(*HttpRule_Get); ok {
|
||||
return x.Get
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *HttpRule) GetPut() string {
|
||||
if x != nil {
|
||||
if x, ok := x.Pattern.(*HttpRule_Put); ok {
|
||||
return x.Put
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *HttpRule) GetPost() string {
|
||||
if x != nil {
|
||||
if x, ok := x.Pattern.(*HttpRule_Post); ok {
|
||||
return x.Post
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *HttpRule) GetDelete() string {
|
||||
if x != nil {
|
||||
if x, ok := x.Pattern.(*HttpRule_Delete); ok {
|
||||
return x.Delete
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *HttpRule) GetPatch() string {
|
||||
if x != nil {
|
||||
if x, ok := x.Pattern.(*HttpRule_Patch); ok {
|
||||
return x.Patch
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *HttpRule) GetCustom() *CustomHttpPattern {
|
||||
if x != nil {
|
||||
if x, ok := x.Pattern.(*HttpRule_Custom); ok {
|
||||
return x.Custom
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *HttpRule) GetBody() string {
|
||||
if x != nil {
|
||||
return x.Body
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *HttpRule) GetResponseBody() string {
|
||||
if x != nil {
|
||||
return x.ResponseBody
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *HttpRule) GetAdditionalBindings() []*HttpRule {
|
||||
if x != nil {
|
||||
return x.AdditionalBindings
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isHttpRule_Pattern interface {
|
||||
isHttpRule_Pattern()
|
||||
}
|
||||
|
||||
type HttpRule_Get struct {
|
||||
Get string `protobuf:"bytes,2,opt,name=get,proto3,oneof"`
|
||||
}
|
||||
|
||||
type HttpRule_Put struct {
|
||||
Put string `protobuf:"bytes,3,opt,name=put,proto3,oneof"`
|
||||
}
|
||||
|
||||
type HttpRule_Post struct {
|
||||
Post string `protobuf:"bytes,4,opt,name=post,proto3,oneof"`
|
||||
}
|
||||
|
||||
type HttpRule_Delete struct {
|
||||
Delete string `protobuf:"bytes,5,opt,name=delete,proto3,oneof"`
|
||||
}
|
||||
|
||||
type HttpRule_Patch struct {
|
||||
Patch string `protobuf:"bytes,6,opt,name=patch,proto3,oneof"`
|
||||
}
|
||||
|
||||
type HttpRule_Custom struct {
|
||||
Custom *CustomHttpPattern `protobuf:"bytes,8,opt,name=custom,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*HttpRule_Get) isHttpRule_Pattern() {}
|
||||
|
||||
func (*HttpRule_Put) isHttpRule_Pattern() {}
|
||||
|
||||
func (*HttpRule_Post) isHttpRule_Pattern() {}
|
||||
|
||||
func (*HttpRule_Delete) isHttpRule_Pattern() {}
|
||||
|
||||
func (*HttpRule_Patch) isHttpRule_Pattern() {}
|
||||
|
||||
func (*HttpRule_Custom) isHttpRule_Pattern() {}
|
||||
|
||||
type CustomHttpPattern struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Kind string `protobuf:"bytes,1,opt,name=kind,proto3" json:"kind,omitempty"`
|
||||
Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CustomHttpPattern) Reset() {
|
||||
*x = CustomHttpPattern{}
|
||||
mi := &file_google_api_http_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CustomHttpPattern) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CustomHttpPattern) ProtoMessage() {}
|
||||
|
||||
func (x *CustomHttpPattern) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_google_api_http_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CustomHttpPattern.ProtoReflect.Descriptor instead.
|
||||
func (*CustomHttpPattern) Descriptor() ([]byte, []int) {
|
||||
return file_google_api_http_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *CustomHttpPattern) GetKind() string {
|
||||
if x != nil {
|
||||
return x.Kind
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CustomHttpPattern) GetPath() string {
|
||||
if x != nil {
|
||||
return x.Path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_google_api_http_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_google_api_http_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x15google/api/http.proto\x12\n" +
|
||||
"google.api\"y\n" +
|
||||
"\x04Http\x12*\n" +
|
||||
"\x05rules\x18\x01 \x03(\v2\x14.google.api.HttpRuleR\x05rules\x12E\n" +
|
||||
"\x1ffully_decode_reserved_expansion\x18\x02 \x01(\bR\x1cfullyDecodeReservedExpansion\"\xda\x02\n" +
|
||||
"\bHttpRule\x12\x1a\n" +
|
||||
"\bselector\x18\x01 \x01(\tR\bselector\x12\x12\n" +
|
||||
"\x03get\x18\x02 \x01(\tH\x00R\x03get\x12\x12\n" +
|
||||
"\x03put\x18\x03 \x01(\tH\x00R\x03put\x12\x14\n" +
|
||||
"\x04post\x18\x04 \x01(\tH\x00R\x04post\x12\x18\n" +
|
||||
"\x06delete\x18\x05 \x01(\tH\x00R\x06delete\x12\x16\n" +
|
||||
"\x05patch\x18\x06 \x01(\tH\x00R\x05patch\x127\n" +
|
||||
"\x06custom\x18\b \x01(\v2\x1d.google.api.CustomHttpPatternH\x00R\x06custom\x12\x12\n" +
|
||||
"\x04body\x18\a \x01(\tR\x04body\x12#\n" +
|
||||
"\rresponse_body\x18\f \x01(\tR\fresponseBody\x12E\n" +
|
||||
"\x13additional_bindings\x18\v \x03(\v2\x14.google.api.HttpRuleR\x12additionalBindingsB\t\n" +
|
||||
"\apattern\";\n" +
|
||||
"\x11CustomHttpPattern\x12\x12\n" +
|
||||
"\x04kind\x18\x01 \x01(\tR\x04kind\x12\x12\n" +
|
||||
"\x04path\x18\x02 \x01(\tR\x04pathBj\n" +
|
||||
"\x0ecom.google.apiB\tHttpProtoP\x01ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\xf8\x01\x01\xa2\x02\x04GAPIb\x06proto3"
|
||||
|
||||
var (
|
||||
file_google_api_http_proto_rawDescOnce sync.Once
|
||||
file_google_api_http_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_google_api_http_proto_rawDescGZIP() []byte {
|
||||
file_google_api_http_proto_rawDescOnce.Do(func() {
|
||||
file_google_api_http_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_google_api_http_proto_rawDesc), len(file_google_api_http_proto_rawDesc)))
|
||||
})
|
||||
return file_google_api_http_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_google_api_http_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
|
||||
var file_google_api_http_proto_goTypes = []any{
|
||||
(*Http)(nil), // 0: google.api.Http
|
||||
(*HttpRule)(nil), // 1: google.api.HttpRule
|
||||
(*CustomHttpPattern)(nil), // 2: google.api.CustomHttpPattern
|
||||
}
|
||||
var file_google_api_http_proto_depIdxs = []int32{
|
||||
1, // 0: google.api.Http.rules:type_name -> google.api.HttpRule
|
||||
2, // 1: google.api.HttpRule.custom:type_name -> google.api.CustomHttpPattern
|
||||
1, // 2: google.api.HttpRule.additional_bindings:type_name -> google.api.HttpRule
|
||||
3, // [3:3] is the sub-list for method output_type
|
||||
3, // [3:3] is the sub-list for method input_type
|
||||
3, // [3:3] is the sub-list for extension type_name
|
||||
3, // [3:3] is the sub-list for extension extendee
|
||||
0, // [0:3] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_google_api_http_proto_init() }
|
||||
func file_google_api_http_proto_init() {
|
||||
if File_google_api_http_proto != nil {
|
||||
return
|
||||
}
|
||||
file_google_api_http_proto_msgTypes[1].OneofWrappers = []any{
|
||||
(*HttpRule_Get)(nil),
|
||||
(*HttpRule_Put)(nil),
|
||||
(*HttpRule_Post)(nil),
|
||||
(*HttpRule_Delete)(nil),
|
||||
(*HttpRule_Patch)(nil),
|
||||
(*HttpRule_Custom)(nil),
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_google_api_http_proto_rawDesc), len(file_google_api_http_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 3,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_google_api_http_proto_goTypes,
|
||||
DependencyIndexes: file_google_api_http_proto_depIdxs,
|
||||
MessageInfos: file_google_api_http_proto_msgTypes,
|
||||
}.Build()
|
||||
File_google_api_http_proto = out.File
|
||||
file_google_api_http_proto_goTypes = nil
|
||||
file_google_api_http_proto_depIdxs = nil
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: auth.proto
|
||||
// source: proto/auth.proto
|
||||
|
||||
package proto
|
||||
|
||||
@ -30,7 +30,7 @@ type ValidateTokenRequest struct {
|
||||
|
||||
func (x *ValidateTokenRequest) Reset() {
|
||||
*x = ValidateTokenRequest{}
|
||||
mi := &file_auth_proto_msgTypes[0]
|
||||
mi := &file_proto_auth_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -42,7 +42,7 @@ func (x *ValidateTokenRequest) String() string {
|
||||
func (*ValidateTokenRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ValidateTokenRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_auth_proto_msgTypes[0]
|
||||
mi := &file_proto_auth_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -55,7 +55,7 @@ func (x *ValidateTokenRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use ValidateTokenRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ValidateTokenRequest) Descriptor() ([]byte, []int) {
|
||||
return file_auth_proto_rawDescGZIP(), []int{0}
|
||||
return file_proto_auth_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *ValidateTokenRequest) GetToken() string {
|
||||
@ -80,7 +80,7 @@ type ValidateTokenResponse struct {
|
||||
|
||||
func (x *ValidateTokenResponse) Reset() {
|
||||
*x = ValidateTokenResponse{}
|
||||
mi := &file_auth_proto_msgTypes[1]
|
||||
mi := &file_proto_auth_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -92,7 +92,7 @@ func (x *ValidateTokenResponse) String() string {
|
||||
func (*ValidateTokenResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ValidateTokenResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_auth_proto_msgTypes[1]
|
||||
mi := &file_proto_auth_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -105,7 +105,7 @@ func (x *ValidateTokenResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use ValidateTokenResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ValidateTokenResponse) Descriptor() ([]byte, []int) {
|
||||
return file_auth_proto_rawDescGZIP(), []int{1}
|
||||
return file_proto_auth_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *ValidateTokenResponse) GetValid() bool {
|
||||
@ -167,7 +167,7 @@ type CheckPermissionRequest struct {
|
||||
|
||||
func (x *CheckPermissionRequest) Reset() {
|
||||
*x = CheckPermissionRequest{}
|
||||
mi := &file_auth_proto_msgTypes[2]
|
||||
mi := &file_proto_auth_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -179,7 +179,7 @@ func (x *CheckPermissionRequest) String() string {
|
||||
func (*CheckPermissionRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CheckPermissionRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_auth_proto_msgTypes[2]
|
||||
mi := &file_proto_auth_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -192,7 +192,7 @@ func (x *CheckPermissionRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CheckPermissionRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CheckPermissionRequest) Descriptor() ([]byte, []int) {
|
||||
return file_auth_proto_rawDescGZIP(), []int{2}
|
||||
return file_proto_auth_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *CheckPermissionRequest) GetToken() string {
|
||||
@ -220,7 +220,7 @@ type CheckPermissionResponse struct {
|
||||
|
||||
func (x *CheckPermissionResponse) Reset() {
|
||||
*x = CheckPermissionResponse{}
|
||||
mi := &file_auth_proto_msgTypes[3]
|
||||
mi := &file_proto_auth_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -232,7 +232,7 @@ func (x *CheckPermissionResponse) String() string {
|
||||
func (*CheckPermissionResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CheckPermissionResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_auth_proto_msgTypes[3]
|
||||
mi := &file_proto_auth_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -245,7 +245,7 @@ func (x *CheckPermissionResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CheckPermissionResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CheckPermissionResponse) Descriptor() ([]byte, []int) {
|
||||
return file_auth_proto_rawDescGZIP(), []int{3}
|
||||
return file_proto_auth_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *CheckPermissionResponse) GetAllowed() bool {
|
||||
@ -269,12 +269,11 @@ func (x *CheckPermissionResponse) GetRoles() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_auth_proto protoreflect.FileDescriptor
|
||||
var File_proto_auth_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_auth_proto_rawDesc = "" +
|
||||
const file_proto_auth_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\n" +
|
||||
"auth.proto\x12\x04auth\",\n" +
|
||||
"\x10proto/auth.proto\x12\x04auth\",\n" +
|
||||
"\x14ValidateTokenRequest\x12\x14\n" +
|
||||
"\x05token\x18\x01 \x01(\tR\x05token\"\xcf\x01\n" +
|
||||
"\x15ValidateTokenResponse\x12\x14\n" +
|
||||
@ -297,29 +296,28 @@ const file_auth_proto_rawDesc = "" +
|
||||
"\x05roles\x18\x03 \x03(\tR\x05roles2\xa7\x01\n" +
|
||||
"\vAuthService\x12H\n" +
|
||||
"\rValidateToken\x12\x1a.auth.ValidateTokenRequest\x1a\x1b.auth.ValidateTokenResponse\x12N\n" +
|
||||
"\x0fCheckPermission\x12\x1c.auth.CheckPermissionRequest\x1a\x1d.auth.CheckPermissionResponseB`\n" +
|
||||
"\bcom.authB\tAuthProtoP\x01Z\x19rag/file-system/api/proto\xa2\x02\x03AXX\xaa\x02\x04Auth\xca\x02\x04Auth\xe2\x02\x10Auth\\GPBMetadata\xea\x02\x04Authb\x06proto3"
|
||||
"\x0fCheckPermission\x12\x1c.auth.CheckPermissionRequest\x1a\x1d.auth.CheckPermissionResponseB\x1bZ\x19rag/file-system/api/protob\x06proto3"
|
||||
|
||||
var (
|
||||
file_auth_proto_rawDescOnce sync.Once
|
||||
file_auth_proto_rawDescData []byte
|
||||
file_proto_auth_proto_rawDescOnce sync.Once
|
||||
file_proto_auth_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_auth_proto_rawDescGZIP() []byte {
|
||||
file_auth_proto_rawDescOnce.Do(func() {
|
||||
file_auth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_auth_proto_rawDesc), len(file_auth_proto_rawDesc)))
|
||||
func file_proto_auth_proto_rawDescGZIP() []byte {
|
||||
file_proto_auth_proto_rawDescOnce.Do(func() {
|
||||
file_proto_auth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_auth_proto_rawDesc), len(file_proto_auth_proto_rawDesc)))
|
||||
})
|
||||
return file_auth_proto_rawDescData
|
||||
return file_proto_auth_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
|
||||
var file_auth_proto_goTypes = []any{
|
||||
var file_proto_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
|
||||
var file_proto_auth_proto_goTypes = []any{
|
||||
(*ValidateTokenRequest)(nil), // 0: auth.ValidateTokenRequest
|
||||
(*ValidateTokenResponse)(nil), // 1: auth.ValidateTokenResponse
|
||||
(*CheckPermissionRequest)(nil), // 2: auth.CheckPermissionRequest
|
||||
(*CheckPermissionResponse)(nil), // 3: auth.CheckPermissionResponse
|
||||
}
|
||||
var file_auth_proto_depIdxs = []int32{
|
||||
var file_proto_auth_proto_depIdxs = []int32{
|
||||
0, // 0: auth.AuthService.ValidateToken:input_type -> auth.ValidateTokenRequest
|
||||
2, // 1: auth.AuthService.CheckPermission:input_type -> auth.CheckPermissionRequest
|
||||
1, // 2: auth.AuthService.ValidateToken:output_type -> auth.ValidateTokenResponse
|
||||
@ -331,26 +329,26 @@ var file_auth_proto_depIdxs = []int32{
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_auth_proto_init() }
|
||||
func file_auth_proto_init() {
|
||||
if File_auth_proto != nil {
|
||||
func init() { file_proto_auth_proto_init() }
|
||||
func file_proto_auth_proto_init() {
|
||||
if File_proto_auth_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_auth_proto_rawDesc), len(file_auth_proto_rawDesc)),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_auth_proto_rawDesc), len(file_proto_auth_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 4,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_auth_proto_goTypes,
|
||||
DependencyIndexes: file_auth_proto_depIdxs,
|
||||
MessageInfos: file_auth_proto_msgTypes,
|
||||
GoTypes: file_proto_auth_proto_goTypes,
|
||||
DependencyIndexes: file_proto_auth_proto_depIdxs,
|
||||
MessageInfos: file_proto_auth_proto_msgTypes,
|
||||
}.Build()
|
||||
File_auth_proto = out.File
|
||||
file_auth_proto_goTypes = nil
|
||||
file_auth_proto_depIdxs = nil
|
||||
File_proto_auth_proto = out.File
|
||||
file_proto_auth_proto_goTypes = nil
|
||||
file_proto_auth_proto_depIdxs = nil
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.1
|
||||
// - protoc (unknown)
|
||||
// source: auth.proto
|
||||
// source: proto/auth.proto
|
||||
|
||||
package proto
|
||||
|
||||
@ -155,5 +155,5 @@ var AuthService_ServiceDesc = grpc.ServiceDesc{
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "auth.proto",
|
||||
Metadata: "proto/auth.proto",
|
||||
}
|
||||
|
||||
16
buf.gen.yaml
16
buf.gen.yaml
@ -1,13 +1,13 @@
|
||||
version: v2
|
||||
managed:
|
||||
enabled: true
|
||||
override:
|
||||
- file_option: go_package_prefix
|
||||
value: rag/file-system/api/proto
|
||||
enabled: false
|
||||
plugins:
|
||||
- remote: buf.build/protocolbuffers/go
|
||||
out: api/proto
|
||||
- local: protoc-gen-go
|
||||
out: api
|
||||
opt: paths=source_relative
|
||||
- remote: buf.build/grpc/go
|
||||
out: api/proto
|
||||
- local: protoc-gen-go-grpc
|
||||
out: api
|
||||
opt: paths=source_relative
|
||||
- local: protoc-gen-go-http
|
||||
out: api
|
||||
opt: paths=source_relative
|
||||
|
||||
9
buf.yaml
9
buf.yaml
@ -1,3 +1,10 @@
|
||||
version: v2
|
||||
modules:
|
||||
- path: api/proto
|
||||
- path: api
|
||||
- path: third_party
|
||||
lint:
|
||||
use:
|
||||
- DEFAULT
|
||||
breaking:
|
||||
use:
|
||||
- FILE
|
||||
|
||||
@ -2,276 +2,64 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"flag"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
_ "rag/file-system/docs"
|
||||
"rag/file-system/internal/api/endpoints"
|
||||
"rag/file-system/internal/api/handlers"
|
||||
"rag/file-system/internal/api/validators"
|
||||
"rag/file-system/internal/common"
|
||||
"rag/file-system/internal/domain/model"
|
||||
"rag/file-system/internal/domain/repository"
|
||||
"rag/file-system/internal/infrastructure/database"
|
||||
grpcAuth "rag/file-system/internal/infrastructure/grpc"
|
||||
"rag/file-system/internal/infrastructure/mediator"
|
||||
dbrepo "rag/file-system/internal/infrastructure/repository"
|
||||
s3infra "rag/file-system/internal/infrastructure/s3"
|
||||
"rag/file-system/internal/middleware"
|
||||
"rag/file-system/internal/conf"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
|
||||
"github.com/go-kratos/kratos/v2/config"
|
||||
fileconfig "github.com/go-kratos/kratos/v2/config/file"
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
)
|
||||
|
||||
// @title RustFS File System API
|
||||
// @version 1.3
|
||||
// @description RustFS file storage API with multipart upload, file preview, and paginated queries.
|
||||
// @host localhost:8080
|
||||
// @BasePath /
|
||||
// @securityDefinitions.apikey ApiKeyAuth
|
||||
// @in header
|
||||
// @name X-API-Key
|
||||
func main() {
|
||||
common.InitLogger()
|
||||
var (
|
||||
flagconf string
|
||||
)
|
||||
|
||||
cfg := common.LoadConfig()
|
||||
if err := cfg.Validate(); err != nil {
|
||||
common.Logger.Error("configuration error", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// OpenTelemetry
|
||||
otelShutdown := common.InitOTel(context.Background(), cfg.OTelEndpoint)
|
||||
defer func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := otelShutdown(ctx); err != nil {
|
||||
common.Logger.Error("OTel shutdown error", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Infrastructure — S3
|
||||
rustfsClient := s3infra.NewRustFSClient(cfg)
|
||||
s3Repo := s3infra.NewS3FileRepository(rustfsClient)
|
||||
m := mediator.NewMediator()
|
||||
|
||||
// Infrastructure — PostgreSQL
|
||||
pgDB, err := database.NewPostgresDB(cfg.DatabaseURL)
|
||||
if err != nil {
|
||||
common.Logger.Error("failed to connect to database", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer pgDB.Close()
|
||||
|
||||
if err := database.RunMigrations(pgDB); err != nil {
|
||||
common.Logger.Error("failed to run migrations", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
folderRepo := dbrepo.NewFolderRepository(pgDB)
|
||||
fileMetaRepo := dbrepo.NewFileMetaRepository(pgDB)
|
||||
shareRepo := dbrepo.NewShareRepository(pgDB)
|
||||
|
||||
// gRPC Auth Client (if configured)
|
||||
var authClient *grpcAuth.AuthClient
|
||||
if cfg.GRPCAuthAddr != "" {
|
||||
var err error
|
||||
authClient, err = grpcAuth.NewAuthClient(cfg.GRPCAuthAddr)
|
||||
if err != nil {
|
||||
common.Logger.Error("failed to connect to auth gRPC server", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer authClient.Close()
|
||||
common.Logger.Info("gRPC auth client connected", "addr", cfg.GRPCAuthAddr)
|
||||
}
|
||||
|
||||
// Handlers
|
||||
uploadHandler := handlers.NewUploadFileHandler(s3Repo)
|
||||
downloadHandler := handlers.NewDownloadFileHandler(s3Repo)
|
||||
createBucketHandler := handlers.NewCreateBucketHandler(s3Repo)
|
||||
listBucketsHandler := handlers.NewListBucketsHandler(s3Repo)
|
||||
deleteBucketHandler := handlers.NewDeleteBucketHandler(s3Repo)
|
||||
listFilesHandler := handlers.NewListFilesHandler(s3Repo)
|
||||
previewHandler := handlers.NewGetFilePreviewHandler(s3Repo)
|
||||
initMultipartHandler := handlers.NewInitMultipartHandler(s3Repo)
|
||||
uploadPartHandler := handlers.NewUploadPartHandler(s3Repo)
|
||||
completeMultipartHandler := handlers.NewCompleteMultipartHandler(s3Repo)
|
||||
deleteFileHandler := handlers.NewDeleteFileHandler(s3Repo)
|
||||
abortMultipartHandler := handlers.NewAbortMultipartHandler(s3Repo)
|
||||
fileContentHandler := handlers.NewGetFileContentHandler(s3Repo)
|
||||
loginHandler := handlers.NewLoginHandler(cfg.AuthAPIKey)
|
||||
|
||||
// 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)
|
||||
mediator.Register[handlers.DeleteBucketCommand, string](m, deleteBucketHandler)
|
||||
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)
|
||||
mediator.Register[handlers.DeleteFileCommand, string](m, deleteFileHandler)
|
||||
mediator.Register[handlers.AbortMultipartCommand, string](m, abortMultipartHandler)
|
||||
mediator.Register[handlers.GetFileContentQuery, string](m, fileContentHandler)
|
||||
mediator.Register[handlers.LoginQuery, handlers.LoginResult](m, loginHandler)
|
||||
|
||||
// --- New folder/file/share handlers ---
|
||||
createFolderHandler := handlers.NewCreateFolderHandler(folderRepo)
|
||||
renameFolderHandler := handlers.NewRenameFolderHandler(folderRepo)
|
||||
deleteFolderHandler := handlers.NewDeleteFolderHandler(folderRepo, s3Repo)
|
||||
getFolderHandler := handlers.NewGetFolderHandler(folderRepo)
|
||||
getFolderTreeHandler := handlers.NewGetFolderTreeHandler(folderRepo)
|
||||
uploadToFolderHandler := handlers.NewUploadToFolderHandler(fileMetaRepo, folderRepo, s3Repo)
|
||||
moveFileHandler := handlers.NewMoveFileHandler(fileMetaRepo)
|
||||
createShareHandler := handlers.NewCreateShareHandler(shareRepo)
|
||||
deleteShareHandler := handlers.NewDeleteShareHandler(shareRepo)
|
||||
getShareInfoHandler := handlers.NewGetShareInfoHandler(shareRepo, fileMetaRepo)
|
||||
downloadShareHandler := handlers.NewDownloadShareHandler(shareRepo, fileMetaRepo, s3Repo)
|
||||
|
||||
mediator.Register[handlers.CreateFolderCommand, *model.Folder](m, createFolderHandler)
|
||||
mediator.Register[handlers.RenameFolderCommand, *model.Folder](m, renameFolderHandler)
|
||||
mediator.Register[handlers.DeleteFolderCommand, string](m, deleteFolderHandler)
|
||||
mediator.Register[handlers.GetFolderQuery, *model.FolderWithChildren](m, getFolderHandler)
|
||||
mediator.Register[handlers.GetFolderTreeQuery, []model.Folder](m, getFolderTreeHandler)
|
||||
mediator.Register[handlers.UploadToFolderCommand, *model.FileMeta](m, uploadToFolderHandler)
|
||||
mediator.Register[handlers.MoveFileCommand, string](m, moveFileHandler)
|
||||
mediator.Register[handlers.CreateShareCommand, *model.ShareLink](m, createShareHandler)
|
||||
mediator.Register[handlers.DeleteShareCommand, string](m, deleteShareHandler)
|
||||
mediator.Register[handlers.GetShareInfoQuery, *model.ShareInfo](m, getShareInfoHandler)
|
||||
mediator.Register[handlers.DownloadShareQuery, string](m, downloadShareHandler)
|
||||
|
||||
// Validators
|
||||
fileValidator := validators.NewFileValidator()
|
||||
createBucketValidator := validators.NewCreateBucketValidator()
|
||||
folderValidator := validators.NewFolderValidator()
|
||||
shareValidator := validators.NewShareValidator()
|
||||
|
||||
// Endpoints
|
||||
fileEndpoint := endpoints.NewFileEndpoint(m, fileValidator)
|
||||
bucketEndpoint := endpoints.NewBucketEndpoint(m, createBucketValidator)
|
||||
authEndpoint := endpoints.NewAuthEndpoint(m)
|
||||
folderEndpoint := endpoints.NewFolderEndpoint(m, folderValidator)
|
||||
shareEndpoint := endpoints.NewShareEndpoint(m, shareValidator)
|
||||
|
||||
// Router
|
||||
r := gin.Default()
|
||||
r.MaxMultipartMemory = 32 << 20 // 32MB
|
||||
|
||||
// OpenTelemetry Gin middleware
|
||||
r.Use(otelgin.Middleware("file-system"))
|
||||
|
||||
// Middleware
|
||||
r.Use(middleware.RequestIDMiddleware())
|
||||
r.Use(middleware.LoggingMiddleware())
|
||||
r.Use(middleware.TimeoutMiddleware(time.Duration(cfg.RequestTimeout) * time.Second))
|
||||
|
||||
// CORS
|
||||
corsConfig := cors.Config{
|
||||
AllowOrigins: []string{"*"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowHeaders: []string{"Content-Type", "Content-Length", "X-API-Key", "X-Request-ID", "Authorization"},
|
||||
AllowCredentials: false,
|
||||
}
|
||||
r.Use(cors.New(corsConfig))
|
||||
|
||||
// Health check
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
"time": time.Now().UTC(),
|
||||
})
|
||||
})
|
||||
|
||||
// Public endpoints (no auth, rate limited)
|
||||
r.POST("/auth/login", middleware.RateLimitMiddleware(1, 5), authEndpoint.Login)
|
||||
|
||||
// Public share access (no auth)
|
||||
r.GET("/share/:token", shareEndpoint.GetShareInfo)
|
||||
r.POST("/share/:token/download", shareEndpoint.DownloadShare)
|
||||
|
||||
// API group with auth middleware
|
||||
api := r.Group("/")
|
||||
if authClient != nil {
|
||||
api.Use(middleware.JWTAuthMiddleware(authClient))
|
||||
} else {
|
||||
api.Use(middleware.AuthMiddleware(cfg.AuthAPIKey))
|
||||
}
|
||||
{
|
||||
// File operations
|
||||
api.POST("/files/upload", fileEndpoint.UploadFile)
|
||||
api.GET("/files/download", fileEndpoint.DownloadFile)
|
||||
api.GET("/files/list", fileEndpoint.ListFiles)
|
||||
api.GET("/files/preview", fileEndpoint.GetPreviewURL)
|
||||
api.GET("/files/content", fileEndpoint.GetFileContent)
|
||||
api.DELETE("/files/delete", fileEndpoint.DeleteFile)
|
||||
|
||||
// Multipart Upload
|
||||
api.POST("/files/multipart/init", fileEndpoint.InitMultipart)
|
||||
api.PUT("/files/multipart/part", fileEndpoint.UploadPart)
|
||||
api.POST("/files/multipart/complete", fileEndpoint.CompleteMultipart)
|
||||
api.POST("/files/multipart/abort", fileEndpoint.AbortMultipart)
|
||||
|
||||
// Bucket operations
|
||||
api.POST("/buckets", bucketEndpoint.CreateBucket)
|
||||
api.GET("/buckets", bucketEndpoint.ListBuckets)
|
||||
api.DELETE("/buckets", bucketEndpoint.DeleteBucket)
|
||||
|
||||
// Folder operations
|
||||
api.POST("/folders", folderEndpoint.CreateFolder)
|
||||
api.GET("/folders/tree", folderEndpoint.GetFolderTree)
|
||||
api.GET("/folders/:id", folderEndpoint.GetFolder)
|
||||
api.PUT("/folders/:id", folderEndpoint.RenameFolder)
|
||||
api.DELETE("/folders/:id", folderEndpoint.DeleteFolder)
|
||||
api.POST("/folders/:folderId/files", folderEndpoint.UploadToFolder)
|
||||
api.POST("/files/:id/move", folderEndpoint.MoveFile)
|
||||
|
||||
// Share management (auth required)
|
||||
api.POST("/share", shareEndpoint.CreateShare)
|
||||
api.DELETE("/share/:id", shareEndpoint.DeleteShare)
|
||||
}
|
||||
|
||||
// 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")
|
||||
})
|
||||
|
||||
// Graceful shutdown
|
||||
srv := &http.Server{
|
||||
Addr: ":" + cfg.ServerPort,
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
go func() {
|
||||
common.Logger.Info("server starting", "port", cfg.ServerPort)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
common.Logger.Error("server failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
common.Logger.Info("shutting down server...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
log.Printf("server forced shutdown: %v", err)
|
||||
}
|
||||
common.Logger.Info("server exited")
|
||||
func init() {
|
||||
flag.StringVar(&flagconf, "conf", "configs/config.yaml", "config path, eg: -conf config.yaml")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
logger := log.With(log.NewStdLogger(os.Stdout),
|
||||
"ts", log.DefaultTimestamp,
|
||||
"caller", log.DefaultCaller,
|
||||
"service.kind", "file-system",
|
||||
)
|
||||
|
||||
c := config.New(
|
||||
config.WithSource(
|
||||
fileconfig.NewSource(flagconf),
|
||||
),
|
||||
)
|
||||
if err := c.Load(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var bc conf.Bootstrap
|
||||
if err := c.Scan(&bc); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ctx, cleanup, err := initApp(&bc, logger)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
// Start Watermill router alongside the Kratos app.
|
||||
if ctx.CQRSBus != nil {
|
||||
go func() {
|
||||
if err := ctx.CQRSBus.Router.Run(context.Background()); err != nil {
|
||||
logger.Log(log.LevelError, "msg", "watermill router error", "error", err)
|
||||
}
|
||||
}()
|
||||
defer ctx.CQRSBus.Router.Close()
|
||||
}
|
||||
|
||||
if err := ctx.App.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
94
cmd/server/wire.go
Normal file
94
cmd/server/wire.go
Normal file
@ -0,0 +1,94 @@
|
||||
//go:build wireinject
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"rag/file-system/internal/biz"
|
||||
"rag/file-system/internal/conf"
|
||||
"rag/file-system/internal/data"
|
||||
"rag/file-system/internal/server"
|
||||
"rag/file-system/internal/service"
|
||||
"rag/file-system/internal/watermark"
|
||||
|
||||
"github.com/go-kratos/kratos/v2"
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
"github.com/go-kratos/kratos/v2/transport/grpc"
|
||||
"github.com/go-kratos/kratos/v2/transport/http"
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
// appContext holds the Kratos app and CQRS bus together for Wire.
|
||||
type appContext struct {
|
||||
App *kratos.App
|
||||
CQRSBus *watermark.CQRSBus
|
||||
}
|
||||
|
||||
// newApp creates a new Kratos application with HTTP and gRPC servers.
|
||||
func newApp(logger log.Logger, hs *http.Server, gs *grpc.Server) *kratos.App {
|
||||
return kratos.New(
|
||||
kratos.Name("file-system"),
|
||||
kratos.Logger(logger),
|
||||
kratos.Server(hs, gs),
|
||||
)
|
||||
}
|
||||
|
||||
// newAppContext wraps the app and CQRSBus into an appContext.
|
||||
func newAppContext(app *kratos.App, bus *watermark.CQRSBus) *appContext {
|
||||
return &appContext{App: app, CQRSBus: bus}
|
||||
}
|
||||
|
||||
// newConfServer extracts the Server config from Bootstrap.
|
||||
func newConfServer(bc *conf.Bootstrap) *conf.Server {
|
||||
return bc.GetServer()
|
||||
}
|
||||
|
||||
// newConfData extracts the Data config from Bootstrap.
|
||||
func newConfData(bc *conf.Bootstrap) *conf.Data {
|
||||
return bc.GetData()
|
||||
}
|
||||
|
||||
// newConfAuth extracts the Auth config from Bootstrap.
|
||||
func newConfAuth(bc *conf.Bootstrap) *conf.Auth {
|
||||
return bc.GetAuth()
|
||||
}
|
||||
|
||||
// newSQLDB extracts the underlying *sql.DB from Data for Watermill.
|
||||
func newSQLDB(d *data.Data) (*sql.DB, error) {
|
||||
return d.SqlDB()
|
||||
}
|
||||
|
||||
// initApp wires up the entire dependency graph.
|
||||
func initApp(*conf.Bootstrap, log.Logger) (*appContext, func(), error) {
|
||||
panic(wire.Build(
|
||||
// Config extraction
|
||||
newConfServer,
|
||||
newConfData,
|
||||
newConfAuth,
|
||||
|
||||
// Provider sets from each layer
|
||||
data.ProviderSet,
|
||||
biz.ProviderSet,
|
||||
service.ProviderSet,
|
||||
server.ProviderSet,
|
||||
watermark.ProviderSet,
|
||||
|
||||
// Extract *sql.DB from Data for Watermill
|
||||
newSQLDB,
|
||||
|
||||
// Interface bindings: biz interfaces -> data implementations
|
||||
wire.Bind(new(biz.FileRepo), new(*data.FileRepo)),
|
||||
wire.Bind(new(biz.FolderRepo), new(*data.FolderRepo)),
|
||||
wire.Bind(new(biz.FileMetaRepo), new(*data.FileMetaRepo)),
|
||||
wire.Bind(new(biz.FileMetaRepoForShare), new(*data.FileMetaRepo)),
|
||||
wire.Bind(new(biz.ShareRepo), new(*data.ShareRepo)),
|
||||
|
||||
// Interface binding: biz EventPublisher -> watermark EventBusPublisher
|
||||
wire.Bind(new(biz.EventPublisher), new(*watermark.EventBusPublisher)),
|
||||
|
||||
// Wire up app + CQRSBus into appContext
|
||||
newAppContext,
|
||||
newApp,
|
||||
))
|
||||
}
|
||||
100
cmd/server/wire_gen.go
Normal file
100
cmd/server/wire_gen.go
Normal file
@ -0,0 +1,100 @@
|
||||
// Code generated by Wire. DO NOT EDIT.
|
||||
|
||||
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
|
||||
//go:build !wireinject
|
||||
// +build !wireinject
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/go-kratos/kratos/v2"
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
"github.com/go-kratos/kratos/v2/transport/grpc"
|
||||
"github.com/go-kratos/kratos/v2/transport/http"
|
||||
"rag/file-system/internal/biz"
|
||||
"rag/file-system/internal/conf"
|
||||
"rag/file-system/internal/data"
|
||||
"rag/file-system/internal/server"
|
||||
"rag/file-system/internal/service"
|
||||
"rag/file-system/internal/watermark"
|
||||
)
|
||||
|
||||
// Injectors from wire.go:
|
||||
|
||||
// initApp wires up the entire dependency graph.
|
||||
func initApp(bootstrap *conf.Bootstrap, logger log.Logger) (*appContext, func(), error) {
|
||||
confServer := newConfServer(bootstrap)
|
||||
auth := newConfAuth(bootstrap)
|
||||
confData := newConfData(bootstrap)
|
||||
fileRepo := data.NewFileRepo(confData)
|
||||
dataData, cleanup, err := data.NewData(confData, logger)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
db, err := newSQLDB(dataData)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
cqrsHandler := watermark.NewCQRSHandler(logger)
|
||||
cqrsBus, err := watermark.NewCQRSBusWithHandlers(db, cqrsHandler, logger)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
eventBusPublisher := watermark.NewEventBusPublisher(cqrsBus)
|
||||
fileUsecase := biz.NewFileUsecase(fileRepo, eventBusPublisher, logger)
|
||||
bucketUsecase := biz.NewBucketUsecase(fileRepo, logger)
|
||||
folderRepo := data.NewFolderRepo(dataData, logger)
|
||||
fileMetaRepo := data.NewFileMetaRepo(dataData, logger)
|
||||
folderUsecase := biz.NewFolderUsecase(folderRepo, fileMetaRepo, fileRepo, eventBusPublisher, logger)
|
||||
shareRepo := data.NewShareRepo(dataData, logger)
|
||||
shareUsecase := biz.NewShareUsecase(shareRepo, fileMetaRepo, fileRepo, eventBusPublisher, logger)
|
||||
fileService := service.NewFileService(fileUsecase, bucketUsecase, folderUsecase, shareUsecase, logger)
|
||||
httpServer := server.NewHTTPServer(confServer, auth, fileService, logger)
|
||||
grpcServer := server.NewGRPCServer(confServer, auth, fileService, logger)
|
||||
app := newApp(logger, httpServer, grpcServer)
|
||||
mainAppContext := newAppContext(app, cqrsBus)
|
||||
return mainAppContext, func() {
|
||||
cleanup()
|
||||
}, nil
|
||||
}
|
||||
|
||||
// wire.go:
|
||||
|
||||
// appContext holds the Kratos app and CQRS bus together for Wire.
|
||||
type appContext struct {
|
||||
App *kratos.App
|
||||
CQRSBus *watermark.CQRSBus
|
||||
}
|
||||
|
||||
// newApp creates a new Kratos application with HTTP and gRPC servers.
|
||||
func newApp(logger log.Logger, hs *http.Server, gs *grpc.Server) *kratos.App {
|
||||
return kratos.New(kratos.Name("file-system"), kratos.Logger(logger), kratos.Server(hs, gs))
|
||||
}
|
||||
|
||||
// newAppContext wraps the app and CQRSBus into an appContext.
|
||||
func newAppContext(app *kratos.App, bus *watermark.CQRSBus) *appContext {
|
||||
return &appContext{App: app, CQRSBus: bus}
|
||||
}
|
||||
|
||||
// newConfServer extracts the Server config from Bootstrap.
|
||||
func newConfServer(bc *conf.Bootstrap) *conf.Server {
|
||||
return bc.GetServer()
|
||||
}
|
||||
|
||||
// newConfData extracts the Data config from Bootstrap.
|
||||
func newConfData(bc *conf.Bootstrap) *conf.Data {
|
||||
return bc.GetData()
|
||||
}
|
||||
|
||||
// newConfAuth extracts the Auth config from Bootstrap.
|
||||
func newConfAuth(bc *conf.Bootstrap) *conf.Auth {
|
||||
return bc.GetAuth()
|
||||
}
|
||||
|
||||
// newSQLDB extracts the underlying *sql.DB from Data for Watermill.
|
||||
func newSQLDB(d *data.Data) (*sql.DB, error) {
|
||||
return d.SqlDB()
|
||||
}
|
||||
21
configs/config.yaml
Normal file
21
configs/config.yaml
Normal file
@ -0,0 +1,21 @@
|
||||
server:
|
||||
http:
|
||||
addr: 0.0.0.0:8090
|
||||
timeout: 30s
|
||||
grpc:
|
||||
addr: 0.0.0.0:9090
|
||||
timeout: 30s
|
||||
|
||||
data:
|
||||
database:
|
||||
driver: postgres
|
||||
source: "postgres://rag:rag123@localhost:5432/file_system?sslmode=disable"
|
||||
s3:
|
||||
endpoint: "http://localhost:9000"
|
||||
access_key: "minioadmin"
|
||||
secret_key: "minioadmin"
|
||||
region: "us-east-1"
|
||||
|
||||
auth:
|
||||
jwt_key: "RagJwtSecretKey2026MustBeAtLeast32CharsLong!"
|
||||
grpc_addr: "localhost:50051"
|
||||
@ -1,34 +1,22 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
file-system-server:
|
||||
image: 192.168.1.154:31010/docker/file-system-server:latest
|
||||
container_name: file-system-server
|
||||
file-system:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
restart: unless-stopped
|
||||
- "9000:9000"
|
||||
environment:
|
||||
- GIN_MODE=release
|
||||
- RUSTFS_ENDPOINT_URL=http://192.168.1.154:9000
|
||||
- RUSTFS_ACCESS_KEY_ID=rustfsadmin
|
||||
- RUSTFS_SECRET_ACCESS_KEY=rustfsadmin123
|
||||
- RUSTFS_REGION=us-east-1
|
||||
- AUTH_API_KEY=rustfsadmin123
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/swagger/index.html"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
- RUSTFS_ACCESS_KEY_ID=${RUSTFS_ACCESS_KEY_ID}
|
||||
- RUSTFS_SECRET_ACCESS_KEY=${RUSTFS_SECRET_ACCESS_KEY}
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
logs:
|
||||
driver: local
|
||||
- ./configs/config.yaml:/app/configs/config.yaml
|
||||
depends_on:
|
||||
- postgres
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: file_system
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
@ -1,124 +0,0 @@
|
||||
# API 授权使用指南
|
||||
|
||||
## 概述
|
||||
所有 API 接口现在都需要通过授权验证才能访问。每个请求都必须在请求头中包含有效的 API 密钥。
|
||||
|
||||
## API 密钥
|
||||
- **密钥值**: `xn001624`
|
||||
- **请求头名称**: `X-API-Key`
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 示例 1: 使用 cURL
|
||||
```bash
|
||||
# 上传文件
|
||||
curl -X POST http://localhost:8080/files/upload \
|
||||
-H "X-API-Key: xn001624" \
|
||||
-F "file=@/path/to/your/file.txt" \
|
||||
-F "bucket=my-bucket" \
|
||||
-F "path=/uploads/"
|
||||
|
||||
# 列出存储桶
|
||||
curl -X GET http://localhost:8080/buckets \
|
||||
-H "X-API-Key: xn001624"
|
||||
|
||||
# 下载文件
|
||||
curl -X GET "http://localhost:8080/files/download?bucket=my-bucket&path=/uploads/file.txt" \
|
||||
-H "X-API-Key: xn001624" \
|
||||
-o downloaded_file.txt
|
||||
```
|
||||
|
||||
### 示例 2: 使用 JavaScript (Fetch)
|
||||
```javascript
|
||||
const response = await fetch('http://localhost:8080/buckets', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-API-Key': 'xn001624',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
```
|
||||
|
||||
### 示例 3: 使用 Python (requests)
|
||||
```python
|
||||
import requests
|
||||
|
||||
headers = {
|
||||
'X-API-Key': 'xn001624'
|
||||
}
|
||||
|
||||
# 列出存储桶
|
||||
response = requests.get(
|
||||
'http://localhost:8080/buckets',
|
||||
headers=headers
|
||||
)
|
||||
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
### 示例 4: 使用 Postman
|
||||
1. 打开 Postman
|
||||
2. 在 Headers 选项卡中添加新的 key-value 对:
|
||||
- Key: `X-API-Key`
|
||||
- Value: `xn001624`
|
||||
3. 发送请求
|
||||
|
||||
## 错误响应
|
||||
|
||||
如果未提供 API 密钥或密钥无效,将返回以下错误:
|
||||
|
||||
**状态码**: `401 Unauthorized`
|
||||
|
||||
**响应体**:
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "未授权:请在请求头中提供有效的API密钥",
|
||||
"error": "Missing or invalid API key"
|
||||
}
|
||||
```
|
||||
|
||||
## 授权范围
|
||||
|
||||
以下所有 API 接口都需要授权:
|
||||
|
||||
### 文件操作
|
||||
- `POST /files/upload` - 上传文件
|
||||
- `GET /files/download` - 下载文件
|
||||
- `GET /files/list` - 列出文件
|
||||
- `GET /files/preview` - 获取文件预览URL
|
||||
- `DELETE /files/delete` - 删除文件
|
||||
|
||||
### 分片上传
|
||||
- `POST /files/multipart/init` - 初始化分片上传
|
||||
- `PUT /files/multipart/part` - 上传分片
|
||||
- `POST /files/multipart/complete` - 完成分片上传
|
||||
|
||||
### 存储桶操作
|
||||
- `POST /buckets` - 创建存储桶
|
||||
- `GET /buckets` - 列出存储桶
|
||||
- `DELETE /buckets` - 删除存储桶
|
||||
|
||||
### 无需授权的接口
|
||||
以下接口不需要授权:
|
||||
- `GET /swagger/*` - Swagger 文档
|
||||
- `GET /web/*` - Web UI
|
||||
- `GET /` - 根路径重定向
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **不要在客户端代码中硬编码 API 密钥**
|
||||
2. **使用环境变量存储 API 密钥**
|
||||
3. **定期更换 API 密钥**
|
||||
4. **在生产环境中使用 HTTPS**
|
||||
5. **实施速率限制以防止滥用**
|
||||
|
||||
## 配置
|
||||
|
||||
如需修改 API 密钥,请编辑文件:
|
||||
`internal/middleware/auth.go`
|
||||
|
||||
修改 `API_KEY_VALUE` 常量的值即可。
|
||||
@ -1,174 +0,0 @@
|
||||
# Web 登录功能说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
Web UI 现已添加密钥登录功能,用户必须先登录才能访问文件管理系统。
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 1. 访问登录页面
|
||||
|
||||
打开浏览器访问:
|
||||
```
|
||||
http://localhost:8080/web/login.html
|
||||
```
|
||||
|
||||
### 2. 输入 API 密钥
|
||||
|
||||
在登录页面输入 API 密钥:
|
||||
```
|
||||
xn001624.
|
||||
```
|
||||
|
||||
### 3. 登录成功
|
||||
|
||||
登录成功后,系统会:
|
||||
- 将密钥保存到浏览器的 localStorage
|
||||
- 自动跳转到主页面 `/web/`
|
||||
- 所有后续 API 请求都会自动携带密钥
|
||||
|
||||
### 4. 退出登录
|
||||
|
||||
点击右上角的"退出"按钮可以:
|
||||
- 清除本地存储的密钥
|
||||
- 跳转回登录页面
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 后端 (遵循 CQRS 模式)
|
||||
|
||||
1. **登录查询** (`internal/api/handlers/auth_handlers.go`)
|
||||
```go
|
||||
type LoginQuery struct {
|
||||
APIKey string `json:"api_key" binding:"required"`
|
||||
}
|
||||
```
|
||||
|
||||
2. **登录处理器**
|
||||
```go
|
||||
type LoginHandler struct {
|
||||
apiKey string
|
||||
}
|
||||
|
||||
func (h *LoginHandler) Handle(ctx context.Context, query LoginQuery) (LoginResult, error)
|
||||
```
|
||||
|
||||
3. **认证端点** (`internal/api/endpoints/auth_endpoints.go`)
|
||||
```go
|
||||
func (e *AuthEndpoint) Login(c *gin.Context)
|
||||
```
|
||||
|
||||
4. **路由配置** (`cmd/server/main.go`)
|
||||
- `/auth/login` - 公开接口,无需授权
|
||||
- 所有其他 API 接口都需要 `X-API-Key` 请求头
|
||||
|
||||
### 前端
|
||||
|
||||
1. **登录页面** (`web/login.html`)
|
||||
- 美观的渐变背景登录界面
|
||||
- 密钥输入框(密码类型)
|
||||
- 表单验证和错误提示
|
||||
- 登录后自动跳转
|
||||
|
||||
2. **主页面** (`web/index.html`)
|
||||
- 检查 localStorage 中的 token
|
||||
- 未登录显示提示信息
|
||||
- 已登录正常显示文件管理界面
|
||||
- 所有 API 请求自动携带 `X-API-Key` 头
|
||||
- 401 错误自动跳转回登录页
|
||||
|
||||
## 安全特性
|
||||
|
||||
1. **前端存储**: 使用 localStorage 存储密钥(适合单用户场景)
|
||||
2. **自动过期**: 当 API 返回 401 时自动清除密钥并跳转登录
|
||||
3. **密钥保护**: 密钥输入框使用 password 类型,屏幕上不可见
|
||||
4. **退出功能**: 支持主动退出登录
|
||||
|
||||
## 接口说明
|
||||
|
||||
### POST /auth/login
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"api_key": "xn001624."
|
||||
}
|
||||
```
|
||||
|
||||
**成功响应** (200):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "登录成功",
|
||||
"token": "xn001624."
|
||||
}
|
||||
```
|
||||
|
||||
**失败响应** (200):
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "API密钥无效"
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 使用 cURL 测试登录接口
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"api_key": "xn001624."}'
|
||||
```
|
||||
|
||||
### 前端 API 调用示例
|
||||
|
||||
登录后,所有 API 请求会自动携带密钥:
|
||||
|
||||
```javascript
|
||||
// 登录
|
||||
const res = await axios.post('/auth/login', {
|
||||
api_key: 'xn001624.'
|
||||
});
|
||||
|
||||
// 保存 token
|
||||
localStorage.setItem('rustfs_token', res.data.token);
|
||||
|
||||
// 后续请求自动携带密钥
|
||||
const api = axios.create({
|
||||
baseURL: window.location.origin,
|
||||
headers: {
|
||||
'X-API-Key': localStorage.getItem('rustfs_token')
|
||||
}
|
||||
});
|
||||
|
||||
// 调用 API
|
||||
const buckets = await api.get('/buckets');
|
||||
```
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
internal/
|
||||
├── api/
|
||||
│ ├── endpoints/
|
||||
│ │ └── auth_endpoints.go # 认证端点
|
||||
│ └── handlers/
|
||||
│ └── auth_handlers.go # 登录处理器
|
||||
└── middleware/
|
||||
└── auth.go # API 授权中间件
|
||||
|
||||
web/
|
||||
├── login.html # 登录页面
|
||||
└── index.html # 主页面(含登录检查)
|
||||
```
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.2
|
||||
- ✨ 添加 Web 登录功能
|
||||
- ✨ 创建登录页面 UI
|
||||
- ✨ 实现 CQRS 模式的登录验证
|
||||
- 🔒 所有 Web 操作需要先登录
|
||||
- 📝 添加登录文档
|
||||
776
docs/docs.go
776
docs/docs.go
@ -1,776 +0,0 @@
|
||||
// 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/file-system_internal_api_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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "删除指定的 S3 存储桶(桶必须为空)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"存储桶管理"
|
||||
],
|
||||
"summary": "删除存储桶",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "删除存储桶请求参数",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/file-system_internal_api_requests.DeleteBucketRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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/delete": {
|
||||
"delete": {
|
||||
"description": "从指定的存储桶删除文件",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"文件操作"
|
||||
],
|
||||
"summary": "删除文件",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "请求参数",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/file-system_internal_api_requests.DeleteFileRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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/file-system_internal_domain_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/file-system_internal_api_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/file-system_internal_api_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": {
|
||||
"file-system_internal_api_requests.CompleteMultipartRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"object_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"parts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/file-system_internal_common.Part"
|
||||
}
|
||||
},
|
||||
"upload_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"file-system_internal_api_requests.CreateBucketRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"file-system_internal_api_requests.DeleteBucketRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"file-system_internal_api_requests.DeleteFileRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"object_key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"file-system_internal_api_requests.InitMultipartRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"object_key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"file-system_internal_common.Part": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"etag": {
|
||||
"type": "string"
|
||||
},
|
||||
"partNumber": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
},
|
||||
"file-system_internal_domain_repository.FileInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"etag": {
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastModified": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"file-system_internal_domain_repository.ListFilesResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/file-system_internal_domain_repository.FileInfo"
|
||||
}
|
||||
},
|
||||
"nextContinuationToken": {
|
||||
"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)
|
||||
}
|
||||
@ -1,752 +0,0 @@
|
||||
{
|
||||
"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/file-system_internal_api_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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "删除指定的 S3 存储桶(桶必须为空)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"存储桶管理"
|
||||
],
|
||||
"summary": "删除存储桶",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "删除存储桶请求参数",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/file-system_internal_api_requests.DeleteBucketRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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/delete": {
|
||||
"delete": {
|
||||
"description": "从指定的存储桶删除文件",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"文件操作"
|
||||
],
|
||||
"summary": "删除文件",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "请求参数",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/file-system_internal_api_requests.DeleteFileRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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/file-system_internal_domain_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/file-system_internal_api_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/file-system_internal_api_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": {
|
||||
"file-system_internal_api_requests.CompleteMultipartRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"object_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"parts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/file-system_internal_common.Part"
|
||||
}
|
||||
},
|
||||
"upload_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"file-system_internal_api_requests.CreateBucketRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"file-system_internal_api_requests.DeleteBucketRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"file-system_internal_api_requests.DeleteFileRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"object_key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"file-system_internal_api_requests.InitMultipartRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"object_key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"file-system_internal_common.Part": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"etag": {
|
||||
"type": "string"
|
||||
},
|
||||
"partNumber": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
},
|
||||
"file-system_internal_domain_repository.FileInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"etag": {
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastModified": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"file-system_internal_domain_repository.ListFilesResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/file-system_internal_domain_repository.FileInfo"
|
||||
}
|
||||
},
|
||||
"nextContinuationToken": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,497 +0,0 @@
|
||||
basePath: /
|
||||
definitions:
|
||||
file-system_internal_api_requests.CompleteMultipartRequest:
|
||||
properties:
|
||||
bucket_name:
|
||||
type: string
|
||||
object_key:
|
||||
type: string
|
||||
parts:
|
||||
items:
|
||||
$ref: '#/definitions/file-system_internal_common.Part'
|
||||
type: array
|
||||
upload_id:
|
||||
type: string
|
||||
type: object
|
||||
file-system_internal_api_requests.CreateBucketRequest:
|
||||
properties:
|
||||
bucket_name:
|
||||
type: string
|
||||
type: object
|
||||
file-system_internal_api_requests.DeleteBucketRequest:
|
||||
properties:
|
||||
bucket_name:
|
||||
type: string
|
||||
type: object
|
||||
file-system_internal_api_requests.DeleteFileRequest:
|
||||
properties:
|
||||
bucket_name:
|
||||
type: string
|
||||
object_key:
|
||||
type: string
|
||||
type: object
|
||||
file-system_internal_api_requests.InitMultipartRequest:
|
||||
properties:
|
||||
bucket_name:
|
||||
type: string
|
||||
object_key:
|
||||
type: string
|
||||
type: object
|
||||
file-system_internal_common.Part:
|
||||
properties:
|
||||
etag:
|
||||
type: string
|
||||
partNumber:
|
||||
format: int32
|
||||
type: integer
|
||||
type: object
|
||||
file-system_internal_domain_repository.FileInfo:
|
||||
properties:
|
||||
etag:
|
||||
type: string
|
||||
key:
|
||||
type: string
|
||||
lastModified:
|
||||
type: string
|
||||
size:
|
||||
format: int64
|
||||
type: integer
|
||||
type: object
|
||||
file-system_internal_domain_repository.ListFilesResult:
|
||||
properties:
|
||||
files:
|
||||
items:
|
||||
$ref: '#/definitions/file-system_internal_domain_repository.FileInfo'
|
||||
type: array
|
||||
nextContinuationToken:
|
||||
type: string
|
||||
type: object
|
||||
host: localhost:8080
|
||||
info:
|
||||
contact: {}
|
||||
description: RustFS 文件存储系统 API,支持分片上传、文件预览、分页查询等高级功能。
|
||||
title: RustFS File System API
|
||||
version: "1.1"
|
||||
paths:
|
||||
/buckets:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 删除指定的 S3 存储桶(桶必须为空)
|
||||
parameters:
|
||||
- description: 删除存储桶请求参数
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/file-system_internal_api_requests.DeleteBucketRequest'
|
||||
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:
|
||||
- 存储桶管理
|
||||
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/file-system_internal_api_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/delete:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 从指定的存储桶删除文件
|
||||
parameters:
|
||||
- description: 请求参数
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/file-system_internal_api_requests.DeleteFileRequest'
|
||||
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/file-system_internal_domain_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/file-system_internal_api_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/file-system_internal_api_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"
|
||||
83
go.mod
83
go.mod
@ -3,34 +3,25 @@ module rag/file-system
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/ThreeDotsLabs/watermill v1.5.2
|
||||
github.com/ThreeDotsLabs/watermill-sql/v2 v2.0.0
|
||||
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-contrib/cors v1.7.7
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/swaggo/swag v1.16.3
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.68.0
|
||||
go.opentelemetry.io/otel v1.43.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0
|
||||
go.opentelemetry.io/otel/log v0.19.0
|
||||
go.opentelemetry.io/otel/sdk v1.43.0
|
||||
go.opentelemetry.io/otel/sdk/log v0.19.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0
|
||||
golang.org/x/time v0.15.0
|
||||
github.com/go-kratos/kratos/v2 v2.9.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/google/wire v0.7.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9
|
||||
google.golang.org/grpc v1.81.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
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
|
||||
dario.cat/mergo v1.0.0 // 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
|
||||
@ -46,58 +37,32 @@ require (
|
||||
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/gopkg v0.1.4 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gin-contrib/sse v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-kratos/aegis v0.2.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // 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.30.2 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
github.com/go-playground/assert/v2 v2.2.0 // indirect
|
||||
github.com/go-playground/form/v4 v4.2.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.9.2 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // 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-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
golang.org/x/arch v0.25.0 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
254
go.sum
254
go.sum
@ -1,9 +1,11 @@
|
||||
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=
|
||||
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/ThreeDotsLabs/watermill v1.5.2 h1:0ES33Eq1jEsP/pWvtE4n8bE0bs+9Jq7boT7wGBCVY6Q=
|
||||
github.com/ThreeDotsLabs/watermill v1.5.2/go.mod h1:i9/968UriGphWfEbfMuYSD1qFbYRjb0mE0r+rV0FPp4=
|
||||
github.com/ThreeDotsLabs/watermill-sql/v2 v2.0.0 h1:wswlLYY0Jc0tloj3lty4Y+VTEA8AM1vYfrIDwWtqyJk=
|
||||
github.com/ThreeDotsLabs/watermill-sql/v2 v2.0.0/go.mod h1:83l/4sKaLHwoHJlrAsDLaXcHN+QOHHntAAyabNmiuO4=
|
||||
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=
|
||||
@ -42,229 +44,161 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX
|
||||
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/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
|
||||
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
|
||||
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.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/cors v1.7.7 h1:Oh9joP463x7Mw72vhvJ61YQm8ODh9b04YR7vsOErD0Q=
|
||||
github.com/gin-contrib/cors v1.7.7/go.mod h1:K5tW0RkzJtWSiOdikXloy8VEZlgdVNpHNw8FpjUPNrE=
|
||||
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.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
|
||||
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/go-kratos/aegis v0.2.0 h1:dObzCDWn3XVjUkgxyBp6ZeWtx/do0DPZ7LY3yNSJLUQ=
|
||||
github.com/go-kratos/aegis v0.2.0/go.mod h1:v0R2m73WgEEYB3XYu6aE2WcMwsZkJ/Rzuf5eVccm7bI=
|
||||
github.com/go-kratos/kratos/v2 v2.9.2 h1:px8GJQBeLpquDKQWQ9zohEWiLA8n4D/pv7aH3asvUvo=
|
||||
github.com/go-kratos/kratos/v2 v2.9.2/go.mod h1:Jc7jaeYd4RAPjetun2C+oFAOO7HNMHTT/Z4LxpuEDJM=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
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.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
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.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
|
||||
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/go-playground/form/v4 v4.2.0 h1:N1wh+Goz61e6w66vo8vJkQt+uwZSoLz50kZPJWR8eic=
|
||||
github.com/go-playground/form/v4 v4.2.0/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIhMHjgvJiGo6K7U=
|
||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
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/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/pgconn v1.6.4 h1:S7T6cx5o2OqmxdHaXLH1ZeD1SbI8jBznyYE9Ec0RCQ8=
|
||||
github.com/jackc/pgconn v1.6.4/go.mod h1:w2pne1C2tZgP+TvjqLpOigGzNqjBgQW9dUw/4Chex78=
|
||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgproto3/v2 v2.0.2 h1:q1Hsy66zh4vuNsajBUF2PNqfAMMfxU5mk594lPE9vjY=
|
||||
github.com/jackc/pgproto3/v2 v2.0.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgtype v1.4.2 h1:t+6LWm5eWPLX1H5Se702JSBcirq6uWa4jiG4wV1rAWY=
|
||||
github.com/jackc/pgtype v1.4.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
|
||||
github.com/jackc/pgx/v4 v4.8.1 h1:SUbCLP2pXvf/Sr/25KsuI4aTxiFYIvpfk4l6aTSdyCw=
|
||||
github.com/jackc/pgx/v4 v4.8.1/go.mod h1:4HOLxrl8wToZJReD04/yB20GDwf4KBYETvlHciCnwW0=
|
||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
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/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
|
||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
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.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
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.0/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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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.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.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.68.0 h1:5FXSL2s6afUC1bzNzl1iedZZ8yqR7GOhbCoEXtyeK6Q=
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.68.0/go.mod h1:MdHW7tLtkeGJnR4TyOrnd5D0zUGZQB1l84uHCe8hRpE=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.43.0 h1:CETqV3QLLPTy5yNrqyMr41VnAOOD4lsRved7n4QG00A=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.43.0/go.mod h1:Q4mCiCdziYzpNR0g+6UqVotAlCDZdzz6L8jwY4knOrw=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU=
|
||||
go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
|
||||
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
|
||||
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
|
||||
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||
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.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
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.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
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.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
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.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
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/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
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.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
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.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||
@ -274,14 +208,12 @@ google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zN
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
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=
|
||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
|
||||
@ -1,48 +0,0 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"rag/file-system/internal/api/handlers"
|
||||
"rag/file-system/internal/infrastructure/mediator"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AuthEndpoint handles authentication endpoints
|
||||
type AuthEndpoint struct {
|
||||
mediator *mediator.Mediator
|
||||
}
|
||||
|
||||
// NewAuthEndpoint creates a new auth endpoint
|
||||
func NewAuthEndpoint(m *mediator.Mediator) *AuthEndpoint {
|
||||
return &AuthEndpoint{
|
||||
mediator: m,
|
||||
}
|
||||
}
|
||||
|
||||
// Login authenticates a user with API key
|
||||
// @Summary User login
|
||||
// @Description Authenticate with API key
|
||||
// @Tags Authentication
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body handlers.LoginQuery true "Login credentials"
|
||||
// @Success 200 {object} handlers.LoginResult
|
||||
// @Failure 400 {object} map[string]string "Invalid request"
|
||||
// @Failure 500 {object} map[string]string "Internal error"
|
||||
// @Router /auth/login [post]
|
||||
func (e *AuthEndpoint) Login(c *gin.Context) {
|
||||
var query handlers.LoginQuery
|
||||
if err := c.ShouldBindJSON(&query); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request parameters"})
|
||||
return
|
||||
}
|
||||
|
||||
result, err := mediator.Send[handlers.LoginQuery, handlers.LoginResult](e.mediator, c.Request.Context(), query)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "authentication failed"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
@ -1,113 +0,0 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"rag/file-system/internal/api/handlers"
|
||||
"rag/file-system/internal/api/requests"
|
||||
"rag/file-system/internal/api/validators"
|
||||
"rag/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 Create bucket
|
||||
// @Description Create a new S3 bucket
|
||||
// @Tags Bucket Management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param request body requests.CreateBucketRequest true "Bucket creation parameters"
|
||||
// @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 {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": result})
|
||||
}
|
||||
|
||||
// ListBuckets godoc
|
||||
// @Summary List buckets
|
||||
// @Description List all available S3 buckets
|
||||
// @Tags Bucket Management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @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 {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"buckets": result})
|
||||
}
|
||||
|
||||
// DeleteBucket godoc
|
||||
// @Summary Delete bucket
|
||||
// @Description Delete an S3 bucket (must be empty)
|
||||
// @Tags Bucket Management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param request body requests.DeleteBucketRequest true "Bucket deletion parameters"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /buckets [delete]
|
||||
func (e *BucketEndpoint) DeleteBucket(c *gin.Context) {
|
||||
var req requests.DeleteBucketRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := e.CreateBucketValidator.ValidateDelete(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
cmd := handlers.DeleteBucketCommand{BucketName: req.BucketName}
|
||||
|
||||
result, err := mediator.Send[handlers.DeleteBucketCommand, string](e.Mediator, c.Request.Context(), cmd)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": result})
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"rag/file-system/internal/common"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func handleError(c *gin.Context, err error) {
|
||||
if be, ok := err.(*common.BusinessException); ok {
|
||||
c.JSON(be.Code, gin.H{"error": be.Message})
|
||||
} else {
|
||||
common.Logger.Error("unhandled error",
|
||||
"error", err,
|
||||
"path", c.Request.URL.Path,
|
||||
"request_id", c.GetString("request_id"),
|
||||
)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
|
||||
}
|
||||
}
|
||||
@ -1,442 +0,0 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"rag/file-system/internal/api/handlers"
|
||||
"rag/file-system/internal/api/requests"
|
||||
"rag/file-system/internal/api/validators"
|
||||
"rag/file-system/internal/common"
|
||||
"rag/file-system/internal/domain/repository"
|
||||
"rag/file-system/internal/infrastructure/mediator"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type FileEndpoint struct {
|
||||
Mediator *mediator.Mediator
|
||||
FileValidator *validators.FileValidator
|
||||
}
|
||||
|
||||
func NewFileEndpoint(m *mediator.Mediator, fv *validators.FileValidator) *FileEndpoint {
|
||||
return &FileEndpoint{
|
||||
Mediator: m,
|
||||
FileValidator: fv,
|
||||
}
|
||||
}
|
||||
|
||||
// UploadFile godoc
|
||||
// @Summary Upload file
|
||||
// @Description Upload a small file to the specified bucket
|
||||
// @Tags Files
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param bucket_name formData string true "Bucket name"
|
||||
// @Param file formData file true "File to upload"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {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.FileValidator.ValidateUpload(&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()
|
||||
|
||||
cmd := handlers.UploadFileCommand{
|
||||
BucketName: req.BucketName,
|
||||
FileName: req.File.Filename,
|
||||
Data: file,
|
||||
}
|
||||
|
||||
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 Download file
|
||||
// @Description Download a file from the specified bucket
|
||||
// @Tags Files
|
||||
// @Accept json
|
||||
// @Produce octet-stream
|
||||
// @Security ApiKeyAuth
|
||||
// @Param bucket_name query string true "Bucket name"
|
||||
// @Param object_key query string true "Object key"
|
||||
// @Success 200 {file} file
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {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.FileValidator.ValidateDownload(&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", fmt.Sprintf(`attachment; filename="%s"`, common.SanitizeFilename(req.ObjectKey)))
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
io.Copy(c.Writer, result)
|
||||
}
|
||||
|
||||
// ListFiles godoc
|
||||
// @Summary List files (paginated)
|
||||
// @Description List files in a bucket with pagination and prefix filtering
|
||||
// @Tags Files
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param bucket_name query string true "Bucket name"
|
||||
// @Param prefix query string false "File name prefix filter"
|
||||
// @Param max_keys query int false "Items per page (default 20)"
|
||||
// @Param token query string false "Pagination token"
|
||||
// @Success 200 {object} repository.ListFilesResult
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {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.FileValidator.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 Get preview URL
|
||||
// @Description Generate a temporary presigned URL for file preview (24h expiry)
|
||||
// @Tags Files
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param bucket_name query string true "Bucket name"
|
||||
// @Param object_key query string true "Object key"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {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.FileValidator.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})
|
||||
}
|
||||
|
||||
// GetFileContent godoc
|
||||
// @Summary Get file text content
|
||||
// @Description Retrieve text content of a file for preview (e.g., Markdown)
|
||||
// @Tags Files
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param bucket_name query string true "Bucket name"
|
||||
// @Param object_key query string true "Object key"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /files/content [get]
|
||||
func (e *FileEndpoint) GetFileContent(c *gin.Context) {
|
||||
var req requests.GetFileContentRequest
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := e.FileValidator.ValidateGetContent(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
query := handlers.GetFileContentQuery{
|
||||
BucketName: req.BucketName,
|
||||
ObjectKey: req.ObjectKey,
|
||||
}
|
||||
|
||||
result, err := mediator.Send[handlers.GetFileContentQuery, string](e.Mediator, c.Request.Context(), query)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"content": result})
|
||||
}
|
||||
|
||||
// InitMultipart godoc
|
||||
// @Summary Initialize multipart upload
|
||||
// @Description Start a new multipart upload session and return upload_id
|
||||
// @Tags Multipart Upload
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param request body requests.InitMultipartRequest true "Request parameters"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {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.FileValidator.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 Upload a part
|
||||
// @Description Upload a single part of a multipart upload (5MB recommended per part)
|
||||
// @Tags Multipart Upload
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param bucket_name formData string true "Bucket name"
|
||||
// @Param object_key formData string true "Object key"
|
||||
// @Param upload_id formData string true "Upload ID"
|
||||
// @Param part_number formData int true "Part number (starting from 1)"
|
||||
// @Param file formData file true "Part data"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {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.FileValidator.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 Complete multipart upload
|
||||
// @Description Assemble all parts to complete the upload
|
||||
// @Tags Multipart Upload
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param request body requests.CompleteMultipartRequest true "Request parameters"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {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.FileValidator.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})
|
||||
}
|
||||
|
||||
// AbortMultipart godoc
|
||||
// @Summary Abort multipart upload
|
||||
// @Description Cancel an in-progress multipart upload
|
||||
// @Tags Multipart Upload
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param request body requests.AbortMultipartRequest true "Request parameters"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /files/multipart/abort [post]
|
||||
func (e *FileEndpoint) AbortMultipart(c *gin.Context) {
|
||||
var req requests.AbortMultipartRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := e.FileValidator.ValidateAbortMultipart(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
cmd := handlers.AbortMultipartCommand{
|
||||
BucketName: req.BucketName,
|
||||
ObjectKey: req.ObjectKey,
|
||||
UploadId: req.UploadId,
|
||||
}
|
||||
result, err := mediator.Send[handlers.AbortMultipartCommand, string](e.Mediator, c.Request.Context(), cmd)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": result})
|
||||
}
|
||||
|
||||
// DeleteFile godoc
|
||||
// @Summary Delete file
|
||||
// @Description Delete a file from the specified bucket (irreversible)
|
||||
// @Tags Files
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param request body requests.DeleteFileRequest true "Request parameters"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /files/delete [delete]
|
||||
func (e *FileEndpoint) DeleteFile(c *gin.Context) {
|
||||
var req requests.DeleteFileRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := e.FileValidator.ValidateDeleteFile(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
cmd := handlers.DeleteFileCommand{
|
||||
BucketName: req.BucketName,
|
||||
ObjectKey: req.ObjectKey,
|
||||
}
|
||||
|
||||
result, err := mediator.Send[handlers.DeleteFileCommand, string](e.Mediator, c.Request.Context(), cmd)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": result})
|
||||
}
|
||||
@ -1,162 +0,0 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"rag/file-system/internal/api/handlers"
|
||||
"rag/file-system/internal/api/requests"
|
||||
"rag/file-system/internal/api/validators"
|
||||
"rag/file-system/internal/domain/model"
|
||||
"rag/file-system/internal/infrastructure/mediator"
|
||||
"rag/file-system/internal/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type FolderEndpoint struct {
|
||||
Mediator *mediator.Mediator
|
||||
FolderValidator *validators.FolderValidator
|
||||
}
|
||||
|
||||
func NewFolderEndpoint(m *mediator.Mediator, fv *validators.FolderValidator) *FolderEndpoint {
|
||||
return &FolderEndpoint{Mediator: m, FolderValidator: fv}
|
||||
}
|
||||
|
||||
func (e *FolderEndpoint) CreateFolder(c *gin.Context) {
|
||||
var req requests.CreateFolderRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := e.FolderValidator.ValidateCreate(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ownerID := c.GetString(middleware.ContextKeyUserID)
|
||||
cmd := handlers.CreateFolderCommand{
|
||||
Name: req.Name,
|
||||
ParentID: req.ParentID,
|
||||
OwnerID: ownerID,
|
||||
}
|
||||
|
||||
result, err := mediator.Send[handlers.CreateFolderCommand, *model.Folder](e.Mediator, c.Request.Context(), cmd)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": result})
|
||||
}
|
||||
|
||||
func (e *FolderEndpoint) GetFolder(c *gin.Context) {
|
||||
folderID := c.Param("id")
|
||||
ownerID := c.GetString(middleware.ContextKeyUserID)
|
||||
|
||||
query := handlers.GetFolderQuery{FolderID: folderID, OwnerID: ownerID}
|
||||
result, err := mediator.Send[handlers.GetFolderQuery, *model.FolderWithChildren](e.Mediator, c.Request.Context(), query)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": result})
|
||||
}
|
||||
|
||||
func (e *FolderEndpoint) GetFolderTree(c *gin.Context) {
|
||||
ownerID := c.GetString(middleware.ContextKeyUserID)
|
||||
|
||||
query := handlers.GetFolderTreeQuery{OwnerID: ownerID}
|
||||
result, err := mediator.Send[handlers.GetFolderTreeQuery, []model.Folder](e.Mediator, c.Request.Context(), query)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": result})
|
||||
}
|
||||
|
||||
func (e *FolderEndpoint) RenameFolder(c *gin.Context) {
|
||||
folderID := c.Param("id")
|
||||
var req requests.RenameFolderRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := e.FolderValidator.ValidateRename(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ownerID := c.GetString(middleware.ContextKeyUserID)
|
||||
cmd := handlers.RenameFolderCommand{FolderID: folderID, Name: req.Name, OwnerID: ownerID}
|
||||
|
||||
result, err := mediator.Send[handlers.RenameFolderCommand, *model.Folder](e.Mediator, c.Request.Context(), cmd)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": result})
|
||||
}
|
||||
|
||||
func (e *FolderEndpoint) DeleteFolder(c *gin.Context) {
|
||||
folderID := c.Param("id")
|
||||
ownerID := c.GetString(middleware.ContextKeyUserID)
|
||||
|
||||
cmd := handlers.DeleteFolderCommand{FolderID: folderID, OwnerID: ownerID}
|
||||
result, err := mediator.Send[handlers.DeleteFolderCommand, string](e.Mediator, c.Request.Context(), cmd)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": result})
|
||||
}
|
||||
|
||||
func (e *FolderEndpoint) UploadToFolder(c *gin.Context) {
|
||||
folderID := c.Param("folderId")
|
||||
ownerID := c.GetString(middleware.ContextKeyUserID)
|
||||
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择文件"})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
s3Bucket := c.DefaultPostForm("bucket", "default")
|
||||
|
||||
cmd := handlers.UploadToFolderCommand{
|
||||
FolderID: folderID,
|
||||
FileName: header.Filename,
|
||||
Data: file,
|
||||
S3Bucket: s3Bucket,
|
||||
OwnerID: ownerID,
|
||||
}
|
||||
|
||||
result, err := mediator.Send[handlers.UploadToFolderCommand, *model.FileMeta](e.Mediator, c.Request.Context(), cmd)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": result})
|
||||
}
|
||||
|
||||
func (e *FolderEndpoint) MoveFile(c *gin.Context) {
|
||||
fileID := c.Param("id")
|
||||
var req requests.MoveFileRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ownerID := c.GetString(middleware.ContextKeyUserID)
|
||||
cmd := handlers.MoveFileCommand{
|
||||
FileID: fileID,
|
||||
TargetFolderID: req.TargetFolderID,
|
||||
OwnerID: ownerID,
|
||||
}
|
||||
|
||||
result, err := mediator.Send[handlers.MoveFileCommand, string](e.Mediator, c.Request.Context(), cmd)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": result})
|
||||
}
|
||||
@ -1,103 +0,0 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"rag/file-system/internal/api/handlers"
|
||||
"rag/file-system/internal/api/requests"
|
||||
"rag/file-system/internal/api/validators"
|
||||
"rag/file-system/internal/domain/model"
|
||||
"rag/file-system/internal/infrastructure/mediator"
|
||||
"rag/file-system/internal/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ShareEndpoint struct {
|
||||
Mediator *mediator.Mediator
|
||||
ShareValidator *validators.ShareValidator
|
||||
}
|
||||
|
||||
func NewShareEndpoint(m *mediator.Mediator, sv *validators.ShareValidator) *ShareEndpoint {
|
||||
return &ShareEndpoint{Mediator: m, ShareValidator: sv}
|
||||
}
|
||||
|
||||
func (e *ShareEndpoint) CreateShare(c *gin.Context) {
|
||||
var req requests.CreateShareRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := e.ShareValidator.ValidateCreate(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ownerID := c.GetString(middleware.ContextKeyUserID)
|
||||
|
||||
var expiresAt *time.Time
|
||||
if req.ExpiresAt != nil && *req.ExpiresAt != "" {
|
||||
t, err := time.Parse(time.RFC3339, *req.ExpiresAt)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "过期时间格式错误,请使用 ISO 8601 格式"})
|
||||
return
|
||||
}
|
||||
expiresAt = &t
|
||||
}
|
||||
|
||||
cmd := handlers.CreateShareCommand{
|
||||
ResourceType: req.ResourceType,
|
||||
ResourceID: req.ResourceID,
|
||||
Password: req.Password,
|
||||
ExpiresAt: expiresAt,
|
||||
MaxDownloads: req.MaxDownloads,
|
||||
CreatedBy: ownerID,
|
||||
}
|
||||
|
||||
result, err := mediator.Send[handlers.CreateShareCommand, *model.ShareLink](e.Mediator, c.Request.Context(), cmd)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": result})
|
||||
}
|
||||
|
||||
func (e *ShareEndpoint) GetShareInfo(c *gin.Context) {
|
||||
token := c.Param("token")
|
||||
|
||||
query := handlers.GetShareInfoQuery{Token: token}
|
||||
result, err := mediator.Send[handlers.GetShareInfoQuery, *model.ShareInfo](e.Mediator, c.Request.Context(), query)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": result})
|
||||
}
|
||||
|
||||
func (e *ShareEndpoint) DownloadShare(c *gin.Context) {
|
||||
token := c.Param("token")
|
||||
var req requests.ShareDownloadRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
|
||||
query := handlers.DownloadShareQuery{Token: token, Password: req.Password}
|
||||
result, err := mediator.Send[handlers.DownloadShareQuery, string](e.Mediator, c.Request.Context(), query)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"url": result})
|
||||
}
|
||||
|
||||
func (e *ShareEndpoint) DeleteShare(c *gin.Context) {
|
||||
shareID := c.Param("id")
|
||||
ownerID := c.GetString(middleware.ContextKeyUserID)
|
||||
|
||||
cmd := handlers.DeleteShareCommand{ShareID: shareID, CreatedBy: ownerID}
|
||||
result, err := mediator.Send[handlers.DeleteShareCommand, string](e.Mediator, c.Request.Context(), cmd)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": result})
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// LoginQuery 登录查询
|
||||
type LoginQuery struct {
|
||||
APIKey string `json:"api_key" binding:"required"`
|
||||
}
|
||||
|
||||
// LoginResult 登录结果
|
||||
type LoginResult struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Token string `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
// LoginHandler 登录处理器
|
||||
type LoginHandler struct {
|
||||
apiKey string
|
||||
}
|
||||
|
||||
// NewLoginHandler 创建登录处理器
|
||||
func NewLoginHandler(apiKey string) *LoginHandler {
|
||||
return &LoginHandler{
|
||||
apiKey: apiKey,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle 处理登录查询
|
||||
func (h *LoginHandler) Handle(ctx context.Context, query LoginQuery) (LoginResult, error) {
|
||||
if query.APIKey == h.apiKey {
|
||||
return LoginResult{
|
||||
Success: true,
|
||||
Message: "登录成功",
|
||||
Token: query.APIKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return LoginResult{
|
||||
Success: false,
|
||||
Message: "API密钥无效",
|
||||
}, nil
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
package handlers
|
||||
|
||||
type CreateBucketCommand struct {
|
||||
BucketName string
|
||||
}
|
||||
|
||||
type ListBucketsQuery struct{}
|
||||
|
||||
type DeleteBucketCommand struct {
|
||||
BucketName string
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"rag/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)
|
||||
}
|
||||
|
||||
type DeleteBucketHandler struct {
|
||||
Repo repository.FileRepository
|
||||
}
|
||||
|
||||
func NewDeleteBucketHandler(repo repository.FileRepository) *DeleteBucketHandler {
|
||||
return &DeleteBucketHandler{Repo: repo}
|
||||
}
|
||||
|
||||
func (h *DeleteBucketHandler) Handle(ctx context.Context, cmd DeleteBucketCommand) (string, error) {
|
||||
err := h.Repo.DeleteBucket(ctx, cmd.BucketName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "Bucket deleted successfully", nil
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"rag/file-system/internal/common"
|
||||
)
|
||||
|
||||
type UploadFileCommand struct {
|
||||
BucketName string
|
||||
FileName string
|
||||
Data io.Reader
|
||||
}
|
||||
|
||||
type DeleteFileCommand struct {
|
||||
BucketName string
|
||||
ObjectKey string
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type AbortMultipartCommand struct {
|
||||
BucketName string
|
||||
ObjectKey string
|
||||
UploadId string
|
||||
}
|
||||
@ -1,140 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"rag/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) {
|
||||
err := h.Repo.UploadFile(ctx, cmd.BucketName, cmd.FileName, cmd.Data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "File uploaded successfully", nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
type GetFileContentHandler struct {
|
||||
Repo repository.FileRepository
|
||||
}
|
||||
|
||||
func NewGetFileContentHandler(repo repository.FileRepository) *GetFileContentHandler {
|
||||
return &GetFileContentHandler{Repo: repo}
|
||||
}
|
||||
|
||||
func (h *GetFileContentHandler) Handle(ctx context.Context, q GetFileContentQuery) (string, error) {
|
||||
return h.Repo.GetFileContent(ctx, q.BucketName, q.ObjectKey)
|
||||
}
|
||||
|
||||
type DeleteFileHandler struct {
|
||||
Repo repository.FileRepository
|
||||
}
|
||||
|
||||
func NewDeleteFileHandler(repo repository.FileRepository) *DeleteFileHandler {
|
||||
return &DeleteFileHandler{Repo: repo}
|
||||
}
|
||||
|
||||
func (h *DeleteFileHandler) Handle(ctx context.Context, cmd DeleteFileCommand) (string, error) {
|
||||
err := h.Repo.DeleteFile(ctx, cmd.BucketName, cmd.ObjectKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "File deleted successfully", nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
type AbortMultipartHandler struct {
|
||||
Repo repository.FileRepository
|
||||
}
|
||||
|
||||
func NewAbortMultipartHandler(repo repository.FileRepository) *AbortMultipartHandler {
|
||||
return &AbortMultipartHandler{Repo: repo}
|
||||
}
|
||||
|
||||
func (h *AbortMultipartHandler) Handle(ctx context.Context, cmd AbortMultipartCommand) (string, error) {
|
||||
err := h.Repo.AbortMultipartUpload(ctx, cmd.BucketName, cmd.ObjectKey, cmd.UploadId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "Multipart upload aborted successfully", nil
|
||||
}
|
||||
@ -1,100 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"rag/file-system/internal/common"
|
||||
"rag/file-system/internal/domain/model"
|
||||
"rag/file-system/internal/domain/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UploadToFolderCommand struct {
|
||||
FolderID string
|
||||
FileName string
|
||||
Data io.Reader
|
||||
S3Bucket string
|
||||
OwnerID string
|
||||
}
|
||||
|
||||
type MoveFileCommand struct {
|
||||
FileID string
|
||||
TargetFolderID string
|
||||
OwnerID string
|
||||
}
|
||||
|
||||
type UploadToFolderHandler struct {
|
||||
FileMetaRepo repository.FileMetaRepository
|
||||
FolderRepo repository.FolderRepository
|
||||
S3Repo repository.FileRepository
|
||||
}
|
||||
|
||||
func NewUploadToFolderHandler(
|
||||
fileMetaRepo repository.FileMetaRepository,
|
||||
folderRepo repository.FolderRepository,
|
||||
s3Repo repository.FileRepository,
|
||||
) *UploadToFolderHandler {
|
||||
return &UploadToFolderHandler{
|
||||
FileMetaRepo: fileMetaRepo,
|
||||
FolderRepo: folderRepo,
|
||||
S3Repo: s3Repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *UploadToFolderHandler) Handle(ctx context.Context, cmd UploadToFolderCommand) (*model.FileMeta, error) {
|
||||
folder, err := h.FolderRepo.GetByID(ctx, cmd.FolderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if folder == nil || folder.OwnerID != cmd.OwnerID {
|
||||
return nil, common.NewNotFoundError("目录不存在")
|
||||
}
|
||||
|
||||
s3Key := uuid.New().String()
|
||||
|
||||
if err := h.S3Repo.UploadFile(ctx, cmd.S3Bucket, s3Key, cmd.Data); err != nil {
|
||||
return nil, fmt.Errorf("S3 upload failed: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
fileMeta := &model.FileMeta{
|
||||
ID: uuid.New().String(),
|
||||
FolderID: cmd.FolderID,
|
||||
Name: cmd.FileName,
|
||||
S3Key: s3Key,
|
||||
S3Bucket: cmd.S3Bucket,
|
||||
ContentType: "application/octet-stream",
|
||||
OwnerID: cmd.OwnerID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err := h.FileMetaRepo.Create(ctx, fileMeta); err != nil {
|
||||
if delErr := h.S3Repo.DeleteFile(ctx, cmd.S3Bucket, s3Key); delErr != nil {
|
||||
common.Logger.Error("compensation failed: could not delete S3 object after PG write failure",
|
||||
"s3_key", s3Key, "error", delErr)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to save file metadata: %w", err)
|
||||
}
|
||||
|
||||
return fileMeta, nil
|
||||
}
|
||||
|
||||
type MoveFileHandler struct {
|
||||
FileMetaRepo repository.FileMetaRepository
|
||||
}
|
||||
|
||||
func NewMoveFileHandler(fileMetaRepo repository.FileMetaRepository) *MoveFileHandler {
|
||||
return &MoveFileHandler{FileMetaRepo: fileMetaRepo}
|
||||
}
|
||||
|
||||
func (h *MoveFileHandler) Handle(ctx context.Context, cmd MoveFileCommand) (string, error) {
|
||||
if err := h.FileMetaRepo.Move(ctx, cmd.FileID, cmd.TargetFolderID, cmd.OwnerID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "文件移动成功", nil
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import "time"
|
||||
|
||||
type DownloadFileQuery struct {
|
||||
BucketName string
|
||||
ObjectKey string
|
||||
}
|
||||
|
||||
type ListFilesQuery struct {
|
||||
BucketName string
|
||||
Prefix string
|
||||
MaxKeys int32
|
||||
Token *string
|
||||
}
|
||||
|
||||
type GetFilePreviewQuery struct {
|
||||
BucketName string
|
||||
ObjectKey string
|
||||
Expiry time.Duration
|
||||
}
|
||||
|
||||
type GetFileContentQuery struct {
|
||||
BucketName string
|
||||
ObjectKey string
|
||||
}
|
||||
@ -1,106 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"rag/file-system/internal/common"
|
||||
"rag/file-system/internal/domain/model"
|
||||
"rag/file-system/internal/domain/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type CreateFolderCommand struct {
|
||||
Name string
|
||||
ParentID *string
|
||||
OwnerID string
|
||||
}
|
||||
|
||||
type RenameFolderCommand struct {
|
||||
FolderID string
|
||||
Name string
|
||||
OwnerID string
|
||||
}
|
||||
|
||||
type DeleteFolderCommand struct {
|
||||
FolderID string
|
||||
OwnerID string
|
||||
}
|
||||
|
||||
type CreateFolderHandler struct {
|
||||
FolderRepo repository.FolderRepository
|
||||
}
|
||||
|
||||
func NewCreateFolderHandler(folderRepo repository.FolderRepository) *CreateFolderHandler {
|
||||
return &CreateFolderHandler{FolderRepo: folderRepo}
|
||||
}
|
||||
|
||||
func (h *CreateFolderHandler) Handle(ctx context.Context, cmd CreateFolderCommand) (*model.Folder, error) {
|
||||
now := time.Now()
|
||||
folder := &model.Folder{
|
||||
ID: uuid.New().String(),
|
||||
ParentID: cmd.ParentID,
|
||||
Name: cmd.Name,
|
||||
OwnerID: cmd.OwnerID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := h.FolderRepo.Create(ctx, folder); err != nil {
|
||||
return nil, fmt.Errorf("failed to create folder: %w", err)
|
||||
}
|
||||
return folder, nil
|
||||
}
|
||||
|
||||
type RenameFolderHandler struct {
|
||||
FolderRepo repository.FolderRepository
|
||||
}
|
||||
|
||||
func NewRenameFolderHandler(folderRepo repository.FolderRepository) *RenameFolderHandler {
|
||||
return &RenameFolderHandler{FolderRepo: folderRepo}
|
||||
}
|
||||
|
||||
func (h *RenameFolderHandler) Handle(ctx context.Context, cmd RenameFolderCommand) (*model.Folder, error) {
|
||||
folder, err := h.FolderRepo.GetByID(ctx, cmd.FolderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if folder == nil || folder.OwnerID != cmd.OwnerID {
|
||||
return nil, common.NewNotFoundError("目录不存在")
|
||||
}
|
||||
folder.Name = cmd.Name
|
||||
folder.UpdatedAt = time.Now()
|
||||
if err := h.FolderRepo.Update(ctx, folder); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return folder, nil
|
||||
}
|
||||
|
||||
type DeleteFolderHandler struct {
|
||||
FolderRepo repository.FolderRepository
|
||||
S3Repo repository.FileRepository
|
||||
}
|
||||
|
||||
func NewDeleteFolderHandler(folderRepo repository.FolderRepository, s3Repo repository.FileRepository) *DeleteFolderHandler {
|
||||
return &DeleteFolderHandler{FolderRepo: folderRepo, S3Repo: s3Repo}
|
||||
}
|
||||
|
||||
func (h *DeleteFolderHandler) Handle(ctx context.Context, cmd DeleteFolderCommand) (string, error) {
|
||||
files, err := h.FolderRepo.GetDescendantFileS3Keys(ctx, cmd.FolderID, cmd.OwnerID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
if delErr := h.S3Repo.DeleteFile(ctx, f.S3Bucket, f.S3Key); delErr != nil {
|
||||
common.Logger.Error("failed to delete S3 object during folder cleanup",
|
||||
"s3_key", f.S3Key, "error", delErr)
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.FolderRepo.Delete(ctx, cmd.FolderID, cmd.OwnerID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "目录删除成功", nil
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"rag/file-system/internal/common"
|
||||
"rag/file-system/internal/domain/model"
|
||||
"rag/file-system/internal/domain/repository"
|
||||
)
|
||||
|
||||
type GetFolderQuery struct {
|
||||
FolderID string
|
||||
OwnerID string
|
||||
}
|
||||
|
||||
type GetFolderTreeQuery struct {
|
||||
OwnerID string
|
||||
}
|
||||
|
||||
type GetFolderHandler struct {
|
||||
FolderRepo repository.FolderRepository
|
||||
}
|
||||
|
||||
func NewGetFolderHandler(folderRepo repository.FolderRepository) *GetFolderHandler {
|
||||
return &GetFolderHandler{FolderRepo: folderRepo}
|
||||
}
|
||||
|
||||
func (h *GetFolderHandler) Handle(ctx context.Context, q GetFolderQuery) (*model.FolderWithChildren, error) {
|
||||
result, err := h.FolderRepo.GetWithChildren(ctx, q.FolderID, q.OwnerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil {
|
||||
return nil, common.NewNotFoundError("目录不存在")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type GetFolderTreeHandler struct {
|
||||
FolderRepo repository.FolderRepository
|
||||
}
|
||||
|
||||
func NewGetFolderTreeHandler(folderRepo repository.FolderRepository) *GetFolderTreeHandler {
|
||||
return &GetFolderTreeHandler{FolderRepo: folderRepo}
|
||||
}
|
||||
|
||||
func (h *GetFolderTreeHandler) Handle(ctx context.Context, q GetFolderTreeQuery) ([]model.Folder, error) {
|
||||
return h.FolderRepo.GetTree(ctx, q.OwnerID)
|
||||
}
|
||||
@ -1,84 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"rag/file-system/internal/domain/model"
|
||||
"rag/file-system/internal/domain/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type CreateShareCommand struct {
|
||||
ResourceType string
|
||||
ResourceID string
|
||||
Password *string
|
||||
ExpiresAt *time.Time
|
||||
MaxDownloads *int
|
||||
CreatedBy string
|
||||
}
|
||||
|
||||
type DeleteShareCommand struct {
|
||||
ShareID string
|
||||
CreatedBy string
|
||||
}
|
||||
|
||||
type CreateShareHandler struct {
|
||||
ShareRepo repository.ShareRepository
|
||||
}
|
||||
|
||||
func NewCreateShareHandler(shareRepo repository.ShareRepository) *CreateShareHandler {
|
||||
return &CreateShareHandler{ShareRepo: shareRepo}
|
||||
}
|
||||
|
||||
func (h *CreateShareHandler) Handle(ctx context.Context, cmd CreateShareCommand) (*model.ShareLink, error) {
|
||||
token, err := generateShareToken()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate token: %w", err)
|
||||
}
|
||||
|
||||
share := &model.ShareLink{
|
||||
ID: uuid.New().String(),
|
||||
ResourceType: cmd.ResourceType,
|
||||
ResourceID: cmd.ResourceID,
|
||||
Token: token,
|
||||
Password: cmd.Password,
|
||||
ExpiresAt: cmd.ExpiresAt,
|
||||
DownloadCount: 0,
|
||||
MaxDownloads: cmd.MaxDownloads,
|
||||
CreatedBy: cmd.CreatedBy,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := h.ShareRepo.Create(ctx, share); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return share, nil
|
||||
}
|
||||
|
||||
type DeleteShareHandler struct {
|
||||
ShareRepo repository.ShareRepository
|
||||
}
|
||||
|
||||
func NewDeleteShareHandler(shareRepo repository.ShareRepository) *DeleteShareHandler {
|
||||
return &DeleteShareHandler{ShareRepo: shareRepo}
|
||||
}
|
||||
|
||||
func (h *DeleteShareHandler) Handle(ctx context.Context, cmd DeleteShareCommand) (string, error) {
|
||||
if err := h.ShareRepo.Delete(ctx, cmd.ShareID, cmd.CreatedBy); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "分享已取消", nil
|
||||
}
|
||||
|
||||
func generateShareToken() (string, error) {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
@ -1,135 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"rag/file-system/internal/common"
|
||||
"rag/file-system/internal/domain/model"
|
||||
"rag/file-system/internal/domain/repository"
|
||||
)
|
||||
|
||||
type GetShareInfoQuery struct {
|
||||
Token string
|
||||
}
|
||||
|
||||
type DownloadShareQuery struct {
|
||||
Token string
|
||||
Password string
|
||||
}
|
||||
|
||||
type GetShareInfoHandler struct {
|
||||
ShareRepo repository.ShareRepository
|
||||
FileMetaRepo repository.FileMetaRepository
|
||||
}
|
||||
|
||||
func NewGetShareInfoHandler(shareRepo repository.ShareRepository, fileMetaRepo repository.FileMetaRepository) *GetShareInfoHandler {
|
||||
return &GetShareInfoHandler{ShareRepo: shareRepo, FileMetaRepo: fileMetaRepo}
|
||||
}
|
||||
|
||||
func (h *GetShareInfoHandler) Handle(ctx context.Context, q GetShareInfoQuery) (*model.ShareInfo, error) {
|
||||
share, err := h.ShareRepo.GetByToken(ctx, q.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if share == nil {
|
||||
return nil, common.NewNotFoundError("分享链接不存在")
|
||||
}
|
||||
|
||||
if share.ExpiresAt != nil && share.ExpiresAt.Before(time.Now()) {
|
||||
return nil, common.NewBusinessException("分享链接已过期")
|
||||
}
|
||||
|
||||
if share.MaxDownloads != nil && share.DownloadCount >= *share.MaxDownloads {
|
||||
return nil, common.NewBusinessException("分享链接下载次数已达上限")
|
||||
}
|
||||
|
||||
info := &model.ShareInfo{
|
||||
Token: share.Token,
|
||||
ResourceType: share.ResourceType,
|
||||
HasPassword: share.Password != nil,
|
||||
ExpiresAt: share.ExpiresAt,
|
||||
}
|
||||
|
||||
if share.ResourceType == "file" {
|
||||
file, err := h.FileMetaRepo.GetByID(ctx, share.ResourceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if file != nil {
|
||||
info.FileName = file.Name
|
||||
info.FileSize = file.Size
|
||||
}
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
type DownloadShareHandler struct {
|
||||
ShareRepo repository.ShareRepository
|
||||
FileMetaRepo repository.FileMetaRepository
|
||||
S3Repo repository.FileRepository
|
||||
}
|
||||
|
||||
func NewDownloadShareHandler(
|
||||
shareRepo repository.ShareRepository,
|
||||
fileMetaRepo repository.FileMetaRepository,
|
||||
s3Repo repository.FileRepository,
|
||||
) *DownloadShareHandler {
|
||||
return &DownloadShareHandler{
|
||||
ShareRepo: shareRepo,
|
||||
FileMetaRepo: fileMetaRepo,
|
||||
S3Repo: s3Repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DownloadShareHandler) Handle(ctx context.Context, q DownloadShareQuery) (string, error) {
|
||||
share, err := h.ShareRepo.GetByToken(ctx, q.Token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if share == nil {
|
||||
return "", common.NewNotFoundError("分享链接不存在")
|
||||
}
|
||||
|
||||
if share.ExpiresAt != nil && share.ExpiresAt.Before(time.Now()) {
|
||||
return "", common.NewBusinessException("分享链接已过期")
|
||||
}
|
||||
|
||||
if share.MaxDownloads != nil && share.DownloadCount >= *share.MaxDownloads {
|
||||
return "", common.NewBusinessException("下载次数已达上限")
|
||||
}
|
||||
|
||||
if share.Password != nil && *share.Password != "" {
|
||||
if q.Password == "" {
|
||||
return "", common.NewBusinessException("此分享需要密码")
|
||||
}
|
||||
if q.Password != *share.Password {
|
||||
return "", common.NewBusinessException("密码错误")
|
||||
}
|
||||
}
|
||||
|
||||
if share.ResourceType != "file" {
|
||||
return "", common.NewBusinessException("仅支持文件分享下载")
|
||||
}
|
||||
|
||||
file, err := h.FileMetaRepo.GetByID(ctx, share.ResourceID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if file == nil {
|
||||
return "", common.NewNotFoundError("文件不存在")
|
||||
}
|
||||
|
||||
presignedURL, err := h.S3Repo.GeneratePresignedURL(ctx, file.S3Bucket, file.S3Key, 5*time.Minute)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate download URL: %w", err)
|
||||
}
|
||||
|
||||
if err := h.ShareRepo.IncrementDownloadCount(ctx, q.Token); err != nil {
|
||||
common.Logger.Error("failed to increment download count", "token", q.Token, "error", err)
|
||||
}
|
||||
|
||||
return presignedURL, nil
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
package requests
|
||||
|
||||
type CreateBucketRequest struct {
|
||||
BucketName string `form:"bucket_name" json:"bucket_name"`
|
||||
}
|
||||
|
||||
type ListBucketsRequest struct{}
|
||||
|
||||
type DeleteBucketRequest struct {
|
||||
BucketName string `json:"bucket_name"`
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
|
||||
"rag/file-system/internal/common"
|
||||
)
|
||||
|
||||
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 GetFileContentRequest 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"`
|
||||
}
|
||||
|
||||
type DeleteFileRequest struct {
|
||||
BucketName string `json:"bucket_name"`
|
||||
ObjectKey string `json:"object_key"`
|
||||
}
|
||||
|
||||
type AbortMultipartRequest struct {
|
||||
BucketName string `json:"bucket_name"`
|
||||
ObjectKey string `json:"object_key"`
|
||||
UploadId string `json:"upload_id"`
|
||||
}
|
||||
|
||||
type DownloadFileRequest struct {
|
||||
BucketName string `form:"bucket_name"`
|
||||
ObjectKey string `form:"object_key"`
|
||||
}
|
||||
|
||||
type UploadFileRequest struct {
|
||||
BucketName string `form:"bucket_name"`
|
||||
File *multipart.FileHeader `form:"file"`
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
package requests
|
||||
|
||||
type CreateFolderRequest struct {
|
||||
Name string `json:"name"`
|
||||
ParentID *string `json:"parent_id"`
|
||||
}
|
||||
|
||||
type RenameFolderRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type MoveFileRequest struct {
|
||||
TargetFolderID string `json:"target_folder_id"`
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
package requests
|
||||
|
||||
type CreateShareRequest struct {
|
||||
ResourceType string `json:"resource_type"`
|
||||
ResourceID string `json:"resource_id"`
|
||||
Password *string `json:"password"`
|
||||
ExpiresAt *string `json:"expires_at"`
|
||||
MaxDownloads *int `json:"max_downloads"`
|
||||
}
|
||||
|
||||
type ShareDownloadRequest struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
package validators
|
||||
|
||||
import (
|
||||
"rag/file-system/internal/api/requests"
|
||||
"rag/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")
|
||||
}
|
||||
if err := common.SanitizeBucketName(req.BucketName); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *CreateBucketValidator) ValidateDelete(req *requests.DeleteBucketRequest) error {
|
||||
if req.BucketName == "" {
|
||||
return common.NewBusinessException("Bucket name cannot be empty")
|
||||
}
|
||||
if err := common.SanitizeBucketName(req.BucketName); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
package validators
|
||||
|
||||
import (
|
||||
"rag/file-system/internal/api/requests"
|
||||
"rag/file-system/internal/common"
|
||||
)
|
||||
|
||||
type FileValidator struct{}
|
||||
|
||||
func NewFileValidator() *FileValidator {
|
||||
return &FileValidator{}
|
||||
}
|
||||
|
||||
func (v *FileValidator) ValidateListFiles(req *requests.ListFilesRequest) error {
|
||||
if req.BucketName == "" {
|
||||
return common.NewBusinessException("Bucket name is required")
|
||||
}
|
||||
if err := common.SanitizeBucketName(req.BucketName); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.MaxKeys <= 0 {
|
||||
req.MaxKeys = 20
|
||||
}
|
||||
if req.MaxKeys > 1000 {
|
||||
req.MaxKeys = 1000
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *FileValidator) ValidatePreview(req *requests.GetFilePreviewRequest) error {
|
||||
if req.BucketName == "" || req.ObjectKey == "" {
|
||||
return common.NewBusinessException("Bucket name and Object key are required")
|
||||
}
|
||||
return common.SanitizeObjectKey(req.ObjectKey)
|
||||
}
|
||||
|
||||
func (v *FileValidator) ValidateGetContent(req *requests.GetFileContentRequest) error {
|
||||
if req.BucketName == "" || req.ObjectKey == "" {
|
||||
return common.NewBusinessException("Bucket name and Object key are required")
|
||||
}
|
||||
return common.SanitizeObjectKey(req.ObjectKey)
|
||||
}
|
||||
|
||||
func (v *FileValidator) ValidateInitMultipart(req *requests.InitMultipartRequest) error {
|
||||
if req.BucketName == "" || req.ObjectKey == "" {
|
||||
return common.NewBusinessException("Bucket name and Object key are required")
|
||||
}
|
||||
return common.SanitizeObjectKey(req.ObjectKey)
|
||||
}
|
||||
|
||||
func (v *FileValidator) 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 common.SanitizeObjectKey(req.ObjectKey)
|
||||
}
|
||||
|
||||
func (v *FileValidator) 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 common.SanitizeObjectKey(req.ObjectKey)
|
||||
}
|
||||
|
||||
func (v *FileValidator) ValidateDeleteFile(req *requests.DeleteFileRequest) error {
|
||||
if req.BucketName == "" || req.ObjectKey == "" {
|
||||
return common.NewBusinessException("Bucket name and Object key are required")
|
||||
}
|
||||
return common.SanitizeObjectKey(req.ObjectKey)
|
||||
}
|
||||
|
||||
func (v *FileValidator) ValidateAbortMultipart(req *requests.AbortMultipartRequest) error {
|
||||
if req.BucketName == "" || req.ObjectKey == "" || req.UploadId == "" {
|
||||
return common.NewBusinessException("Bucket name, Object key, and Upload ID are required")
|
||||
}
|
||||
return common.SanitizeObjectKey(req.ObjectKey)
|
||||
}
|
||||
|
||||
func (v *FileValidator) ValidateDownload(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 common.SanitizeObjectKey(req.ObjectKey)
|
||||
}
|
||||
|
||||
func (v *FileValidator) ValidateUpload(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 common.SanitizeObjectKey(req.File.Filename)
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
package validators
|
||||
|
||||
import (
|
||||
"rag/file-system/internal/api/requests"
|
||||
"rag/file-system/internal/common"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type FolderValidator struct{}
|
||||
|
||||
func NewFolderValidator() *FolderValidator {
|
||||
return &FolderValidator{}
|
||||
}
|
||||
|
||||
func (v *FolderValidator) ValidateCreate(req *requests.CreateFolderRequest) error {
|
||||
name := strings.TrimSpace(req.Name)
|
||||
if name == "" {
|
||||
return common.NewBusinessException("目录名称不能为空")
|
||||
}
|
||||
if utf8.RuneCountInString(name) > 255 {
|
||||
return common.NewBusinessException("目录名称不能超过255个字符")
|
||||
}
|
||||
if strings.Contains(name, "/") || strings.Contains(name, "\\") {
|
||||
return common.NewBusinessException("目录名称不能包含 / 或 \\")
|
||||
}
|
||||
req.Name = name
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *FolderValidator) ValidateRename(req *requests.RenameFolderRequest) error {
|
||||
name := strings.TrimSpace(req.Name)
|
||||
if name == "" {
|
||||
return common.NewBusinessException("目录名称不能为空")
|
||||
}
|
||||
if utf8.RuneCountInString(name) > 255 {
|
||||
return common.NewBusinessException("目录名称不能超过255个字符")
|
||||
}
|
||||
if strings.Contains(name, "/") || strings.Contains(name, "\\") {
|
||||
return common.NewBusinessException("目录名称不能包含 / 或 \\")
|
||||
}
|
||||
req.Name = name
|
||||
return nil
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
package validators
|
||||
|
||||
import (
|
||||
"rag/file-system/internal/api/requests"
|
||||
"rag/file-system/internal/common"
|
||||
)
|
||||
|
||||
type ShareValidator struct{}
|
||||
|
||||
func NewShareValidator() *ShareValidator {
|
||||
return &ShareValidator{}
|
||||
}
|
||||
|
||||
func (v *ShareValidator) ValidateCreate(req *requests.CreateShareRequest) error {
|
||||
if req.ResourceType != "file" && req.ResourceType != "folder" {
|
||||
return common.NewBusinessException("resource_type 必须是 file 或 folder")
|
||||
}
|
||||
if req.ResourceID == "" {
|
||||
return common.NewBusinessException("resource_id 不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
10
internal/biz/biz.go
Normal file
10
internal/biz/biz.go
Normal file
@ -0,0 +1,10 @@
|
||||
package biz
|
||||
|
||||
import "github.com/google/wire"
|
||||
|
||||
var ProviderSet = wire.NewSet(
|
||||
NewFileUsecase,
|
||||
NewBucketUsecase,
|
||||
NewFolderUsecase,
|
||||
NewShareUsecase,
|
||||
)
|
||||
36
internal/biz/bucket.go
Normal file
36
internal/biz/bucket.go
Normal file
@ -0,0 +1,36 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
)
|
||||
|
||||
// BucketUsecase wraps FileRepo bucket operations.
|
||||
type BucketUsecase struct {
|
||||
repo FileRepo
|
||||
log *log.Helper
|
||||
}
|
||||
|
||||
// NewBucketUsecase creates a new BucketUsecase.
|
||||
func NewBucketUsecase(repo FileRepo, logger log.Logger) *BucketUsecase {
|
||||
return &BucketUsecase{
|
||||
repo: repo,
|
||||
log: log.NewHelper(logger),
|
||||
}
|
||||
}
|
||||
|
||||
// ListBuckets returns all bucket names.
|
||||
func (uc *BucketUsecase) ListBuckets(ctx context.Context) ([]string, error) {
|
||||
return uc.repo.ListBuckets(ctx)
|
||||
}
|
||||
|
||||
// CreateBucket creates a new S3 bucket.
|
||||
func (uc *BucketUsecase) CreateBucket(ctx context.Context, name string) error {
|
||||
return uc.repo.CreateBucket(ctx, name)
|
||||
}
|
||||
|
||||
// DeleteBucket deletes an S3 bucket.
|
||||
func (uc *BucketUsecase) DeleteBucket(ctx context.Context, name string) error {
|
||||
return uc.repo.DeleteBucket(ctx, name)
|
||||
}
|
||||
8
internal/biz/event.go
Normal file
8
internal/biz/event.go
Normal file
@ -0,0 +1,8 @@
|
||||
package biz
|
||||
|
||||
import "context"
|
||||
|
||||
// EventPublisher defines the interface for publishing domain events.
|
||||
type EventPublisher interface {
|
||||
Publish(ctx context.Context, event interface{}) error
|
||||
}
|
||||
120
internal/biz/file.go
Normal file
120
internal/biz/file.go
Normal file
@ -0,0 +1,120 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"rag/file-system/internal/data"
|
||||
"rag/file-system/internal/watermark"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
)
|
||||
|
||||
// FileRepo defines the interface for S3 storage operations.
|
||||
type FileRepo interface {
|
||||
UploadFile(ctx context.Context, bucket, key string, fileData io.Reader) error
|
||||
DownloadFile(ctx context.Context, bucket, key string) (io.ReadCloser, error)
|
||||
ListBuckets(ctx context.Context) ([]string, error)
|
||||
CreateBucket(ctx context.Context, name string) error
|
||||
DeleteBucket(ctx context.Context, name string) error
|
||||
ListObjectsV2(ctx context.Context, bucket, prefix string, maxKeys int32, token *string) (*data.ListFilesResult, error)
|
||||
GeneratePresignedURL(ctx context.Context, bucket, key string, expiry time.Duration) (string, error)
|
||||
GetFileContent(ctx context.Context, bucket, key string) (string, error)
|
||||
DeleteFile(ctx context.Context, bucket, key string) error
|
||||
CreateMultipartUpload(ctx context.Context, bucket, key string) (string, error)
|
||||
UploadPart(ctx context.Context, bucket, key, uploadID string, partNumber int32, fileData io.Reader) (string, error)
|
||||
CompleteMultipartUpload(ctx context.Context, bucket, key, uploadID string, parts []data.Part) (string, error)
|
||||
AbortMultipartUpload(ctx context.Context, bucket, key, uploadID string) error
|
||||
}
|
||||
|
||||
// FileUsecase wraps FileRepo and provides file-level business operations.
|
||||
type FileUsecase struct {
|
||||
repo FileRepo
|
||||
eventPub EventPublisher
|
||||
log *log.Helper
|
||||
}
|
||||
|
||||
// NewFileUsecase creates a new FileUsecase.
|
||||
func NewFileUsecase(repo FileRepo, eventPub EventPublisher, logger log.Logger) *FileUsecase {
|
||||
return &FileUsecase{
|
||||
repo: repo,
|
||||
eventPub: eventPub,
|
||||
log: log.NewHelper(logger),
|
||||
}
|
||||
}
|
||||
|
||||
// UploadFile uploads data to the specified bucket and key.
|
||||
func (uc *FileUsecase) UploadFile(ctx context.Context, bucket, key string, fileData io.Reader) error {
|
||||
if err := uc.repo.UploadFile(ctx, bucket, key, fileData); err != nil {
|
||||
return err
|
||||
}
|
||||
// Publish domain event on success.
|
||||
if err := uc.eventPub.Publish(ctx, &watermark.FileUploadedEvent{
|
||||
BucketName: bucket,
|
||||
ObjectKey: key,
|
||||
}); err != nil {
|
||||
uc.log.Errorf("failed to publish FileUploadedEvent: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DownloadFile downloads an object from S3.
|
||||
func (uc *FileUsecase) DownloadFile(ctx context.Context, bucket, key string) (io.ReadCloser, error) {
|
||||
return uc.repo.DownloadFile(ctx, bucket, key)
|
||||
}
|
||||
|
||||
// GetFileContent retrieves text content for preview.
|
||||
func (uc *FileUsecase) GetFileContent(ctx context.Context, bucket, key string) (string, error) {
|
||||
return uc.repo.GetFileContent(ctx, bucket, key)
|
||||
}
|
||||
|
||||
// DeleteFile removes a file from S3.
|
||||
func (uc *FileUsecase) DeleteFile(ctx context.Context, bucket, key string) error {
|
||||
if err := uc.repo.DeleteFile(ctx, bucket, key); err != nil {
|
||||
return err
|
||||
}
|
||||
// Publish domain event on success.
|
||||
if err := uc.eventPub.Publish(ctx, &watermark.FileDeletedEvent{
|
||||
BucketName: bucket,
|
||||
ObjectKey: key,
|
||||
}); err != nil {
|
||||
uc.log.Errorf("failed to publish FileDeletedEvent: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListObjectsV2 lists files with pagination support.
|
||||
func (uc *FileUsecase) ListObjectsV2(ctx context.Context, bucket, prefix string, maxKeys int32, token *string) (*data.ListFilesResult, error) {
|
||||
return uc.repo.ListObjectsV2(ctx, bucket, prefix, maxKeys, token)
|
||||
}
|
||||
|
||||
// GeneratePresignedURL generates a presigned URL with custom expiry.
|
||||
func (uc *FileUsecase) GeneratePresignedURL(ctx context.Context, bucket, key string, expiry time.Duration) (string, error) {
|
||||
return uc.repo.GeneratePresignedURL(ctx, bucket, key, expiry)
|
||||
}
|
||||
|
||||
// GetPreviewURL generates a presigned URL with 24h expiry for file preview.
|
||||
func (uc *FileUsecase) GetPreviewURL(ctx context.Context, bucket, key string) (string, error) {
|
||||
return uc.repo.GeneratePresignedURL(ctx, bucket, key, 24*time.Hour)
|
||||
}
|
||||
|
||||
// CreateMultipartUpload initializes a multipart upload session.
|
||||
func (uc *FileUsecase) CreateMultipartUpload(ctx context.Context, bucket, key string) (string, error) {
|
||||
return uc.repo.CreateMultipartUpload(ctx, bucket, key)
|
||||
}
|
||||
|
||||
// UploadPart uploads a single part of a multipart upload.
|
||||
func (uc *FileUsecase) UploadPart(ctx context.Context, bucket, key, uploadID string, partNumber int32, fileData io.Reader) (string, error) {
|
||||
return uc.repo.UploadPart(ctx, bucket, key, uploadID, partNumber, fileData)
|
||||
}
|
||||
|
||||
// CompleteMultipartUpload assembles all parts to complete the upload.
|
||||
func (uc *FileUsecase) CompleteMultipartUpload(ctx context.Context, bucket, key, uploadID string, parts []data.Part) (string, error) {
|
||||
return uc.repo.CompleteMultipartUpload(ctx, bucket, key, uploadID, parts)
|
||||
}
|
||||
|
||||
// AbortMultipartUpload cancels an in-progress multipart upload.
|
||||
func (uc *FileUsecase) AbortMultipartUpload(ctx context.Context, bucket, key, uploadID string) error {
|
||||
return uc.repo.AbortMultipartUpload(ctx, bucket, key, uploadID)
|
||||
}
|
||||
175
internal/biz/folder.go
Normal file
175
internal/biz/folder.go
Normal file
@ -0,0 +1,175 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"rag/file-system/internal/data"
|
||||
"rag/file-system/internal/pkg/sanitize"
|
||||
"rag/file-system/internal/watermark"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// FolderRepo defines the interface for folder persistence operations.
|
||||
type FolderRepo interface {
|
||||
Create(ctx context.Context, folder *data.FolderPO) error
|
||||
GetByID(ctx context.Context, id string) (*data.FolderPO, error)
|
||||
GetWithChildren(ctx context.Context, id, ownerID string) (*data.FolderWithChildren, error)
|
||||
GetTree(ctx context.Context, ownerID string) ([]data.FolderPO, error)
|
||||
Update(ctx context.Context, folder *data.FolderPO) error
|
||||
Delete(ctx context.Context, id, ownerID string) error
|
||||
GetDescendantFileS3Keys(ctx context.Context, id, ownerID string) ([]data.FileMetaPO, error)
|
||||
}
|
||||
|
||||
// FileMetaRepo defines the interface for file metadata persistence operations.
|
||||
type FileMetaRepo interface {
|
||||
Create(ctx context.Context, file *data.FileMetaPO) error
|
||||
GetByID(ctx context.Context, id string) (*data.FileMetaPO, error)
|
||||
GetByFolder(ctx context.Context, folderID string) ([]data.FileMetaPO, error)
|
||||
Move(ctx context.Context, fileID, targetFolderID, ownerID string) error
|
||||
Delete(ctx context.Context, id, ownerID string) error
|
||||
}
|
||||
|
||||
// FolderUsecase handles folder and file metadata business logic.
|
||||
type FolderUsecase struct {
|
||||
folderRepo FolderRepo
|
||||
fileRepo FileMetaRepo
|
||||
s3Repo FileRepo
|
||||
eventPub EventPublisher
|
||||
log *log.Helper
|
||||
}
|
||||
|
||||
// NewFolderUsecase creates a new FolderUsecase.
|
||||
func NewFolderUsecase(folderRepo FolderRepo, fileRepo FileMetaRepo, s3Repo FileRepo, eventPub EventPublisher, logger log.Logger) *FolderUsecase {
|
||||
return &FolderUsecase{
|
||||
folderRepo: folderRepo,
|
||||
fileRepo: fileRepo,
|
||||
s3Repo: s3Repo,
|
||||
eventPub: eventPub,
|
||||
log: log.NewHelper(logger),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateFolder creates a new folder under the given parent.
|
||||
func (uc *FolderUsecase) CreateFolder(ctx context.Context, parentID *string, name, ownerID string) (*data.FolderPO, error) {
|
||||
folder := &data.FolderPO{
|
||||
ParentID: parentID,
|
||||
Name: sanitize.Filename(name),
|
||||
OwnerID: ownerID,
|
||||
}
|
||||
if err := uc.folderRepo.Create(ctx, folder); err != nil {
|
||||
return nil, fmt.Errorf("failed to create folder: %w", err)
|
||||
}
|
||||
// Publish domain event on success.
|
||||
if err := uc.eventPub.Publish(ctx, &watermark.FolderCreatedEvent{
|
||||
FolderID: folder.ID,
|
||||
Name: folder.Name,
|
||||
OwnerID: folder.OwnerID,
|
||||
}); err != nil {
|
||||
uc.log.Errorf("failed to publish FolderCreatedEvent: %v", err)
|
||||
}
|
||||
return folder, nil
|
||||
}
|
||||
|
||||
// GetFolder retrieves a folder with its children (sub-folders and files).
|
||||
func (uc *FolderUsecase) GetFolder(ctx context.Context, id, ownerID string) (*data.FolderPO, []data.FolderPO, []data.FileMetaPO, error) {
|
||||
result, err := uc.folderRepo.GetWithChildren(ctx, id, ownerID)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("failed to get folder: %w", err)
|
||||
}
|
||||
if result == nil {
|
||||
return nil, nil, nil, nil
|
||||
}
|
||||
return &result.Folder, result.SubFolders, result.Files, nil
|
||||
}
|
||||
|
||||
// GetFolderTree retrieves all folders owned by the given owner.
|
||||
func (uc *FolderUsecase) GetFolderTree(ctx context.Context, ownerID string) ([]data.FolderPO, error) {
|
||||
return uc.folderRepo.GetTree(ctx, ownerID)
|
||||
}
|
||||
|
||||
// RenameFolder updates a folder's name.
|
||||
func (uc *FolderUsecase) RenameFolder(ctx context.Context, id, name, ownerID string) (*data.FolderPO, error) {
|
||||
folder, err := uc.folderRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get folder: %w", err)
|
||||
}
|
||||
if folder == nil || folder.OwnerID != ownerID {
|
||||
return nil, fmt.Errorf("folder not found or not owned by user")
|
||||
}
|
||||
folder.Name = sanitize.Filename(name)
|
||||
folder.UpdatedAt = time.Now().UTC()
|
||||
if err := uc.folderRepo.Update(ctx, folder); err != nil {
|
||||
return nil, fmt.Errorf("failed to rename folder: %w", err)
|
||||
}
|
||||
return folder, nil
|
||||
}
|
||||
|
||||
// DeleteFolder deletes a folder and all descendant files from both S3 and the database.
|
||||
func (uc *FolderUsecase) DeleteFolder(ctx context.Context, id, ownerID string) error {
|
||||
// First, get all descendant file S3 keys so we can delete from S3.
|
||||
files, err := uc.folderRepo.GetDescendantFileS3Keys(ctx, id, ownerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get descendant files: %w", err)
|
||||
}
|
||||
|
||||
// Delete each file from S3.
|
||||
for _, f := range files {
|
||||
if delErr := uc.s3Repo.DeleteFile(ctx, f.S3Bucket, f.S3Key); delErr != nil {
|
||||
uc.log.Errorf("failed to delete S3 object %s/%s: %v", f.S3Bucket, f.S3Key, delErr)
|
||||
// Continue deleting other files even if one fails.
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the folder record (cascade should handle child files in DB).
|
||||
if err := uc.folderRepo.Delete(ctx, id, ownerID); err != nil {
|
||||
return fmt.Errorf("failed to delete folder: %w", err)
|
||||
}
|
||||
|
||||
// Publish domain event on success.
|
||||
if err := uc.eventPub.Publish(ctx, &watermark.FolderDeletedEvent{
|
||||
FolderID: id,
|
||||
OwnerID: ownerID,
|
||||
}); err != nil {
|
||||
uc.log.Errorf("failed to publish FolderDeletedEvent: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UploadToFolder uploads a file to a folder: generates a UUID S3 key, uploads to S3, saves metadata.
|
||||
func (uc *FolderUsecase) UploadToFolder(ctx context.Context, folderID, fileName string, fileData []byte, contentType, ownerID string) (*data.FileMetaPO, error) {
|
||||
s3Key := uuid.New().String()
|
||||
safeName := sanitize.Filename(fileName)
|
||||
bucket := "files" // Default bucket for folder-based uploads.
|
||||
|
||||
if err := uc.s3Repo.UploadFile(ctx, bucket, s3Key, bytes.NewReader(fileData)); err != nil {
|
||||
return nil, fmt.Errorf("failed to upload file to S3: %w", err)
|
||||
}
|
||||
|
||||
meta := &data.FileMetaPO{
|
||||
FolderID: folderID,
|
||||
Name: safeName,
|
||||
S3Key: s3Key,
|
||||
S3Bucket: bucket,
|
||||
Size: int64(len(fileData)),
|
||||
ContentType: contentType,
|
||||
OwnerID: ownerID,
|
||||
}
|
||||
if err := uc.fileRepo.Create(ctx, meta); err != nil {
|
||||
// Attempt to clean up the S3 object on metadata save failure.
|
||||
_ = uc.s3Repo.DeleteFile(ctx, bucket, s3Key)
|
||||
return nil, fmt.Errorf("failed to save file metadata: %w", err)
|
||||
}
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
// MoveFile moves a file from one folder to another.
|
||||
func (uc *FolderUsecase) MoveFile(ctx context.Context, fileID, targetFolderID, ownerID string) error {
|
||||
return uc.fileRepo.Move(ctx, fileID, targetFolderID, ownerID)
|
||||
}
|
||||
|
||||
161
internal/biz/share.go
Normal file
161
internal/biz/share.go
Normal file
@ -0,0 +1,161 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"rag/file-system/internal/data"
|
||||
"rag/file-system/internal/watermark"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
)
|
||||
|
||||
// ShareRepo defines the interface for share link persistence operations.
|
||||
type ShareRepo interface {
|
||||
Create(ctx context.Context, share *data.ShareLinkPO) error
|
||||
GetByToken(ctx context.Context, token string) (*data.ShareLinkPO, error)
|
||||
GetByID(ctx context.Context, id string) (*data.ShareLinkPO, error)
|
||||
Delete(ctx context.Context, id, createdBy string) error
|
||||
IncrementDownloadCount(ctx context.Context, token string) error
|
||||
ListByResource(ctx context.Context, resourceType, resourceID string) ([]data.ShareLinkPO, error)
|
||||
}
|
||||
|
||||
// FileMetaRepoForShare is a minimal interface for share usecase to look up file metadata.
|
||||
type FileMetaRepoForShare interface {
|
||||
GetByID(ctx context.Context, id string) (*data.FileMetaPO, error)
|
||||
}
|
||||
|
||||
// ShareUsecase handles share link business logic.
|
||||
type ShareUsecase struct {
|
||||
shareRepo ShareRepo
|
||||
fileRepo FileMetaRepoForShare
|
||||
s3Repo FileRepo
|
||||
eventPub EventPublisher
|
||||
log *log.Helper
|
||||
}
|
||||
|
||||
// NewShareUsecase creates a new ShareUsecase.
|
||||
func NewShareUsecase(shareRepo ShareRepo, fileRepo FileMetaRepoForShare, s3Repo FileRepo, eventPub EventPublisher, logger log.Logger) *ShareUsecase {
|
||||
return &ShareUsecase{
|
||||
shareRepo: shareRepo,
|
||||
fileRepo: fileRepo,
|
||||
s3Repo: s3Repo,
|
||||
eventPub: eventPub,
|
||||
log: log.NewHelper(logger),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateShare creates a new share link with a random token.
|
||||
func (uc *ShareUsecase) CreateShare(ctx context.Context, resourceType, resourceID, password string, expiresAt *time.Time, maxDownloads *int, createdBy string) (*data.ShareLinkPO, error) {
|
||||
token, err := generateToken(16)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate share token: %w", err)
|
||||
}
|
||||
|
||||
share := &data.ShareLinkPO{
|
||||
ResourceType: resourceType,
|
||||
ResourceID: resourceID,
|
||||
Token: token,
|
||||
ExpiresAt: expiresAt,
|
||||
MaxDownloads: maxDownloads,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
if password != "" {
|
||||
pwd := password
|
||||
share.Password = &pwd
|
||||
}
|
||||
|
||||
if err := uc.shareRepo.Create(ctx, share); err != nil {
|
||||
return nil, fmt.Errorf("failed to create share link: %w", err)
|
||||
}
|
||||
|
||||
// Publish domain event on success.
|
||||
if err := uc.eventPub.Publish(ctx, &watermark.ShareCreatedEvent{
|
||||
ShareID: share.ID,
|
||||
ResourceType: share.ResourceType,
|
||||
ResourceID: share.ResourceID,
|
||||
Token: share.Token,
|
||||
CreatedBy: share.CreatedBy,
|
||||
}); err != nil {
|
||||
uc.log.Errorf("failed to publish ShareCreatedEvent: %v", err)
|
||||
}
|
||||
|
||||
return share, nil
|
||||
}
|
||||
|
||||
// DeleteShare removes a share link by ID and creator.
|
||||
func (uc *ShareUsecase) DeleteShare(ctx context.Context, id, createdBy string) error {
|
||||
return uc.shareRepo.Delete(ctx, id, createdBy)
|
||||
}
|
||||
|
||||
// GetShareInfo retrieves share details and the associated file metadata.
|
||||
func (uc *ShareUsecase) GetShareInfo(ctx context.Context, token string) (*data.ShareLinkPO, *data.FileMetaPO, error) {
|
||||
share, err := uc.shareRepo.GetByToken(ctx, token)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get share link: %w", err)
|
||||
}
|
||||
if share == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
fileMeta, err := uc.fileRepo.GetByID(ctx, share.ResourceID)
|
||||
if err != nil {
|
||||
return share, nil, fmt.Errorf("failed to get file metadata: %w", err)
|
||||
}
|
||||
return share, fileMeta, nil
|
||||
}
|
||||
|
||||
// DownloadShare validates the share link, increments download count, and returns a presigned URL + filename.
|
||||
func (uc *ShareUsecase) DownloadShare(ctx context.Context, token string) (string, string, error) {
|
||||
share, err := uc.shareRepo.GetByToken(ctx, token)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get share link: %w", err)
|
||||
}
|
||||
if share == nil {
|
||||
return "", "", fmt.Errorf("share link not found")
|
||||
}
|
||||
|
||||
// Check expiry.
|
||||
if share.ExpiresAt != nil && share.ExpiresAt.Before(time.Now().UTC()) {
|
||||
return "", "", fmt.Errorf("share link has expired")
|
||||
}
|
||||
|
||||
// Check max downloads.
|
||||
if share.MaxDownloads != nil && share.DownloadCount >= *share.MaxDownloads {
|
||||
return "", "", fmt.Errorf("download limit reached")
|
||||
}
|
||||
|
||||
// Get file metadata for bucket/key info.
|
||||
fileMeta, err := uc.fileRepo.GetByID(ctx, share.ResourceID)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get file metadata: %w", err)
|
||||
}
|
||||
if fileMeta == nil {
|
||||
return "", "", fmt.Errorf("file not found")
|
||||
}
|
||||
|
||||
// Generate presigned URL.
|
||||
url, err := uc.s3Repo.GeneratePresignedURL(ctx, fileMeta.S3Bucket, fileMeta.S3Key, 15*time.Minute)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to generate download URL: %w", err)
|
||||
}
|
||||
|
||||
// Increment download count.
|
||||
if err := uc.shareRepo.IncrementDownloadCount(ctx, token); err != nil {
|
||||
uc.log.Errorf("failed to increment download count for token %s: %v", token, err)
|
||||
}
|
||||
|
||||
return url, fileMeta.Name, nil
|
||||
}
|
||||
|
||||
// generateToken creates a cryptographically secure random hex token.
|
||||
func generateToken(byteLen int) (string, error) {
|
||||
b := make([]byte, byteLen)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
RustFSEndpoint string
|
||||
RustFSAccessKeyID string
|
||||
RustFSSecretAccessKey string
|
||||
RustFSRegion string
|
||||
ServerPort string
|
||||
AuthAPIKey string
|
||||
RequestTimeout int
|
||||
GRPCAuthAddr string
|
||||
OTelEndpoint string
|
||||
DatabaseURL string
|
||||
}
|
||||
|
||||
func LoadConfig() *Config {
|
||||
return &Config{
|
||||
RustFSEndpoint: getEnv("RUSTFS_ENDPOINT_URL", "http://192.168.1.154:9000"),
|
||||
RustFSAccessKeyID: getEnv("RUSTFS_ACCESS_KEY_ID", ""),
|
||||
RustFSSecretAccessKey: getEnv("RUSTFS_SECRET_ACCESS_KEY", ""),
|
||||
RustFSRegion: getEnv("RUSTFS_REGION", "us-east-1"),
|
||||
ServerPort: getEnv("SERVER_PORT", "8080"),
|
||||
AuthAPIKey: getEnv("AUTH_API_KEY", ""),
|
||||
RequestTimeout: 30,
|
||||
GRPCAuthAddr: getEnv("GRPC_AUTH_ADDR", ""),
|
||||
OTelEndpoint: getEnv("OTEL_ENDPOINT", "192.168.1.154:4316"),
|
||||
DatabaseURL: getEnv("DATABASE_URL", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
if c.AuthAPIKey == "" && c.GRPCAuthAddr == "" {
|
||||
return fmt.Errorf("AUTH_API_KEY or GRPC_AUTH_ADDR is required")
|
||||
}
|
||||
if c.RustFSAccessKeyID == "" {
|
||||
return fmt.Errorf("RUSTFS_ACCESS_KEY_ID is required")
|
||||
}
|
||||
if c.RustFSSecretAccessKey == "" {
|
||||
return fmt.Errorf("RUSTFS_SECRET_ACCESS_KEY is required")
|
||||
}
|
||||
if c.RustFSEndpoint == "" {
|
||||
return fmt.Errorf("RUSTFS_ENDPOINT_URL is required")
|
||||
}
|
||||
if c.DatabaseURL == "" {
|
||||
return fmt.Errorf("DATABASE_URL is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if value, ok := os.LookupEnv(key); ok {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
package common
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestConfig_Validate_MissingAuthAPIKey(t *testing.T) {
|
||||
cfg := &Config{
|
||||
RustFSAccessKeyID: "key",
|
||||
RustFSSecretAccessKey: "secret",
|
||||
RustFSEndpoint: "http://localhost:9000",
|
||||
}
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Error("expected error when AuthAPIKey is empty, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_Validate_MissingRustFSAccessKeyID(t *testing.T) {
|
||||
cfg := &Config{
|
||||
AuthAPIKey: "api-key",
|
||||
RustFSSecretAccessKey: "secret",
|
||||
RustFSEndpoint: "http://localhost:9000",
|
||||
}
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Error("expected error when RustFSAccessKeyID is empty, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_Validate_AllFieldsPresent(t *testing.T) {
|
||||
cfg := &Config{
|
||||
AuthAPIKey: "api-key",
|
||||
RustFSAccessKeyID: "access-key",
|
||||
RustFSSecretAccessKey: "secret-key",
|
||||
RustFSEndpoint: "http://localhost:9000",
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Errorf("expected nil when all fields present, got error: %v", err)
|
||||
}
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
func NewNotFoundError(message string) *BusinessException {
|
||||
return &BusinessException{
|
||||
Message: message,
|
||||
Code: 404,
|
||||
}
|
||||
}
|
||||
|
||||
func NewConflictError(message string) *BusinessException {
|
||||
return &BusinessException{
|
||||
Message: message,
|
||||
Code: 409,
|
||||
}
|
||||
}
|
||||
|
||||
func NewBusinessExceptionWithCode(code int, message string) *BusinessException {
|
||||
return &BusinessException{
|
||||
Message: message,
|
||||
Code: code,
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
)
|
||||
|
||||
var Logger *slog.Logger
|
||||
|
||||
func InitLogger() {
|
||||
Logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
}
|
||||
@ -1,104 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"go.opentelemetry.io/otel/log/global"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
sdklog "go.opentelemetry.io/otel/sdk/log"
|
||||
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
)
|
||||
|
||||
// InitOTel initializes OpenTelemetry tracing, metrics, and logging providers.
|
||||
// It returns a shutdown function that should be called on application exit.
|
||||
func InitOTel(ctx context.Context, otelEndpoint string) (shutdown func(context.Context) error) {
|
||||
res, err := resource.New(ctx,
|
||||
resource.WithAttributes(
|
||||
semconv.ServiceNameKey.String("file-system"),
|
||||
semconv.ServiceVersionKey.String("1.0.0"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("OTel resource creation failed: %v", err)
|
||||
return func(ctx context.Context) error { return nil }
|
||||
}
|
||||
|
||||
// Tracer provider
|
||||
traceExp, err := otlptracegrpc.New(ctx,
|
||||
otlptracegrpc.WithEndpoint(otelEndpoint),
|
||||
otlptracegrpc.WithInsecure(),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("OTel trace exporter creation failed: %v", err)
|
||||
} else {
|
||||
tp := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithBatcher(traceExp),
|
||||
sdktrace.WithResource(res),
|
||||
)
|
||||
otel.SetTracerProvider(tp)
|
||||
}
|
||||
|
||||
// Meter provider
|
||||
metricExp, err := otlpmetricgrpc.New(ctx,
|
||||
otlpmetricgrpc.WithEndpoint(otelEndpoint),
|
||||
otlpmetricgrpc.WithInsecure(),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("OTel metric exporter creation failed: %v", err)
|
||||
} else {
|
||||
mp := sdkmetric.NewMeterProvider(
|
||||
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExp,
|
||||
sdkmetric.WithInterval(30*time.Second))),
|
||||
sdkmetric.WithResource(res),
|
||||
)
|
||||
otel.SetMeterProvider(mp)
|
||||
}
|
||||
|
||||
// Logger provider
|
||||
logExp, err := otlploggrpc.New(ctx,
|
||||
otlploggrpc.WithEndpoint(otelEndpoint),
|
||||
otlploggrpc.WithInsecure(),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("OTel log exporter creation failed: %v", err)
|
||||
} else {
|
||||
lp := sdklog.NewLoggerProvider(
|
||||
sdklog.WithProcessor(sdklog.NewBatchProcessor(logExp)),
|
||||
sdklog.WithResource(res),
|
||||
)
|
||||
global.SetLoggerProvider(lp)
|
||||
}
|
||||
|
||||
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
|
||||
propagation.TraceContext{},
|
||||
propagation.Baggage{},
|
||||
))
|
||||
|
||||
return func(ctx context.Context) error {
|
||||
var errs []error
|
||||
if tp, ok := otel.GetTracerProvider().(*sdktrace.TracerProvider); ok {
|
||||
errs = append(errs, tp.Shutdown(ctx))
|
||||
}
|
||||
if mp, ok := otel.GetMeterProvider().(*sdkmetric.MeterProvider); ok {
|
||||
errs = append(errs, mp.Shutdown(ctx))
|
||||
}
|
||||
if lp, ok := global.GetLoggerProvider().(*sdklog.LoggerProvider); ok {
|
||||
errs = append(errs, lp.Shutdown(ctx))
|
||||
}
|
||||
for _, e := range errs {
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
)
|
||||
|
||||
func WrapS3Error(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
noSuchBucket *types.NoSuchBucket
|
||||
noSuchKey *types.NoSuchKey
|
||||
notFound *types.NotFound
|
||||
)
|
||||
|
||||
if errors.As(err, &noSuchBucket) || errors.As(err, &noSuchKey) || errors.As(err, ¬Found) {
|
||||
return NewNotFoundError("resource not found")
|
||||
}
|
||||
|
||||
return fmt.Errorf("storage operation failed")
|
||||
}
|
||||
@ -1,108 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SanitizeObjectKey
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestSanitizeObjectKey_ValidInput(t *testing.T) {
|
||||
keys := []string{"folder/file.txt", "file.csv", "a/b/c/d.json"}
|
||||
for _, key := range keys {
|
||||
if err := SanitizeObjectKey(key); err != nil {
|
||||
t.Errorf("expected nil for key %q, got error: %v", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeObjectKey_PathTraversal(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
key string
|
||||
}{
|
||||
{"double dot", "../etc/passwd"},
|
||||
{"double dot middle", "a/../b"},
|
||||
{"double slash", "folder//file.txt"},
|
||||
{"leading slash", "/absolute/path"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := SanitizeObjectKey(tc.key)
|
||||
if err == nil {
|
||||
t.Errorf("expected error for key %q, got nil", tc.key)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SanitizeBucketName
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestSanitizeBucketName_Valid(t *testing.T) {
|
||||
names := []string{"my-bucket", "bucket123", "a1b", "my.bucket.name"}
|
||||
for _, name := range names {
|
||||
if err := SanitizeBucketName(name); err != nil {
|
||||
t.Errorf("expected nil for bucket %q, got error: %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeBucketName_Invalid(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{"uppercase", "MyBucket"},
|
||||
{"too short", "ab"},
|
||||
{"starts with hyphen", "-bucket"},
|
||||
{"starts with dot", ".bucket"},
|
||||
{"contains underscore", "my_bucket"},
|
||||
{"empty", ""},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := SanitizeBucketName(tc.input)
|
||||
if err == nil {
|
||||
t.Errorf("expected error for bucket %q, got nil", tc.input)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeBucketName_TooLong(t *testing.T) {
|
||||
longName := strings.Repeat("a", 64)
|
||||
if err := SanitizeBucketName(longName); err == nil {
|
||||
t.Error("expected error for bucket name > 63 chars, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SanitizeFilename
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestSanitizeFilename_RemovesCRLF(t *testing.T) {
|
||||
input := "file\r\nname.txt"
|
||||
got := SanitizeFilename(input)
|
||||
if strings.Contains(got, "\r") || strings.Contains(got, "\n") {
|
||||
t.Errorf("expected \\r and \\n removed, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFilename_EscapesQuotes(t *testing.T) {
|
||||
input := `some"file.txt`
|
||||
got := SanitizeFilename(input)
|
||||
if strings.Contains(got, `"`) && !strings.Contains(got, `\"`) {
|
||||
t.Errorf("expected quotes escaped, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFilename_CleanInput(t *testing.T) {
|
||||
input := "clean-file.txt"
|
||||
if got := SanitizeFilename(input); got != input {
|
||||
t.Errorf("expected %q, got %q", input, got)
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
package common
|
||||
|
||||
type Part struct {
|
||||
PartNumber int32
|
||||
ETag string
|
||||
}
|
||||
566
internal/conf/conf.pb.go
Normal file
566
internal/conf/conf.pb.go
Normal file
@ -0,0 +1,566 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: conf.proto
|
||||
|
||||
package conf
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
durationpb "google.golang.org/protobuf/types/known/durationpb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type Bootstrap struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Server *Server `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"`
|
||||
Data *Data `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
|
||||
Auth *Auth `protobuf:"bytes,3,opt,name=auth,proto3" json:"auth,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Bootstrap) Reset() {
|
||||
*x = Bootstrap{}
|
||||
mi := &file_conf_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Bootstrap) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Bootstrap) ProtoMessage() {}
|
||||
|
||||
func (x *Bootstrap) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_conf_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Bootstrap.ProtoReflect.Descriptor instead.
|
||||
func (*Bootstrap) Descriptor() ([]byte, []int) {
|
||||
return file_conf_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Bootstrap) GetServer() *Server {
|
||||
if x != nil {
|
||||
return x.Server
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Bootstrap) GetData() *Data {
|
||||
if x != nil {
|
||||
return x.Data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Bootstrap) GetAuth() *Auth {
|
||||
if x != nil {
|
||||
return x.Auth
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Http *Server_HTTP `protobuf:"bytes,1,opt,name=http,proto3" json:"http,omitempty"`
|
||||
Grpc *Server_GRPC `protobuf:"bytes,2,opt,name=grpc,proto3" json:"grpc,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Server) Reset() {
|
||||
*x = Server{}
|
||||
mi := &file_conf_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Server) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Server) ProtoMessage() {}
|
||||
|
||||
func (x *Server) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_conf_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Server.ProtoReflect.Descriptor instead.
|
||||
func (*Server) Descriptor() ([]byte, []int) {
|
||||
return file_conf_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *Server) GetHttp() *Server_HTTP {
|
||||
if x != nil {
|
||||
return x.Http
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Server) GetGrpc() *Server_GRPC {
|
||||
if x != nil {
|
||||
return x.Grpc
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Data struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Database *Data_Database `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"`
|
||||
S3 *Data_S3 `protobuf:"bytes,2,opt,name=s3,proto3" json:"s3,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Data) Reset() {
|
||||
*x = Data{}
|
||||
mi := &file_conf_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Data) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Data) ProtoMessage() {}
|
||||
|
||||
func (x *Data) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_conf_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Data.ProtoReflect.Descriptor instead.
|
||||
func (*Data) Descriptor() ([]byte, []int) {
|
||||
return file_conf_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *Data) GetDatabase() *Data_Database {
|
||||
if x != nil {
|
||||
return x.Database
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Data) GetS3() *Data_S3 {
|
||||
if x != nil {
|
||||
return x.S3
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
JwtKey string `protobuf:"bytes,1,opt,name=jwt_key,json=jwtKey,proto3" json:"jwt_key,omitempty"`
|
||||
GrpcAddr string `protobuf:"bytes,2,opt,name=grpc_addr,json=grpcAddr,proto3" json:"grpc_addr,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Auth) Reset() {
|
||||
*x = Auth{}
|
||||
mi := &file_conf_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Auth) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Auth) ProtoMessage() {}
|
||||
|
||||
func (x *Auth) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_conf_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Auth.ProtoReflect.Descriptor instead.
|
||||
func (*Auth) Descriptor() ([]byte, []int) {
|
||||
return file_conf_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *Auth) GetJwtKey() string {
|
||||
if x != nil {
|
||||
return x.JwtKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Auth) GetGrpcAddr() string {
|
||||
if x != nil {
|
||||
return x.GrpcAddr
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Server_HTTP struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Addr string `protobuf:"bytes,1,opt,name=addr,proto3" json:"addr,omitempty"`
|
||||
Timeout *durationpb.Duration `protobuf:"bytes,2,opt,name=timeout,proto3" json:"timeout,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Server_HTTP) Reset() {
|
||||
*x = Server_HTTP{}
|
||||
mi := &file_conf_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Server_HTTP) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Server_HTTP) ProtoMessage() {}
|
||||
|
||||
func (x *Server_HTTP) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_conf_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Server_HTTP.ProtoReflect.Descriptor instead.
|
||||
func (*Server_HTTP) Descriptor() ([]byte, []int) {
|
||||
return file_conf_proto_rawDescGZIP(), []int{1, 0}
|
||||
}
|
||||
|
||||
func (x *Server_HTTP) GetAddr() string {
|
||||
if x != nil {
|
||||
return x.Addr
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Server_HTTP) GetTimeout() *durationpb.Duration {
|
||||
if x != nil {
|
||||
return x.Timeout
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Server_GRPC struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Addr string `protobuf:"bytes,1,opt,name=addr,proto3" json:"addr,omitempty"`
|
||||
Timeout *durationpb.Duration `protobuf:"bytes,2,opt,name=timeout,proto3" json:"timeout,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Server_GRPC) Reset() {
|
||||
*x = Server_GRPC{}
|
||||
mi := &file_conf_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Server_GRPC) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Server_GRPC) ProtoMessage() {}
|
||||
|
||||
func (x *Server_GRPC) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_conf_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Server_GRPC.ProtoReflect.Descriptor instead.
|
||||
func (*Server_GRPC) Descriptor() ([]byte, []int) {
|
||||
return file_conf_proto_rawDescGZIP(), []int{1, 1}
|
||||
}
|
||||
|
||||
func (x *Server_GRPC) GetAddr() string {
|
||||
if x != nil {
|
||||
return x.Addr
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Server_GRPC) GetTimeout() *durationpb.Duration {
|
||||
if x != nil {
|
||||
return x.Timeout
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Data_Database struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Driver string `protobuf:"bytes,1,opt,name=driver,proto3" json:"driver,omitempty"`
|
||||
Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Data_Database) Reset() {
|
||||
*x = Data_Database{}
|
||||
mi := &file_conf_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Data_Database) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Data_Database) ProtoMessage() {}
|
||||
|
||||
func (x *Data_Database) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_conf_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Data_Database.ProtoReflect.Descriptor instead.
|
||||
func (*Data_Database) Descriptor() ([]byte, []int) {
|
||||
return file_conf_proto_rawDescGZIP(), []int{2, 0}
|
||||
}
|
||||
|
||||
func (x *Data_Database) GetDriver() string {
|
||||
if x != nil {
|
||||
return x.Driver
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Data_Database) GetSource() string {
|
||||
if x != nil {
|
||||
return x.Source
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Data_S3 struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Endpoint string `protobuf:"bytes,1,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
|
||||
AccessKey string `protobuf:"bytes,2,opt,name=access_key,json=accessKey,proto3" json:"access_key,omitempty"`
|
||||
SecretKey string `protobuf:"bytes,3,opt,name=secret_key,json=secretKey,proto3" json:"secret_key,omitempty"`
|
||||
Region string `protobuf:"bytes,4,opt,name=region,proto3" json:"region,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Data_S3) Reset() {
|
||||
*x = Data_S3{}
|
||||
mi := &file_conf_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Data_S3) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Data_S3) ProtoMessage() {}
|
||||
|
||||
func (x *Data_S3) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_conf_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Data_S3.ProtoReflect.Descriptor instead.
|
||||
func (*Data_S3) Descriptor() ([]byte, []int) {
|
||||
return file_conf_proto_rawDescGZIP(), []int{2, 1}
|
||||
}
|
||||
|
||||
func (x *Data_S3) GetEndpoint() string {
|
||||
if x != nil {
|
||||
return x.Endpoint
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Data_S3) GetAccessKey() string {
|
||||
if x != nil {
|
||||
return x.AccessKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Data_S3) GetSecretKey() string {
|
||||
if x != nil {
|
||||
return x.SecretKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Data_S3) GetRegion() string {
|
||||
if x != nil {
|
||||
return x.Region
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_conf_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_conf_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\n" +
|
||||
"conf.proto\x12\x04conf\x1a\x1egoogle/protobuf/duration.proto\"q\n" +
|
||||
"\tBootstrap\x12$\n" +
|
||||
"\x06server\x18\x01 \x01(\v2\f.conf.ServerR\x06server\x12\x1e\n" +
|
||||
"\x04data\x18\x02 \x01(\v2\n" +
|
||||
".conf.DataR\x04data\x12\x1e\n" +
|
||||
"\x04auth\x18\x03 \x01(\v2\n" +
|
||||
".conf.AuthR\x04auth\"\xf8\x01\n" +
|
||||
"\x06Server\x12%\n" +
|
||||
"\x04http\x18\x01 \x01(\v2\x11.conf.Server.HTTPR\x04http\x12%\n" +
|
||||
"\x04grpc\x18\x02 \x01(\v2\x11.conf.Server.GRPCR\x04grpc\x1aO\n" +
|
||||
"\x04HTTP\x12\x12\n" +
|
||||
"\x04addr\x18\x01 \x01(\tR\x04addr\x123\n" +
|
||||
"\atimeout\x18\x02 \x01(\v2\x19.google.protobuf.DurationR\atimeout\x1aO\n" +
|
||||
"\x04GRPC\x12\x12\n" +
|
||||
"\x04addr\x18\x01 \x01(\tR\x04addr\x123\n" +
|
||||
"\atimeout\x18\x02 \x01(\v2\x19.google.protobuf.DurationR\atimeout\"\x8a\x02\n" +
|
||||
"\x04Data\x12/\n" +
|
||||
"\bdatabase\x18\x01 \x01(\v2\x13.conf.Data.DatabaseR\bdatabase\x12\x1d\n" +
|
||||
"\x02s3\x18\x02 \x01(\v2\r.conf.Data.S3R\x02s3\x1a:\n" +
|
||||
"\bDatabase\x12\x16\n" +
|
||||
"\x06driver\x18\x01 \x01(\tR\x06driver\x12\x16\n" +
|
||||
"\x06source\x18\x02 \x01(\tR\x06source\x1av\n" +
|
||||
"\x02S3\x12\x1a\n" +
|
||||
"\bendpoint\x18\x01 \x01(\tR\bendpoint\x12\x1d\n" +
|
||||
"\n" +
|
||||
"access_key\x18\x02 \x01(\tR\taccessKey\x12\x1d\n" +
|
||||
"\n" +
|
||||
"secret_key\x18\x03 \x01(\tR\tsecretKey\x12\x16\n" +
|
||||
"\x06region\x18\x04 \x01(\tR\x06region\"<\n" +
|
||||
"\x04Auth\x12\x17\n" +
|
||||
"\ajwt_key\x18\x01 \x01(\tR\x06jwtKey\x12\x1b\n" +
|
||||
"\tgrpc_addr\x18\x02 \x01(\tR\bgrpcAddrB\x1fZ\x1drag/file-system/internal/confb\x06proto3"
|
||||
|
||||
var (
|
||||
file_conf_proto_rawDescOnce sync.Once
|
||||
file_conf_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_conf_proto_rawDescGZIP() []byte {
|
||||
file_conf_proto_rawDescOnce.Do(func() {
|
||||
file_conf_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_conf_proto_rawDesc), len(file_conf_proto_rawDesc)))
|
||||
})
|
||||
return file_conf_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_conf_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
|
||||
var file_conf_proto_goTypes = []any{
|
||||
(*Bootstrap)(nil), // 0: conf.Bootstrap
|
||||
(*Server)(nil), // 1: conf.Server
|
||||
(*Data)(nil), // 2: conf.Data
|
||||
(*Auth)(nil), // 3: conf.Auth
|
||||
(*Server_HTTP)(nil), // 4: conf.Server.HTTP
|
||||
(*Server_GRPC)(nil), // 5: conf.Server.GRPC
|
||||
(*Data_Database)(nil), // 6: conf.Data.Database
|
||||
(*Data_S3)(nil), // 7: conf.Data.S3
|
||||
(*durationpb.Duration)(nil), // 8: google.protobuf.Duration
|
||||
}
|
||||
var file_conf_proto_depIdxs = []int32{
|
||||
1, // 0: conf.Bootstrap.server:type_name -> conf.Server
|
||||
2, // 1: conf.Bootstrap.data:type_name -> conf.Data
|
||||
3, // 2: conf.Bootstrap.auth:type_name -> conf.Auth
|
||||
4, // 3: conf.Server.http:type_name -> conf.Server.HTTP
|
||||
5, // 4: conf.Server.grpc:type_name -> conf.Server.GRPC
|
||||
6, // 5: conf.Data.database:type_name -> conf.Data.Database
|
||||
7, // 6: conf.Data.s3:type_name -> conf.Data.S3
|
||||
8, // 7: conf.Server.HTTP.timeout:type_name -> google.protobuf.Duration
|
||||
8, // 8: conf.Server.GRPC.timeout:type_name -> google.protobuf.Duration
|
||||
9, // [9:9] is the sub-list for method output_type
|
||||
9, // [9:9] is the sub-list for method input_type
|
||||
9, // [9:9] is the sub-list for extension type_name
|
||||
9, // [9:9] is the sub-list for extension extendee
|
||||
0, // [0:9] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_conf_proto_init() }
|
||||
func file_conf_proto_init() {
|
||||
if File_conf_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_conf_proto_rawDesc), len(file_conf_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 8,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_conf_proto_goTypes,
|
||||
DependencyIndexes: file_conf_proto_depIdxs,
|
||||
MessageInfos: file_conf_proto_msgTypes,
|
||||
}.Build()
|
||||
File_conf_proto = out.File
|
||||
file_conf_proto_goTypes = nil
|
||||
file_conf_proto_depIdxs = nil
|
||||
}
|
||||
46
internal/conf/conf.proto
Normal file
46
internal/conf/conf.proto
Normal file
@ -0,0 +1,46 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package conf;
|
||||
|
||||
option go_package = "rag/file-system/internal/conf";
|
||||
|
||||
import "google/protobuf/duration.proto";
|
||||
|
||||
message Bootstrap {
|
||||
Server server = 1;
|
||||
Data data = 2;
|
||||
Auth auth = 3;
|
||||
}
|
||||
|
||||
message Server {
|
||||
message HTTP {
|
||||
string addr = 1;
|
||||
google.protobuf.Duration timeout = 2;
|
||||
}
|
||||
message GRPC {
|
||||
string addr = 1;
|
||||
google.protobuf.Duration timeout = 2;
|
||||
}
|
||||
HTTP http = 1;
|
||||
GRPC grpc = 2;
|
||||
}
|
||||
|
||||
message Data {
|
||||
message Database {
|
||||
string driver = 1;
|
||||
string source = 2;
|
||||
}
|
||||
message S3 {
|
||||
string endpoint = 1;
|
||||
string access_key = 2;
|
||||
string secret_key = 3;
|
||||
string region = 4;
|
||||
}
|
||||
Database database = 1;
|
||||
S3 s3 = 2;
|
||||
}
|
||||
|
||||
message Auth {
|
||||
string jwt_key = 1;
|
||||
string grpc_addr = 2;
|
||||
}
|
||||
138
internal/data/data.go
Normal file
138
internal/data/data.go
Normal file
@ -0,0 +1,138 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"rag/file-system/internal/conf"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
"github.com/google/wire"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Data holds the GORM database connection and provides transaction support.
|
||||
type Data struct {
|
||||
db *gorm.DB
|
||||
log *log.Helper
|
||||
}
|
||||
|
||||
// contextTxKey is the context key for storing the current transaction.
|
||||
type contextTxKey struct{}
|
||||
|
||||
// NewData creates a new Data instance with GORM connected to PostgreSQL.
|
||||
func NewData(c *conf.Data, logger log.Logger) (*Data, func(), error) {
|
||||
helper := log.NewHelper(logger)
|
||||
db, err := gorm.Open(postgres.Open(c.Database.Source), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sqlDB.SetMaxOpenConns(25)
|
||||
sqlDB.SetMaxIdleConns(5)
|
||||
|
||||
if err := db.AutoMigrate(&FolderPO{}, &FileMetaPO{}, &ShareLinkPO{}); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
helper.Info("connected to PostgreSQL via GORM")
|
||||
cleanup := func() {
|
||||
sqlDB.Close()
|
||||
}
|
||||
return &Data{db: db, log: helper}, cleanup, nil
|
||||
}
|
||||
|
||||
// SqlDB returns the underlying *sql.DB for use by Watermill.
|
||||
func (d *Data) SqlDB() (*sql.DB, error) {
|
||||
return d.db.DB()
|
||||
}
|
||||
|
||||
// DB returns the *gorm.DB for the given context. If a transaction is active
|
||||
// in the context, it returns the transaction DB; otherwise the global DB.
|
||||
func (d *Data) DB(ctx context.Context) *gorm.DB {
|
||||
tx, ok := ctx.Value(contextTxKey{}).(*gorm.DB)
|
||||
if ok {
|
||||
return tx
|
||||
}
|
||||
return d.db.WithContext(ctx)
|
||||
}
|
||||
|
||||
// Transaction is the interface for executing operations within a database transaction.
|
||||
type Transaction interface {
|
||||
InTx(ctx context.Context, fn func(ctx context.Context) error) error
|
||||
}
|
||||
|
||||
// InTx executes fn inside a database transaction.
|
||||
func (d *Data) InTx(ctx context.Context, fn func(ctx context.Context) error) error {
|
||||
return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
ctx = context.WithValue(ctx, contextTxKey{}, tx)
|
||||
return fn(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
// ProviderSet is the Wire provider set for the data layer.
|
||||
var ProviderSet = wire.NewSet(NewData, NewFileRepo, NewFolderRepo, NewFileMetaRepo, NewShareRepo)
|
||||
|
||||
// --- GORM Models (Persistence Objects) ---
|
||||
|
||||
// FolderPO maps to the "folders" table.
|
||||
type FolderPO struct {
|
||||
ID string `gorm:"primaryKey;type:uuid;default:gen_random_uuid();comment:主键ID"`
|
||||
ParentID *string `gorm:"type:uuid;index:idx_folders_parent;comment:父文件夹ID"`
|
||||
Name string `gorm:"type:varchar(255);not null;comment:文件夹名称"`
|
||||
OwnerID string `gorm:"type:varchar(36);not null;index:idx_folders_owner;comment:所有者ID"`
|
||||
CreatedBy string `gorm:"type:varchar(36);not null;default:'';comment:创建人ID"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间"`
|
||||
UpdatedBy string `gorm:"type:varchar(36);not null;default:'';comment:更新人ID"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime;comment:更新时间"`
|
||||
IsDeleted bool `gorm:"not null;default:false;comment:是否软删除"`
|
||||
OperatorIP string `gorm:"type:varchar(500);comment:操作人IP地址"`
|
||||
}
|
||||
|
||||
func (FolderPO) TableName() string { return "folders" }
|
||||
|
||||
// FileMetaPO maps to the "files" table.
|
||||
type FileMetaPO struct {
|
||||
ID string `gorm:"primaryKey;type:uuid;default:gen_random_uuid();comment:主键ID"`
|
||||
FolderID string `gorm:"type:uuid;index:idx_files_folder;comment:所属文件夹ID"`
|
||||
Name string `gorm:"type:varchar(255);not null;comment:文件名称"`
|
||||
S3Key string `gorm:"type:varchar(512);not null;index:idx_files_s3_key;comment:S3对象键"`
|
||||
S3Bucket string `gorm:"type:varchar(255);not null;comment:S3存储桶名称"`
|
||||
Size int64 `gorm:"default:0;comment:文件大小(字节)"`
|
||||
ContentType string `gorm:"type:varchar(255);default:'application/octet-stream';comment:文件MIME类型"`
|
||||
OwnerID string `gorm:"type:varchar(36);not null;index:idx_files_owner;comment:所有者ID"`
|
||||
CreatedBy string `gorm:"type:varchar(36);not null;default:'';comment:创建人ID"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间"`
|
||||
UpdatedBy string `gorm:"type:varchar(36);not null;default:'';comment:更新人ID"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime;comment:更新时间"`
|
||||
IsDeleted bool `gorm:"not null;default:false;comment:是否软删除"`
|
||||
OperatorIP string `gorm:"type:varchar(500);comment:操作人IP地址"`
|
||||
}
|
||||
|
||||
func (FileMetaPO) TableName() string { return "files" }
|
||||
|
||||
// ShareLinkPO maps to the "share_links" table.
|
||||
type ShareLinkPO struct {
|
||||
ID string `gorm:"primaryKey;type:uuid;default:gen_random_uuid();comment:主键ID"`
|
||||
ResourceType string `gorm:"type:varchar(10);not null;comment:资源类型(folder/file)"`
|
||||
ResourceID string `gorm:"type:uuid;not null;comment:资源ID"`
|
||||
Token string `gorm:"type:varchar(32);not null;uniqueIndex:idx_share_token;comment:分享令牌"`
|
||||
Password *string `gorm:"type:varchar(255);comment:访问密码"`
|
||||
ExpiresAt *time.Time `gorm:"type:timestamptz;comment:过期时间"`
|
||||
DownloadCount int `gorm:"default:0;comment:下载次数"`
|
||||
MaxDownloads *int `gorm:"comment:最大下载次数"`
|
||||
CreatedBy string `gorm:"type:varchar(36);not null;comment:创建人ID"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间"`
|
||||
UpdatedBy string `gorm:"type:varchar(36);not null;default:'';comment:更新人ID"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime;comment:更新时间"`
|
||||
IsDeleted bool `gorm:"not null;default:false;comment:是否软删除"`
|
||||
OperatorIP string `gorm:"type:varchar(500);comment:操作人IP地址"`
|
||||
}
|
||||
|
||||
func (ShareLinkPO) TableName() string { return "share_links" }
|
||||
90
internal/data/file_meta_repo.go
Normal file
90
internal/data/file_meta_repo.go
Normal file
@ -0,0 +1,90 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
)
|
||||
|
||||
// FileMetaRepo implements file metadata persistence operations using GORM.
|
||||
type FileMetaRepo struct {
|
||||
data *Data
|
||||
log *log.Helper
|
||||
}
|
||||
|
||||
// NewFileMetaRepo creates a new FileMetaRepo.
|
||||
func NewFileMetaRepo(data *Data, logger log.Logger) *FileMetaRepo {
|
||||
return &FileMetaRepo{
|
||||
data: data,
|
||||
log: log.NewHelper(logger),
|
||||
}
|
||||
}
|
||||
|
||||
// Create inserts a new file metadata record.
|
||||
func (r *FileMetaRepo) Create(ctx context.Context, file *FileMetaPO) error {
|
||||
return r.data.DB(ctx).Create(file).Error
|
||||
}
|
||||
|
||||
// GetByID retrieves file metadata by ID. Returns nil if not found.
|
||||
func (r *FileMetaRepo) GetByID(ctx context.Context, id string) (*FileMetaPO, error) {
|
||||
var file FileMetaPO
|
||||
err := r.data.DB(ctx).Where("id = ?", id).First(&file).Error
|
||||
if err != nil {
|
||||
if isRecordNotFound(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get file meta: %w", err)
|
||||
}
|
||||
return &file, nil
|
||||
}
|
||||
|
||||
// GetByFolder retrieves all files in a folder, ordered by name.
|
||||
func (r *FileMetaRepo) GetByFolder(ctx context.Context, folderID string) ([]FileMetaPO, error) {
|
||||
var files []FileMetaPO
|
||||
err := r.data.DB(ctx).
|
||||
Where("folder_id = ?", folderID).
|
||||
Order("name").
|
||||
Find(&files).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get files by folder: %w", err)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// Move updates the folder_id of a file (moves it to another folder).
|
||||
func (r *FileMetaRepo) Move(ctx context.Context, fileID string, targetFolderID string, ownerID string) error {
|
||||
result := r.data.DB(ctx).Model(&FileMetaPO{}).
|
||||
Where("id = ? AND owner_id = ?", fileID, ownerID).
|
||||
Updates(map[string]interface{}{
|
||||
"folder_id": targetFolderID,
|
||||
"updated_at": gormNow(),
|
||||
})
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("failed to move file: %w", result.Error)
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("file not found or not owned by user")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes a file metadata record by ID and ownerID.
|
||||
func (r *FileMetaRepo) Delete(ctx context.Context, id string, ownerID string) error {
|
||||
result := r.data.DB(ctx).
|
||||
Where("id = ? AND owner_id = ?", id, ownerID).
|
||||
Delete(&FileMetaPO{})
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("failed to delete file meta: %w", result.Error)
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("file not found or not owned by user")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// gormNow returns the current UTC time for use in GORM updates.
|
||||
func gormNow() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
285
internal/data/file_repo.go
Normal file
285
internal/data/file_repo.go
Normal file
@ -0,0 +1,285 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"rag/file-system/internal/conf"
|
||||
"rag/file-system/internal/pkg/s3errors"
|
||||
|
||||
"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"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
)
|
||||
|
||||
const maxContentPreviewSize = 10 * 1024 * 1024 // 10MB
|
||||
|
||||
// Part represents a single uploaded part in a multipart upload.
|
||||
type Part struct {
|
||||
ETag string
|
||||
PartNumber int32
|
||||
}
|
||||
|
||||
// FileInfo holds metadata about an S3 object.
|
||||
type FileInfo struct {
|
||||
Key string
|
||||
Size int64
|
||||
LastModified time.Time
|
||||
ETag string
|
||||
}
|
||||
|
||||
// ListFilesResult holds the result of a paginated list operation.
|
||||
type ListFilesResult struct {
|
||||
Files []FileInfo
|
||||
NextContinuationToken *string
|
||||
}
|
||||
|
||||
// FileRepo handles all S3 storage operations.
|
||||
type FileRepo struct {
|
||||
client *s3.Client
|
||||
presignClient *s3.PresignClient
|
||||
}
|
||||
|
||||
// NewFileRepo creates a new FileRepo with an S3 client configured from the provided config.
|
||||
func NewFileRepo(c *conf.Data) *FileRepo {
|
||||
s3Conf := c.GetS3()
|
||||
customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
|
||||
return aws.Endpoint{
|
||||
URL: s3Conf.GetEndpoint(),
|
||||
SigningRegion: s3Conf.GetRegion(),
|
||||
}, nil
|
||||
})
|
||||
|
||||
awsCfg, err := config.LoadDefaultConfig(context.TODO(),
|
||||
config.WithRegion(s3Conf.GetRegion()),
|
||||
config.WithEndpointResolverWithOptions(customResolver),
|
||||
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
|
||||
s3Conf.GetAccessKey(),
|
||||
s3Conf.GetSecretKey(),
|
||||
"",
|
||||
)),
|
||||
)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("unable to load S3 SDK config: %v", err))
|
||||
}
|
||||
|
||||
client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
|
||||
o.UsePathStyle = true
|
||||
})
|
||||
|
||||
return &FileRepo{
|
||||
client: client,
|
||||
presignClient: s3.NewPresignClient(client),
|
||||
}
|
||||
}
|
||||
|
||||
// UploadFile uploads data to the specified bucket and object key.
|
||||
func (r *FileRepo) UploadFile(ctx context.Context, bucketName string, objectKey string, data io.Reader) error {
|
||||
_, err := r.client.PutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
Body: data,
|
||||
})
|
||||
return s3errors.Wrap(err)
|
||||
}
|
||||
|
||||
// DownloadFile downloads an object from S3 and returns its body as a ReadCloser.
|
||||
func (r *FileRepo) DownloadFile(ctx context.Context, bucketName string, objectKey string) (io.ReadCloser, error) {
|
||||
resp, err := r.client.GetObject(ctx, &s3.GetObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, s3errors.Wrap(err)
|
||||
}
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
// ListBuckets returns all bucket names.
|
||||
func (r *FileRepo) ListBuckets(ctx context.Context) ([]string, error) {
|
||||
resp, err := r.client.ListBuckets(ctx, &s3.ListBucketsInput{})
|
||||
if err != nil {
|
||||
return nil, s3errors.Wrap(err)
|
||||
}
|
||||
var buckets []string
|
||||
for _, b := range resp.Buckets {
|
||||
if b.Name != nil {
|
||||
buckets = append(buckets, *b.Name)
|
||||
}
|
||||
}
|
||||
return buckets, nil
|
||||
}
|
||||
|
||||
// CreateBucket creates a new S3 bucket.
|
||||
func (r *FileRepo) CreateBucket(ctx context.Context, bucketName string) error {
|
||||
_, err := r.client.CreateBucket(ctx, &s3.CreateBucketInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
return s3errors.Wrap(err)
|
||||
}
|
||||
|
||||
// DeleteBucket deletes an S3 bucket.
|
||||
func (r *FileRepo) DeleteBucket(ctx context.Context, bucketName string) error {
|
||||
_, err := r.client.DeleteBucket(ctx, &s3.DeleteBucketInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
return s3errors.Wrap(err)
|
||||
}
|
||||
|
||||
// GetFileContent retrieves text file content for preview (e.g., Markdown files).
|
||||
func (r *FileRepo) GetFileContent(ctx context.Context, bucketName string, objectKey string) (string, error) {
|
||||
resp, err := r.client.GetObject(ctx, &s3.GetObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
if err != nil {
|
||||
return "", s3errors.Wrap(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, maxContentPreviewSize))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if int64(len(data)) >= maxContentPreviewSize {
|
||||
return "", fmt.Errorf("file too large for content preview (max 10MB)")
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// DeleteFile removes a file from the bucket.
|
||||
func (r *FileRepo) DeleteFile(ctx context.Context, bucketName string, objectKey string) error {
|
||||
_, err := r.client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
return s3errors.Wrap(err)
|
||||
}
|
||||
|
||||
// ListObjectsV2 lists files with pagination support.
|
||||
func (r *FileRepo) ListObjectsV2(ctx context.Context, bucketName string, prefix string, maxKeys int32, continuationToken *string) (*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.ListObjectsV2(ctx, input)
|
||||
if err != nil {
|
||||
return nil, s3errors.Wrap(err)
|
||||
}
|
||||
|
||||
files := make([]FileInfo, 0, len(resp.Contents))
|
||||
for _, obj := range resp.Contents {
|
||||
if obj.Key == nil || obj.Size == nil || obj.LastModified == nil || obj.ETag == nil {
|
||||
continue
|
||||
}
|
||||
files = append(files, FileInfo{
|
||||
Key: *obj.Key,
|
||||
Size: *obj.Size,
|
||||
LastModified: *obj.LastModified,
|
||||
ETag: *obj.ETag,
|
||||
})
|
||||
}
|
||||
|
||||
return &ListFilesResult{
|
||||
Files: files,
|
||||
NextContinuationToken: resp.NextContinuationToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GeneratePresignedURL generates a presigned URL for temporary file access.
|
||||
func (r *FileRepo) GeneratePresignedURL(ctx context.Context, bucketName string, objectKey string, expiry time.Duration) (string, error) {
|
||||
presignResult, err := r.presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
}, func(opts *s3.PresignOptions) {
|
||||
opts.Expires = expiry
|
||||
})
|
||||
if err != nil {
|
||||
return "", s3errors.Wrap(err)
|
||||
}
|
||||
return presignResult.URL, nil
|
||||
}
|
||||
|
||||
// CreateMultipartUpload initializes a multipart upload session.
|
||||
func (r *FileRepo) CreateMultipartUpload(ctx context.Context, bucketName string, objectKey string) (string, error) {
|
||||
resp, err := r.client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
if err != nil {
|
||||
return "", s3errors.Wrap(err)
|
||||
}
|
||||
if resp.UploadId == nil {
|
||||
return "", fmt.Errorf("failed to initialize multipart upload")
|
||||
}
|
||||
return *resp.UploadId, nil
|
||||
}
|
||||
|
||||
// UploadPart uploads a single part of a multipart upload.
|
||||
func (r *FileRepo) UploadPart(ctx context.Context, bucketName string, objectKey string, uploadId string, partNumber int32, data io.Reader) (string, error) {
|
||||
resp, err := r.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 "", s3errors.Wrap(err)
|
||||
}
|
||||
if resp.ETag == nil {
|
||||
return "", fmt.Errorf("failed to upload part")
|
||||
}
|
||||
return *resp.ETag, nil
|
||||
}
|
||||
|
||||
// CompleteMultipartUpload assembles all parts to complete the upload.
|
||||
func (r *FileRepo) CompleteMultipartUpload(ctx context.Context, bucketName string, objectKey string, uploadId string, parts []Part) (string, error) {
|
||||
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.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 "", s3errors.Wrap(err)
|
||||
}
|
||||
if resp.Location == nil {
|
||||
return "", fmt.Errorf("failed to complete multipart upload")
|
||||
}
|
||||
return *resp.Location, nil
|
||||
}
|
||||
|
||||
// AbortMultipartUpload cancels an in-progress multipart upload.
|
||||
func (r *FileRepo) AbortMultipartUpload(ctx context.Context, bucketName string, objectKey string, uploadId string) error {
|
||||
_, err := r.client.AbortMultipartUpload(ctx, &s3.AbortMultipartUploadInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
UploadId: aws.String(uploadId),
|
||||
})
|
||||
return s3errors.Wrap(err)
|
||||
}
|
||||
148
internal/data/folder_repo.go
Normal file
148
internal/data/folder_repo.go
Normal file
@ -0,0 +1,148 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// FolderRepo implements folder persistence operations using GORM.
|
||||
type FolderRepo struct {
|
||||
data *Data
|
||||
log *log.Helper
|
||||
}
|
||||
|
||||
// NewFolderRepo creates a new FolderRepo.
|
||||
func NewFolderRepo(data *Data, logger log.Logger) *FolderRepo {
|
||||
return &FolderRepo{
|
||||
data: data,
|
||||
log: log.NewHelper(logger),
|
||||
}
|
||||
}
|
||||
|
||||
// FolderWithChildren holds a folder along with its sub-folders and files.
|
||||
type FolderWithChildren struct {
|
||||
Folder FolderPO
|
||||
SubFolders []FolderPO
|
||||
Files []FileMetaPO
|
||||
}
|
||||
|
||||
// Create inserts a new folder record.
|
||||
func (r *FolderRepo) Create(ctx context.Context, folder *FolderPO) error {
|
||||
return r.data.DB(ctx).Create(folder).Error
|
||||
}
|
||||
|
||||
// GetByID retrieves a folder by its ID. Returns nil if not found.
|
||||
func (r *FolderRepo) GetByID(ctx context.Context, id string) (*FolderPO, error) {
|
||||
var folder FolderPO
|
||||
err := r.data.DB(ctx).Where("id = ?", id).First(&folder).Error
|
||||
if err != nil {
|
||||
if isRecordNotFound(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get folder: %w", err)
|
||||
}
|
||||
return &folder, nil
|
||||
}
|
||||
|
||||
// GetWithChildren retrieves a folder with its sub-folders and files, filtered by ownerID.
|
||||
func (r *FolderRepo) GetWithChildren(ctx context.Context, id string, ownerID string) (*FolderWithChildren, error) {
|
||||
folder, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if folder == nil || folder.OwnerID != ownerID {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var subFolders []FolderPO
|
||||
if err := r.data.DB(ctx).
|
||||
Where("parent_id = ? AND owner_id = ?", id, ownerID).
|
||||
Order("name").
|
||||
Find(&subFolders).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to query sub-folders: %w", err)
|
||||
}
|
||||
|
||||
var files []FileMetaPO
|
||||
if err := r.data.DB(ctx).
|
||||
Where("folder_id = ? AND owner_id = ?", id, ownerID).
|
||||
Order("name").
|
||||
Find(&files).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to query files: %w", err)
|
||||
}
|
||||
|
||||
return &FolderWithChildren{
|
||||
Folder: *folder,
|
||||
SubFolders: subFolders,
|
||||
Files: files,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetTree retrieves all folders owned by the given ownerID.
|
||||
func (r *FolderRepo) GetTree(ctx context.Context, ownerID string) ([]FolderPO, error) {
|
||||
var folders []FolderPO
|
||||
err := r.data.DB(ctx).
|
||||
Where("owner_id = ?", ownerID).
|
||||
Order("name").
|
||||
Find(&folders).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get folder tree: %w", err)
|
||||
}
|
||||
return folders, nil
|
||||
}
|
||||
|
||||
// Update modifies a folder's name and updated_at timestamp.
|
||||
func (r *FolderRepo) Update(ctx context.Context, folder *FolderPO) error {
|
||||
result := r.data.DB(ctx).Model(&FolderPO{}).
|
||||
Where("id = ?", folder.ID).
|
||||
Updates(map[string]interface{}{
|
||||
"name": folder.Name,
|
||||
"updated_at": folder.UpdatedAt,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("failed to update folder: %w", result.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes a folder by ID and ownerID. Returns error if not found.
|
||||
func (r *FolderRepo) Delete(ctx context.Context, id string, ownerID string) error {
|
||||
result := r.data.DB(ctx).
|
||||
Where("id = ? AND owner_id = ?", id, ownerID).
|
||||
Delete(&FolderPO{})
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("failed to delete folder: %w", result.Error)
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("folder not found or not owned by user")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDescendantFileS3Keys returns all files in a folder and all its descendant folders.
|
||||
// Uses a recursive CTE to walk the folder tree.
|
||||
func (r *FolderRepo) GetDescendantFileS3Keys(ctx context.Context, id string, ownerID string) ([]FileMetaPO, error) {
|
||||
var files []FileMetaPO
|
||||
// Raw SQL with recursive CTE - GORM doesn't natively support recursive queries
|
||||
err := r.data.DB(ctx).Raw(`
|
||||
WITH RECURSIVE descendants AS (
|
||||
SELECT id FROM folders WHERE id = ? AND owner_id = ?
|
||||
UNION
|
||||
SELECT f.id FROM folders f INNER JOIN descendants d ON f.parent_id = d.id
|
||||
)
|
||||
SELECT fi.id, fi.folder_id, fi.name, fi.s3_key, fi.s3_bucket, fi.size, fi.content_type, fi.owner_id, fi.created_at, fi.updated_at
|
||||
FROM files fi INNER JOIN descendants d ON fi.folder_id = d.id
|
||||
WHERE fi.owner_id = ?`,
|
||||
id, ownerID, ownerID).Scan(&files).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get descendant file S3 keys: %w", err)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// isRecordNotFound checks if the error is a GORM record-not-found error.
|
||||
func isRecordNotFound(err error) bool {
|
||||
return err == gorm.ErrRecordNotFound
|
||||
}
|
||||
88
internal/data/share_repo.go
Normal file
88
internal/data/share_repo.go
Normal file
@ -0,0 +1,88 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ShareRepo implements share link persistence operations using GORM.
|
||||
type ShareRepo struct {
|
||||
data *Data
|
||||
log *log.Helper
|
||||
}
|
||||
|
||||
// NewShareRepo creates a new ShareRepo.
|
||||
func NewShareRepo(data *Data, logger log.Logger) *ShareRepo {
|
||||
return &ShareRepo{
|
||||
data: data,
|
||||
log: log.NewHelper(logger),
|
||||
}
|
||||
}
|
||||
|
||||
// Create inserts a new share link record.
|
||||
func (r *ShareRepo) Create(ctx context.Context, share *ShareLinkPO) error {
|
||||
return r.data.DB(ctx).Create(share).Error
|
||||
}
|
||||
|
||||
// GetByToken retrieves a share link by its token. Returns nil if not found.
|
||||
func (r *ShareRepo) GetByToken(ctx context.Context, token string) (*ShareLinkPO, error) {
|
||||
return r.queryOne(ctx, "token = ?", token)
|
||||
}
|
||||
|
||||
// GetByID retrieves a share link by its ID. Returns nil if not found.
|
||||
func (r *ShareRepo) GetByID(ctx context.Context, id string) (*ShareLinkPO, error) {
|
||||
return r.queryOne(ctx, "id = ?", id)
|
||||
}
|
||||
|
||||
// Delete removes a share link by ID and creator. Returns error if not found.
|
||||
func (r *ShareRepo) Delete(ctx context.Context, id string, createdBy string) error {
|
||||
result := r.data.DB(ctx).
|
||||
Where("id = ? AND created_by = ?", id, createdBy).
|
||||
Delete(&ShareLinkPO{})
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("failed to delete share link: %w", result.Error)
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("share link not found or not owned by user")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IncrementDownloadCount atomically increments the download count for the given token.
|
||||
func (r *ShareRepo) IncrementDownloadCount(ctx context.Context, token string) error {
|
||||
result := r.data.DB(ctx).Model(&ShareLinkPO{}).
|
||||
Where("token = ?", token).
|
||||
UpdateColumn("download_count", gorm.Expr("download_count + 1"))
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("failed to increment download count: %w", result.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListByResource retrieves all share links for a given resource, ordered by created_at descending.
|
||||
func (r *ShareRepo) ListByResource(ctx context.Context, resourceType string, resourceID string) ([]ShareLinkPO, error) {
|
||||
var links []ShareLinkPO
|
||||
err := r.data.DB(ctx).
|
||||
Where("resource_type = ? AND resource_id = ?", resourceType, resourceID).
|
||||
Order("created_at DESC").
|
||||
Find(&links).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list share links: %w", err)
|
||||
}
|
||||
return links, nil
|
||||
}
|
||||
|
||||
func (r *ShareRepo) queryOne(ctx context.Context, query string, args ...interface{}) (*ShareLinkPO, error) {
|
||||
var share ShareLinkPO
|
||||
err := r.data.DB(ctx).Where(query, args...).First(&share).Error
|
||||
if err != nil {
|
||||
if isRecordNotFound(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to query share link: %w", err)
|
||||
}
|
||||
return &share, nil
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type FileMeta struct {
|
||||
ID string
|
||||
FolderID string
|
||||
Name string
|
||||
S3Key string
|
||||
S3Bucket string
|
||||
Size int64
|
||||
ContentType string
|
||||
OwnerID string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type Folder struct {
|
||||
ID string
|
||||
ParentID *string
|
||||
Name string
|
||||
OwnerID string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type FolderWithChildren struct {
|
||||
Folder Folder
|
||||
SubFolders []Folder
|
||||
Files []FileMeta
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type ShareLink struct {
|
||||
ID string
|
||||
ResourceType string
|
||||
ResourceID string
|
||||
Token string
|
||||
Password *string
|
||||
ExpiresAt *time.Time
|
||||
DownloadCount int
|
||||
MaxDownloads *int
|
||||
CreatedBy string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type ShareInfo struct {
|
||||
Token string
|
||||
ResourceType string
|
||||
FileName string
|
||||
FileSize int64
|
||||
HasPassword bool
|
||||
ExpiresAt *time.Time
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"rag/file-system/internal/domain/model"
|
||||
)
|
||||
|
||||
type FileMetaRepository interface {
|
||||
Create(ctx context.Context, file *model.FileMeta) error
|
||||
GetByID(ctx context.Context, id string) (*model.FileMeta, error)
|
||||
GetByFolder(ctx context.Context, folderID string) ([]model.FileMeta, error)
|
||||
Move(ctx context.Context, fileID string, targetFolderID string, ownerID string) error
|
||||
Delete(ctx context.Context, id string, ownerID string) error
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"rag/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
|
||||
|
||||
// File listing with pagination
|
||||
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)
|
||||
|
||||
// File content retrieval for text preview
|
||||
GetFileContent(ctx context.Context, bucketName string, objectKey string) (string, error)
|
||||
|
||||
// File deletion
|
||||
DeleteFile(ctx context.Context, bucketName string, objectKey string) error
|
||||
|
||||
// Multipart upload
|
||||
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
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"rag/file-system/internal/domain/model"
|
||||
)
|
||||
|
||||
type FolderRepository interface {
|
||||
Create(ctx context.Context, folder *model.Folder) error
|
||||
GetByID(ctx context.Context, id string) (*model.Folder, error)
|
||||
GetWithChildren(ctx context.Context, id string, ownerID string) (*model.FolderWithChildren, error)
|
||||
GetTree(ctx context.Context, ownerID string) ([]model.Folder, error)
|
||||
Update(ctx context.Context, folder *model.Folder) error
|
||||
Delete(ctx context.Context, id string, ownerID string) error
|
||||
GetDescendantFileS3Keys(ctx context.Context, id string, ownerID string) ([]model.FileMeta, error)
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"rag/file-system/internal/domain/model"
|
||||
)
|
||||
|
||||
type ShareRepository interface {
|
||||
Create(ctx context.Context, share *model.ShareLink) error
|
||||
GetByToken(ctx context.Context, token string) (*model.ShareLink, error)
|
||||
GetByID(ctx context.Context, id string) (*model.ShareLink, error)
|
||||
Delete(ctx context.Context, id string, createdBy string) error
|
||||
IncrementDownloadCount(ctx context.Context, token string) error
|
||||
ListByResource(ctx context.Context, resourceType string, resourceID string) ([]model.ShareLink, error)
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
|
||||
"rag/file-system/internal/common"
|
||||
)
|
||||
|
||||
func NewPostgresDB(databaseURL string) (*sql.DB, error) {
|
||||
db, err := sql.Open("pgx", databaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
db.SetMaxOpenConns(25)
|
||||
db.SetMaxIdleConns(5)
|
||||
|
||||
common.Logger.Info("connected to PostgreSQL")
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func RunMigrations(db *sql.DB) error {
|
||||
migrations := []string{
|
||||
`CREATE TABLE IF NOT EXISTS folders (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
parent_id UUID REFERENCES folders(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
owner_id VARCHAR(36) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE(parent_id, name, owner_id)
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_folders_parent ON folders(parent_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_folders_owner ON folders(owner_id)`,
|
||||
|
||||
`CREATE TABLE IF NOT EXISTS files (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
folder_id UUID REFERENCES folders(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
s3_key VARCHAR(512) NOT NULL,
|
||||
s3_bucket VARCHAR(255) NOT NULL,
|
||||
size BIGINT NOT NULL DEFAULT 0,
|
||||
content_type VARCHAR(255) NOT NULL DEFAULT 'application/octet-stream',
|
||||
owner_id VARCHAR(36) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_files_folder ON files(folder_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_files_owner ON files(owner_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_files_s3_key ON files(s3_key)`,
|
||||
|
||||
`CREATE TABLE IF NOT EXISTS share_links (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
resource_type VARCHAR(10) NOT NULL,
|
||||
resource_id UUID NOT NULL,
|
||||
token VARCHAR(32) NOT NULL UNIQUE,
|
||||
password VARCHAR(255),
|
||||
expires_at TIMESTAMPTZ,
|
||||
download_count INT NOT NULL DEFAULT 0,
|
||||
max_downloads INT,
|
||||
created_by VARCHAR(36) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_share_token ON share_links(token)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_share_resource ON share_links(resource_type, resource_id)`,
|
||||
}
|
||||
|
||||
for _, m := range migrations {
|
||||
if _, err := db.Exec(m); err != nil {
|
||||
return fmt.Errorf("migration failed: %w\nSQL: %s", err, m)
|
||||
}
|
||||
}
|
||||
|
||||
common.Logger.Info("database migrations completed")
|
||||
return nil
|
||||
}
|
||||
@ -1,101 +0,0 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"rag/file-system/api/proto"
|
||||
"rag/file-system/internal/common"
|
||||
"time"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
type TokenInfo struct {
|
||||
Valid bool
|
||||
UserId string
|
||||
Username string
|
||||
Email string
|
||||
Roles []string
|
||||
Permissions []string
|
||||
ExpiresAt int64
|
||||
}
|
||||
|
||||
type AuthClient struct {
|
||||
conn *grpc.ClientConn
|
||||
client proto.AuthServiceClient
|
||||
cache *cache.Cache
|
||||
}
|
||||
|
||||
func NewAuthClient(addr string) (*AuthClient, error) {
|
||||
conn, err := grpc.NewClient(addr,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gRPC connect failed: %w", err)
|
||||
}
|
||||
|
||||
common.Logger.Info("gRPC auth client connected", "addr", addr)
|
||||
|
||||
return &AuthClient{
|
||||
conn: conn,
|
||||
client: proto.NewAuthServiceClient(conn),
|
||||
cache: cache.New(2*time.Minute, 5*time.Minute),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *AuthClient) ValidateToken(ctx context.Context, token string) (*TokenInfo, error) {
|
||||
if cached, ok := a.cache.Get(token); ok {
|
||||
return cached.(*TokenInfo), nil
|
||||
}
|
||||
|
||||
resp, err := a.client.ValidateToken(ctx, &proto.ValidateTokenRequest{Token: token})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gRPC ValidateToken failed: %w", err)
|
||||
}
|
||||
|
||||
info := &TokenInfo{
|
||||
Valid: resp.Valid,
|
||||
UserId: resp.UserId,
|
||||
Username: resp.Username,
|
||||
Email: resp.Email,
|
||||
Roles: resp.Roles,
|
||||
Permissions: resp.Permissions,
|
||||
ExpiresAt: resp.ExpiresAt,
|
||||
}
|
||||
|
||||
if info.Valid {
|
||||
ttl := a.cacheTTL(resp.ExpiresAt)
|
||||
a.cache.Set(token, info, ttl)
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (a *AuthClient) HasPermission(token, permission string) (bool, error) {
|
||||
resp, err := a.client.CheckPermission(context.Background(), &proto.CheckPermissionRequest{
|
||||
Token: token,
|
||||
Permission: permission,
|
||||
})
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("gRPC CheckPermission failed: %w", err)
|
||||
}
|
||||
return resp.Allowed, nil
|
||||
}
|
||||
|
||||
func (a *AuthClient) Close() error {
|
||||
return a.conn.Close()
|
||||
}
|
||||
|
||||
func (a *AuthClient) cacheTTL(expiresAt int64) time.Duration {
|
||||
expires := time.Unix(expiresAt, 0)
|
||||
ttl := time.Until(expires) - 30*time.Second
|
||||
if ttl < time.Minute {
|
||||
ttl = time.Minute
|
||||
}
|
||||
if ttl > 2*time.Minute {
|
||||
ttl = 2 * time.Minute
|
||||
}
|
||||
return ttl
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
package mediator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// --- test request / response types ---
|
||||
|
||||
type testRequest struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
type testResponse struct {
|
||||
Result string
|
||||
}
|
||||
|
||||
// --- stub handler ---
|
||||
|
||||
type stubHandler struct {
|
||||
response testResponse
|
||||
err error
|
||||
}
|
||||
|
||||
func (h *stubHandler) Handle(_ context.Context, _ testRequest) (testResponse, error) {
|
||||
return h.response, h.err
|
||||
}
|
||||
|
||||
// --- tests ---
|
||||
|
||||
func TestRegisterAndSend_Success(t *testing.T) {
|
||||
m := NewMediator()
|
||||
Register[testRequest, testResponse](m, &stubHandler{
|
||||
response: testResponse{Result: "ok"},
|
||||
})
|
||||
|
||||
resp, err := Send[testRequest, testResponse](m, context.Background(), testRequest{Message: "hello"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if resp.Result != "ok" {
|
||||
t.Errorf("expected result %q, got %q", "ok", resp.Result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSend_UnregisteredType(t *testing.T) {
|
||||
m := NewMediator()
|
||||
|
||||
_, err := Send[testRequest, testResponse](m, context.Background(), testRequest{Message: "hello"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unregistered type, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSend_HandlerError(t *testing.T) {
|
||||
m := NewMediator()
|
||||
expectedErr := errors.New("something went wrong")
|
||||
Register[testRequest, testResponse](m, &stubHandler{
|
||||
err: expectedErr,
|
||||
})
|
||||
|
||||
_, err := Send[testRequest, testResponse](m, context.Background(), testRequest{Message: "hello"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error from handler, got nil")
|
||||
}
|
||||
if err.Error() != expectedErr.Error() {
|
||||
t.Errorf("expected error %q, got %q", expectedErr.Error(), err.Error())
|
||||
}
|
||||
}
|
||||
@ -1,91 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"rag/file-system/internal/domain/model"
|
||||
)
|
||||
|
||||
type FileMetaRepoImpl struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewFileMetaRepository(db *sql.DB) *FileMetaRepoImpl {
|
||||
return &FileMetaRepoImpl{db: db}
|
||||
}
|
||||
|
||||
func (r *FileMetaRepoImpl) Create(ctx context.Context, file *model.FileMeta) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`INSERT INTO files (id, folder_id, name, s3_key, s3_bucket, size, content_type, owner_id, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
||||
file.ID, file.FolderID, file.Name, file.S3Key, file.S3Bucket, file.Size, file.ContentType, file.OwnerID, file.CreatedAt, file.UpdatedAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file meta: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *FileMetaRepoImpl) GetByID(ctx context.Context, id string) (*model.FileMeta, error) {
|
||||
var f model.FileMeta
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
`SELECT id, folder_id, name, s3_key, s3_bucket, size, content_type, owner_id, created_at, updated_at FROM files WHERE id = $1`, id).
|
||||
Scan(&f.ID, &f.FolderID, &f.Name, &f.S3Key, &f.S3Bucket, &f.Size, &f.ContentType, &f.OwnerID, &f.CreatedAt, &f.UpdatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get file meta: %w", err)
|
||||
}
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
func (r *FileMetaRepoImpl) GetByFolder(ctx context.Context, folderID string) ([]model.FileMeta, error) {
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
`SELECT id, folder_id, name, s3_key, s3_bucket, size, content_type, owner_id, created_at, updated_at FROM files WHERE folder_id = $1 ORDER BY name`, folderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get files by folder: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return scanFiles(rows)
|
||||
}
|
||||
|
||||
func (r *FileMetaRepoImpl) Move(ctx context.Context, fileID string, targetFolderID string, ownerID string) error {
|
||||
result, err := r.db.ExecContext(ctx,
|
||||
`UPDATE files SET folder_id = $1, updated_at = now() WHERE id = $2 AND owner_id = $3`,
|
||||
targetFolderID, fileID, ownerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to move file: %w", err)
|
||||
}
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("file not found or not owned by user")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *FileMetaRepoImpl) Delete(ctx context.Context, id string, ownerID string) error {
|
||||
result, err := r.db.ExecContext(ctx, `DELETE FROM files WHERE id = $1 AND owner_id = $2`, id, ownerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete file meta: %w", err)
|
||||
}
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("file not found or not owned by user")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func scanFiles(rows *sql.Rows) ([]model.FileMeta, error) {
|
||||
var files []model.FileMeta
|
||||
for rows.Next() {
|
||||
var f model.FileMeta
|
||||
if err := rows.Scan(&f.ID, &f.FolderID, &f.Name, &f.S3Key, &f.S3Bucket, &f.Size, &f.ContentType, &f.OwnerID, &f.CreatedAt, &f.UpdatedAt); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan file: %w", err)
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
return files, rows.Err()
|
||||
}
|
||||
@ -1,154 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"rag/file-system/internal/domain/model"
|
||||
)
|
||||
|
||||
type FolderRepoImpl struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewFolderRepository(db *sql.DB) *FolderRepoImpl {
|
||||
return &FolderRepoImpl{db: db}
|
||||
}
|
||||
|
||||
func (r *FolderRepoImpl) Create(ctx context.Context, folder *model.Folder) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`INSERT INTO folders (id, parent_id, name, owner_id, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
folder.ID, folder.ParentID, folder.Name, folder.OwnerID, folder.CreatedAt, folder.UpdatedAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create folder: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *FolderRepoImpl) GetByID(ctx context.Context, id string) (*model.Folder, error) {
|
||||
var f model.Folder
|
||||
var parentID sql.NullString
|
||||
err := r.db.QueryRowContext(ctx,
|
||||
`SELECT id, parent_id, name, owner_id, created_at, updated_at FROM folders WHERE id = $1`, id).
|
||||
Scan(&f.ID, &parentID, &f.Name, &f.OwnerID, &f.CreatedAt, &f.UpdatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get folder: %w", err)
|
||||
}
|
||||
if parentID.Valid {
|
||||
f.ParentID = &parentID.String
|
||||
}
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
func (r *FolderRepoImpl) GetWithChildren(ctx context.Context, id string, ownerID string) (*model.FolderWithChildren, error) {
|
||||
folder, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if folder == nil || folder.OwnerID != ownerID {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
subFolders, err := r.queryFolders(ctx,
|
||||
`SELECT id, parent_id, name, owner_id, created_at, updated_at FROM folders WHERE parent_id = $1 AND owner_id = $2 ORDER BY name`, id, ownerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files, err := r.queryFiles(ctx,
|
||||
`SELECT id, folder_id, name, s3_key, s3_bucket, size, content_type, owner_id, created_at, updated_at FROM files WHERE folder_id = $1 AND owner_id = $2 ORDER BY name`, id, ownerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.FolderWithChildren{
|
||||
Folder: *folder,
|
||||
SubFolders: subFolders,
|
||||
Files: files,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *FolderRepoImpl) GetTree(ctx context.Context, ownerID string) ([]model.Folder, error) {
|
||||
return r.queryFolders(ctx,
|
||||
`SELECT id, parent_id, name, owner_id, created_at, updated_at FROM folders WHERE owner_id = $1 ORDER BY name`, ownerID)
|
||||
}
|
||||
|
||||
func (r *FolderRepoImpl) Update(ctx context.Context, folder *model.Folder) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`UPDATE folders SET name = $1, updated_at = $2 WHERE id = $3`,
|
||||
folder.Name, folder.UpdatedAt, folder.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update folder: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *FolderRepoImpl) Delete(ctx context.Context, id string, ownerID string) error {
|
||||
result, err := r.db.ExecContext(ctx, `DELETE FROM folders WHERE id = $1 AND owner_id = $2`, id, ownerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete folder: %w", err)
|
||||
}
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("folder not found or not owned by user")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *FolderRepoImpl) GetDescendantFileS3Keys(ctx context.Context, id string, ownerID string) ([]model.FileMeta, error) {
|
||||
query := `
|
||||
WITH RECURSIVE descendants AS (
|
||||
SELECT id FROM folders WHERE id = $1 AND owner_id = $2
|
||||
UNION
|
||||
SELECT f.id FROM folders f INNER JOIN descendants d ON f.parent_id = d.id
|
||||
)
|
||||
SELECT fi.id, fi.folder_id, fi.name, fi.s3_key, fi.s3_bucket, fi.size, fi.content_type, fi.owner_id, fi.created_at, fi.updated_at
|
||||
FROM files fi INNER JOIN descendants d ON fi.folder_id = d.id
|
||||
WHERE fi.owner_id = $2`
|
||||
return r.queryFiles(ctx, query, id, ownerID)
|
||||
}
|
||||
|
||||
func (r *FolderRepoImpl) queryFolders(ctx context.Context, query string, args ...interface{}) ([]model.Folder, error) {
|
||||
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query folders: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var folders []model.Folder
|
||||
for rows.Next() {
|
||||
var f model.Folder
|
||||
var parentID sql.NullString
|
||||
if err := rows.Scan(&f.ID, &parentID, &f.Name, &f.OwnerID, &f.CreatedAt, &f.UpdatedAt); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan folder: %w", err)
|
||||
}
|
||||
if parentID.Valid {
|
||||
f.ParentID = &parentID.String
|
||||
}
|
||||
folders = append(folders, f)
|
||||
}
|
||||
return folders, rows.Err()
|
||||
}
|
||||
|
||||
func (r *FolderRepoImpl) queryFiles(ctx context.Context, query string, args ...interface{}) ([]model.FileMeta, error) {
|
||||
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query files: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var files []model.FileMeta
|
||||
for rows.Next() {
|
||||
var f model.FileMeta
|
||||
if err := rows.Scan(&f.ID, &f.FolderID, &f.Name, &f.S3Key, &f.S3Bucket, &f.Size, &f.ContentType, &f.OwnerID, &f.CreatedAt, &f.UpdatedAt); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan file: %w", err)
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
return files, rows.Err()
|
||||
}
|
||||
@ -1,90 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"rag/file-system/internal/domain/model"
|
||||
)
|
||||
|
||||
type ShareRepoImpl struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewShareRepository(db *sql.DB) *ShareRepoImpl {
|
||||
return &ShareRepoImpl{db: db}
|
||||
}
|
||||
|
||||
func (r *ShareRepoImpl) Create(ctx context.Context, share *model.ShareLink) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
`INSERT INTO share_links (id, resource_type, resource_id, token, password, expires_at, download_count, max_downloads, created_by, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
||||
share.ID, share.ResourceType, share.ResourceID, share.Token, share.Password, share.ExpiresAt,
|
||||
share.DownloadCount, share.MaxDownloads, share.CreatedBy, share.CreatedAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create share link: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ShareRepoImpl) GetByToken(ctx context.Context, token string) (*model.ShareLink, error) {
|
||||
return r.queryOne(ctx, `SELECT id, resource_type, resource_id, token, password, expires_at, download_count, max_downloads, created_by, created_at FROM share_links WHERE token = $1`, token)
|
||||
}
|
||||
|
||||
func (r *ShareRepoImpl) GetByID(ctx context.Context, id string) (*model.ShareLink, error) {
|
||||
return r.queryOne(ctx, `SELECT id, resource_type, resource_id, token, password, expires_at, download_count, max_downloads, created_by, created_at FROM share_links WHERE id = $1`, id)
|
||||
}
|
||||
|
||||
func (r *ShareRepoImpl) Delete(ctx context.Context, id string, createdBy string) error {
|
||||
result, err := r.db.ExecContext(ctx, `DELETE FROM share_links WHERE id = $1 AND created_by = $2`, id, createdBy)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete share link: %w", err)
|
||||
}
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("share link not found or not owned by user")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ShareRepoImpl) IncrementDownloadCount(ctx context.Context, token string) error {
|
||||
_, err := r.db.ExecContext(ctx, `UPDATE share_links SET download_count = download_count + 1 WHERE token = $1`, token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to increment download count: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ShareRepoImpl) ListByResource(ctx context.Context, resourceType string, resourceID string) ([]model.ShareLink, error) {
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
`SELECT id, resource_type, resource_id, token, password, expires_at, download_count, max_downloads, created_by, created_at FROM share_links WHERE resource_type = $1 AND resource_id = $2 ORDER BY created_at DESC`,
|
||||
resourceType, resourceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list share links: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var links []model.ShareLink
|
||||
for rows.Next() {
|
||||
var s model.ShareLink
|
||||
if err := rows.Scan(&s.ID, &s.ResourceType, &s.ResourceID, &s.Token, &s.Password, &s.ExpiresAt, &s.DownloadCount, &s.MaxDownloads, &s.CreatedBy, &s.CreatedAt); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan share link: %w", err)
|
||||
}
|
||||
links = append(links, s)
|
||||
}
|
||||
return links, rows.Err()
|
||||
}
|
||||
|
||||
func (r *ShareRepoImpl) queryOne(ctx context.Context, query string, args ...interface{}) (*model.ShareLink, error) {
|
||||
var s model.ShareLink
|
||||
err := r.db.QueryRowContext(ctx, query, args...).
|
||||
Scan(&s.ID, &s.ResourceType, &s.ResourceID, &s.Token, &s.Password, &s.ExpiresAt, &s.DownloadCount, &s.MaxDownloads, &s.CreatedBy, &s.CreatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query share link: %w", err)
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"rag/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 {
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RustFSClient) S3Client() *s3.Client {
|
||||
return c.client
|
||||
}
|
||||
|
||||
func (c *RustFSClient) PresignClient() *s3.PresignClient {
|
||||
return c.presignClient
|
||||
}
|
||||
@ -1,225 +0,0 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"rag/file-system/internal/common"
|
||||
"rag/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"
|
||||
)
|
||||
|
||||
const maxContentPreviewSize = 10 * 1024 * 1024 // 10MB
|
||||
|
||||
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.S3Client().PutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
Body: data,
|
||||
})
|
||||
return common.WrapS3Error(err)
|
||||
}
|
||||
|
||||
func (r *S3FileRepository) DownloadFile(ctx context.Context, bucketName string, objectKey string) (io.ReadCloser, error) {
|
||||
resp, err := r.client.S3Client().GetObject(ctx, &s3.GetObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, common.WrapS3Error(err)
|
||||
}
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
func (r *S3FileRepository) ListBuckets(ctx context.Context) ([]string, error) {
|
||||
resp, err := r.client.S3Client().ListBuckets(ctx, &s3.ListBucketsInput{})
|
||||
if err != nil {
|
||||
return nil, common.WrapS3Error(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.S3Client().CreateBucket(ctx, &s3.CreateBucketInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
return common.WrapS3Error(err)
|
||||
}
|
||||
|
||||
func (r *S3FileRepository) DeleteBucket(ctx context.Context, bucketName string) error {
|
||||
_, err := r.client.S3Client().DeleteBucket(ctx, &s3.DeleteBucketInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
return common.WrapS3Error(err)
|
||||
}
|
||||
|
||||
// GetFileContent retrieves text file content for preview (e.g., Markdown files)
|
||||
func (r *S3FileRepository) GetFileContent(ctx context.Context, bucketName string, objectKey string) (string, error) {
|
||||
resp, err := r.client.S3Client().GetObject(ctx, &s3.GetObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
if err != nil {
|
||||
return "", common.WrapS3Error(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, maxContentPreviewSize))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if int64(len(data)) >= maxContentPreviewSize {
|
||||
return "", common.NewBusinessException("file too large for content preview (max 10MB)")
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// DeleteFile removes a file from the bucket
|
||||
func (r *S3FileRepository) DeleteFile(ctx context.Context, bucketName string, objectKey string) error {
|
||||
_, err := r.client.S3Client().DeleteObject(ctx, &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
return common.WrapS3Error(err)
|
||||
}
|
||||
|
||||
// ListObjectsV2 lists files with pagination support
|
||||
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.S3Client().ListObjectsV2(ctx, input)
|
||||
if err != nil {
|
||||
return nil, common.WrapS3Error(err)
|
||||
}
|
||||
|
||||
files := make([]repository.FileInfo, 0, len(resp.Contents))
|
||||
for _, obj := range resp.Contents {
|
||||
if obj.Key == nil || obj.Size == nil || obj.LastModified == nil || obj.ETag == nil {
|
||||
continue
|
||||
}
|
||||
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 generates a presigned URL for temporary file access
|
||||
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 "", common.WrapS3Error(err)
|
||||
}
|
||||
return presignResult.URL, nil
|
||||
}
|
||||
|
||||
// CreateMultipartUpload initializes a multipart upload session
|
||||
func (r *S3FileRepository) CreateMultipartUpload(ctx context.Context, bucketName string, objectKey string) (string, error) {
|
||||
resp, err := r.client.S3Client().CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
if err != nil {
|
||||
return "", common.WrapS3Error(err)
|
||||
}
|
||||
if resp.UploadId == nil {
|
||||
return "", common.NewBusinessException("failed to initialize multipart upload")
|
||||
}
|
||||
return *resp.UploadId, nil
|
||||
}
|
||||
|
||||
// UploadPart uploads a single part of a multipart upload
|
||||
func (r *S3FileRepository) UploadPart(ctx context.Context, bucketName string, objectKey string, uploadId string, partNumber int32, data io.Reader) (string, error) {
|
||||
resp, err := r.client.S3Client().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 "", common.WrapS3Error(err)
|
||||
}
|
||||
if resp.ETag == nil {
|
||||
return "", common.NewBusinessException("failed to upload part")
|
||||
}
|
||||
return *resp.ETag, nil
|
||||
}
|
||||
|
||||
// CompleteMultipartUpload assembles all parts to complete the upload
|
||||
func (r *S3FileRepository) CompleteMultipartUpload(ctx context.Context, bucketName string, objectKey string, uploadId string, parts []common.Part) (string, error) {
|
||||
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.S3Client().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 "", common.WrapS3Error(err)
|
||||
}
|
||||
if resp.Location == nil {
|
||||
return "", common.NewBusinessException("failed to complete multipart upload")
|
||||
}
|
||||
return *resp.Location, nil
|
||||
}
|
||||
|
||||
// AbortMultipartUpload cancels an in-progress multipart upload
|
||||
func (r *S3FileRepository) AbortMultipartUpload(ctx context.Context, bucketName string, objectKey string, uploadId string) error {
|
||||
_, err := r.client.S3Client().AbortMultipartUpload(ctx, &s3.AbortMultipartUploadInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(objectKey),
|
||||
UploadId: aws.String(uploadId),
|
||||
})
|
||||
return common.WrapS3Error(err)
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const API_KEY_HEADER = "X-API-Key"
|
||||
|
||||
// AuthMiddleware 验证API密钥的中间件
|
||||
func AuthMiddleware(apiKey string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 从请求头中获取API密钥
|
||||
key := c.GetHeader(API_KEY_HEADER)
|
||||
|
||||
// 验证密钥是否正确
|
||||
if key != apiKey {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": http.StatusUnauthorized,
|
||||
"message": "未授权:请在请求头中提供有效的API密钥",
|
||||
"error": "Missing or invalid API key",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 密钥验证通过,继续处理请求
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gin.SetMode(gin.TestMode)
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_MissingKey(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.Use(AuthMiddleware("secret-key"))
|
||||
r.GET("/test", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_WrongKey(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.Use(AuthMiddleware("secret-key"))
|
||||
r.GET("/test", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
||||
req.Header.Set("X-API-Key", "wrong-key")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_CorrectKey(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.Use(AuthMiddleware("secret-key"))
|
||||
r.GET("/test", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
||||
req.Header.Set("X-API-Key", "secret-key")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
@ -1,80 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"rag/file-system/internal/infrastructure/grpc"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderAuthorization = "Authorization"
|
||||
BearerPrefix = "Bearer "
|
||||
|
||||
ContextKeyUserID = "user_id"
|
||||
ContextKeyUsername = "username"
|
||||
ContextKeyEmail = "email"
|
||||
ContextKeyRoles = "roles"
|
||||
ContextKeyPermissions = "permissions"
|
||||
)
|
||||
|
||||
func JWTAuthMiddleware(authClient *grpc.AuthClient) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader(HeaderAuthorization)
|
||||
if authHeader == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": http.StatusUnauthorized,
|
||||
"message": "未授权:请提供 Bearer Token",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(authHeader, BearerPrefix) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": http.StatusUnauthorized,
|
||||
"message": "未授权:Token 格式错误,需要 Bearer <token>",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
token := strings.TrimPrefix(authHeader, BearerPrefix)
|
||||
if token == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": http.StatusUnauthorized,
|
||||
"message": "未授权:Token 不能为空",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
info, err := authClient.ValidateToken(c.Request.Context(), token)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": http.StatusUnauthorized,
|
||||
"message": "Token 验证失败",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if !info.Valid {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": http.StatusUnauthorized,
|
||||
"message": "Token 无效或已过期",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set(ContextKeyUserID, info.UserId)
|
||||
c.Set(ContextKeyUsername, info.Username)
|
||||
c.Set(ContextKeyEmail, info.Email)
|
||||
c.Set(ContextKeyRoles, info.Roles)
|
||||
c.Set(ContextKeyPermissions, info.Permissions)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"rag/file-system/internal/common"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func LoggingMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
c.Next()
|
||||
duration := time.Since(start)
|
||||
|
||||
common.Logger.Info("request",
|
||||
"method", c.Request.Method,
|
||||
"path", c.Request.URL.Path,
|
||||
"status", c.Writer.Status(),
|
||||
"duration_ms", duration.Milliseconds(),
|
||||
"request_id", c.GetString("request_id"),
|
||||
"client_ip", c.ClientIP(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
func RateLimitMiddleware(rps float64, burst int) gin.HandlerFunc {
|
||||
var limiters sync.Map
|
||||
|
||||
return func(c *gin.Context) {
|
||||
key := c.ClientIP()
|
||||
l, _ := limiters.LoadOrStore(key, rate.NewLimiter(rate.Limit(rps), burst))
|
||||
if !l.(*rate.Limiter).Allow() {
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{"error": "too many requests"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RequestIDMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
b := make([]byte, 8)
|
||||
rand.Read(b)
|
||||
requestID = hex.EncodeToString(b)
|
||||
}
|
||||
c.Set("request_id", requestID)
|
||||
c.Header("X-Request-ID", requestID)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gin.SetMode(gin.TestMode)
|
||||
}
|
||||
|
||||
func TestRequestIDMiddleware_GeneratesID(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.Use(RequestIDMiddleware())
|
||||
r.GET("/test", func(c *gin.Context) {
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
gotID := w.Header().Get("X-Request-ID")
|
||||
if gotID == "" {
|
||||
t.Error("expected X-Request-ID to be generated, got empty string")
|
||||
}
|
||||
if len(gotID) != 16 {
|
||||
t.Errorf("expected 16-char hex ID, got %d chars: %q", len(gotID), gotID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestIDMiddleware_PreservesExisting(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.Use(RequestIDMiddleware())
|
||||
r.GET("/test", func(c *gin.Context) {
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
||||
req.Header.Set("X-Request-ID", "my-custom-id")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
gotID := w.Header().Get("X-Request-ID")
|
||||
if gotID != "my-custom-id" {
|
||||
t.Errorf("expected X-Request-ID to be preserved as %q, got %q", "my-custom-id", gotID)
|
||||
}
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), timeout)
|
||||
defer cancel()
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
32
internal/pkg/s3errors/s3errors.go
Normal file
32
internal/pkg/s3errors/s3errors.go
Normal file
@ -0,0 +1,32 @@
|
||||
package s3errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
)
|
||||
|
||||
func Wrap(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var (
|
||||
noSuchBucket *types.NoSuchBucket
|
||||
noSuchKey *types.NoSuchKey
|
||||
notFound *types.NotFound
|
||||
)
|
||||
if errors.As(err, &noSuchBucket) || errors.As(err, &noSuchKey) || errors.As(err, ¬Found) {
|
||||
return fmt.Errorf("resource not found: %w", err)
|
||||
}
|
||||
return fmt.Errorf("storage operation failed: %w", err)
|
||||
}
|
||||
|
||||
func IsNotFound(err error) bool {
|
||||
var (
|
||||
noSuchBucket *types.NoSuchBucket
|
||||
noSuchKey *types.NoSuchKey
|
||||
notFound *types.NotFound
|
||||
)
|
||||
return errors.As(err, &noSuchBucket) || errors.As(err, &noSuchKey) || errors.As(err, ¬Found)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user