package objectstorage import ( "fmt" "net/url" "path/filepath" "strings" "github.com/geo-platform/tenant-api/internal/shared/config" ) func buildPublicURL(cfg config.ObjectStorageConfig, objectKey string, bucketInHost bool) (string, error) { objectKey = strings.TrimSpace(objectKey) if objectKey == "" { return "", fmt.Errorf("object key is required") } defaultScheme := "http" if cfg.UseSSL { defaultScheme = "https" } if publicBaseURL := strings.TrimSpace(cfg.PublicBaseURL); publicBaseURL != "" { baseURL, err := parseURLWithScheme(publicBaseURL, defaultScheme) if err != nil { return "", fmt.Errorf("parse public_base_url: %w", err) } baseURL.Path = joinEscapedPath(baseURL.Path, objectKey) baseURL.RawPath = "" return baseURL.String(), nil } endpoint := strings.TrimSpace(cfg.Endpoint) bucket := strings.TrimSpace(cfg.Bucket) if endpoint == "" || bucket == "" { return "", fmt.Errorf("%w: object storage endpoint or bucket is missing", ErrNotConfigured) } endpointURL, err := parseURLWithScheme(endpoint, defaultScheme) if err != nil { return "", fmt.Errorf("parse object storage endpoint: %w", err) } if bucketInHost { hostname := endpointURL.Hostname() if hostname == "" { return "", fmt.Errorf("invalid object storage endpoint host") } host := hostname if !strings.HasPrefix(host, bucket+".") { host = bucket + "." + host } if port := endpointURL.Port(); port != "" { host += ":" + port } endpointURL.Host = host endpointURL.Path = joinEscapedPath("", objectKey) endpointURL.RawPath = "" return endpointURL.String(), nil } endpointURL.Path = joinEscapedPath(endpointURL.Path, bucket, objectKey) endpointURL.RawPath = "" return endpointURL.String(), nil } func DownloadContentDisposition(objectKey string) string { name := strings.TrimSpace(filepath.Base(strings.TrimRight(objectKey, "/"))) if name == "" || name == "." || name == "/" || name == "\\" { name = "object" } name = strings.Map(func(r rune) rune { if r == '"' || r == '\\' || r == '\r' || r == '\n' { return -1 } return r }, name) if name == "" { name = "object" } return fmt.Sprintf( `attachment; filename="%s"; filename*=UTF-8''%s`, asciiContentDispositionFileName(name), url.PathEscape(name), ) } func downloadContentDisposition(objectKey string) string { return DownloadContentDisposition(objectKey) } func asciiContentDispositionFileName(name string) string { var builder strings.Builder for _, r := range name { if r < 0x20 || r >= 0x7f || r == '"' || r == '\\' { continue } builder.WriteRune(r) } fallback := strings.TrimSpace(builder.String()) if fallback == "" || fallback == "." || fallback == ".." || strings.HasPrefix(fallback, ".") { return "object" + safeASCIIExtension(name) } return fallback } func safeASCIIExtension(name string) string { ext := filepath.Ext(name) if ext == "" || ext == "." { return "" } var builder strings.Builder for _, r := range ext { if r < 0x20 || r >= 0x7f || r == '"' || r == '\\' { continue } builder.WriteRune(r) } ext = strings.TrimSpace(builder.String()) if !strings.HasPrefix(ext, ".") || ext == "." { return "" } return ext } func parseURLWithScheme(raw string, defaultScheme string) (*url.URL, error) { raw = strings.TrimSpace(raw) if raw == "" { return nil, fmt.Errorf("url is empty") } if !strings.Contains(raw, "://") { raw = defaultScheme + "://" + raw } parsed, err := url.Parse(raw) if err != nil { return nil, err } if parsed.Host == "" { return nil, fmt.Errorf("url host is empty") } return parsed, nil } func joinEscapedPath(basePath string, extra ...string) string { segments := make([]string, 0) appendPath := func(value string) { value = strings.TrimSpace(value) value = strings.Trim(value, "/") if value == "" { return } normalized := normalizeObjectKeyPath(value) if normalized != "" { segments = append(segments, strings.Split(normalized, "/")...) } } appendPath(basePath) for _, value := range extra { appendPath(value) } if len(segments) == 0 { return "/" } return "/" + strings.Join(segments, "/") } func normalizeObjectKeyPath(value string) string { segments := make([]string, 0) value = strings.TrimSpace(value) value = strings.Trim(value, "/") if value == "" { return "" } for _, segment := range strings.Split(value, "/") { segment = strings.TrimSpace(segment) if segment == "" { continue } if decoded, err := url.PathUnescape(segment); err == nil { segment = decoded } segments = append(segments, segment) } return strings.Join(segments, "/") }