package logic import ( "context" "spark-server/internal/module/upload/entity" "spark-server/internal/pkg/errs" "spark-server/internal/pkg/i18n" "spark-server/internal/pkg/reqctx" "spark-server/internal/pkg/validate" ) // CreateInput requests a single-object Presigned PUT. The client does not choose the key. type CreateInput struct { Purpose string `json:"purpose" validate:"required" label:"field.purpose" errmsg:"required:validate.upload.purpose_required"` Mime string `json:"mime" validate:"required,max=128" label:"field.mime" errmsg:"required:validate.upload.mime_required;max:validate.upload.mime_invalid"` Size int64 `json:"size" validate:"required,gt=0" label:"field.size" errmsg:"required:validate.upload.size_required;gt:validate.upload.size_invalid"` GroupID int64 `json:"group_id" validate:"omitempty,gt=0" label:"field.group_id"` } func (s *Service) CreateCredential(ctx context.Context, userID int64, in *CreateInput) (*entity.Credential, error) { if !s.ready() { return nil, errs.ErrAvailable } if userID <= 0 { return nil, errs.ErrUnauthorized } if in == nil { return nil, errs.ErrInvalidArgument } if err := validate.StructLang(reqctx.Lang(ctx), in); err != nil { return nil, err } if !s.allow(userID) { return nil, errs.ErrRateLimited } _, mime, err := matchSpec(ctx, in) if err != nil { return nil, err } objectID, err := s.ids.NextID() if err != nil { return nil, err } ownerID := userID if in.Purpose == entity.PurposeGroupAvatar { if err := s.readyGroupAvatar(ctx, userID, in.GroupID); err != nil { return nil, err } ownerID = in.GroupID } key := entity.BuildObjectKey(in.Purpose, ownerID, objectID, dayOf(s.now()), entity.ExtForMIME(mime)) url, err := s.store.PresignPut(ctx, key, mime, s.ttl) if err != nil { return nil, err } return &entity.Credential{ ObjectKey: key, UploadURL: url, Method: entity.MethodPUT, Headers: map[string]string{"Content-Type": mime}, ExpiresIn: s.expiresIn(), }, nil } func matchSpec(ctx context.Context, in *CreateInput) (entity.Spec, string, error) { lang := reqctx.Lang(ctx) spec, ok := entity.LookupSpec(in.Purpose) if !ok { return entity.Spec{}, "", errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeyUploadPurposeInvalid, nil)) } mime := entity.NormalizeMIME(in.Mime) if _, ok := spec.MIMEs[mime]; !ok { return entity.Spec{}, "", errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeyUploadMIMEInvalid, nil)) } if in.Size > spec.MaxSize { return entity.Spec{}, "", errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeyUploadSizeExceeded, nil)) } return spec, mime, nil } func (s *Service) readyGroupAvatar(ctx context.Context, userID, groupID int64) error { if groupID <= 0 { return errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeyUploadGroupRequired, nil)) } if s.groups == nil { return nil } return s.groups.CanSetAvatar(ctx, userID, groupID) }