// internal/utils/files.go package utils import ( "crypto/sha256" "encoding/hex" "fmt" "io" "math/rand" "net/http" "os" "path/filepath" "strings" "time" "bytes" ) func init() { // Seed the random number generator rand.Seed(time.Now().UnixNano()) } // EnsureDir makes sure the directory exists func EnsureDir(dir string) error { return os.MkdirAll(dir, 0755) } // FileExists checks if a file exists and is not a directory func FileExists(filename string) bool { info, err := os.Stat(filename) if os.IsNotExist(err) { return false } return !info.IsDir() } // GetRandomFile returns a random file from the specified directory // It only includes files with the given extensions (no dot, e.g. "jpg", "png") // If extensions is empty, it includes all files func GetRandomFile(dir string, extensions []string) (string, error) { files, err := os.ReadDir(dir) if err != nil { return "", fmt.Errorf("failed to read directory: %w", err) } // Filter files based on extensions var validFiles []string for _, file := range files { if file.IsDir() { continue } if len(extensions) == 0 { validFiles = append(validFiles, file.Name()) continue } ext := strings.TrimPrefix(filepath.Ext(file.Name()), ".") for _, validExt := range extensions { if strings.EqualFold(ext, validExt) { validFiles = append(validFiles, file.Name()) break } } } if len(validFiles) == 0 { return "", fmt.Errorf("no valid files found in directory") } // Pick a random file randomIndex := rand.Intn(len(validFiles)) return filepath.Join(dir, validFiles[randomIndex]), nil } // MoveFile moves a file from src to dst func MoveFile(src, dst string) error { // Ensure the destination directory exists if err := EnsureDir(filepath.Dir(dst)); err != nil { return err } // First try to rename (which works within the same filesystem) err := os.Rename(src, dst) if err == nil { return nil } // If rename fails (e.g., across filesystems), copy and delete the original if err := CopyFile(src, dst); err != nil { return err } return os.Remove(src) } // CopyFile copies a file from src to dst func CopyFile(src, dst string) error { // Ensure the destination directory exists if err := EnsureDir(filepath.Dir(dst)); err != nil { return err } // Open the source file sourceFile, err := os.Open(src) if err != nil { return err } defer sourceFile.Close() // Create the destination file destFile, err := os.Create(dst) if err != nil { return err } defer destFile.Close() // Copy the content _, err = io.Copy(destFile, sourceFile) if err != nil { return err } // Flush the write buffer to disk return destFile.Sync() } // CalculateFileHash returns the SHA-256 hash of a file func CalculateFileHash(filePath string) (string, error) { file, err := os.Open(filePath) if err != nil { return "", err } defer file.Close() hasher := sha256.New() if _, err := io.Copy(hasher, file); err != nil { return "", err } return hex.EncodeToString(hasher.Sum(nil)), nil } // GetFileContentType tries to determine the content type of a file func GetFileContentType(filePath string) (string, error) { // Open the file file, err := os.Open(filePath) if err != nil { return "", err } defer file.Close() // Read the first 512 bytes to determine the content type buffer := make([]byte, 512) bytesRead, err := file.Read(buffer) if err != nil && err != io.EOF { return "", err } // If we read less than 512 bytes, resize the buffer if bytesRead < 512 { buffer = buffer[:bytesRead] } // Reset the file pointer _, err = file.Seek(0, 0) if err != nil { return "", err } // Detect the content type contentType := http.DetectContentType(buffer) // If the detection gave us a generic type, try to be more specific using the extension if contentType == "application/octet-stream" || contentType == "text/plain" { ext := strings.ToLower(filepath.Ext(filePath)) switch ext { case ".jpg", ".jpeg": return "image/jpeg", nil case ".png": return "image/png", nil case ".gif": return "image/gif", nil case ".webp": return "image/webp", nil case ".mp4": return "video/mp4", nil case ".mov": return "video/quicktime", nil case ".webm": return "video/webm", nil case ".mp3": return "audio/mpeg", nil case ".wav": return "audio/wav", nil case ".pdf": return "application/pdf", nil } } // Check for WebP signature, which is sometimes not detected correctly if contentType == "application/octet-stream" || contentType == "image/webp" { if bytesRead > 12 { if bytes.HasPrefix(buffer, []byte("RIFF")) && bytes.Contains(buffer[8:12], []byte("WEBP")) { return "image/webp", nil } } } return contentType, nil } // GetSupportedImageExtensions returns a list of supported image extensions func GetSupportedImageExtensions() []string { return []string{"jpg", "jpeg", "png", "gif", "webp", "avif"} } // GetSupportedVideoExtensions returns a list of supported video extensions func GetSupportedVideoExtensions() []string { return []string{"mp4", "webm", "mov", "avi"} } // GetAllSupportedMediaExtensions returns all supported media extensions func GetAllSupportedMediaExtensions() []string { return append(GetSupportedImageExtensions(), GetSupportedVideoExtensions()...) }