| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- package service
- import (
- "context"
- "mime"
- "net/http"
- "path"
- "path/filepath"
- "strings"
- "cmr-backend/internal/apperr"
- "cmr-backend/internal/platform/assets"
- "cmr-backend/internal/platform/security"
- "cmr-backend/internal/store/postgres"
- )
- type AdminAssetService struct {
- store *postgres.Store
- assetBaseURL string
- assetPublisher *assets.OSSUtilPublisher
- }
- type ManagedAssetSummary struct {
- ID string `json:"id"`
- AssetType string `json:"assetType"`
- AssetCode string `json:"assetCode"`
- Version string `json:"version"`
- Title *string `json:"title,omitempty"`
- SourceMode string `json:"sourceMode"`
- StorageProvider string `json:"storageProvider"`
- ObjectKey *string `json:"objectKey,omitempty"`
- PublicURL string `json:"publicUrl"`
- FileName *string `json:"fileName,omitempty"`
- ContentType *string `json:"contentType,omitempty"`
- FileSizeBytes *int64 `json:"fileSizeBytes,omitempty"`
- ChecksumSHA256 *string `json:"checksumSha256,omitempty"`
- Status string `json:"status"`
- Metadata map[string]any `json:"metadata,omitempty"`
- }
- type RegisterLinkAssetInput struct {
- AssetType string `json:"assetType"`
- AssetCode string `json:"assetCode"`
- Version string `json:"version"`
- Title *string `json:"title,omitempty"`
- PublicURL string `json:"publicUrl"`
- FileName *string `json:"fileName,omitempty"`
- ContentType *string `json:"contentType,omitempty"`
- Status string `json:"status"`
- Metadata map[string]any `json:"metadata,omitempty"`
- }
- type UploadAssetFileInput struct {
- AssetType string
- AssetCode string
- Version string
- Title *string
- ObjectDir *string
- FileName string
- ContentType string
- FileSize int64
- Checksum string
- TempPath string
- Status string
- Metadata map[string]any
- }
- func NewAdminAssetService(store *postgres.Store, assetBaseURL string, assetPublisher *assets.OSSUtilPublisher) *AdminAssetService {
- return &AdminAssetService{
- store: store,
- assetBaseURL: strings.TrimRight(strings.TrimSpace(assetBaseURL), "/"),
- assetPublisher: assetPublisher,
- }
- }
- func (s *AdminAssetService) ListManagedAssets(ctx context.Context, limit int) ([]ManagedAssetSummary, error) {
- items, err := s.store.ListManagedAssets(ctx, limit)
- if err != nil {
- return nil, err
- }
- result := make([]ManagedAssetSummary, 0, len(items))
- for _, item := range items {
- result = append(result, buildManagedAssetSummary(item))
- }
- return result, nil
- }
- func (s *AdminAssetService) GetManagedAsset(ctx context.Context, assetPublicID string) (*ManagedAssetSummary, error) {
- record, err := s.store.GetManagedAssetByPublicID(ctx, strings.TrimSpace(assetPublicID))
- if err != nil {
- return nil, err
- }
- if record == nil {
- return nil, apperr.New(http.StatusNotFound, "asset_not_found", "asset not found")
- }
- summary := buildManagedAssetSummary(*record)
- return &summary, nil
- }
- func (s *AdminAssetService) RegisterExternalLink(ctx context.Context, input RegisterLinkAssetInput) (*ManagedAssetSummary, error) {
- if err := validateManagedAssetInput(input.AssetType, input.AssetCode, input.Version); err != nil {
- return nil, err
- }
- publicURL := strings.TrimSpace(input.PublicURL)
- if publicURL == "" {
- return nil, apperr.New(http.StatusBadRequest, "invalid_params", "publicUrl is required")
- }
- publicID, err := security.GeneratePublicID("asset")
- if err != nil {
- return nil, err
- }
- tx, err := s.store.Begin(ctx)
- if err != nil {
- return nil, err
- }
- defer tx.Rollback(ctx)
- record, err := s.store.CreateManagedAsset(ctx, tx, postgres.CreateManagedAssetParams{
- PublicID: publicID,
- AssetType: normalizeCode(input.AssetType),
- AssetCode: normalizeCode(input.AssetCode),
- Version: strings.TrimSpace(input.Version),
- Title: assetTrimStringPtr(input.Title),
- SourceMode: "external_link",
- StorageProvider: "external",
- ObjectKey: nil,
- PublicURL: publicURL,
- FileName: assetTrimStringPtr(input.FileName),
- ContentType: assetTrimStringPtr(input.ContentType),
- FileSizeBytes: nil,
- ChecksumSHA256: nil,
- Status: normalizeManagedAssetStatus(input.Status),
- MetadataJSONB: normalizeJSONMap(input.Metadata),
- })
- if err != nil {
- return nil, err
- }
- if err := tx.Commit(ctx); err != nil {
- return nil, err
- }
- summary := buildManagedAssetSummary(*record)
- return &summary, nil
- }
- func (s *AdminAssetService) UploadAssetFile(ctx context.Context, input UploadAssetFileInput) (*ManagedAssetSummary, error) {
- if err := validateManagedAssetInput(input.AssetType, input.AssetCode, input.Version); err != nil {
- return nil, err
- }
- if !s.assetPublisher.Enabled() {
- return nil, apperr.New(http.StatusFailedDependency, "asset_publisher_not_configured", "asset publisher is not configured")
- }
- if strings.TrimSpace(input.TempPath) == "" || strings.TrimSpace(input.FileName) == "" {
- return nil, apperr.New(http.StatusBadRequest, "invalid_params", "upload file is required")
- }
- objectDir := s.defaultObjectDir(input.AssetType, input.AssetCode, input.Version, input.ObjectDir)
- publicURL := s.assetBaseURL + "/" + strings.TrimLeft(path.Join(objectDir, sanitizeFileName(input.FileName)), "/")
- if err := s.assetPublisher.UploadFile(ctx, publicURL, input.TempPath); err != nil {
- return nil, err
- }
- publicID, err := security.GeneratePublicID("asset")
- if err != nil {
- return nil, err
- }
- objectKey := strings.TrimPrefix(strings.TrimPrefix(publicURL, s.assetBaseURL), "/")
- fileName := sanitizeFileName(input.FileName)
- contentType := detectContentType(fileName, input.ContentType)
- checksum := strings.TrimSpace(input.Checksum)
- tx, err := s.store.Begin(ctx)
- if err != nil {
- return nil, err
- }
- defer tx.Rollback(ctx)
- record, err := s.store.CreateManagedAsset(ctx, tx, postgres.CreateManagedAssetParams{
- PublicID: publicID,
- AssetType: normalizeCode(input.AssetType),
- AssetCode: normalizeCode(input.AssetCode),
- Version: strings.TrimSpace(input.Version),
- Title: assetTrimStringPtr(input.Title),
- SourceMode: "uploaded",
- StorageProvider: "oss",
- ObjectKey: stringPtr(objectKey),
- PublicURL: publicURL,
- FileName: stringPtr(fileName),
- ContentType: stringPtr(contentType),
- FileSizeBytes: &input.FileSize,
- ChecksumSHA256: stringPtr(checksum),
- Status: normalizeManagedAssetStatus(input.Status),
- MetadataJSONB: normalizeJSONMap(input.Metadata),
- })
- if err != nil {
- return nil, err
- }
- if err := tx.Commit(ctx); err != nil {
- return nil, err
- }
- summary := buildManagedAssetSummary(*record)
- return &summary, nil
- }
- func (s *AdminAssetService) defaultObjectDir(assetType, assetCode, version string, preferred *string) string {
- if preferred != nil && strings.TrimSpace(*preferred) != "" {
- return strings.Trim(strings.ReplaceAll(strings.TrimSpace(*preferred), "\\", "/"), "/")
- }
- return path.Join("uploads", normalizeCode(assetType), normalizeCode(assetCode), strings.TrimSpace(version))
- }
- func buildManagedAssetSummary(record postgres.ManagedAssetRecord) ManagedAssetSummary {
- return ManagedAssetSummary{
- ID: record.PublicID,
- AssetType: record.AssetType,
- AssetCode: record.AssetCode,
- Version: record.Version,
- Title: record.Title,
- SourceMode: record.SourceMode,
- StorageProvider: record.StorageProvider,
- ObjectKey: record.ObjectKey,
- PublicURL: record.PublicURL,
- FileName: record.FileName,
- ContentType: record.ContentType,
- FileSizeBytes: record.FileSizeBytes,
- ChecksumSHA256: record.ChecksumSHA256,
- Status: record.Status,
- Metadata: normalizeJSONMap(record.MetadataJSONB),
- }
- }
- func validateManagedAssetInput(assetType, assetCode, version string) error {
- if normalizeCode(assetType) == "" || normalizeCode(assetCode) == "" || strings.TrimSpace(version) == "" {
- return apperr.New(http.StatusBadRequest, "invalid_params", "assetType, assetCode and version are required")
- }
- return nil
- }
- func normalizeManagedAssetStatus(value string) string {
- switch strings.ToLower(strings.TrimSpace(value)) {
- case "", "active":
- return "active"
- case "draft", "disabled", "archived":
- return strings.ToLower(strings.TrimSpace(value))
- default:
- return "active"
- }
- }
- func normalizeCode(value string) string {
- value = strings.TrimSpace(strings.ToLower(value))
- value = strings.ReplaceAll(value, " ", "-")
- return value
- }
- func sanitizeFileName(name string) string {
- name = filepath.Base(strings.TrimSpace(name))
- name = strings.ReplaceAll(name, " ", "-")
- return name
- }
- func detectContentType(fileName, provided string) string {
- if strings.TrimSpace(provided) != "" {
- return strings.TrimSpace(provided)
- }
- if ext := filepath.Ext(fileName); ext != "" {
- if guessed := mime.TypeByExtension(ext); guessed != "" {
- return guessed
- }
- }
- return "application/octet-stream"
- }
- func stringPtr(value string) *string {
- value = strings.TrimSpace(value)
- if value == "" {
- return nil
- }
- return &value
- }
- func assetTrimStringPtr(value *string) *string {
- if value == nil {
- return nil
- }
- trimmed := strings.TrimSpace(*value)
- if trimmed == "" {
- return nil
- }
- return &trimmed
- }
- func normalizeJSONMap(value map[string]any) map[string]any {
- if value == nil {
- return map[string]any{}
- }
- return value
- }
|