mirror of
https://github.com/ivuorinen/gibidify.git
synced 2026-02-09 22:47:52 +00:00
feat: update go to 1.25, add permissions and envs (#49)
* chore(ci): update go to 1.25, add permissions and envs * fix(ci): update pr-lint.yml * chore: update go, fix linting * fix: tests and linting * fix(lint): lint fixes, renovate should now pass * fix: updates, security upgrades * chore: workflow updates, lint * fix: more lint, checkmake, and other fixes * fix: more lint, convert scripts to POSIX compliant * fix: simplify codeql workflow * tests: increase test coverage, fix found issues * fix(lint): editorconfig checking, add to linters * fix(lint): shellcheck, add to linters * fix(lint): apply cr comment suggestions * fix(ci): remove step-security/harden-runner * fix(lint): remove duplication, apply cr fixes * fix(ci): tests in CI/CD pipeline * chore(lint): deduplication of strings * fix(lint): apply cr comment suggestions * fix(ci): actionlint * fix(lint): apply cr comment suggestions * chore: lint, add deps management
This commit is contained in:
@@ -3,6 +3,7 @@ package fileproc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -13,7 +14,7 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/ivuorinen/gibidify/config"
|
||||
"github.com/ivuorinen/gibidify/utils"
|
||||
"github.com/ivuorinen/gibidify/gibidiutils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -33,6 +34,26 @@ type WriteRequest struct {
|
||||
Reader io.Reader
|
||||
}
|
||||
|
||||
// multiReaderCloser wraps an io.Reader with a Close method that closes underlying closers.
|
||||
type multiReaderCloser struct {
|
||||
reader io.Reader
|
||||
closers []io.Closer
|
||||
}
|
||||
|
||||
func (m *multiReaderCloser) Read(p []byte) (n int, err error) {
|
||||
return m.reader.Read(p)
|
||||
}
|
||||
|
||||
func (m *multiReaderCloser) Close() error {
|
||||
var firstErr error
|
||||
for _, c := range m.closers {
|
||||
if err := c.Close(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// FileProcessor handles file processing operations.
|
||||
type FileProcessor struct {
|
||||
rootPath string
|
||||
@@ -58,6 +79,34 @@ func NewFileProcessorWithMonitor(rootPath string, monitor *ResourceMonitor) *Fil
|
||||
}
|
||||
}
|
||||
|
||||
// checkContextCancellation checks if context is cancelled and logs an error if so.
|
||||
// Returns true if context is cancelled, false otherwise.
|
||||
func (p *FileProcessor) checkContextCancellation(ctx context.Context, filePath, stage string) bool {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Format stage with leading space if provided
|
||||
stageMsg := stage
|
||||
if stage != "" {
|
||||
stageMsg = " " + stage
|
||||
}
|
||||
gibidiutils.LogErrorf(
|
||||
gibidiutils.NewStructuredError(
|
||||
gibidiutils.ErrorTypeValidation,
|
||||
gibidiutils.CodeResourceLimitTimeout,
|
||||
fmt.Sprintf("file processing cancelled%s", stageMsg),
|
||||
filePath,
|
||||
nil,
|
||||
),
|
||||
"File processing cancelled%s: %s",
|
||||
stageMsg,
|
||||
filePath,
|
||||
)
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessFile reads the file at filePath and sends a formatted output to outCh.
|
||||
// It automatically chooses between loading the entire file or streaming based on file size.
|
||||
func ProcessFile(filePath string, outCh chan<- WriteRequest, rootPath string) {
|
||||
@@ -67,7 +116,13 @@ func ProcessFile(filePath string, outCh chan<- WriteRequest, rootPath string) {
|
||||
}
|
||||
|
||||
// ProcessFileWithMonitor processes a file using a shared resource monitor.
|
||||
func ProcessFileWithMonitor(ctx context.Context, filePath string, outCh chan<- WriteRequest, rootPath string, monitor *ResourceMonitor) {
|
||||
func ProcessFileWithMonitor(
|
||||
ctx context.Context,
|
||||
filePath string,
|
||||
outCh chan<- WriteRequest,
|
||||
rootPath string,
|
||||
monitor *ResourceMonitor,
|
||||
) {
|
||||
processor := NewFileProcessorWithMonitor(rootPath, monitor)
|
||||
processor.ProcessWithContext(ctx, filePath, outCh)
|
||||
}
|
||||
@@ -86,10 +141,17 @@ func (p *FileProcessor) ProcessWithContext(ctx context.Context, filePath string,
|
||||
|
||||
// Wait for rate limiting
|
||||
if err := p.resourceMonitor.WaitForRateLimit(fileCtx); err != nil {
|
||||
if err == context.DeadlineExceeded {
|
||||
utils.LogErrorf(
|
||||
utils.NewStructuredError(utils.ErrorTypeValidation, utils.CodeResourceLimitTimeout, "file processing timeout during rate limiting", filePath, nil),
|
||||
"File processing timeout during rate limiting: %s", filePath,
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
gibidiutils.LogErrorf(
|
||||
gibidiutils.NewStructuredError(
|
||||
gibidiutils.ErrorTypeValidation,
|
||||
gibidiutils.CodeResourceLimitTimeout,
|
||||
"file processing timeout during rate limiting",
|
||||
filePath,
|
||||
nil,
|
||||
),
|
||||
"File processing timeout during rate limiting: %s",
|
||||
filePath,
|
||||
)
|
||||
}
|
||||
return
|
||||
@@ -103,10 +165,17 @@ func (p *FileProcessor) ProcessWithContext(ctx context.Context, filePath string,
|
||||
|
||||
// Acquire read slot for concurrent processing
|
||||
if err := p.resourceMonitor.AcquireReadSlot(fileCtx); err != nil {
|
||||
if err == context.DeadlineExceeded {
|
||||
utils.LogErrorf(
|
||||
utils.NewStructuredError(utils.ErrorTypeValidation, utils.CodeResourceLimitTimeout, "file processing timeout waiting for read slot", filePath, nil),
|
||||
"File processing timeout waiting for read slot: %s", filePath,
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
gibidiutils.LogErrorf(
|
||||
gibidiutils.NewStructuredError(
|
||||
gibidiutils.ErrorTypeValidation,
|
||||
gibidiutils.CodeResourceLimitTimeout,
|
||||
"file processing timeout waiting for read slot",
|
||||
filePath,
|
||||
nil,
|
||||
),
|
||||
"File processing timeout waiting for read slot: %s",
|
||||
filePath,
|
||||
)
|
||||
}
|
||||
return
|
||||
@@ -115,7 +184,7 @@ func (p *FileProcessor) ProcessWithContext(ctx context.Context, filePath string,
|
||||
|
||||
// Check hard memory limits before processing
|
||||
if err := p.resourceMonitor.CheckHardMemoryLimit(); err != nil {
|
||||
utils.LogErrorf(err, "Hard memory limit check failed for file: %s", filePath)
|
||||
gibidiutils.LogErrorf(err, "Hard memory limit check failed for file: %s", filePath)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -138,7 +207,6 @@ func (p *FileProcessor) ProcessWithContext(ctx context.Context, filePath string,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// validateFileWithLimits checks if the file can be processed with resource limits.
|
||||
func (p *FileProcessor) validateFileWithLimits(ctx context.Context, filePath string) (os.FileInfo, error) {
|
||||
// Check context cancellation
|
||||
@@ -150,24 +218,27 @@ func (p *FileProcessor) validateFileWithLimits(ctx context.Context, filePath str
|
||||
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
structErr := utils.WrapError(err, utils.ErrorTypeFileSystem, utils.CodeFSAccess, "failed to stat file").WithFilePath(filePath)
|
||||
utils.LogErrorf(structErr, "Failed to stat file %s", filePath)
|
||||
return nil, err
|
||||
structErr := gibidiutils.WrapError(
|
||||
err, gibidiutils.ErrorTypeFileSystem, gibidiutils.CodeFSAccess,
|
||||
"failed to stat file",
|
||||
).WithFilePath(filePath)
|
||||
gibidiutils.LogErrorf(structErr, "Failed to stat file %s", filePath)
|
||||
return nil, structErr
|
||||
}
|
||||
|
||||
// Check traditional size limit
|
||||
if fileInfo.Size() > p.sizeLimit {
|
||||
context := map[string]interface{}{
|
||||
filesizeContext := map[string]interface{}{
|
||||
"file_size": fileInfo.Size(),
|
||||
"size_limit": p.sizeLimit,
|
||||
}
|
||||
utils.LogErrorf(
|
||||
utils.NewStructuredError(
|
||||
utils.ErrorTypeValidation,
|
||||
utils.CodeValidationSize,
|
||||
gibidiutils.LogErrorf(
|
||||
gibidiutils.NewStructuredError(
|
||||
gibidiutils.ErrorTypeValidation,
|
||||
gibidiutils.CodeValidationSize,
|
||||
fmt.Sprintf("file size (%d bytes) exceeds limit (%d bytes)", fileInfo.Size(), p.sizeLimit),
|
||||
filePath,
|
||||
context,
|
||||
filesizeContext,
|
||||
),
|
||||
"Skipping large file %s", filePath,
|
||||
)
|
||||
@@ -176,7 +247,7 @@ func (p *FileProcessor) validateFileWithLimits(ctx context.Context, filePath str
|
||||
|
||||
// Check resource limits
|
||||
if err := p.resourceMonitor.ValidateFileProcessing(filePath, fileInfo.Size()); err != nil {
|
||||
utils.LogErrorf(err, "Resource limit validation failed for file: %s", filePath)
|
||||
gibidiutils.LogErrorf(err, "Resource limit validation failed for file: %s", filePath)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -192,66 +263,54 @@ func (p *FileProcessor) getRelativePath(filePath string) string {
|
||||
return relPath
|
||||
}
|
||||
|
||||
|
||||
// processInMemoryWithContext loads the entire file into memory with context awareness.
|
||||
func (p *FileProcessor) processInMemoryWithContext(ctx context.Context, filePath, relPath string, outCh chan<- WriteRequest) {
|
||||
func (p *FileProcessor) processInMemoryWithContext(
|
||||
ctx context.Context,
|
||||
filePath, relPath string,
|
||||
outCh chan<- WriteRequest,
|
||||
) {
|
||||
// Check context before reading
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
utils.LogErrorf(
|
||||
utils.NewStructuredError(utils.ErrorTypeValidation, utils.CodeResourceLimitTimeout, "file processing cancelled", filePath, nil),
|
||||
"File processing cancelled: %s", filePath,
|
||||
)
|
||||
if p.checkContextCancellation(ctx, filePath, "") {
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filePath) // #nosec G304 - filePath is validated by walker
|
||||
// #nosec G304 - filePath is validated by walker
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
structErr := utils.WrapError(err, utils.ErrorTypeProcessing, utils.CodeProcessingFileRead, "failed to read file").WithFilePath(filePath)
|
||||
utils.LogErrorf(structErr, "Failed to read file %s", filePath)
|
||||
structErr := gibidiutils.WrapError(
|
||||
err, gibidiutils.ErrorTypeProcessing, gibidiutils.CodeProcessingFileRead,
|
||||
"failed to read file",
|
||||
).WithFilePath(filePath)
|
||||
gibidiutils.LogErrorf(structErr, "Failed to read file %s", filePath)
|
||||
return
|
||||
}
|
||||
|
||||
// Check context again after reading
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
utils.LogErrorf(
|
||||
utils.NewStructuredError(utils.ErrorTypeValidation, utils.CodeResourceLimitTimeout, "file processing cancelled after read", filePath, nil),
|
||||
"File processing cancelled after read: %s", filePath,
|
||||
)
|
||||
if p.checkContextCancellation(ctx, filePath, "after read") {
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// Try to send the result, but respect context cancellation
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
utils.LogErrorf(
|
||||
utils.NewStructuredError(utils.ErrorTypeValidation, utils.CodeResourceLimitTimeout, "file processing cancelled before output", filePath, nil),
|
||||
"File processing cancelled before output: %s", filePath,
|
||||
)
|
||||
// Check context before sending output
|
||||
if p.checkContextCancellation(ctx, filePath, "before output") {
|
||||
return
|
||||
case outCh <- WriteRequest{
|
||||
}
|
||||
|
||||
outCh <- WriteRequest{
|
||||
Path: relPath,
|
||||
Content: p.formatContent(relPath, string(content)),
|
||||
IsStream: false,
|
||||
}:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// processStreamingWithContext creates a streaming reader for large files with context awareness.
|
||||
func (p *FileProcessor) processStreamingWithContext(ctx context.Context, filePath, relPath string, outCh chan<- WriteRequest) {
|
||||
func (p *FileProcessor) processStreamingWithContext(
|
||||
ctx context.Context,
|
||||
filePath, relPath string,
|
||||
outCh chan<- WriteRequest,
|
||||
) {
|
||||
// Check context before creating reader
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
utils.LogErrorf(
|
||||
utils.NewStructuredError(utils.ErrorTypeValidation, utils.CodeResourceLimitTimeout, "streaming processing cancelled", filePath, nil),
|
||||
"Streaming processing cancelled: %s", filePath,
|
||||
)
|
||||
if p.checkContextCancellation(ctx, filePath, "before streaming") {
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
reader := p.createStreamReaderWithContext(ctx, filePath, relPath)
|
||||
@@ -259,43 +318,47 @@ func (p *FileProcessor) processStreamingWithContext(ctx context.Context, filePat
|
||||
return // Error already logged
|
||||
}
|
||||
|
||||
// Try to send the result, but respect context cancellation
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
utils.LogErrorf(
|
||||
utils.NewStructuredError(utils.ErrorTypeValidation, utils.CodeResourceLimitTimeout, "streaming processing cancelled before output", filePath, nil),
|
||||
"Streaming processing cancelled before output: %s", filePath,
|
||||
)
|
||||
// Check context before sending output
|
||||
if p.checkContextCancellation(ctx, filePath, "before streaming output") {
|
||||
// Close the reader to prevent file descriptor leak
|
||||
if closer, ok := reader.(io.Closer); ok {
|
||||
_ = closer.Close()
|
||||
}
|
||||
return
|
||||
case outCh <- WriteRequest{
|
||||
}
|
||||
|
||||
outCh <- WriteRequest{
|
||||
Path: relPath,
|
||||
Content: "", // Empty since content is in Reader
|
||||
IsStream: true,
|
||||
Reader: reader,
|
||||
}:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// createStreamReaderWithContext creates a reader that combines header and file content with context awareness.
|
||||
func (p *FileProcessor) createStreamReaderWithContext(ctx context.Context, filePath, relPath string) io.Reader {
|
||||
// Check context before opening file
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if p.checkContextCancellation(ctx, filePath, "before opening file") {
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
file, err := os.Open(filePath) // #nosec G304 - filePath is validated by walker
|
||||
// #nosec G304 - filePath is validated by walker
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
structErr := utils.WrapError(err, utils.ErrorTypeProcessing, utils.CodeProcessingFileRead, "failed to open file for streaming").WithFilePath(filePath)
|
||||
utils.LogErrorf(structErr, "Failed to open file for streaming %s", filePath)
|
||||
structErr := gibidiutils.WrapError(
|
||||
err, gibidiutils.ErrorTypeProcessing, gibidiutils.CodeProcessingFileRead,
|
||||
"failed to open file for streaming",
|
||||
).WithFilePath(filePath)
|
||||
gibidiutils.LogErrorf(structErr, "Failed to open file for streaming %s", filePath)
|
||||
return nil
|
||||
}
|
||||
// Note: file will be closed by the writer
|
||||
|
||||
header := p.formatHeader(relPath)
|
||||
return io.MultiReader(header, file)
|
||||
// Wrap in multiReaderCloser to ensure file is closed even on cancellation
|
||||
return &multiReaderCloser{
|
||||
reader: io.MultiReader(header, file),
|
||||
closers: []io.Closer{file},
|
||||
}
|
||||
}
|
||||
|
||||
// formatContent formats the file content with header.
|
||||
|
||||
Reference in New Issue
Block a user