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" PrefixPerm = "perm/" PrefixChat = "chat/" PrefixAvatar = "perm/avatar/" PrefixGroup = "perm/group/" PrefixFav = "perm/fav/" MethodPUT = "PUT" MethodGET = "GET" ) const ( maxAvatar = 2 << 20 maxImage = 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, objectID int64, day, ext string) string { if ext == "" { ext = extDefault } switch purpose { case PurposeAvatar: return fmt.Sprintf("%s%d/%s/%d.%s", PrefixAvatar, ownerID, day, objectID, ext) case PurposeGroupAvatar: return fmt.Sprintf("%s%d/%d.%s", PrefixGroup, ownerID, objectID, ext) default: return fmt.Sprintf("%s%s/%d/%d.%s", PrefixChat, day, ownerID, objectID, ext) } } func BuildFavKey(userID, objectID int64, day, ext string) string { if ext == "" { ext = extDefault } return fmt.Sprintf("%s%d/%s/%d.%s", PrefixFav, userID, day, objectID, ext) } 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}, PurposeChatImage: {Purpose: PurposeChatImage, MaxSize: maxImage, MIMEs: imageMIMEs}, PurposeChatGIF: {Purpose: PurposeChatGIF, MaxSize: maxImage, 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", "image/webp", "image/heic", "image/heif") 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/heic": "heic", "image/heif": "heif", "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 }