diff --git a/api.go b/api.go index 1a034f49f18551f79d5e92c4cf57f0a6fd1bda09..77bcd1a8ae21922f4b26918da91629243e2844ad 100644 --- a/api.go +++ b/api.go @@ -5,40 +5,14 @@ import ( log "github.com/sirupsen/logrus" "io/ioutil" "net/http" + "random.chars.jp/git/image-board/api" "random.chars.jp/git/image-board/store" "strconv" "strings" ) -type UserPayload struct { - Username string `json:"username"` - ID string `json:"id"` - Privileged bool `json:"privileged"` -} - -type UserCreatePayload struct { - Username string `json:"username"` - Password string `json:"password"` - Privileged bool `json:"privileged"` -} - -type UserUpdatePayload struct { - Username string `json:"username"` -} - -type TagUpdatePayload struct { - Type string `json:"type"` -} - -type ImageUpdatePayload struct { - Source string `json:"source"` -} - -var unauthorized = gin.H{"error": "not authorized"} -var denied = gin.H{"error": "permission denied"} - func registerAPI() { - router.GET("/api", func(context *gin.Context) { + router.GET(api.Base, func(context *gin.Context) { context.JSON(http.StatusOK, store.Info{ Revision: instance.Revision, Compat: instance.Compat, @@ -49,99 +23,110 @@ func registerAPI() { }) }) - router.GET("/api/single_user", func(context *gin.Context) { + router.GET(api.SingleUser, func(context *gin.Context) { context.JSON(http.StatusOK, instance.SingleUser) }) - router.GET("/api/user", func(context *gin.Context) { + router.GET(api.User, func(context *gin.Context) { context.JSON(http.StatusOK, instance.Users()) }) - router.PUT("/api/user", func(context *gin.Context) { + router.PUT(api.User, func(context *gin.Context) { user, ok := getUser(context) if !instance.Register { if !ok { - context.JSON(http.StatusForbidden, gin.H{ - "error": "user registration disallowed", - }) + context.JSON(http.StatusForbidden, api.Error{Error: "user registration daisallowed"}) return } if !user.Privileged { - context.JSON(http.StatusForbidden, denied) + context.JSON(http.StatusForbidden, api.Denied) return } } - var payload UserCreatePayload + var payload api.UserCreatePayload if err := context.ShouldBindJSON(&payload); err != nil { - context.JSON(http.StatusBadRequest, gin.H{ - "error": err.Error(), - }) + context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) return } if !user.Privileged && payload.Privileged { - context.JSON(http.StatusForbidden, denied) + context.JSON(http.StatusForbidden, api.Denied) return } context.JSON(http.StatusOK, instance.UserAdd(payload.Username, payload.Privileged, payload.Password)) }) - router.GET("/api/user/this", func(context *gin.Context) { + router.GET(api.UserThis, func(context *gin.Context) { info, ok := getUser(context) if !ok { - context.JSON(http.StatusForbidden, unauthorized) + context.JSON(http.StatusForbidden, api.Unauthorized) return } - context.JSON(http.StatusOK, UserPayload{ + context.JSON(http.StatusOK, api.UserPayload{ Username: info.Username, ID: info.Snowflake, Privileged: info.Privileged, }) }) - router.GET("/api/user/:flake", func(context *gin.Context) { + router.GET(api.UserField, func(context *gin.Context) { info := instance.User(context.Param("flake")) - context.JSON(http.StatusOK, UserPayload{ + context.JSON(http.StatusOK, api.UserPayload{ Username: info.Username, ID: info.Snowflake, Privileged: info.Privileged, }) }) - router.PATCH("/api/user/:flake", func(context *gin.Context) { + router.PATCH(api.UserField, func(context *gin.Context) { info, ok := getUser(context) // Require sign in if !ok { - context.JSON(http.StatusForbidden, unauthorized) + context.JSON(http.StatusForbidden, api.Unauthorized) return } flake := context.Param("flake") if !info.Privileged && (info.Snowflake != flake) { - context.JSON(http.StatusForbidden, denied) + context.JSON(http.StatusForbidden, api.Denied) return } - var payload UserUpdatePayload + var payload api.UserUpdatePayload if err := context.ShouldBindJSON(&payload); err != nil { - context.JSON(http.StatusBadRequest, gin.H{ - "error": err.Error(), - }) + context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) return } instance.UserUsernameUpdate(flake, payload.Username) }) - router.DELETE("/api/user/:flake/password", func(context *gin.Context) { + router.DELETE(api.UserField, func(context *gin.Context) { + info, ok := getUser(context) + + // Require sign in + if !ok { + context.JSON(http.StatusForbidden, api.Unauthorized) + return + } + + flake := context.Param("flake") + if !info.Privileged && (info.Snowflake != flake) { + context.JSON(http.StatusForbidden, api.Denied) + return + } + instance.UserDestroy(flake) + }) + + router.DELETE(api.UserPassword, func(context *gin.Context) { info, ok := getUser(context) // Require privileged if !ok || !info.Privileged { - context.JSON(http.StatusForbidden, unauthorized) + context.JSON(http.StatusForbidden, api.Unauthorized) return } @@ -150,34 +135,30 @@ func registerAPI() { instance.UserSecretRegen(flake) }) - router.PUT("/api/user/:flake/password", func(context *gin.Context) { + router.PUT(api.UserPassword, func(context *gin.Context) { info, ok := getUser(context) // Require sign in if !ok { - context.JSON(http.StatusForbidden, unauthorized) + context.JSON(http.StatusForbidden, api.Unauthorized) return } flake := context.Param("flake") if !info.Privileged && (info.Snowflake != flake) { - context.JSON(http.StatusForbidden, denied) + context.JSON(http.StatusForbidden, api.Denied) } var newPass string if payload, err := context.GetRawData(); err != nil { - context.JSON(http.StatusBadRequest, gin.H{ - "error": err.Error(), - }) + context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) return } else { newPass = string(payload) } if newPass == "" { - context.JSON(http.StatusBadRequest, gin.H{ - "error": "empty passwords are not allowed", - }) + context.JSON(http.StatusBadRequest, api.Error{Error: "empty passwords are not allowed"}) return } @@ -187,9 +168,9 @@ func registerAPI() { }) }) - router.GET("/api/username/:name", func(context *gin.Context) { + router.GET(api.UsernameField, func(context *gin.Context) { info := instance.UserUsername(context.Param("name")) - payload := UserPayload{ + payload := api.UserPayload{ Username: info.Username, ID: info.Snowflake, Privileged: info.Privileged, @@ -197,13 +178,11 @@ func registerAPI() { context.JSON(http.StatusOK, payload) }) - router.POST("/api/username/:name/auth", func(context *gin.Context) { + router.POST(api.UsernameAuth, func(context *gin.Context) { var password string if payload, err := context.GetRawData(); err != nil { - context.JSON(http.StatusBadRequest, gin.H{ - "error": err.Error(), - }) + context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) return } else { password = string(payload) @@ -213,148 +192,72 @@ func registerAPI() { if instance.UserUsernamePasswordValidate(username, password) { context.String(http.StatusOK, instance.UserUsername(username).Secret) } else { - context.JSON(http.StatusForbidden, denied) + context.JSON(http.StatusForbidden, api.Denied) } }) - router.DELETE("/api/user/:flake", func(context *gin.Context) { + router.GET(api.UserSecret, func(context *gin.Context) { info, ok := getUser(context) // Require sign in if !ok { - context.JSON(http.StatusForbidden, unauthorized) + context.JSON(http.StatusForbidden, api.Unauthorized) return } flake := context.Param("flake") if !info.Privileged && (info.Snowflake != flake) { - context.JSON(http.StatusForbidden, denied) - return - } - instance.UserDestroy(flake) - }) - - router.GET("/api/user/:flake/secret", func(context *gin.Context) { - info, ok := getUser(context) - - // Require sign in - if !ok { - context.JSON(http.StatusForbidden, unauthorized) - return - } - - flake := context.Param("flake") - if !info.Privileged && (info.Snowflake != flake) { - context.JSON(http.StatusForbidden, denied) + context.JSON(http.StatusForbidden, api.Denied) return } context.String(http.StatusOK, instance.User(flake).Secret) }) - router.PUT("/api/user/:flake/secret", func(context *gin.Context) { + router.PUT(api.UserSecret, func(context *gin.Context) { info, ok := getUser(context) // Require sign in if !ok { - context.JSON(http.StatusForbidden, unauthorized) + context.JSON(http.StatusForbidden, api.Unauthorized) return } flake := context.Param("flake") if !info.Privileged && (info.Snowflake != flake) { - context.JSON(http.StatusForbidden, denied) + context.JSON(http.StatusForbidden, api.Denied) return } context.String(http.StatusOK, instance.UserSecretRegen(flake)) }) - router.GET("/api/user/:flake/image", func(context *gin.Context) { + router.GET(api.UserImage, func(context *gin.Context) { context.JSON(http.StatusOK, instance.UserImages(context.Param("flake"))) }) - router.GET("/api/search/:tags", func(context *gin.Context) { + router.GET(api.SearchField, func(context *gin.Context) { tagsPayload := context.Param("tags") tags := strings.Split(tagsPayload, "!") context.JSON(http.StatusOK, instance.ImageSearch(tags)) }) - router.GET("/api/image/snowflake", func(context *gin.Context) { + router.GET(api.Image, func(context *gin.Context) { info, ok := getUser(context) // Require privileged if !ok || !info.Privileged { - context.JSON(http.StatusForbidden, unauthorized) + context.JSON(http.StatusForbidden, api.Unauthorized) return } context.JSON(http.StatusOK, instance.ImageSnowflakes()) }) - router.GET("/api/image/snowflake/:flake", func(context *gin.Context) { - flake := context.Param("flake") - if _, err := strconv.Atoi(flake); err != nil { - context.JSON(http.StatusBadRequest, gin.H{ - "error": "invalid snowflake", - }) - return - } - context.JSON(http.StatusOK, instance.ImageSnowflake(flake)) - }) - - router.GET("/api/image/snowflake/:flake/file", func(context *gin.Context) { - flake := context.Param("flake") - if _, err := strconv.Atoi(flake); err != nil { - context.JSON(http.StatusBadRequest, gin.H{ - "error": "invalid snowflake", - }) - return - } - image, data := instance.ImageData(instance.ImageSnowflakeHash(flake), false) - if image.Hash == "" { - context.JSON(http.StatusNotFound, gin.H{ - "error": "not found", - }) - return - } - context.Data(http.StatusOK, "image/"+image.Type, data) - }) - - router.GET("/api/image/snowflake/:flake/preview", func(context *gin.Context) { - flake := context.Param("flake") - if _, err := strconv.Atoi(flake); err != nil { - context.JSON(http.StatusBadRequest, gin.H{ - "error": "invalid snowflake", - }) - return - } - image, data := instance.ImageData(instance.ImageSnowflakeHash(flake), true) - if image.Hash == "" { - context.JSON(http.StatusNotFound, gin.H{ - "error": "not found", - }) - return - } - context.Data(http.StatusOK, "image/jpeg", data) - }) - - router.GET("/api/image", func(context *gin.Context) { - info, ok := getUser(context) - - // Require privileged - if !ok || !info.Privileged { - context.JSON(http.StatusForbidden, unauthorized) - return - } - - context.JSON(http.StatusOK, instance.Images()) - }) - - router.POST("/api/image", func(context *gin.Context) { + router.POST(api.Image, func(context *gin.Context) { info, ok := getUser(context) // Require sign in if !ok { - context.JSON(http.StatusForbidden, unauthorized) + context.JSON(http.StatusForbidden, api.Unauthorized) return } @@ -377,264 +280,254 @@ func registerAPI() { } image := instance.ImageAdd(data, info.Snowflake) if image.Hash == "" { - context.JSON(http.StatusBadRequest, gin.H{ - "error": "invalid image", - }) + context.JSON(http.StatusBadRequest, api.Error{Error: "invalid image"}) return } context.JSON(http.StatusOK, image) }) - router.GET("/api/image/:flake", func(context *gin.Context) { + router.GET(api.ImageField, func(context *gin.Context) { context.JSON(http.StatusOK, instance.ImageSnowflake(context.Param("flake"))) }) - router.PATCH("/api/image/:flake", func(context *gin.Context) { + router.PATCH(api.ImageField, func(context *gin.Context) { user, ok := getUser(context) // Require sign in if !ok { - context.JSON(http.StatusForbidden, unauthorized) + context.JSON(http.StatusForbidden, api.Unauthorized) return } info := instance.ImageSnowflake(context.Param("flake")) if !user.Privileged && (info.User != user.Snowflake) { - context.JSON(http.StatusForbidden, denied) + context.JSON(http.StatusForbidden, api.Denied) return } - var payload ImageUpdatePayload + var payload api.ImageUpdatePayload if err := context.ShouldBindJSON(&payload); err != nil { - context.JSON(http.StatusBadRequest, gin.H{ - "error": err.Error(), - }) + context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) return } instance.ImageSource(info.Hash, payload.Source) }) - router.DELETE("/api/image/:flake", func(context *gin.Context) { + router.DELETE(api.ImageField, func(context *gin.Context) { info, ok := getUser(context) // Require sign in if !ok { - context.JSON(http.StatusForbidden, unauthorized) + context.JSON(http.StatusForbidden, api.Unauthorized) return } flake := context.Param("flake") image := instance.ImageSnowflake(flake) if !info.Privileged && (info.Snowflake != image.User) { - context.JSON(http.StatusForbidden, denied) + context.JSON(http.StatusForbidden, api.Denied) return } instance.ImageDestroy(image.Hash) }) - router.GET("/api/image/:flake/file", func(context *gin.Context) { + router.GET(api.ImageFile, func(context *gin.Context) { flake := context.Param("flake") image, data := instance.ImageData(instance.ImageSnowflakeHash(flake), false) if image.Snowflake == flake { - context.JSON(http.StatusNotFound, gin.H{ - "error": "not found", - }) + context.JSON(http.StatusNotFound, api.Error{Error: "not found"}) return } context.Data(http.StatusOK, "image/"+image.Type, data) }) - router.GET("/api/image/:flake/preview", func(context *gin.Context) { + router.GET(api.ImagePreview, func(context *gin.Context) { flake := context.Param("flake") image, data := instance.ImageData(instance.ImageSnowflakeHash(flake), true) if image.Snowflake == flake { - context.JSON(http.StatusNotFound, gin.H{ - "error": "not found", - }) + context.JSON(http.StatusNotFound, api.Error{Error: "not found"}) return } context.Data(http.StatusOK, "image/jpeg", data) }) - router.GET("/api/image/:flake/tag", func(context *gin.Context) { + router.GET(api.ImageTag, func(context *gin.Context) { context.JSON(http.StatusOK, instance.ImageTags(context.Param("flake"))) }) - router.PUT("/api/image/:flake/tag/:tag", func(context *gin.Context) { + router.PUT(api.ImageTagField, func(context *gin.Context) { info, ok := getUser(context) // Require sign in if !ok { - context.JSON(http.StatusForbidden, unauthorized) + context.JSON(http.StatusForbidden, api.Unauthorized) return } flake := context.Param("flake") if !info.Privileged && (info.Snowflake != instance.ImageSnowflake(flake).User) { - context.JSON(http.StatusForbidden, denied) + context.JSON(http.StatusForbidden, api.Denied) return } instance.ImageTagAdd(flake, context.Param("tag")) }) - router.DELETE("/api/image/:flake/tag/:tag", func(context *gin.Context) { + router.DELETE(api.ImageTagField, func(context *gin.Context) { info, ok := getUser(context) // Require sign in if !ok { - context.JSON(http.StatusForbidden, unauthorized) + context.JSON(http.StatusForbidden, api.Unauthorized) return } flake := context.Param("flake") if !info.Privileged && (info.Snowflake != instance.ImageSnowflake(flake).User) { - context.JSON(http.StatusForbidden, denied) + context.JSON(http.StatusForbidden, api.Denied) return } instance.ImageTagRemove(flake, context.Param("tag")) }) - router.GET("/api/tag", func(context *gin.Context) { + router.GET(api.Tag, func(context *gin.Context) { context.JSON(http.StatusOK, instance.Tags()) }) - router.GET("/api/tag/:tag", func(context *gin.Context) { + router.GET(api.TagField, func(context *gin.Context) { info, ok := getUser(context) // Require sign in if !ok { - context.JSON(http.StatusForbidden, unauthorized) + context.JSON(http.StatusForbidden, api.Unauthorized) return } if !info.Privileged { - context.JSON(http.StatusForbidden, denied) + context.JSON(http.StatusForbidden, api.Denied) return } context.JSON(http.StatusOK, instance.Tag(context.Param("tag"))) }) - router.PUT("/api/tag/:tag", func(context *gin.Context) { + router.PUT(api.TagField, func(context *gin.Context) { info, ok := getUser(context) // Require sign in if !ok { - context.JSON(http.StatusForbidden, unauthorized) + context.JSON(http.StatusForbidden, api.Unauthorized) return } if !info.Privileged { - context.JSON(http.StatusForbidden, denied) + context.JSON(http.StatusForbidden, api.Denied) return } context.JSON(http.StatusOK, instance.TagCreate(context.Param("tag"))) }) - router.DELETE("/api/tag/:tag", func(context *gin.Context) { + router.DELETE(api.TagField, func(context *gin.Context) { info, ok := getUser(context) // Require sign in if !ok { - context.JSON(http.StatusForbidden, unauthorized) + context.JSON(http.StatusForbidden, api.Unauthorized) return } if !info.Privileged { - context.JSON(http.StatusForbidden, denied) + context.JSON(http.StatusForbidden, api.Denied) return } instance.TagDestroy(context.Param("tag")) }) - router.GET("/api/tag/:tag/page", func(context *gin.Context) { + router.GET(api.TagPage, func(context *gin.Context) { tag := context.Param("tag") if !instance.MatchName(tag) { - context.JSON(http.StatusBadRequest, denied) + context.JSON(http.StatusBadRequest, api.Denied) return } context.String(http.StatusOK, strconv.Itoa(instance.PageTotal("tag_"+tag))) }) - router.GET("/api/tag/:tag/page/:entry", func(context *gin.Context) { + router.GET(api.TagPageField, func(context *gin.Context) { tag := context.Param("tag") if !instance.MatchName(tag) { - context.JSON(http.StatusBadRequest, denied) + context.JSON(http.StatusBadRequest, api.Denied) return } param := context.Param("entry") entry, err := strconv.Atoi(param) if err != nil { - context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) return } context.JSON(http.StatusOK, instance.Page("tag_"+tag, entry)) }) - router.GET("/api/tag/:tag/page/:entry/image", func(context *gin.Context) { + router.GET(api.TagPageImage, func(context *gin.Context) { tag := context.Param("tag") if !instance.MatchName(tag) { - context.JSON(http.StatusBadRequest, denied) + context.JSON(http.StatusBadRequest, api.Denied) return } param := context.Param("entry") entry, err := strconv.Atoi(param) if err != nil { - context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) return } context.JSON(http.StatusOK, instance.PageImages("tag_"+tag, entry)) }) - router.GET("/api/tag/:tag/info", func(context *gin.Context) { + router.GET(api.TagInfo, func(context *gin.Context) { context.JSON(http.StatusOK, instance.TagInfo(context.Param("tag"))) }) - router.PATCH("/api/tag/:tag/info", func(context *gin.Context) { + router.PATCH(api.TagInfo, func(context *gin.Context) { info, ok := getUser(context) // Require sign in if !ok { - context.JSON(http.StatusForbidden, unauthorized) + context.JSON(http.StatusForbidden, api.Unauthorized) return } if !info.Privileged { - context.JSON(http.StatusForbidden, denied) + context.JSON(http.StatusForbidden, api.Denied) return } - var payload TagUpdatePayload + var payload api.TagUpdatePayload if err := context.ShouldBindJSON(&payload); err != nil { - context.JSON(http.StatusBadRequest, gin.H{ - "error": err.Error(), - }) + context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) return } instance.TagType(context.Param("tag"), payload.Type) }) - router.GET("/api/page", func(context *gin.Context) { + router.GET(api.Page, func(context *gin.Context) { context.String(http.StatusOK, strconv.Itoa(instance.PageTotal(store.ImageRootPageVariant))) }) - router.GET("/api/page/:entry", func(context *gin.Context) { + router.GET(api.PageField, func(context *gin.Context) { param := context.Param("entry") entry, err := strconv.Atoi(param) if err != nil { - context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) return } context.JSON(http.StatusOK, instance.Page(store.ImageRootPageVariant, entry)) }) - router.GET("/api/page/:entry/image", func(context *gin.Context) { + router.GET(api.PageImage, func(context *gin.Context) { param := context.Param("entry") entry, err := strconv.Atoi(param) if err != nil { - context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) return } context.JSON(http.StatusOK, instance.PageImages(store.ImageRootPageVariant, entry)) diff --git a/api/errors.go b/api/errors.go new file mode 100644 index 0000000000000000000000000000000000000000..7da482d9f68730737d1da23455e351a7222cd904 --- /dev/null +++ b/api/errors.go @@ -0,0 +1,8 @@ +package api + +type Error struct { + Error string `json:"error"` +} + +var Unauthorized = Error{"not authorized"} +var Denied = Error{"permission denied"} diff --git a/api/paths.go b/api/paths.go new file mode 100644 index 0000000000000000000000000000000000000000..5738d9269a9e6ad08d59297372ee7510ed6a57b0 --- /dev/null +++ b/api/paths.go @@ -0,0 +1,32 @@ +package api + +const ( + Base = "/api" + SingleUser = Base + "/single_user" + Image = Base + "/image" + ImageField = Image + "/:flake" + ImageFile = ImageField + "/file" + ImagePreview = ImageField + "/preview" + ImageTag = ImageField + "/tag" + ImageTagField = ImageTag + "/:tag" + Tag = Base + "/tag" + TagField = Tag + "/:tag" + TagInfo = TagField + "/info" + TagPage = TagField + "/page" + TagPageField = TagPage + "/:entry" + TagPageImage = TagPageField + "/image" + Page = Base + "/page" + PageField = Page + "/:entry" + PageImage = PageField + "/image" + Search = Base + "/search" + SearchField = Search + "/:tags" + User = Base + "/user" + UserThis = User + "/this" + UserField = User + "/:flake" + UserSecret = UserField + "/secret" + UserImage = UserField + "/image" + UserPassword = UserField + "/password" + Username = Base + "/username" + UsernameField = Username + "/:name" + UsernameAuth = UsernameField + "/auth" +) diff --git a/api/types.go b/api/types.go new file mode 100644 index 0000000000000000000000000000000000000000..b0ea0155f2db158af24b7d540ac4ef939ed46a21 --- /dev/null +++ b/api/types.go @@ -0,0 +1,25 @@ +package api + +type UserPayload struct { + Username string `json:"username"` + ID string `json:"id"` + Privileged bool `json:"privileged"` +} + +type UserCreatePayload struct { + Username string `json:"username"` + Password string `json:"password"` + Privileged bool `json:"privileged"` +} + +type UserUpdatePayload struct { + Username string `json:"username"` +} + +type TagUpdatePayload struct { + Type string `json:"type"` +} + +type ImageUpdatePayload struct { + Source string `json:"source"` +} diff --git a/store/image.go b/store/image.go index 323fa45865aaeb884c115c25b1ede8821e06cdaa..771efb83e2873950549f393d24c7ddb7033e95bc 100644 --- a/store/image.go +++ b/store/image.go @@ -27,7 +27,7 @@ type Image struct { CommentaryTranslation string `json:"commentary_translation"` } -// Images returns a list of images. +// Images returns a slice of image hashes. func (s *Store) Images() []string { var images []string if entries, err := os.ReadDir(s.ImagesDir()); err != nil { @@ -305,7 +305,7 @@ func (s *Store) ImageSource(hash, source string) { log.Infof("Image hash %s source set to %s.", hash, source) } -// ImageSnowflakes returns all snowflakes. +// ImageSnowflakes returns a slice of image snowflakes. func (s *Store) ImageSnowflakes() []string { var snowflakes []string if entries, err := os.ReadDir(s.ImagesSnowflakeDir()); err != nil {