mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-09-01 04:38:47 +02:00
- Add cut_to_file() and cut_space() utility functions for file processing and field extraction - Add comprehensive E2E tests for decision condition routing with function/command execution - Add test workflows for decision conditions and inline decision execution - Add short-mode skip guards to all cloud E2E tests to allow quick test runs - Register new functions in constants and goja runtime
970 lines
31 KiB
Go
970 lines
31 KiB
Go
package functions
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/dop251/goja"
|
|
"github.com/j3ssie/osmedeus/v5/internal/fileio"
|
|
"github.com/j3ssie/osmedeus/v5/internal/logger"
|
|
"github.com/j3ssie/osmedeus/v5/internal/terminal"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// fileExists checks if a file exists
|
|
func (vf *vmFunc) fileExists(call goja.FunctionCall) goja.Value {
|
|
path := call.Argument(0).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("fileExists"), zap.String("path", path))
|
|
|
|
if path == "undefined" || path == "" {
|
|
logger.Get().Warn("fileExists: empty path provided")
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
_, err := os.Stat(path)
|
|
exists := err == nil
|
|
|
|
logger.Get().Debug(terminal.HiGreen("fileExists")+" result", zap.String("path", path), zap.Bool("exists", exists))
|
|
return vf.vm.ToValue(exists)
|
|
}
|
|
|
|
// fileLength returns the number of lines in a file.
|
|
// Uses memory-mapped I/O for large files (>1MB) for better performance.
|
|
func (vf *vmFunc) fileLength(call goja.FunctionCall) goja.Value {
|
|
path := call.Argument(0).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("fileLength"), zap.String("path", path))
|
|
|
|
if path == "undefined" || path == "" {
|
|
logger.Get().Warn("fileLength: empty path provided")
|
|
return vf.vm.ToValue(0)
|
|
}
|
|
|
|
count, err := fileio.CountNonEmptyLines(path)
|
|
if err != nil {
|
|
logger.Get().Warn("fileLength: failed to count lines", zap.String("path", path), zap.Error(err))
|
|
return vf.vm.ToValue(0)
|
|
}
|
|
|
|
logger.Get().Debug(terminal.HiGreen("fileLength")+" result", zap.String("path", path), zap.Int("count", count))
|
|
return vf.vm.ToValue(count)
|
|
}
|
|
|
|
// dirLength returns the number of entries in a directory
|
|
func (vf *vmFunc) dirLength(call goja.FunctionCall) goja.Value {
|
|
path := call.Argument(0).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("dirLength"), zap.String("path", path))
|
|
|
|
if path == "undefined" || path == "" {
|
|
logger.Get().Warn("dirLength: empty path provided")
|
|
return vf.vm.ToValue(0)
|
|
}
|
|
|
|
entries, err := os.ReadDir(path)
|
|
if err != nil {
|
|
logger.Get().Warn("dirLength: failed to read directory", zap.String("path", path), zap.Error(err))
|
|
return vf.vm.ToValue(0)
|
|
}
|
|
|
|
logger.Get().Debug(terminal.HiGreen("dirLength")+" result", zap.String("path", path), zap.Int("count", len(entries)))
|
|
return vf.vm.ToValue(len(entries))
|
|
}
|
|
|
|
// fileContains checks if a file contains a pattern (regex)
|
|
func (vf *vmFunc) fileContains(call goja.FunctionCall) goja.Value {
|
|
path := call.Argument(0).String()
|
|
pattern := call.Argument(1).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("fileContains"), zap.String("path", path), zap.String("pattern", pattern))
|
|
|
|
if path == "undefined" || path == "" || pattern == "undefined" || pattern == "" {
|
|
logger.Get().Warn("fileContains: empty path or pattern provided")
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
logger.Get().Warn("fileContains: failed to read file", zap.String("path", path), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
// Try regex match first
|
|
re, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
logger.Get().Debug(terminal.HiGreen("fileContains")+": invalid regex, falling back to string contains", zap.String("pattern", pattern))
|
|
// Fall back to simple string contains
|
|
contains := strings.Contains(string(content), pattern)
|
|
logger.Get().Debug(terminal.HiGreen("fileContains")+" result", zap.String("path", path), zap.Bool("contains", contains))
|
|
return vf.vm.ToValue(contains)
|
|
}
|
|
|
|
matches := re.MatchString(string(content))
|
|
logger.Get().Debug(terminal.HiGreen("fileContains")+" result", zap.String("path", path), zap.Bool("matches", matches))
|
|
return vf.vm.ToValue(matches)
|
|
}
|
|
|
|
// regexExtract extracts matching lines from a file
|
|
func (vf *vmFunc) regexExtract(call goja.FunctionCall) goja.Value {
|
|
path := call.Argument(0).String()
|
|
pattern := call.Argument(1).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("regexExtract"), zap.String("path", path), zap.String("pattern", pattern))
|
|
|
|
if path == "undefined" || path == "" || pattern == "undefined" || pattern == "" {
|
|
logger.Get().Warn("regexExtract: empty path or pattern provided")
|
|
return vf.vm.ToValue([]string{})
|
|
}
|
|
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
logger.Get().Warn("regexExtract: failed to open file", zap.String("path", path), zap.Error(err))
|
|
return vf.vm.ToValue([]string{})
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
|
|
re, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
logger.Get().Warn("regexExtract: invalid regex pattern", zap.String("pattern", pattern), zap.Error(err))
|
|
return vf.vm.ToValue([]string{})
|
|
}
|
|
|
|
var matches []string
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if re.MatchString(line) {
|
|
matches = append(matches, line)
|
|
}
|
|
}
|
|
|
|
logger.Get().Debug(terminal.HiGreen("regexExtract")+" result", zap.String("path", path), zap.Int("matches", len(matches)))
|
|
return vf.vm.ToValue(matches)
|
|
}
|
|
|
|
// readFile reads the entire contents of a file
|
|
func (vf *vmFunc) readFile(call goja.FunctionCall) goja.Value {
|
|
path := call.Argument(0).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("readFile"), zap.String("path", path))
|
|
|
|
if path == "undefined" || path == "" {
|
|
logger.Get().Warn("readFile: empty path provided")
|
|
return vf.vm.ToValue("")
|
|
}
|
|
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
logger.Get().Warn("readFile: failed to read file", zap.String("path", path), zap.Error(err))
|
|
return vf.vm.ToValue("")
|
|
}
|
|
|
|
logger.Get().Debug(terminal.HiGreen("readFile")+" result", zap.String("path", path), zap.Int("bytes", len(content)))
|
|
return vf.vm.ToValue(string(content))
|
|
}
|
|
|
|
// readLines reads a file and returns an array of lines
|
|
func (vf *vmFunc) readLines(call goja.FunctionCall) goja.Value {
|
|
path := call.Argument(0).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("readLines"), zap.String("path", path))
|
|
|
|
if path == "undefined" || path == "" {
|
|
logger.Get().Warn("readLines: empty path provided")
|
|
return vf.vm.ToValue([]string{})
|
|
}
|
|
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
logger.Get().Warn("readLines: failed to open file", zap.String("path", path), zap.Error(err))
|
|
return vf.vm.ToValue([]string{})
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
|
|
var lines []string
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
lines = append(lines, scanner.Text())
|
|
}
|
|
|
|
logger.Get().Debug(terminal.HiGreen("readLines")+" result", zap.String("path", path), zap.Int("lines", len(lines)))
|
|
return vf.vm.ToValue(lines)
|
|
}
|
|
|
|
func (vf *vmFunc) createFolder(call goja.FunctionCall) goja.Value {
|
|
path := call.Argument(0).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("createFolder"), zap.String("path", path))
|
|
|
|
if path == "undefined" || path == "" {
|
|
logger.Get().Warn("createFolder: empty path provided")
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
err := os.MkdirAll(path, 0755)
|
|
if err != nil {
|
|
logger.Get().Warn("createFolder: failed to create folder", zap.String("path", path), zap.Error(err))
|
|
}
|
|
return vf.vm.ToValue(err == nil)
|
|
}
|
|
|
|
func (vf *vmFunc) appendFile(call goja.FunctionCall) goja.Value {
|
|
dest := call.Argument(0).String()
|
|
source := call.Argument(1).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("appendFile"), zap.String("dest", dest), zap.String("source", source))
|
|
|
|
if dest == "undefined" || dest == "" || source == "undefined" || source == "" {
|
|
logger.Get().Warn("appendFile: empty dest or source provided")
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
content, err := os.ReadFile(source)
|
|
if err != nil {
|
|
logger.Get().Warn("appendFile: failed to read source file", zap.String("source", source), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
|
|
logger.Get().Warn("appendFile: failed to create destination directory", zap.String("dest", dest), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
f, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
|
if err != nil {
|
|
logger.Get().Warn("appendFile: failed to open destination file", zap.String("dest", dest), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
_, err = f.Write(content)
|
|
if err != nil {
|
|
logger.Get().Warn("appendFile: failed to append content", zap.String("dest", dest), zap.Error(err))
|
|
}
|
|
return vf.vm.ToValue(err == nil)
|
|
}
|
|
|
|
func readMatchedLines(path string, matcher func(string) bool) ([]string, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
var lines []string
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if matcher(line) {
|
|
lines = append(lines, line)
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return lines, nil
|
|
}
|
|
|
|
func writeLinesToFile(path string, lines []string) error {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
content := strings.Join(lines, "\n")
|
|
if len(lines) > 0 {
|
|
content += "\n"
|
|
}
|
|
return os.WriteFile(path, []byte(content), 0644)
|
|
}
|
|
|
|
func (vf *vmFunc) grepStringToFile(call goja.FunctionCall) goja.Value {
|
|
dest := call.Argument(0).String()
|
|
source := call.Argument(1).String()
|
|
filter := call.Argument(2).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("grepStringToFile"), zap.String("dest", dest), zap.String("source", source), zap.String("filter", filter))
|
|
|
|
if dest == "undefined" || dest == "" || source == "undefined" || source == "" || filter == "undefined" || filter == "" {
|
|
logger.Get().Warn("grepStringToFile: empty dest, source, or filter provided")
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
lines, err := readMatchedLines(source, func(line string) bool {
|
|
return strings.Contains(line, filter)
|
|
})
|
|
if err != nil {
|
|
logger.Get().Warn("grepStringToFile: failed to read source", zap.String("source", source), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
if err := writeLinesToFile(dest, lines); err != nil {
|
|
logger.Get().Warn("grepStringToFile: failed to write destination", zap.String("dest", dest), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
return vf.vm.ToValue(true)
|
|
}
|
|
|
|
func (vf *vmFunc) grepRegexToFile(call goja.FunctionCall) goja.Value {
|
|
dest := call.Argument(0).String()
|
|
source := call.Argument(1).String()
|
|
pattern := call.Argument(2).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("grepRegexToFile"), zap.String("dest", dest), zap.String("source", source), zap.String("pattern", pattern))
|
|
|
|
if dest == "undefined" || dest == "" || source == "undefined" || source == "" || pattern == "undefined" || pattern == "" {
|
|
logger.Get().Warn("grepRegexToFile: empty dest, source, or pattern provided")
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
re, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
logger.Get().Warn("grepRegexToFile: invalid regex pattern", zap.String("pattern", pattern), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
lines, err := readMatchedLines(source, func(line string) bool {
|
|
return re.MatchString(line)
|
|
})
|
|
if err != nil {
|
|
logger.Get().Warn("grepRegexToFile: failed to read source", zap.String("source", source), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
if err := writeLinesToFile(dest, lines); err != nil {
|
|
logger.Get().Warn("grepRegexToFile: failed to write destination", zap.String("dest", dest), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
return vf.vm.ToValue(true)
|
|
}
|
|
|
|
func (vf *vmFunc) grepString(call goja.FunctionCall) goja.Value {
|
|
source := call.Argument(0).String()
|
|
filter := call.Argument(1).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("grepString"), zap.String("source", source), zap.String("filter", filter))
|
|
|
|
if source == "undefined" || source == "" || filter == "undefined" || filter == "" {
|
|
logger.Get().Warn("grepString: empty source or filter provided")
|
|
return vf.vm.ToValue("")
|
|
}
|
|
|
|
lines, err := readMatchedLines(source, func(line string) bool {
|
|
return strings.Contains(line, filter)
|
|
})
|
|
if err != nil {
|
|
logger.Get().Warn("grepString: failed to read source", zap.String("source", source), zap.Error(err))
|
|
return vf.vm.ToValue("")
|
|
}
|
|
|
|
return vf.vm.ToValue(strings.Join(lines, "\n"))
|
|
}
|
|
|
|
func (vf *vmFunc) grepRegex(call goja.FunctionCall) goja.Value {
|
|
source := call.Argument(0).String()
|
|
pattern := call.Argument(1).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("grepRegex"), zap.String("source", source), zap.String("pattern", pattern))
|
|
|
|
if source == "undefined" || source == "" || pattern == "undefined" || pattern == "" {
|
|
logger.Get().Warn("grepRegex: empty source or pattern provided")
|
|
return vf.vm.ToValue("")
|
|
}
|
|
|
|
re, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
logger.Get().Warn("grepRegex: invalid regex pattern", zap.String("pattern", pattern), zap.Error(err))
|
|
return vf.vm.ToValue("")
|
|
}
|
|
|
|
lines, err := readMatchedLines(source, func(line string) bool {
|
|
return re.MatchString(line)
|
|
})
|
|
if err != nil {
|
|
logger.Get().Warn("grepRegex: failed to read source", zap.String("source", source), zap.Error(err))
|
|
return vf.vm.ToValue("")
|
|
}
|
|
|
|
return vf.vm.ToValue(strings.Join(lines, "\n"))
|
|
}
|
|
|
|
func (vf *vmFunc) glob(call goja.FunctionCall) goja.Value {
|
|
pattern := call.Argument(0).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("glob"), zap.String("pattern", pattern))
|
|
|
|
if pattern == "undefined" || pattern == "" {
|
|
logger.Get().Warn("glob: empty pattern provided")
|
|
return vf.vm.ToValue([]string{})
|
|
}
|
|
|
|
matches, err := filepath.Glob(pattern)
|
|
if err != nil {
|
|
logger.Get().Warn("glob: invalid glob pattern", zap.String("pattern", pattern), zap.Error(err))
|
|
return vf.vm.ToValue([]string{})
|
|
}
|
|
sort.Strings(matches)
|
|
return vf.vm.ToValue(matches)
|
|
}
|
|
|
|
// removeBlankLines removes blank lines from a file in-place
|
|
// Usage: remove_blank_lines(path) -> bool
|
|
func (vf *vmFunc) removeBlankLines(call goja.FunctionCall) goja.Value {
|
|
path := call.Argument(0).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("removeBlankLines"), zap.String("path", path))
|
|
|
|
if path == "undefined" || path == "" {
|
|
logger.Get().Warn("removeBlankLines: empty path provided")
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
// Check if file exists and is not a directory
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
logger.Get().Warn("removeBlankLines: file does not exist", zap.String("path", path), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
if info.IsDir() {
|
|
logger.Get().Warn("removeBlankLines: path is a directory, not a file", zap.String("path", path))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
// Read the file
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
logger.Get().Warn("removeBlankLines: failed to open file", zap.String("path", path), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
var nonBlankLines []string
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if strings.TrimSpace(line) != "" {
|
|
nonBlankLines = append(nonBlankLines, line)
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
_ = file.Close()
|
|
logger.Get().Warn("removeBlankLines: failed to read file", zap.String("path", path), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
_ = file.Close()
|
|
|
|
// Write back to the same file
|
|
content := strings.Join(nonBlankLines, "\n")
|
|
if len(nonBlankLines) > 0 {
|
|
content += "\n" // Add trailing newline if there are lines
|
|
}
|
|
|
|
if err := os.WriteFile(path, []byte(content), info.Mode()); err != nil {
|
|
logger.Get().Warn("removeBlankLines: failed to write file", zap.String("path", path), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
logger.Get().Debug(terminal.HiGreen("removeBlankLines")+" result", zap.String("path", path), zap.Int("lines", len(nonBlankLines)))
|
|
return vf.vm.ToValue(true)
|
|
}
|
|
|
|
// chunkFile splits an input file into chunks of N lines each, writing chunk paths to an output manifest.
|
|
// Blank lines are skipped. Chunk files are named {base}_part_{N}{ext} in the same directory as input.
|
|
// Usage: chunk_file(input, lines_per_chunk, output) -> bool
|
|
func (vf *vmFunc) chunkFile(call goja.FunctionCall) goja.Value {
|
|
input := call.Argument(0).String()
|
|
chunkSize := int(call.Argument(1).ToInteger())
|
|
output := call.Argument(2).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("chunk_file"),
|
|
zap.String("input", input), zap.Int("chunkSize", chunkSize), zap.String("output", output))
|
|
|
|
if input == "undefined" || input == "" || output == "undefined" || output == "" {
|
|
logger.Get().Warn("chunk_file: input and output paths are required")
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
if chunkSize <= 0 {
|
|
logger.Get().Warn("chunk_file: lines_per_chunk must be > 0", zap.Int("chunkSize", chunkSize))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
// Read input file, skipping blank lines
|
|
file, err := os.Open(input)
|
|
if err != nil {
|
|
logger.Get().Warn("chunk_file: failed to open input file", zap.String("input", input), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
var lines []string
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if strings.TrimSpace(line) != "" {
|
|
lines = append(lines, line)
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
_ = file.Close()
|
|
logger.Get().Warn("chunk_file: failed to read input file", zap.String("input", input), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
_ = file.Close()
|
|
|
|
if len(lines) == 0 {
|
|
logger.Get().Debug(terminal.HiGreen("chunk_file") + ": input file has no non-blank lines")
|
|
// Write empty manifest
|
|
if err := os.MkdirAll(filepath.Dir(output), 0755); err != nil {
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
if err := os.WriteFile(output, []byte(""), 0644); err != nil {
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
return vf.vm.ToValue(true)
|
|
}
|
|
|
|
// Compute chunk file naming from input path
|
|
dir := filepath.Dir(input)
|
|
ext := filepath.Ext(input)
|
|
base := strings.TrimSuffix(filepath.Base(input), ext)
|
|
|
|
var chunkPaths []string
|
|
chunkIdx := 0
|
|
|
|
for i := 0; i < len(lines); i += chunkSize {
|
|
end := i + chunkSize
|
|
if end > len(lines) {
|
|
end = len(lines)
|
|
}
|
|
chunk := lines[i:end]
|
|
|
|
chunkName := fmt.Sprintf("%s_part_%d%s", base, chunkIdx, ext)
|
|
chunkPath := filepath.Join(dir, chunkName)
|
|
|
|
content := strings.Join(chunk, "\n") + "\n"
|
|
if err := os.WriteFile(chunkPath, []byte(content), 0644); err != nil {
|
|
logger.Get().Warn("chunk_file: failed to write chunk",
|
|
zap.String("chunkPath", chunkPath), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
chunkPaths = append(chunkPaths, chunkPath)
|
|
chunkIdx++
|
|
}
|
|
|
|
// Write manifest (one chunk path per line)
|
|
if err := os.MkdirAll(filepath.Dir(output), 0755); err != nil {
|
|
logger.Get().Warn("chunk_file: failed to create output directory", zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
manifest := strings.Join(chunkPaths, "\n") + "\n"
|
|
if err := os.WriteFile(output, []byte(manifest), 0644); err != nil {
|
|
logger.Get().Warn("chunk_file: failed to write manifest", zap.String("output", output), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
logger.Get().Debug(terminal.HiGreen("chunk_file")+" result",
|
|
zap.String("input", input),
|
|
zap.Int("totalLines", len(lines)),
|
|
zap.Int("chunks", len(chunkPaths)),
|
|
zap.String("output", output))
|
|
return vf.vm.ToValue(true)
|
|
}
|
|
|
|
// cutToFile reads a file line by line, extracts a field by delimiter, and writes results to output
|
|
// Usage: cut_to_file(input_file, delim, field, output_file) -> bool
|
|
func (vf *vmFunc) cutToFile(call goja.FunctionCall) goja.Value {
|
|
inputFile := call.Argument(0).String()
|
|
delim := call.Argument(1).String()
|
|
field := int(call.Argument(2).ToInteger())
|
|
outputFile := call.Argument(3).String()
|
|
log := logger.Get()
|
|
|
|
log.Debug("Calling "+terminal.HiGreen("cut_to_file"),
|
|
zap.String("input", inputFile), zap.String("delim", delim),
|
|
zap.Int("field", field), zap.String("output", outputFile))
|
|
|
|
if inputFile == "undefined" || inputFile == "" || outputFile == "undefined" || outputFile == "" {
|
|
log.Warn("cut_to_file: input and output paths are required")
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
if delim == "undefined" || delim == "" {
|
|
log.Warn("cut_to_file: delimiter is required")
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
f, err := os.Open(inputFile)
|
|
if err != nil {
|
|
log.Warn("cut_to_file: failed to open input file", zap.String("input", inputFile), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
idx := field - 1 // Convert 1-indexed to 0-indexed
|
|
var results []string
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if strings.TrimSpace(line) == "" {
|
|
continue
|
|
}
|
|
parts := strings.Split(line, delim)
|
|
if idx >= 0 && idx < len(parts) {
|
|
val := parts[idx]
|
|
if val != "" {
|
|
results = append(results, val)
|
|
}
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
log.Warn("cut_to_file: failed to read input file", zap.String("input", inputFile), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
if err := writeLinesToFile(outputFile, results); err != nil {
|
|
log.Warn("cut_to_file: failed to write output file", zap.String("output", outputFile), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
log.Debug(terminal.HiGreen("cut_to_file")+" result",
|
|
zap.String("input", inputFile), zap.Int("lines", len(results)), zap.String("output", outputFile))
|
|
return vf.vm.ToValue(true)
|
|
}
|
|
|
|
// zipDir creates a zip archive from a directory using Go's archive/zip
|
|
// Usage: zip_dir(source, dest) -> bool
|
|
func (vf *vmFunc) zipDir(call goja.FunctionCall) goja.Value {
|
|
source := call.Argument(0).String()
|
|
dest := call.Argument(1).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("zipDir"), zap.String("source", source), zap.String("dest", dest))
|
|
|
|
if source == "undefined" || source == "" || dest == "undefined" || dest == "" {
|
|
logger.Get().Warn("zipDir: empty source or dest provided")
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
// Check if source exists
|
|
info, err := os.Stat(source)
|
|
if err != nil {
|
|
logger.Get().Warn("zipDir: source does not exist", zap.String("source", source), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
// Ensure output directory exists
|
|
dir := filepath.Dir(dest)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
logger.Get().Warn("zipDir: failed to create output directory", zap.String("dir", dir), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
// Create the zip file
|
|
zipFile, err := os.Create(dest)
|
|
if err != nil {
|
|
logger.Get().Warn("zipDir: failed to create zip file", zap.String("dest", dest), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
defer func() { _ = zipFile.Close() }()
|
|
|
|
zipWriter := zip.NewWriter(zipFile)
|
|
defer func() { _ = zipWriter.Close() }()
|
|
|
|
if info.IsDir() {
|
|
// Walk the directory
|
|
err = filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create header
|
|
header, err := zip.FileInfoHeader(info)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Calculate relative path
|
|
relPath, err := filepath.Rel(source, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.IsDir() {
|
|
header.Name = relPath + "/"
|
|
} else {
|
|
header.Name = relPath
|
|
}
|
|
|
|
header.Method = zip.Deflate
|
|
|
|
writer, err := zipWriter.CreateHeader(header)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
|
|
_, err = io.Copy(writer, file)
|
|
return err
|
|
})
|
|
} else {
|
|
// Single file
|
|
header, err := zip.FileInfoHeader(info)
|
|
if err != nil {
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
header.Method = zip.Deflate
|
|
|
|
writer, err := zipWriter.CreateHeader(header)
|
|
if err != nil {
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
file, err := os.Open(source)
|
|
if err != nil {
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
|
|
_, err = io.Copy(writer, file)
|
|
if err != nil {
|
|
logger.Get().Warn("zipDir: failed to copy file content", zap.String("source", source), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
}
|
|
|
|
success := err == nil
|
|
if success {
|
|
logger.Get().Debug(terminal.HiGreen("zipDir")+" result", zap.String("source", source), zap.String("dest", dest), zap.Bool("success", success))
|
|
} else {
|
|
logger.Get().Warn("zipDir: compression failed", zap.String("source", source), zap.Error(err))
|
|
}
|
|
return vf.vm.ToValue(success)
|
|
}
|
|
|
|
// unzipDir extracts a zip archive to a directory using Go's archive/zip
|
|
// Usage: unzip_dir(source, dest) -> bool
|
|
func (vf *vmFunc) unzipDir(call goja.FunctionCall) goja.Value {
|
|
source := call.Argument(0).String()
|
|
dest := call.Argument(1).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("unzipDir"), zap.String("source", source), zap.String("dest", dest))
|
|
|
|
if source == "undefined" || source == "" || dest == "undefined" || dest == "" {
|
|
logger.Get().Warn("unzipDir: empty source or dest provided")
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
// Open the zip file
|
|
reader, err := zip.OpenReader(source)
|
|
if err != nil {
|
|
logger.Get().Warn("unzipDir: failed to open zip file", zap.String("source", source), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
defer func() { _ = reader.Close() }()
|
|
|
|
// Ensure destination exists
|
|
if err := os.MkdirAll(dest, 0755); err != nil {
|
|
logger.Get().Warn("unzipDir: failed to create destination directory", zap.String("dest", dest), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
// Extract files
|
|
for _, file := range reader.File {
|
|
// Sanitize the path to prevent zip slip
|
|
destPath := filepath.Join(dest, file.Name)
|
|
if !strings.HasPrefix(destPath, filepath.Clean(dest)+string(os.PathSeparator)) {
|
|
continue // Skip files outside destination
|
|
}
|
|
|
|
if file.FileInfo().IsDir() {
|
|
if err := os.MkdirAll(destPath, file.Mode()); err != nil {
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Create parent directory
|
|
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
// Create the file
|
|
outFile, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
|
if err != nil {
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
rc, err := file.Open()
|
|
if err != nil {
|
|
_ = outFile.Close()
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
_, err = io.Copy(outFile, rc)
|
|
_ = rc.Close()
|
|
_ = outFile.Close()
|
|
|
|
if err != nil {
|
|
logger.Get().Warn("unzipDir: failed to extract file", zap.String("file", file.Name), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
}
|
|
|
|
logger.Get().Debug(terminal.HiGreen("unzipDir")+" result", zap.String("source", source), zap.String("dest", dest), zap.Bool("success", true))
|
|
return vf.vm.ToValue(true)
|
|
}
|
|
|
|
// extractTo auto-detects archive format and extracts to destination.
|
|
// Supports .zip, .tar.gz, .tgz, .tar.bz2, .tar.xz.
|
|
// Removes the destination directory first if it already exists.
|
|
// Usage: extract_to(source, dest) -> bool
|
|
func (vf *vmFunc) extractTo(call goja.FunctionCall) goja.Value {
|
|
source := call.Argument(0).String()
|
|
dest := call.Argument(1).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("extractTo"), zap.String("source", source), zap.String("dest", dest))
|
|
|
|
if source == "undefined" || source == "" || dest == "undefined" || dest == "" {
|
|
logger.Get().Warn("extractTo: empty source or dest provided")
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
// Verify source exists
|
|
if _, err := os.Stat(source); err != nil {
|
|
logger.Get().Warn("extractTo: source does not exist", zap.String("source", source), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
// Remove destination if it already exists
|
|
if _, err := os.Stat(dest); err == nil {
|
|
logger.Get().Debug("extractTo: removing existing destination", zap.String("dest", dest))
|
|
if err := os.RemoveAll(dest); err != nil {
|
|
logger.Get().Warn("extractTo: failed to remove existing destination", zap.String("dest", dest), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
}
|
|
|
|
// Create destination directory
|
|
if err := os.MkdirAll(dest, 0755); err != nil {
|
|
logger.Get().Warn("extractTo: failed to create destination directory", zap.String("dest", dest), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
lower := strings.ToLower(source)
|
|
var err error
|
|
|
|
switch {
|
|
case strings.HasSuffix(lower, ".zip"):
|
|
// Use Go's archive/zip for .zip files
|
|
err = extractZip(source, dest)
|
|
|
|
case strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz"):
|
|
err = exec.Command("tar", "-xzf", source, "-C", dest).Run()
|
|
|
|
case strings.HasSuffix(lower, ".tar.bz2"):
|
|
err = exec.Command("tar", "-xjf", source, "-C", dest).Run()
|
|
|
|
case strings.HasSuffix(lower, ".tar.xz"):
|
|
err = exec.Command("tar", "-xJf", source, "-C", dest).Run()
|
|
|
|
default:
|
|
// Try to detect format by file magic bytes when extension is missing
|
|
if detected := detectArchiveFormat(source); detected != "" {
|
|
logger.Get().Debug("extractTo: detected format by magic bytes", zap.String("source", source), zap.String("format", detected))
|
|
switch detected {
|
|
case "zip":
|
|
err = extractZip(source, dest)
|
|
case "gzip":
|
|
err = exec.Command("tar", "-xzf", source, "-C", dest).Run()
|
|
case "bzip2":
|
|
err = exec.Command("tar", "-xjf", source, "-C", dest).Run()
|
|
case "xz":
|
|
err = exec.Command("tar", "-xJf", source, "-C", dest).Run()
|
|
}
|
|
} else {
|
|
logger.Get().Warn("extractTo: unsupported archive format", zap.String("source", source))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
logger.Get().Warn("extractTo: extraction failed", zap.String("source", source), zap.String("dest", dest), zap.Error(err))
|
|
return vf.vm.ToValue(false)
|
|
}
|
|
|
|
logger.Get().Debug(terminal.HiGreen("extractTo")+" result", zap.String("source", source), zap.String("dest", dest), zap.Bool("success", true))
|
|
return vf.vm.ToValue(true)
|
|
}
|
|
|
|
// detectArchiveFormat reads the first few bytes of a file to identify the archive format.
|
|
// Returns "zip", "gzip", "bzip2", "xz", or "" if unrecognized.
|
|
func detectArchiveFormat(path string) string {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
header := make([]byte, 6)
|
|
n, err := f.Read(header)
|
|
if err != nil || n < 4 {
|
|
return ""
|
|
}
|
|
|
|
switch {
|
|
case header[0] == 0x50 && header[1] == 0x4B && header[2] == 0x03 && header[3] == 0x04:
|
|
return "zip"
|
|
case header[0] == 0x1F && header[1] == 0x8B:
|
|
return "gzip"
|
|
case header[0] == 0x42 && header[1] == 0x5A && header[2] == 0x68:
|
|
return "bzip2"
|
|
case n >= 6 && header[0] == 0xFD && header[1] == 0x37 && header[2] == 0x7A && header[3] == 0x58 && header[4] == 0x5A && header[5] == 0x00:
|
|
return "xz"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// extractDiff compares two files and returns lines only in file2 (new content)
|
|
// Usage: extractDiff(file1, file2) -> string
|
|
func (vf *vmFunc) extractDiff(call goja.FunctionCall) goja.Value {
|
|
file1Path := call.Argument(0).String()
|
|
file2Path := call.Argument(1).String()
|
|
logger.Get().Debug("Calling "+terminal.HiGreen("extractDiff"), zap.String("file1", file1Path), zap.String("file2", file2Path))
|
|
|
|
if file1Path == "undefined" || file1Path == "" || file2Path == "undefined" || file2Path == "" {
|
|
logger.Get().Warn("extractDiff: empty file paths provided")
|
|
return vf.vm.ToValue("")
|
|
}
|
|
|
|
// Read file1 lines into a set
|
|
file1Lines := make(map[string]bool)
|
|
file1, err := os.Open(file1Path)
|
|
if err == nil {
|
|
scanner := bufio.NewScanner(file1)
|
|
for scanner.Scan() {
|
|
file1Lines[scanner.Text()] = true
|
|
}
|
|
_ = file1.Close()
|
|
} else {
|
|
logger.Get().Debug(terminal.HiGreen("extractDiff")+": file1 not found, treating all lines in file2 as new", zap.String("file1", file1Path))
|
|
}
|
|
// If file1 doesn't exist, all lines in file2 are "new"
|
|
|
|
// Read file2 and find lines not in file1
|
|
file2, err := os.Open(file2Path)
|
|
if err != nil {
|
|
logger.Get().Warn("extractDiff: failed to open file2", zap.String("file2", file2Path), zap.Error(err))
|
|
return vf.vm.ToValue("")
|
|
}
|
|
defer func() { _ = file2.Close() }()
|
|
|
|
var newLines []string
|
|
scanner := bufio.NewScanner(file2)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if !file1Lines[line] {
|
|
newLines = append(newLines, line)
|
|
}
|
|
}
|
|
|
|
logger.Get().Debug(terminal.HiGreen("extractDiff")+" result", zap.String("file1", file1Path), zap.String("file2", file2Path), zap.Int("new_lines", len(newLines)))
|
|
return vf.vm.ToValue(strings.Join(newLines, "\n"))
|
|
}
|