prepend.go
package tools
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
)
// PrependInput contains the parameters for the prepend operation.
type PrependInput struct {
Folder string
Prefix string
Recursive bool
DryRun bool
}
// PrependResult contains the result of a single file rename operation.
type PrependResult struct {
Old string `json:"old"`
New string `json:"new"`
}
// PrependSkipped contains info about a skipped file.
type PrependSkipped struct {
File string `json:"file"`
Reason string `json:"reason"`
}
// PrependError contains info about a failed rename.
type PrependError struct {
File string `json:"file"`
Error string `json:"error"`
}
// PrependOutput contains the complete result of the prepend operation.
type PrependOutput struct {
Folder string `json:"folder"`
Prefix string `json:"prefix"`
Recursive bool `json:"recursive"`
DryRun bool `json:"dry_run"`
Renamed []PrependResult `json:"renamed"`
Skipped []PrependSkipped `json:"skipped"`
Errors []PrependError `json:"errors"`
}
// datestringRegex matches filenames starting with YYYYMMDD_HHMMSS.
var datestringRegex = regexp.MustCompile(`^\d{8}_\d{6}\.`)
// prependFolders collects the folders to process (root + immediate subdirs if recursive).
func prependFolders(input PrependInput) ([]string, error) {
folders := []string{input.Folder}
if !input.Recursive {
return folders, nil
}
entries, err := os.ReadDir(input.Folder)
if err != nil {
return nil, fmt.Errorf("failed to read folder: %w", err)
}
for _, entry := range entries {
if entry.IsDir() {
folders = append(folders, filepath.Join(input.Folder, entry.Name()))
}
}
return folders, nil
}
// processPrependFile handles a single file within a folder rename pass.
func processPrependFile(folder, filename, prefix string, dryRun bool, output *PrependOutput) {
oldPath := filepath.Join(folder, filename)
shouldRename, skipReason := shouldPrependFile(filename, prefix)
if !shouldRename {
if skipReason != "" {
output.Skipped = append(output.Skipped, PrependSkipped{File: oldPath, Reason: skipReason})
}
return
}
newPath := filepath.Join(folder, prefix+"_"+filename)
if dryRun {
output.Renamed = append(output.Renamed, PrependResult{Old: oldPath, New: newPath})
return
}
if err := os.Rename(oldPath, newPath); err != nil {
output.Errors = append(output.Errors, PrependError{File: oldPath, Error: err.Error()})
return
}
output.Renamed = append(output.Renamed, PrependResult{Old: oldPath, New: newPath})
}
// Prepend renames files in a folder by prepending a prefix.
// WAV files (.wav, .WAV) and their .data files are only renamed if they start with a datestring.
// log.txt is always renamed if present.
func Prepend(input PrependInput) (*PrependOutput, error) {
output := &PrependOutput{
Folder: input.Folder,
Prefix: input.Prefix,
Recursive: input.Recursive,
DryRun: input.DryRun,
Renamed: []PrependResult{},
Skipped: []PrependSkipped{},
Errors: []PrependError{},
}
folders, err := prependFolders(input)
if err != nil {
return nil, err
}
for _, folder := range folders {
entries, err := os.ReadDir(folder)
if err != nil {
return nil, fmt.Errorf("failed to read folder %s: %w", folder, err)
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
processPrependFile(folder, entry.Name(), input.Prefix, input.DryRun, output)
}
}
return output, nil
}
// shouldPrependFile determines if a file should be prepended.
// Returns (shouldRename, skipReason). If shouldRename is false and skipReason is empty,
// the file is not a target type (silently ignored).
func shouldPrependFile(filename, prefix string) (bool, string) {
lowerName := strings.ToLower(filename)
// Check if already prefixed (applies to all target files)
if strings.HasPrefix(filename, prefix+"_") {
// Only report as "already prefixed" if it's a target file type
if filename == prefix+"_log.txt" || isWavOrData(lowerName) {
return false, "already prefixed"
}
return false, ""
}
// Check for log.txt (exact match, case-sensitive as per spec)
if filename == "log.txt" {
return true, ""
}
// Check for WAV files and their .data files
if !isWavOrData(lowerName) {
return false, "" // Not a target file type, silently ignore
}
// Check for datestring prefix (YYYYMMDD_HHMMSS.)
if !datestringRegex.MatchString(filename) {
return false, "no datestring prefix"
}
return true, ""
}
// isWavOrData checks if the lowercase filename is a .wav or .wav.data file
func isWavOrData(lowerName string) bool {
return strings.HasSuffix(lowerName, ".wav") || strings.HasSuffix(lowerName, ".wav.data")
}