Initial commit: Эфир мессенджер
This commit is contained in:
106
cmd/migrate/main.go
Normal file
106
cmd/migrate/main.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
log.Fatal("Usage: go run cmd/migrate/main.go [up|down]")
|
||||
}
|
||||
|
||||
command := os.Args[1]
|
||||
dbPath := "./messenger.db"
|
||||
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
switch command {
|
||||
case "up":
|
||||
if err := migrateUp(db); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("Migrations completed successfully")
|
||||
case "down":
|
||||
if err := migrateDown(db); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("Rollback completed successfully")
|
||||
default:
|
||||
log.Fatal("Unknown command. Use 'up' or 'down'")
|
||||
}
|
||||
}
|
||||
|
||||
func migrateUp(db *sql.DB) error {
|
||||
files, err := filepath.Glob("migrations/*.up.sql")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
fmt.Printf("Applying migration: %s\n", file)
|
||||
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Разделяем по ";"
|
||||
statements := strings.Split(string(content), ";")
|
||||
|
||||
for _, stmt := range statements {
|
||||
stmt = strings.TrimSpace(stmt)
|
||||
if stmt == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := db.Exec(stmt); err != nil {
|
||||
return fmt.Errorf("failed to execute %s: %v\nStatement: %s", file, err, stmt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateDown(db *sql.DB) error {
|
||||
files, err := filepath.Glob("migrations/*.down.sql")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
fmt.Printf("Rolling back: %s\n", file)
|
||||
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
statements := strings.Split(string(content), ";")
|
||||
|
||||
for _, stmt := range statements {
|
||||
stmt = strings.TrimSpace(stmt)
|
||||
if stmt == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := db.Exec(stmt); err != nil {
|
||||
return fmt.Errorf("failed to execute %s: %v", file, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
183
cmd/server/main.go
Normal file
183
cmd/server/main.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
"runtime"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/rs/cors"
|
||||
|
||||
"messenger/internal/api/handlers"
|
||||
"messenger/internal/api/middleware"
|
||||
"messenger/internal/config"
|
||||
"messenger/internal/crypto"
|
||||
"messenger/internal/pkg/logger"
|
||||
"messenger/internal/repository/sqlite"
|
||||
"messenger/internal/service"
|
||||
"messenger/internal/websocket"
|
||||
)
|
||||
|
||||
|
||||
func main() {
|
||||
// Загружаем конфиг
|
||||
cfg := config.Load()
|
||||
|
||||
// Инициализируем логгер
|
||||
logger.Init(cfg.Environment)
|
||||
|
||||
// Подключаемся к БД
|
||||
db, err := sqlite.NewDB(cfg.DBPath)
|
||||
if err != nil {
|
||||
logger.Error("Failed to connect to database", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Инициализируем шифрование
|
||||
encryptor, err := crypto.NewEncryptor(cfg.EncryptionKey)
|
||||
if err != nil {
|
||||
logger.Error("Failed to initialize encryption", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Создаем репозитории
|
||||
userRepo := sqlite.NewUserRepository(db)
|
||||
profileRepo := sqlite.NewProfileRepository(db)
|
||||
chatRepo := sqlite.NewChatRepository(db)
|
||||
messageRepo := sqlite.NewMessageRepository(db)
|
||||
attachmentRepo := sqlite.NewAttachmentRepository(db)
|
||||
|
||||
// Создаем сервисы
|
||||
authService := service.NewAuthService(userRepo, profileRepo, cfg.JWTSecret, cfg.JWTExpiryHours)
|
||||
userService := service.NewUserService(userRepo, profileRepo)
|
||||
chatService := service.NewChatService(chatRepo, userRepo, messageRepo)
|
||||
messageService := service.NewMessageService(messageRepo, chatRepo, userRepo, attachmentRepo, encryptor)
|
||||
fileService := service.NewFileService(attachmentRepo, chatRepo, cfg.StoragePath, cfg.MaxFileSizeMB)
|
||||
adminService := service.NewAdminService(userRepo, chatRepo, messageRepo, fileService)
|
||||
|
||||
// Создаем WebSocket Hub
|
||||
wsHub := websocket.NewHub(messageService, chatService)
|
||||
go wsHub.Run()
|
||||
|
||||
// Создаем хендлеры
|
||||
authHandler := handlers.NewAuthHandler(authService)
|
||||
userHandler := handlers.NewUserHandler(userService)
|
||||
chatHandler := handlers.NewChatHandler(chatService, userService)
|
||||
messageHandler := handlers.NewMessageHandler(messageService, chatService)
|
||||
fileHandler := handlers.NewFileHandler(fileService)
|
||||
adminHandler := handlers.NewAdminHandler(adminService)
|
||||
wsHandler := handlers.NewWebSocketHandler(wsHub, authService)
|
||||
|
||||
// Создаем основной роутер для API
|
||||
apiRouter := chi.NewRouter()
|
||||
|
||||
// Middleware для API (кроме WebSocket)
|
||||
apiRouter.Use(middleware.Recovery)
|
||||
apiRouter.Use(middleware.Logging)
|
||||
apiRouter.Use(cors.New(cors.Options{
|
||||
AllowedOrigins: cfg.CORSAllowedOrigins,
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Content-Type", "Authorization"},
|
||||
AllowCredentials: true,
|
||||
}).Handler)
|
||||
|
||||
// Публичные роуты API
|
||||
apiRouter.Post("/api/register", authHandler.Register)
|
||||
apiRouter.Post("/api/login", authHandler.Login)
|
||||
apiRouter.Get("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"status":"ok"}`))
|
||||
})
|
||||
|
||||
// Защищенные API роуты
|
||||
apiRouter.Group(func(r chi.Router) {
|
||||
r.Use(middleware.JWTAuth(authService))
|
||||
|
||||
r.Get("/api/me", authHandler.GetMe)
|
||||
r.Get("/api/profile", userHandler.GetProfile)
|
||||
r.Put("/api/profile", userHandler.UpdateProfile)
|
||||
r.Get("/api/users", userHandler.SearchUsers)
|
||||
|
||||
r.Post("/api/chats/private", chatHandler.CreatePrivateChat)
|
||||
r.Post("/api/chats/group", chatHandler.CreateGroupChat)
|
||||
r.Get("/api/chats", chatHandler.GetMyChats)
|
||||
r.Get("/api/chats/{id}", chatHandler.GetChatByID)
|
||||
r.Get("/api/chats/{id}/members", chatHandler.GetChatMembers)
|
||||
r.Post("/api/chats/{id}/members", chatHandler.AddMembers)
|
||||
r.Delete("/api/chats/{id}/members/{user_id}", chatHandler.RemoveMember)
|
||||
r.Put("/api/chats/{id}/title", chatHandler.UpdateChatTitle)
|
||||
|
||||
r.Get("/api/chats/{id}/messages", messageHandler.GetMessages)
|
||||
r.Put("/api/messages/{id}/read", messageHandler.MarkAsRead)
|
||||
|
||||
r.Post("/api/chats/{id}/upload", fileHandler.UploadFile)
|
||||
r.Get("/api/attachments/{id}", fileHandler.DownloadFile)
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.RequireGlobalAdmin)
|
||||
r.Delete("/api/admin/users/{id}", adminHandler.DeleteUser)
|
||||
r.Delete("/api/admin/messages/{id}", adminHandler.DeleteMessage)
|
||||
})
|
||||
})
|
||||
|
||||
// Создаем отдельный роутер для WebSocket (без middleware)
|
||||
wsRouter := http.NewServeMux()
|
||||
wsRouter.HandleFunc("/ws", wsHandler.HandleWebSocket)
|
||||
|
||||
// Объединяем роутеры
|
||||
mainRouter := http.NewServeMux()
|
||||
mainRouter.Handle("/", apiRouter)
|
||||
mainRouter.Handle("/ws", wsRouter)
|
||||
|
||||
// Запускаем сервер
|
||||
srv := &http.Server{
|
||||
Addr: ":" + cfg.ServerPort,
|
||||
Handler: mainRouter,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
// Graceful shutdown
|
||||
go func() {
|
||||
logger.Info("Server starting", "port", cfg.ServerPort)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logger.Error("Server failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
// Запускаем периодический сборщик мусора для оптимизации памяти
|
||||
go func() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
runtime.GC()
|
||||
logger.Debug("Garbage collection triggered")
|
||||
}
|
||||
}()
|
||||
|
||||
// Ожидаем сигнал завершения
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
logger.Info("Shutting down server...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
wsHub.Stop()
|
||||
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
logger.Error("Server forced to shutdown", "error", err)
|
||||
}
|
||||
|
||||
logger.Info("Server stopped")
|
||||
}
|
||||
Reference in New Issue
Block a user