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" PurposeSquareImage = "square_image" PurposeSquareVideo = "square_video" 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/" PrefixSquareImage = "perm/square/image/" PrefixSquareVideo = "perm/square/video/" 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) case PurposeSquareImage: return fmt.Sprintf("%s%s/%s.%s", PrefixSquareImage, day, name, ext) case PurposeSquareVideo: return fmt.Sprintf("%s%s/%s.%s", PrefixSquareVideo, 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}, PurposeSquareImage: {Purpose: PurposeSquareImage, MaxSize: maxMoment, MIMEs: momentImageMIMEs}, PurposeSquareVideo: {Purpose: PurposeSquareVideo, 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 }