package data import ( "bytes" "context" "errors" "io/fs" "os" "path/filepath" "sort" "strings" "time" "kra/internal/biz" ) func (r *auditRepo) 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 *auditRepo) 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 *auditRepo) 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 *auditRepo) 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 }