Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ FROM ${NODE_IMAGE} AS frontend-builder
WORKDIR /app/frontend

# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
RUN corepack enable && corepack prepare pnpm@9.15.9 --activate

# Install dependencies first (better caching)
COPY frontend/package.json frontend/pnpm-lock.yaml ./
COPY frontend/pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile

# Copy frontend source and build
Expand Down
54 changes: 42 additions & 12 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,24 +1,54 @@
FROM golang:1.26.3-alpine
ARG GOLANG_IMAGE=golang:1.26.3-alpine
ARG ALPINE_IMAGE=alpine:3.21
ARG POSTGRES_IMAGE=postgres:18-alpine
ARG GOPROXY=https://goproxy.cn,direct
ARG GOSUMDB=sum.golang.google.cn

FROM ${GOLANG_IMAGE} AS builder

ARG GOPROXY
ARG GOSUMDB

ENV GOPROXY=${GOPROXY}
ENV GOSUMDB=${GOSUMDB}

WORKDIR /app

# 安装必要的工具
RUN apk add --no-cache git
RUN apk add --no-cache git ca-certificates tzdata

# 复制go.mod和go.sum
COPY go.mod go.sum ./

# 下载依赖
RUN go mod download

# 复制源代码
COPY . .

# 构建应用
RUN go build -o main ./cmd/server/
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags="-s -w" \
-trimpath \
-o /app/sub2api \
./cmd/server

FROM ${POSTGRES_IMAGE} AS pg-client

FROM ${ALPINE_IMAGE}

RUN apk add --no-cache \
ca-certificates \
tzdata \
libpq \
zstd-libs \
lz4-libs \
krb5-libs \
libldap \
libedit \
&& rm -rf /var/cache/apk/*

COPY --from=pg-client /usr/local/bin/pg_dump /usr/local/bin/pg_dump
COPY --from=pg-client /usr/local/bin/psql /usr/local/bin/psql
COPY --from=pg-client /usr/local/lib/libpq.so.5* /usr/local/lib/
COPY --from=builder /app/sub2api /app/sub2api

WORKDIR /app

# 暴露端口
EXPOSE 8080

# 运行应用
CMD ["./main"]
CMD ["/app/sub2api"]
18 changes: 18 additions & 0 deletions backend/internal/handler/admin/backup_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,24 @@ func (h *BackupHandler) GetDownloadURL(c *gin.Context) {
response.Success(c, gin.H{"url": url})
}

func (h *BackupHandler) PreviewImportBackups(c *gin.Context) {
preview, err := h.backupService.PreviewImportBackups(c.Request.Context())
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, preview)
}

func (h *BackupHandler) ImportMissingBackups(c *gin.Context) {
result, err := h.backupService.ImportMissingBackups(c.Request.Context())
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, result)
}

// ─── 恢复操作(需要重新输入管理员密码) ───

type RestoreBackupRequest struct {
Expand Down
16 changes: 16 additions & 0 deletions backend/internal/repository/backup_pg_dumper.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,19 @@ func NewPgDumper(cfg *config.Config) service.DBDumper {
return &PgDumper{cfg: &cfg.Database}
}

func requirePostgresClient(name string) error {
if _, err := exec.LookPath(name); err != nil {
return fmt.Errorf("%s not found in PATH; backup/restore requires PostgreSQL client tools in the runtime image: %w", name, err)
}
return nil
}

// Dump executes pg_dump and returns a streaming reader of the output
func (d *PgDumper) Dump(ctx context.Context) (io.ReadCloser, error) {
if err := requirePostgresClient("pg_dump"); err != nil {
return nil, err
}

args := []string{
"-h", d.cfg.Host,
"-p", fmt.Sprintf("%d", d.cfg.Port),
Expand Down Expand Up @@ -56,11 +67,16 @@ func (d *PgDumper) Dump(ctx context.Context) (io.ReadCloser, error) {

// Restore executes psql to restore from a streaming reader
func (d *PgDumper) Restore(ctx context.Context, data io.Reader) error {
if err := requirePostgresClient("psql"); err != nil {
return err
}

args := []string{
"-h", d.cfg.Host,
"-p", fmt.Sprintf("%d", d.cfg.Port),
"-U", d.cfg.User,
"-d", d.cfg.DBName,
"-v", "ON_ERROR_STOP=1",
"--single-transaction",
}

Expand Down
34 changes: 34 additions & 0 deletions backend/internal/repository/backup_s3_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,40 @@ func (s *S3BackupStore) Download(ctx context.Context, key string) (io.ReadCloser
return result.Body, nil
}

func (s *S3BackupStore) List(ctx context.Context, prefix string) ([]service.BackupObjectInfo, error) {
input := &s3.ListObjectsV2Input{
Bucket: &s.bucket,
}
if prefix != "" {
input.Prefix = aws.String(prefix)
}

paginator := s3.NewListObjectsV2Paginator(s.client, input)
objects := make([]service.BackupObjectInfo, 0)

for paginator.HasMorePages() {
page, err := paginator.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("S3 ListObjectsV2: %w", err)
}
for _, object := range page.Contents {
if object.Key == nil {
continue
}
info := service.BackupObjectInfo{
Key: aws.ToString(object.Key),
SizeBytes: aws.ToInt64(object.Size),
}
if object.LastModified != nil {
info.LastModified = object.LastModified.Format(time.RFC3339)
}
objects = append(objects, info)
}
}

return objects, nil
}

func (s *S3BackupStore) Delete(ctx context.Context, key string) error {
_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &s.bucket,
Expand Down
2 changes: 2 additions & 0 deletions backend/internal/server/routes/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,8 @@ func registerBackupRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
// 备份操作
backup.POST("", h.Admin.Backup.CreateBackup)
backup.GET("", h.Admin.Backup.ListBackups)
backup.GET("/import-preview", h.Admin.Backup.PreviewImportBackups)
backup.POST("/import", h.Admin.Backup.ImportMissingBackups)
backup.GET("/:id", h.Admin.Backup.GetBackup)
backup.DELETE("/:id", h.Admin.Backup.DeleteBackup)
backup.GET("/:id/download-url", h.Admin.Backup.GetDownloadURL)
Expand Down
Loading
Loading