package main import ( "context" "log" "net/http" "os" "github.com/flatrender/file-svc/internal/db" "github.com/flatrender/file-svc/internal/handlers" "github.com/flatrender/file-svc/internal/middleware" "github.com/flatrender/file-svc/internal/storage" "github.com/gin-gonic/gin" ) func main() { // ── Config from env ─────────────────────────────────────────────────────── dsn := mustEnv("DATABASE_URL") jwtSecret := mustEnv("JWT_SECRET") minioEndpoint := envOr("MINIO_ENDPOINT", "localhost:9000") minioAccessKey := envOr("MINIO_ACCESS_KEY", "minioadmin") minioSecretKey := envOr("MINIO_SECRET_KEY", "minioadmin") minioUseSSL := os.Getenv("MINIO_USE_SSL") == "true" uploadBucket := envOr("MINIO_UPLOAD_BUCKET", "user-uploads") port := envOr("PORT", "8080") // ── Database ────────────────────────────────────────────────────────────── store, err := db.New(dsn) if err != nil { log.Fatalf("db: %v", err) } defer store.Close() if err := store.Ping(context.Background()); err != nil { log.Fatalf("db ping: %v", err) } log.Println("db: connected") // ── MinIO ───────────────────────────────────────────────────────────────── minioClient, err := storage.NewMinioClient(storage.Config{ Endpoint: minioEndpoint, AccessKeyID: minioAccessKey, SecretAccessKey: minioSecretKey, UseSSL: minioUseSSL, }) if err != nil { log.Fatalf("minio: %v", err) } log.Println("minio: connected") // ── Handlers ────────────────────────────────────────────────────────────── fileHandler := handlers.NewFileHandler(store, minioClient, uploadBucket) // ── Router ──────────────────────────────────────────────────────────────── if os.Getenv("GIN_MODE") == "" { gin.SetMode(gin.ReleaseMode) } r := gin.New() r.Use(gin.Logger(), gin.Recovery()) r.GET("/health", func(c *gin.Context) { if err := store.Ping(c.Request.Context()); err != nil { c.JSON(http.StatusServiceUnavailable, gin.H{"status": "unhealthy", "error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "healthy"}) }) v1 := r.Group("/v1", middleware.Auth(jwtSecret)) { // Files v1.GET("/files", fileHandler.ListFiles) v1.GET("/files/:id", fileHandler.GetFile) v1.POST("/files/presigned-upload", fileHandler.PresignedUpload) v1.POST("/files/:id/confirm", fileHandler.ConfirmUpload) v1.DELETE("/files/:id", fileHandler.DeleteFile) v1.GET("/files/:id/download", fileHandler.GetDownloadURL) // Folders v1.GET("/folders", fileHandler.ListFolders) v1.POST("/folders", fileHandler.CreateFolder) v1.DELETE("/folders/:id", fileHandler.DeleteFolder) // Quota v1.GET("/quota", fileHandler.GetQuota) } log.Printf("file-svc listening on :%s", port) if err := r.Run(":" + port); err != nil { log.Fatalf("server: %v", err) } } func mustEnv(key string) string { v := os.Getenv(key) if v == "" { log.Fatalf("required env var %s not set", key) } return v } func envOr(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def }