spark-server/internal/module/upload/entity/purpose.go
2026-09-18 17:51:42 +08:00

192 lines
5.7 KiB
Go

package entity
import (
"fmt"
"path"
"strings"
"unicode"
)
const (
PurposeAvatar = "avatar"
PurposeGroupAvatar = "group_avatar"
PurposeChatImage = "chat_image"
PurposeChatVoice = "chat_voice"
PurposeChatVideo = "chat_video"
PurposeChatGIF = "chat_gif"
PurposeChatFile = "chat_file"
PurposeMomentImage = "moment_image"
PurposeMomentVideo = "moment_video"
PurposeMomentCover = "moment_cover"
PrefixPerm = "perm/"
PrefixChat = "chat/"
PrefixAvatar = "perm/avatar/"
PrefixGroup = "perm/group/"
PrefixFav = "perm/fav/"
PrefixChatImage = "chat/image/"
PrefixChatVoice = "chat/voice/"
PrefixChatVideo = "chat/video/"
PrefixChatGIF = "chat/gif/"
PrefixChatFile = "chat/file/"
PrefixMoment = "perm/moment/"
PrefixMomentImage = "perm/moment/image/"
PrefixMomentVideo = "perm/moment/video/"
PrefixMomentCover = "perm/moment/cover/"
MethodPUT = "PUT"
MethodGET = "GET"
)
const (
maxAvatar = 2 << 20
maxImage = 32 << 20
maxMoment = 10 << 20
maxGIF = 10 << 20
maxVoice = 16 << 20
maxVideo = 100 << 20
maxFile = 50 << 20
maxKeyLen = 512
extDefault = "bin"
)
// Spec is the server-owned policy for one upload purpose.
type Spec struct {
Purpose string
MaxSize int64
MIMEs map[string]struct{}
}
func LookupSpec(purpose string) (Spec, bool) {
s, ok := specs[purpose]
return s, ok
}
func NormalizeMIME(raw string) string {
s := strings.ToLower(strings.TrimSpace(raw))
s, _, _ = strings.Cut(s, ";")
return strings.TrimSpace(s)
}
func ExtForMIME(mime string) string {
if ext, ok := mimeExt[NormalizeMIME(mime)]; ok {
return ext
}
return extDefault
}
func ExtOfKey(key string) string {
base := path.Base(strings.TrimSpace(key))
ext := strings.ToLower(strings.TrimPrefix(path.Ext(base), "."))
if ext == "" || len(ext) > 8 {
return extDefault
}
for _, r := range ext {
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
return extDefault
}
}
return ext
}
func BuildObjectKey(purpose string, ownerID int64, name, day, ext string) string {
if ext == "" {
ext = extDefault
}
if name == "" {
name = "0"
}
switch purpose {
case PurposeAvatar:
return fmt.Sprintf("%s%s/%s.%s", PrefixAvatar, day, name, ext)
case PurposeGroupAvatar:
return fmt.Sprintf("%s%d/%s.%s", PrefixGroup, ownerID, name, ext)
case PurposeMomentImage:
return fmt.Sprintf("%s%s/%s.%s", PrefixMomentImage, day, name, ext)
case PurposeMomentVideo:
return fmt.Sprintf("%s%s/%s.%s", PrefixMomentVideo, day, name, ext)
case PurposeMomentCover:
return fmt.Sprintf("%s%s/%s.%s", PrefixMomentCover, day, name, ext)
default:
return fmt.Sprintf("%s%s/%s/%s.%s", PrefixChat, chatKind(purpose), day, name, ext)
}
}
func BuildFavKey(name, day, ext string) string {
if ext == "" {
ext = extDefault
}
if name == "" {
name = "0"
}
return fmt.Sprintf("%s%s/%s.%s", PrefixFav, day, name, ext)
}
func chatKind(purpose string) string {
switch purpose {
case PurposeChatImage:
return "image"
case PurposeChatVoice:
return "voice"
case PurposeChatVideo:
return "video"
case PurposeChatGIF:
return "gif"
default:
return "file"
}
}
func ValidObjectKey(key string) bool {
if key == "" || len(key) > maxKeyLen {
return false
}
if strings.Contains(key, "..") || strings.ContainsAny(key, " \t\n\\") || strings.HasPrefix(key, "/") {
return false
}
return strings.HasPrefix(key, PrefixPerm) || strings.HasPrefix(key, PrefixChat)
}
var specs = map[string]Spec{
PurposeAvatar: {Purpose: PurposeAvatar, MaxSize: maxAvatar, MIMEs: imageMIMEs},
PurposeGroupAvatar: {Purpose: PurposeGroupAvatar, MaxSize: maxAvatar, MIMEs: imageMIMEs},
PurposeMomentCover: {Purpose: PurposeMomentCover, MaxSize: maxAvatar, MIMEs: imageMIMEs},
PurposeMomentImage: {Purpose: PurposeMomentImage, MaxSize: maxMoment, MIMEs: momentImageMIMEs},
PurposeMomentVideo: {Purpose: PurposeMomentVideo, MaxSize: maxVideo, MIMEs: videoMIMEs},
PurposeChatImage: {Purpose: PurposeChatImage, MaxSize: maxImage, MIMEs: imageMIMEs},
PurposeChatGIF: {Purpose: PurposeChatGIF, MaxSize: maxGIF, MIMEs: gifMIMEs},
PurposeChatVoice: {Purpose: PurposeChatVoice, MaxSize: maxVoice, MIMEs: voiceMIMEs},
PurposeChatVideo: {Purpose: PurposeChatVideo, MaxSize: maxVideo, MIMEs: videoMIMEs},
PurposeChatFile: {Purpose: PurposeChatFile, MaxSize: maxFile, MIMEs: fileMIMEs},
}
var imageMIMEs = set("image/jpeg", "image/jpg", "image/png")
var momentImageMIMEs = set("image/jpeg", "image/jpg", "image/png", "image/gif")
var gifMIMEs = set("image/gif", "image/webp")
var voiceMIMEs = set("audio/mpeg", "audio/mp4", "audio/aac", "audio/amr", "audio/ogg", "audio/wav", "audio/webm", "audio/x-m4a")
var videoMIMEs = set("video/mp4", "video/quicktime", "video/webm")
var fileMIMEs = set(
"application/pdf", "application/zip", "application/x-zip-compressed",
"application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/octet-stream", "text/plain",
)
var mimeExt = map[string]string{
"image/jpeg": "jpg", "image/jpg": "jpg", "image/png": "png", "image/webp": "webp",
"image/gif": "gif",
"audio/mpeg": "mp3", "audio/mp4": "m4a", "audio/aac": "aac", "audio/amr": "amr",
"audio/ogg": "ogg", "audio/wav": "wav", "audio/webm": "weba", "audio/x-m4a": "m4a",
"video/mp4": "mp4", "video/quicktime": "mov", "video/webm": "webm",
"application/pdf": "pdf", "application/zip": "zip", "application/x-zip-compressed": "zip",
"application/octet-stream": extDefault, "text/plain": "txt",
}
func set(values ...string) map[string]struct{} {
m := make(map[string]struct{}, len(values))
for _, v := range values {
m[v] = struct{}{}
}
return m
}