package document_service import ( "bytes" "context" "fmt" "mime" "net/http" "path/filepath" "strings" "time" "github.com/XingfenD/yoresee_doc/internal/config" "github.com/XingfenD/yoresee_doc/internal/dto" "github.com/XingfenD/yoresee_doc/internal/model" "github.com/XingfenD/yoresee_doc/internal/status" "github.com/XingfenD/yoresee_doc/internal/utils" "github.com/XingfenD/yoresee_doc/pkg/storage" "github.com/sirupsen/logrus" ) const maxAttachmentSize = 5 * 1024 * 1024 func (s *DocumentService) UploadAttachment( ctx context.Context, userExternalID string, documentExternalID string, file []byte, fileName string, contentType string, ) (*dto.AttachmentResponse, error) { if s == nil || strings.TrimSpace(userExternalID) == "" || strings.TrimSpace(documentExternalID) == "" { return nil, status.GenErrWithCustomMsg(status.StatusParamError, "invalid upload attachment request") } if len(file) == 0 || len(file) > maxAttachmentSize { return nil, status.GenErrWithCustomMsg(status.StatusParamError, "invalid attachment file") } if config.GlobalConfig == nil || strings.TrimSpace(config.GlobalConfig.Minio.Bucket) == "" { return nil, status.GenErrWithCustomMsg(status.StatusServiceInternalError, "attachment storage is not configured") } documentID, err := s.documentRepo.GetIDByExternalID(documentExternalID).Exec(ctx) if err != nil { logrus.Errorf("[Service layer: DocumentService] query document id failed, document_external_id=%s, err=%+v", documentExternalID, err) return nil, status.GenErrWithCustomMsg(status.StatusReadDBError, "query document failed") } if documentID <= 0 { return nil, status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "document not found") } userID, err := s.userRepo.GetIDByExternalID(userExternalID).Exec() if err != nil || userID <= 0 { logrus.Errorf("[Service layer: DocumentService] query user id failed, user_external_id=%s, err=%+v", userExternalID, err) return nil, status.GenErrWithCustomMsg(status.StatusUserNotFound, "user not found") } safeName := strings.TrimSpace(fileName) if safeName == "" { safeName = "attachment.bin" } contentType = strings.TrimSpace(contentType) if contentType == "" { contentType = http.DetectContentType(file) } attachmentExternalID := utils.GenerateExternalID(utils.ExternalIDContextAttachment) objectName := fmt.Sprintf( "attachments/%s/%s-%d%s", documentExternalID, attachmentExternalID, time.Now().UnixNano(), resolveAttachmentExt(safeName, contentType), ) err = storage.PutFile( config.GlobalConfig.Minio.Bucket, objectName, bytes.NewReader(file), int64(len(file)), contentType, ) if err != nil { logrus.Errorf("[Service layer: DocumentService] upload attachment to minio failed, document_external_id=%s, object_name=%s, err=%+v", documentExternalID, objectName, err) return nil, status.GenErrWithCustomMsg(status.StatusServiceInternalError, "upload attachment failed") } attachment := &model.Attachment{ ExternalID: attachmentExternalID, DocumentID: documentID, Name: safeName, Path: objectName, Size: int64(len(file)), MimeType: contentType, UserID: userID, } if err = s.attachmentRepo.Create(attachment); err != nil { logrus.Errorf("[Service layer: DocumentService] save attachment meta failed, document_external_id=%s, attachment_external_id=%s, err=%+v", documentExternalID, attachmentExternalID, err) return nil, status.GenErrWithCustomMsg(status.StatusWriteDBError, "save attachment failed") } return &dto.AttachmentResponse{ ExternalID: attachment.ExternalID, DocumentExternalID: documentExternalID, Name: attachment.Name, Size: attachment.Size, MimeType: attachment.MimeType, Path: attachment.Path, URL: storage.BuildPublicObjectPath(attachment.Path), CreatedAt: attachment.CreatedAt, UpdatedAt: attachment.UpdatedAt, }, nil } func (s *DocumentService) ListAttachments(ctx context.Context, documentExternalID string) ([]*dto.AttachmentResponse, error) { if s == nil || strings.TrimSpace(documentExternalID) == "" { return nil, status.GenErrWithCustomMsg(status.StatusParamError, "invalid list attachments request") } if config.GlobalConfig == nil || strings.TrimSpace(config.GlobalConfig.Minio.Bucket) == "" { return nil, status.GenErrWithCustomMsg(status.StatusServiceInternalError, "attachment storage is not configured") } documentID, err := s.documentRepo.GetIDByExternalID(documentExternalID).Exec(ctx) if err != nil { logrus.Errorf("[Service layer: DocumentService] query document id failed, document_external_id=%s, err=%+v", documentExternalID, err) return nil, status.GenErrWithCustomMsg(status.StatusReadDBError, "query document failed") } if documentID <= 0 { return nil, status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "document not found") } attachments, err := s.attachmentRepo.ListByDocumentID(documentID) if err != nil { logrus.Errorf("[Service layer: DocumentService] list attachments failed, document_external_id=%s, document_id=%d, err=%+v", documentExternalID, documentID, err) return nil, status.GenErrWithCustomMsg(status.StatusReadDBError, "list attachments failed") } resp := make([]*dto.AttachmentResponse, 0, len(attachments)) for _, attachment := range attachments { resp = append(resp, &dto.AttachmentResponse{ ExternalID: attachment.ExternalID, DocumentExternalID: documentExternalID, Name: attachment.Name, Size: attachment.Size, MimeType: attachment.MimeType, Path: attachment.Path, URL: storage.BuildPublicObjectPath(attachment.Path), CreatedAt: attachment.CreatedAt, UpdatedAt: attachment.UpdatedAt, }) } return resp, nil } func (s *DocumentService) DeleteAttachment(ctx context.Context, documentExternalID, attachmentExternalID string) error { if s == nil || strings.TrimSpace(documentExternalID) == "" || strings.TrimSpace(attachmentExternalID) == "" { return status.GenErrWithCustomMsg(status.StatusParamError, "invalid delete attachment request") } if config.GlobalConfig == nil || strings.TrimSpace(config.GlobalConfig.Minio.Bucket) == "" { return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "attachment storage is not configured") } documentID, err := s.documentRepo.GetIDByExternalID(documentExternalID).Exec(ctx) if err != nil { logrus.Errorf("[Service layer: DocumentService] query document id failed, document_external_id=%s, err=%+v", documentExternalID, err) return status.GenErrWithCustomMsg(status.StatusReadDBError, "query document failed") } if documentID <= 0 { return status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "document not found") } attachment, err := s.attachmentRepo.GetByExternalIDAndDocumentID(attachmentExternalID, documentID) if err != nil { logrus.Errorf("[Service layer: DocumentService] query attachment failed, document_external_id=%s, attachment_external_id=%s, err=%+v", documentExternalID, attachmentExternalID, err) return status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "attachment not found") } if err = storage.DeleteFile(config.GlobalConfig.Minio.Bucket, attachment.Path); err != nil { logrus.Errorf("[Service layer: DocumentService] delete attachment object failed, attachment_external_id=%s, path=%s, err=%+v", attachmentExternalID, attachment.Path, err) return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "delete attachment failed") } if err = s.attachmentRepo.DeleteByID(attachment.ID); err != nil { logrus.Errorf("[Service layer: DocumentService] delete attachment meta failed, attachment_external_id=%s, err=%+v", attachmentExternalID, err) return status.GenErrWithCustomMsg(status.StatusWriteDBError, "delete attachment failed") } return nil } func resolveAttachmentExt(filename, contentType string) string { ext := strings.TrimSpace(strings.ToLower(filepath.Ext(filename))) if ext != "" { return ext } exts, err := mime.ExtensionsByType(contentType) if err == nil && len(exts) > 0 { return exts[0] } return ".bin" }