package main import ( "flag" "fmt" "io/fs" "log" "net/http" "os" "os/signal" "strconv" "strings" "sync" "syscall" "time" "git.sovbit.dev/enki/torrentGateway/internal/api" "git.sovbit.dev/enki/torrentGateway/internal/blossom" "git.sovbit.dev/enki/torrentGateway/internal/config" "git.sovbit.dev/enki/torrentGateway/internal/dht" "git.sovbit.dev/enki/torrentGateway/internal/middleware" "git.sovbit.dev/enki/torrentGateway/internal/storage" "git.sovbit.dev/enki/torrentGateway/internal/web" "github.com/gorilla/mux" ) func main() { // Parse command line flags configPath := flag.String("config", "configs/config.yaml", "Path to configuration file") mode := flag.String("mode", "", "Override mode (unified, gateway-only, blossom-only, dht-only)") flag.Parse() // Load configuration cfg, err := config.LoadConfig(*configPath) if err != nil { log.Fatalf("Failed to load configuration: %v", err) } // Override mode if specified if *mode != "" { cfg.Mode = *mode } log.Printf("Starting Blossom-BitTorrent Gateway in %s mode", cfg.Mode) // Initialize shared storage backend var storageBackend *storage.Backend if cfg.IsServiceEnabled("gateway") || cfg.IsServiceEnabled("blossom") { storageBackend, err = storage.NewBackend( cfg.Storage.MetadataDB, cfg.Storage.ChunkStorage, cfg.Storage.BlobStorage, cfg.Storage.ChunkSize, cfg, ) if err != nil { log.Fatalf("Failed to initialize storage backend: %v", err) } defer storageBackend.Close() // Start automated cleanup service if enabled if cfg.Admin.AutoCleanup { log.Printf("Starting automated cleanup service") go startCleanupService(storageBackend, cfg) } } // Track running services var wg sync.WaitGroup var servers []ServiceManager // Declare gateway instance for DHT integration var gatewayInstance *api.Gateway // Start Gateway service if cfg.IsServiceEnabled("gateway") { log.Printf("Starting Gateway service on port %d", cfg.Gateway.Port) r := mux.NewRouter() // Apply security middleware to ALL routes (not just API) r.Use(middleware.SecurityHeaders) r.Use(middleware.InputSanitization) maxUploadSize, _ := cfg.GetMaxUploadSizeBytes() r.Use(middleware.RequestSizeLimit(maxUploadSize)) r.Use(middleware.AntiCrawler) r.Use(middleware.CORS) // Register API routes with /api prefix apiRouter := r.PathPrefix("/api").Subrouter() // Register tracker routes on main router (no /api prefix for BitTorrent compatibility) api.RegisterTrackerRoutes(r, cfg, storageBackend) // Register main API routes and get gateway instance gatewayInstance = api.RegisterRoutes(apiRouter, cfg, storageBackend) // Serve static files webFS := web.GetFS() staticFS, _ := fs.Sub(webFS, "static") r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(staticFS)))) // Serve main pages r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { data, err := web.GetFile("index.html") if err != nil { http.Error(w, "Page not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "text/html") w.Write(data) }) r.HandleFunc("/player.html", func(w http.ResponseWriter, r *http.Request) { data, err := web.GetFile("player.html") if err != nil { http.Error(w, "Page not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "text/html") w.Write(data) }) r.HandleFunc("/admin", func(w http.ResponseWriter, r *http.Request) { data, err := web.GetFile("admin.html") if err != nil { http.Error(w, "Page not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "text/html") w.Write(data) }) gatewayServer := &http.Server{ Addr: fmt.Sprintf("0.0.0.0:%d", cfg.Gateway.Port), Handler: r, } servers = append(servers, &HTTPServiceManager{ name: "Gateway", server: gatewayServer, }) wg.Add(1) go func() { defer wg.Done() if err := gatewayServer.ListenAndServe(); err != http.ErrServerClosed { log.Printf("Gateway server error: %v", err) } }() } // Start Blossom server if cfg.IsServiceEnabled("blossom") { log.Printf("Starting Blossom server on port %d", cfg.BlossomServer.Port) blossomServer := blossom.NewServer(storageBackend, &cfg.BlossomServer, cfg) httpServer := &http.Server{ Addr: fmt.Sprintf(":%d", cfg.BlossomServer.Port), Handler: blossomServer, } servers = append(servers, &HTTPServiceManager{ name: "Blossom", server: httpServer, }) wg.Add(1) go func() { defer wg.Done() if err := httpServer.ListenAndServe(); err != http.ErrServerClosed { log.Printf("Blossom server error: %v", err) } }() } // Start DHT node if cfg.IsServiceEnabled("dht") { log.Printf("Starting DHT node on port %d", cfg.DHT.Port) dhtNode, err := dht.NewDHT(&cfg.DHT) if err != nil { log.Fatalf("Failed to create DHT node: %v", err) } // Create DHT bootstrap manager (only if gateway is enabled) var dhtBootstrap *dht.DHTBootstrap if gatewayInstance != nil { dhtBootstrap = dht.NewDHTBootstrap(dhtNode, gatewayInstance, &cfg.DHT) } servers = append(servers, &DHTServiceManager{ name: "DHT", node: dhtNode, bootstrap: dhtBootstrap, }) wg.Add(1) go func() { defer wg.Done() if err := dhtNode.Start(); err != nil { log.Printf("DHT node error: %v", err) return } // Initialize bootstrap functionality after DHT starts (only if gateway is enabled) if dhtBootstrap != nil { if err := dhtBootstrap.Initialize(); err != nil { log.Printf("DHT bootstrap error: %v", err) } // Connect DHT bootstrap to gateway gatewayInstance.SetDHTBootstrap(dhtBootstrap) } }() } // Handle shutdown signals sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) // Wait for shutdown signal <-sigCh log.Println("Shutdown signal received, stopping services...") // Stop all services for _, srv := range servers { if err := srv.Stop(); err != nil { log.Printf("Error stopping %s: %v", srv.Name(), err) } } // Wait for all services to stop wg.Wait() log.Println("All services stopped") } // ServiceManager interface for managing different types of services type ServiceManager interface { Stop() error Name() string } // HTTPServiceManager manages HTTP servers type HTTPServiceManager struct { name string server *http.Server } func (h *HTTPServiceManager) Stop() error { log.Printf("Stopping %s server", h.name) return h.server.Close() } func (h *HTTPServiceManager) Name() string { return h.name } // DHTServiceManager manages DHT nodes type DHTServiceManager struct { name string node *dht.DHT bootstrap *dht.DHTBootstrap } func (d *DHTServiceManager) Stop() error { log.Printf("Stopping %s node", d.name) if d.bootstrap != nil { d.bootstrap.Stop() } return d.node.Stop() } func (d *DHTServiceManager) Name() string { return d.name } // startCleanupService starts the automated cleanup service func startCleanupService(storageBackend *storage.Backend, cfg *config.Config) { // Parse cleanup age cleanupAge, err := parseDuration(cfg.Admin.CleanupAge) if err != nil { log.Printf("Invalid cleanup_age format '%s': %v", cfg.Admin.CleanupAge, err) return } // Default cleanup interval to 1 hour if not set cleanupInterval := 1 * time.Hour log.Printf("Cleanup service started: cleaning files older than %v every %v", cleanupAge, cleanupInterval) ticker := time.NewTicker(cleanupInterval) defer ticker.Stop() // Run initial cleanup after 1 minute initialDelay := time.NewTimer(1 * time.Minute) defer initialDelay.Stop() for { select { case <-initialDelay.C: runCleanupCycle(storageBackend, cfg, cleanupAge) case <-ticker.C: runCleanupCycle(storageBackend, cfg, cleanupAge) } } } // runCleanupCycle performs a full cleanup cycle func runCleanupCycle(storageBackend *storage.Backend, cfg *config.Config, cleanupAge time.Duration) { log.Printf("Starting cleanup cycle...") start := time.Now() // Clean old files if fileResults, err := cleanupOldFiles(storageBackend, cleanupAge); err != nil { log.Printf("Error cleaning old files: %v", err) } else { log.Printf("Cleaned old files: %s", formatCleanupResults(fileResults)) } // Clean orphaned chunks if chunkResults, err := cleanupOrphanedChunks(storageBackend, cfg); err != nil { log.Printf("Error cleaning orphaned chunks: %v", err) } else { log.Printf("Cleaned orphaned chunks: %s", formatCleanupResults(chunkResults)) } // Clean inactive users (users with no activity for 180 days) if userResults, err := cleanupInactiveUsers(storageBackend, 180); err != nil { log.Printf("Error cleaning inactive users: %v", err) } else { log.Printf("Cleaned inactive users: %s", formatCleanupResults(userResults)) } log.Printf("Cleanup cycle completed in %v", time.Since(start)) } // cleanupOldFiles removes files older than the specified duration func cleanupOldFiles(storageBackend *storage.Backend, olderThan time.Duration) (map[string]interface{}, error) { cutoffTime := time.Now().Add(-olderThan) rows, err := storageBackend.GetDB().Query(` SELECT hash, original_name, storage_type, created_at FROM files WHERE created_at < ? OR last_access < ? `, cutoffTime, cutoffTime) if err != nil { return nil, err } defer rows.Close() var deletedCount int var totalSize int64 for rows.Next() { var hash, originalName, storageType string var createdAt time.Time if err := rows.Scan(&hash, &originalName, &storageType, &createdAt); err != nil { continue } // Get file size before deletion if metadata, err := storageBackend.GetFileMetadata(hash); err == nil && metadata != nil { totalSize += metadata.Size } // Delete the file if err := storageBackend.AdminDeleteFile(hash); err != nil { log.Printf("Error deleting file %s: %v", hash, err) continue } deletedCount++ log.Printf("Deleted old file: %s (%s)", originalName, hash[:8]) } return map[string]interface{}{ "deleted_count": deletedCount, "total_size": totalSize, }, nil } // cleanupOrphanedChunks removes chunks that don't belong to any file func cleanupOrphanedChunks(storageBackend *storage.Backend, cfg *config.Config) (map[string]interface{}, error) { rows, err := storageBackend.GetDB().Query(` SELECT c.chunk_hash, c.size FROM chunks c LEFT JOIN files f ON c.file_hash = f.hash WHERE f.hash IS NULL `) if err != nil { return nil, err } defer rows.Close() var deletedCount int var totalSize int64 for rows.Next() { var chunkHash string var size int64 if err := rows.Scan(&chunkHash, &size); err != nil { continue } // Delete chunk record from database if _, err := storageBackend.GetDB().Exec("DELETE FROM chunks WHERE chunk_hash = ?", chunkHash); err != nil { log.Printf("Error deleting chunk record %s: %v", chunkHash, err) continue } // Delete chunk file from disk chunkPath := fmt.Sprintf("%s/%s", cfg.Storage.ChunkStorage, chunkHash) if err := os.Remove(chunkPath); err != nil { log.Printf("Error deleting chunk file %s: %v", chunkHash, err) } deletedCount++ totalSize += size } return map[string]interface{}{ "deleted_count": deletedCount, "total_size": totalSize, }, nil } // cleanupInactiveUsers removes users with no activity for specified days func cleanupInactiveUsers(storageBackend *storage.Backend, days int) (map[string]interface{}, error) { cutoffTime := time.Now().AddDate(0, 0, -days) result, err := storageBackend.GetDB().Exec(` DELETE FROM users WHERE last_activity < ? AND pubkey NOT IN ( SELECT DISTINCT owner_pubkey FROM files WHERE owner_pubkey IS NOT NULL ) `, cutoffTime) if err != nil { return nil, err } deletedCount, _ := result.RowsAffected() return map[string]interface{}{ "deleted_count": deletedCount, }, nil } // formatCleanupResults formats cleanup results for logging func formatCleanupResults(results map[string]interface{}) string { deletedCount := results["deleted_count"].(int) if totalSize, hasSize := results["total_size"]; hasSize { return fmt.Sprintf("%d items (%s)", deletedCount, formatBytes(totalSize.(int64))) } return fmt.Sprintf("%d items", deletedCount) } // formatBytes formats byte count as human readable string func formatBytes(bytes int64) string { const unit = 1024 if bytes < unit { return fmt.Sprintf("%d B", bytes) } div, exp := int64(unit), 0 for n := bytes / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) } // parseDuration parses duration strings like "90d", "30d", "24h" func parseDuration(s string) (time.Duration, error) { if len(s) < 2 { return 0, fmt.Errorf("invalid duration format") } unit := s[len(s)-1:] valueStr := s[:len(s)-1] value, err := strconv.Atoi(valueStr) if err != nil { return 0, err } switch strings.ToLower(unit) { case "d": return time.Duration(value) * 24 * time.Hour, nil case "h": return time.Duration(value) * time.Hour, nil case "m": return time.Duration(value) * time.Minute, nil case "s": return time.Duration(value) * time.Second, nil default: return time.ParseDuration(s) } }