106 lines
2.6 KiB
Go
106 lines
2.6 KiB
Go
package runtimelog
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.pcux.cn/xuans/Smail/utils/logpolicy"
|
|
)
|
|
|
|
type File struct {
|
|
Name string `json:"name"`
|
|
Bytes int64 `json:"bytes"`
|
|
Compressed bool `json:"compressed"`
|
|
}
|
|
|
|
type Page struct {
|
|
Lines []string `json:"lines"`
|
|
HasMore bool `json:"hasMore"`
|
|
}
|
|
|
|
func (w *Writer) Files() ([]File, error) {
|
|
w.archiveMu.Lock()
|
|
defer w.archiveMu.Unlock()
|
|
return w.files()
|
|
}
|
|
|
|
func (w *Writer) files() ([]File, error) {
|
|
if w.closed {
|
|
return nil, ErrInvalid
|
|
}
|
|
directory, err := w.root.Open(".")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer directory.Close()
|
|
entries, err := directory.ReadDir(-1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
files := make([]File, 0)
|
|
for _, entry := range entries {
|
|
day, ok := fileDay(entry.Name())
|
|
if !ok || !day.Add(24*time.Hour).After(logpolicy.Cutoff(w.now())) {
|
|
continue
|
|
}
|
|
if err := w.regular(entry.Name(), false); err != nil {
|
|
return nil, err
|
|
}
|
|
// Windows 目录枚举可能缓存仍打开文件的旧长度,必须通过文件句柄读取。
|
|
handle, err := w.root.Open(entry.Name())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
info, statErr := handle.Stat()
|
|
if err := errors.Join(statErr, handle.Close()); err != nil {
|
|
return nil, err
|
|
}
|
|
files = append(files, File{Name: entry.Name(), Bytes: info.Size(), Compressed: strings.HasSuffix(entry.Name(), ".gz")})
|
|
}
|
|
sort.Slice(files, func(i, j int) bool { return files[i].Name > files[j].Name })
|
|
return files, nil
|
|
}
|
|
|
|
// Read 仅允许固定名称的受管日志,不接受任意路径;压缩内容在服务端流式读取。
|
|
func (w *Writer) Read(ctx context.Context, name string, offset, limit int) (Page, error) {
|
|
w.archiveMu.Lock()
|
|
defer w.archiveMu.Unlock()
|
|
if _, ok := fileDay(name); !ok || offset < 0 || offset > 1000000 || limit < 1 || limit > 500 || w.closed {
|
|
return Page{}, ErrInvalid
|
|
}
|
|
page := Page{Lines: make([]string, 0, limit)}
|
|
cutoff := logpolicy.Cutoff(w.now())
|
|
position := 0
|
|
const maxResponseBytes = 2 * 1024 * 1024
|
|
bytes := 0
|
|
finished := errors.New("当前页读取完成")
|
|
err := w.scan(ctx, name, func(line []byte, at time.Time) error {
|
|
if at.Before(cutoff) {
|
|
return nil
|
|
}
|
|
// 先过滤再分页,偏移、统计和下一页判断均按实际可见记录计算。
|
|
visible, keep := presentLine(string(line))
|
|
if !keep {
|
|
return nil
|
|
}
|
|
position++
|
|
if position <= offset {
|
|
return nil
|
|
}
|
|
if len(page.Lines) >= limit || bytes+len(visible) > maxResponseBytes {
|
|
page.HasMore = true
|
|
return finished
|
|
}
|
|
page.Lines = append(page.Lines, visible)
|
|
bytes += len(visible)
|
|
return nil
|
|
})
|
|
if errors.Is(err, finished) {
|
|
err = nil
|
|
}
|
|
return page, err
|
|
}
|