178 lines
4.5 KiB
Go
178 lines
4.5 KiB
Go
package utils
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/KarakuriAgent/clawdroid/pkg/logger"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const maxImageFileSize = 50 * 1024 * 1024 // 50MB
|
|
|
|
// EncodeFileToDataURL reads a local file and returns a base64 data URL string.
|
|
// Supported: JPEG, PNG, WEBP, GIF. Max 50MB.
|
|
// Returns empty string on error.
|
|
func EncodeFileToDataURL(path string) string {
|
|
ext := strings.ToLower(filepath.Ext(path))
|
|
var mime string
|
|
switch ext {
|
|
case ".jpg", ".jpeg":
|
|
mime = "image/jpeg"
|
|
case ".png":
|
|
mime = "image/png"
|
|
case ".webp":
|
|
mime = "image/webp"
|
|
case ".gif":
|
|
mime = "image/gif"
|
|
default:
|
|
logger.WarnCF("media", "Unsupported image extension", map[string]interface{}{
|
|
"path": path,
|
|
"ext": ext,
|
|
})
|
|
return ""
|
|
}
|
|
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
logger.ErrorCF("media", "Failed to stat image file", map[string]interface{}{
|
|
"path": path,
|
|
"error": err.Error(),
|
|
})
|
|
return ""
|
|
}
|
|
if info.Size() > maxImageFileSize {
|
|
logger.WarnCF("media", "Image file too large, skipping", map[string]interface{}{
|
|
"path": path,
|
|
"size_mb": info.Size() / (1024 * 1024),
|
|
"max_mb": maxImageFileSize / (1024 * 1024),
|
|
})
|
|
return ""
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
logger.ErrorCF("media", "Failed to read image file", map[string]interface{}{
|
|
"path": path,
|
|
"error": err.Error(),
|
|
})
|
|
return ""
|
|
}
|
|
|
|
encoded := base64.StdEncoding.EncodeToString(data)
|
|
return "data:" + mime + ";base64," + encoded
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// DownloadFile downloads a file from URL to a local temp directory.
|
|
// Returns the local file path or empty string on error.
|
|
func DownloadFile(url, filename string, opts DownloadOptions) string {
|
|
// Set defaults
|
|
if opts.Timeout == 0 {
|
|
opts.Timeout = 60 * time.Second
|
|
}
|
|
if opts.LoggerPrefix == "" {
|
|
opts.LoggerPrefix = "utils"
|
|
}
|
|
|
|
mediaDir := filepath.Join(os.TempDir(), "clawdroid_media")
|
|
if err := os.MkdirAll(mediaDir, 0700); err != nil {
|
|
logger.ErrorCF(opts.LoggerPrefix, "Failed to create media directory", map[string]interface{}{
|
|
"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", url, nil)
|
|
if err != nil {
|
|
logger.ErrorCF(opts.LoggerPrefix, "Failed to create download request", map[string]interface{}{
|
|
"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}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
logger.ErrorCF(opts.LoggerPrefix, "Failed to download file", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"url": url,
|
|
})
|
|
return ""
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
logger.ErrorCF(opts.LoggerPrefix, "File download returned non-200 status", map[string]interface{}{
|
|
"status": resp.StatusCode,
|
|
"url": url,
|
|
})
|
|
return ""
|
|
}
|
|
|
|
out, err := os.Create(localPath)
|
|
if err != nil {
|
|
logger.ErrorCF(opts.LoggerPrefix, "Failed to create local file", map[string]interface{}{
|
|
"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]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return ""
|
|
}
|
|
|
|
logger.DebugCF(opts.LoggerPrefix, "File downloaded successfully", map[string]interface{}{
|
|
"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",
|
|
})
|
|
}
|