package system import ( "bytes" "context" "errors" "fmt" "io" "io/fs" "os" pathpkg "path" "path/filepath" "sort" "strings" "time" "kra/app/system/biz" ) const ( defaultLogChunkLines = 500 maxLogChunkBytes = 2 * 1024 * 1024 logReadBlockSize = 64 * 1024 ) func (r *logFileRepo) configuredLogRoot() (root string, exists bool, err error) { admin := r.data.Runtime().Admin() if admin == nil || admin.Zap == nil || strings.TrimSpace(admin.Zap.Director) == "" { return "", false, biz.ErrLogRootUnavailable } root, err = filepath.Abs(admin.Zap.Director) if err != nil { return "", false, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err) } info, err := os.Stat(root) if errors.Is(err, fs.ErrNotExist) { return root, false, nil } if err != nil { return "", false, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err) } if !info.IsDir() { return "", false, biz.ErrLogRootUnavailable } return root, true, nil } func (r *logFileRepo) openConfiguredLogRoot() (root *os.Root, exists bool, err error) { rootPath, exists, err := r.configuredLogRoot() if err != nil || !exists { return nil, exists, err } root, err = os.OpenRoot(rootPath) if err != nil { return nil, false, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err) } return root, true, nil } func (r *logFileRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate, error) { if err := validateLogMonth(month); err != nil { return nil, err } logRoot, exists, err := r.openConfiguredLogRoot() if err != nil { return nil, err } if !exists { return []biz.LogDate{}, nil } defer logRoot.Close() entries, err := fs.ReadDir(logRoot.FS(), ".") if err != nil { return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err) } result := make([]biz.LogDate, 0) for _, entry := range entries { if err = ctx.Err(); err != nil { return nil, err } if entry.Type()&os.ModeSymlink != 0 || !entry.IsDir() || !strings.HasPrefix(entry.Name(), month+"-") { continue } if validateLogDate(entry.Name()) != nil { continue } count, countErr := countLogFiles(ctx, logRoot, entry.Name()) if countErr != nil { return nil, countErr } if count > 0 { result = append(result, biz.LogDate{Date: entry.Name(), FileCount: count}) } } sort.Slice(result, func(i, j int) bool { return result[i].Date < result[j].Date }) return result, nil } func (r *logFileRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile, error) { if err := validateLogDate(date); err != nil { return nil, err } logRoot, exists, err := r.openConfiguredLogRoot() if err != nil { return nil, err } if !exists { return []biz.LogFile{}, nil } defer logRoot.Close() info, err := logRoot.Lstat(date) if errors.Is(err, fs.ErrNotExist) { return []biz.LogFile{}, nil } if err != nil { return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err) } if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() { return []biz.LogFile{}, nil } dateRoot, err := logRoot.OpenRoot(date) if err != nil { return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err) } defer dateRoot.Close() result := make([]biz.LogFile, 0) err = fs.WalkDir(dateRoot.FS(), ".", func(path string, entry fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } if err := ctx.Err(); err != nil { return err } if path == "." { return nil } 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 } fileInfo, infoErr := entry.Info() if infoErr != nil { return infoErr } if fileInfo.Mode().IsRegular() { result = append(result, biz.LogFile{Path: path, Name: entry.Name(), Size: fileInfo.Size(), ModifiedAt: fileInfo.ModTime()}) } return nil }) if err != nil { return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err) } sort.Slice(result, func(i, j int) bool { return result[i].Path < result[j].Path }) return result, nil } func (r *logFileRepo) LogContent(ctx context.Context, date, apiPath string, cursor *int64) (*biz.LogContent, error) { result := &biz.LogContent{Date: date, Path: apiPath} if err := ctx.Err(); err != nil { return nil, err } if cursor != nil && *cursor < 0 { return nil, biz.ErrInvalidLogPath } file, info, err := r.openValidatedLogFile(date, apiPath) if err != nil { return nil, err } defer file.Close() end := info.Size() if cursor != nil && *cursor <= end { end = *cursor } start, limitedByBytes, err := findLogChunkStart(file, end) if err != nil { return nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err) } data, err := readLogRange(file, start, end) if err != nil { return nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err) } if int64(len(data)) < end-start { currentInfo, statErr := file.Stat() if statErr != nil || currentInfo.Size() < start { if statErr != nil { return nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, statErr) } return nil, biz.ErrLogFileUnreadable } info = currentInfo } result.Content = string(data) result.LineCount = countLogicalLines(data) result.NextCursor = start result.HasMore = start > 0 result.LimitedByBytes = limitedByBytes result.Size = info.Size() result.ModifiedAt = info.ModTime() return result, nil } func (r *logFileRepo) openValidatedLogFile(date, apiPath string) (file *os.File, info os.FileInfo, err error) { if err = validateLogDate(date); err != nil { return nil, nil, err } segments, err := validateLogAPIPath(apiPath) if err != nil { return nil, nil, err } logRoot, exists, err := r.openConfiguredLogRoot() if err != nil { return nil, nil, err } if !exists { return nil, nil, biz.ErrLogFileNotFound } defer logRoot.Close() dateInfo, err := logRoot.Lstat(date) if errors.Is(err, fs.ErrNotExist) { return nil, nil, biz.ErrLogFileNotFound } if err != nil { return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err) } if dateInfo.Mode()&os.ModeSymlink != 0 || !dateInfo.IsDir() { return nil, nil, biz.ErrInvalidLogPath } dateRoot, err := logRoot.OpenRoot(date) if err != nil { return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err) } defer dateRoot.Close() relativePath := "" var validatedInfo os.FileInfo for index, segment := range segments { relativePath = filepath.Join(relativePath, segment) validatedInfo, err = dateRoot.Lstat(relativePath) if errors.Is(err, fs.ErrNotExist) { return nil, nil, biz.ErrLogFileNotFound } if err != nil { return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err) } if validatedInfo.Mode()&os.ModeSymlink != 0 { return nil, nil, biz.ErrInvalidLogPath } if index < len(segments)-1 && !validatedInfo.IsDir() { return nil, nil, biz.ErrInvalidLogPath } } if validatedInfo == nil || !validatedInfo.Mode().IsRegular() { return nil, nil, biz.ErrInvalidLogPath } file, err = dateRoot.Open(relativePath) if err != nil { if errors.Is(err, fs.ErrNotExist) { return nil, nil, biz.ErrLogFileNotFound } return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err) } openedInfo, statErr := file.Stat() if statErr != nil || !openedInfo.Mode().IsRegular() || !os.SameFile(validatedInfo, openedInfo) { file.Close() if statErr != nil { return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, statErr) } return nil, nil, biz.ErrInvalidLogPath } return file, openedInfo, nil } func validateLogMonth(month string) error { parsed, err := time.Parse("2006-01", month) if err != nil || parsed.Format("2006-01") != month { return biz.ErrInvalidLogMonth } return nil } func validateLogDate(date string) error { parsed, err := time.Parse("2006-01-02", date) if err != nil || parsed.Format("2006-01-02") != date { return biz.ErrInvalidLogDate } return nil } func validateLogAPIPath(apiPath string) ([]string, error) { if apiPath == "" || strings.Contains(apiPath, "\\") || strings.Contains(apiPath, ":") || pathpkg.IsAbs(apiPath) { return nil, biz.ErrInvalidLogPath } segments := strings.Split(apiPath, "/") for _, segment := range segments { if segment == "" || segment == "." || segment == ".." { return nil, biz.ErrInvalidLogPath } } if !strings.EqualFold(pathpkg.Ext(apiPath), ".log") { return nil, biz.ErrInvalidLogPath } return segments, nil } func countLogFiles(ctx context.Context, logRoot *os.Root, date string) (count int, err error) { dateInfo, err := logRoot.Lstat(date) if err != nil || dateInfo.Mode()&os.ModeSymlink != 0 || !dateInfo.IsDir() { return 0, err } dateRoot, err := logRoot.OpenRoot(date) if err != nil { return 0, err } defer dateRoot.Close() err = fs.WalkDir(dateRoot.FS(), ".", func(path string, entry fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } if err := ctx.Err(); err != nil { return err } if path == "." { return nil } 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 } if info.Mode().IsRegular() { count++ } return nil }) if err != nil { return 0, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err) } return count, nil } func readLogRange(file *os.File, start, end int64) ([]byte, error) { if end <= start { return []byte{}, nil } data := make([]byte, int(end-start)) n, err := file.ReadAt(data, start) if err != nil && !errors.Is(err, io.EOF) { return nil, err } return data[:n], nil } func findLogChunkStart(file *os.File, end int64) (start int64, limitedByBytes bool, err error) { if end <= 0 { return 0, false, nil } lastByte := []byte{0} if _, err = file.ReadAt(lastByte, end-1); err != nil { return 0, false, err } targetNewlines := defaultLogChunkLines if lastByte[0] == '\n' { targetNewlines++ } position, scanned, newlines := end, int64(0), 0 for position > 0 && scanned < maxLogChunkBytes { readSize := int64(logReadBlockSize) if readSize > position { readSize = position } if remaining := int64(maxLogChunkBytes) - scanned; readSize > remaining { readSize = remaining } blockStart := position - readSize block := make([]byte, int(readSize)) n, readErr := file.ReadAt(block, blockStart) if readErr != nil && !errors.Is(readErr, io.EOF) { return 0, false, readErr } for index := n - 1; index >= 0; index-- { if block[index] == '\n' { newlines++ if newlines == targetNewlines { return blockStart + int64(index) + 1, false, nil } } } scanned += int64(n) position = blockStart } if position > 0 { return end - int64(maxLogChunkBytes), true, nil } return 0, false, nil } func countLogicalLines(data []byte) int { if len(data) == 0 { return 0 } count := bytes.Count(data, []byte{'\n'}) if data[len(data)-1] != '\n' { count++ } return count }