From bb52442373e03c7f9cf90b56b5d5c8b0062e9de4 Mon Sep 17 00:00:00 2001 From: hex Date: Sat, 14 Jun 2025 17:48:05 +0200 Subject: [PATCH] feat: s3 support --- .env.example | 19 +++- go.mod | 11 +++ go.sum | 23 +++++ internal/api/routes/files.go | 164 ++++++++++++++++++++++------------- internal/types/types.go | 14 ++- main.go | 29 +++++-- 6 files changed, 187 insertions(+), 73 deletions(-) diff --git a/.env.example b/.env.example index 4fc749b..7a4a794 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,26 @@ +# Path to store images IMAGE_PATH=/tmp -REDIRECT_URI=http://localhost:8081/api/auth/callback + +# URL to frontend FRONTEND_URI= + +# Domain (e.g. stereo.cat) DOMAIN= + +# Discord oAuth2 credentials +REDIRECT_URI=http://localhost:8081/api/auth/callback CLIENT_ID= CLIENT_SECRET= -FRONTEND_URI= -DOMAIN=localhost + +# Listening port PORT= +# S3 +S3_ENDPOINT= +S3_KEY= +S3_SECRET= +S3_BUCKET= + # can be either postgres or sqlite DATABASE_TYPE= diff --git a/go.mod b/go.mod index 72ef5f2..97c77aa 100644 --- a/go.mod +++ b/go.mod @@ -12,14 +12,18 @@ require ( github.com/bytedance/sonic v1.13.2 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cloudwego/base64x v0.1.5 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/h2non/filetype v1.1.3 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.4 // indirect @@ -27,16 +31,23 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lithammer/shortuuid/v4 v4.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.28 // indirect + github.com/minio/crc64nvme v1.0.1 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/minio-go/v7 v7.0.93 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/tinylib/msgp v1.3.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.16.0 // indirect diff --git a/go.sum b/go.sum index ce498a0..e3ca063 100644 --- a/go.sum +++ b/go.sum @@ -10,12 +10,16 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -26,6 +30,8 @@ github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= @@ -33,6 +39,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= +github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -49,6 +57,9 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= @@ -68,6 +79,12 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= +github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.93 h1:lAB4QJp8Nq3vDMOU0eKgMuyBiEGMNlXQ5Glc8qAxqSU= +github.com/minio/minio-go/v7 v7.0.93/go.mod h1:71t2CqDt3ThzESgZUlU1rBN54mksGGlkLcFgguDnnAc= 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= @@ -75,12 +92,16 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -91,6 +112,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= +github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= diff --git a/internal/api/routes/files.go b/internal/api/routes/files.go index d45d4d5..09a8cbb 100644 --- a/internal/api/routes/files.go +++ b/internal/api/routes/files.go @@ -1,17 +1,30 @@ package routes import ( - "os" - "path/filepath" + "bytes" + "io" + "log" "strings" "time" "github.com/gin-gonic/gin" + "github.com/gofrs/uuid" "github.com/golang-jwt/jwt/v5" + "github.com/h2non/filetype" + "github.com/minio/minio-go/v7" "stereo.cat/backend/internal/auth" "stereo.cat/backend/internal/types" ) +func getLengthFromReader(r io.Reader) (io.Reader, int64, error) { + buf, err := io.ReadAll(r) + if err != nil { + return nil, 0, err + } + + return io.NopCloser(bytes.NewBuffer(buf)), int64(len(buf)), nil +} + func RegisterFileRoutes(cfg *types.StereoConfig, api *gin.RouterGroup) { api.POST("/upload", auth.JwtMiddleware(cfg.JWTSecret), func(c *gin.Context) { claims := c.MustGet("claims").(jwt.MapClaims) @@ -29,28 +42,32 @@ func RegisterFileRoutes(cfg *types.StereoConfig, api *gin.RouterGroup) { return } - filePath := filepath.Join(cfg.ImagePath, uid, file.Filename) - - if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { - c.JSON(500, gin.H{"error": "failed to create directory"}) - return - } - - if err := c.SaveUploadedFile(file, filePath); err != nil { - c.JSON(500, gin.H{"error": "failed to save file"}) - return - } - if file.Size <= 0 { c.JSON(400, gin.H{"error": "file size must be greater than zero"}) return } + fileReader, err := file.Open() + + if err != nil { + c.JSON(500, gin.H{"error": "couldn't open file"}) + log.Println(err) + return + } + + fileType, err := filetype.MatchReader(fileReader) + + if err != nil { + c.JSON(500, gin.H{"error": "couldn't open file"}) + log.Println(err) + return + } + fileMeta := types.File{ - Name: file.Filename, Owner: uid, CreatedAt: time.Now(), Size: file.Size, + Extension: fileType.Extension, } if err := cfg.Database.Create(&fileMeta).Error; err != nil { @@ -58,10 +75,26 @@ func RegisterFileRoutes(cfg *types.StereoConfig, api *gin.RouterGroup) { return } - c.JSON(200, gin.H{"message": "file uploaded successfully", "name": fileMeta.Name}) + newReader, length, err := getLengthFromReader(fileReader) + + if err != nil { + c.JSON(500, gin.H{"error": "couldn't open file"}) + log.Println(err) + return + } + + _, err = cfg.MinioClient.PutObject(cfg.Context, cfg.Bucket, fileMeta.ID.String(), newReader, length, minio.PutObjectOptions{ContentType: fileType.MIME.Value}) + + if err != nil { + c.JSON(500, gin.H{"error": "failed to upload file"}) + log.Println(err) + return + } + + c.JSON(200, gin.H{"message": "file uploaded successfully", "name": fileMeta.ID.String()}) }) - api.DELETE("/:uid/:name", auth.JwtMiddleware(cfg.JWTSecret), func(c *gin.Context) { + api.DELETE("/:id", auth.JwtMiddleware(cfg.JWTSecret), func(c *gin.Context) { claims := c.MustGet("claims").(jwt.MapClaims) user := claims["user"].(auth.User) @@ -77,70 +110,77 @@ func RegisterFileRoutes(cfg *types.StereoConfig, api *gin.RouterGroup) { return } - filename := c.Param("name") - filename = strings.TrimSpace(filename) - if filename == "" { - c.JSON(400, gin.H{"error": "filename is required"}) - return - } - - path := filepath.Join(cfg.ImagePath, uid, filename) - if _, err := os.Stat(path); os.IsNotExist(err) { + fileID, err := uuid.FromString(strings.TrimSpace(c.Param("id"))) + if err != nil { c.JSON(404, gin.H{"error": "file not found"}) return } - if err := os.Remove(path); err != nil { + var file *types.File + + cfg.Database.First(&file, fileID) + + if file == nil { + c.JSON(404, gin.H{"error": "file not found"}) + return + } + + if err := cfg.MinioClient.RemoveObject(cfg.Context, cfg.Bucket, fileID.String(), minio.RemoveObjectOptions{}); err != nil { c.JSON(500, gin.H{"error": "failed to delete file"}) return } - if err := cfg.Database.Where("owner = ? AND name = ?", uid, filename).Delete(&types.File{}).Error; err != nil { + + if err := cfg.Database.Delete(&file).Error; err != nil { c.JSON(500, gin.H{"error": "failed to delete file metadata"}) return } + c.JSON(200, gin.H{"message": "file deleted successfully"}) }) - api.GET("/:uid/:name", func(c *gin.Context) { - uid := c.Param("uid") - uid = strings.TrimSpace(uid) - if uid == "" { - c.JSON(400, gin.H{"error": "uid is required"}) + api.GET("/:id", func(c *gin.Context) { + fileID := c.Param("id") + fileID = strings.TrimSpace(fileID) + + var file *types.File + + id, err := uuid.FromString(fileID) + if err != nil { + c.JSON(500, gin.H{"error": "invalid file id"}) + log.Println(err) return } - filename := c.Param("name") - filename = strings.TrimSpace(filename) + cfg.Database.First(&file, id) - safe := "" - for _, r := range filename { - if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || - (r >= '0' && r <= '9') || r == '_' || r == '.' || r == '-' { - safe += string(r) - } else { - safe += "_" - } - } - filename = safe - - // parts := strings.SplitN(name, "_", 2) - // if len(parts) != 2 { - // c.JSON(400, gin.H{"error": "invalid file name"}) - // return - // } - - // uid, filename := parts[0], parts[1] - // if uid == "" || filename == "" { - // c.JSON(400, gin.H{"error": "invalid file name"}) - // return - // } - - path := filepath.Join(cfg.ImagePath, uid, filename) - if _, err := os.Stat(path); err != nil { - c.JSON(404, gin.H{"error": "file not found"}) + if file == nil { + c.JSON(500, gin.H{"error": "file does not exist"}) + log.Println(err) return } - c.File(path) + + object, err := cfg.MinioClient.GetObject(cfg.Context, cfg.Bucket, fileID, minio.GetObjectOptions{}) + if err != nil { + c.JSON(500, gin.H{"error": "failed to retrieve file"}) + log.Println(err) + return + } + + stat, err := object.Stat() + if err != nil { + c.JSON(500, gin.H{"error": "failed to retrieve file"}) + log.Println(err) + return + } + + fileType := filetype.GetType(file.Extension) + if err != nil { + c.JSON(500, gin.H{"error": "failed to retrieve file"}) + log.Println(err) + return + } + + c.DataFromReader(200, stat.Size, fileType.MIME.Value, object, nil) }) api.GET("/list", auth.JwtMiddleware(cfg.JWTSecret), func(c *gin.Context) { diff --git a/internal/types/types.go b/internal/types/types.go index 9dd0178..70ab2cb 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1,9 +1,12 @@ package types import ( + "context" "time" "github.com/gin-gonic/gin" + "github.com/gofrs/uuid" + "github.com/minio/minio-go/v7" "gorm.io/gorm" "stereo.cat/backend/internal/auth/client" ) @@ -16,17 +19,26 @@ type Route struct { type StereoConfig struct { ImagePath string + Bucket string + MinioClient *minio.Client Router *gin.Engine Client client.Client Database *gorm.DB JWTSecret string FrontendUri string Domain string + Context context.Context } type File struct { - Name string `gorm:"primaryKey"` + ID uuid.UUID `gorm:"type:uuid;primaryKey"` Owner string `gorm:"not null;index"` Size int64 `gorm:"not null;type:bigint"` CreatedAt time.Time `gorm:"autoCreateTime"` + Extension string +} + +func (f *File) BeforeCreate(tx *gorm.DB) (err error) { + f.ID, err = uuid.NewV4() + return } diff --git a/main.go b/main.go index 5b58333..7d23b8f 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "fmt" "log" @@ -8,6 +9,8 @@ import ( "github.com/gin-gonic/gin" "github.com/joho/godotenv" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" @@ -46,22 +49,34 @@ func main() { } } + minioClient, err := minio.New( + requireEnv("S3_ENDPOINT"), + &minio.Options{ + Creds: credentials.NewStaticV4(requireEnv("S3_KEY"), requireEnv("S3_SECRET"), ""), + Secure: true, + }, + ) + + if err != nil { + log.Fatal(err) + } + c := types.StereoConfig{ - Router: gin.Default(), - ImagePath: imagePath, + Router: gin.Default(), + MinioClient: minioClient, + Bucket: requireEnv("S3_BUCKET"), + Context: context.Background(), + ImagePath: imagePath, Client: client.New( requireEnv("REDIRECT_URI"), requireEnv("CLIENT_ID"), requireEnv("CLIENT_SECRET"), ), FrontendUri: requireEnv("FRONTEND_URI"), - Domain: requireEnv("DOMAIN"), - JWTSecret: requireEnv("JWT_SECRET"), + Domain: requireEnv("DOMAIN"), + JWTSecret: requireEnv("JWT_SECRET"), } - log.Println(c.Domain) - log.Println(c.FrontendUri) - switch databaseType { case "sqlite": db, err := gorm.Open(sqlite.Open(sqliteFile), &gorm.Config{})