First upload endpoint

This commit is contained in:
Aurélie Delhaie
2022-05-08 14:48:51 +02:00
parent b20a53cc48
commit 445f7e8508
10 changed files with 183 additions and 16 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
config.yml config.yml
cache/ cache/
opensavecloudserver opensavecloudserver
storage/
.idea/

View File

@@ -72,7 +72,7 @@ func Register(user *Registration) error {
if err != nil { if err != nil {
return err 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) { func token(userId int) (string, error) {

View File

@@ -6,4 +6,6 @@ database:
username: root username: root
features: features:
allow_register: false allow_register: false
path:
cache: "/var/osc/cache" cache: "/var/osc/cache"
storage: "/var/osc/storage"

View File

@@ -10,7 +10,12 @@ import (
type Configuration struct { type Configuration struct {
Database DatabaseConfiguration `yaml:"database"` Database DatabaseConfiguration `yaml:"database"`
Features FeaturesConfiguration `yaml:"features"` Features FeaturesConfiguration `yaml:"features"`
Path PathConfiguration `yaml:"path"`
}
type PathConfiguration struct {
Cache string `yaml:"cache"` Cache string `yaml:"cache"`
Storage string `yaml:"storage"`
} }
type DatabaseConfiguration struct { type DatabaseConfiguration struct {
@@ -47,6 +52,6 @@ func Features() *FeaturesConfiguration {
return &currentConfig.Features return &currentConfig.Features
} }
func Cache() string { func Path() *PathConfiguration {
return currentConfig.Cache return &currentConfig.Path
} }

View File

@@ -1,19 +1,28 @@
package database package database
import ( import (
"errors"
"fmt" "fmt"
"github.com/google/uuid"
"gorm.io/driver/mysql" "gorm.io/driver/mysql"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger" "gorm.io/gorm/logger"
"log" "log"
"opensavecloudserver/config" "opensavecloudserver/config"
"os" "os"
"sync"
"time" "time"
) )
var (
locks map[int]GameUploadToken
mu sync.Mutex
)
var db *gorm.DB var db *gorm.DB
func init() { func init() {
locks = make(map[int]GameUploadToken)
dbConfig := config.Database() dbConfig := config.Database()
var err error var err error
db, err = gorm.Open(mysql.Open( db, err = gorm.Open(mysql.Open(
@@ -36,8 +45,15 @@ func init() {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
go func() {
for {
time.Sleep(time.Minute)
clearLocks()
}
}()
} }
// UserByUsername get a user by the username
func UserByUsername(username string) (*User, error) { func UserByUsername(username string) (*User, error) {
var user *User var user *User
err := db.Model(User{}).Where(User{Username: username}).First(&user).Error err := db.Model(User{}).Where(User{Username: username}).First(&user).Error
@@ -47,6 +63,7 @@ func UserByUsername(username string) (*User, error) {
return user, nil return user, nil
} }
// UserById get a user
func UserById(userId int) (*User, error) { func UserById(userId int) (*User, error) {
var user *User var user *User
err := db.Model(User{}).Where(User{ID: userId}).First(&user).Error err := db.Model(User{}).Where(User{ID: userId}).First(&user).Error
@@ -56,6 +73,7 @@ func UserById(userId int) (*User, error) {
return user, nil return user, nil
} }
// AddUser register a user
func AddUser(username string, password []byte) error { func AddUser(username string, password []byte) error {
user := &User{ user := &User{
Username: username, Username: username,
@@ -64,6 +82,7 @@ func AddUser(username string, password []byte) error {
return db.Save(user).Error return db.Save(user).Error
} }
// GameInfoById return information of a game
func GameInfoById(userId, gameId int) (*Game, error) { func GameInfoById(userId, gameId int) (*Game, error) {
var game *Game var game *Game
err := db.Model(Game{}).Where(Game{ID: gameId, UserId: userId}).First(&game).Error 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 return game, nil
} }
// GameInfosByUserId get all saved games for a user
func GameInfosByUserId(userId int) ([]*Game, error) { func GameInfosByUserId(userId int) ([]*Game, error) {
var games []*Game var games []*Game
err := db.Model(Game{}).Where(Game{UserId: userId}).Find(&games).Error err := db.Model(Game{}).Where(Game{UserId: userId}).Find(&games).Error
@@ -81,3 +101,53 @@ func GameInfosByUserId(userId int) ([]*Game, error) {
} }
return games, nil 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)
}
}

View File

@@ -18,3 +18,9 @@ type Game struct {
UserId int `json:"-"` UserId int `json:"-"`
Available bool `json:"available"` Available bool `json:"available"`
} }
type GameUploadToken struct {
GameId int `json:"-"`
UploadToken string `json:"upload_token"`
Expire time.Time `json:"expire"`
}

1
go.mod
View File

@@ -5,6 +5,7 @@ go 1.18
require ( require (
github.com/go-chi/chi/v5 v5.0.7 github.com/go-chi/chi/v5 v5.0.7
github.com/golang-jwt/jwt v3.2.2+incompatible 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 golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
gorm.io/driver/mysql v1.3.3 gorm.io/driver/mysql v1.3.3

2
go.sum
View File

@@ -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/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 h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 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 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 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= github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=

View File

@@ -1 +1,76 @@
package server 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)
}

View File

@@ -20,7 +20,8 @@ func Serve() {
router := chi.NewRouter() router := chi.NewRouter()
router.Use(middleware.Logger) router.Use(middleware.Logger)
router.Use(recovery) router.Use(recovery)
router.Route("/api", func(r chi.Router) { router.Route("/api", func(rApi chi.Router) {
rApi.Route("/v1", func(r chi.Router) {
r.Post("/login", Login) r.Post("/login", Login)
if config.Features().AllowRegister { if config.Features().AllowRegister {
r.Post("/register", Register) r.Post("/register", Register)
@@ -30,6 +31,9 @@ func Serve() {
}) })
r.Group(func(secureRouter chi.Router) { r.Group(func(secureRouter chi.Router) {
secureRouter.Use(authMiddleware) secureRouter.Use(authMiddleware)
secureRouter.Post("/game/create", CreateGame)
secureRouter.Post("/game/upload/init", AskForUpload)
})
}) })
}) })
log.Println("Server is listening...") log.Println("Server is listening...")