178 lines
5.0 KiB
Go
178 lines
5.0 KiB
Go
package data
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"kra/internal/biz"
|
|
)
|
|
|
|
func (r *logFileRepo) logRoot() (string, error) {
|
|
admin := r.data.runtime.Admin()
|
|
if admin == nil || admin.Zap == nil || strings.TrimSpace(admin.Zap.Director) == "" {
|
|
return "", biz.ErrLogRootUnavailable
|
|
}
|
|
root, err := filepath.Abs(admin.Zap.Director)
|
|
if err != nil {
|
|
return "", errors.Join(biz.ErrLogRootUnavailable, err)
|
|
}
|
|
info, err := os.Stat(root)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return root, nil
|
|
}
|
|
if err != nil {
|
|
return "", errors.Join(biz.ErrLogRootUnavailable, err)
|
|
}
|
|
if !info.IsDir() {
|
|
return "", biz.ErrLogRootUnavailable
|
|
}
|
|
return root, nil
|
|
}
|
|
func (r *logFileRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate, error) {
|
|
if parsed, err := time.Parse("2006-01", month); err != nil || parsed.Format("2006-01") != month {
|
|
return nil, biz.ErrInvalidLogMonth
|
|
}
|
|
root, err := r.logRoot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entries, err := os.ReadDir(root)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return []biz.LogDate{}, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := []biz.LogDate{}
|
|
for _, entry := range entries {
|
|
if ctx.Err() != nil {
|
|
return nil, ctx.Err()
|
|
}
|
|
if entry.Type()&os.ModeSymlink != 0 || !entry.IsDir() || !strings.HasPrefix(entry.Name(), month+"-") {
|
|
continue
|
|
}
|
|
if parsed, parseErr := time.Parse("2006-01-02", entry.Name()); parseErr != nil || parsed.Format("2006-01-02") != entry.Name() {
|
|
continue
|
|
}
|
|
count := 0
|
|
_ = filepath.WalkDir(filepath.Join(root, entry.Name()), func(_ string, item fs.DirEntry, walkErr error) error {
|
|
if walkErr == nil && !item.IsDir() && strings.EqualFold(filepath.Ext(item.Name()), ".log") {
|
|
count++
|
|
}
|
|
return nil
|
|
})
|
|
if count > 0 {
|
|
out = append(out, biz.LogDate{Date: entry.Name(), FileCount: count})
|
|
}
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Date < out[j].Date })
|
|
return out, nil
|
|
}
|
|
func (r *logFileRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile, error) {
|
|
if parsed, err := time.Parse("2006-01-02", date); err != nil || parsed.Format("2006-01-02") != date {
|
|
return nil, biz.ErrInvalidLogDate
|
|
}
|
|
root, err := r.logRoot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dateRoot := filepath.Join(root, date)
|
|
out := []biz.LogFile{}
|
|
err = filepath.WalkDir(dateRoot, func(path string, entry fs.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
if errors.Is(walkErr, fs.ErrNotExist) {
|
|
return nil
|
|
}
|
|
return walkErr
|
|
}
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
if entry.Type()&os.ModeSymlink != 0 {
|
|
if entry.IsDir() {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), ".log") {
|
|
return nil
|
|
}
|
|
info, infoErr := entry.Info()
|
|
if infoErr != nil {
|
|
return infoErr
|
|
}
|
|
relative, _ := filepath.Rel(dateRoot, path)
|
|
out = append(out, biz.LogFile{Path: filepath.ToSlash(relative), Name: entry.Name(), Size: info.Size(), ModifiedAt: info.ModTime()})
|
|
return nil
|
|
})
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Path < out[j].Path })
|
|
return out, err
|
|
}
|
|
func (r *logFileRepo) LogContent(ctx context.Context, date, path string, cursor *int64) (*biz.LogContent, error) {
|
|
if parsed, err := time.Parse("2006-01-02", date); err != nil || parsed.Format("2006-01-02") != date {
|
|
return nil, biz.ErrInvalidLogDate
|
|
}
|
|
root, err := r.logRoot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dateRoot := filepath.Join(root, date)
|
|
target := filepath.Clean(filepath.Join(dateRoot, filepath.FromSlash(path)))
|
|
if target == dateRoot || !strings.HasPrefix(target, dateRoot+string(os.PathSeparator)) || !strings.EqualFold(filepath.Ext(target), ".log") {
|
|
return nil, biz.ErrInvalidLogPath
|
|
}
|
|
info, err := os.Lstat(target)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, biz.ErrLogFileNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, errors.Join(biz.ErrLogFileUnreadable, err)
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
|
|
return nil, biz.ErrInvalidLogPath
|
|
}
|
|
end := info.Size()
|
|
if cursor != nil && *cursor >= 0 && *cursor < end {
|
|
end = *cursor
|
|
}
|
|
start := end - int64(2*1024*1024)
|
|
limited := start > 0
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
file, err := os.Open(target)
|
|
if err != nil {
|
|
return nil, errors.Join(biz.ErrLogFileUnreadable, err)
|
|
}
|
|
defer file.Close()
|
|
data := make([]byte, end-start)
|
|
n, err := file.ReadAt(data, start)
|
|
if err != nil && n == 0 {
|
|
return nil, errors.Join(biz.ErrLogFileUnreadable, err)
|
|
}
|
|
data = data[:n]
|
|
lines := bytes.Split(data, []byte("\n"))
|
|
if len(lines) > 501 {
|
|
drop := len(lines) - 501
|
|
offset := 0
|
|
for _, line := range lines[:drop] {
|
|
offset += len(line) + 1
|
|
}
|
|
start += int64(offset)
|
|
data = data[offset:]
|
|
limited = true
|
|
}
|
|
lineCount := bytes.Count(data, []byte("\n"))
|
|
if len(data) > 0 && data[len(data)-1] != '\n' {
|
|
lineCount++
|
|
}
|
|
return &biz.LogContent{Date: date, Path: path, Content: string(data), LineCount: lineCount, NextCursor: start, HasMore: start > 0, LimitedByBytes: limited, Size: info.Size(), ModifiedAt: info.ModTime()}, nil
|
|
}
|