package utils import ( "fmt" "io" "net/http" "net/url" "os" "path/filepath" "strings" "time" "github.com/google/uuid" "github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/media" ) var audioExtensions = []string{".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac", ".wma"} func AudioFormat(path string) (string, error) { ext := strings.ToLower(filepath.Ext(path)) for _, supportedExt := range audioExtensions { if ext == supportedExt { return strings.TrimPrefix(ext, "."), nil } } return "", fmt.Errorf("unsupported audio format for %q", path) } // IsAudioFile checks if a file is an audio file based on its filename extension and content type. func IsAudioFile(filename, contentType string) bool { audioTypes := []string{"audio/", "application/ogg", "application/x-ogg"} for _, ext := range audioExtensions { if strings.HasSuffix(strings.ToLower(filename), ext) { return true } } for _, audioType := range audioTypes { if strings.HasPrefix(strings.ToLower(contentType), audioType) { return true } } return false } // SanitizeFilename removes potentially dangerous characters from a filename // and returns a safe version for local filesystem storage. func SanitizeFilename(filename string) string { // Get the base filename without path base := filepath.Base(filename) // Remove any directory traversal attempts base = strings.ReplaceAll(base, "..", "") base = strings.ReplaceAll(base, "/", "_") base = strings.ReplaceAll(base, "\\", "_") return base } // DownloadOptions holds optional parameters for downloading files type DownloadOptions struct { Timeout time.Duration ExtraHeaders map[string]string LoggerPrefix string ProxyURL string } // DownloadFile downloads a file from URL to a local temp directory. // Returns the local file path or empty string on error. func DownloadFile(urlStr, filename string, opts DownloadOptions) string { // Set defaults if opts.Timeout == 0 { opts.Timeout = 60 * time.Second } if opts.LoggerPrefix == "" { opts.LoggerPrefix = "utils" } mediaDir := media.TempDir() if err := os.MkdirAll(mediaDir, 0o700); err != nil { logger.ErrorCF(opts.LoggerPrefix, "Failed to create media directory", map[string]any{ "error": err.Error(), }) return "" } // Generate unique filename with UUID prefix to prevent conflicts safeName := SanitizeFilename(filename) localPath := filepath.Join(mediaDir, uuid.New().String()[:8]+"_"+safeName) // Create HTTP request req, err := http.NewRequest("GET", urlStr, nil) if err != nil { logger.ErrorCF(opts.LoggerPrefix, "Failed to create download request", map[string]any{ "error": err.Error(), }) return "" } // Add extra headers (e.g., Authorization for Slack) for key, value := range opts.ExtraHeaders { req.Header.Set(key, value) } client := &http.Client{Timeout: opts.Timeout} if opts.ProxyURL != "" { proxyURL, parseErr := url.Parse(opts.ProxyURL) if parseErr != nil { logger.ErrorCF(opts.LoggerPrefix, "Invalid proxy URL for download", map[string]any{ "error": parseErr.Error(), "proxy": opts.ProxyURL, }) return "" } client.Transport = &http.Transport{ Proxy: http.ProxyURL(proxyURL), } } resp, err := client.Do(req) if err != nil { logger.ErrorCF(opts.LoggerPrefix, "Failed to download file", map[string]any{ "error": err.Error(), "url": urlStr, }) return "" } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { logger.ErrorCF(opts.LoggerPrefix, "File download returned non-200 status", map[string]any{ "status": resp.StatusCode, "url": urlStr, }) return "" } out, err := os.Create(localPath) if err != nil { logger.ErrorCF(opts.LoggerPrefix, "Failed to create local file", map[string]any{ "error": err.Error(), }) return "" } defer out.Close() if _, err := io.Copy(out, resp.Body); err != nil { out.Close() os.Remove(localPath) logger.ErrorCF(opts.LoggerPrefix, "Failed to write file", map[string]any{ "error": err.Error(), }) return "" } logger.DebugCF(opts.LoggerPrefix, "File downloaded successfully", map[string]any{ "path": localPath, }) return localPath } // DownloadFileSimple is a simplified version of DownloadFile without options func DownloadFileSimple(url, filename string) string { return DownloadFile(url, filename, DownloadOptions{ LoggerPrefix: "media", }) }