diff --git a/.gitignore b/.gitignore index 9a8df94033f7efc395b2e5a3e561c8e5ce15a4c6..e68131e02cb407ad962e0c1ba819df9c775a56f9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,11 +2,12 @@ *.dylib *.test *.out +*.core +*.swp /.idea/ /image-board -/server.toml +/server.conf /db -/logs client.js* -client.min.js* \ No newline at end of file +client.min.js* diff --git a/api.go b/api.go index 178c3f06004a8d120029552b385aad619309f921..00970d73e82acda78684d1e675a7e1c73a36064f 100644 --- a/api.go +++ b/api.go @@ -1,648 +1,18 @@ package main -import ( - "github.com/gin-gonic/gin" - 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" - "unicode/utf8" -) +import "random.chars.jp/git/image-board/v2/api" func registerAPI() { - router.GET(api.Base, func(context *gin.Context) { - context.JSON(http.StatusOK, store.Info{ - Revision: instance.Revision, - Compat: instance.Compat, - Register: instance.Register, - InitialUser: instance.InitialUser, - PermissionDir: instance.PermissionDir, - PermissionFile: instance.PermissionFile, - }) - }) - - router.GET(api.SingleUser, func(context *gin.Context) { - context.JSON(http.StatusOK, instance.SingleUser) - }) - - router.GET(api.Private, func(context *gin.Context) { - context.JSON(http.StatusOK, instance.Private) - }) - - router.GET(api.User, func(context *gin.Context) { - if !privateAccessible(context) { - return - } - - context.JSON(http.StatusOK, instance.Users()) - }) - - router.PUT(api.User, func(context *gin.Context) { - if !privateAccessible(context) { - return - } - - user, ok := getUser(context) - if !instance.Register { - if !ok { - context.JSON(http.StatusForbidden, api.Error{Error: "user registration disallowed"}) - return - } - - if !user.Privileged { - context.JSON(http.StatusForbidden, api.Denied) - return - } - } - - var payload api.UserCreatePayload - if err := context.ShouldBindJSON(&payload); err != nil { - context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) - return - } - if !user.Privileged && payload.Privileged { - context.JSON(http.StatusForbidden, api.Denied) - return - } - - context.JSON(http.StatusOK, instance.UserAdd(payload.Username, payload.Password, payload.Privileged)) - }) - - router.GET(api.UserThis, func(context *gin.Context) { - info, ok := getUser(context) - if !ok { - context.JSON(http.StatusForbidden, api.Unauthorized) - return - } - context.JSON(http.StatusOK, api.UserPayload{ - Username: info.Username, - ID: info.Snowflake, - Privileged: info.Privileged, - }) - }) - - router.GET(api.UserField, func(context *gin.Context) { - if !privateAccessible(context) { - return - } - - info := instance.User(context.Param("flake")) - context.JSON(http.StatusOK, api.UserPayload{ - Username: info.Username, - ID: info.Snowflake, - Privileged: info.Privileged, - }) - }) - - router.PATCH(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 - } - - var payload api.UserUpdatePayload - if err := context.ShouldBindJSON(&payload); err != nil { - context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) - return - } - - if info.Privileged { - instance.UserPrivileged(flake, payload.Privileged) - } - instance.UserUsernameUpdate(flake, payload.Username) - }) - - 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, api.Unauthorized) - return - } - - flake := context.Param("flake") - instance.UserPasswordUpdate(flake, "") - instance.UserSecretRegen(flake) - }) - - router.PUT(api.UserPassword, 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) - } - - var newPass string - if payload, err := context.GetRawData(); err != nil { - context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) - return - } else { - if !utf8.Valid(payload) { - context.JSON(http.StatusBadRequest, api.Error{Error: "invalid encoding"}) - return - } - newPass = string(payload) - if len(newPass) > 8192 || strings.Contains(newPass, "\n") { - context.JSON(http.StatusBadRequest, api.Error{Error: "invalid password"}) - return - } - } - - if newPass == "" { - context.JSON(http.StatusBadRequest, api.Error{Error: "empty passwords are not allowed"}) - return - } - - instance.UserPasswordUpdate(info.Snowflake, newPass) - context.JSON(http.StatusOK, api.UserSecretPayload{Secret: instance.UserSecretRegen(info.Snowflake)}) - }) - - router.GET(api.UsernameField, func(context *gin.Context) { - if !privateAccessible(context) { - return - } - - info := instance.UserUsername(context.Param("name")) - payload := api.UserPayload{ - Username: info.Username, - ID: info.Snowflake, - Privileged: info.Privileged, - } - context.JSON(http.StatusOK, payload) - }) - - router.POST(api.UsernameAuth, func(context *gin.Context) { - var password string - - if payload, err := context.GetRawData(); err != nil { - context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) - return - } else { - password = string(payload) - } - - username := context.Param("name") - if instance.UserUsernamePasswordValidate(username, password) { - context.JSON(http.StatusOK, api.UserSecretPayload{Secret: instance.UserUsername(username).Secret}) - } else { - context.JSON(http.StatusForbidden, api.Denied) - } - }) - - router.GET(api.UserSecret, func(context *gin.Context) { - info, ok := getUser(context) - - // Require sign in - if !ok { - context.JSON(http.StatusForbidden, api.Unauthorized) - return - } - - // Only allow lookup if user is current user or privileged - flake := context.Param("flake") - if !info.Privileged && (info.Snowflake != flake) { - context.JSON(http.StatusForbidden, api.Denied) - return - } - context.JSON(http.StatusOK, api.UserSecretPayload{Secret: instance.User(flake).Secret}) - }) - - router.PUT(api.UserSecret, func(context *gin.Context) { - info, ok := getUser(context) - - // Require sign in - if !ok { - context.JSON(http.StatusForbidden, api.Unauthorized) - return - } - - // Only allow set if user is current user or privileged - flake := context.Param("flake") - if !info.Privileged && (info.Snowflake != flake) { - context.JSON(http.StatusForbidden, api.Denied) - return - } - context.JSON(http.StatusOK, api.UserSecretPayload{Secret: instance.UserSecretRegen(flake)}) - }) - - router.GET(api.UserImage, func(context *gin.Context) { - if !privateAccessible(context) { - return - } - - context.JSON(http.StatusOK, instance.UserImages(context.Param("flake"))) - }) - - router.GET(api.SearchField, func(context *gin.Context) { - if !privateAccessible(context) { - return - } - - tagsPayload := context.Param("tags") - tags := strings.Split(tagsPayload, "!") - context.JSON(http.StatusOK, instance.ImageSearch(tags)) - }) - - router.GET(api.Image, func(context *gin.Context) { - info, ok := getUser(context) - - // Require privileged - if !ok || !info.Privileged { - context.JSON(http.StatusForbidden, api.Unauthorized) - return - } - - context.JSON(http.StatusOK, instance.ImageSnowflakes()) - }) - - router.POST(api.Image, func(context *gin.Context) { - info, ok := getUser(context) - - // Require sign in - if !ok { - context.JSON(http.StatusForbidden, api.Unauthorized) - return - } - - payload, err := context.FormFile("image") - if err != nil { - context.JSON(http.StatusInternalServerError, api.Error{Error: err.Error()}) - return - } - file, err := payload.Open() - if err != nil { - log.Errorf("Error while opening uploaded file %s, %s", payload.Filename, err) - context.JSON(http.StatusInternalServerError, api.Error{Error: err.Error()}) - return - } - data, err := ioutil.ReadAll(file) - if err != nil { - log.Errorf("Error while reading uploaded file %s, %s", payload.Filename, err) - context.JSON(http.StatusInternalServerError, api.Error{Error: err.Error()}) - return - } - image := instance.ImageAdd(data, info.Snowflake) - if image.Hash == "" { - context.JSON(http.StatusBadRequest, api.Error{Error: "invalid image"}) - return - } - context.JSON(http.StatusOK, image) - }) - - router.GET(api.ImagePage, func(context *gin.Context) { - if !privateAccessible(context) { - return - } - - context.String(http.StatusOK, strconv.Itoa(instance.PageTotal(store.ImageRootPageVariant))) - }) - - router.GET(api.ImagePageField, func(context *gin.Context) { - if !privateAccessible(context) { - return - } - - param := context.Param("entry") - entry, err := strconv.Atoi(param) - if err != nil { - context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) - return - } - context.JSON(http.StatusOK, instance.Page(store.ImageRootPageVariant, entry)) - }) - - router.GET(api.ImagePageImage, func(context *gin.Context) { - if !privateAccessible(context) { - return - } - - param := context.Param("entry") - entry, err := strconv.Atoi(param) - if err != nil { - context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) - return - } - context.JSON(http.StatusOK, instance.PageImages(store.ImageRootPageVariant, entry)) - }) - - router.GET(api.ImageField, func(context *gin.Context) { - if !privateAccessible(context) { - return - } - - context.JSON(http.StatusOK, instance.ImageSnowflake(context.Param("flake"))) - }) - - router.PATCH(api.ImageField, func(context *gin.Context) { - user, ok := getUser(context) - - // Require sign in - if !ok { - context.JSON(http.StatusForbidden, api.Unauthorized) - return - } - - info := instance.ImageSnowflake(context.Param("flake")) - if !user.Privileged && (info.User != user.Snowflake) { - context.JSON(http.StatusForbidden, api.Denied) - return - } - - var payload api.ImageUpdatePayload - if err := context.ShouldBindJSON(&payload); err != nil { - context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) - return - } - - instance.ImageUpdate(info.Hash, payload.Source, payload.Parent, payload.Commentary, payload.CommentaryTranslation) - }) - - router.DELETE(api.ImageField, func(context *gin.Context) { - info, ok := getUser(context) - - // Require sign in - if !ok { - 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, api.Denied) - return - } - instance.ImageDestroy(image.Hash) - }) - - router.GET(api.ImageFile, func(context *gin.Context) { - if !privateAccessible(context) { - return - } - - flake := context.Param("flake") - image, data := instance.ImageData(instance.ImageSnowflakeHash(flake), false) - if image.Snowflake != flake { - context.JSON(http.StatusNotFound, api.Error{Error: "not found"}) - return - } - context.Data(http.StatusOK, "image/"+image.Type, data) - }) - - router.GET(api.ImagePreview, func(context *gin.Context) { - if !privateAccessible(context) { - return - } - - flake := context.Param("flake") - image, data := instance.ImageData(instance.ImageSnowflakeHash(flake), true) - if image.Snowflake != flake { - context.JSON(http.StatusNotFound, api.Error{Error: "not found"}) - return - } - context.Data(http.StatusOK, "image/jpeg", data) - }) - - router.GET(api.ImageTag, func(context *gin.Context) { - if !privateAccessible(context) { - return - } - - context.JSON(http.StatusOK, instance.ImageTags(context.Param("flake"))) - }) - - router.PUT(api.ImageTagField, 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 != instance.ImageSnowflake(flake).User) { - context.JSON(http.StatusForbidden, api.Denied) - return - } - instance.ImageTagAdd(flake, context.Param("tag")) - }) - - router.DELETE(api.ImageTagField, 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 != instance.ImageSnowflake(flake).User) { - context.JSON(http.StatusForbidden, api.Denied) - return - } - instance.ImageTagRemove(flake, context.Param("tag")) - }) - - router.GET(api.Tag, func(context *gin.Context) { - if !privateAccessible(context) { - return - } - - context.JSON(http.StatusOK, instance.Tags()) - }) - - router.GET(api.TagField, func(context *gin.Context) { - info, ok := getUser(context) - - // Require sign in - if !ok { - context.JSON(http.StatusForbidden, api.Unauthorized) - return - } - - if !info.Privileged { - context.JSON(http.StatusForbidden, api.Denied) - return - } - context.JSON(http.StatusOK, instance.Tag(context.Param("tag"))) - }) - - router.PUT(api.TagField, func(context *gin.Context) { - info, ok := getUser(context) - - // Require sign in - if !ok { - context.JSON(http.StatusForbidden, api.Unauthorized) - return - } - - if !info.Privileged { - context.JSON(http.StatusForbidden, api.Denied) - return - } - context.JSON(http.StatusOK, instance.TagCreate(context.Param("tag"))) - }) - - router.DELETE(api.TagField, func(context *gin.Context) { - info, ok := getUser(context) - - // Require sign in - if !ok { - context.JSON(http.StatusForbidden, api.Unauthorized) - return - } - - if !info.Privileged { - context.JSON(http.StatusForbidden, api.Denied) - return - } - instance.TagDestroy(context.Param("tag")) - }) - - router.GET(api.TagPage, func(context *gin.Context) { - if !privateAccessible(context) { - return - } - - tag := context.Param("tag") - if !instance.MatchName(tag) { - context.JSON(http.StatusBadRequest, api.Denied) - return - } - context.String(http.StatusOK, strconv.Itoa(instance.PageTotal("tag_"+tag))) - }) - - router.GET(api.TagPageField, func(context *gin.Context) { - if !privateAccessible(context) { - return - } - - tag := context.Param("tag") - if !instance.MatchName(tag) { - context.JSON(http.StatusBadRequest, api.Denied) - return - } - - param := context.Param("entry") - entry, err := strconv.Atoi(param) - if err != nil { - context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) - return - } - context.JSON(http.StatusOK, instance.Page("tag_"+tag, entry)) - }) - - router.GET(api.TagPageImage, func(context *gin.Context) { - if !privateAccessible(context) { - return - } - - tag := context.Param("tag") - if !instance.MatchName(tag) { - context.JSON(http.StatusBadRequest, api.Denied) - return - } - - param := context.Param("entry") - entry, err := strconv.Atoi(param) - if err != nil { - context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) - return - } - context.JSON(http.StatusOK, instance.PageImages("tag_"+tag, entry)) - }) - - router.GET(api.TagInfo, func(context *gin.Context) { - if !privateAccessible(context) { - return - } - - context.JSON(http.StatusOK, instance.TagInfo(context.Param("tag"))) - }) - - router.PATCH(api.TagInfo, func(context *gin.Context) { - info, ok := getUser(context) - - // Require sign in - if !ok { - context.JSON(http.StatusForbidden, api.Unauthorized) - return - } - - if !info.Privileged { - context.JSON(http.StatusForbidden, api.Denied) - return - } - - var payload api.TagUpdatePayload - if err := context.ShouldBindJSON(&payload); err != nil { - context.JSON(http.StatusBadRequest, api.Error{Error: err.Error()}) - return - } - instance.TagType(context.Param("tag"), payload.Type) - }) -} - -func privateAccessible(context *gin.Context) bool { - if !instance.Private { - return true + s := &api.Server{ + Instance: instance, + Engine: router, + Config: &api.ServerConfig{ + SingleUser: config.System.SingleUser, + Private: config.System.Private, + Register: config.System.Register, + }, } - if _, ok := getUser(context); !ok { - context.JSON(http.StatusForbidden, api.Denied) - return false - } - - return true -} - -func getUser(context *gin.Context) (store.User, bool) { - if instance.SingleUser { - return instance.User(instance.InitialUser), true - } - secret := context.GetHeader("secret") - info := instance.SecretLookup(secret) - if info.Secret != secret || info.Snowflake == "" { - return store.User{}, false - } - return info, true + s.V1() + s.V2() } diff --git a/api/errors.go b/api/errors.go index 7da482d9f68730737d1da23455e351a7222cd904..5d786d516c75e8e9296dd75f40f8de38710ada54 100644 --- a/api/errors.go +++ b/api/errors.go @@ -1,8 +1,41 @@ package api +import ( + "github.com/gin-gonic/gin" + "net/http" + "random.chars.jp/git/image-board/v2/store" +) + type Error struct { Error string `json:"error"` } -var Unauthorized = Error{"not authorized"} -var Denied = Error{"permission denied"} +var ( + ErrUnauthorized = Error{"not authorized"} + ErrPermissionDenied = Error{"permission denied"} + ErrRegistrationDisabled = Error{Error: "user creation disallowed"} +) + +func doError(context *gin.Context, err error) bool { + switch err { + case nil: + return false + case store.ErrNoEntry: + context.JSON(http.StatusNotFound, Error{Error: err.Error()}) + case store.ErrInvalidInput: + context.JSON(http.StatusBadRequest, Error{Error: err.Error()}) + default: + context.JSON(http.StatusInternalServerError, Error{Error: err.Error()}) + } + return true +} + +func doErrorAPI(context *gin.Context, err error) bool { + switch err { + case nil: + return false + default: + context.JSON(http.StatusBadRequest, Error{Error: err.Error()}) + } + return true +} diff --git a/api/f.go b/api/f.go new file mode 100644 index 0000000000000000000000000000000000000000..987053e332353712e7eec3007f35927b2963a0f3 --- /dev/null +++ b/api/f.go @@ -0,0 +1,81 @@ +package api + +import ( + "github.com/gin-gonic/gin" + "net/http" + "random.chars.jp/git/image-board/v2/store" +) + +type Server struct { + Instance store.Store + Engine *gin.Engine + Config *ServerConfig +} + +var initialUser *store.User + +func (s *Server) privateAccessible(context *gin.Context) bool { + if !s.Config.Private { + return true + } + + return s.Instance.SecretValidate(context.GetHeader("secret")) +} + +func (s *Server) getUser(context *gin.Context) (*store.User, error) { + if s.Config.SingleUser { + if initialUser != nil { + return initialUser, nil + } + if user, err := s.Instance.User(s.Instance.UserInitial()); err != nil { + return nil, err + } else { + initialUser = user + return user, nil + } + } + + if info, err := s.Instance.SecretLookup(context.GetHeader("secret")); err != nil { + return nil, err + } else { + return info, nil + } +} + +func (s *Server) mustGetUser(context *gin.Context) *store.User { + if user, err := s.getUser(context); err == store.ErrNoEntry { + context.JSON(http.StatusUnauthorized, ErrUnauthorized) + return nil + } else if doError(context, err) { + return nil + } else { + return user + } +} + +func (s *Server) json(context *gin.Context) func(obj interface{}, err error) bool { + return func(obj interface{}, err error) bool { + if doError(context, err) { + return false + } + context.JSON(http.StatusOK, obj) + return true + } +} + +func (s *Server) pageImages(variant string, entry uint64) ([]*store.Image, error) { + if page, err := s.Instance.Page(variant, entry); err != nil { + return nil, err + } else { + images := make([]*store.Image, len(page)) + for i, flake := range page { + var image *store.Image + if image, err = s.Instance.Image(flake); err != nil { + return nil, err + } else { + images[i] = image + } + } + return images, nil + } +} diff --git a/api/paths.go b/api/paths.go index 4caec4a1bde50822b08269294f7c4ec1d9ae88e7..dc3bc0ea2297ca4756c8d03ca8a0023c1964e149 100644 --- a/api/paths.go +++ b/api/paths.go @@ -1,33 +1,7 @@ package api const ( - Base = "/api" - SingleUser = Base + "/single_user" - Private = Base + "/private" - Image = Base + "/image" - ImagePage = Image + "/page" - ImagePageField = ImagePage + "/:entry" - ImagePageImage = ImagePageField + "/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" - 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" + API = "/api" + Base = API + "/v2" + Config = Base + "/config" ) diff --git a/api/types.go b/api/types.go index a86cfb73bd4725049b4e56faa8ed9b5b6279dbad..c20d6136e7cd121e71cb403ffc1b43295bcc8ae7 100644 --- a/api/types.go +++ b/api/types.go @@ -1,11 +1,32 @@ package api +import "random.chars.jp/git/image-board/v2/store" + +type BackendInfo struct { + Backend string `json:"backend"` + InitialUser string `json:"initial_user"` +} + +type ServerConfig struct { + SingleUser bool `json:"single_user"` + Private bool `json:"private"` + Register bool `json:"register"` +} + type UserPayload struct { Username string `json:"username"` ID string `json:"id"` Privileged bool `json:"privileged"` } +func userPayload(user *store.User) *UserPayload { + return &UserPayload{ + Username: user.Username, + ID: user.Snowflake, + Privileged: user.Privileged, + } +} + type UserCreatePayload struct { Username string `json:"username"` Password string `json:"password"` diff --git a/api/v1.go b/api/v1.go new file mode 100644 index 0000000000000000000000000000000000000000..cd57f4cadbd1eb88c6b296f18082a861799da8bf --- /dev/null +++ b/api/v1.go @@ -0,0 +1,668 @@ +package api + +import ( + "github.com/gin-gonic/gin" + "io" + "mime/multipart" + "net/http" + "random.chars.jp/git/image-board/v2/backend/filesystem" + "random.chars.jp/git/image-board/v2/store" + "runtime" + "strconv" + "strings" + "unicode/utf8" +) + +const ( + v1Base = API + "/v1" + v1SingleUser = v1Base + "/single_user" + v1Private = v1Base + "/private" + v1Register = v1Base + "/register" + v1Image = v1Base + "/image" + v1ImagePage = v1Image + "/page" + v1ImagePageField = v1ImagePage + "/:entry" + v1ImagePageImage = v1ImagePageField + "/image" + v1ImageField = v1Image + "/:flake" + v1ImageFile = v1ImageField + "/file" + v1ImagePreview = v1ImageField + "/preview" + v1ImageTag = v1ImageField + "/tag" + v1ImageTagField = v1ImageTag + "/:tag" + v1Tag = v1Base + "/tag" + v1TagField = v1Tag + "/:tag" + v1TagInfo = v1TagField + "/info" + v1TagPage = v1TagField + "/page" + v1TagPageField = v1TagPage + "/:entry" + v1TagPageImage = v1TagPageField + "/image" + v1Search = v1Base + "/search" + v1SearchField = v1Search + "/:tags" + v1User = v1Base + "/user" + v1UserThis = v1User + "/this" + v1UserField = v1User + "/:flake" + v1UserSecret = v1UserField + "/secret" + v1UserImage = v1UserField + "/image" + v1UserPassword = v1UserField + "/password" + v1Username = v1Base + "/username" + v1UsernameField = v1Username + "/:name" + v1UsernameAuth = v1UsernameField + "/auth" +) + +func (s *Server) V1() { + s.Engine.GET(v1Base, func(context *gin.Context) { + context.JSON(http.StatusOK, filesystem.Store{ + // this is always 1 for backwards compatibility + Revision: 1, + // determined at runtime + Compat: runtime.GOOS == "windows", + // proper initial user + InitialUser: s.Instance.UserInitial(), + // default values + PermissionDir: 0700, + PermissionFile: 0600, + }) + }) + + s.Engine.GET(v1SingleUser, func(context *gin.Context) { + context.JSON(http.StatusOK, s.Config.SingleUser) + }) + + s.Engine.GET(v1Private, func(context *gin.Context) { + context.JSON(http.StatusOK, s.Config.Private) + }) + + s.Engine.GET(v1Register, func(context *gin.Context) { + context.JSON(http.StatusOK, s.Config.Register) + }) + + s.Engine.GET(v1User, func(context *gin.Context) { + if !s.privateAccessible(context) { + return + } + + s.json(context)(s.Instance.Users()) + }) + + s.Engine.PUT(v1User, func(context *gin.Context) { + if !s.privateAccessible(context) { + return + } + + privileged := false + if user, err := s.getUser(context); err == nil && user.Privileged { + privileged = true + } else if err != store.ErrNoEntry { + doError(context, err) + return + } + + if !s.Config.Register { + if !privileged { + context.JSON(http.StatusForbidden, ErrRegistrationDisabled) + return + } + } + + var payload UserCreatePayload + if err := context.ShouldBindJSON(&payload); doErrorAPI(context, err) { + return + } + + if !privileged && payload.Privileged { + context.JSON(http.StatusForbidden, ErrPermissionDenied) + return + } + + s.json(context)(s.Instance.UserAdd(payload.Username, payload.Password, payload.Privileged)) + }) + + s.Engine.GET(v1UserThis, func(context *gin.Context) { + user := s.mustGetUser(context) + if user == nil { + return + } + + context.JSON(http.StatusOK, userPayload(user)) + }) + + s.Engine.GET(v1UserField, func(context *gin.Context) { + if !s.privateAccessible(context) { + return + } + + s.json(context)(func() (interface{}, error) { + user, err := s.Instance.User(context.Param("flake")) + if err == nil { + return userPayload(user), nil + } + return nil, err + }()) + }) + + s.Engine.PATCH(v1UserField, func(context *gin.Context) { + user := s.mustGetUser(context) + if user == nil { + return + } + + flake := context.Param("flake") + if !user.Privileged && (user.Snowflake != flake) { + context.JSON(http.StatusForbidden, ErrUnauthorized) + return + } + + var payload UserUpdatePayload + if err := context.ShouldBindJSON(&payload); doErrorAPI(context, err) { + return + } + + if user.Privileged { + if err := s.Instance.UserPrivileged(flake, payload.Privileged); doError(context, err) { + return + } + } + if err := s.Instance.UserUsernameUpdate(flake, payload.Username); doError(context, err) { + return + } + }) + + s.Engine.DELETE(v1UserField, func(context *gin.Context) { + user := s.mustGetUser(context) + if user == nil { + return + } + + flake := context.Param("flake") + if !user.Privileged && (user.Snowflake != flake) { + context.JSON(http.StatusForbidden, ErrUnauthorized) + return + } + + doError(context, s.Instance.UserDestroy(flake)) + }) + + s.Engine.DELETE(v1UserPassword, func(context *gin.Context) { + user := s.mustGetUser(context) + if user == nil { + return + } else if !user.Privileged { + context.JSON(http.StatusForbidden, ErrUnauthorized) + return + } + + flake := context.Param("flake") + if doError(context, s.Instance.UserPasswordUpdate(flake, "")) { + return + } else { + _, err := s.Instance.UserSecretRegen(flake) + doError(context, err) + } + }) + + s.Engine.PUT(v1UserPassword, func(context *gin.Context) { + user := s.mustGetUser(context) + if user == nil { + return + } + + flake := context.Param("flake") + if !user.Privileged && (user.Snowflake != flake) { + context.JSON(http.StatusForbidden, ErrUnauthorized) + } + + var newPass string + if payload, err := context.GetRawData(); doErrorAPI(context, err) { + return + } else { + if !utf8.Valid(payload) { + context.JSON(http.StatusBadRequest, Error{Error: "invalid encoding"}) + return + } + newPass = string(payload) + if len(newPass) > 8192 || strings.Contains(newPass, "\n") { + context.JSON(http.StatusBadRequest, Error{Error: "invalid password"}) + return + } + } + + if newPass == "" { + context.JSON(http.StatusBadRequest, Error{Error: "empty password not allowed"}) + return + } + + if doError(context, s.Instance.UserPasswordUpdate(flake, newPass)) { + return + } + + s.json(context)(func() (interface{}, error) { + secret, err := s.Instance.UserSecretRegen(flake) + return UserSecretPayload{Secret: secret}, err + }()) + }) + + s.Engine.GET(v1UsernameField, func(context *gin.Context) { + if !s.privateAccessible(context) { + return + } + + s.json(context)(func() (interface{}, error) { + user, err := s.Instance.UserUsername(context.Param("name")) + return userPayload(user), err + }()) + }) + + s.Engine.POST(v1UsernameAuth, func(context *gin.Context) { + var password string + + if payload, err := context.GetRawData(); doErrorAPI(context, err) { + return + } else { + password = string(payload) + } + + username := context.Param("name") + if valid, err := s.Instance.UserPasswordValidate(nil, &username, password); doError(context, err) { + return + } else if valid { + s.json(context)(func() (interface{}, error) { + var user *store.User + user, err = s.Instance.UserUsername(username) + if err != nil { + return nil, err + } + return UserSecretPayload{Secret: user.Secret}, nil + }()) + } else { + context.JSON(http.StatusForbidden, ErrUnauthorized) + } + }) + + s.Engine.GET(v1UserSecret, func(context *gin.Context) { + user := s.mustGetUser(context) + if user == nil { + return + } + + flake := context.Param("flake") + if !user.Privileged && (user.Snowflake != flake) { + context.JSON(http.StatusForbidden, ErrUnauthorized) + return + } + + s.json(context)(func() (interface{}, error) { + var err error + user, err = s.Instance.User(flake) + if err != nil { + return nil, err + } + return UserSecretPayload{Secret: user.Secret}, nil + }()) + }) + + s.Engine.PUT(v1UserSecret, func(context *gin.Context) { + user := s.mustGetUser(context) + if user == nil { + return + } + + flake := context.Param("flake") + if !user.Privileged && (user.Snowflake != flake) { + context.JSON(http.StatusForbidden, ErrUnauthorized) + return + } + + s.json(context)(func() (interface{}, error) { + secret, err := s.Instance.UserSecretRegen(flake) + return UserSecretPayload{Secret: secret}, err + }()) + }) + + s.Engine.GET(v1UserImage, func(context *gin.Context) { + if !s.privateAccessible(context) { + return + } + + s.json(context)(s.Instance.UserImages(context.Param("flake"))) + }) + + s.Engine.GET(v1SearchField, func(context *gin.Context) { + if !s.privateAccessible(context) { + return + } + + s.json(context)(s.Instance.ImageSearch(strings.Split(context.Param("tags"), "!"))) + }) + + s.Engine.GET(v1Image, func(context *gin.Context) { + user := s.mustGetUser(context) + if user == nil { + return + } else if !user.Privileged { + context.JSON(http.StatusForbidden, ErrUnauthorized) + return + } + + s.json(context)(s.Instance.Images()) + }) + + s.Engine.POST(v1Image, func(context *gin.Context) { + user := s.mustGetUser(context) + if user == nil { + return + } + + if payload, err := context.FormFile("image"); doErrorAPI(context, err) { + return + } else { + var file multipart.File + if file, err = payload.Open(); doErrorAPI(context, err) { + return + } else { + var data []byte + if data, err = io.ReadAll(file); doErrorAPI(context, err) { + return + } else { + s.json(context)(s.Instance.ImageAdd(data, user.Snowflake)) + } + } + } + }) + + s.Engine.GET(v1ImagePage, func(context *gin.Context) { + if !s.privateAccessible(context) { + return + } + + s.json(context)(s.Instance.PageTotal(store.ImageRootPageVariant)) + }) + + s.Engine.GET(v1ImagePageField, func(context *gin.Context) { + if !s.privateAccessible(context) { + return + } + + param := context.Param("entry") + if entry, err := strconv.Atoi(param); doErrorAPI(context, err) { + return + } else { + s.json(context)(s.Instance.Page(store.ImageRootPageVariant, uint64(entry))) + } + }) + + s.Engine.GET(v1ImagePageImage, func(context *gin.Context) { + if !s.privateAccessible(context) { + return + } + + param := context.Param("entry") + if entry, err := strconv.Atoi(param); doErrorAPI(context, err) { + return + } else { + s.json(context)(s.pageImages(store.ImageRootPageVariant, uint64(entry))) + } + }) + + s.Engine.GET(v1ImageField, func(context *gin.Context) { + if !s.privateAccessible(context) { + return + } + + s.json(context)(s.Instance.Image(context.Param("flake"))) + }) + + s.Engine.PATCH(v1ImageField, func(context *gin.Context) { + user := s.mustGetUser(context) + if user == nil { + return + } + + var image *store.Image + if i, err := s.Instance.Image(context.Param("flake")); doError(context, err) { + return + } else { + image = i + } + + if !user.Privileged && (image.User != user.Snowflake) { + context.JSON(http.StatusForbidden, ErrUnauthorized) + return + } + + var payload ImageUpdatePayload + if err := context.ShouldBindJSON(&payload); doErrorAPI(context, err) { + return + } + + doError(context, + s.Instance.ImageUpdate(image.Snowflake, + payload.Source, payload.Parent, payload.Commentary, payload.CommentaryTranslation)) + }) + + s.Engine.DELETE(v1ImageField, func(context *gin.Context) { + user := s.mustGetUser(context) + if user == nil { + return + } + + var image *store.Image + if i, err := s.Instance.Image(context.Param("flake")); doError(context, err) { + return + } else { + image = i + } + + if !user.Privileged && (user.Snowflake != image.User) { + context.JSON(http.StatusForbidden, ErrUnauthorized) + return + } + doError(context, s.Instance.ImageDestroy(image.Snowflake)) + }) + + s.Engine.GET(v1ImageFile, func(context *gin.Context) { + if !s.privateAccessible(context) { + return + } + + var ( + image *store.Image + data []byte + ) + if h, err := s.Instance.ImageSnowflakeHash(context.Param("flake")); doError(context, err) { + return + } else { + if image, data, err = s.Instance.ImageData(h, false); err != nil { + if err == store.ErrNoEntry { + // simulate behaviour of old api + context.JSON(http.StatusNotFound, Error{Error: "not found"}) + return + } + doError(context, err) + return + } + } + + context.Data(http.StatusOK, "image/"+image.Type, data) + }) + + s.Engine.GET(v1ImagePreview, func(context *gin.Context) { + if !s.privateAccessible(context) { + return + } + + var data []byte + if h, err := s.Instance.ImageSnowflakeHash(context.Param("flake")); doError(context, err) { + return + } else { + if _, data, err = s.Instance.ImageData(h, true); err != nil { + if err == store.ErrNoEntry { + // simulate behaviour of old api + context.JSON(http.StatusNotFound, Error{Error: "not found"}) + return + } + doError(context, err) + return + } + } + + context.Data(http.StatusOK, "image/jpeg", data) + }) + + s.Engine.GET(v1ImageTag, func(context *gin.Context) { + if !s.privateAccessible(context) { + return + } + + s.json(context)(s.Instance.ImageTags(context.Param("flake"))) + }) + + s.Engine.PUT(v1ImageTagField, func(context *gin.Context) { + user := s.mustGetUser(context) + if user == nil { + return + } + + flake := context.Param("flake") + if valid, err := s.Instance.UserImage(user.Snowflake, flake); doError(context, err) { + return + } else if !valid { + context.JSON(http.StatusForbidden, ErrUnauthorized) + return + } + + doError(context, s.Instance.ImageTagAdd(flake, context.Param("tag"))) + }) + + s.Engine.DELETE(v1ImageTagField, func(context *gin.Context) { + user := s.mustGetUser(context) + if user == nil { + return + } + + flake := context.Param("flake") + if valid, err := s.Instance.UserImage(user.Snowflake, flake); doError(context, err) { + return + } else if !valid { + context.JSON(http.StatusForbidden, ErrUnauthorized) + return + } + + doError(context, s.Instance.ImageTagRemove(flake, context.Param("tag"))) + }) + + s.Engine.GET(v1Tag, func(context *gin.Context) { + if !s.privateAccessible(context) { + return + } + + s.json(context)(s.Instance.Tags()) + }) + + s.Engine.GET(v1TagField, func(context *gin.Context) { + user := s.mustGetUser(context) + if user == nil { + return + } else if !user.Privileged { + context.JSON(http.StatusForbidden, ErrUnauthorized) + return + } + + s.json(context)(s.Instance.TagImages(context.Param("tag"))) + }) + + s.Engine.PUT(v1TagField, func(context *gin.Context) { + user := s.mustGetUser(context) + if user == nil { + return + } else if !user.Privileged { + context.JSON(http.StatusForbidden, ErrUnauthorized) + return + } + + doError(context, s.Instance.TagAdd(context.Param("tag"))) + }) + + s.Engine.DELETE(v1TagField, func(context *gin.Context) { + user := s.mustGetUser(context) + if user == nil { + return + } else if !user.Privileged { + context.JSON(http.StatusForbidden, ErrUnauthorized) + return + } + + doError(context, s.Instance.TagDestroy(context.Param("tag"))) + }) + + s.Engine.GET(v1TagPage, func(context *gin.Context) { + if !s.privateAccessible(context) { + return + } + + tag := context.Param("tag") + if !store.MatchName(tag) { + context.JSON(http.StatusBadRequest, Error{Error: store.ErrInvalidInput.Error()}) + return + } + s.json(context)(s.Instance.PageTotal("tag_" + tag)) + }) + + s.Engine.GET(v1TagPageField, func(context *gin.Context) { + if !s.privateAccessible(context) { + return + } + + tag := context.Param("tag") + if !store.MatchName(tag) { + context.JSON(http.StatusBadRequest, Error{Error: store.ErrInvalidInput.Error()}) + return + } + + param := context.Param("entry") + if entry, err := strconv.Atoi(param); doErrorAPI(context, err) { + return + } else { + s.json(context)(s.Instance.Page("tag_"+tag, uint64(entry))) + } + }) + + s.Engine.GET(v1TagPageImage, func(context *gin.Context) { + if !s.privateAccessible(context) { + return + } + + tag := context.Param("tag") + if !store.MatchName(tag) { + context.JSON(http.StatusBadRequest, Error{Error: store.ErrInvalidInput.Error()}) + return + } + + param := context.Param("entry") + if entry, err := strconv.Atoi(param); doErrorAPI(context, err) { + return + } else { + s.json(context)(s.pageImages("tag_"+tag, uint64(entry))) + } + }) + + s.Engine.GET(v1TagInfo, func(context *gin.Context) { + if !s.privateAccessible(context) { + return + } + + s.json(context)(s.Instance.Tag(context.Param("tag"))) + }) + + s.Engine.PATCH(v1TagInfo, func(context *gin.Context) { + user := s.mustGetUser(context) + if user == nil { + return + } else if !user.Privileged { + context.JSON(http.StatusForbidden, ErrUnauthorized) + return + } + + var payload TagUpdatePayload + if err := context.ShouldBindJSON(&payload); doErrorAPI(context, err) { + return + } + doError(context, s.Instance.TagType(context.Param("tag"), payload.Type)) + }) +} diff --git a/api/v2.go b/api/v2.go new file mode 100644 index 0000000000000000000000000000000000000000..8938ad3b64c5e7a02a89cbcb4028fad4d619a74a --- /dev/null +++ b/api/v2.go @@ -0,0 +1,21 @@ +package api + +import ( + "github.com/gin-gonic/gin" + "net/http" +) + +func (s *Server) V2() { + backend := BackendInfo{ + Backend: s.Instance.Name(), + InitialUser: s.Instance.UserInitial(), + } + + s.Engine.GET(Base, func(context *gin.Context) { + context.JSON(http.StatusOK, backend) + }) + + s.Engine.GET(Config, func(context *gin.Context) { + context.JSON(http.StatusOK, s.Config) + }) +} diff --git a/backend/filesystem/image.go b/backend/filesystem/image.go new file mode 100644 index 0000000000000000000000000000000000000000..3f2b2e4230e64e34df65881d9f50a01ebeba391f --- /dev/null +++ b/backend/filesystem/image.go @@ -0,0 +1,553 @@ +package filesystem + +import ( + "bytes" + "crypto/sha256" + "encoding/json" + "fmt" + "image" + _ "image/gif" + "image/jpeg" + _ "image/png" + "log" + "os" + "random.chars.jp/git/image-board/v2/store" +) + +// ImageHashes returns a slice of image hashes. +func (s *Store) ImageHashes() ([]string, error) { + var images []string + if entries, err := os.ReadDir(s.ImagesHashDir()); err != nil { + return nil, err + } else { + for _, entry := range entries { + if entry.IsDir() { + var subEntries []os.DirEntry + if subEntries, err = os.ReadDir(s.ImagesHashDir() + "/" + entry.Name()); err != nil { + return nil, err + } else { + for _, subEntry := range subEntries { + images = append(images, entry.Name()+subEntry.Name()) + } + } + } + } + } + return images, nil +} + +// ImageHash returns an image with specific hash. +func (s *Store) ImageHash(hash string) (*store.Image, error) { + if !store.MatchSha256(hash) { + return nil, store.ErrInvalidInput + } else if !s.file(s.ImageMetadataPath(hash)) { + return nil, store.ErrNoEntry + } + + s.getLock(hash).RLock() + defer s.getLock(hash).RUnlock() + + return s.imageMetadataRead(s.ImageMetadataPath(hash)) +} + +// imageMetadataRead reads an image metadata file. +func (s *Store) imageMetadataRead(path string) (*store.Image, error) { + var metadata store.Image + if payload, err := os.ReadFile(path); err != nil { + if os.IsNotExist(err) { + return nil, store.ErrNoEntry + } + return nil, err + } else { + if err = json.Unmarshal(payload, &metadata); err != nil { + return nil, err + } + } + return &metadata, nil +} + +// ImageData returns an image and its data with a specific hash. +func (s *Store) ImageData(hash string, preview bool) (*store.Image, []byte, error) { + if !store.MatchSha256(hash) { + return nil, nil, store.ErrInvalidInput + } else if !s.file(s.ImageMetadataPath(hash)) { + return nil, nil, store.ErrNoEntry + } + + s.getLock(hash).RLock() + defer s.getLock(hash).RUnlock() + + var metadata *store.Image + if m, err := s.imageMetadataRead(s.ImageMetadataPath(hash)); err != nil { + return nil, nil, err + } else { + metadata = m + } + + var path string + if !preview { + path = s.ImageFilePath(hash) + } else { + path = s.ImagePreviewFilePath(hash) + } + if data, err := os.ReadFile(path); err != nil { + return nil, nil, err + } else { + return metadata, data, nil + } +} + +// ImageTags returns tags of an image with specific flake. +func (s *Store) ImageTags(flake string) ([]string, error) { + if !store.Numerical(flake) { + return nil, store.ErrInvalidInput + } else if !s.dir(s.ImageTagsPath(flake)) { + return nil, store.ErrNoEntry + } + + // Lock flake for directory-based operations + s.getLock(flake).RLock() + defer s.getLock(flake).RUnlock() + + return s.imageTags(flake) +} + +func (s *Store) imageTags(flake string) ([]string, error) { + var tags []string + if entries, err := os.ReadDir(s.ImageTagsPath(flake)); err != nil { + return nil, err + } else { + for _, entry := range entries { + tags = append(tags, entry.Name()) + } + } + return tags, nil +} + +// ImageHasTag figures out if an image has a tag. +func (s *Store) ImageHasTag(flake, tag string) (bool, error) { + if !store.Numerical(flake) { + return false, store.ErrInvalidInput + } else if !store.MatchName(tag) { + return false, store.ErrNoEntry + } + return s.file(s.ImageTagsPath(flake) + "/" + tag), nil +} + +//ImageSearch searches for images with specific tags. +func (s *Store) ImageSearch(tags []string) ([]string, error) { + if len(tags) < 1 || tags == nil { + return nil, store.ErrInvalidInput + } + + // Check if every tag matches name regex and exists + for _, tag := range tags { + if !store.MatchName(tag) { + return nil, store.ErrInvalidInput + } else if !s.file(s.TagPath(tag)) { + return nil, store.ErrNoEntry + } + } + + // Return if there's only one tag to search for + if len(tags) == 1 { + return s.TagImages(tags[0]) + } + + // Find entry with the least pages + entry := struct { + min uint64 + index int + }{} + + entry.index = 0 + if pt, err := s.PageTotal("tag_" + tags[0]); err != nil { + return nil, err + } else { + entry.min = pt + } + + for i := 1; i < len(tags); i++ { + if entry.min <= 1 { + break + } + + if pages, err := s.PageTotal("tag_" + tags[i]); err != nil { + return nil, err + } else if pages < entry.min { + entry.min = pages + entry.index = i + } + } + + // Get initial tag + var initial []string + if init, err := s.TagImages(tags[entry.index]); err != nil { + return nil, err + } else { + initial = init + } + + // Result slice + var result []string + + // Walk flakes from initial tag + for _, flake := range initial { + match := true + // Walk all remaining tags + for i, tag := range tags { + // Skip the entrypoint entry + if i == entry.index { + continue + } + + // Check if match + if b, err := s.ImageHasTag(flake, tag); err != nil { + return nil, err + } else if !b { + match = false + break + } + } + + // Append flake if all tags matched + if match { + result = append(result, flake) + } + } + + return result, nil +} + +// ImageAdd adds an image to the store. +func (s *Store) ImageAdd(data []byte, flake string) (*store.Image, error) { + if !store.Numerical(flake) { + return nil, store.ErrInvalidInput + } else if !s.dir(s.UserPath(flake)) { + return nil, store.ErrNoEntry + } + + info := store.Image{Snowflake: store.MakeFlake(store.ImageNode).String(), User: flake} + info.Hash = fmt.Sprintf("%x", sha256.Sum256(data)) + + if s.file(s.ImagePath(info.Hash)) { + return nil, store.ErrAlreadyExists + } + + s.getLock(info.Hash).Lock() + defer s.getLock(info.Hash).Unlock() + + var prev image.Image + if i, format, err := image.Decode(bytes.NewReader(data)); err != nil { + return nil, err + } else { + prev = store.MakePreview(i) + info.Type = format + } + + if err := os.MkdirAll(s.ImageHashTagsPath(info.Hash), s.PermissionDir); err != nil { + return nil, err + } + + if payload, err := json.Marshal(info); err != nil { + return nil, err + } else if err = os.WriteFile(s.ImageMetadataPath(info.Hash), payload, s.PermissionFile); err != nil { + return nil, err + } + + if err := os.WriteFile(s.ImageFilePath(info.Hash), data, s.PermissionFile); err != nil { + return nil, err + } + + if preview, err := os.Create(s.ImagePreviewFilePath(info.Hash)); err != nil { + return nil, err + } else if err = jpeg.Encode(preview, prev, &jpeg.Options{Quality: 100}); err != nil { + return nil, err + } else if err = preview.Close(); err != nil { + return nil, err + } + + if err := s.link("../images/"+s.ImageHashSplit(info.Hash), s.ImageSnowflakePath(info.Snowflake)); err != nil { + return nil, err + } + if err := s.link("../../../images/"+s.ImageHashSplit(info.Hash), s.UserImagesPath(flake)+"/"+info.Snowflake); err != nil { + return nil, err + } + + if err := s.pageInsert(store.ImageRootPageVariant, info.Snowflake); err != nil { + return nil, err + } + + log.Printf("image hash %s snowflake %s type %s added by user %s", info.Hash, info.Snowflake, info.Type, info.User) + return &info, nil +} + +// ImageUpdate updates image metadata. +func (s *Store) ImageUpdate(flake, source, parent, commentary, commentaryTranslation string) error { + if len(source) >= 1024 || + len(commentary) >= 65536 || len(commentaryTranslation) >= 65536 { + return store.ErrInvalidInput + } + + var info *store.Image + if i, err := s.Image(flake); err != nil { + return err + } else { + info = i + } + + s.getLock(info.Hash).Lock() + defer s.getLock(info.Hash).Unlock() + + var msg string + + if source != "\000" && store.MatchURL(source) { + info.Source = source + msg += "source" + } + + if parent != "\000" && parent != info.Snowflake && parent != info.Parent { + var p *store.Image + if parent == "" { + if par, err := s.Image(info.Parent); err != nil { + return err + } else { + p = par + } + p.Child = "" + } else { + if par, err := s.Image(parent); err != nil { + return err + } else { + p = par + } + if p.Child != "" { + goto end + } + p.Child = info.Snowflake + } + + info.Parent = parent + + s.getLock(p.Hash).Lock() + if err := s.imageMetadataWrite(p); err != nil { + return err + } + s.getLock(p.Hash).Unlock() + + if msg != "" { + msg += ", " + } + msg += "parent " + parent + end: + } + if commentary != "\000" { + info.Commentary = commentary + + if msg != "" { + msg += ", " + } + msg += "commentary" + } + if commentaryTranslation != "\000" { + info.CommentaryTranslation = commentaryTranslation + + if msg != "" { + msg += ", " + } + msg += "commentary translation" + } + + if msg != "" { + if err := s.imageMetadataWrite(info); err != nil { + return err + } else { + log.Printf("image %s %s updated", info.Snowflake, msg) + return nil + } + } + + return nil +} + +func (s *Store) imageMetadataWrite(info *store.Image) error { + if payload, err := json.Marshal(info); err != nil { + return err + } else { + return os.WriteFile(s.ImageMetadataPath(info.Hash), payload, s.PermissionFile) + } +} + +// Images returns a slice of image snowflakes. +func (s *Store) Images() ([]string, error) { + var snowflakes []string + if entries, err := os.ReadDir(s.ImagesSnowflakeDir()); err != nil { + return nil, err + } else { + for _, entry := range entries { + snowflakes = append(snowflakes, entry.Name()) + } + } + return snowflakes, nil +} + +// ImageSnowflakeHash returns image hash from snowflake. +func (s *Store) ImageSnowflakeHash(flake string) (string, error) { + if !store.Numerical(flake) { + return "", store.ErrInvalidInput + } + + if !s.Compat { + img, err := s.imageMetadataRead(s.ImageSnowflakePath(flake) + "/" + infoJson) + return img.Hash, err + } else { + if path, err := os.ReadFile(s.ImageSnowflakePath(flake)); err != nil { + if os.IsNotExist(err) { + return "", store.ErrNoEntry + } + return "", err + } else { + var img *store.Image + img, err = s.imageMetadataRead(string(path) + "/" + infoJson) + return img.Hash, err + } + } +} + +// Image returns image that has specific snowflake. +func (s *Store) Image(flake string) (*store.Image, error) { + if hash, err := s.ImageSnowflakeHash(flake); err != nil { + return nil, err + } else { + return s.ImageHash(hash) + } +} + +// ImageDestroy destroys an image. +func (s *Store) ImageDestroy(flake string) error { + if !store.Numerical(flake) { + return store.ErrInvalidInput + } else if !s.dir(s.ImageSnowflakePath(flake)) { + return store.ErrNoEntry + } + + var hash string + + if h, err := s.ImageSnowflakeHash(flake); err != nil { + return err + } else { + hash = h + } + + // Attempt to disassociate parent + if err := s.ImageUpdate(flake, "\000", "", "\000", "\000"); err != nil { + return err + } + + s.getLock(hash).Lock() + defer s.getLock(hash).Unlock() + + var info *store.Image + + if i, err := s.imageMetadataRead(s.ImageMetadataPath(hash)); err != nil { + return err + } else { + info = i + } + + // Disassociate child if set + if info.Child != "" { + if err := s.ImageUpdate(info.Child, "\000", "", "\000", "\000"); err != nil { + return err + } + } + + // Untag the image completely + if tags, err := s.imageTags(info.Snowflake); err != nil { + return err + } else { + for _, tag := range tags { + if err = s.imageTagRemove(info.Snowflake, tag); err != nil { + return err + } + } + } + + if err := os.Remove(s.ImageSnowflakePath(info.Snowflake)); err != nil { + return err + } + + if err := os.Remove(s.UserImagesPath(info.User) + "/" + info.Snowflake); err != nil { + return err + } + + if err := os.RemoveAll(s.ImagePath(hash)); err != nil { + return err + } + + if err := s.pageRegisterRemove(store.ImageRootPageVariant, info.Snowflake); err != nil { + return err + } + + log.Printf("image hash %s snowflake %s destroyed", info.Hash, info.Snowflake) + return nil +} + +// ImageTagAdd adds a tag to an image with specific snowflake. +func (s *Store) ImageTagAdd(flake, tag string) error { + if !store.MatchName(tag) || !store.Numerical(flake) { + return store.ErrInvalidInput + } else if !s.dir(s.ImageTagsPath(flake)) || !s.dir(s.TagPath(tag)) || s.file(s.TagPath(tag)+"/"+flake) { + return store.ErrNoEntry + } + + s.getLock(flake).Lock() + defer s.getLock(flake).Unlock() + + if err := s.link("../../snowflakes/"+flake, s.TagPath(tag)+"/"+flake); err != nil { + return err + } + if err := s.link("../../../../tags/"+tag, s.ImageSnowflakePath(flake)+"/tags/"+tag); err != nil { + return err + } + if err := s.pageInsert("tag_"+tag, flake); err != nil { + return err + } + + log.Printf("image snowflake %s tagged with %s", flake, tag) + return nil +} + +// ImageTagRemove removes a tag from an image with specific snowflake. +func (s *Store) ImageTagRemove(flake, tag string) error { + if !store.MatchName(tag) || !store.Numerical(flake) { + return store.ErrInvalidInput + } else if !s.dir(s.ImageTagsPath(flake)) || !s.dir(s.TagPath(tag)) { + return store.ErrNoEntry + } + + s.getLock(flake).Lock() + defer s.getLock(flake).Unlock() + + return s.imageTagRemove(flake, tag) +} + +func (s *Store) imageTagRemove(flake, tag string) error { + if s.file(s.ImageTagsPath(flake) + "/" + tag) { + if err := os.Remove(s.ImageTagsPath(flake) + "/" + tag); err != nil { + return err + } + } + if s.file(s.TagPath(tag) + "/" + flake) { + if err := os.Remove(s.TagPath(tag) + "/" + flake); err != nil { + return err + } + } + + if err := s.pageRegisterRemove("tag_"+tag, flake); err != nil { + return err + } else { + log.Printf("image snowflake %s untagged %s", flake, tag) + return nil + } +} diff --git a/backend/filesystem/page.go b/backend/filesystem/page.go new file mode 100644 index 0000000000000000000000000000000000000000..d9bfc9511ac769533f93ab934ec17d2d93325cdc --- /dev/null +++ b/backend/filesystem/page.go @@ -0,0 +1,213 @@ +package filesystem + +import ( + "encoding/binary" + "github.com/syndtr/goleveldb/leveldb" + "log" + "os" + "random.chars.jp/git/image-board/v2/store" +) + +// pageDB returns leveldb of page variant and creates it as required. +func (s *Store) pageDB(variant string) (*leveldb.DB, error) { + mutex := s.getLock("pageDB_get") + mutex.Lock() + defer mutex.Unlock() + + if ldb, ok := s.pageldb[variant]; ok && ldb != nil { + return ldb, nil + } else { + if db, err := leveldb.OpenFile(s.PageVariantPath(variant), nil); err != nil { + return nil, err + } else { + s.pageldb[variant] = db + if _, err = db.Get([]byte("\000"), nil); err != nil { + log.Printf("Page variant %s created.", variant) + if err = s.pageSetTotalCountNoDestroy(0, db); err != nil { + return nil, err + } + } + return db, nil + } + } +} + +// pageDBDestroy destroys leveldb of page variant. +func (s *Store) pageDBDestroy(variant string) error { + var db *leveldb.DB + if d, err := s.pageDB(variant); err != nil { + return err + } else { + db = d + } + if err := db.Close(); err != nil { + return err + } else { + delete(s.pageldb, variant) + } + + if err := os.RemoveAll(s.PageVariantPath(variant)); err != nil { + return err + } else { + log.Printf("page variant %s destroyed", variant) + return nil + } +} + +// pageGetTotalCount gets total count of a page variant. +func (s *Store) pageGetTotalCount(variant string) (uint64, error) { + var db *leveldb.DB + if d, err := s.pageDB(variant); err != nil { + return 0, err + } else { + db = d + } + + if payload, err := db.Get([]byte("\000"), nil); err != nil { + return 0, err + } else { + return binary.LittleEndian.Uint64(payload), nil + } +} + +// pageSetTotalCountNoDestroy sets total count of a page variant. +func (s *Store) pageSetTotalCountNoDestroy(value uint64, db *leveldb.DB) error { + payload := make([]byte, 8) + binary.LittleEndian.PutUint64(payload, value) + return db.Put([]byte("\000"), payload, nil) +} + +// pageSetTotalCount sets total count of a page variant and destroys it if zero. +func (s *Store) pageSetTotalCount(variant string, value uint64) error { + if value == 0 { + return s.pageDBDestroy(variant) + } + + var db *leveldb.DB + if d, err := s.pageDB(variant); err != nil { + return err + } else { + db = d + } + return s.pageSetTotalCountNoDestroy(value, db) +} + +// pageAdvanceTotalCount advances total count of a page variant. +func (s *Store) pageAdvanceTotalCount(variant string) error { + if t, err := s.pageGetTotalCount(variant); err != nil { + return err + } else { + return s.pageSetTotalCount(variant, t+1) + } +} + +// pageReduceTotalCount reduces total count of a page variant. +func (s *Store) pageReduceTotalCount(variant string) error { + if total, err := s.pageGetTotalCount(variant); err != nil { + return err + } else if total == 0 { + return nil + } else { + return s.pageSetTotalCount(variant, total-1) + } +} + +// PageTotal returns total amount of pages. +func (s *Store) PageTotal(variant string) (uint64, error) { + if t, err := s.pageGetTotalCount(variant); err != nil { + return 0, err + } else { + if t == 0 { + return t, nil + } else { + return (t / store.PageSize) + 1, nil + } + } +} + +// Page returns all entries in a page. +func (s *Store) Page(variant string, entry uint64) ([]string, error) { + if pt, err := s.PageTotal(variant); err != nil { + return nil, err + } else { + if entry >= pt { + return nil, store.ErrNoEntry + } + } + + var page []string + start := entry * store.PageSize + end := start + store.PageSize + begin := false + + var db *leveldb.DB + if d, err := s.pageDB(variant); err != nil { + return nil, err + } else { + db = d + } + + iter := db.NewIterator(nil, nil) + var i uint64 = 0 + for iter.Next() { + if i == end { + break + } + if begin { + page = append(page, string(iter.Key())) + } else { + if i >= start { + begin = true + } + } + i++ + } + iter.Release() + if err := iter.Error(); err != nil { + return nil, err + } + + return page, nil +} + +// PageInsert inserts an image into the index. +func (s *Store) pageInsert(variant, flake string) error { + if !store.Numerical(flake) { + return store.ErrInvalidInput + } else if !s.dir(s.ImageSnowflakePath(flake)) { + return store.ErrNoEntry + } + + s.getLock("page_" + variant).Lock() + defer s.getLock("page_" + variant).Unlock() + + var db *leveldb.DB + if d, err := s.pageDB(variant); err != nil { + return err + } else { + db = d + } + + if err := db.Put([]byte(flake), []byte{}, nil); err != nil { + return err + } + return s.pageAdvanceTotalCount(variant) +} + +// PageRegisterRemove registers an image remove. +func (s *Store) pageRegisterRemove(variant, flake string) error { + s.getLock("page_" + variant).Lock() + defer s.getLock("page_" + variant).Unlock() + + var db *leveldb.DB + if d, err := s.pageDB(variant); err != nil { + return err + } else { + db = d + } + + if err := db.Delete([]byte(flake), nil); err != nil { + return err + } + return s.pageReduceTotalCount(variant) +} diff --git a/store/paths.go b/backend/filesystem/paths.go similarity index 73% rename from store/paths.go rename to backend/filesystem/paths.go index b31f3d86eccbaac356d2b6b96112cc94ea72793c..7189b189ea856359de1cad8d1a07b2c8ccaac436 100644 --- a/store/paths.go +++ b/backend/filesystem/paths.go @@ -1,15 +1,15 @@ -package store +package filesystem const infoJson = "info.json" // LockPath returns path to lock file. func (s *Store) LockPath() string { - return s.Path + "/lock" + return s.path + "/lock" } // TagsDir returns path to tags. func (s *Store) TagsDir() string { - return s.Path + "/tags" + return s.path + "/tags" } // TagPath returns path to a specific tag. @@ -22,14 +22,19 @@ func (s *Store) TagMetadataPath(tag string) string { return s.TagPath(tag) + "/info.json" } -// ImagesDir returns path to images. -func (s *Store) ImagesDir() string { - return s.Path + "/images" +// ImagesBaseDir returns path to images. +func (s *Store) ImagesBaseDir() string { + return s.path + "/images" +} + +// ImagesHashDir returns path to image hashes. +func (s *Store) ImagesHashDir() string { + return s.ImagesBaseDir() + "/hashes" } // ImagePath returns path to an image with specific hash. func (s *Store) ImagePath(hash string) string { - return s.ImagesDir() + "/" + s.ImageHashSplit(hash) + return s.ImagesHashDir() + "/" + s.ImageHashSplit(hash) } // ImageHashSplit returns split image hash. @@ -64,7 +69,7 @@ func (s *Store) ImageHashTagsPath(hash string) string { // ImagesSnowflakeDir returns path to image snowflakes. func (s *Store) ImagesSnowflakeDir() string { - return s.Path + "/snowflakes" + return s.ImagesBaseDir() + "/snowflakes" } // ImageSnowflakePath returns path to an image with specific snowflake. @@ -72,9 +77,29 @@ func (s *Store) ImageSnowflakePath(flake string) string { return s.ImagesSnowflakeDir() + "/" + flake } +// TombstoneDir returns path to tombstones. +func (s *Store) TombstoneDir() string { + return s.UsersBaseDir() + "/tombstones" +} + +// TombstonePath returns path to a tombstone with specific snowflake. +func (s *Store) TombstonePath(flake string) string { + return s.TombstoneDir() + "/" + flake +} + +// TombstoneMetadataPath returns path to a tombstone's metadata with specific snowflake. +func (s *Store) TombstoneMetadataPath(flake string) string { + return s.TombstonePath(flake) + "/tombstone" +} + +// UsersBaseDir returns path to users. +func (s *Store) UsersBaseDir() string { + return s.path + "/users" +} + // UsersDir returns path to users. func (s *Store) UsersDir() string { - return s.Path + "/users" + return s.UsersBaseDir() + "/snowflakes" } // UserPath returns path to a user with specific snowflake. @@ -99,7 +124,7 @@ func (s *Store) UserPasswordPath(flake string) string { // UsernamesDir returns path to usernames. func (s *Store) UsernamesDir() string { - return s.Path + "/usernames" + return s.UsersBaseDir() + "/usernames" } // UsernamePath returns path to username. @@ -109,7 +134,7 @@ func (s *Store) UsernamePath(name string) string { // SecretsDir returns path to tokens. func (s *Store) SecretsDir() string { - return s.Path + "/secrets" + return s.UsersBaseDir() + "/secrets" } // SecretPath returns path to tokens. @@ -119,7 +144,7 @@ func (s *Store) SecretPath(secret string) string { // PageBaseDir returns path to page base directory. func (s *Store) PageBaseDir() string { - return s.Path + "/pages" + return s.path + "/pages" } // PageVariantPath returns path to pages of a variant. diff --git a/backend/filesystem/secret.go b/backend/filesystem/secret.go new file mode 100644 index 0000000000000000000000000000000000000000..1fc36008ce66e07a4a6f7a0c42d6157f82ff59ea --- /dev/null +++ b/backend/filesystem/secret.go @@ -0,0 +1,47 @@ +package filesystem + +import ( + "os" + "random.chars.jp/git/image-board/v2/store" +) + +// SecretLookup looks up a user from a secret. +func (s *Store) SecretLookup(secret string) (*store.User, error) { + if !store.MatchSecret(secret) { + return nil, store.ErrInvalidInput + } else if !s.file(s.SecretPath(secret)) { + return nil, store.ErrNoEntry + } + if !s.Compat { + return s.user(s.SecretPath(secret) + "/" + infoJson) + } else { + if path, err := os.ReadFile(s.SecretPath(secret)); err != nil { + return nil, err + } else { + return s.user(string(path) + "/" + infoJson) + } + } +} + +// vvvvvvvvvvvvvv Don't take this comment seriously! + +// SecretValidate validates the validity of a probably-valid secret with valid format. +func (s *Store) SecretValidate(secret string) bool { + return store.MatchSecret(secret) && s.file(s.SecretPath(secret)) +} + +// secretAssociate associates a secret with a user. +func (s *Store) secretAssociate(secret, flake string) error { + if s.file(s.SecretPath(secret)) { + return store.ErrAlreadyExists + } + return s.link("../users/"+flake, s.SecretPath(secret)) +} + +// secretDisassociate disassociates a secret. +func (s *Store) secretDisassociate(secret string) error { + if !s.file(s.SecretPath(secret)) { + return store.ErrNoEntry + } + return os.Remove(s.SecretPath(secret)) +} diff --git a/backend/filesystem/store.go b/backend/filesystem/store.go new file mode 100644 index 0000000000000000000000000000000000000000..3dcb9ee449b2f65dafe0055cf58cf7d750542804 --- /dev/null +++ b/backend/filesystem/store.go @@ -0,0 +1,266 @@ +package filesystem + +import ( + "encoding/json" + "fmt" + "github.com/syndtr/goleveldb/leveldb" + "log" + "os" + "random.chars.jp/git/image-board/v2/store" + "runtime" + "strconv" + "sync" +) + +const revision = 2 + +// Store represents a file store. +type Store struct { + Revision int `json:"revision"` + Compat bool `json:"compat"` + InitialUser string `json:"initial_user"` + PermissionDir os.FileMode `json:"permission_dir"` + PermissionFile os.FileMode `json:"permission_file"` + + verbose bool + path string + pageldb map[string]*leveldb.DB + mutex map[string]*sync.RWMutex + giant sync.RWMutex +} + +// New returns a pointer to a new instance of Store. +func New(path string, verbose bool) *Store { + return &Store{ + verbose: verbose, + path: path, + pageldb: make(map[string]*leveldb.DB), + mutex: make(map[string]*sync.RWMutex), + giant: sync.RWMutex{}, + } +} + +// Open opens the Store. +func (s *Store) Open() error { + if stat, err := os.Stat(s.path); err != nil { + if !os.IsNotExist(err) { + return err + } + + log.Printf("initializing new store %s", s.path) + + s.Revision = revision + s.Compat = runtime.GOOS == "windows" + s.PermissionDir = 0700 + s.PermissionFile = 0600 + + if err = s.create(); err != nil { + return err + } + } else { + if !stat.IsDir() { + return store.ErrNotDirectory + } + + // Load and parse store info. + var payload []byte + if payload, err = os.ReadFile(s.path + "/" + infoJson); err != nil { + return err + } else { + if err = json.Unmarshal(payload, &s); err != nil { + return err + } + } + + if s.Revision != revision { + if err = s.upgrade(); err != nil { + return err + } + } + } + + if s.file(s.LockPath()) { + if pid, err := os.ReadFile(s.LockPath()); err != nil { + return fmt.Errorf("stored locked. error file read error: %s", err) + } else { + return fmt.Errorf("stored locked by process %s", string(pid)) + } + } + if err := os.WriteFile(s.LockPath(), []byte(strconv.Itoa(os.Getpid())), s.PermissionFile); err != nil { + return err + } + + return nil +} + +// Close closes the Store. +func (s *Store) Close() error { + for variant, ldb := range s.pageldb { + if err := ldb.Close(); err != nil { + log.Printf("error page variant %s close: %s", variant, err) + return err + } else { + if s.verbose { + log.Printf("page variant %s closed", variant) + } + } + } + + if err := os.Remove(s.LockPath()); err != nil { + return err + } + + return nil +} + +// Name returns name of the Backend. +func (s *Store) Name() string { + return "filesystem" +} + +// upgrade the Store to a new format. +func (s *Store) upgrade() error { + // upgrade code goes here + + return fmt.Errorf("upgrading from revision %d to %d is not supported", s.Revision, revision) +} + +// create sets up the store directory if it does not exist. +func (s *Store) create() error { + if _, err := os.Stat(s.path); err == nil { + return store.ErrAlreadyExists + } + + if err := os.Mkdir(s.path, s.PermissionDir); err != nil { + return err + } + if err := os.Mkdir(s.TagsDir(), s.PermissionDir); err != nil { + return err + } + if err := os.Mkdir(s.PageBaseDir(), s.PermissionDir); err != nil { + return err + } + + if err := os.MkdirAll(s.ImagesHashDir(), s.PermissionDir); err != nil { + return err + } + if err := os.Mkdir(s.ImagesSnowflakeDir(), s.PermissionDir); err != nil { + return err + } + + if err := os.MkdirAll(s.UsersDir(), s.PermissionDir); err != nil { + return err + } + if err := os.Mkdir(s.TombstoneDir(), s.PermissionDir); err != nil { + return err + } + if err := os.Mkdir(s.SecretsDir(), s.PermissionDir); err != nil { + return err + } + if err := os.Mkdir(s.UsernamesDir(), s.PermissionDir); err != nil { + return err + } + + if info, err := s.UserAdd("root", store.InitialPassword, true); err != nil { + log.Fatalf("error adding initial user: %s", err) + } else { + log.Printf("initial user added with username \"root\"") + s.InitialUser = info.Snowflake + } + + // Create information file + if payload, err := json.Marshal(s); err != nil { + return err + } else { + if err = os.WriteFile(s.path+"/"+infoJson, payload, s.PermissionFile); err != nil { + return err + } + } + + return nil +} + +// getLock returns a lock associated with a string. +func (s *Store) getLock(entry string) *sync.RWMutex { + s.giant.RLock() + mutex, ok := s.mutex[entry] + s.giant.RUnlock() + if !ok { + s.giant.Lock() + mutex = &sync.RWMutex{} + s.mutex[entry] = mutex + s.giant.Unlock() + } + return mutex +} + +// file probes for the existence of specified entry on the filesystem. +func (s *Store) file(path string) bool { + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return false + } else { + if s.verbose { + log.Printf("file warning: %s stat error: %s", path, err) + } + return true + } + } + return true +} + +// dir probes for the presence of specified directory on the filesystem. +func (s *Store) dir(path string) bool { + if stat, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return false + } else { + if s.verbose { + log.Printf("dir warning: %s stat error: %s", path, err) + } + return false + } + } else { + if !stat.IsDir() { + if s.verbose { + log.Printf("dir warning: %s is not a directory", path) + } + return false + } + } + return true +} + +// link provides symlink-like usage with window compatibility. +func (s *Store) link(old, new string) error { + if !s.Compat { + if err := os.Symlink(old, new); err != nil { + return err + } else { + return nil + } + } else { + if err := os.WriteFile(new, []byte(old), s.PermissionFile); err != nil { + return err + } else { + return nil + } + } +} + +// readlink provides readlink-like usage with window compatibility. +func (s *Store) readlink(path string) (string, error) { + if !s.Compat { + //return os.Readlink(path) + return path, nil + } else { + if final, err := os.ReadFile(path); err != nil { + if os.IsNotExist(err) { + return "", store.ErrNoEntry + } + return "", err + } else { + return string(final), nil + } + } +} diff --git a/backend/filesystem/tag.go b/backend/filesystem/tag.go new file mode 100644 index 0000000000000000000000000000000000000000..ec03e749ef5c4b71a3c45990f3c071025e533654 --- /dev/null +++ b/backend/filesystem/tag.go @@ -0,0 +1,148 @@ +package filesystem + +import ( + "encoding/json" + "log" + "os" + "random.chars.jp/git/image-board/v2/store" + "time" +) + +// Tags returns a slice of tag names. +func (s *Store) Tags() ([]string, error) { + var tags []string + if entries, err := os.ReadDir(s.TagsDir()); err != nil { + return nil, err + } else { + for _, entry := range entries { + if entry.IsDir() { + tags = append(tags, entry.Name()) + } + } + } + return tags, nil +} + +// Tag returns information of a tag. +func (s *Store) Tag(tag string) (*store.Tag, error) { + if !store.MatchName(tag) { + return nil, store.ErrInvalidInput + } else if !s.file(s.TagMetadataPath(tag)) { + return nil, store.ErrNoEntry + } + + s.getLock("tag_" + tag).RLock() + defer s.getLock("tag_" + tag).RUnlock() + + if payload, err := os.ReadFile(s.TagMetadataPath(tag)); err != nil { + return nil, err + } else { + var info store.Tag + err = json.Unmarshal(payload, &info) + return &info, err + } +} + +// TagImages returns a slice of image snowflakes in a specific tag. +func (s *Store) TagImages(tag string) ([]string, error) { + if !store.MatchName(tag) { + return nil, store.ErrInvalidInput + } else if !s.dir(s.TagPath(tag)) { + return nil, store.ErrNoEntry + } + var images []string + if entries, err := os.ReadDir(s.TagPath(tag)); err != nil { + return nil, err + } else { + for _, entry := range entries { + if entry.Name() == infoJson { + continue + } + images = append(images, entry.Name()) + } + } + return images, nil +} + +// TagAdd creates a tag. +func (s *Store) TagAdd(tag string) error { + if len(tag) > 128 || !store.MatchName(tag) { + return store.ErrInvalidInput + } else if s.file(s.TagPath(tag)) { + return store.ErrAlreadyExists + } + + s.getLock("tag_" + tag).Lock() + defer s.getLock("tag_" + tag).Unlock() + if err := os.Mkdir(s.TagPath(tag), s.PermissionDir); err != nil { + return err + } + if payload, err := json.Marshal(store.Tag{Type: store.GenericType, CreationTime: time.Now().UTC()}); err != nil { + return err + } else { + if err = os.WriteFile(s.TagMetadataPath(tag), payload, s.PermissionFile); err != nil { + return err + } else { + log.Printf("tag %s added", tag) + return nil + } + } +} + +// TagDestroy removes all references from a tag and removes it. +func (s *Store) TagDestroy(tag string) error { + if !store.MatchName(tag) { + return store.ErrInvalidInput + } else if !s.dir(s.TagPath(tag)) { + return store.ErrNoEntry + } + + if flakes, err := s.TagImages(tag); err != nil { + return err + } else { + for _, flake := range flakes { + if err = s.ImageTagRemove(flake, tag); err != nil { + return err + } + } + } + + if err := os.Remove(s.TagMetadataPath(tag)); err != nil { + return err + } + if err := os.Remove(s.TagPath(tag)); err != nil { + return err + } + + log.Printf("tag %s destroyed", tag) + return nil +} + +// TagType sets type of tag. +func (s *Store) TagType(tag, t string) error { + if !store.MatchName(tag) || !store.MatchTagType(t) { + return store.ErrInvalidInput + } else if !s.file(s.TagMetadataPath(tag)) { + return store.ErrNoEntry + } + + if info, err := s.Tag(tag); err != nil { + return err + } else { + s.getLock("tag_" + tag).Lock() + defer s.getLock("tag_" + tag).Unlock() + + info.Type = t + var payload []byte + if payload, err = json.Marshal(info); err != nil { + return err + } else { + if err = os.WriteFile(s.TagMetadataPath(tag), payload, s.PermissionFile); err != nil { + return err + } else { + log.Printf("tag %s type set to %s", tag, t) + return nil + } + } + } +} diff --git a/backend/filesystem/user.go b/backend/filesystem/user.go new file mode 100644 index 0000000000000000000000000000000000000000..5d3958cbd79ccbd2033dbccb1bf4cb375c4bdbb6 --- /dev/null +++ b/backend/filesystem/user.go @@ -0,0 +1,381 @@ +package filesystem + +import ( + "encoding/json" + "log" + "os" + "random.chars.jp/git/image-board/v2/store" + "time" +) + +// UserInitial returns flake of initial user. +func (s *Store) UserInitial() string { + return s.InitialUser +} + +// user parses user metadata file. +func (s *Store) user(path string) (*store.User, error) { + if payload, err := os.ReadFile(path); err != nil { + return nil, err + } else { + var info store.User + if err = json.Unmarshal(payload, &info); err != nil { + return nil, err + } else { + return &info, nil + } + } +} + +// User returns user information with specific snowflake. +func (s *Store) User(flake string) (*store.User, error) { + if !store.Numerical(flake) { + return nil, store.ErrInvalidInput + } else if !s.file(s.UserPath(flake)) { + return nil, store.ErrNoEntry + } + + s.getLock(flake).RLock() + defer s.getLock(flake).RUnlock() + return s.user(s.UserMetadataPath(flake)) +} + +// Users returns a slice of user snowflakes. +func (s *Store) Users() ([]string, error) { + var users []string + if entries, err := os.ReadDir(s.UsersDir()); err != nil { + return nil, err + } else { + for _, entry := range entries { + if entry.IsDir() { + users = append(users, entry.Name()) + } + } + } + return users, nil +} + +// userMetadata sets user metadata. +func (s *Store) userMetadata(info *store.User) error { + if payload, err := json.Marshal(info); err != nil { + return err + } else { + return os.WriteFile(s.UserMetadataPath(info.Snowflake), payload, s.PermissionFile) + } +} + +// UserAdd creates a user. +func (s *Store) UserAdd(username, password string, privileged bool) (*store.User, error) { + if len(username) > 64 || !store.MatchName(username) || !store.MatchPassword(password) { + return nil, store.ErrInvalidInput + } else if s.file(s.UsernamePath(username)) { + return nil, store.ErrAlreadyExists + } + + var secret string + if se, err := store.SecretNew(); err != nil { + return nil, err + } else { + secret = se + } + info := store.User{ + Secret: secret, + Privileged: privileged, + Snowflake: store.MakeFlake(store.UserNode).String(), + Username: username, + } + // Create user directory and images + if err := os.MkdirAll(s.UserImagesPath(info.Snowflake), s.PermissionDir); err != nil { + return nil, err + } + + s.getLock(info.Snowflake).Lock() + defer s.getLock(info.Snowflake).Unlock() + + if err := s.userMetadata(&info); err != nil { + return nil, err + } + if err := s.userUsernameAssociate(info.Snowflake, info.Username); err != nil { + return nil, err + } + if err := s.userPasswordUpdate(s.UserPath(info.Snowflake), password); err != nil { + return nil, err + } + if err := s.secretAssociate(info.Secret, info.Snowflake); err != nil { + return nil, err + } + + log.Printf("user %s added with username %s privilege %v secret %s.", + info.Snowflake, info.Username, info.Privileged, info.Secret) + return &info, nil +} + +// UserPrivileged sets privileged status of user with specific snowflake. +func (s *Store) UserPrivileged(flake string, privileged bool) error { + if !store.Numerical(flake) { + return store.ErrInvalidInput + } + + s.getLock(flake).Lock() + defer s.getLock(flake).Unlock() + + if info, err := s.user(s.UserMetadataPath(flake)); err != nil { + return err + } else { + info.Privileged = privileged + if err = s.userMetadata(info); err != nil { + return err + } else { + log.Printf("user %s privileged %v", flake, privileged) + return nil + } + } +} + +// UserUsernameUpdate updates username of user with specific snowflake. +func (s *Store) UserUsernameUpdate(flake, username string) error { + if !store.Numerical(flake) || !store.MatchName(username) { + return store.ErrInvalidInput + } else if s.file(s.UsernamePath(username)) { + return store.ErrAlreadyExists + } + + s.getLock(flake).Lock() + defer s.getLock(flake).Unlock() + + if info, err := s.user(s.UserMetadataPath(flake)); err != nil { + return err + } else { + s.getLock(info.Username).Lock() + defer s.getLock(info.Username).Unlock() + + if info.Username != "" { + if err = s.userUsernameDisassociate(info.Username); err != nil { + return err + } + } + if err = s.userUsernameAssociate(flake, username); err != nil { + return err + } + + info.Username = username + if err = s.userMetadata(info); err != nil { + return err + } + log.Printf("user %s username updated to %s.", flake, username) + return nil + } + +} + +// UserSecretRegen regenerates secret of user with specific snowflake. +func (s *Store) UserSecretRegen(flake string) (string, error) { + if !store.Numerical(flake) { + return "", store.ErrInvalidInput + } + + s.getLock(flake).Lock() + defer s.getLock(flake).Unlock() + + if info, err := s.user(s.UserMetadataPath(flake)); err != nil { + return "", err + } else { + // Disassociate old user + if err = s.secretDisassociate(info.Secret); err != nil { + return "", err + } + // Generate new secret + if info.Secret, err = store.SecretNew(); err != nil { + return "", err + } + // Write metadata + if err = s.userMetadata(info); err != nil { + return "", err + } + // Associate new secret + if err = s.secretAssociate(info.Secret, info.Snowflake); err != nil { + return "", err + } + + log.Printf("user %s secret reset to %s", flake, info.Secret) + return info.Secret, nil + } +} + +// UserUsername returns user via username. +func (s *Store) UserUsername(username string) (*store.User, error) { + if !store.MatchName(username) { + return nil, store.ErrInvalidInput + } else if !s.file(s.UsernamePath(username)) { + return nil, store.ErrNoEntry + } + + s.getLock(username).RLock() + defer s.getLock(username).RUnlock() + + var rl string + if r, err := s.readlink(s.UsernamePath(username)); err != nil { + return nil, err + } else { + rl = r + } + if user, err := s.user(rl + "/" + infoJson); err != nil { + return nil, err + } else { + return user, nil + } +} + +// userUsernameAssociate associates user snowflake with specific username. +func (s *Store) userUsernameAssociate(flake, username string) error { + return s.link("../users/"+flake, s.UsernamePath(username)) +} + +// userUsernameDisassociate disassociates specific username. +func (s *Store) userUsernameDisassociate(username string) error { + return os.Remove(s.UsernamePath(username)) +} + +// userPassword returns password of user from path to user directory. +func (s *Store) userPassword(path string) (string, error) { + if payload, err := os.ReadFile(path + "/passwd"); err != nil { + if os.IsNotExist(err) { + return "", store.ErrNoEntry + } + return "", err + } else { + return string(payload), nil + } +} + +// userPasswordUpdate updates user password of user from path to user directory. +func (s *Store) userPasswordUpdate(path, password string) error { + return os.WriteFile(path+"/passwd", []byte(password), s.PermissionFile) +} + +// UserPasswordValidate validates password of specified user. +func (s *Store) UserPasswordValidate(flake, username *string, password string) (bool, error) { + if flake != nil { + if !store.Numerical(*flake) || !store.MatchPassword(password) { + return false, store.ErrInvalidInput + } else if !s.file(s.UserPath(*flake)) { + return false, store.ErrNoEntry + } + + s.getLock(*flake).RLock() + defer s.getLock(*flake).RUnlock() + + if p, err := s.userPassword(s.UserPath(*flake)); err != nil { + return false, err + } else { + return password != "" && password == p, nil + } + } else if username != nil { + if !store.MatchName(*username) || !store.MatchPassword(password) { + return false, store.ErrInvalidInput + } else if !s.file(s.UsernamePath(*username)) { + return false, store.ErrNoEntry + } + + s.getLock(*username).RLock() + defer s.getLock(*username).RUnlock() + + var p string + if r, err := s.readlink(s.UsernamePath(*username)); err != nil { + return false, err + } else { + if p, err = s.userPassword(r); err != nil { + return false, err + } + } + + return password != "" && password == p, nil + } else { + return false, store.ErrInvalidInput + } +} + +// UserPasswordUpdate updates password of specified user. +func (s *Store) UserPasswordUpdate(flake, password string) error { + if !store.Numerical(flake) || !store.MatchPassword(password) { + return store.ErrInvalidInput + } + + s.getLock(flake).Lock() + defer s.getLock(flake).Unlock() + + return s.userPasswordUpdate(s.UserPath(flake), password) +} + +// UserDestroy destroys a user with specific snowflake. +func (s *Store) UserDestroy(flake string) error { + if !store.Numerical(flake) { + return store.ErrInvalidInput + } + if !s.dir(s.UserPath(flake)) { + return store.ErrNoEntry + } + + if info, err := s.User(flake); err != nil { + return err + } else { + s.getLock(info.Snowflake).Lock() + defer s.getLock(info.Snowflake).Unlock() + + if err = s.secretDisassociate(info.Secret); err != nil { + return err + } + if err = s.userUsernameDisassociate(info.Username); err != nil { + return err + } + + if err = os.Rename(s.UserPath(flake), s.TombstonePath(flake)); err != nil { + return err + } + + var tombstone *os.File + if tombstone, err = os.Create(s.TombstoneMetadataPath(flake)); err != nil { + return err + } else { + if err = json.NewEncoder(tombstone).Encode(store.Tombstone{Time: int(time.Now().Unix())}); err != nil { + return err + } else if err = tombstone.Close(); err != nil { + return err + } + log.Printf("user %s username %s destroyed", info.Snowflake, info.Username) + return nil + } + } +} + +// UserImages returns slice of a user's images. +func (s *Store) UserImages(flake string) ([]string, error) { + if !store.Numerical(flake) { + return nil, store.ErrInvalidInput + } + if !s.dir(s.UserImagesPath(flake)) { + return nil, store.ErrNoEntry + } + + var images []string + if entries, err := os.ReadDir(s.UserImagesPath(flake)); err != nil { + return nil, err + } else { + for _, entry := range entries { + images = append(images, entry.Name()) + } + } + return images, nil +} + +// UserImage validates whether a user owns an Image. +func (s *Store) UserImage(flake, imageFlake string) (bool, error) { + if !store.Numerical(flake) { + return false, store.ErrInvalidInput + } + if !s.dir(s.UserImagesPath(flake)) { + return false, store.ErrNoEntry + } + + return s.dir(s.UserImagesPath(flake) + "/" + imageFlake), nil +} diff --git a/cleanup.go b/cleanup.go index 9925b6bfa6d8ab532a6837242939ea398146bc37..cfe659dc1cc5c5266d3f9a020fb16828ea7d8fc9 100644 --- a/cleanup.go +++ b/cleanup.go @@ -2,25 +2,18 @@ package main import ( "context" - log "github.com/sirupsen/logrus" + "log" "time" ) -func cleanup(restart bool) { - var err error - - // Set restart - d = true - r = restart - - // Close store - instance.Close() +func cleanup() { + if err := instance.Close(); err != nil { + log.Printf("error closing instance: %s", err) + } - // Shutdown web server ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - err = server.Shutdown(ctx) - if err != nil { - log.Errorf("Error while shutting down web server, %s", err) + if err := server.Shutdown(ctx); err != nil { + log.Printf("error shutting down web server: %s", err) } } diff --git a/client/image.go b/client/image.go deleted file mode 100644 index 6fcd8ed1ad7969e3f33b4abf191e30b93ab0a6f4..0000000000000000000000000000000000000000 --- a/client/image.go +++ /dev/null @@ -1,195 +0,0 @@ -package client - -import ( - "bytes" - "fmt" - "io" - "mime/multipart" - "net/http" - "random.chars.jp/git/image-board/api" - "random.chars.jp/git/image-board/store" - "strconv" -) - -// Images returns a slice of snowflakes of all images. Only available to privileged users. -func (r *Remote) Images() ([]string, error) { - var flakes []string - err := r.fetch(http.MethodGet, api.Image, &flakes, nil) - return flakes, err -} - -// ImageAdd adds an image to Remote and returns a store.Image. -func (r *Remote) ImageAdd(reader io.Reader) (store.Image, error) { - if c, ok := reader.(io.Closer); ok { - defer func() { - if err := c.Close(); err != nil { - fmt.Printf("Error closing Closer, %s", err) - } - }() - } - - buf := &bytes.Buffer{} - w := multipart.NewWriter(buf) - if f, err := w.CreateFormFile("image", "image"); err != nil { - return store.Image{}, err - } else { - if _, err = io.Copy(f, reader); err != nil { - return store.Image{}, err - } - } - - if err := w.Close(); err != nil { - return store.Image{}, err - } - - if req, err := http.NewRequest(http.MethodPost, r.URL(api.Image), buf); err != nil { - return store.Image{}, err - } else { - req.Header.Set("Content-Type", w.FormDataContentType()) - var resp *http.Response - if resp, err = r.send(req); err != nil { - return store.Image{}, err - } else { - var image store.Image - err = unmarshal(resp.Body, &image) - return image, err - } - } -} - -// Image returns store.Image with given snowflake. -func (r *Remote) Image(flake string) (store.Image, error) { - var image store.Image - err := r.fetch(http.MethodGet, populateField(api.ImageField, "flake", flake), &image, nil) - return image, err -} - -// ImageGroup returns an entire group of store.Image. -func (r *Remote) ImageGroup(flake string) ([]store.Image, error) { - if image, err := r.Image(flake); err != nil { - return nil, err - } else { - var group []store.Image - - // Iterate forwards - if err = func(image store.Image) error { - group = []store.Image{image} - for image.Child != "" { - if image, err = r.Image(image.Child); err != nil { - return err - } - group = append(group, image) - } - return nil - }(image); err != nil { - return group, err - } - - // Iterate backwards - if err = func(image store.Image) error { - for image.Parent != "" { - if image, err = r.Image(image.Parent); err != nil { - return err - } - group = append([]store.Image{image}, group...) - } - return nil - }(image); err != nil { - return group, err - } - - return group, nil - } -} - -// ImageUpdate updates metadata of store.Image with given snowflake. To persist original value in a field set \000. -func (r *Remote) ImageUpdate(flake, source, parent, commentary, commentaryTranslation string) error { - payload := api.ImageUpdatePayload{ - Source: source, - Parent: parent, - Commentary: commentary, - CommentaryTranslation: commentaryTranslation, - } - return r.requestJSONnoResp(http.MethodPatch, populateField(api.ImageField, "flake", flake), payload) -} - -// ImageDestroy destroys a store.Image with given snowflake. -func (r *Remote) ImageDestroy(flake string) error { - return r.requestNoResp(http.MethodDelete, populateField(api.ImageField, "flake", flake), nil) -} - -// ImageFile returns a slice of bytes of the image with given snowflake. -func (r *Remote) ImageFile(flake string, preview bool) ([]byte, error) { - switch preview { - case true: - return r.fetchAll(http.MethodGet, populateField(api.ImagePreview, "flake", flake), nil) - case false: - return r.fetchAll(http.MethodGet, populateField(api.ImageFile, "flake", flake), nil) - default: - return nil, nil - } -} - -// ImageTag returns a slice of tags of an image with given snowflake. -func (r *Remote) ImageTag(flake string) ([]string, error) { - var tags []string - err := r.fetch(http.MethodGet, populateField(api.ImageTag, "flake", flake), &tags, nil) - return tags, err -} - -// ImageTagAdd adds a tag to an image with given snowflake. -func (r *Remote) ImageTagAdd(flake, tag string) error { - return r.requestNoResp(http.MethodPut, - populateField(populateField(api.ImageTagField, - "flake", flake), - "tag", tag), - nil) -} - -// ImageTagRemove removes a tag from an image with given snowflake. -func (r *Remote) ImageTagRemove(flake, tag string) error { - return r.requestNoResp(http.MethodDelete, - populateField(populateField(api.ImageTagField, - "flake", flake), - "tag", tag), - nil) -} - -// ImagePages returns total amount of image pages. -func (r *Remote) ImagePages() (int, error) { - if payload, err := r.fetchAll(http.MethodGet, api.ImagePage, nil); err != nil { - return 0, err - } else { - var pages int - if pages, err = strconv.Atoi(string(payload)); err != nil { - return 0, err - } else { - return pages, nil - } - } -} - -// ImagePage returns a slice of snowflakes of images in a page. -func (r *Remote) ImagePage(entry int) ([]string, error) { - var flakes []string - err := r.fetch(http.MethodGet, populateField(api.ImagePageField, "entry", strconv.Itoa(entry)), &flakes, nil) - return flakes, err -} - -// ImagePageImage returns a slice of store.Image in a page. -func (r *Remote) ImagePageImage(entry int) ([]store.Image, error) { - var images []store.Image - err := r.fetch(http.MethodGet, populateField(api.ImagePageImage, "entry", strconv.Itoa(entry)), &images, nil) - return images, err -} - -// ImageSearch searches for images that contains all specified tags. -func (r *Remote) ImageSearch(tags []string) ([]string, error) { - t := tags[0] - for i := 1; i < len(tags); i++ { - t += "!" + tags[i] - } - var flakes []string - err := r.fetch(http.MethodGet, populateField(api.SearchField, "tags", t), &flakes, nil) - return flakes, err -} diff --git a/client/js/README b/client/js/README deleted file mode 100644 index f827973e260b73a9f5b31ce30190f4be6ac73424..0000000000000000000000000000000000000000 --- a/client/js/README +++ /dev/null @@ -1 +0,0 @@ -This is for code generation only. \ No newline at end of file diff --git a/client/js/main.go b/client/js/main.go deleted file mode 100644 index caecbddab772cedc77ca12acd72d75dc631f3f89..0000000000000000000000000000000000000000 --- a/client/js/main.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -//go:generate gopherjs build -o client.js -//go:generate gopherjs build -o client.min.js --minify - -import ( - "github.com/gopherjs/gopherjs/js" - "random.chars.jp/git/image-board/client" -) - -func main() { - js.Global.Set("newRemote", client.New) -} diff --git a/client/misc.go b/client/misc.go deleted file mode 100644 index b37f5682c9a4858b81b771df79cbbc15a0214c6a..0000000000000000000000000000000000000000 --- a/client/misc.go +++ /dev/null @@ -1,7 +0,0 @@ -package client - -import "strings" - -func populateField(path, field, content string) string { - return strings.Replace(path, ":"+field, content, 1) -} diff --git a/client/remote.go b/client/remote.go deleted file mode 100644 index 34a0684f216e004861ffe0c3b0a10af1f1e403d6..0000000000000000000000000000000000000000 --- a/client/remote.go +++ /dev/null @@ -1,71 +0,0 @@ -package client - -import ( - "fmt" - "net/http" - "random.chars.jp/git/image-board/api" - "random.chars.jp/git/image-board/store" -) - -// Remote represents a remote image board server. -type Remote struct { - url string - single bool - private bool - secret string - client *http.Client - user *api.UserPayload - store.Info -} - -// New returns a pointer to a new Remote. -func New(url string) (*Remote, error) { - remote := &Remote{url: url, client: &http.Client{}} - return remote, remote.Handshake() -} - -// URL returns the URL of Remote. -func (r *Remote) URL(path string) string { - return r.url + path -} - -// SingleUser returns whether the Remote is running in single user mode. -func (r *Remote) SingleUser() bool { - return r.single -} - -// Private returns whether the Remote is running in private mode. -func (r *Remote) Private() bool { - return r.private -} - -// Handshake checks if the server is still online and updates Remote. -func (r *Remote) Handshake() error { - if err := r.fetch(http.MethodGet, api.SingleUser, &r.single, nil); err != nil { - return err - } - if err := r.fetch(http.MethodGet, api.Private, &r.private, nil); err != nil { - return err - } - return r.fetch(http.MethodGet, api.Base, r, nil) -} - -// Secret authenticates and sets secret. -func (r *Remote) Secret(secret string) (api.UserPayload, bool) { - // Clear secret if empty - if secret == "" { - r.secret = secret - return api.UserPayload{}, true - } - - prev := r.secret - r.secret = secret - if user, err := r.This(); err != nil { - fmt.Printf("Error getting user post secret change, %s", err) - r.secret = prev - return api.UserPayload{}, false - } else { - r.user = &user - return user, true - } -} diff --git a/client/request.go b/client/request.go deleted file mode 100644 index 93ada62bd2297019e9a8c9377375812b7849c6d3..0000000000000000000000000000000000000000 --- a/client/request.go +++ /dev/null @@ -1,115 +0,0 @@ -package client - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "random.chars.jp/git/image-board/api" -) - -func (r *Remote) send(req *http.Request) (*http.Response, error) { - if req == nil { - return nil, nil - } - - if r.secret != "" { - req.Header.Add("secret", r.secret) - } - - return r.client.Do(req) -} - -func (r *Remote) request(method, path string, body io.Reader) (*http.Response, error) { - if req, err := http.NewRequest(method, r.URL(path), body); err != nil { - return nil, err - } else { - return r.send(req) - } -} - -func (r *Remote) requestNoResp(method, path string, body io.Reader) error { - if resp, err := r.request(method, path, body); err != nil { - return err - } else { - return resp.Body.Close() - } -} - -func (r *Remote) requestJSON(method, path string, v interface{}) (*http.Response, error) { - var reader *bytes.Reader - if v != nil { - if payload, err := json.Marshal(v); err != nil { - return nil, err - } else { - reader = bytes.NewReader(payload) - } - } - return r.request(method, path, reader) -} - -func (r *Remote) requestJSONnoResp(method, path string, v interface{}) error { - if resp, err := r.requestJSON(method, path, v); err != nil { - return err - } else { - return resp.Body.Close() - } -} - -func (r *Remote) fetch(method, path string, v interface{}, body io.Reader) error { - if resp, err := r.request(method, path, body); err != nil { - return err - } else { - if v == nil { - return nil - } - if err = unmarshal(resp.Body, v); err != nil { - return err - } - } - return nil -} - -func (r *Remote) fetchAll(method, path string, body io.Reader) ([]byte, error) { - if resp, err := r.request(method, path, body); err != nil { - return nil, err - } else { - defer func() { - if err = resp.Body.Close(); err != nil { - fmt.Printf("Error closing body after reading all, %s\n", err) - } - }() - var content []byte - if content, err = io.ReadAll(resp.Body); err != nil { - return nil, err - } else { - return content, nil - } - } -} - -func unmarshal(reader io.ReadCloser, v interface{}) error { - defer func() { - if err := reader.Close(); err != nil { - fmt.Printf("Error closing reader after unmarshalling, %s\n", err) - } - }() - - var data []byte - if d, err := io.ReadAll(reader); err != nil { - return err - } else { - data = d - } - - if err := json.Unmarshal(data, v); err != nil { - var errPayload api.Error - if tryErr := json.Unmarshal(data, &errPayload); tryErr == nil { - return errors.New(errPayload.Error) - } - return err - } - return nil -} diff --git a/client/tag.go b/client/tag.go deleted file mode 100644 index 37b8745cdd7532029610f90774872c62f7d1da13..0000000000000000000000000000000000000000 --- a/client/tag.go +++ /dev/null @@ -1,81 +0,0 @@ -package client - -import ( - "net/http" - "random.chars.jp/git/image-board/api" - "random.chars.jp/git/image-board/store" - "strconv" -) - -// Tags returns a slice of tag names on Remote. -func (r *Remote) Tags() ([]string, error) { - var tags []string - err := r.fetch(http.MethodGet, api.Tag, &tags, nil) - return tags, err -} - -// Tag returns a slice of snowflakes in a given tag. -func (r *Remote) Tag(tag string) ([]string, error) { - var flakes []string - err := r.fetch(http.MethodGet, populateField(api.TagField, "tag", tag), &flakes, nil) - return flakes, err -} - -// TagPage returns a slice of snowflakes in a page of a given tag. -func (r *Remote) TagPage(tag string, entry int) ([]string, error) { - var flakes []string - err := r.fetch(http.MethodGet, - populateField(populateField(api.TagPageField, - "tag", tag), - "entry", strconv.Itoa(entry)), - &flakes, nil) - return flakes, err -} - -// TagPageImages returns a slice of store.Image in a page of a given tag. -func (r *Remote) TagPageImages(tag string, entry int) ([]store.Image, error) { - var images []store.Image - err := r.fetch(http.MethodGet, - populateField(populateField(api.TagPageImage, - "tag", tag), - "entry", strconv.Itoa(entry)), - &images, nil) - return images, err -} - -// TagPages returns total amount of pages of a tag. -func (r *Remote) TagPages(tag string) (int, error) { - if payload, err := r.fetchAll(http.MethodGet, - populateField(api.TagPage, "tag", tag), nil); err != nil { - return 0, err - } else { - var pages int - if pages, err = strconv.Atoi(string(payload)); err != nil { - return 0, err - } else { - return pages, nil - } - } -} - -// TagCreate creates a tag on Remote. -func (r *Remote) TagCreate(tag string) error { - return r.requestNoResp(http.MethodPut, populateField(api.TagField, "tag", tag), nil) -} - -// TagDestroy destroys a tag on Remote. -func (r *Remote) TagDestroy(tag string) error { - return r.requestNoResp(http.MethodDelete, populateField(api.TagField, "tag", tag), nil) -} - -// TagInfo returns store.Tag of given tag. -func (r *Remote) TagInfo(tag string) (store.Tag, error) { - var t store.Tag - err := r.fetch(http.MethodGet, populateField(api.TagInfo, "tag", tag), &t, nil) - return t, err -} - -// TagType sets type of given tag. -func (r *Remote) TagType(tag, t string) error { - return r.requestJSONnoResp(http.MethodPatch, populateField(api.TagInfo, "tag", tag), api.TagUpdatePayload{Type: t}) -} diff --git a/client/user.go b/client/user.go deleted file mode 100644 index 53c88291dfe55249b2cc191c63ff3edaa5ff7f9b..0000000000000000000000000000000000000000 --- a/client/user.go +++ /dev/null @@ -1,107 +0,0 @@ -package client - -import ( - "net/http" - "random.chars.jp/git/image-board/api" - "random.chars.jp/git/image-board/store" - "strings" -) - -// User returns api.UserPayload of a snowflake. -func (r *Remote) User(flake string) (api.UserPayload, error) { - var user api.UserPayload - err := r.fetch(http.MethodGet, api.User+"/"+flake, &user, nil) - return user, err -} - -// This returns api.UserPayload currently authenticated as. -func (r *Remote) This() (api.UserPayload, error) { - return r.User("this") -} - -// Username returns api.UserPayload of username. -func (r *Remote) Username(name string) (api.UserPayload, error) { - var user api.UserPayload - err := r.fetch(http.MethodGet, api.Username+"/"+name, &user, nil) - return user, err -} - -// Auth authenticates using a username and its corresponding password. -func (r *Remote) Auth(username, password string) (string, error) { - var secret api.UserSecretPayload - err := r.fetch(http.MethodPost, - populateField(api.UsernameAuth, "name", username), - &secret, - strings.NewReader(password)) - return secret.Secret, err -} - -// UserAdd adds a new user. -func (r *Remote) UserAdd(username string, password string, privileged bool) (store.User, error) { - if resp, err := r.requestJSON(http.MethodPut, api.User, api.UserCreatePayload{ - Username: username, - Password: password, - Privileged: privileged, - }); err != nil { - return store.User{}, err - } else { - var user store.User - err = unmarshal(resp.Body, &user) - return user, err - } -} - -// UserUpdate updates a user. -func (r *Remote) UserUpdate(flake, newname string, privileged bool) error { - return r.requestJSONnoResp(http.MethodPatch, - populateField(api.UserField, "flake", flake), - api.UserUpdatePayload{Username: newname, Privileged: privileged}) -} - -// UserDestroy destroys a user. -func (r *Remote) UserDestroy(flake string) error { - return r.requestJSONnoResp(http.MethodDelete, - populateField(api.UserField, "flake", flake), - nil) -} - -// UserDisable disables a user. -func (r *Remote) UserDisable(flake string) error { - return r.requestJSONnoResp(http.MethodDelete, - populateField(api.UserPassword, "flake", flake), - nil) -} - -// UserPassword sets a user's password. -func (r *Remote) UserPassword(flake, password string) (string, error) { - if resp, err := r.request(http.MethodPut, - populateField(api.UserPassword, "flake", flake), - strings.NewReader(password)); err != nil { - return "", err - } else { - var payload api.UserSecretPayload - err = unmarshal(resp.Body, &payload) - return payload.Secret, err - } -} - -// UserSecret returns a user's secret. -func (r *Remote) UserSecret(flake string) (string, error) { - var secret api.UserSecretPayload - err := r.fetch(http.MethodGet, populateField(api.UserSecret, "flake", flake), &secret, nil) - return secret.Secret, err -} - -// UserSecretRegen regenerates a user's secret. -func (r *Remote) UserSecretRegen(flake string) (string, error) { - var secret api.UserSecretPayload - err := r.fetch(http.MethodPut, populateField(api.UserSecret, "flake", flake), &secret, nil) - return secret.Secret, err -} - -// UserImages returns a slice of snowflakes of a user's images. -func (r *Remote) UserImages(flake string) ([]string, error) { - var flakes []string - err := r.fetch(http.MethodGet, populateField(api.UserImage, "flake", flake), &flakes, nil) - return flakes, err -} diff --git a/config.go b/config.go index b064e143718d5d7734d267bafe7ff39bd1553c51..fe336098849cf7ab8d8e48d187dc95df7cee2f68 100644 --- a/config.go +++ b/config.go @@ -1,99 +1,81 @@ package main import ( - "github.com/fsnotify/fsnotify" - log "github.com/sirupsen/logrus" - "github.com/spf13/viper" - "strconv" + "flag" + "fmt" + "github.com/BurntSushi/toml" + "log" + "os" ) +type conf struct { + System systemConf `toml:"system"` + Server serverConf `toml:"server"` +} + +type serverConf struct { + Host string `toml:"host"` + Unix bool `toml:"unix"` + Port uint16 `toml:"port"` + Proxy bool `toml:"proxy"` +} + +type systemConf struct { + Verbose bool `toml:"verbose"` + Store string `toml:"store"` + SingleUser bool `toml:"single-user"` + Private bool `toml:"private"` + Register bool `toml:"register"` +} + var ( - serverConfig map[string]interface{} - systemConfig map[string]interface{} + config conf + confPath string + confRead = false ) -func configSetup() { - // Configure configuration file parameters - viper.SetConfigName("server") - viper.SetConfigType("toml") - viper.SetConfigPermissions(0600) - viper.AddConfigPath(".") - viper.AddConfigPath("/etc/imageboard/") - viper.AddConfigPath("$HOME/.config/imageboard/") - - // Configure default values - viper.SetDefault("system", map[string]interface{}{ - "loglevel": "info", - "store": "./db", - "single-user": true, - "private": false, - }) - viper.SetDefault("server", map[string]interface{}{ - "host": "127.0.0.1", - "port": int64(7777), - "unix": false, - "proxy": true, - }) +func init() { + flag.StringVar(&confPath, "c", "server.conf", "specify path to configuration file") +} - // Load configuration - log.Info("Loading configuration file.") - err := viper.ReadInConfig() - if err != nil { - if _, ok := err.(viper.ConfigFileNotFoundError); ok { - err = viper.WriteConfigAs("server.toml") - if err != nil { - log.Fatalf("Error generating default configuration, %s", err) - } - log.Warn("Generated default server.toml in current directory.") - } else { - log.Fatalf("Error loading configuration, %s", err) - } +func confLoad() { + if confRead { + panic("configuration read called when already read") } + defer func() { confRead = true }() - if viper.ConfigFileUsed() != "" { - log.Infof("Successfully loaded configuration %s, enabling watch.", viper.ConfigFileUsed()) - } - viper.WatchConfig() - viper.OnConfigChange(func(event fsnotify.Event) { - if d { - return - } - log.Infof("Configuration file %s updated.", event.Name) - if viper.GetStringMap("server")["unix"] != serverConfig["unix"] || - viper.GetStringMap("server")["host"] != serverConfig["host"] || - viper.GetStringMap("server")["port"] != serverConfig["port"] || - viper.GetStringMap("system")["single-user"] != systemConfig["single-user"] || - viper.GetStringMap("system")["private"] != systemConfig["private"] { - log.Warn("Configuration change requires restart.") - cleanup(true) - return + if meta, err := toml.DecodeFile(confPath, &config); err != nil { + if !os.IsNotExist(err) { + log.Fatalf("error parsing configuration: %s", err) } - // Update configuration - setLevel() - serverConfig = viper.GetStringMap("server") - systemConfig = viper.GetStringMap("system") - }) - - // Read initial config - serverConfig = viper.GetStringMap("server") - systemConfig = viper.GetStringMap("system") -} - -func parseBool(v interface{}) bool { - if s, ok := v.(bool); !ok { - var sS string - if sS, ok = v.(string); !ok { - return false - } else { - if b, err := strconv.ParseBool(sS); err != nil { - log.Warnf("Error parsing boolean value, %s", err) - return false - } else { - return b - } + var file *os.File + if file, err = os.Create(confPath); err != nil { + log.Fatalf("error creating configuration file: %s", err) + } else if err = toml.NewEncoder(file).Encode(defConf); err != nil { + log.Fatalf("error generating default configuration: %s", err) } + config = defConf + return } else { - return s + for _, key := range meta.Undecoded() { + fmt.Printf("unused key in configuration file: %s", key.String()) + } } } + +var defConf = conf{ + System: systemConf{ + Verbose: false, + Store: "db", + SingleUser: true, + Private: false, + Register: false, + }, + Server: serverConf{ + Host: "127.0.0.1", + Unix: false, + Port: 7777, + Proxy: true, + }, +} diff --git a/go.mod b/go.mod index d395498736584a2761e22aa8c88e23ea0c0c6819..f5a549b54a379622ebc203fb0c09d2fc1a525379 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,31 @@ -module random.chars.jp/git/image-board +module random.chars.jp/git/image-board/v2 -go 1.16 +go 1.17 require ( + github.com/BurntSushi/toml v0.3.1 github.com/bwmarrin/snowflake v0.3.0 - github.com/fsnotify/fsnotify v1.4.9 github.com/gin-gonic/gin v1.7.4 - github.com/gopherjs/gopherjs v0.0.0-20210901121439-eee08aaf2717 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 - github.com/sirupsen/logrus v1.8.1 - github.com/spf13/afero v1.1.2 // indirect - github.com/spf13/viper v1.7.1 github.com/syndtr/goleveldb v1.0.0 ) + +require ( + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.13.0 // indirect + github.com/go-playground/universal-translator v0.17.0 // indirect + github.com/go-playground/validator/v10 v10.4.1 // indirect + github.com/golang/protobuf v1.3.3 // indirect + github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect + github.com/json-iterator/go v1.1.9 // indirect + github.com/kr/pretty v0.1.0 // indirect + github.com/leodido/go-urn v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/ugorji/go/codec v1.1.7 // indirect + golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect + golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum index 75642a416534a19a50a964d170ef48988ddcf1e2..36db0f865a991c68d20ddcf8ca77bb26686ea998 100644 --- a/go.sum +++ b/go.sum @@ -1,430 +1,98 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA= -github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM= github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/go-playground/validator/v10 v10.6.1 h1:W6TRDXt4WcWp4c4nf/G+6BkGdhiIo0k417gfr+V6u4I= -github.com/go-playground/validator/v10 v10.6.1/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v0.0.0-20210901121439-eee08aaf2717 h1:V1j4G8AXIJeyzT3ng2Oh4IRo/VEgRWYAsyYwhOz5rko= -github.com/gopherjs/gopherjs v0.0.0-20210901121439-eee08aaf2717/go.mod h1:0RnbP5ioI0nqRf3R9iK3iQaUJgsn0htlZEHCMn8FSfw= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= -github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= -github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 h1:D6paGObi5Wud7xg83MaEFyjxQB1W5bz5d0IFppr+ymk= -github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= -github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c h1:bY6ktFuJkt+ZXkX0RChQch2FtHpWQLVS8Qo1YasiIVk= -github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk= -github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= -github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= -github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= -github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E= -github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ= -github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 h1:C+AwYEtBp/VQwoLntUmQ/yx3MS9vmZaKNdw5eOpoQe8= -golang.org/x/sys v0.0.0-20210608053332-aa57babbf139/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/log.go b/log.go deleted file mode 100644 index 47b27c46e2338ff08c0892c56efe963680d33a02..0000000000000000000000000000000000000000 --- a/log.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - log "github.com/sirupsen/logrus" - "github.com/spf13/viper" - "go/types" -) - -type logger types.Nil - -func (logger) Write(p []byte) (n int, err error) { - log.Info(string(p)) - return len(p), err -} - -func setFormatter() { - log.SetFormatter(&log.TextFormatter{ - ForceColors: false, - DisableColors: false, - ForceQuote: false, - DisableQuote: false, - EnvironmentOverrideColors: false, - DisableTimestamp: false, - FullTimestamp: true, - TimestampFormat: "", - DisableSorting: true, - SortingFunc: nil, - DisableLevelTruncation: false, - PadLevelText: false, - QuoteEmptyFields: false, - FieldMap: nil, - CallerPrettyfier: nil, - }) -} - -func setLevel() { - level, err := log.ParseLevel(viper.GetStringMap("system")["loglevel"].(string)) - if err != nil { - log.Fatalf("Error parsing log level, %s", err) - } - log.SetLevel(level) -} diff --git a/main.go b/main.go index febca53400d862903d550783822342e4ed5a52f7..2a1a46b57e6446cc831fa6c3d9e3aa095bf50fbb 100644 --- a/main.go +++ b/main.go @@ -1,108 +1,77 @@ package main import ( - log "github.com/sirupsen/logrus" - "net/http" + "flag" + "log" + "math/rand" "os" "os/signal" - "random.chars.jp/git/image-board/store" + "random.chars.jp/git/image-board/v2/backend/filesystem" + "random.chars.jp/git/image-board/v2/store" "syscall" + "time" ) -var ( - instance *store.Store - server = http.Server{} - executable string -) - -var ( - r bool - d bool -) +var instance store.Store func init() { - setFormatter() - var err error - executable, err = os.Executable() - if err != nil { - log.Warnf("Error while obtaining executable path, %s. Restarting will no longer work.", err) - } + rand.Seed(time.Now().UnixNano()) } func main() { + flag.Parse() - // Setup configuration stuff - configSetup() + confLoad() - // Initial config update - setLevel() + // TODO: support more backends + instance = filesystem.New(config.System.Store, config.System.Verbose) + if err := instance.Open(); err != nil { + log.Printf("error opening store: %s", err) + return + } else { + log.Printf("store path %s revision %v compat %v", + config.System.Store, instance.(*filesystem.Store).Revision, instance.(*filesystem.Store).Compat) + } - // Open store - openStore() + if info, err := instance.User(instance.UserInitial()); err == nil { + log.Printf("initial user ID %s secret %s.", info.Snowflake, info.Secret) - // Set up web - webSetup() - - // Configure listener - listenerSetup() + var p bool + if p, err = instance.UserPasswordValidate(&info.Snowflake, nil, store.InitialPassword); err != nil { + log.Fatalf("error validating password of initial user: %s", err) + } else if p { + log.Printf("warning: initial user still has the password \"%s\"", store.InitialPassword) + } + } else { + if config.System.SingleUser { + log.Fatal("no initial user found, single user mode unavailable") + } + } + if config.System.SingleUser { + log.Print("server running single-user, all operations performed as initial user") + } else if config.System.Private { + log.Print("server in private mode, all operations require authentication") + } - // Register API and web handlers - registerAPI() - registerWebpage() + webSetup() - // Signal handling - signalChannel := make(chan os.Signal, 1) - signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, os.Interrupt, os.Kill) + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) go func() { - // Cleanup on function return - defer func() { cleanup(false) }() + defer func() { cleanup() }() for { - currentSignal := <-signalChannel - switch currentSignal { + s := <-sig + switch s { case os.Interrupt: println() - log.Info("Gracefully exiting.") + log.Print("shutting down") return default: - log.Info("Gracefully exiting.") + log.Print("shutting down") return } } }() - // Start server - runWebServer() - - // Restart if needed - if r { - restart() - } -} - -func openStore() { - path := systemConfig["store"].(string) - single := parseBool(systemConfig["single-user"]) - private := parseBool(systemConfig["private"]) - - instance = store.New(path, single, private) - if instance == nil { - log.Fatalf("Error initializing store.") - } - log.Infof("Store opened on %s revision %v compat %v.", path, instance.Revision, instance.Compat) - info := instance.User(instance.InitialUser) - if info.Snowflake == instance.InitialUser { - log.Infof("Initial user ID %s secret %s.", info.Snowflake, info.Secret) - if instance.UserPasswordValidate(info.Snowflake, "initial") { - log.Warnf("Initial user still has the initial password.") - } - } else { - if single { - log.Fatal("Instance has no initial user, single user mode unavailable.") - } - } - if single { - log.Info("Server running in single user mode, all operations are performed as the initial user.") - } else if private { - log.Info("Server running in private mode, all operations will require authentication.") - } + serve() } diff --git a/recover.go b/recover.go index d84c79ab123744422dc76f017c3a7fff1b3a6e2f..eafd63e2ee3b856b8151703168c90160a1b7f0a3 100644 --- a/recover.go +++ b/recover.go @@ -1,8 +1,9 @@ package main import ( + "fmt" "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" + "log" "net/http" "runtime/debug" ) @@ -12,11 +13,11 @@ func recovery() gin.HandlerFunc { defer func() { p := recover() if p != nil { - log.Errorf("Panic occurred in web server, %s", p) + log.Printf("panic in web server %s", p) context.JSON(http.StatusInternalServerError, gin.H{ "error": "panic", }) - log.Error(string(debug.Stack())) + fmt.Println(string(debug.Stack())) } }() context.Next() diff --git a/restart.go b/restart.go deleted file mode 100644 index 8b2ec793246c62d049ececd536903c5fdc5e4a86..0000000000000000000000000000000000000000 --- a/restart.go +++ /dev/null @@ -1,23 +0,0 @@ -// +build !windows - -package main - -import ( - log "github.com/sirupsen/logrus" - "os" - "syscall" -) - -func restart() { - var err error - - if _, err = os.Stat(executable); err != nil { - log.Fatalf("Error stat executable path, %s", err) - } - - log.Infof("Re-executing %s...", executable) - err = syscall.Exec(executable, os.Args, os.Environ()) - if err != nil { - log.Fatalf("Error re-executing, %s", err) - } -} diff --git a/restart_windows.go b/restart_windows.go deleted file mode 100644 index 59239fb7d28af61c18335264828b68b457e1f0ea..0000000000000000000000000000000000000000 --- a/restart_windows.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - log "github.com/sirupsen/logrus" - "os" -) - -func restart() { - if _, err := os.Stat(executable); err != nil { - log.Fatalf("Error getting executable path, %s", err) - } - log.Infof("Program found at %s.", executable) - wd, err := os.Getwd() - if err != nil { - log.Fatalf("Error getting working directory, %s", err) - } - log.Infof("Current working directory is %s.", wd) - _, err = os.StartProcess(executable, []string{}, &os.ProcAttr{ - Dir: wd, - Env: nil, - Files: []*os.File{os.Stderr, os.Stdin, os.Stdout}, - Sys: nil, - }) - if err != nil { - log.Fatalf("Error creating new process, %s", err) - } -} diff --git a/store/flake.go b/store/flake.go new file mode 100644 index 0000000000000000000000000000000000000000..53528eb54cf2325a8c176a0ff782145cf6c48424 --- /dev/null +++ b/store/flake.go @@ -0,0 +1,59 @@ +package store + +import ( + "github.com/bwmarrin/snowflake" + "log" + "math/rand" +) + +const ( + ImageNode = iota + UserNode + PostNode + EventNode +) + +var flakeNodes [4][256]*snowflake.Node + +func init() { + snowflake.Epoch = 0 + + for i := 0; i < 256; i++ { + offset := i * 4 + + if n, err := snowflake.NewNode(int64(offset + ImageNode)); err != nil { + log.Fatalf("error creating image snowflake node, %s", err) + } else { + flakeNodes[ImageNode][i] = n + } + if n, err := snowflake.NewNode(int64(offset + UserNode)); err != nil { + log.Fatalf("error creating user snowflake node, %s", err) + } else { + flakeNodes[UserNode][i] = n + } + if n, err := snowflake.NewNode(int64(offset + PostNode)); err != nil { + log.Fatalf("error creating post snowflake node, %s", err) + } else { + flakeNodes[PostNode][i] = n + } + if n, err := snowflake.NewNode(int64(offset + EventNode)); err != nil { + log.Fatalf("error creating event snowflake node, %s", err) + } else { + flakeNodes[EventNode][i] = n + } + } +} + +// MakeFlake makes a flake of specified type. +func MakeFlake(t int) snowflake.ID { + return flakeNodes[t][rand.Intn(256)].Generate() +} + +// FlakeType returns the type of the snowflake. +func FlakeType(flake string) (int, error) { + if id, err := snowflake.ParseString(flake); err != nil { + return 0, err + } else { + return int(id.Node() % 4), nil + } +} diff --git a/store/image.go b/store/image.go deleted file mode 100644 index 4bd360fb52822d32044cf238304502bf6ce4a04b..0000000000000000000000000000000000000000 --- a/store/image.go +++ /dev/null @@ -1,514 +0,0 @@ -package store - -import ( - "bytes" - "crypto/sha256" - "encoding/json" - "fmt" - "github.com/nfnt/resize" - log "github.com/sirupsen/logrus" - "image" - _ "image/gif" - "image/jpeg" - _ "image/png" - "os" -) - -const ImageRootPageVariant = "root" - -// Image represents metadata of an image. -type Image struct { - Snowflake string `json:"snowflake"` - Hash string `json:"hash"` - Type string `json:"type"` - User string `json:"user"` - Source string `json:"source"` - Parent string `json:"parent"` - Child string `json:"child"` - Commentary string `json:"commentary"` - CommentaryTranslation string `json:"commentary_translation"` -} - -// MakePreview compresses an image.Image to preview-size. -func MakePreview(img image.Image) image.Image { - return resize.Thumbnail(256, 256, img, resize.Bilinear) -} - -// Images returns a slice of image hashes. -func (s *Store) Images() []string { - var images []string - if entries, err := os.ReadDir(s.ImagesDir()); err != nil { - s.fatalClose(fmt.Sprintf("Error reading first level image directory, %s", err)) - } else { - for _, entry := range entries { - if entry.IsDir() { - var subEntries []os.DirEntry - if subEntries, err = os.ReadDir(s.ImagesDir() + "/" + entry.Name()); err != nil { - s.fatalClose(fmt.Sprintf("Error reading second level image directory %s, %s", entry.Name(), err)) - } else { - for _, subEntry := range subEntries { - images = append(images, entry.Name()+subEntry.Name()) - } - } - } - } - } - return images -} - -// Image returns an image with specific hash. -func (s *Store) Image(hash string) Image { - if !sha256Regex.MatchString(hash) || !s.file(s.ImageMetadataPath(hash)) { - return Image{} - } - - s.getLock(hash).RLock() - defer s.getLock(hash).RUnlock() - return s.ImageMetadataRead(s.ImageMetadataPath(hash)) -} - -// ImageMetadataRead reads an image metadata file. -func (s *Store) ImageMetadataRead(path string) Image { - var metadata Image - if payload, err := os.ReadFile(path); err != nil { - if os.IsNotExist(err) { - return Image{} - } - s.fatalClose(fmt.Sprintf("Error reading image metadata %s, %s", path, err)) - } else { - if err = json.Unmarshal(payload, &metadata); err != nil { - s.fatalClose(fmt.Sprintf("Error parsing image metadata %s, %s", path, err)) - } - } - return metadata -} - -// ImageData returns an image and its data with a specific hash. -func (s *Store) ImageData(hash string, preview bool) (Image, []byte) { - if !sha256Regex.MatchString(hash) || !s.file(s.ImageMetadataPath(hash)) { - return Image{}, nil - } - - s.getLock(hash).RLock() - defer s.getLock(hash).RUnlock() - var metadata Image - if payload, err := os.ReadFile(s.ImageMetadataPath(hash)); err != nil { - s.fatalClose(fmt.Sprintf("Error reading image %s metadata, %s", hash, err)) - } else { - if err = json.Unmarshal(payload, &metadata); err != nil { - s.fatalClose(fmt.Sprintf("Error parsing image %s metadata, %s", hash, err)) - } - } - var path string - if !preview { - path = s.ImageFilePath(hash) - } else { - path = s.ImagePreviewFilePath(hash) - } - if data, err := os.ReadFile(path); err != nil { - s.fatalClose(fmt.Sprintf("Error reading image %s file, %s", hash, err)) - } else { - return metadata, data - } - return Image{}, nil -} - -// ImageTags returns tags of an image with specific hash. -func (s *Store) ImageTags(flake string) []string { - if !numerical(flake) || !s.dir(s.ImageTagsPath(flake)) { - return nil - } - - s.getLock(flake).RLock() - defer s.getLock(flake).RUnlock() - return s.imageTags(flake) -} - -func (s *Store) imageTags(flake string) []string { - var tags []string - if entries, err := os.ReadDir(s.ImageTagsPath(flake)); err != nil { - s.fatalClose(fmt.Sprintf("Error reading tags of image %s, %s", flake, err)) - } else { - for _, entry := range entries { - tags = append(tags, entry.Name()) - } - } - return tags -} - -// ImageHasTag figures out if an image has a tag. -func (s *Store) ImageHasTag(flake, tag string) bool { - if !numerical(flake) || !nameRegex.MatchString(tag) { - return false - } - return s.file(s.ImageTagsPath(flake) + "/" + tag) -} - -//ImageSearch searches for images with specific tags. -func (s *Store) ImageSearch(tags []string) []string { - if len(tags) < 1 || tags == nil { - return nil - } - - // Check if every tag matches name regex and exists - for _, tag := range tags { - if !nameRegex.MatchString(tag) || !s.file(s.TagPath(tag)) { - return nil - } - } - - // Return if there's only one tag to search for - if len(tags) == 1 { - return s.Tag(tags[0]) - } - - // Find entry with the least pages - entry := struct { - min int - index int - }{ - min: s.PageTotal("tag_" + tags[0]), - index: 0, - } - for i := 1; i < len(tags); i++ { - if entry.min <= 1 { - break - } - - pages := s.PageTotal("tag_" + tags[i]) - if pages < entry.min { - entry.min = pages - entry.index = i - } - } - - // Get initial tag - initial := s.Tag(tags[entry.index]) - - // Result slice - var result []string - - // Walk flakes from initial tag - for _, flake := range initial { - match := true - // Walk all remaining tags - for i, tag := range tags { - // Skip the entrypoint entry - if i == entry.index { - continue - } - - // Check if match - if !s.ImageHasTag(flake, tag) { - match = false - break - } - } - - // Append flake if all tags matched - if match { - result = append(result, flake) - } - } - - return result -} - -// ImageAdd adds an image to the store. -func (s *Store) ImageAdd(data []byte, flake string) Image { - if !numerical(flake) || !s.dir(s.UserPath(flake)) { - return Image{} - } - - // Create image info and set time - info := Image{Snowflake: imageNode.Generate().String(), User: flake} - - // Calculate sha256 and convert to string - info.Hash = fmt.Sprintf("%x", sha256.Sum256(data)) - - // Return existing image if already exists. - e := s.Image(info.Hash) - if e.Hash == info.Hash { - return e - } - - s.getLock(info.Hash).Lock() - defer s.getLock(info.Hash).Unlock() - - // Decode image and set format - var img image.Image - if i, format, err := image.Decode(bytes.NewReader(data)); err != nil { - log.Warnf("Error decoding upload %s, %s", info.Hash, err) - return Image{} - } else { - img = MakePreview(i) - info.Type = format - } - - // Create image directory and tags - if err := os.MkdirAll(s.ImageHashTagsPath(info.Hash), s.PermissionDir); err != nil { - s.fatalClose(fmt.Sprintf("Error creating image %s directory, %s", info.Hash, err)) - } - - // Generate and save image metadata - if payload, err := json.Marshal(info); err != nil { - s.fatalClose(fmt.Sprintf("Error encoding metadata of image %s, %s", info.Hash, err)) - } else { - if err = os.WriteFile(s.ImageMetadataPath(info.Hash), payload, s.PermissionFile); err != nil { - s.fatalClose(fmt.Sprintf("Error saving metadata of image %s, %s", info.Hash, err)) - } - } - - // Save image - if err := os.WriteFile(s.ImageFilePath(info.Hash), data, s.PermissionFile); err != nil { - s.fatalClose(fmt.Sprintf("Error saving image %s, %s", info.Hash, err)) - } - - // Save preview image - if preview, err := os.Create(s.ImagePreviewFilePath(info.Hash)); err != nil { - s.fatalClose(fmt.Sprintf("Error creating image %s preview, %s", info.Hash, err)) - } else { - if err = jpeg.Encode(preview, img, &jpeg.Options{Quality: 100}); err != nil { - s.fatalClose(fmt.Sprintf("Error saving image %s preview, %s", info.Hash, err)) - } - if err = preview.Close(); err != nil { - s.fatalClose(fmt.Sprintf("Error closing image %s preview, %s", info.Hash, err)) - } - } - - // Symbolically link image directory to snowflake directory - s.link("../images/"+s.ImageHashSplit(info.Hash), s.ImageSnowflakePath(info.Snowflake)) - s.link("../../../images/"+s.ImageHashSplit(info.Hash), s.UserImagesPath(flake)+"/"+info.Snowflake) - - // Insert image into page index - go s.PageInsert(ImageRootPageVariant, info.Snowflake) - - log.Infof("Image hash %s snowflake %s type %s added by user %s.", info.Hash, info.Snowflake, info.Type, info.User) - return info -} - -// ImageUpdate updates image metadata. -func (s *Store) ImageUpdate(hash, source, parent, commentary, commentaryTranslation string) { - // Only accept URLs and below 1024 in length - if len(source) >= 1024 { - return - } - - // Only accept commentary and translations below 65536 in length - if len(commentary) >= 65536 || len(commentaryTranslation) >= 65536 { - return - } - - // Get info - info := s.Image(hash) - if info.Hash != hash { - return - } - - s.getLock(hash).Lock() - defer s.getLock(hash).Unlock() - - var msg string - - // Update and save - if source != "\000" && s.MatchURL(source) { - info.Source = source - msg += "source" - } - - if parent != "\000" && parent != info.Snowflake { - if p := s.ImageSnowflake(parent); p.Snowflake == parent { - // If no parent, then get the current parent and unset - if parent == "" { - p = s.ImageSnowflake(info.Parent) - // If no current parent, nothing to do - if p.Snowflake == "" { - goto end - } - } else { - // If setting parent but parent has child, reject - if p.Child != "" { - goto end - } - } - - info.Parent = parent - - // Update the parent to reflect the child - p.Child = info.Snowflake - if parent == "" { - p.Child = "" - } - s.getLock(p.Hash).Lock() - s.imageMetadataWrite(p) - s.getLock(p.Hash).Unlock() - - if msg != "" { - msg += ", " - } - msg += "parent " + parent - } - end: - } - if commentary != "\000" { - info.Commentary = commentary - - if msg != "" { - msg += ", " - } - msg += "commentary" - } - if commentaryTranslation != "\000" { - info.CommentaryTranslation = commentaryTranslation - - if msg != "" { - msg += ", " - } - msg += "commentary translation" - } - - if msg != "" { - s.imageMetadataWrite(info) - log.Infof("Image %s %s updated.", info.Snowflake, msg) - } -} - -func (s *Store) imageMetadataWrite(info Image) { - if payload, err := json.Marshal(info); err != nil { - s.fatalClose(fmt.Sprintf("Error encoding metadata of image %s, %s", info.Hash, err)) - } else { - if err = os.WriteFile(s.ImageMetadataPath(info.Hash), payload, s.PermissionFile); err != nil { - s.fatalClose(fmt.Sprintf("Error saving metadata of image %s, %s", info.Hash, err)) - } - } -} - -// ImageSnowflakes returns a slice of image snowflakes. -func (s *Store) ImageSnowflakes() []string { - var snowflakes []string - if entries, err := os.ReadDir(s.ImagesSnowflakeDir()); err != nil { - s.fatalClose(fmt.Sprintf("Error reading snowflakes, %s", err)) - } else { - for _, entry := range entries { - snowflakes = append(snowflakes, entry.Name()) - } - } - return snowflakes -} - -// ImageSnowflakeHash returns image hash from snowflake. -func (s *Store) ImageSnowflakeHash(flake string) string { - if !numerical(flake) { - return "" - } - - if !s.Compat { - return s.ImageMetadataRead(s.ImageSnowflakePath(flake) + "/" + infoJson).Hash - } else { - if path, err := os.ReadFile(s.ImageSnowflakePath(flake)); err != nil { - if os.IsNotExist(err) { - return "" - } - s.fatalClose(fmt.Sprintf("Error reading snowflake %s association file, %s", flake, err)) - } else { - return s.ImageMetadataRead(string(path) + "/" + infoJson).Hash - } - } - return "" -} - -// ImageSnowflake returns image that has specific snowflake. -func (s *Store) ImageSnowflake(flake string) Image { - return s.Image(s.ImageSnowflakeHash(flake)) -} - -// ImageDestroy destroys an image. -func (s *Store) ImageDestroy(hash string) { - if !sha256Regex.MatchString(hash) || !s.dir(s.ImagePath(hash)) { - return - } - - // Attempt to disassociate parent - s.ImageUpdate(hash, "\000", "", "\000", "\000") - - s.getLock(hash).Lock() - defer s.getLock(hash).Unlock() - - info := s.ImageMetadataRead(s.ImageMetadataPath(hash)) - - // Disassociate child if set - if info.Child != "" { - if child := s.ImageSnowflake(info.Child); child.Snowflake == info.Child { - s.ImageUpdate(child.Hash, "\000", "", "\000", "\000") - } - } - - // Untag the image completely - tags := s.imageTags(info.Snowflake) - for _, tag := range tags { - s.imageTagRemove(info.Snowflake, tag) - } - - // Remove snowflake - if err := os.Remove(s.ImageSnowflakePath(info.Snowflake)); err != nil { - s.fatalClose(fmt.Sprintf("Error destroying snowflake %s of image %s, %s", info.Snowflake, hash, err)) - } - - // Disassociate user - if err := os.Remove(s.UserImagesPath(info.User) + "/" + info.Snowflake); err != nil { - s.fatalClose(fmt.Sprintf("Error destroying association %s with user %s, %s.", info.Snowflake, info.User, err)) - } - - // Remove data directory - if err := os.RemoveAll(s.ImagePath(hash)); err != nil { - s.fatalClose(fmt.Sprintf("Error destroying image %s, %s", hash, err)) - } - - // Register remove counter - s.PageRegisterRemove(ImageRootPageVariant, info.Snowflake) - - log.Infof("Image hash %s snowflake %s destroyed.", info.Hash, info.Snowflake) -} - -// ImageTagAdd adds a tag to an image with specific snowflake. -func (s *Store) ImageTagAdd(flake, tag string) { - if !nameRegex.MatchString(tag) || !numerical(flake) || !s.dir(s.ImageTagsPath(flake)) || !s.dir(s.TagPath(tag)) || - s.file(s.TagPath(tag)+"/"+flake) { - return - } - - s.getLock(flake).Lock() - defer s.getLock(flake).Unlock() - - s.link("../../snowflakes/"+flake, s.TagPath(tag)+"/"+flake) - s.link("../../../../tags/"+tag, s.ImageSnowflakePath(flake)+"/tags/"+tag) - s.PageInsert("tag_"+tag, flake) - log.Infof("Image snowflake %s tagged with %s.", flake, tag) -} - -// ImageTagRemove removes a tag from an image with specific snowflake. -func (s *Store) ImageTagRemove(flake, tag string) { - if !nameRegex.MatchString(tag) || !numerical(flake) || !s.dir(s.ImageTagsPath(flake)) || !s.dir(s.TagPath(tag)) { - return - } - - s.getLock(flake).Lock() - defer s.getLock(flake).Unlock() - - s.imageTagRemove(flake, tag) -} - -func (s *Store) imageTagRemove(flake, tag string) { - if s.file(s.ImageTagsPath(flake) + "/" + tag) { - if err := os.Remove(s.ImageTagsPath(flake) + "/" + tag); err != nil { - s.fatalClose(fmt.Sprintf("Error unreferencing image %s from tag %s, %s", flake, tag, err)) - } - } - if s.file(s.TagPath(tag) + "/" + flake) { - if err := os.Remove(s.TagPath(tag) + "/" + flake); err != nil { - s.fatalClose(fmt.Sprintf("Error unreferencing tag %s from image %s, %s", tag, flake, err)) - } - } - s.PageRegisterRemove("tag_"+tag, flake) - log.Infof("Image snowflake %s untagged %s.", flake, tag) -} diff --git a/store/misc.go b/store/misc.go deleted file mode 100644 index 38a1c47ece99a0d9c03a83576291f7702b07ba07..0000000000000000000000000000000000000000 --- a/store/misc.go +++ /dev/null @@ -1,41 +0,0 @@ -package store - -import ( - "errors" - "net/url" - "regexp" - "strconv" -) - -const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - -var ( - nameRegex = regexp.MustCompile(`^[a-z0-9()_-]{3,}$`) - sha256Regex = regexp.MustCompile(`\b[A-Fa-f0-9]{64}\b`) - secretRegex = regexp.MustCompile(`\b[A-Za-z]{64}\b`) -) - -var ( - // AlreadyExists is returned when store already exists. - AlreadyExists = errors.New("store path already exists") -) - -// numerical validates a numerical string. -func numerical(flake string) bool { - if flake == "" { - return false - } - _, err := strconv.Atoi(flake) - return err == nil -} - -// MatchName determines if str is a valid name. -func MatchName(str string) bool { - return nameRegex.MatchString(str) -} - -// MatchURL determines if str is a valid URL. -func MatchURL(str string) bool { - u, err := url.Parse(str) - return err == nil && u.Scheme != "" && u.Host != "" -} diff --git a/store/page.go b/store/page.go deleted file mode 100644 index a247e7017c49300161800ac10e15e3ec30c8786d..0000000000000000000000000000000000000000 --- a/store/page.go +++ /dev/null @@ -1,183 +0,0 @@ -package store - -import ( - "encoding/binary" - "fmt" - log "github.com/sirupsen/logrus" - "github.com/syndtr/goleveldb/leveldb" - "os" -) - -const PageSize = 64 - -// pageDB returns leveldb of page variant and creates it as required. -func (s *Store) pageDB(variant string) *leveldb.DB { - mutex := s.getLock("pageDB_get") - mutex.Lock() - defer mutex.Unlock() - - if ldb := s.pageldb[variant]; ldb != nil { - return ldb - } else { - if db, err := leveldb.OpenFile(s.PageVariantPath(variant), nil); err != nil { - s.fatalClose(fmt.Sprintf("Error opening leveldb for page variant %s, %s", variant, err)) - } else { - s.pageldb[variant] = db - if _, err = db.Get([]byte("\000"), nil); err != nil { - log.Infof("Page variant %s created.", variant) - s.pageSetTotalCountNoDestroy(variant, 0, db) - } - return db - } - } - return nil -} - -// pageDBDestroy destroys leveldb of page variant. -func (s *Store) pageDBDestroy(variant string) { - if err := s.pageDB(variant).Close(); err != nil { - s.fatalClose(fmt.Sprintf("Error closing leveldb for page variant %s, %s", variant, err)) - } else { - delete(s.pageldb, variant) - } - - if err := os.RemoveAll(s.PageVariantPath(variant)); err != nil { - s.fatalClose(fmt.Sprintf("Error destroying page variant %s, %s", variant, err)) - } - - log.Infof("Page variant %s destroyed.", variant) -} - -// pageGetTotalCount gets total count of a page variant. -func (s *Store) pageGetTotalCount(variant string) uint64 { - db := s.pageDB(variant) - - if payload, err := db.Get([]byte("\000"), nil); err != nil { - s.fatalClose(fmt.Sprintf("Error getting page variant %s total count, %s", variant, err)) - } else { - return binary.LittleEndian.Uint64(payload) - } - - return 0 -} - -// pageSetTotalCountNoDestroy sets total count of a page variant. -func (s *Store) pageSetTotalCountNoDestroy(variant string, value uint64, db *leveldb.DB) { - payload := make([]byte, 8) - binary.LittleEndian.PutUint64(payload, value) - - if err := db.Put([]byte("\000"), payload, nil); err != nil { - s.fatalClose(fmt.Sprintf("Error setting page variant %s total count, %s", variant, err)) - } -} - -// pageSetTotalCount sets total count of a page variant and destroys it if zero. -func (s *Store) pageSetTotalCount(variant string, value uint64) { - if value == 0 { - s.pageDBDestroy(variant) - return - } - s.pageSetTotalCountNoDestroy(variant, value, s.pageDB(variant)) -} - -// pageAdvanceTotalCount advances total count of a page variant. -func (s *Store) pageAdvanceTotalCount(variant string) { - s.pageSetTotalCount(variant, s.pageGetTotalCount(variant)+1) -} - -// pageReduceTotalCount reduces total count of a page variant. -func (s *Store) pageReduceTotalCount(variant string) { - if total := s.pageGetTotalCount(variant); total == 0 { - return - } else { - s.pageSetTotalCount(variant, total-1) - } -} - -// PageTotal returns total amount of pages. -func (s *Store) PageTotal(variant string) int { - totalCount := int(s.pageGetTotalCount(variant)) - if totalCount == 0 { - return 0 - } - return (totalCount / PageSize) + 1 -} - -// Page returns all entries in a page. -func (s *Store) Page(variant string, entry int) []string { - if entry >= s.PageTotal(variant) { - return nil - } - - var page []string - start := entry * PageSize - end := start + PageSize - begin := false - - db := s.pageDB(variant) - - iter := db.NewIterator(nil, nil) - i := 0 - for iter.Next() { - if i == end { - break - } - if begin { - page = append(page, string(iter.Key())) - } else { - if i >= start { - begin = true - } - } - i++ - } - iter.Release() - if err := iter.Error(); err != nil { - log.Warnf("Error iterating page variant %s entry %v, %s", variant, entry, err) - return nil - } - - return page -} - -// PageImages returns all images in a page. -func (s *Store) PageImages(variant string, entry int) []Image { - flakes := s.Page(variant, entry) - if flakes == nil { - return nil - } - - images := make([]Image, len(flakes)) - for i, flake := range flakes { - images[i] = s.ImageSnowflake(flake) - } - return images -} - -// PageInsert inserts an image into the index. -func (s *Store) PageInsert(variant, flake string) { - if !s.dir(s.ImageSnowflakePath(flake)) { - return - } - - s.getLock("page_" + variant).Lock() - defer s.getLock("page_" + variant).Unlock() - - db := s.pageDB(variant) - if err := db.Put([]byte(flake), []byte{}, nil); err != nil { - s.fatalClose(fmt.Sprintf("Error inserting image %s into page variant %s, %s", flake, variant, err)) - } - s.pageAdvanceTotalCount(variant) -} - -// PageRegisterRemove registers an image remove. -func (s *Store) PageRegisterRemove(variant, flake string) { - s.getLock("page_" + variant).Lock() - defer s.getLock("page_" + variant).Unlock() - - db := s.pageDB(variant) - if err := db.Delete([]byte(flake), nil); err != nil { - s.fatalClose(fmt.Sprintf("Error removing image %s from page variant %s, %s", flake, variant, err)) - } - s.pageReduceTotalCount(variant) -} diff --git a/store/secret.go b/store/secret.go deleted file mode 100644 index 10d48d2fb877d43448477bbbe3313b25d1945d63..0000000000000000000000000000000000000000 --- a/store/secret.go +++ /dev/null @@ -1,56 +0,0 @@ -package store - -import ( - "crypto/rand" - "fmt" - "math/big" - "os" -) - -// SecretNew generates a new user secret. -func (s *Store) SecretNew() string { - secret := make([]byte, 64) - for i := 0; i < 64; i++ { - if n, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))); err != nil { - s.fatalClose(fmt.Sprintf("Error generating secret, %s", err)) - } else { - secret[i] = letters[n.Int64()] - } - } - return string(secret) -} - -// SecretLookup looks up a user from a secret. -func (s *Store) SecretLookup(secret string) User { - if !secretRegex.MatchString(secret) || !s.file(s.SecretPath(secret)) { - return User{} - } - if !s.Compat { - return s.user(s.SecretPath(secret) + "/" + infoJson) - } else { - if path, err := os.ReadFile(s.SecretPath(secret)); err != nil { - s.fatalClose(fmt.Sprintf("Error reading association file of secret %s, %s", secret, err)) - } else { - return s.user(string(path) + "/" + infoJson) - } - } - return User{} -} - -// SecretAssociate associates a secret with a user. -func (s *Store) SecretAssociate(secret, flake string) { - if s.file(s.SecretPath(secret)) { - return - } - s.link("../users/"+flake, s.SecretPath(secret)) -} - -// SecretDisassociate disassociates a secret. -func (s *Store) SecretDisassociate(secret string) { - if !s.file(s.SecretPath(secret)) { - return - } - if err := os.Remove(s.SecretPath(secret)); err != nil { - s.fatalClose(fmt.Sprintf("Error disassociating secret %s, %s", secret, err)) - } -} diff --git a/store/spec.go b/store/spec.go new file mode 100644 index 0000000000000000000000000000000000000000..effc4ce621672917dd9bd80e772cd0c091bc9976 --- /dev/null +++ b/store/spec.go @@ -0,0 +1,54 @@ +package store + +import ( + "github.com/nfnt/resize" + "image" + "regexp" +) + +const ( + // PreviewLimitSize is the maximum amount of pixels of a dimension a preview can have. + PreviewLimitSize = 256 + // ImageRootPageVariant is the name of the root page variant. + ImageRootPageVariant = "root" + // PageSize is the length of a page. + PageSize = 64 + // MinPasswordLength is the minimum password length in bytes. + MinPasswordLength = 8 + // MaxPasswordLength is the maximum password length in bytes. + MaxPasswordLength = 1024 + // InitialPassword is the default password of the initial user. + InitialPassword = "initial_password" +) + +const ( + // ArtistType is the tag type artist. + ArtistType = "artist" + // CharacterType is the tag type character. + CharacterType = "character" + // CopyrightType is the tag type copyright. + CopyrightType = "copyright" + // MetaType is the tag type meta. + MetaType = "meta" + // GenericType is the tag type generic. + GenericType = "generic" +) + +var allowedTagTypesMap = map[string]bool{ + ArtistType: true, + CharacterType: true, + CopyrightType: true, + MetaType: true, + GenericType: true, +} + +var ( + nameRegex = regexp.MustCompile(`^[a-z0-9()_-]{3,}$`) + sha256Regex = regexp.MustCompile(`\b[A-Fa-f0-9]{64}\b`) + secretRegex = regexp.MustCompile(`\b[A-Za-z]{64}\b`) +) + +// MakePreview compresses an image.Image to preview-size. +func MakePreview(img image.Image) image.Image { + return resize.Thumbnail(PreviewLimitSize, PreviewLimitSize, img, resize.Bilinear) +} diff --git a/store/store.go b/store/store.go index bf359da9c093f3fcba888e881efdb67c231c3c95..899bea00c3ed7415d6006b290e534eb7de71e394 100644 --- a/store/store.go +++ b/store/store.go @@ -1,318 +1,105 @@ package store import ( - "encoding/json" - "fmt" - "github.com/bwmarrin/snowflake" - log "github.com/sirupsen/logrus" - "github.com/syndtr/goleveldb/leveldb" - "os" - "runtime" - "strconv" - "sync" -) - -const revision = 1 - -const ( - UserSnowflakeNodeID = 7 - ImageSnowflakeNodeID = 9 + "errors" ) var ( - imageNode *snowflake.Node - userNode *snowflake.Node + // ErrNotDirectory is returned when target path is not a directory. + ErrNotDirectory = errors.New("not a directory") + // ErrAlreadyExists is returned when target path already exists. + ErrAlreadyExists = errors.New("already exists") + // ErrNoEntry is returned when the entry does not exist. + ErrNoEntry = errors.New("no entry") + // ErrInvalidInput is returned when input is invalid. + ErrInvalidInput = errors.New("invalid input") ) -// Info represents system information of a store. -type Info struct { - Revision int `json:"revision"` - Compat bool `json:"compat"` - Register bool `json:"register"` - InitialUser string `json:"initial_user"` - PermissionDir os.FileMode `json:"permission_dir"` - PermissionFile os.FileMode `json:"permission_file"` -} - -// Store represents a file store. -type Store struct { - Path string - SingleUser bool - Private bool - Revision int - Compat bool - Register bool - InitialUser string - PermissionDir os.FileMode - PermissionFile os.FileMode - pageldb map[string]*leveldb.DB - mutex map[string]*sync.RWMutex - sync.RWMutex -} - -func init() { - // Set Epoch to beginning of time (01/01/1970) - snowflake.Epoch = 0 - - // Create snowflake nodes - var err error - userNode, err = snowflake.NewNode(UserSnowflakeNodeID) - if err != nil { - log.Fatalf("Error creating user snowflake node, %s", err) - } - imageNode, err = snowflake.NewNode(ImageSnowflakeNodeID) - if err != nil { - log.Fatalf("Error creating image snowflake node, %s", err) - } -} - -// New initialises a new store instance. -func New(path string, single, private bool) *Store { - var store *Store - if stat, err := os.Stat(path); err != nil { - log.Infof("Initializing new store at %s.", path) - // Initialise empty store. - store = &Store{ - Path: path, - SingleUser: single, - Private: private, - Revision: revision, - Compat: runtime.GOOS == "windows", - Register: false, - PermissionDir: 0700, - PermissionFile: 0600, - pageldb: make(map[string]*leveldb.DB), - mutex: make(map[string]*sync.RWMutex), - } - if err = store.create(); err != nil { - log.Fatalf("Error creating store, %s", err) - } - } else { - // Exit if store is not a directory. - if !stat.IsDir() { - log.Fatal("Store is not a directory.") - } - - // Load and parse store info. - var info Info - var payload []byte - if payload, err = os.ReadFile(path + "/" + infoJson); err != nil { - log.Fatalf("Error reading store information, %s", err) - } else { - if err = json.Unmarshal(payload, &info); err != nil { - log.Fatalf("Error parsing store information, %s", err) - } - } - if info.Revision != revision { - log.Fatalf("Store format revision %v, expecting revision %v.", info.Revision, revision) - } - - // Create store instance. - store = &Store{ - Path: path, - SingleUser: single, - Private: private, - Revision: info.Revision, - Compat: info.Compat, - Register: info.Register, - InitialUser: info.InitialUser, - PermissionDir: info.PermissionDir, - PermissionFile: info.PermissionFile, - pageldb: make(map[string]*leveldb.DB), - mutex: make(map[string]*sync.RWMutex), - } - } - - // Handle store locking - if store.file(store.LockPath()) { - if pid, err := os.ReadFile(store.LockPath()); err != nil { - log.Fatalf("Store locked, lock file unreadable, %s", err) - } else { - log.Fatalf("Store locked by process %s.", string(pid)) - } - } - if err := os.WriteFile(store.LockPath(), []byte(strconv.Itoa(os.Getpid())), store.PermissionFile); err != nil { - log.Fatalf("Error locking store, %s", err) - } - - return store -} - -// Close closes the store. -func (s *Store) Close() { - // Close all leveldb - for variant, ldb := range s.pageldb { - if err := ldb.Close(); err != nil { - log.Errorf("Error closing leveldb variant %s, %s", variant, err) - } - log.Infof("Page variant %s closed.", variant) - } - - // Unlock store - if err := os.Remove(s.LockPath()); err != nil { - log.Errorf("Error unlocking store, %s", err) - } - - log.Info("Store closed.") -} - -// fatalClose closes the store and as cleanly as possible and exits with a fatal message. -func (s *Store) fatalClose(message string) { - s.Close() - log.Fatal(message) -} - -// create sets up the store directory if it does not exist. -func (s *Store) create() error { - // Check if exists - if _, err := os.Stat(s.Path); err == nil { - return AlreadyExists - } - - // Create directories - if err := os.Mkdir(s.Path, s.PermissionDir); err != nil { - return err - } - if err := os.Mkdir(s.TagsDir(), s.PermissionDir); err != nil { - return err - } - if err := os.Mkdir(s.ImagesDir(), s.PermissionDir); err != nil { - return err - } - if err := os.Mkdir(s.ImagesSnowflakeDir(), s.PermissionDir); err != nil { - return err - } - if err := os.Mkdir(s.UsersDir(), s.PermissionDir); err != nil { - return err - } - if err := os.Mkdir(s.SecretsDir(), s.PermissionDir); err != nil { - return err - } - if err := os.Mkdir(s.UsernamesDir(), s.PermissionDir); err != nil { - return err - } - if err := os.Mkdir(s.PageBaseDir(), s.PermissionDir); err != nil { - return err - } - - // Create initial user - info := s.UserAdd("root", "initial", true) - if info.Snowflake == "" { - log.Fatal("Error creating initial user.") - } else { - log.Infof("Created initial user with username root and password initial.") - } - s.InitialUser = info.Snowflake - - // Create information file - if payload, err := json.Marshal(Info{ - Revision: s.Revision, - Compat: s.Compat, - Register: s.Register, - InitialUser: s.InitialUser, - PermissionDir: s.PermissionDir, - PermissionFile: s.PermissionFile, - }); err != nil { - return err - } else { - if err = os.WriteFile(s.Path+"/"+infoJson, payload, s.PermissionFile); err != nil { - return err - } - } - - return nil -} - -// getLock returns a lock associated with a string. -func (s *Store) getLock(entry string) *sync.RWMutex { - s.RLock() - mutex, ok := s.mutex[entry] - s.RUnlock() - if !ok { - s.Lock() - mutex = &sync.RWMutex{} - s.mutex[entry] = mutex - s.Unlock() - } - return mutex -} - -// file probes for the existence of specified entry on the filesystem. -func (s *Store) file(path string) bool { - if _, err := os.Stat(path); err != nil { - if os.IsNotExist(err) { - return false - } else { - s.fatalClose(fmt.Sprintf("Error stat %s, %s", path, err)) - } - } - return true -} - -// dir probes for the presence of specified directory on the filesystem. -func (s *Store) dir(path string) bool { - if stat, err := os.Stat(path); err != nil { - if os.IsNotExist(err) { - return false - } else { - s.fatalClose(fmt.Sprintf("Error stat %s, %s", path, err)) - } - } else { - if !stat.IsDir() { - s.fatalClose(fmt.Sprintf("Path %s is not a directory.", path)) - } - } - return true -} - -// link provides symlink-like usage while considering compat mode. -func (s *Store) link(old, new string) { - if !s.Compat { - if err := os.Symlink(old, new); err != nil { - s.fatalClose(fmt.Sprintf("Error linking %s to %s, %s", old, new, err)) - } - } else { - if err := os.WriteFile(new, []byte(old), s.PermissionFile); err != nil { - s.fatalClose(fmt.Sprintf("Error writing associate file for %s to %s, %s", old, new, err)) - } - } -} - -// readlink provides readlink-like usage while considering compat mode. -func (s *Store) readlink(path string) string { - if !s.Compat { - //if final, err := os.Readlink(path); err != nil { - // if os.IsNotExist(err) { - // return "" - // } else { - // log.Fatalf("Error reading symlink %s, %s.", path, err) - // } - //} else { - // return final - //} - return path - } else { - if final, err := os.ReadFile(path); err != nil { - if os.IsNotExist(err) { - return "" - } else { - s.fatalClose(fmt.Sprintf("Error reading association file %s, %s.", path, err)) - } - } else { - return string(final) - } - } - return "" -} - -// TODO: These two really shouldn't be methods... Maybe change that for v2. - -// MatchName determines if str is a valid name. As of v1, this just calls MatchName. -func (s *Store) MatchName(str string) bool { - return MatchName(str) -} - -// MatchURL determines if str is a valid URL. As of v1, this just calls MatchURL. -func (s *Store) MatchURL(str string) bool { - return MatchURL(str) +// Store represents a file store implementation. +type Store interface { + // Open sets up the Backend. + Open() error + // Close cleans up and closes the Backend. + Close() error + // Name returns name of the Backend. + Name() string + + // Users returns a slice of User snowflakes. + Users() ([]string, error) + // User returns pointer to User by flake. + User(flake string) (*User, error) + // UserUsername returns pointer to User by username. + UserUsername(username string) (*User, error) + // UserInitial returns snowflake of initial User. + UserInitial() string + // UserAdd adds User and returns pointer to its metadata. + UserAdd(username string, password string, privileged bool) (*User, error) + // UserPrivileged sets privilege of User. + UserPrivileged(flake string, privileged bool) error + // UserUsernameUpdate updates the username of User. + UserUsernameUpdate(flake string, username string) error + // UserSecretRegen regenerates the secret of User. + UserSecretRegen(flake string) (string, error) + // UserPasswordValidate validates the password of User. + UserPasswordValidate(flake, username *string, password string) (bool, error) + // UserPasswordUpdate updates the password of User. + UserPasswordUpdate(flake string, password string) error + // UserImages return a slice of image snowflakes owned by User. + UserImages(flake string) ([]string, error) + // UserImage validates whether a user owns an Image. + UserImage(flake, imageFlake string) (bool, error) + // UserDestroy destroys User. + UserDestroy(flake string) error + + // SecretLookup looks up the corresponding User of a secret. + SecretLookup(secret string) (*User, error) + // SecretValidate validates the validity of a secret. + SecretValidate(secret string) bool + + // Images returns a slice of Image snowflakes. + Images() ([]string, error) + // Image returns pointer to Image by flake. + Image(flake string) (*Image, error) + // ImageAdd adds an Image and returns pointer to Image. + ImageAdd(data []byte, flake string) (*Image, error) + // ImageTags return a slice of tags of Image. + ImageTags(flake string) ([]string, error) + // ImageHasTag returns whether Image has a tag. + ImageHasTag(flake string, tag string) (bool, error) + // ImageSearch searches for images that contains slice of tags passed. + ImageSearch(tags []string) ([]string, error) + // ImageUpdate updates Image. + ImageUpdate(flake string, source string, parent string, commentary string, commentaryTranslation string) error + // ImageDestroy destroys Image. + ImageDestroy(flake string) error + // ImageTagAdd adds a tag to Image. + ImageTagAdd(flake string, tag string) error + // ImageTagRemove removes a tag from Image. + ImageTagRemove(flake string, tag string) error + // ImageHashes return a slice of Image hashes. + ImageHashes() ([]string, error) + // ImageHash returns pointer to Image by hash. + ImageHash(hash string) (*Image, error) + // ImageData returns data of an Image by hash. + ImageData(hash string, preview bool) (*Image, []byte, error) + // ImageSnowflakeHash returns snowflake of Image by hash. + ImageSnowflakeHash(flake string) (string, error) + + // Tags returns a slice of Tag. + Tags() ([]string, error) + // Tag returns pointer to Tag. + Tag(tag string) (*Tag, error) + // TagImages returns a slice of Image flakes with Tag. + TagImages(tag string) ([]string, error) + // TagAdd adds a Tag. + TagAdd(tag string) error + // TagDestroy destroys a Tag. + TagDestroy(tag string) error + // TagType sets type of Tag. + TagType(tag string, t string) error + + // PageTotal returns total amount of pages of variant. + PageTotal(variant string) (uint64, error) + // Page returns page content of a specific entry in a page variant. + Page(variant string, entry uint64) ([]string, error) } diff --git a/store/store_test.go b/store/store_test.go new file mode 100644 index 0000000000000000000000000000000000000000..72440ea2a61d80d1f2121906091cdd28cfe67ffd --- /dev/null +++ b/store/store_test.go @@ -0,0 +1 @@ +package store diff --git a/store/structs.go b/store/structs.go new file mode 100644 index 0000000000000000000000000000000000000000..78f192e5fcdc85f9b3196f5914255afdc8ebd214 --- /dev/null +++ b/store/structs.go @@ -0,0 +1,52 @@ +package store + +import ( + "crypto/rand" + "math/big" + "time" +) + +// User represents metadata of a user. +type User struct { + Secret string `json:"secret"` + Privileged bool `json:"privileged"` + Snowflake string `json:"snowflake"` + Username string `json:"username"` +} + +// Tombstone represents user deletion information. +type Tombstone struct { + Time int `json:"time"` +} + +// Image represents metadata of an image. +type Image struct { + Snowflake string `json:"snowflake"` + Hash string `json:"hash"` + Type string `json:"type"` + User string `json:"user"` + Source string `json:"source"` + Parent string `json:"parent"` + Child string `json:"child"` + Commentary string `json:"commentary"` + CommentaryTranslation string `json:"commentary_translation"` +} + +// Tag represents metadata of a tag. +type Tag struct { + Type string `json:"type"` + CreationTime time.Time `json:"creation_time"` +} + +// SecretNew generates a new user secret. +func SecretNew() (string, error) { + secret := make([]byte, 64) + for i := 0; i < 64; i++ { + if n, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))); err != nil { + return "", err + } else { + secret[i] = letters[n.Int64()] + } + } + return string(secret), nil +} diff --git a/store/tag.go b/store/tag.go deleted file mode 100644 index 24d1f09140d238d99846d1ffc9b40a8060ec79ce..0000000000000000000000000000000000000000 --- a/store/tag.go +++ /dev/null @@ -1,170 +0,0 @@ -package store - -import ( - "encoding/json" - "fmt" - log "github.com/sirupsen/logrus" - "os" - "strings" - "time" -) - -const ( - // ArtistType is the tag type artist. - ArtistType = "artist" - // CharacterType is the tag type character. - CharacterType = "character" - // CopyrightType is the tag type copyright. - CopyrightType = "copyright" - // MetaType is the tag type meta. - MetaType = "meta" - // GenericType is the tag type generic. - GenericType = "generic" -) - -var ( - // AllowedTagTypes represent tag type strings that are allowed. - AllowedTagTypes = []string{ArtistType, CharacterType, CopyrightType, MetaType, GenericType} - allowedTagTypesMap = map[string]bool{ - ArtistType: true, - CharacterType: true, - CopyrightType: true, - MetaType: true, - GenericType: true, - } -) - -// TagTypeAllowed returns whether str is an allowed tag type. -func TagTypeAllowed(str string) bool { - return allowedTagTypesMap[str] -} - -// Tag represents metadata of a tag. -type Tag struct { - Type string `json:"type"` - CreationTime time.Time `json:"creation_time"` -} - -// Tags returns a slice of tag names. -func (s *Store) Tags() []string { - var tags []string - if entries, err := os.ReadDir(s.TagsDir()); err != nil { - s.fatalClose(fmt.Sprintf("Error reading tags, %s", err)) - } else { - for _, entry := range entries { - if entry.IsDir() { - tags = append(tags, entry.Name()) - } - } - } - return tags -} - -// Tag returns a slice of image snowflakes in a specific tag. -func (s *Store) Tag(tag string) []string { - if !nameRegex.MatchString(tag) || !s.dir(s.TagPath(tag)) { - return nil - } - var images []string - if entries, err := os.ReadDir(s.TagPath(tag)); err != nil { - s.fatalClose(fmt.Sprintf("Error reading tag %s, %s", tag, err)) - } else { - for _, entry := range entries { - if entry.Name() == infoJson { - continue - } - images = append(images, entry.Name()) - } - } - return images -} - -// TagCreate creates a tag and returns ok value. -func (s *Store) TagCreate(tag string) bool { - if len(tag) > 128 || !nameRegex.MatchString(tag) { - return false - } - if !s.dir(s.TagPath(tag)) { - s.getLock("tag_" + tag).Lock() - defer s.getLock("tag_" + tag).Unlock() - if err := os.Mkdir(s.TagPath(tag), s.PermissionDir); err != nil { - s.fatalClose(fmt.Sprintf("Error creating tag %s, %s", tag, err)) - } - if payload, err := json.Marshal(Tag{Type: GenericType, CreationTime: time.Now().UTC()}); err != nil { - s.fatalClose(fmt.Sprintf("Error generating tag %s metadata, %s", tag, err)) - } else { - if err = os.WriteFile(s.TagMetadataPath(tag), payload, s.PermissionFile); err != nil { - s.fatalClose(fmt.Sprintf("Error writing tag %s metadata, %s", tag, err)) - } - } - log.Infof("Tag %s created.", tag) - return true - } - return true -} - -// TagDestroy removes all references from a tag and removes it. -func (s *Store) TagDestroy(tag string) { - if !nameRegex.MatchString(tag) || !s.dir(s.TagPath(tag)) { - return - } - - flakes := s.Tag(tag) - for _, flake := range flakes { - s.ImageTagRemove(flake, tag) - } - if err := os.Remove(s.TagMetadataPath(tag)); err != nil { - s.fatalClose(fmt.Sprintf("Error removing tag %s metadata, %s", tag, err)) - } - if err := os.Remove(s.TagPath(tag)); err != nil { - s.fatalClose(fmt.Sprintf("Error removing tag %s, %s", tag, err)) - } - log.Infof("Tag %s destroyed.", tag) -} - -// TagInfo returns information of a tag. -func (s *Store) TagInfo(tag string) Tag { - if !nameRegex.MatchString(tag) || !s.file(s.TagMetadataPath(tag)) { - return Tag{} - } - - s.getLock("tag_" + tag).RLock() - defer s.getLock("tag_" + tag).RUnlock() - if payload, err := os.ReadFile(s.TagMetadataPath(tag)); err != nil { - s.fatalClose(fmt.Sprintf("Error reading tag %s metadata, %s", tag, err)) - } else { - var info Tag - if err = json.Unmarshal(payload, &info); err != nil { - s.fatalClose(fmt.Sprintf("Error parsing tag %s metadata, %s", tag, err)) - } else { - return info - } - } - return Tag{} -} - -// TagType sets type of tag. -func (s *Store) TagType(tag, t string) { - if !nameRegex.MatchString(tag) || !s.file(s.TagMetadataPath(tag)) { - return - } - - if !TagTypeAllowed(t) { - log.Warnf("Invalid tag change on tag %s, got %s, expecting one of %s.", tag, t, strings.Join(AllowedTagTypes, ", ")) - return - } - info := s.TagInfo(tag) - - s.getLock("tag_" + tag).Lock() - defer s.getLock("tag_" + tag).Unlock() - - info.Type = t - if payload, err := json.Marshal(info); err != nil { - s.fatalClose(fmt.Sprintf("Error updating tag %s metadata, %s", tag, err)) - } else { - if err = os.WriteFile(s.TagMetadataPath(tag), payload, s.PermissionFile); err != nil { - s.fatalClose(fmt.Sprintf("Error writing tag %s metadata, %s", tag, err)) - } - } - log.Infof("Tag %s type set to %s.", tag, t) -} diff --git a/store/user.go b/store/user.go deleted file mode 100644 index f8795a2bd0f13d7c8a1b081f3c8d9b23539ea566..0000000000000000000000000000000000000000 --- a/store/user.go +++ /dev/null @@ -1,313 +0,0 @@ -package store - -import ( - "encoding/json" - "fmt" - log "github.com/sirupsen/logrus" - "os" -) - -// User represents metadata of a user. -type User struct { - Secret string `json:"secret"` - Privileged bool `json:"privileged"` - Snowflake string `json:"snowflake"` - Username string `json:"username"` -} - -// user parses user metadata file. -func (s *Store) user(path string) User { - if payload, err := os.ReadFile(path); err != nil { - s.fatalClose(fmt.Sprintf("Error reading user metadata %s, %s", path, err)) - } else { - var info User - if err = json.Unmarshal(payload, &info); err != nil { - s.fatalClose(fmt.Sprintf("Error parsing user metadata %s, %s", path, err)) - } else { - return info - } - } - return User{} -} - -// User returns user information with specific snowflake. -func (s *Store) User(flake string) User { - if !numerical(flake) || !s.file(s.UserPath(flake)) { - return User{} - } - - s.getLock(flake).RLock() - defer s.getLock(flake).RUnlock() - return s.user(s.UserMetadataPath(flake)) -} - -// Users returns a slice of user snowflakes. -func (s *Store) Users() []string { - var users []string - if entries, err := os.ReadDir(s.UsersDir()); err != nil { - s.fatalClose(fmt.Sprintf("Error reading users, %s", err)) - } else { - for _, entry := range entries { - if entry.IsDir() { - users = append(users, entry.Name()) - } - } - } - return users -} - -// UserMetadata sets user metadata. -func (s *Store) UserMetadata(info User) { - if payload, err := json.Marshal(info); err != nil { - s.fatalClose(fmt.Sprintf("Error updating user %s metadata, %s", info.Snowflake, err)) - } else { - if err = os.WriteFile(s.UserMetadataPath(info.Snowflake), payload, s.PermissionFile); err != nil { - s.fatalClose(fmt.Sprintf("Error writing user %s metadata, %s", info.Snowflake, err)) - } - } -} - -// UserAdd creates a user. -func (s *Store) UserAdd(username, password string, privileged bool) User { - if len(username) > 64 || !nameRegex.MatchString(username) || s.file(s.UsernamePath(username)) { - return User{} - } - - info := User{ - Secret: s.SecretNew(), - Privileged: privileged, - Snowflake: userNode.Generate().String(), - Username: username, - } - // Create user directory and images - if err := os.MkdirAll(s.UserImagesPath(info.Snowflake), s.PermissionDir); err != nil { - s.fatalClose(fmt.Sprintf("Error creating user %s directory, %s", info.Snowflake, err)) - } - s.getLock(info.Snowflake).Lock() - defer s.getLock(info.Snowflake).Unlock() - // Save user metadata - s.UserMetadata(info) - // Associate new user secret - s.SecretAssociate(info.Secret, info.Snowflake) - // Associate username - s.userUsernameAssociate(info.Snowflake, info.Username) - // Set password - s.userPasswordUpdate(s.UserPath(info.Snowflake), password) - - log.Infof("User %s added with username %s privilege %v secret %s.", info.Snowflake, info.Username, info.Privileged, info.Secret) - return info -} - -// UserPrivileged sets privileged status of user with specific snowflake. -func (s *Store) UserPrivileged(flake string, privileged bool) { - if !numerical(flake) { - return - } - - s.getLock(flake).Lock() - defer s.getLock(flake).Unlock() - - info := s.user(s.UserMetadataPath(flake)) - if info.Snowflake != flake { - return - } - info.Privileged = privileged - s.UserMetadata(info) - log.Infof("User %s privileged %v", flake, privileged) -} - -// UserUsernameUpdate updates username of user with specific snowflake. -func (s *Store) UserUsernameUpdate(flake, username string) bool { - if !numerical(flake) || !nameRegex.MatchString(username) || s.file(s.UsernamePath(username)) { - return false - } - - s.getLock(flake).Lock() - defer s.getLock(flake).Unlock() - - info := s.user(s.UserMetadataPath(flake)) - if info.Snowflake != flake { - return false - } - // Lock old username - s.getLock(info.Username).Lock() - defer s.getLock(info.Username).Unlock() - // Disassociate old username and associate new - if info.Username != "" { - s.userUsernameDisassociate(info.Username) - } - s.userUsernameAssociate(flake, username) - // Set username in metadata - info.Username = username - s.UserMetadata(info) - - log.Infof("User %s username updated to %s.", flake, username) - return true -} - -// UserSecretRegen regenerates secret of user with specific snowflake. -func (s *Store) UserSecretRegen(flake string) string { - if !numerical(flake) { - return "" - } - - s.getLock(flake).Lock() - defer s.getLock(flake).Unlock() - - info := s.user(s.UserMetadataPath(flake)) - // Check if obtained info is valid - if info.Snowflake != flake { - return "" - } - // Disassociate old user - s.SecretDisassociate(info.Secret) - // Generate new secret - info.Secret = s.SecretNew() - // Write metadata - s.UserMetadata(info) - // Associate new secret - s.SecretAssociate(info.Secret, info.Snowflake) - // Log the event - log.Infof("User %s secret reset to %s.", flake, info.Secret) - return info.Secret -} - -// UserUsername returns user via username. -func (s *Store) UserUsername(username string) User { - if !nameRegex.MatchString(username) || !s.file(s.UsernamePath(username)) { - return User{} - } - s.getLock(username).RLock() - user := s.user(s.readlink(s.UsernamePath(username)) + "/" + infoJson) - s.getLock(username).RUnlock() - return user -} - -// userUsernameAssociate associates user snowflake with specific username. -func (s *Store) userUsernameAssociate(flake, username string) { - s.link("../users/"+flake, s.UsernamePath(username)) -} - -// userUsernameDisassociate disassociates specific username. -func (s *Store) userUsernameDisassociate(username string) { - if err := os.Remove(s.UsernamePath(username)); err != nil { - s.fatalClose(fmt.Sprintf("Error disassociating username %s, %s", username, err)) - } -} - -// userPassword returns password of user from path to user directory. -func (s *Store) userPassword(path string) string { - if payload, err := os.ReadFile(path + "/passwd"); err != nil { - if os.IsNotExist(err) { - return "" - } - s.fatalClose(fmt.Sprintf("Error reading password from user directory %s, %s", path, err)) - } else { - return string(payload) - } - return "" -} - -// userPasswordUpdate updates user password of user from path to user directory. -func (s *Store) userPasswordUpdate(path, password string) bool { - if len(password) > 1024 { - return false - } - if err := os.WriteFile(path+"/passwd", []byte(password), s.PermissionFile); err != nil { - s.fatalClose(fmt.Sprintf("Error setting password for user directory %s, %s", path, err)) - } else { - return true - } - return false -} - -// UserPasswordValidate validates password of specified user. -func (s *Store) UserPasswordValidate(flake, password string) bool { - if !numerical(flake) || !s.file(s.UserPath(flake)) { - return false - } - - s.getLock(flake).RLock() - defer s.getLock(flake).RUnlock() - - // Check password is not empty and matches - if password != "" && password == s.userPassword(s.UserPath(flake)) { - return true - } - - return false -} - -// UserUsernamePasswordValidate validates password of specified user from username. -func (s *Store) UserUsernamePasswordValidate(username, password string) bool { - if !nameRegex.MatchString(username) || !s.file(s.UsernamePath(username)) { - return false - } - - s.getLock(username).RLock() - defer s.getLock(username).RUnlock() - - if password != "" && password == s.userPassword(s.readlink(s.UsernamePath(username))) { - return true - } - - return false -} - -// UserPasswordUpdate updates password of specified user. -func (s *Store) UserPasswordUpdate(flake, password string) bool { - if !numerical(flake) { - return false - } - - s.getLock(flake).Lock() - defer s.getLock(flake).Unlock() - - return s.userPasswordUpdate(s.UserPath(flake), password) -} - -// UserDestroy destroys a user with specific snowflake. -func (s *Store) UserDestroy(flake string) { - if !numerical(flake) { - return - } - if !s.dir(s.UserPath(flake)) { - return - } - - s.getLock(flake).Lock() - defer s.getLock(flake).Unlock() - - info := s.User(flake) - // Check if info correct - if info.Snowflake != flake { - return - } - // Disassociate secret - s.SecretDisassociate(info.Snowflake) - // Remove user data directory - if err := os.RemoveAll(s.UserPath(flake)); err != nil { - s.fatalClose(fmt.Sprintf("Error destroying user %s data directory, %s", flake, err)) - } - log.Infof("User %s username %s destroyed.", info.Snowflake, info.Username) -} - -// UserImages returns slice of a user's images. -func (s *Store) UserImages(flake string) []string { - if !numerical(flake) { - return nil - } - if !s.dir(s.UserImagesPath(flake)) { - return nil - } - - var images []string - if entries, err := os.ReadDir(s.UserImagesPath(flake)); err != nil { - s.fatalClose(fmt.Sprintf("Error reading user %s images, %s", flake, err)) - } else { - for _, entry := range entries { - images = append(images, entry.Name()) - } - } - return images -} diff --git a/store/validation.go b/store/validation.go new file mode 100644 index 0000000000000000000000000000000000000000..6f78e9e53cf124064d0da078d6793f5efb68677f --- /dev/null +++ b/store/validation.go @@ -0,0 +1,57 @@ +package store + +import ( + "net/url" + "strconv" + "unicode" +) + +const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +// Numerical validates whether a string is numerical. +func Numerical(flake string) bool { + if flake == "" { + return false + } + _, err := strconv.Atoi(flake) + return err == nil +} + +// MatchName determines if str is a valid name. +func MatchName(str string) bool { + return nameRegex.MatchString(str) +} + +// MatchSha256 determines if str is a sha256 representation. +func MatchSha256(str string) bool { + return sha256Regex.MatchString(str) +} + +// MatchSecret determines if str is a secret. +func MatchSecret(str string) bool { + return secretRegex.MatchString(str) +} + +// MatchURL determines if str is a valid URL. +func MatchURL(str string) bool { + u, err := url.Parse(str) + return err == nil && u.Scheme != "" && u.Host != "" +} + +// MatchTagType returns whether str is an allowed tag type. +func MatchTagType(str string) bool { + return allowedTagTypesMap[str] +} + +// MatchPassword returns whether str is a valid password. +func MatchPassword(str string) bool { + if l := len(str); l > MaxPasswordLength || l < MinPasswordLength { + return false + } + for _, r := range str { + if r > unicode.MaxASCII { + return false + } + } + return true +} diff --git a/web.go b/web.go index 6fc7eeb9a1c553f98713cd1f085ec16aea03e2b7..508cab3feba59a4a2399c92d96cd706fe9c5626c 100644 --- a/web.go +++ b/web.go @@ -2,16 +2,13 @@ package main import ( "embed" - "fmt" "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" - "github.com/spf13/viper" "io/fs" + "log" "net" "net/http" "os" "strconv" - "syscall" ) //go:embed assets @@ -20,87 +17,72 @@ var assets embed.FS var ( router *gin.Engine listener net.Listener + server = http.Server{} ) func webSetup() { - // Set log level according to - if log.GetLevel() == log.DebugLevel { + gin.SetMode(gin.ReleaseMode) + if config.System.Verbose { gin.SetMode(gin.DebugMode) - } else { - gin.SetMode(gin.ReleaseMode) } - // Configure gin router router = gin.New() router.Use(recovery()) - router.ForwardedByClientIP = viper.GetStringMap("server")["proxy"].(bool) - - // Configure logger - if log.GetLevel() == log.DebugLevel { - router.Use(gin.LoggerWithWriter(logger{})) + router.ForwardedByClientIP = config.Server.Proxy + if config.System.Verbose { + router.Use(gin.Logger()) } - - // Redirect on no route router.NoRoute(func(context *gin.Context) { context.Redirect(http.StatusTemporaryRedirect, "/web") }) -} -func listenerSetup() { - var err error - switch serverConfig["unix"].(bool) { - case false: - address := fmt.Sprintf("%s:%s", - serverConfig["host"].(string), - strconv.Itoa(int(serverConfig["port"].(int64)))) - listener, err = net.Listen("tcp", address) - if err != nil { - log.Errorf("Error binding %s, %s", address, err) + if config.Server.Unix { + if l, err := net.Listen("unix", config.Server.Host); err != nil { + log.Fatalf("error listening on socket: %s", err) + } else { + listener = l } - log.Infof("Web server listening on %s.", address) - case true: - path := serverConfig["host"].(string) - listener, err = net.Listen("unix", path) - if err != nil { - log.Errorf("Error binding %s, %s", path, err) + + if err := os.Chmod(config.Server.Host, 0777); err != nil { + log.Printf("error chmod: %s", err) } - err = syscall.Chmod(path, 0777) - if err != nil { - log.Errorf("Error changing permission of web server socket, %s", err) + log.Printf("web server listening on socket %s", config.Server.Host) + } else { + if l, err := net.Listen("tcp", config.Server.Host+":"+strconv.Itoa(int(config.Server.Port))); err != nil { + log.Fatalf("error listening on TCP port: %s", err) + } else { + listener = l } - log.Infof("Web server listening on unix socket %s.", path) + log.Printf("web server listening on %s:%d", config.Server.Host, config.Server.Port) } - - // Configure server server.Handler = router -} - -func runWebServer() { - err := server.Serve(listener) - if err == http.ErrServerClosed { - log.Info("Web server closed.") - } else { - log.Errorf("Error starting server, %s", err) - } -} -func registerWebpage() { router.GET("/", func(context *gin.Context) { context.Redirect(http.StatusTemporaryRedirect, "web") }) if stat, err := os.Stat("assets/public"); err == nil && stat.IsDir() { - log.Info("Serving web interface from filesystem.") + log.Print("serving web assets from filesystem") router.Static("/web", "assets/public") } else { - log.Info("Serving bundled assets.") + log.Print("serving bundled web assets") var public fs.FS public, err = fs.Sub(assets, "assets/public") if err != nil { - log.Fatalf("Error getting subdirectory, %s", err) + log.Fatalf("error subdirectory: %s", err) } router.StaticFS("/web", http.FS(public)) } + + registerAPI() +} + +func serve() { + if err := server.Serve(listener); err == http.ErrServerClosed { + log.Printf("web server closed") + } else { + log.Printf("error serve: %s", err) + } }