package services import ( "bufio" "bytes" "io" "mime/multipart" "net/http" "strings" "time" "code.tczkiot.com/wlw/ai-agent/internal/models" "code.tczkiot.com/wlw/ai-agent/internal/pkg/config" "code.tczkiot.com/wlw/ai-agent/internal/pkg/dto" "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums" "code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx" "code.tczkiot.com/wlw/ai-agent/internal/pkg/utils" "code.tczkiot.com/wlw/ai-agent/internal/repositories" "code.tczkiot.com/wlw/ai-agent/internal/services/storage" "github.com/google/uuid" "github.com/mlogclub/simple/sqls" ) var AssetService = newAssetService() func newAssetService() *assetService { return &assetService{} } type assetService struct { } func (s *assetService) Get(id int64) *models.Asset { return repositories.AssetRepository.Get(sqls.DB(), id) } func (s *assetService) GetByAssetID(assetID string) *models.Asset { return repositories.AssetRepository.GetByAssetID(sqls.DB(), strings.TrimSpace(assetID)) } func (s *assetService) GetByStorageKey(storageKey string) *models.Asset { return repositories.AssetRepository.GetByStorageKey(sqls.DB(), strings.TrimSpace(storageKey)) } func (s *assetService) FindPageByCnd(cnd *sqls.Cnd) (list []models.Asset, paging *sqls.Paging) { return repositories.AssetRepository.FindPageByCnd(sqls.DB(), cnd) } func (s *assetService) OpenReader(asset *models.Asset) (io.ReadCloser, error) { if asset == nil { return nil, errorsx.InvalidParamI18n("error.e0146") } provider, err := storage.NewProvider(asset.Provider) if err != nil { return nil, err } return provider.Read(asset.StorageKey) } func (s *assetService) UploadBytes(data []byte, prefix, filename string, principal *dto.AuthPrincipal) (*models.Asset, error) { return s.uploadBytes(data, prefix, filename, 0, principal) } func (s *assetService) UploadConversationBytes(data []byte, prefix, filename string, conversationID int64, principal *dto.AuthPrincipal) (*models.Asset, error) { if conversationID <= 0 { return nil, errorsx.InvalidParamI18n("error.e0064") } return s.uploadBytes(data, prefix, filename, conversationID, principal) } func (s *assetService) uploadBytes(data []byte, prefix, filename string, conversationID int64, principal *dto.AuthPrincipal) (*models.Asset, error) { src := bytes.NewReader(data) return s.Upload(src, storage.UploadInfo{ Prefix: prefix, ConversationID: conversationID, Filename: filename, FileSize: int64(len(data)), MimeType: http.DetectContentType(data), Principal: principal, }) } func (s *assetService) UploadFile(file *multipart.FileHeader, prefix string, principal *dto.AuthPrincipal) (*models.Asset, error) { return s.uploadFile(file, prefix, 0, false, principal) } func (s *assetService) UploadConversationFile(file *multipart.FileHeader, prefix string, conversationID int64, principal *dto.AuthPrincipal) (*models.Asset, error) { if conversationID <= 0 { return nil, errorsx.InvalidParamI18n("error.e0064") } return s.uploadFile(file, prefix, conversationID, false, principal) } func (s *assetService) UploadConversationImageFile(file *multipart.FileHeader, prefix string, conversationID int64, principal *dto.AuthPrincipal) (*models.Asset, error) { if conversationID <= 0 { return nil, errorsx.InvalidParamI18n("error.e0064") } return s.uploadFile(file, prefix, conversationID, true, principal) } func (s *assetService) uploadFile(file *multipart.FileHeader, prefix string, conversationID int64, imageOnly bool, principal *dto.AuthPrincipal) (*models.Asset, error) { if file == nil { return nil, errorsx.InvalidParamI18n("error.e0323") } cfg := config.Current() if file.Size > cfg.Storage.MaxUploadSizeBytes() { return nil, errorsx.InvalidParamI18n("error.e0079") } src, err := file.Open() if err != nil { return nil, err } defer func() { _ = src.Close() }() reader := bufio.NewReader(src) header, _ := reader.Peek(512) mimeType := strings.TrimSpace(strings.Split(http.DetectContentType(header), ";")[0]) if imageOnly && !isSupportedVisionImageMIME(mimeType) { return nil, errorsx.InvalidParamI18n("error.e0090") } return s.Upload(reader, storage.UploadInfo{ Prefix: prefix, ConversationID: conversationID, Filename: file.Filename, FileSize: file.Size, MimeType: mimeType, Principal: principal, }) } func (s *assetService) Upload(reader io.Reader, info storage.UploadInfo) (*models.Asset, error) { provider, err := storage.GetDefault() if err != nil { return nil, err } assetID, key := storage.GenerateStorageKey(info) item := &models.Asset{ ConversationID: info.ConversationID, AssetID: assetID, Provider: provider.ProviderType(), StorageKey: key, Filename: info.Filename, FileSize: info.FileSize, MimeType: info.MimeType, Status: enums.AssetStatusPending, AuditFields: utils.BuildAuditFields(info.Principal), } if err := repositories.AssetRepository.Create(sqls.DB(), item); err != nil { return nil, err } if _, err := provider.Upload(reader, key, storage.UploadInfo{ Prefix: info.Prefix, ConversationID: info.ConversationID, Filename: info.Filename, FileSize: info.FileSize, MimeType: info.MimeType, Principal: info.Principal, }); err != nil { _ = s.markAssetStatus(item.ID, enums.AssetStatusFailed, info.Principal) return nil, err } item.Status = enums.AssetStatusSuccess _ = repositories.AssetRepository.UpdateColumn(sqls.DB(), item.ID, "status", enums.AssetStatusSuccess) return item, nil } func isSupportedVisionImageMIME(mimeType string) bool { switch strings.ToLower(strings.TrimSpace(strings.Split(mimeType, ";")[0])) { case "image/jpeg", "image/png", "image/gif", "image/webp": return true default: return false } } func (s *assetService) GetSignedURL(id int64) (string, error) { item := s.Get(id) if item == nil { return "", errorsx.InvalidParamI18n("error.e0214") } if item.Status != enums.AssetStatusSuccess { return "", errorsx.InvalidParamI18n("error.e0213") } provider, err := storage.NewProvider(item.Provider) if err != nil { return "", err } accessURL := provider.GetSignedURL(item.StorageKey) return accessURL, nil } func (s *assetService) DeleteAsset(id int64, principal *dto.AuthPrincipal) error { if principal == nil { return errorsx.UnauthorizedI18n("error.auth.expired") } item := s.Get(id) if item == nil { return errorsx.InvalidParamI18n("error.e0214") } return repositories.AssetRepository.Updates(sqls.DB(), id, map[string]any{ "status": enums.AssetStatusDeleted, "update_user_id": principal.UserID, "update_user_name": principal.Username, "updated_at": time.Now(), }) } func (s *assetService) markAssetStatus(id int64, status enums.AssetStatus, principal *dto.AuthPrincipal) error { updates := map[string]any{ "status": status, "updated_at": time.Now(), } if principal != nil { updates["update_user_id"] = principal.UserID updates["update_user_name"] = principal.Username } return repositories.AssetRepository.Updates(sqls.DB(), id, updates) } func (s *assetService) buildFilenameFromMime(mimeType string) string { mimeType = strings.TrimSpace(strings.Split(mimeType, ";")[0]) ext := ".bin" switch mimeType { case "image/jpeg": ext = ".jpg" case "image/png": ext = ".png" case "image/gif": ext = ".gif" case "image/webp": ext = ".webp" case "application/pdf": ext = ".pdf" case "text/plain": ext = ".txt" } return "wxwork_" + strings.ReplaceAll(uuid.NewString(), "-", "") + ext }