spark-server/internal/module/upload/logic/credential.go
2026-09-22 17:07:26 +08:00

97 lines
3.1 KiB
Go

package logic
import (
"context"
"strconv"
"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
}
return s.issuePut(ctx, userID, in)
}
func (s *Service) issuePut(ctx context.Context, userID int64, in *CreateInput) (*entity.Credential, error) {
_, mime, err := matchSpec(ctx, in)
if err != nil {
return nil, err
}
name := reqctx.NewObjectName()
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, name, dayOf(s.now()), entity.ExtForMIME(mime))
url, err := s.store.PresignPut(ctx, key, mime, in.Size, 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,
"Content-Length": strconv.FormatInt(in.Size, 10),
"If-None-Match": "*",
},
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)
}