Compare commits

...

17 Commits

Author SHA1 Message Date
向宁
1cd46bc6db refactor: update config structure and data layer for Watermill CQRS 2026-05-25 20:33:20 +08:00
向宁
11315fd00b feat: wire Watermill CQRS — EventBus in usecases, event handlers, router lifecycle
- Add EventPublisher interface in biz layer for domain event publishing
- Wire EventBusPublisher (Watermill EventBus adapter) into FileUsecase, FolderUsecase, ShareUsecase
- Publish events after UploadFile, DeleteFile, CreateFolder, DeleteFolder, CreateShare
- Implement CQRSHandler with logging event handlers for all 6 event types
- Register event handlers via CQRSBus.RegisterHandlers using Watermill EventProcessor
- Store subscriber and wmLogger in CQRSBus for EventProcessor wiring
- Expose SqlDB() on Data struct for Watermill SQL pub/sub
- Start Watermill router in goroutine alongside Kratos app with graceful close
- Use appContext wrapper struct to pass CQRSBus through Wire DI graph
2026-05-25 13:52:05 +08:00
向宁
3eb1a1839d feat: add JWT auth middleware with public endpoint selector
Add HS256 JWT authentication to both HTTP and gRPC servers using
Kratos jwt middleware with selector to skip auth for public share
endpoints (GetShareInfo, DownloadShare). Wire DI updated to inject
conf.Auth into server constructors.
2026-05-25 13:43:34 +08:00
向宁
95f76bbc70 chore: remove leftover grpc auth client (will be re-integrated as Kratos middleware) 2026-05-25 13:14:52 +08:00
向宁
4b35503b4f docs: rewrite CLAUDE.md for Kratos+Watermill architecture 2026-05-25 13:14:15 +08:00
向宁
80507c0e18 chore: update Dockerfile and docker-compose for Kratos binary 2026-05-25 13:13:00 +08:00
向宁
42addaea7d feat: add Wire DI and rewrite main.go as Kratos app entry point
- Create wire.go with interface bindings (biz interfaces → data implementations)
- Rewrite main.go to use Kratos config loading + Wire-generated initApp
- Remove temporary deps.go pinning file
- Wire generates complete dependency graph: config → data → biz → service → server → app
2026-05-25 13:12:18 +08:00
向宁
dfaead4766 feat: add Watermill CQRS setup with commands, events, and handler stubs 2026-05-25 13:08:55 +08:00
向宁
b9edb7b7de feat: add server layer with HTTP and gRPC transport
Create internal/server package with HTTP and gRPC server constructors
using Kratos transport layer. Includes Wire provider set for DI,
recovery/tracing/logging middleware, and graceful nil-safe config handling.

Fix .gitignore 'server' pattern to only match root-level binary.
2026-05-25 13:05:33 +08:00
向宁
2647314fe7 feat: add service layer implementing proto FileService interface
Implements all 23 FileServiceServer methods: file upload/download/list/
preview/content/delete, multipart upload lifecycle, bucket CRUD, folder
CRUD with tree support, file-to-folder upload, file move, and share link
create/delete/info/download. Includes PO-to-proto conversion helpers.
2026-05-25 13:04:01 +08:00
向宁
4927de90cc feat: add biz layer with usecases for file, bucket, folder, share
Defines repo interfaces in biz (dependency inversion) implemented by data layer.
Removes old domain layer replaced by data layer in previous commit.
2026-05-25 13:00:48 +08:00
向宁
bcd637387a feat: add data layer with GORM models, S3 repo, PG repos
Replace old infrastructure layer with Kratos-style data layer:
- data.go: GORM connection, transaction support, Wire ProviderSet, PO models
- file_repo.go: All 12 S3 operations (upload, download, multipart, presign, buckets)
- folder_repo.go: GORM queries including recursive CTE for descendant files
- file_meta_repo.go: CRUD + move operations for file metadata
- share_repo.go: CRUD + increment download count for share links

Deleted old infrastructure/database, infrastructure/repository, infrastructure/s3.
Kept infrastructure/grpc for later integration.
2026-05-25 12:57:42 +08:00
向宁
7faddfed05 feat: add shared sanitize and s3errors packages 2026-05-25 12:52:39 +08:00
向宁
b9b5838938 chore: add Kratos, Watermill, GORM, Wire dependencies for migration
Add new framework dependencies needed for the Gin-to-Kratos migration:
- go-kratos/kratos/v2 (HTTP/gRPC transport, config, middleware, JWT auth)
- google/wire (compile-time dependency injection)
- ThreeDotsLabs/watermill + watermill-sql/v2 (event-driven CQRS)
- gorm.io/gorm + gorm.io/driver/postgres (PostgreSQL ORM)

The old Gin/Swagger deps remain as direct deps since existing code still
imports them. They will be removed in later migration tasks when code is
rewritten. Blank imports in internal/deps.go ensure go mod tidy keeps
the new deps until actual code imports them directly.
2026-05-25 12:51:26 +08:00
向宁
1cfa43a33c feat: define file service proto API with HTTP+gRPC annotations 2026-05-25 12:45:35 +08:00
向宁
ed47904a85 chore: initialize Kratos project skeleton, add proto config and Makefile
- Add Makefile with api/config/wire/build/run/test/clean targets
- Update buf.yaml with api, internal/conf, and third_party modules
- Update buf.gen.yaml with protobuf, grpc, and grpc-gateway plugins
- Add internal/conf/conf.proto (Kratos config schema: Bootstrap/Server/Data/Auth)
- Generate internal/conf/conf.pb.go via buf
- Add configs/config.yaml with HTTP/gRPC server, Postgres, S3, and auth settings
- Add third_party/google/api proto files (annotations, http)
- Remove old Gin-based layers: internal/api, internal/infrastructure/mediator,
  internal/middleware, internal/common, docs
- Update .gitignore to exclude server binary and bin/
2026-05-25 12:37:45 +08:00
向宁
654b7d9bb6 chore: initial commit before Kratos migration 2026-05-25 12:29:31 +08:00
116 changed files with 9506 additions and 6757 deletions

2
.gitignore vendored
View File

@ -5,6 +5,8 @@
*.so
*.dylib
file-service
/server
bin/
# Test binary, built with `go test -c`
*.test

188
CLAUDE.md
View File

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

View File

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

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

View 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

File diff suppressed because it is too large Load Diff

339
api/file/v1/file.proto Normal file
View 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

File diff suppressed because it is too large Load Diff

979
api/file/v1/file_http.pb.go Normal file
View 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
}

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,10 @@
version: v2
modules:
- path: api/proto
- path: api
- path: third_party
lint:
use:
- DEFAULT
breaking:
use:
- FILE

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

View File

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

View File

@ -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` 常量的值即可。

View File

@ -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 操作需要先登录
- 📝 添加登录文档

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, &notFound) {
return NewNotFoundError("resource not found")
}
return fmt.Errorf("storage operation failed")
}

View File

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

View File

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

566
internal/conf/conf.pb.go Normal file
View 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
View 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
View 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" }

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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, &notFound) {
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, &notFound)
}

Some files were not shown because too many files have changed in this diff Show More