Compare commits

...

7 commits

Author SHA1 Message Date
grngxd
81aa06d716 Merge branch 'state' into dev 2025-08-01 10:46:28 +01:00
grngxd
997774c5de use thumbhash instead of blurhash 2025-08-01 10:45:56 +01:00
grngxd
264739fd76 add blurhash 2025-08-01 08:43:25 +01:00
grngxd
bc42a2bb75 enhance file retrieval: order files by creation date in queries 2025-08-01 08:33:09 +01:00
grngxd
1c5eca8f72 oops 2025-07-31 22:48:50 +01:00
grngxd
698f3b1a02 revert commit & add meta/:id route 2025-07-31 22:46:34 +01:00
grngxd
b48a610e90 add roles 2025-07-31 22:08:54 +01:00
7 changed files with 90 additions and 42 deletions

1
api.md
View file

@ -9,4 +9,5 @@
| POST | /api/upload | Upload file | Upload key | | POST | /api/upload | Upload file | Upload key |
| DELETE | /api/:id | Delete file | Upload key | | DELETE | /api/:id | Delete file | Upload key |
| GET | /api/:id | Get file | Upload key | | GET | /api/:id | Get file | Upload key |
| GET | /api/:id/meta | Get file metadata | None |
| GET | /api/list | Get a list of uploaded files | Session key | | GET | /api/list | Get a list of uploaded files | Session key |

19
go.mod
View file

@ -3,8 +3,17 @@ module stereo.cat/backend
go 1.24.2 go 1.24.2
require ( require (
github.com/cristalhq/base64 v0.1.2
github.com/galdor/go-thumbhash v1.0.0
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
github.com/gofrs/uuid v4.4.0+incompatible
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/h2non/filetype v1.1.3
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/minio/minio-go/v7 v7.0.93
golang.org/x/crypto v0.37.0
gorm.io/driver/postgres v1.5.11
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.26.0 gorm.io/gorm v1.26.0
) )
@ -12,7 +21,6 @@ require (
github.com/bytedance/sonic v1.13.2 // indirect github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cristalhq/base64 v0.1.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
@ -21,10 +29,7 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/goccy/go-json v0.10.5 // 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/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/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.4 // indirect github.com/jackc/pgx/v5 v5.7.4 // indirect
@ -36,12 +41,10 @@ require (
github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/leodido/go-urn v1.4.0 // 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-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.28 // indirect github.com/mattn/go-sqlite3 v1.14.28 // indirect
github.com/minio/crc64nvme v1.0.1 // indirect github.com/minio/crc64nvme v1.0.1 // indirect
github.com/minio/md5-simd v1.1.2 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
@ -52,14 +55,10 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.16.0 // indirect golang.org/x/arch v0.16.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect golang.org/x/net v0.39.0 // indirect
golang.org/x/sync v0.14.0 // indirect golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.32.0 // indirect golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect golang.org/x/text v0.24.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/postgres v1.5.11 // indirect
gorm.io/driver/sqlite v1.5.7 // indirect
) )

7
go.sum
View file

@ -16,6 +16,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/galdor/go-thumbhash v1.0.0 h1:Q7xSnaDvSC91SuNmQI94JuUVHva29FDdA4/PkV0EHjU=
github.com/galdor/go-thumbhash v1.0.0/go.mod h1:gEK2wZqIxS2W4mXNf48lPl6HWjX0vWsH1LpK/cU74Ho=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 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-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 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
@ -66,17 +68,12 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c=
github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=

View file

@ -19,11 +19,18 @@ package routes
import ( import (
"bytes" "bytes"
"encoding/base64"
"io" "io"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"github.com/galdor/go-thumbhash"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
@ -78,18 +85,35 @@ func RegisterFileRoutes(cfg *types.StereoConfig, api *gin.RouterGroup) {
} }
fileType, err := filetype.Match(buf) fileType, err := filetype.Match(buf)
if err != nil { if err != nil {
types.ErrorReaderOpen.Throw(c, err) types.ErrorInvalidFile.Throw(c, err)
return return
} }
mime := fileType.MIME.Value
fileMeta := types.File{ fileMeta := types.File{
Owner: uid, Owner: uid,
Name: file.Filename, Name: file.Filename,
CreatedAt: time.Now(), CreatedAt: time.Now(),
Size: int64(len(buf)), Size: int64(len(buf)),
Mime: fileType.MIME.Value, Mime: mime,
}
if filetype.IsImage(buf) {
img, _, err := image.Decode(bytes.NewReader(buf))
if err != nil {
types.ErrorInvalidFile.Throw(c, err)
return
}
hashBytes := thumbhash.EncodeImage(img)
// encode the byte[] into a string
hash := base64.StdEncoding.EncodeToString(hashBytes)
fileMeta.Hash = hash
fileMeta.Width = img.Bounds().Dx()
fileMeta.Height = img.Bounds().Dy()
} }
if err := cfg.Database.Create(&fileMeta).Error; err != nil { if err := cfg.Database.Create(&fileMeta).Error; err != nil {
@ -176,13 +200,38 @@ func RegisterFileRoutes(cfg *types.StereoConfig, api *gin.RouterGroup) {
c.DataFromReader(200, file.Size, file.Mime, object, nil) c.DataFromReader(200, file.Size, file.Mime, object, nil)
}) })
api.GET("/:id/meta", func(c *gin.Context) {
fileID := c.Param("id")
fileID = strings.TrimSpace(fileID)
var file *types.File
id, err := uuid.FromString(fileID)
if err != nil {
types.ErrorInvalidFile.Throw(c, err)
return
}
if err := cfg.Database.First(&file, id).Error; err != nil {
types.ErrorFileNotFound.Throw(c, err)
return
}
if file == nil {
types.ErrorFileNotFound.Throw(c, nil)
return
}
c.JSON(200, file)
})
api.GET("/list", session.SessionMiddleware(cfg.JWTSecret), func(c *gin.Context) { api.GET("/list", session.SessionMiddleware(cfg.JWTSecret), func(c *gin.Context) {
claims := c.MustGet("claims").(jwt.MapClaims) claims := c.MustGet("claims").(jwt.MapClaims)
user := claims["user"].(auth.User) user := claims["user"].(auth.User)
var files []types.File
if c.Query("page") == "" || c.Query("size") == "" { if c.Query("page") == "" || c.Query("size") == "" {
var files []types.File if err := cfg.Database.Where("owner = ?", user.ID).Order("created_at DESC").Find(&files).Error; err != nil {
if err := cfg.Database.Where("owner = ?", user.ID).Find(&files).Error; err != nil {
types.ErrorDatabase.Throw(c, err) types.ErrorDatabase.Throw(c, err)
return return
} }
@ -206,12 +255,12 @@ func RegisterFileRoutes(cfg *types.StereoConfig, api *gin.RouterGroup) {
return return
} }
var files []types.File
offset := (pageNum - 1) * sizeNum offset := (pageNum - 1) * sizeNum
if offset < 0 { if offset < 0 {
offset = 0 offset = 0
} }
if err := cfg.Database.Where("owner = ?", user.ID).Offset(offset).Limit(sizeNum).Find(&files).Error; err != nil {
if err := cfg.Database.Where("owner = ?", user.ID).Order("created_at DESC").Offset(offset).Limit(sizeNum).Find(&files).Error; err != nil {
types.ErrorDatabase.Throw(c, err) types.ErrorDatabase.Throw(c, err)
return return
} }

View file

@ -32,12 +32,12 @@ type TokenResponse struct {
} }
type User struct { type User struct {
ID string `json:"id" gorm:"primaryKey"` ID string `json:"id" gorm:"primaryKey"`
Username string `json:"username"` Username string `json:"username"`
Blacklisted bool `json:"blacklisted"` Blacklisted bool `json:"blacklisted"`
Email string `json:"email"` Email string `json:"email"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
HashedApiKey string `json:"hashed_api_key"` HashedApiKey string `json:"hashed_api_key"`
} }
type AvatarDecorationData struct { type AvatarDecorationData struct {

View file

@ -1,23 +1,22 @@
/* /*
Copyright (C) 2025 hexlocation (hex@iwakura.rip) & grngxd (grng@iwakura.rip) Copyright (C) 2025 hexlocation (hex@iwakura.rip) & grngxd (grng@iwakura.rip)
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package types package types
import ( import (
"errors"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -37,7 +36,7 @@ func (e *StereoError) Throw(c *gin.Context, err error) {
if err != nil { if err != nil {
c.Error(err) c.Error(err)
} else { } else {
c.Error(errors.New(fmt.Sprintf("Got an error with code: %v", e.Code))) c.Error(fmt.Errorf("got an error with code: %v", e.Code))
} }
} }

View file

@ -53,7 +53,10 @@ type File struct {
Owner string `gorm:"not null;index"` Owner string `gorm:"not null;index"`
Size int64 `gorm:"not null;type:bigint"` Size int64 `gorm:"not null;type:bigint"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
Mime string Mime string `gorm:"type:text"`
Hash string `gorm:"type:text"`
Width int
Height int
} }
func (f *File) BeforeCreate(tx *gorm.DB) (err error) { func (f *File) BeforeCreate(tx *gorm.DB) (err error) {