|
| 1 | +// file: internal/api/logs_explorer.go |
| 2 | +package api |
| 3 | + |
| 4 | +import ( |
| 5 | + "io/fs" |
| 6 | + "io/ioutil" |
| 7 | + "net/http" |
| 8 | + "os" |
| 9 | + "path/filepath" |
| 10 | + "strconv" |
| 11 | + "strings" |
| 12 | + "time" |
| 13 | + |
| 14 | + "github.com/gorilla/mux" |
| 15 | + "github.com/mirkobrombin/goup/internal/config" |
| 16 | +) |
| 17 | + |
| 18 | +// LogFileInfo holds data about a single log file. |
| 19 | +type LogFileInfo struct { |
| 20 | + Domain string `json:"domain"` |
| 21 | + Plugin string `json:"plugin,omitempty"` |
| 22 | + Year int `json:"year"` |
| 23 | + Month int `json:"month"` |
| 24 | + Day int `json:"day"` |
| 25 | + FileName string `json:"file_name"` |
| 26 | + SizeBytes int64 `json:"size_bytes"` |
| 27 | + ModTime int64 `json:"mod_time_unix"` |
| 28 | +} |
| 29 | + |
| 30 | +// GET /api/logfiles?start=YYYY-MM-DD&end=YYYY-MM-DD&plugin=somePlugin |
| 31 | +// Lists all log files, optionally filtered by date range or plugin name. |
| 32 | +func listLogFilesHandler(w http.ResponseWriter, r *http.Request) { |
| 33 | + startStr := r.URL.Query().Get("start") // YYYY-MM-DD |
| 34 | + endStr := r.URL.Query().Get("end") // YYYY-MM-DD |
| 35 | + pluginQ := r.URL.Query().Get("plugin") // plugin name |
| 36 | + |
| 37 | + var startTime, endTime time.Time |
| 38 | + var err error |
| 39 | + |
| 40 | + if startStr != "" { |
| 41 | + startTime, err = time.Parse("2006-01-02", startStr) |
| 42 | + if err != nil { |
| 43 | + http.Error(w, "Invalid 'start' date format (expected YYYY-MM-DD)", http.StatusBadRequest) |
| 44 | + return |
| 45 | + } |
| 46 | + } |
| 47 | + if endStr != "" { |
| 48 | + endTime, err = time.Parse("2006-01-02", endStr) |
| 49 | + if err != nil { |
| 50 | + http.Error(w, "Invalid 'end' date format (expected YYYY-MM-DD)", http.StatusBadRequest) |
| 51 | + return |
| 52 | + } |
| 53 | + } |
| 54 | + |
| 55 | + rootDir := config.GetLogDir() |
| 56 | + var results []LogFileInfo |
| 57 | + |
| 58 | + _ = filepath.Walk(rootDir, func(path string, info fs.FileInfo, walkErr error) error { |
| 59 | + if walkErr != nil || info.IsDir() { |
| 60 | + return walkErr |
| 61 | + } |
| 62 | + rel, _ := filepath.Rel(rootDir, path) |
| 63 | + parts := strings.Split(rel, string(os.PathSeparator)) |
| 64 | + if len(parts) != 4 { |
| 65 | + return nil |
| 66 | + } |
| 67 | + domain := parts[0] |
| 68 | + yearStr := parts[1] |
| 69 | + monthStr := parts[2] |
| 70 | + fileName := parts[3] // e.g. "05.log" or "05-something.log" |
| 71 | + |
| 72 | + year, yErr := strconv.Atoi(yearStr) |
| 73 | + month, mErr := strconv.Atoi(monthStr) |
| 74 | + if yErr != nil || mErr != nil { |
| 75 | + return nil |
| 76 | + } |
| 77 | + |
| 78 | + // Parse day and optional plugin from the file name |
| 79 | + // e.g. "05.log" -> day=05, plugin="" |
| 80 | + // or "05-MyPlugin.log" -> day=05, plugin="MyPlugin" |
| 81 | + dayStr, pluginName := parseDayAndPlugin(fileName) |
| 82 | + day, dErr := strconv.Atoi(dayStr) |
| 83 | + if dErr != nil { |
| 84 | + return nil |
| 85 | + } |
| 86 | + |
| 87 | + fileDate := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) |
| 88 | + |
| 89 | + // Filter by date if set |
| 90 | + if !startTime.IsZero() && fileDate.Before(startTime) { |
| 91 | + return nil |
| 92 | + } |
| 93 | + if !endTime.IsZero() && fileDate.After(endTime) { |
| 94 | + return nil |
| 95 | + } |
| 96 | + if pluginQ != "" && pluginName != pluginQ { |
| 97 | + return nil |
| 98 | + } |
| 99 | + |
| 100 | + results = append(results, LogFileInfo{ |
| 101 | + Domain: domain, |
| 102 | + Plugin: pluginName, |
| 103 | + Year: year, |
| 104 | + Month: month, |
| 105 | + Day: day, |
| 106 | + FileName: rel, |
| 107 | + SizeBytes: info.Size(), |
| 108 | + ModTime: info.ModTime().Unix(), |
| 109 | + }) |
| 110 | + return nil |
| 111 | + }) |
| 112 | + |
| 113 | + jsonResponse(w, results) |
| 114 | +} |
| 115 | + |
| 116 | +// GET /api/logfiles/{fileName} |
| 117 | +// Returns the content of a log file. |
| 118 | +func getLogFileHandler(w http.ResponseWriter, r *http.Request) { |
| 119 | + vars := mux.Vars(r) |
| 120 | + fileName := vars["fileName"] |
| 121 | + |
| 122 | + if fileName == "" { |
| 123 | + http.Error(w, "fileName is required", http.StatusBadRequest) |
| 124 | + return |
| 125 | + } |
| 126 | + fullPath := filepath.Join(config.GetLogDir(), fileName) |
| 127 | + if _, err := os.Stat(fullPath); os.IsNotExist(err) { |
| 128 | + http.Error(w, "Log file not found", http.StatusNotFound) |
| 129 | + return |
| 130 | + } |
| 131 | + data, err := ioutil.ReadFile(fullPath) |
| 132 | + if err != nil { |
| 133 | + http.Error(w, "Failed to read log file", http.StatusInternalServerError) |
| 134 | + return |
| 135 | + } |
| 136 | + w.Header().Set("Content-Type", "text/plain") |
| 137 | + w.Write(data) |
| 138 | +} |
| 139 | + |
| 140 | +// parseDayAndPlugin extracts the day and optional plugin name from a log |
| 141 | +// file name. |
| 142 | +func parseDayAndPlugin(fileName string) (string, string) { |
| 143 | + base := strings.TrimSuffix(fileName, ".log") |
| 144 | + parts := strings.SplitN(base, "-", 2) |
| 145 | + if len(parts) == 1 { |
| 146 | + return parts[0], "" |
| 147 | + } |
| 148 | + return parts[0], parts[1] |
| 149 | +} |
0 commit comments