diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ae3cede..8de1ec1 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -39,18 +39,6 @@ jobs: tags: woodchen/${{ env.IMAGE_NAME }}:latest platforms: linux/amd64,linux/arm64 - - name: Create artifact - run: | - zip -r public.zip public - - - name: Deploy public directory to server - uses: appleboy/scp-action@master - with: - host: ${{ secrets.SERVER_HOST }} - username: root - key: ${{ secrets.SERVER_SSH_KEY }} - source: "public.zip" - target: "/tmp" - name: Execute deployment commands uses: appleboy/ssh-action@master @@ -59,22 +47,6 @@ jobs: username: root key: ${{ secrets.SERVER_SSH_KEY }} script: | - # 解压文件 - unzip -o /tmp/public.zip -d /tmp/public_temp - - # 删除目标目录中的现有文件 - rm -rf /opt/1panel/docker/compose/random-api-go/public/* - - # 移动新文件到目标目录 - mv -f /tmp/public_temp/public/* /opt/1panel/docker/compose/random-api-go/public/ - - # 设置目录及其子文件的所有权和权限 - chmod -R 0755 /opt/1panel/docker/compose/random-api-go/public - - # 清理临时文件 - rm /tmp/public.zip - rm -rf /tmp/public_temp - # 拉取镜像 docker pull woodchen/random-api-go:latest diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..ab9ab58 --- /dev/null +++ b/config/config.go @@ -0,0 +1,20 @@ +package config + +import ( + "math/rand" + "time" +) + +const ( + Port = ":5003" + RequestTimeout = 10 * time.Second + EnvBaseURL = "BASE_URL" +) + +var ( + RNG *rand.Rand +) + +func InitRNG(r *rand.Rand) { + RNG = r +} diff --git a/handlers/api_handler.go b/handlers/api_handler.go new file mode 100644 index 0000000..78ac3f6 --- /dev/null +++ b/handlers/api_handler.go @@ -0,0 +1,92 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "net/url" + "random-api-go/services" + "random-api-go/stats" + "random-api-go/utils" + + "strings" + "time" +) + +var statsManager *stats.StatsManager + +// InitializeHandlers 初始化处理器 +func InitializeHandlers(sm *stats.StatsManager) error { + statsManager = sm + return services.InitializeCSVService() +} + +func HandleAPIRequest(w http.ResponseWriter, r *http.Request) { + start := time.Now() + realIP := utils.GetRealIP(r) + referer := r.Referer() + + var sourceDomain string + if referer != "" { + if parsedURL, err := url.Parse(referer); err == nil { + sourceDomain = parsedURL.Hostname() + } + } + if sourceDomain == "" { + sourceDomain = "direct" + } + + path := strings.TrimPrefix(r.URL.Path, "/") + pathSegments := strings.Split(path, "/") + + if len(pathSegments) < 2 { + http.NotFound(w, r) + return + } + + prefix := pathSegments[0] + suffix := pathSegments[1] + + services.Mu.RLock() + csvPath, ok := services.CSVPathsCache[prefix][suffix] + services.Mu.RUnlock() + + if !ok { + http.NotFound(w, r) + return + } + + selector, err := services.GetCSVContent(csvPath) + if err != nil { + http.Error(w, "Failed to fetch CSV content", http.StatusInternalServerError) + log.Printf("Error fetching CSV content: %v", err) + return + } + + if len(selector.URLs) == 0 { + http.Error(w, "No content available", http.StatusNotFound) + return + } + + randomURL := selector.GetRandomURL() + + // 记录统计 + endpoint := fmt.Sprintf("%s/%s", prefix, suffix) + statsManager.IncrementCalls(endpoint) + + duration := time.Since(start) + log.Printf("请求:%s %s,来自 %s -来源:%s -持续时间: %v - 重定向至: %s", + r.Method, r.URL.Path, realIP, sourceDomain, duration, randomURL) + + http.Redirect(w, r, randomURL, http.StatusFound) +} + +func HandleStats(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + stats := statsManager.GetStats() + if err := json.NewEncoder(w).Encode(stats); err != nil { + http.Error(w, "Error encoding stats", http.StatusInternalServerError) + log.Printf("Error encoding stats: %v", err) + } +} diff --git a/main.go b/main.go index f97b890..1467d69 100644 --- a/main.go +++ b/main.go @@ -1,69 +1,21 @@ package main import ( - "encoding/json" - "fmt" - "io" "log" "math/rand" "net/http" - "net/url" "os" "os/signal" - "path/filepath" + "random-api-go/config" + "random-api-go/handlers" "random-api-go/logging" "random-api-go/stats" - "random-api-go/utils" - "strings" - "sync" + "syscall" "time" ) -const ( - port = ":5003" - requestTimeout = 10 * time.Second - envBaseURL = "BASE_URL" -) - -var ( - csvPathsCache map[string]map[string]string - csvCache = make(map[string]*URLSelector) - mu sync.RWMutex - rng *rand.Rand - statsManager *stats.StatsManager -) - -type URLSelector struct { - URLs []string - mu sync.Mutex -} - -func NewURLSelector(urls []string) *URLSelector { - return &URLSelector{ - URLs: urls, - } -} - -func (us *URLSelector) ShuffleURLs() { - for i := len(us.URLs) - 1; i > 0; i-- { - j := rng.Intn(i + 1) - us.URLs[i], us.URLs[j] = us.URLs[j], us.URLs[i] - } -} - -func (us *URLSelector) GetRandomURL() string { - us.mu.Lock() - defer us.mu.Unlock() - - if len(us.URLs) == 0 { - return "" - } - return us.URLs[rng.Intn(len(us.URLs))] -} - func init() { - // 确保数据目录存在 if err := os.MkdirAll("data", 0755); err != nil { log.Fatal("Failed to create data directory:", err) } @@ -71,249 +23,45 @@ func init() { func main() { source := rand.NewSource(time.Now().UnixNano()) - rng = rand.New(source) + config.InitRNG(rand.New(source)) logging.SetupLogging() - statsManager = stats.NewStatsManager("data/stats.json") + statsManager := stats.NewStatsManager("data/stats.json") // 设置优雅关闭 + setupGracefulShutdown(statsManager) + + // 初始化handlers + if err := handlers.InitializeHandlers(statsManager); err != nil { + log.Fatal("Failed to initialize handlers:", err) + } + + // 设置路由 + setupRoutes() + + log.Printf("Server starting on %s...\n", config.Port) + if err := http.ListenAndServe(config.Port, nil); err != nil { + log.Fatal(err) + } +} + +func setupGracefulShutdown(statsManager *stats.StatsManager) { c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c log.Println("Server is shutting down...") - - // 关闭统计管理器,确保统计数据被保存 statsManager.Shutdown() log.Println("Stats manager shutdown completed") - os.Exit(0) }() +} - if err := loadCSVPaths(); err != nil { - log.Fatal("Failed to load CSV paths:", err) - } - - // 设置静态文件服务 +func setupRoutes() { fs := http.FileServer(http.Dir("./public")) http.Handle("/", fs) - - // 设置 API 路由 - http.HandleFunc("/pic/", handleAPIRequest) - http.HandleFunc("/video/", handleAPIRequest) - http.HandleFunc("/stats", handleStats) - - log.Printf("Server starting on %s...\n", port) - if err := http.ListenAndServe(port, nil); err != nil { - log.Fatal(err) - } -} - -func loadCSVPaths() error { - var data []byte - var err error - - // 获取环境变量中的基础URL - baseURL := os.Getenv(envBaseURL) - - if baseURL != "" { - // 构建完整的URL - var fullURL string - if strings.HasPrefix(baseURL, "http://") || strings.HasPrefix(baseURL, "https://") { - fullURL = utils.JoinURLPath(baseURL, "url.json") - } else { - fullURL = "https://" + utils.JoinURLPath(baseURL, "url.json") - } - - log.Printf("Attempting to read url.json from: %s", fullURL) - - // 创建HTTP客户端 - client := &http.Client{ - Timeout: requestTimeout, - } - - resp, err := client.Get(fullURL) - if err != nil { - return fmt.Errorf("failed to fetch url.json: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("failed to fetch url.json, status code: %d", resp.StatusCode) - } - - data, err = io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("failed to read url.json response: %w", err) - } - } else { - // 从本地文件读取 - jsonPath := filepath.Join("public", "url.json") - log.Printf("Attempting to read local file: %s", jsonPath) - - data, err = os.ReadFile(jsonPath) - if err != nil { - return fmt.Errorf("failed to read local url.json: %w", err) - } - } - - var result map[string]map[string]string - if err := json.Unmarshal(data, &result); err != nil { - return fmt.Errorf("failed to unmarshal url.json: %w", err) - } - - mu.Lock() - csvPathsCache = result - mu.Unlock() - - log.Println("CSV paths loaded from url.json") - return nil -} - -func getCSVContent(path string) (*URLSelector, error) { - mu.RLock() - selector, exists := csvCache[path] - mu.RUnlock() - if exists { - return selector, nil - } - - var fileContent []byte - var err error - - // 获取环境变量中的基础URL - baseURL := os.Getenv(envBaseURL) - - if baseURL != "" { - // 如果设置了基础URL,构建完整的URL - var fullURL string - if strings.HasPrefix(baseURL, "http://") || strings.HasPrefix(baseURL, "https://") { - // 如果baseURL已经包含协议,直接使用 - fullURL = utils.JoinURLPath(baseURL, path) - } else { - // 如果没有协议,添加https:// - fullURL = "https://" + utils.JoinURLPath(baseURL, path) - } - - log.Printf("尝试从URL获取: %s", fullURL) - - // 创建HTTP客户端 - client := &http.Client{ - Timeout: requestTimeout, - } - - resp, err := client.Get(fullURL) - if err != nil { - return nil, fmt.Errorf("HTTP请求失败: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("HTTP请求返回非200状态码: %d", resp.StatusCode) - } - - fileContent, err = io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("读取响应内容失败: %w", err) - } - } else { - // 如果没有设置基础URL,从本地文件读取 - fullPath := filepath.Join("public", path) - log.Printf("尝试读取本地文件: %s", fullPath) - - fileContent, err = os.ReadFile(fullPath) - if err != nil { - return nil, fmt.Errorf("读取CSV内容时出错: %w", err) - } - } - - lines := strings.Split(string(fileContent), "\n") - uniqueURLs := make(map[string]bool) - var fileArray []string - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if trimmed != "" && !strings.HasPrefix(trimmed, "#") && !uniqueURLs[trimmed] { - fileArray = append(fileArray, trimmed) - uniqueURLs[trimmed] = true - } - } - - selector = NewURLSelector(fileArray) - - mu.Lock() - csvCache[path] = selector - mu.Unlock() - - return selector, nil -} - -func handleAPIRequest(w http.ResponseWriter, r *http.Request) { - start := time.Now() - realIP := utils.GetRealIP(r) - referer := r.Referer() - - var sourceDomain string - if referer != "" { - if parsedURL, err := url.Parse(referer); err == nil { - sourceDomain = parsedURL.Hostname() - } - } - if sourceDomain == "" { - sourceDomain = "direct" - } - - path := strings.TrimPrefix(r.URL.Path, "/") - pathSegments := strings.Split(path, "/") - - if len(pathSegments) < 2 { - http.NotFound(w, r) - return - } - - prefix := pathSegments[0] - suffix := pathSegments[1] - - mu.RLock() - csvPath, ok := csvPathsCache[prefix][suffix] - mu.RUnlock() - - if !ok { - http.NotFound(w, r) - return - } - - selector, err := getCSVContent(csvPath) - if err != nil { - http.Error(w, "Failed to fetch CSV content", http.StatusInternalServerError) - log.Printf("Error fetching CSV content: %v", err) - return - } - - if len(selector.URLs) == 0 { - http.Error(w, "No content available", http.StatusNotFound) - return - } - - randomURL := selector.GetRandomURL() - - // 记录统计 - endpoint := fmt.Sprintf("%s/%s", prefix, suffix) - statsManager.IncrementCalls(endpoint) - - duration := time.Since(start) - log.Printf("请求:%s %s,来自 %s -来源:%s -持续时间: %v - 重定向至: %s", - r.Method, r.URL.Path, realIP, sourceDomain, duration, randomURL) - - http.Redirect(w, r, randomURL, http.StatusFound) -} - -// 统计API处理函数 -func handleStats(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - stats := statsManager.GetStats() - if err := json.NewEncoder(w).Encode(stats); err != nil { - http.Error(w, "Error encoding stats", http.StatusInternalServerError) - log.Printf("Error encoding stats: %v", err) - } + http.HandleFunc("/pic/", handlers.HandleAPIRequest) + http.HandleFunc("/video/", handlers.HandleAPIRequest) + http.HandleFunc("/stats", handlers.HandleStats) } diff --git a/models/url_selector.go b/models/url_selector.go new file mode 100644 index 0000000..54c2d31 --- /dev/null +++ b/models/url_selector.go @@ -0,0 +1,27 @@ +package models + +import ( + "random-api-go/config" + "sync" +) + +type URLSelector struct { + URLs []string + mu sync.Mutex +} + +func NewURLSelector(urls []string) *URLSelector { + return &URLSelector{ + URLs: urls, + } +} + +func (us *URLSelector) GetRandomURL() string { + us.mu.Lock() + defer us.mu.Unlock() + + if len(us.URLs) == 0 { + return "" + } + return us.URLs[config.RNG.Intn(len(us.URLs))] +} diff --git a/services/csv_service.go b/services/csv_service.go new file mode 100644 index 0000000..ca5143a --- /dev/null +++ b/services/csv_service.go @@ -0,0 +1,165 @@ +package services + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "random-api-go/config" + "random-api-go/models" + "random-api-go/utils" + "strings" + "sync" +) + +var ( + CSVPathsCache map[string]map[string]string + csvCache = make(map[string]*models.URLSelector) + Mu sync.RWMutex +) + +// InitializeCSVService 初始化CSV服务 +func InitializeCSVService() error { + return LoadCSVPaths() +} + +func LoadCSVPaths() error { + var data []byte + var err error + + // 获取环境变量中的基础URL + baseURL := os.Getenv(config.EnvBaseURL) + + if baseURL != "" { + // 构建完整的URL + var fullURL string + if strings.HasPrefix(baseURL, "http://") || strings.HasPrefix(baseURL, "https://") { + fullURL = utils.JoinURLPath(baseURL, "url.json") + } else { + fullURL = "https://" + utils.JoinURLPath(baseURL, "url.json") + } + + log.Printf("Attempting to read url.json from: %s", fullURL) + + // 创建HTTP客户端 + client := &http.Client{ + Timeout: config.RequestTimeout, + } + + resp, err := client.Get(fullURL) + if err != nil { + return fmt.Errorf("failed to fetch url.json: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to fetch url.json, status code: %d", resp.StatusCode) + } + + data, err = io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read url.json response: %w", err) + } + } else { + // 从本地文件读取 + jsonPath := filepath.Join("public", "url.json") + log.Printf("Attempting to read local file: %s", jsonPath) + + data, err = os.ReadFile(jsonPath) + if err != nil { + return fmt.Errorf("failed to read local url.json: %w", err) + } + } + + var result map[string]map[string]string + if err := json.Unmarshal(data, &result); err != nil { + return fmt.Errorf("failed to unmarshal url.json: %w", err) + } + + Mu.Lock() + CSVPathsCache = result + Mu.Unlock() + + log.Println("CSV paths loaded from url.json") + return nil +} + +func GetCSVContent(path string) (*models.URLSelector, error) { + Mu.RLock() + selector, exists := csvCache[path] + Mu.RUnlock() + if exists { + return selector, nil + } + + var fileContent []byte + var err error + + // 获取环境变量中的基础URL + baseURL := os.Getenv(config.EnvBaseURL) + + if baseURL != "" { + // 如果设置了基础URL,构建完整的URL + var fullURL string + if strings.HasPrefix(baseURL, "http://") || strings.HasPrefix(baseURL, "https://") { + // 如果baseURL已经包含协议,直接使用 + fullURL = utils.JoinURLPath(baseURL, path) + } else { + // 如果没有协议,添加https:// + fullURL = "https://" + utils.JoinURLPath(baseURL, path) + } + + log.Printf("尝试从URL获取: %s", fullURL) + + // 创建HTTP客户端 + client := &http.Client{ + Timeout: config.RequestTimeout, + } + + resp, err := client.Get(fullURL) + if err != nil { + return nil, fmt.Errorf("HTTP请求失败: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP请求返回非200状态码: %d", resp.StatusCode) + } + + fileContent, err = io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("读取响应内容失败: %w", err) + } + } else { + // 如果没有设置基础URL,从本地文件读取 + fullPath := filepath.Join("public", path) + log.Printf("尝试读取本地文件: %s", fullPath) + + fileContent, err = os.ReadFile(fullPath) + if err != nil { + return nil, fmt.Errorf("读取CSV内容时出错: %w", err) + } + } + + lines := strings.Split(string(fileContent), "\n") + uniqueURLs := make(map[string]bool) + var fileArray []string + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed != "" && !strings.HasPrefix(trimmed, "#") && !uniqueURLs[trimmed] { + fileArray = append(fileArray, trimmed) + uniqueURLs[trimmed] = true + } + } + + selector = models.NewURLSelector(fileArray) + + Mu.Lock() + csvCache[path] = selector + Mu.Unlock() + + return selector, nil +} diff --git a/utils/utils.go b/utils/http_utils.go similarity index 100% rename from utils/utils.go rename to utils/http_utils.go