feat: add image upload functionality for articles
- 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.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
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, "/")
|
||||
}
|
||||
Reference in New Issue
Block a user