diff --git a/.gitignore b/.gitignore index e503885..92bdb86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ config.yml cache/ -opensavecloudserver \ No newline at end of file +opensavecloudserver +storage/ +.idea/ \ No newline at end of file diff --git a/authentication/authentication.go b/authentication/authentication.go index f96242f..133ebfb 100644 --- a/authentication/authentication.go +++ b/authentication/authentication.go @@ -72,7 +72,7 @@ func Register(user *Registration) error { if err != nil { return err } - return database.AddUser(user.Username, user.Firstname, user.Lastname, hash, user.Pronouns) + return database.AddUser(user.Username, hash) } func token(userId int) (string, error) { diff --git a/config.default.yml b/config.default.yml index 5d666ab..f1f6891 100644 --- a/config.default.yml +++ b/config.default.yml @@ -6,4 +6,6 @@ database: username: root features: allow_register: false -cache: "/var/osc/cache" +path: + cache: "/var/osc/cache" + storage: "/var/osc/storage" \ No newline at end of file diff --git a/config/config.go b/config/config.go index aac3ff1..c5211ad 100644 --- a/config/config.go +++ b/config/config.go @@ -10,7 +10,12 @@ import ( type Configuration struct { Database DatabaseConfiguration `yaml:"database"` Features FeaturesConfiguration `yaml:"features"` - Cache string `yaml:"cache"` + Path PathConfiguration `yaml:"path"` +} + +type PathConfiguration struct { + Cache string `yaml:"cache"` + Storage string `yaml:"storage"` } type DatabaseConfiguration struct { @@ -47,6 +52,6 @@ func Features() *FeaturesConfiguration { return ¤tConfig.Features } -func Cache() string { - return currentConfig.Cache +func Path() *PathConfiguration { + return ¤tConfig.Path } diff --git a/database/database.go b/database/database.go index 458df08..8e61358 100644 --- a/database/database.go +++ b/database/database.go @@ -1,19 +1,28 @@ package database import ( + "errors" "fmt" + "github.com/google/uuid" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" "log" "opensavecloudserver/config" "os" + "sync" "time" ) +var ( + locks map[int]GameUploadToken + mu sync.Mutex +) + var db *gorm.DB func init() { + locks = make(map[int]GameUploadToken) dbConfig := config.Database() var err error db, err = gorm.Open(mysql.Open( @@ -36,8 +45,15 @@ func init() { if err != nil { log.Fatal(err) } + go func() { + for { + time.Sleep(time.Minute) + clearLocks() + } + }() } +// UserByUsername get a user by the username func UserByUsername(username string) (*User, error) { var user *User err := db.Model(User{}).Where(User{Username: username}).First(&user).Error @@ -47,6 +63,7 @@ func UserByUsername(username string) (*User, error) { return user, nil } +// UserById get a user func UserById(userId int) (*User, error) { var user *User err := db.Model(User{}).Where(User{ID: userId}).First(&user).Error @@ -56,6 +73,7 @@ func UserById(userId int) (*User, error) { return user, nil } +// AddUser register a user func AddUser(username string, password []byte) error { user := &User{ Username: username, @@ -64,6 +82,7 @@ func AddUser(username string, password []byte) error { return db.Save(user).Error } +// GameInfoById return information of a game func GameInfoById(userId, gameId int) (*Game, error) { var game *Game err := db.Model(Game{}).Where(Game{ID: gameId, UserId: userId}).First(&game).Error @@ -73,6 +92,7 @@ func GameInfoById(userId, gameId int) (*Game, error) { return game, nil } +// GameInfosByUserId get all saved games for a user func GameInfosByUserId(userId int) ([]*Game, error) { var games []*Game err := db.Model(Game{}).Where(Game{UserId: userId}).Find(&games).Error @@ -81,3 +101,53 @@ func GameInfosByUserId(userId int) ([]*Game, error) { } return games, nil } + +// CreateGame create an entry for a new game save, do this only for create a new entry +func CreateGame(userId int, name string) (*Game, error) { + gameUUID := uuid.New() + game := &Game{ + Name: name, + Revision: 0, + PathStorage: gameUUID.String(), + UserId: userId, + Available: false, + } + if err := db.Save(&game).Error; err != nil { + return nil, err + } + return game, nil +} + +// AskForUpload Create a lock for upload a new revision of a game +func AskForUpload(userId, gameId int) (*GameUploadToken, error) { + mu.Lock() + defer mu.Unlock() + _, err := GameInfoById(userId, gameId) + if err != nil { + return nil, err + } + if _, ok := locks[gameId]; !ok { + token := uuid.New() + return &GameUploadToken{ + GameId: gameId, + UploadToken: token.String(), + }, nil + } + return nil, errors.New("game already locked") +} + +// clearLocks clear lock of zombi upload +func clearLocks() { + mu.Lock() + defer mu.Unlock() + now := time.Now() + toUnlock := make([]int, 0) + for gameId, lock := range locks { + if lock.Expire.After(now) { + toUnlock = append(toUnlock, gameId) + } + } + for _, gameId := range toUnlock { + delete(locks, gameId) + } +} diff --git a/database/model.go b/database/model.go index 142dcd1..bf925d3 100644 --- a/database/model.go +++ b/database/model.go @@ -18,3 +18,9 @@ type Game struct { UserId int `json:"-"` Available bool `json:"available"` } + +type GameUploadToken struct { + GameId int `json:"-"` + UploadToken string `json:"upload_token"` + Expire time.Time `json:"expire"` +} diff --git a/go.mod b/go.mod index a77f7a2..b4669e4 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/go-chi/chi/v5 v5.0.7 github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/google/uuid v1.3.0 golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b gorm.io/driver/mysql v1.3.3 diff --git a/go.sum b/go.sum index faefa0c..ad26ba0 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas= diff --git a/server/data.go b/server/data.go index abb4e43..0182d8f 100644 --- a/server/data.go +++ b/server/data.go @@ -1 +1,76 @@ package server + +import ( + "encoding/json" + "io" + "log" + "net/http" + "opensavecloudserver/database" +) + +type NewGameInfo struct { + Name string `json:"name"` +} + +type UploadGameInfo struct { + GameId int `json:"game_id"` +} + +type LockError struct { + Message string `json:"message"` +} + +func CreateGame(w http.ResponseWriter, r *http.Request) { + userId, err := userIdFromContext(r.Context()) + if err != nil { + internalServerError(w, r) + return + } + body, err := io.ReadAll(r.Body) + if err != nil { + internalServerError(w, r) + log.Println(err) + return + } + gameInfo := new(NewGameInfo) + err = json.Unmarshal(body, gameInfo) + if err != nil { + internalServerError(w, r) + log.Println(err) + return + } + game, err := database.CreateGame(userId, gameInfo.Name) + if err != nil { + internalServerError(w, r) + log.Println(err) + return + } + ok(game, w, r) +} + +func AskForUpload(w http.ResponseWriter, r *http.Request) { + userId, err := userIdFromContext(r.Context()) + if err != nil { + internalServerError(w, r) + return + } + body, err := io.ReadAll(r.Body) + if err != nil { + internalServerError(w, r) + log.Println(err) + return + } + gameInfo := new(UploadGameInfo) + err = json.Unmarshal(body, gameInfo) + if err != nil { + internalServerError(w, r) + log.Println(err) + return + } + token, err := database.AskForUpload(userId, gameInfo.GameId) + if err != nil { + ok(LockError{Message: err.Error()}, w, r) + return + } + ok(token, w, r) +} diff --git a/server/server.go b/server/server.go index 6e599e4..4cc8031 100644 --- a/server/server.go +++ b/server/server.go @@ -20,16 +20,20 @@ func Serve() { router := chi.NewRouter() router.Use(middleware.Logger) router.Use(recovery) - router.Route("/api", func(r chi.Router) { - r.Post("/login", Login) - if config.Features().AllowRegister { - r.Post("/register", Register) - } - r.Route("/system", func(systemRouter chi.Router) { - systemRouter.Get("/information", Information) - }) - r.Group(func(secureRouter chi.Router) { - secureRouter.Use(authMiddleware) + router.Route("/api", func(rApi chi.Router) { + rApi.Route("/v1", func(r chi.Router) { + r.Post("/login", Login) + if config.Features().AllowRegister { + r.Post("/register", Register) + } + r.Route("/system", func(systemRouter chi.Router) { + systemRouter.Get("/information", Information) + }) + r.Group(func(secureRouter chi.Router) { + secureRouter.Use(authMiddleware) + secureRouter.Post("/game/create", CreateGame) + secureRouter.Post("/game/upload/init", AskForUpload) + }) }) }) log.Println("Server is listening...")