diff --git a/cmds/serve.go b/cmds/serve.go index 04e2f0c..707c3f4 100644 --- a/cmds/serve.go +++ b/cmds/serve.go @@ -7,10 +7,13 @@ import ( "gitea.konchin.com/go2025/backend/handlers/api" "gitea.konchin.com/go2025/backend/handlers/auth" + "gitea.konchin.com/go2025/backend/handlers/img" "gitea.konchin.com/go2025/backend/implements" "gitea.konchin.com/go2025/backend/middlewares" "gitea.konchin.com/go2025/backend/tracing" "gitea.konchin.com/go2025/backend/utils" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" "github.com/spf13/cobra" "github.com/spf13/viper" httpSwagger "github.com/swaggo/http-swagger" @@ -57,39 +60,38 @@ var serveCmd = &cobra.Command{ panic(err) } - /* - // Initialize MinIO instance - mc, err := minio.New(viper.GetString("minio-host"), &minio.Options{ - Creds: credentials.NewStaticV4( - viper.GetString("minio-accesskey"), - viper.GetString("minio-secretkey"), - "", - ), - Secure: viper.GetBool("minio-usessl"), - }) - if err != nil { - tracing.Logger.Ctx(ctx). - Error("failed to create minio client", - zap.Error(err)) - panic(err) - } + // Initialize MinIO instance + mc, err := minio.New(viper.GetString("minio-host"), &minio.Options{ + Creds: credentials.NewStaticV4( + viper.GetString("minio-accesskey"), + viper.GetString("minio-secretkey"), + "", + ), + Secure: viper.GetBool("minio-usessl"), + }) + if err != nil { + tracing.Logger.Ctx(ctx). + Error("failed to create minio client", + zap.Error(err)) + panic(err) + } - if err := utils.InitMinIO(ctx, mc); err != nil { - tracing.Logger.Ctx(ctx). - Error("failed to minio init", - zap.Error(err)) - panic(err) - } - */ + if err := utils.InitMinIO(ctx, mc); err != nil { + tracing.Logger.Ctx(ctx). + Error("failed to minio init", + zap.Error(err)) + panic(err) + } // Initialize custom interfaces db := implements.NewBunDatabase(bunDB) - // s3 := implements.NewMinIOObjectStorage(mc) + s3 := implements.NewMinIOObjectStorage(mc) // Initialize handlers midHandlers := middlewares.NewHandlers(db) - apis := api.NewHandlers(db) + apis := api.NewHandlers(db, s3) auths := auth.NewHandlers(db) + imgs := img.NewHandlers(s3) // Initialize backend router router := bunrouter.New() @@ -103,12 +105,17 @@ var serveCmd = &cobra.Command{ Use(midHandlers.CheckRefreshToken). Use(midHandlers.CheckAccessToken) apiGroup.GET("/images", apis.GetImages) + apiGroup.GET("/aliases", apis.GetAliases) + apiGroup.PUT("/image/:id/aliases", apis.PutImageAliases) authGroup := backend.NewGroup("/auth") authGroup.POST("/login", auths.PostLogin) authGroup.POST("/gen-login-url", midHandlers.CheckPresharedKey(auths.PostGenLoginUrl)) + imgGroup := backend.NewGroup("/img") + imgGroup.GET("/:filename", imgs.Get) + if viper.GetBool("swagger") { backend.GET("/swagger/*any", bunrouter.HTTPHandlerFunc( diff --git a/docs/docs.go b/docs/docs.go index 1da2f4c..ea4c661 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -143,10 +143,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "type": "array", - "items": { - "type": "string" - } + "$ref": "#/definitions/api.putImageAliasesInput" } } ], @@ -285,6 +282,17 @@ const docTemplate = `{ } } }, + "api.putImageAliasesInput": { + "type": "object", + "properties": { + "aliases": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "auth.postGenLoginUrlInput": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index f098f7e..46c903b 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -135,10 +135,7 @@ "in": "body", "required": true, "schema": { - "type": "array", - "items": { - "type": "string" - } + "$ref": "#/definitions/api.putImageAliasesInput" } } ], @@ -277,6 +274,17 @@ } } }, + "api.putImageAliasesInput": { + "type": "object", + "properties": { + "aliases": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "auth.postGenLoginUrlInput": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c3db4bb..dea2a50 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -20,6 +20,13 @@ definitions: uploadedUserId: type: string type: object + api.putImageAliasesInput: + properties: + aliases: + items: + type: string + type: array + type: object auth.postGenLoginUrlInput: properties: userId: @@ -127,9 +134,7 @@ paths: name: payload required: true schema: - items: - type: string - type: array + $ref: '#/definitions/api.putImageAliasesInput' responses: "200": description: OK diff --git a/handlers/api/getImages.go b/handlers/api/getImages.go index 4f0a614..2b474bf 100644 --- a/handlers/api/getImages.go +++ b/handlers/api/getImages.go @@ -29,8 +29,8 @@ func (self *Handlers) GetImages( ) error { ctx := req.Context() - rawReqImages := strings.Split(req.Param("images"), ",") - rawReqAliases := strings.Split(req.Param("aliases"), ",") + rawReqImages := strings.Split(req.URL.Query().Get("images"), ",") + rawReqAliases := strings.Split(req.URL.Query().Get("aliases"), ",") if (len(rawReqImages) == 0 && len(rawReqAliases) == 0) || (len(rawReqImages) > 0 && len(rawReqAliases) > 0) { diff --git a/handlers/api/handlers.go b/handlers/api/handlers.go index ead77c8..06e2997 100644 --- a/handlers/api/handlers.go +++ b/handlers/api/handlers.go @@ -4,8 +4,15 @@ import "gitea.konchin.com/go2025/backend/interfaces" type Handlers struct { db interfaces.Database + s3 interfaces.ObjectStorage } -func NewHandlers(db interfaces.Database) *Handlers { - return &Handlers{db: db} +func NewHandlers( + db interfaces.Database, + s3 interfaces.ObjectStorage, +) *Handlers { + return &Handlers{ + db: db, + s3: s3, + } } diff --git a/handlers/api/postImage.go b/handlers/api/postImage.go index e39749d..ad93173 100644 --- a/handlers/api/postImage.go +++ b/handlers/api/postImage.go @@ -3,8 +3,11 @@ package api import ( "net/http" "strings" + "time" "gitea.konchin.com/go2025/backend/middlewares" + "gitea.konchin.com/go2025/backend/models" + "gitea.konchin.com/go2025/backend/types" "gitea.konchin.com/go2025/backend/utils" "github.com/uptrace/bunrouter" ) @@ -21,6 +24,16 @@ import ( func (self *Handlers) PostImage( w http.ResponseWriter, req bunrouter.Request, ) error { + ctx := req.Context() + + claim, ok := ctx.Value(types.AccessToken("")).(models.AccessTokenClaim) + if !ok { + return middlewares.HTTPError{ + StatusCode: http.StatusUnauthorized, + Message: "missing access token", + } + } + typeHeader := strings.Split(req.Header.Get("Content-Type"), "/") if len(typeHeader) != 2 || typeHeader[0] != "image" { return middlewares.HTTPError{ @@ -29,6 +42,28 @@ func (self *Handlers) PostImage( } } - // TODO + image := models.Image{ + Extension: typeHeader[1], + Uploader: claim.UserId, + UploadTS: time.Now(), + } + err := self.db.InsertImage(ctx, &image) + if err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusInternalServerError, + Message: "failed to insert image to db", + OriginError: err, + } + } + + err = self.s3.PutImage(ctx, + image.Filename(), req.Body, req.ContentLength) + if err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusInternalServerError, + Message: "failed to put image to s3", + OriginError: err, + } + } return utils.Success(w) } diff --git a/handlers/img/get.go b/handlers/img/get.go new file mode 100644 index 0000000..8e8dcc9 --- /dev/null +++ b/handlers/img/get.go @@ -0,0 +1,35 @@ +package img + +import ( + "io" + "net/http" + + "gitea.konchin.com/go2025/backend/middlewares" + "github.com/uptrace/bunrouter" +) + +func (self *Handlers) Get( + w http.ResponseWriter, + req bunrouter.Request, +) error { + ctx := req.Context() + + filename := req.Param("filename") + + r, err := self.s3.GetImage(ctx, filename) + if err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusInternalServerError, + Message: "failed to get image", + OriginError: err, + } + } + if _, err := io.Copy(w, r); err != nil { + return middlewares.HTTPError{ + StatusCode: http.StatusInternalServerError, + Message: "failed to copy buffer", + OriginError: err, + } + } + return nil +} diff --git a/handlers/img/handlers.go b/handlers/img/handlers.go new file mode 100644 index 0000000..c0a7ee2 --- /dev/null +++ b/handlers/img/handlers.go @@ -0,0 +1,11 @@ +package img + +import "gitea.konchin.com/go2025/backend/interfaces" + +type Handlers struct { + s3 interfaces.ObjectStorage +} + +func NewHandlers(s3 interfaces.ObjectStorage) *Handlers { + return &Handlers{s3: s3} +} diff --git a/implements/bunDatabase.go b/implements/bunDatabase.go index f783d34..ecc954a 100644 --- a/implements/bunDatabase.go +++ b/implements/bunDatabase.go @@ -184,3 +184,13 @@ func (self *BunDatabase) UpdateAliases( return nil }) } + +func (self *BunDatabase) InsertImage( + ctx context.Context, + image *models.Image, +) error { + _, err := self.db.NewInsert(). + Model(image). + Exec(ctx) + return err +} diff --git a/implements/minioObjectStorage.go b/implements/minioObjectStorage.go index 1eb8a73..6f4df02 100644 --- a/implements/minioObjectStorage.go +++ b/implements/minioObjectStorage.go @@ -1,4 +1,36 @@ package implements +import ( + "context" + "io" + + "github.com/minio/minio-go/v7" + "github.com/spf13/viper" +) + type MinIOObjectStorage struct { + mc *minio.Client +} + +func NewMinIOObjectStorage(mc *minio.Client) *MinIOObjectStorage { + return &MinIOObjectStorage{mc: mc} +} + +func (self *MinIOObjectStorage) PutImage( + ctx context.Context, + name string, + r io.Reader, + size int64, +) error { + _, err := self.mc.PutObject(ctx, + viper.GetString("minio-bucket"), name, r, size, minio.PutObjectOptions{}) + return err +} + +func (self *MinIOObjectStorage) GetImage( + ctx context.Context, + name string, +) (io.Reader, error) { + return self.mc.GetObject(ctx, + viper.GetString("minio-bucket"), name, minio.GetObjectOptions{}) } diff --git a/interfaces/database.go b/interfaces/database.go index 7d1cbd5..a3472fc 100644 --- a/interfaces/database.go +++ b/interfaces/database.go @@ -37,4 +37,9 @@ type Database interface { imageId int64, aliasNames []string, ) error + + InsertImage( + ctx context.Context, + image *models.Image, + ) error } diff --git a/interfaces/objectStorage.go b/interfaces/objectStorage.go index 0f57f30..e31a5a3 100644 --- a/interfaces/objectStorage.go +++ b/interfaces/objectStorage.go @@ -1,4 +1,19 @@ package interfaces +import ( + "context" + "io" +) + type ObjectStorage interface { + PutImage( + ctx context.Context, + name string, + r io.Reader, + size int64, + ) error + GetImage( + ctx context.Context, + name string, + ) (io.Reader, error) } diff --git a/middlewares/checkAccessToken.go b/middlewares/checkAccessToken.go index 52b264d..17d8ec6 100644 --- a/middlewares/checkAccessToken.go +++ b/middlewares/checkAccessToken.go @@ -79,7 +79,7 @@ func (self *Handlers) CheckAccessToken( var claim models.AccessTokenClaim token, err := jwt.ParseWithClaims(accessTokenString, &claim, func(*jwt.Token) (interface{}, error) { - return []byte(viper.GetString("ACCESS_TOKEN_SECRET")), nil + return []byte(viper.GetString("access-token-secret")), nil }) if err != nil || !token.Valid { accessTokenString, err = refreshAccessToken(ctx, self.db, w, req) @@ -92,7 +92,7 @@ func (self *Handlers) CheckAccessToken( } token, err := jwt.ParseWithClaims(accessTokenString, &claim, func(*jwt.Token) (interface{}, error) { - return []byte(viper.GetString("ACCESS_TOKEN_SECRET")), nil + return []byte(viper.GetString("access-token-secret")), nil }) if err != nil || !token.Valid { return HTTPError{ diff --git a/middlewares/checkRefreshToken.go b/middlewares/checkRefreshToken.go index b848748..f8f1449 100644 --- a/middlewares/checkRefreshToken.go +++ b/middlewares/checkRefreshToken.go @@ -30,7 +30,7 @@ func (self *Handlers) CheckRefreshToken( var claim models.RefreshTokenClaim token, err := jwt.ParseWithClaims(refreshTokenCookie.Value, &claim, func(*jwt.Token) (interface{}, error) { - return []byte(viper.GetString("REFRESH_TOKEN_SECRET")), nil + return []byte(viper.GetString("refresh-token-secret")), nil }) if err != nil { return HTTPError{ @@ -48,7 +48,7 @@ func (self *Handlers) CheckRefreshToken( // check time and refresh timeLeft := claim.ExpiresAt.Time.Sub(time.Now()) / time.Second - if int64(timeLeft) < viper.GetInt64("REFRESH_TOKEN_TIMEOUT")/2 { + if int64(timeLeft) < viper.GetInt64("refresh-token-timeout")/2 { session, err := self.db.UpdateRefreshToken(ctx, claim.UserId) if err != nil { return HTTPError{ diff --git a/models/image.go b/models/image.go index 4e11175..b2b0d4a 100644 --- a/models/image.go +++ b/models/image.go @@ -1,6 +1,7 @@ package models import ( + "fmt" "time" "github.com/uptrace/bun" @@ -9,9 +10,14 @@ import ( type Image struct { bun.BaseModel `bun:"table:image"` - Id int64 `bun:"id,pk,autoincrement"` - Uploader string `bun:"uploader"` - UploadTS time.Time `bun:"upload_timestamp"` + Id int64 `bun:"id,pk,autoincrement"` + Extension string `bun:"extension"` + Uploader string `bun:"uploader"` + UploadTS time.Time `bun:"upload_timestamp"` Aliases []Alias `bun:"m2m:alias_image,join:Image=Alias"` } + +func (self *Image) Filename() string { + return fmt.Sprintf("%d.%s", self.Id, self.Extension) +}