package storage import ( "crypto/rand" "io" "mime" "net/http" "os" "path/filepath" "regexp" "strings" ) const slugChars = "23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ" var safeFilenameRegex = regexp.MustCompile(`[^a-zA-Z0-9._-]+`) func GenerateSlug(length int) string { b := make([]byte, length) if _, err := rand.Read(b); err != nil { return "x" + strings.ToLower(slugChars[:length-1]) } charsLen := byte(len(slugChars)) for i := 0; i < length; i++ { b[i] = slugChars[b[i]%charsLen] } return string(b) } func SanitizeFilename(name string) string { base := filepath.Base(name) sanitized := safeFilenameRegex.ReplaceAllString(base, "_") if len(sanitized) > 100 { ext := filepath.Ext(sanitized) sanitized = sanitized[:100-len(ext)] + ext } if sanitized == "" || sanitized == "." { sanitized = "archivo.bin" } return sanitized } func DetectMimeType(filename string, reader io.ReadSeeker) string { ext := strings.ToLower(filepath.Ext(filename)) if m := mime.TypeByExtension(ext); m != "" { return m } // Sniff first 512 bytes if reader != nil { buf := make([]byte, 512) n, err := reader.Read(buf) _, _ = reader.Seek(0, io.SeekStart) if err == nil && n > 0 { detected := http.DetectContentType(buf[:n]) if detected != "application/octet-stream" { return detected } } } // Known fallback extensions switch ext { case ".pdf": return "application/pdf" case ".mp4": return "video/mp4" case ".webm": return "video/webm" case ".mp3": return "audio/mpeg" case ".wav": return "audio/wav" case ".ogg": return "audio/ogg" case ".png": return "image/png" case ".jpg", ".jpeg": return "image/jpeg" case ".gif": return "image/gif" case ".webp": return "image/webp" case ".svg": return "image/svg+xml" case ".txt", ".log", ".env": return "text/plain; charset=utf-8" case ".json": return "application/json" case ".js": return "application/javascript" case ".go", ".py", ".rs", ".java", ".c", ".cpp", ".sh", ".sql", ".html", ".css", ".md": return "text/plain; charset=utf-8" default: return "application/octet-stream" } } func SaveFile(uploadDir, slug, originalFilename string, src io.Reader) (string, int64, error) { if err := os.MkdirAll(uploadDir, 0755); err != nil { return "", 0, err } safeName := SanitizeFilename(originalFilename) storedFilename := slug + "_" + safeName destPath := filepath.Join(uploadDir, storedFilename) dest, err := os.Create(destPath) if err != nil { return "", 0, err } defer dest.Close() written, err := io.Copy(dest, src) if err != nil { _ = os.Remove(destPath) return "", 0, err } return storedFilename, written, nil }