94f7186cce
- Implemented image upload API in the article service, allowing users to upload images associated with articles. - Added validation for image size and type, ensuring only supported formats (PNG, JPG, GIF, WebP) are accepted. - Created a new Aliyun object storage client to handle image storage and retrieval. - Updated the article handler to include an endpoint for image uploads. - Enhanced error handling for image upload failures and added relevant error messages. - Introduced public URL generation for stored images, allowing access via signed tokens. - Updated localization files to include new messages related to image upload functionality. - Added tests for image upload and retrieval processes.
117 lines
2.7 KiB
Go
117 lines
2.7 KiB
Go
package objectstorage
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"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 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
|
|
}
|
|
|
|
for _, segment := range strings.Split(value, "/") {
|
|
segment = strings.TrimSpace(segment)
|
|
if segment == "" {
|
|
continue
|
|
}
|
|
segments = append(segments, url.PathEscape(segment))
|
|
}
|
|
}
|
|
|
|
appendPath(basePath)
|
|
for _, value := range extra {
|
|
appendPath(value)
|
|
}
|
|
|
|
if len(segments) == 0 {
|
|
return "/"
|
|
}
|
|
return "/" + strings.Join(segments, "/")
|
|
}
|