package app import ( "io" "os" "strconv" "strings" ) // --------------------------------------------------------------------- // tail helper // --------------------------------------------------------------------- const defaultTailMaxBytes = 512 * 1024 func tailFile(path string, maxLines int) []string { if maxLines <= 0 { return nil } // читаем лимит из env, если задан maxBytes := defaultTailMaxBytes if env := os.Getenv("SVPN_TAIL_MAX_BYTES"); env != "" { if n, err := strconv.Atoi(env); err == nil && n > 0 { maxBytes = n } } f, err := os.Open(path) if err != nil { // файла нет или нет прав — просто ничего не отдаём return nil } defer f.Close() fi, err := f.Stat() if err != nil { return nil } size := fi.Size() if size <= 0 { return nil } // с какого смещения читаем хвост start := int64(0) if size > int64(maxBytes) { start = size - int64(maxBytes) } // двигаем указатель в файле if _, err := f.Seek(start, io.SeekStart); err != nil { return nil } // читаем хвост data, err := io.ReadAll(f) if err != nil { return nil } // режем по строкам lines := strings.Split(string(data), "\n") // если мы начали читать с середины файла (start > 0), // первая строка почти наверняка обрезана — выбрасываем её. if start > 0 && len(lines) > 0 { lines = lines[1:] } // убираем финальную пустую строку, если есть if n := len(lines); n > 0 && lines[n-1] == "" { lines = lines[:n-1] } // берём только последние maxLines if len(lines) > maxLines { lines = lines[len(lines)-maxLines:] } return lines }