diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..65326bb --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 84083eb..332f139 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,36 @@ jobs: uses: actions/checkout@v3 - name: Download dependencies - run: sudo apt update && sudo apt install -y build-essential libpng-dev + run: | + sudo apt update && sudo apt install -y build-essential libpng-dev protobuf-compiler + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 + + - name: Go Generate + run: go generate -tags tools -x ./... + + - name: Build + run: go build -v -o api-linux-amd64 . + env: + CGO_ENABLED: 1 + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.19 + + - name: Check out code into the Go module directory + uses: actions/checkout@v3 + + - name: Download dependencies + run: | + sudo apt update && sudo apt install -y build-essential libpng-dev protobuf-compiler + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 - name: Go Generate run: go generate -tags tools -x ./... @@ -24,11 +53,41 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: + version: v1.49.0 skip-pkg-cache: true skip-build-cache: true args: --timeout 5m - - name: Build - run: go build -v -o api-linux-amd64 . + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.19 + + - name: Check out code into the Go module directory + uses: actions/checkout@v3 + + - name: Download dependencies + run: | + sudo apt update && sudo apt install -y build-essential libpng-dev protobuf-compiler + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 + + - name: Go Generate + run: go generate -tags tools -x ./... + + - name: Start stack + run: docker-compose -f docker-compose-dev.yml up -d + + - name: Test + run: go test -v -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... ./... env: CGO_ENABLED: 1 + REPO_PASETO.PUBLIC_KEY: 408c5155a389aeabf1c1b0da73ff5a3079b6aa6628e4c661b1e1ce412181cc8a + REPO_PASETO.PRIVATE_KEY: a5f7409588f6b72d443db0d432f37f1214a5ec88cb55a70e24b90194ed549465408c5155a389aeabf1c1b0da73ff5a3079b6aa6628e4c661b1e1ce412181cc8a + + - name: Codecov + uses: codecov/codecov-action@v1 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4e54877..fc5a0ef 100755 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,10 @@ jobs: fetch-depth: 0 - name: Download dependencies - run: sudo apt update && sudo apt install -y build-essential libpng-dev + run: | + sudo apt update && sudo apt install -y build-essential libpng-dev protobuf-compiler + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v3 diff --git a/.golangci.yml b/.golangci.yml index 5fd1ecd..37211ec 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -14,6 +14,32 @@ linters-settings: ignorePackageGlobs: - github.com/satisfactorymodding/smr-api/* + govet: + check-shadowing: true + enable-all: true + disable: + - shadow + + gocritic: + disabled-checks: + - ifElseChain + + gci: + custom-order: true + sections: + - standard + - default + - prefix(github.com/satisfactorymodding/smr-api) + - blank + - dot + +run: + skip-files: + - ./generated/generated.go + - ./generated/models_gen.go + skip-dirs: + - ./docs/ + issues: exclude: - should pass the context parameter @@ -21,16 +47,13 @@ issues: linters: disable-all: true enable: - - deadcode - errcheck - gosimple - govet - ineffassign - staticcheck - - structcheck - typecheck - unused - - varcheck - bidichk - contextcheck - durationcheck @@ -38,8 +61,11 @@ linters: - goconst - goimports - revive - - ifshort - misspell - prealloc - whitespace - - wrapcheck \ No newline at end of file + - wrapcheck + - gci + - gocritic + - gofumpt + - nonamedreturns diff --git a/Dockerfile b/Dockerfile index d1d66b8..83c9552 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,8 @@ -FROM golang:1.18-alpine AS builder +FROM golang:1.19-alpine3.18 AS builder -RUN apk add --no-cache git build-base libpng-dev +RUN apk add --no-cache git build-base libpng-dev protoc +RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 +RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 WORKDIR $GOPATH/src/github.com/satisfactorymodding/smr-api/ diff --git a/README.md b/README.md index b9e8dfb..220b9da 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# SMR API [![build](https://github.com/satisfactorymodding/smr-api/actions/workflows/build.yml/badge.svg)](https://github.com/satisfactorymodding/smr-api/actions/workflows/build.yml) +# SMR API ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/satisfactorymodding/smr-api/build) ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/satisfactorymodding/smr-api) [![codecov](https://codecov.io/gh/satisfactorymodding/smr-api/branch/master/graph/badge.svg?token=LFNKYWS0N2)](https://codecov.io/gh/satisfactorymodding/smr-api) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/satisfactorymodding/smr-api) The Satisfactory Mod Repository backend API @@ -30,7 +30,7 @@ To run the API, you will need to have a working Postgres, Redis and Storage. The start via: ```bash -docker-compose -f docker-compose.dev.yml up -d +docker-compose -f docker-compose-dev.yml up -d ``` It is suggested you create a configuration file at `config.json` (but you can also use environment variables). @@ -49,6 +49,14 @@ Main configuration options: The config format can be seen in `config/config.go` (each dot means a new level of nesting). +After startup requires the following minio commands to be executed: + +```shell +mc alias set local http://localhost:9000 minio minio123 +mc admin user svcacct add local minio --access-key REPLACE_ME_KEY --secret-key REPLACE_ME_SECRET +mc anonymous set public local/smr +``` + ## Contributing Before contributing, please run the [linter](https://golangci-lint.run/) to ensure the code is clean and well-formed: diff --git a/api.go b/api.go index c167f7f..ed58933 100644 --- a/api.go +++ b/api.go @@ -9,30 +9,6 @@ import ( "syscall" "time" - "github.com/satisfactorymodding/smr-api/auth" - "github.com/satisfactorymodding/smr-api/config" - "github.com/satisfactorymodding/smr-api/dataloader" - "github.com/satisfactorymodding/smr-api/db" - "github.com/satisfactorymodding/smr-api/db/postgres" - - "github.com/pkg/errors" - - // Load REST docs - _ "github.com/satisfactorymodding/smr-api/docs" - "github.com/satisfactorymodding/smr-api/generated" - "github.com/satisfactorymodding/smr-api/gql" - "github.com/satisfactorymodding/smr-api/migrations" - "github.com/satisfactorymodding/smr-api/nodes" - "github.com/satisfactorymodding/smr-api/oauth" - "github.com/satisfactorymodding/smr-api/redis" - "github.com/satisfactorymodding/smr-api/redis/jobs" - - // Load redis consumers - _ "github.com/satisfactorymodding/smr-api/redis/jobs/consumers" - "github.com/satisfactorymodding/smr-api/storage" - "github.com/satisfactorymodding/smr-api/util" - "github.com/satisfactorymodding/smr-api/validation" - "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/handler/extension" "github.com/99designs/gqlgen/graphql/handler/lru" @@ -42,6 +18,7 @@ import ( "github.com/labstack/echo-contrib/pprof" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" + "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/spf13/viper" echoSwagger "github.com/swaggo/echo-swagger" @@ -54,6 +31,27 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.7.0" "go.opentelemetry.io/otel/trace" "gopkg.in/go-playground/validator.v9" + + "github.com/satisfactorymodding/smr-api/auth" + "github.com/satisfactorymodding/smr-api/config" + "github.com/satisfactorymodding/smr-api/dataloader" + "github.com/satisfactorymodding/smr-api/db" + "github.com/satisfactorymodding/smr-api/db/postgres" + "github.com/satisfactorymodding/smr-api/generated" + "github.com/satisfactorymodding/smr-api/gql" + "github.com/satisfactorymodding/smr-api/migrations" + "github.com/satisfactorymodding/smr-api/nodes" + "github.com/satisfactorymodding/smr-api/oauth" + "github.com/satisfactorymodding/smr-api/redis" + "github.com/satisfactorymodding/smr-api/redis/jobs" + "github.com/satisfactorymodding/smr-api/storage" + "github.com/satisfactorymodding/smr-api/util" + "github.com/satisfactorymodding/smr-api/validation" + + // Load REST docs + _ "github.com/satisfactorymodding/smr-api/docs" + // Load redis consumers + _ "github.com/satisfactorymodding/smr-api/redis/jobs/consumers" ) type CustomValidator struct { @@ -64,8 +62,8 @@ func (cv *CustomValidator) Validate(i interface{}) error { return errors.Wrap(cv.validator.Struct(i), "validation error") } -func Serve() { - ctx := config.InitializeConfig() +func Initialize(baseCtx context.Context) context.Context { + ctx := config.InitializeConfig(baseCtx) if os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") != "" { cleanup := installExportPipeline(ctx) @@ -81,9 +79,18 @@ func Serve() { auth.InitializeAuth() jobs.InitializeJobs(ctx) validation.InitializeVirusTotal() + util.PrintFeatureFlags() + return ctx +} + +func Migrate(ctx context.Context) { migrations.RunMigrations(ctx) +} +var e *echo.Echo + +func Setup(ctx context.Context) { if viper.GetBool("profiler") { go func() { debugServer := echo.New() @@ -101,7 +108,7 @@ func Serve() { dataValidator := validator.New() - e := echo.New() + e = echo.New() e.HideBanner = true e.Validator = &CustomValidator{validator: dataValidator} @@ -271,7 +278,9 @@ func Serve() { <-signals _ = e.Close() }() +} +func Serve() { address := fmt.Sprintf(":%d", viper.GetInt("port")) log.Info().Str("address", address).Msg("starting server") @@ -311,3 +320,14 @@ func newResource() *resource.Resource { ) return r } + +func Start() { + ctx := Initialize(context.Background()) + Migrate(ctx) + Setup(ctx) + Serve() +} + +func Stop() error { + return errors.Wrap(e.Close(), "failed to stop http server") +} diff --git a/auth/permissions.go b/auth/permissions.go index 85f1588..9a84f68 100755 --- a/auth/permissions.go +++ b/auth/permissions.go @@ -105,8 +105,10 @@ var ( } ) -var idToGroupMapping = make(map[string]*Group) -var roleToGroupMapping = make(map[*Role][]*Group) +var ( + idToGroupMapping = make(map[string]*Group) + roleToGroupMapping = make(map[*Role][]*Group) +) func initializePermissions() { groups := []*Group{GroupAdmin, GroupModerator, GroupSMLDev, GroupBootstrapDev, GroupCompatibilityOfficer} diff --git a/cmd/api/serve.go b/cmd/api/serve.go index 250aba1..2fc957c 100644 --- a/cmd/api/serve.go +++ b/cmd/api/serve.go @@ -1,6 +1,6 @@ package main -import smr "github.com/satisfactorymodding/smr-api" +import "github.com/satisfactorymodding/smr-api" // @title Satisfactory Mod Repo API // @version 1 @@ -12,5 +12,5 @@ import smr "github.com/satisfactorymodding/smr-api" // @host api.ficsit.app // @BasePath /v1 func main() { - smr.Serve() + smr.Start() } diff --git a/cmd/paseto/main.go b/cmd/paseto/main.go index e09e7f1..c282972 100644 --- a/cmd/paseto/main.go +++ b/cmd/paseto/main.go @@ -7,7 +7,6 @@ import ( func main() { publicKey, privateKey, err := ed25519.GenerateKey(nil) - if err != nil { panic(err) } diff --git a/cmd/validate-zip/main.go b/cmd/validate-zip/main.go index ea1707d..77dad0d 100644 --- a/cmd/validate-zip/main.go +++ b/cmd/validate-zip/main.go @@ -16,7 +16,6 @@ func main() { validation.InitializeValidator() _, err := validation.ExtractModInfo(context.Background(), f, true, true, "N/A") - if err != nil { panic(err) } diff --git a/config.sample.json b/config.sample.json index 6003768..f865d27 100644 --- a/config.sample.json +++ b/config.sample.json @@ -27,7 +27,8 @@ "key": "REPLACE_ME_KEY", "secret": "REPLACE_ME_SECRET", "endpoint": "http://localhost:9000", - "base_url": "http://localhost:9000" + "base_url": "http://localhost:9000", + "keypath": "%s/%s/%s" }, "oauth": { @@ -52,5 +53,9 @@ "frontend": { "url": "http://localhost:4200" + }, + + "feature_flags": { + "allow_multi_target_upload": false } } \ No newline at end of file diff --git a/config/config.go b/config/config.go index c1103c1..1fd6ea3 100644 --- a/config/config.go +++ b/config/config.go @@ -11,9 +11,15 @@ import ( "github.com/spf13/viper" ) -func InitializeConfig() context.Context { +var configDir = "." + +func SetConfigDir(newConfigDir string) { + configDir = newConfigDir +} + +func InitializeConfig(baseCtx context.Context) context.Context { viper.SetConfigName("config") - viper.AddConfigPath(".") + viper.AddConfigPath(configDir) viper.AutomaticEnv() viper.SetEnvPrefix("repo") @@ -30,7 +36,12 @@ func InitializeConfig() context.Context { } log.Logger = zerolog.New(out).With().Str("service", "api").Timestamp().Logger() - ctx := log.Logger.WithContext(context.Background()) + + if baseCtx == nil { + baseCtx = context.Background() + } + + ctx := log.Logger.WithContext(baseCtx) if err != nil { log.Warn().Err(err).Msg("config initialized using defaults and environment only!") @@ -67,6 +78,7 @@ func initializeDefaults() { viper.SetDefault("storage.endpoint", "http://localhost:9000") viper.SetDefault("storage.region", "eu-central-1") viper.SetDefault("storage.base_url", "http://localhost:9000") + viper.SetDefault("storage.keypath", "%s/file/%s/%s") viper.SetDefault("oauth.github.client_id", "") viper.SetDefault("oauth.github.client_secret", "") @@ -88,4 +100,8 @@ func initializeDefaults() { viper.SetDefault("frontend.url", "") viper.SetDefault("virustotal.key", "") + + viper.SetDefault("feature_flags.allow_multi_target_upload", false) + + viper.SetDefault("extractor_host", "localhost:50051") } diff --git a/dataloader/loaders.go b/dataloader/loaders.go index e1f9d88..861ed84 100644 --- a/dataloader/loaders.go +++ b/dataloader/loaders.go @@ -4,10 +4,10 @@ import ( "context" "time" - "github.com/satisfactorymodding/smr-api/db/postgres" - "github.com/labstack/echo/v4" "github.com/patrickmn/go-cache" + + "github.com/satisfactorymodding/smr-api/db/postgres" ) type loadersKey struct{} @@ -109,7 +109,7 @@ func Middleware() func(handlerFunc echo.HandlerFunc) echo.HandlerFunc { var entities []postgres.Version reqCtx := c.Request().Context() - postgres.DBCtx(reqCtx).Where("approved = ? AND denied = ? AND mod_id IN ?", true, false, fetchIds).Order("created_at desc").Find(&entities) + postgres.DBCtx(reqCtx).Preload("Targets").Where("approved = ? AND denied = ? AND mod_id IN ?", true, false, fetchIds).Order("created_at desc").Find(&entities) for _, entity := range entities { byID[entity.ModID] = append(byID[entity.ModID], entity) @@ -145,7 +145,7 @@ func Middleware() func(handlerFunc echo.HandlerFunc) echo.HandlerFunc { var entities []postgres.Version reqCtx := c.Request().Context() - postgres.DBCtx(reqCtx).Select( + postgres.DBCtx(reqCtx).Preload("Targets").Select( "id", "created_at", "updated_at", diff --git a/dataloader/userloader_gen.go b/dataloader/userloader_gen.go index c148a7d..a91e92c 100755 --- a/dataloader/userloader_gen.go +++ b/dataloader/userloader_gen.go @@ -32,34 +32,20 @@ func NewUserLoader(config UserLoaderConfig) *UserLoader { // UserLoader batches and caches requests type UserLoader struct { - // this method provides the data for the loader - fetch func(keys []string) ([]*postgres.User, []error) - - // how long to done before sending a batch - wait time.Duration - - // this will limit the maximum number of keys to send in one batch, 0 = no limit + fetch func(keys []string) ([]*postgres.User, []error) + cache map[string]*postgres.User + batch *userLoaderBatch + wait time.Duration maxBatch int - - // INTERNAL - - // lazily created cache - cache map[string]*postgres.User - - // the current batch. keys will continue to be collected until timeout is hit, - // then everything will be sent to the fetch method and out to the listeners - batch *userLoaderBatch - - // mutex to prevent races - mu sync.Mutex + mu sync.Mutex } type userLoaderBatch struct { + done chan struct{} keys []string data []*postgres.User error []error closing bool - done chan struct{} } // Load a User by key, batching and caching will be applied automatically diff --git a/dataloader/usermodloader_gen.go b/dataloader/usermodloader_gen.go index 3dab894..caf0335 100755 --- a/dataloader/usermodloader_gen.go +++ b/dataloader/usermodloader_gen.go @@ -32,34 +32,20 @@ func NewUserModLoader(config UserModLoaderConfig) *UserModLoader { // UserModLoader batches and caches requests type UserModLoader struct { - // this method provides the data for the loader - fetch func(keys []string) ([][]postgres.UserMod, []error) - - // how long to done before sending a batch - wait time.Duration - - // this will limit the maximum number of keys to send in one batch, 0 = no limit + fetch func(keys []string) ([][]postgres.UserMod, []error) + cache map[string][]postgres.UserMod + batch *userModLoaderBatch + wait time.Duration maxBatch int - - // INTERNAL - - // lazily created cache - cache map[string][]postgres.UserMod - - // the current batch. keys will continue to be collected until timeout is hit, - // then everything will be sent to the fetch method and out to the listeners - batch *userModLoaderBatch - - // mutex to prevent races - mu sync.Mutex + mu sync.Mutex } type userModLoaderBatch struct { + done chan struct{} keys []string data [][]postgres.UserMod error []error closing bool - done chan struct{} } // Load a UserMod by key, batching and caching will be applied automatically diff --git a/dataloader/versiondependencyloader_gen.go b/dataloader/versiondependencyloader_gen.go index a0c3532..dc05046 100755 --- a/dataloader/versiondependencyloader_gen.go +++ b/dataloader/versiondependencyloader_gen.go @@ -32,34 +32,20 @@ func NewVersionDependencyLoader(config VersionDependencyLoaderConfig) *VersionDe // VersionDependencyLoader batches and caches requests type VersionDependencyLoader struct { - // this method provides the data for the loader - fetch func(keys []string) ([][]postgres.VersionDependency, []error) - - // how long to done before sending a batch - wait time.Duration - - // this will limit the maximum number of keys to send in one batch, 0 = no limit + fetch func(keys []string) ([][]postgres.VersionDependency, []error) + cache map[string][]postgres.VersionDependency + batch *versionDependencyLoaderBatch + wait time.Duration maxBatch int - - // INTERNAL - - // lazily created cache - cache map[string][]postgres.VersionDependency - - // the current batch. keys will continue to be collected until timeout is hit, - // then everything will be sent to the fetch method and out to the listeners - batch *versionDependencyLoaderBatch - - // mutex to prevent races - mu sync.Mutex + mu sync.Mutex } type versionDependencyLoaderBatch struct { + done chan struct{} keys []string data [][]postgres.VersionDependency error []error closing bool - done chan struct{} } // Load a VersionDependency by key, batching and caching will be applied automatically diff --git a/dataloader/versionloader_gen.go b/dataloader/versionloader_gen.go index 5fafe79..75cd9c6 100755 --- a/dataloader/versionloader_gen.go +++ b/dataloader/versionloader_gen.go @@ -32,34 +32,20 @@ func NewVersionLoader(config VersionLoaderConfig) *VersionLoader { // VersionLoader batches and caches requests type VersionLoader struct { - // this method provides the data for the loader - fetch func(keys []string) ([][]postgres.Version, []error) - - // how long to done before sending a batch - wait time.Duration - - // this will limit the maximum number of keys to send in one batch, 0 = no limit + fetch func(keys []string) ([][]postgres.Version, []error) + cache map[string][]postgres.Version + batch *versionLoaderBatch + wait time.Duration maxBatch int - - // INTERNAL - - // lazily created cache - cache map[string][]postgres.Version - - // the current batch. keys will continue to be collected until timeout is hit, - // then everything will be sent to the fetch method and out to the listeners - batch *versionLoaderBatch - - // mutex to prevent races - mu sync.Mutex + mu sync.Mutex } type versionLoaderBatch struct { + done chan struct{} keys []string data [][]postgres.Version error []error closing bool - done chan struct{} } // Load a Version by key, batching and caching will be applied automatically diff --git a/dataloader/versionloadernometa_gen.go b/dataloader/versionloadernometa_gen.go index de26282..c55b5ae 100755 --- a/dataloader/versionloadernometa_gen.go +++ b/dataloader/versionloadernometa_gen.go @@ -32,34 +32,20 @@ func NewVersionLoaderNoMeta(config VersionLoaderNoMetaConfig) *VersionLoaderNoMe // VersionLoaderNoMeta batches and caches requests type VersionLoaderNoMeta struct { - // this method provides the data for the loader - fetch func(keys []string) ([][]postgres.Version, []error) - - // how long to done before sending a batch - wait time.Duration - - // this will limit the maximum number of keys to send in one batch, 0 = no limit + fetch func(keys []string) ([][]postgres.Version, []error) + cache map[string][]postgres.Version + batch *versionLoaderNoMetaBatch + wait time.Duration maxBatch int - - // INTERNAL - - // lazily created cache - cache map[string][]postgres.Version - - // the current batch. keys will continue to be collected until timeout is hit, - // then everything will be sent to the fetch method and out to the listeners - batch *versionLoaderNoMetaBatch - - // mutex to prevent races - mu sync.Mutex + mu sync.Mutex } type versionLoaderNoMetaBatch struct { + done chan struct{} keys []string data [][]postgres.Version error []error closing bool - done chan struct{} } // Load a Version by key, batching and caching will be applied automatically diff --git a/db/postgres/announcement.go b/db/postgres/announcement.go index c7e614f..46481be 100644 --- a/db/postgres/announcement.go +++ b/db/postgres/announcement.go @@ -4,6 +4,7 @@ import ( "context" "github.com/patrickmn/go-cache" + "github.com/satisfactorymodding/smr-api/util" ) @@ -27,7 +28,7 @@ func GetAnnouncementByID(ctx context.Context, announcementID string) *Announceme return nil } - dbCache.Set(cacheKey, announcement, cache.DefaultExpiration) + dbCache.Set(cacheKey, &announcement, cache.DefaultExpiration) return &announcement } diff --git a/db/postgres/bootstrap_version.go b/db/postgres/bootstrap_version.go index 4038a87..3e08ea7 100644 --- a/db/postgres/bootstrap_version.go +++ b/db/postgres/bootstrap_version.go @@ -37,7 +37,7 @@ func GetBootstrapVersions(ctx context.Context, filter *models.BootstrapVersionFi Order(string(*filter.OrderBy) + " " + string(*filter.Order)) if filter.Search != nil && *filter.Search != "" { - query = query.Where("to_tsvector(name) @@ to_tsquery(?)", strings.Replace(*filter.Search, " ", " & ", -1)) + query = query.Where("to_tsvector(name) @@ to_tsquery(?)", strings.ReplaceAll(*filter.Search, " ", " & ")) } } @@ -62,7 +62,7 @@ func GetBootstrapVersionCount(ctx context.Context, filter *models.BootstrapVersi if filter != nil { if filter.Search != nil && *filter.Search != "" { - query = query.Where("to_tsvector(name) @@ to_tsquery(?)", strings.Replace(*filter.Search, " ", " & ", -1)) + query = query.Where("to_tsvector(name) @@ to_tsquery(?)", strings.ReplaceAll(*filter.Search, " ", " & ")) } } diff --git a/db/postgres/guide.go b/db/postgres/guide.go index 4406a55..bf938a9 100644 --- a/db/postgres/guide.go +++ b/db/postgres/guide.go @@ -7,10 +7,10 @@ import ( "strings" "time" + "github.com/patrickmn/go-cache" + "github.com/satisfactorymodding/smr-api/models" "github.com/satisfactorymodding/smr-api/util" - - "github.com/patrickmn/go-cache" ) func CreateGuide(ctx context.Context, guide *Guide) (*Guide, error) { @@ -85,7 +85,7 @@ func GetGuides(ctx context.Context, filter *models.GuideFilter) []Guide { Order(string(*filter.OrderBy) + " " + string(*filter.Order)) if filter.Search != nil && *filter.Search != "" { - query = query.Where("to_tsvector(name) @@ to_tsquery(?)", strings.Replace(*filter.Search, " ", " & ", -1)) + query = query.Where("to_tsvector(name) @@ to_tsquery(?)", strings.ReplaceAll(*filter.Search, " ", " & ")) } if filter.TagIDs != nil && len(filter.TagIDs) > 0 { @@ -136,7 +136,7 @@ func GetGuideCount(ctx context.Context, filter *models.GuideFilter) int64 { if filter != nil { if filter.Search != nil && *filter.Search != "" { - query = query.Where("to_tsvector(name) @@ to_tsquery(?)", strings.Replace(*filter.Search, " ", " & ", -1)) + query = query.Where("to_tsvector(name) @@ to_tsquery(?)", strings.ReplaceAll(*filter.Search, " ", " & ")) } } diff --git a/db/postgres/mod.go b/db/postgres/mod.go index c31a4cb..9c4fcf9 100644 --- a/db/postgres/mod.go +++ b/db/postgres/mod.go @@ -7,12 +7,12 @@ import ( "strings" "time" + "github.com/patrickmn/go-cache" + "gorm.io/gorm" + "github.com/satisfactorymodding/smr-api/generated" "github.com/satisfactorymodding/smr-api/models" "github.com/satisfactorymodding/smr-api/util" - - "github.com/patrickmn/go-cache" - "gorm.io/gorm" ) func GetModByID(ctx context.Context, modID string) *Mod { @@ -26,7 +26,7 @@ func GetModByID(ctx context.Context, modID string) *Mod { func GetModByIDNoCache(ctx context.Context, modID string) *Mod { var mod Mod - DBCtx(ctx).Preload("Tags").Find(&mod, "id = ?", modID) + DBCtx(ctx).Preload("Tags").Preload("Versions.Targets").Find(&mod, "id = ?", modID) if mod.ID == "" { return nil @@ -44,7 +44,7 @@ func GetModByReference(ctx context.Context, modReference string) *Mod { } var mod Mod - DBCtx(ctx).Preload("Tags").Find(&mod, "mod_reference = ?", modReference) + DBCtx(ctx).Preload("Tags").Preload("Versions.Targets").Find(&mod, "mod_reference = ?", modReference) if mod.ID == "" { return nil @@ -89,7 +89,7 @@ func GetModCount(ctx context.Context, search string, unapproved bool) int64 { query := DBCtx(ctx).Model(Mod{}).Where("approved = ? AND denied = ?", !unapproved, false) if search != "" { - query = query.Where("to_tsvector(name) @@ to_tsquery(?)", strings.Replace(search, " ", " & ", -1)) + query = query.Where("to_tsvector(name) @@ to_tsquery(?)", strings.ReplaceAll(search, " ", " & ")) } query.Count(&modCount) @@ -141,7 +141,7 @@ func GetMods(ctx context.Context, limit int, offset int, orderBy string, order s query = query.Where("approved = ? AND denied = ?", !unapproved, false) if search != "" { - query = query.Where("to_tsvector(name) @@ to_tsquery(?)", strings.Replace(search, " ", " & ", -1)) + query = query.Where("to_tsvector(name) @@ to_tsquery(?)", strings.ReplaceAll(search, " ", " & ")) } query.Find(&mods) @@ -213,10 +213,10 @@ func NewModQuery(ctx context.Context, filter *models.ModFilter, unapproved bool, } query = query.Where("approved = ? AND denied = ?", !unapproved, false) - query = query.Preload("Tags") + query = query.Preload("Tags").Preload("Versions.Targets") if filter != nil { if filter.Search != nil && *filter.Search != "" { - cleanSearch := strings.Replace(strings.TrimSpace(*filter.Search), " ", " & ", -1) + cleanSearch := strings.ReplaceAll(strings.TrimSpace(*filter.Search), " ", " & ") sub := DBCtx(ctx).Table("mods") sub = sub.Select("id, (similarity(name, ?) * 2 + similarity(short_description, ?) + similarity(full_description, ?) * 0.5) as s", cleanSearch, cleanSearch, cleanSearch) @@ -270,7 +270,7 @@ func GetModByIDOrReference(ctx context.Context, modIDOrReference string) *Mod { } var mod Mod - DBCtx(ctx).Preload("Tags").Find(&mod, "mod_reference = ? OR id = ?", modIDOrReference, modIDOrReference) + DBCtx(ctx).Preload("Tags").Preload("Versions.Targets").Find(&mod, "mod_reference = ? OR id = ?", modIDOrReference, modIDOrReference) if mod.ID == "" { return nil diff --git a/db/postgres/otel/callbacks.go b/db/postgres/otel/callbacks.go index be653c8..cc691f7 100644 --- a/db/postgres/otel/callbacks.go +++ b/db/postgres/otel/callbacks.go @@ -4,12 +4,10 @@ import ( "strings" "go.opentelemetry.io/otel/attribute" - semconv "go.opentelemetry.io/otel/semconv/v1.7.0" - - "gorm.io/gorm" - "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.7.0" oteltrace "go.opentelemetry.io/otel/trace" + "gorm.io/gorm" ) const ( diff --git a/db/postgres/otel/config.go b/db/postgres/otel/config.go index ee01363..996e680 100644 --- a/db/postgres/otel/config.go +++ b/db/postgres/otel/config.go @@ -5,8 +5,8 @@ import ( ) type config struct { - serviceName string tracerProvider oteltrace.TracerProvider + serviceName string } // Option is used to configure the client. diff --git a/db/postgres/otel/plugin.go b/db/postgres/otel/plugin.go index 9355d7d..4769d2e 100644 --- a/db/postgres/otel/plugin.go +++ b/db/postgres/otel/plugin.go @@ -3,11 +3,10 @@ package otel import ( "fmt" - "gorm.io/gorm" - "go.opentelemetry.io/contrib" "go.opentelemetry.io/otel" oteltrace "go.opentelemetry.io/otel/trace" + "gorm.io/gorm" ) const ( diff --git a/db/postgres/postgres.go b/db/postgres/postgres.go index c104b89..cccec4e 100644 --- a/db/postgres/postgres.go +++ b/db/postgres/postgres.go @@ -5,8 +5,6 @@ import ( "fmt" "time" - "github.com/satisfactorymodding/smr-api/db/postgres/otel" - "github.com/patrickmn/go-cache" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -15,10 +13,14 @@ import ( "gorm.io/gorm" "gorm.io/gorm/logger" "gorm.io/gorm/utils" + + "github.com/satisfactorymodding/smr-api/db/postgres/otel" ) -var db *gorm.DB -var dbCache *cache.Cache +var ( + db *gorm.DB + dbCache *cache.Cache +) type UserKey struct{} @@ -70,6 +72,8 @@ func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (stri } } +var debugEnabled = false + func InitializePostgres(ctx context.Context) { connection := postgres.Open(fmt.Sprintf( "sslmode=disable host=%s port=%d user=%s dbname=%s password=%s", @@ -96,6 +100,10 @@ func InitializePostgres(ctx context.Context) { db = dbInit + if debugEnabled { + db = db.Debug() + } + dbCache = cache.New(time.Second*5, time.Second*10) // TODO Create search indexes @@ -109,10 +117,12 @@ func Save(ctx context.Context, object interface{}) { func Delete(ctx context.Context, object interface{}) { DBCtx(ctx).Delete(object) + ClearCache() } func DeleteForced(ctx context.Context, object interface{}) { DBCtx(ctx).Unscoped().Delete(object) + ClearCache() } func DBCtx(ctx context.Context) *gorm.DB { @@ -127,3 +137,15 @@ func DBCtx(ctx context.Context) *gorm.DB { return db } + +func ClearCache() { + dbCache.Flush() +} + +func EnableDebug() { + if db != nil { + db = db.Debug() + } + + debugEnabled = true +} diff --git a/db/postgres/postgres_types.go b/db/postgres/postgres_types.go index 003495d..4d12da1 100644 --- a/db/postgres/postgres_types.go +++ b/db/postgres/postgres_types.go @@ -6,6 +6,10 @@ import ( "gorm.io/gorm" ) +type Tabler interface { + TableName() string +} + type SMRDates struct { CreatedAt time.Time UpdatedAt time.Time @@ -18,56 +22,47 @@ type SMRModel struct { } type User struct { + GithubID *string + GoogleID *string + FacebookID *string SMRModel - Email string `gorm:"type:varchar(256);unique_index"` Username string `gorm:"type:varchar(32)"` Avatar string JoinedFrom string - Banned bool `gorm:"default:false;not null"` - - GithubID *string - GoogleID *string - FacebookID *string - - Mods []Mod `gorm:"many2many:user_mods;"` + Mods []Mod `gorm:"many2many:user_mods;"` + Banned bool `gorm:"default:false;not null"` } type UserSession struct { SMRModel - - UserID string - User User - + UserID string Token string `gorm:"type:varchar(256);unique_index"` UserAgent string + User User } type Mod struct { + LastVersionDate *time.Time + Compatibility *CompatibilityInfo `gorm:"serializer:json"` SMRModel - - Name string `gorm:"type:varchar(32)"` - ShortDescription string `gorm:"type:varchar(128)"` - FullDescription string + CreatorID string Logo string SourceURL string - CreatorID string - Approved bool `gorm:"default:false;not null"` - Denied bool `gorm:"default:false;not null"` - Views uint + FullDescription string + ShortDescription string `gorm:"type:varchar(128)"` + Name string `gorm:"type:varchar(32)"` + ModReference string + Versions []Version + Tags []Tag `gorm:"many2many:mod_tags"` + Users []User `gorm:"many2many:user_mods;"` Downloads uint - Hotness uint Popularity uint - LastVersionDate *time.Time - ModReference string + Hotness uint + Views uint Hidden bool - Compatibility *CompatibilityInfo `gorm:"serializer:json"` - - Users []User `gorm:"many2many:user_mods;"` - - Tags []Tag `gorm:"many2many:mod_tags"` - - Versions []Version + Denied bool `gorm:"default:false;not null"` + Approved bool `gorm:"default:false;not null"` } type UserMod struct { @@ -78,39 +73,50 @@ type UserMod struct { // If updated, update dataloader type Version struct { - SMRModel - - ModID string - - Version string `gorm:"type:varchar(16)"` - SMLVersion string `gorm:"type:varchar(16)"` - Changelog string - Downloads uint - Key string - Stability string `gorm:"default:'alpha'" sql:"type:version_stability"` - Approved bool `gorm:"default:false;not null"` - Denied bool `gorm:"default:false;not null"` - Hotness uint Metadata *string - ModReference *string - VersionMajor *int - VersionMinor *int - VersionPatch *int - Size *int64 Hash *string + Size *int64 + VersionPatch *int + VersionMinor *int + VersionMajor *int + ModReference *string + SMRModel + Changelog string + Stability string `gorm:"default:'alpha'" sql:"type:version_stability"` + Key string + SMLVersion string `gorm:"type:varchar(16)"` + Version string `gorm:"type:varchar(16)"` + ModID string + Targets []VersionTarget `gorm:"foreignKey:VersionID"` + Hotness uint + Downloads uint + Denied bool `gorm:"default:false;not null"` + Approved bool `gorm:"default:false;not null"` +} + +type TinyVersion struct { + Hash *string + Size *int64 + SMRModel + SMLVersion string `gorm:"type:varchar(16)"` + Version string `gorm:"type:varchar(16)"` + Targets []VersionTarget `gorm:"foreignKey:VersionID;preload:true"` + Dependencies []VersionDependency `gorm:"foreignKey:VersionID"` +} + +func (TinyVersion) TableName() string { + return "versions" } type Guide struct { SMRModel - Name string `gorm:"type:varchar(50)"` ShortDescription string `gorm:"type:varchar(128)"` Guide string - Views uint + UserID string Tags []Tag `gorm:"many2many:guide_tags"` - - UserID string - User User + User User + Views uint } type UserGroup struct { @@ -121,15 +127,16 @@ type UserGroup struct { } type SMLVersion struct { + Date time.Time + BootstrapVersion *string SMRModel - Version string `gorm:"type:varchar(32);unique_index"` - SatisfactoryVersion int Stability string `sql:"type:version_stability"` - Date time.Time Link string Changelog string - BootstrapVersion *string + EngineVersion string + Targets []SMLVersionTarget `gorm:"foreignKey:VersionID"` + SatisfactoryVersion int } type VersionDependency struct { @@ -143,14 +150,13 @@ type VersionDependency struct { } type BootstrapVersion struct { + Date time.Time SMRModel - Version string `gorm:"type:varchar(32);unique_index"` - SatisfactoryVersion int Stability string `sql:"type:version_stability"` - Date time.Time Link string Changelog string + SatisfactoryVersion int } type Announcement struct { @@ -163,7 +169,8 @@ type Announcement struct { type Tag struct { SMRModel - Name string `gorm:"type:varchar(24)"` + Name string `gorm:"type:varchar(24)"` + Description string `gorm:"type:varchar(512)"` Mods []Mod `gorm:"many2many:mod_tags"` } @@ -187,3 +194,17 @@ type Compatibility struct { State string Note string } + +type VersionTarget struct { + VersionID string `gorm:"primary_key;type:varchar(14)"` + TargetName string `gorm:"primary_key;type:varchar(16)"` + Key string + Hash string + Size int64 +} + +type SMLVersionTarget struct { + VersionID string `gorm:"primary_key;type:varchar(14)"` + TargetName string `gorm:"primary_key;type:varchar(16)"` + Link string +} diff --git a/db/postgres/sml_version.go b/db/postgres/sml_version.go index 64e84c9..46f6e21 100644 --- a/db/postgres/sml_version.go +++ b/db/postgres/sml_version.go @@ -18,7 +18,7 @@ func CreateSMLVersion(ctx context.Context, smlVersion *SMLVersion) (*SMLVersion, func GetSMLVersionByID(ctx context.Context, smlVersionID string) *SMLVersion { var smlVersion SMLVersion - DBCtx(ctx).Find(&smlVersion, "id = ?", smlVersionID) + DBCtx(ctx).Preload("Targets").Find(&smlVersion, "id in (?)", smlVersionID) if smlVersion.ID == "" { return nil @@ -37,17 +37,18 @@ func GetSMLVersions(ctx context.Context, filter *models.SMLVersionFilter) []SMLV Order(string(*filter.OrderBy) + " " + string(*filter.Order)) if filter.Search != nil && *filter.Search != "" { - query = query.Where("to_tsvector(name) @@ to_tsquery(?)", strings.Replace(*filter.Search, " ", " & ", -1)) + query = query.Where("to_tsvector(name) @@ to_tsquery(?)", strings.ReplaceAll(*filter.Search, " ", " & ")) } } - query.Find(&smlVersions) + query.Preload("Targets").Find(&smlVersions) + return smlVersions } func GetSMLVersionsByID(ctx context.Context, smlVersionIds []string) []SMLVersion { var smlVersions []SMLVersion - DBCtx(ctx).Find(&smlVersions, "id in (?)", smlVersionIds) + DBCtx(ctx).Preload("Targets").Find(&smlVersions, "id in (?)", smlVersionIds) if len(smlVersionIds) != len(smlVersions) { return nil @@ -62,7 +63,7 @@ func GetSMLVersionCount(ctx context.Context, filter *models.SMLVersionFilter) in if filter != nil { if filter.Search != nil && *filter.Search != "" { - query = query.Where("to_tsvector(name) @@ to_tsquery(?)", strings.Replace(*filter.Search, " ", " & ", -1)) + query = query.Where("to_tsvector(name) @@ to_tsquery(?)", strings.ReplaceAll(*filter.Search, " ", " & ")) } } @@ -73,9 +74,17 @@ func GetSMLVersionCount(ctx context.Context, filter *models.SMLVersionFilter) in func GetSMLLatestVersions(ctx context.Context) *[]SMLVersion { var smlVersions []SMLVersion - DBCtx(ctx).Select("distinct on (stability) *"). + DBCtx(ctx).Preload("Targets").Select("distinct on (stability) *"). Order("stability, created_at desc"). Find(&smlVersions) return &smlVersions } + +func GetSMLVersionTargets(ctx context.Context, smlVersionID string) []SMLVersionTarget { + var smlVersionTargets []SMLVersionTarget + + DBCtx(ctx).Find(&smlVersionTargets, "version_id = ?", smlVersionID) + + return smlVersionTargets +} diff --git a/db/postgres/tags.go b/db/postgres/tags.go index a157105..3ab95d1 100644 --- a/db/postgres/tags.go +++ b/db/postgres/tags.go @@ -9,14 +9,12 @@ import ( "strings" "time" + "github.com/finnbear/moderation" "github.com/mitchellh/hashstructure/v2" - "github.com/satisfactorymodding/smr-api/generated" + "github.com/patrickmn/go-cache" + "github.com/satisfactorymodding/smr-api/generated" "github.com/satisfactorymodding/smr-api/util" - - "github.com/finnbear/moderation" - - "github.com/patrickmn/go-cache" ) func ValidateTagName(tag string) error { @@ -88,7 +86,7 @@ func GetTagByName(ctx context.Context, tagName string) *Tag { return nil } - dbCache.Set(cacheKey, tag, cache.DefaultExpiration) + dbCache.Set(cacheKey, &tag, cache.DefaultExpiration) return &tag } @@ -107,7 +105,7 @@ func GetTagByID(ctx context.Context, tagID string) *Tag { return nil } - dbCache.Set(cacheKey, tag, cache.DefaultExpiration) + dbCache.Set(cacheKey, &tag, cache.DefaultExpiration) return &tag } @@ -126,7 +124,7 @@ func GetTags(ctx context.Context, filter *generated.TagFilter) []Tag { if filter != nil { if filter.Search != nil && *filter.Search != "" { - cleanSearch := strings.Replace(strings.TrimSpace(*filter.Search), " ", " & ", -1) + cleanSearch := strings.ReplaceAll(strings.TrimSpace(*filter.Search), " ", " & ") sub := DBCtx(ctx).Table("tags") sub = sub.Select("id, similarity(name, ?) as s", cleanSearch, cleanSearch, cleanSearch) diff --git a/db/postgres/version.go b/db/postgres/version.go index 4cff8ef..ea88cdf 100644 --- a/db/postgres/version.go +++ b/db/postgres/version.go @@ -9,10 +9,10 @@ import ( "strings" "time" + "github.com/patrickmn/go-cache" + "github.com/satisfactorymodding/smr-api/models" "github.com/satisfactorymodding/smr-api/util" - - "github.com/patrickmn/go-cache" ) var semverCheck = regexp.MustCompile(`^(<=|<|>|>=|\^)?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`) @@ -24,7 +24,7 @@ func GetVersionsByID(ctx context.Context, versionIds []string) []Version { } var versions []Version - DBCtx(ctx).Find(&versions, "id in (?)", versionIds) + DBCtx(ctx).Preload("Targets").Find(&versions, "id in (?)", versionIds) if len(versionIds) != len(versions) { return nil @@ -43,7 +43,7 @@ func GetModLatestVersions(ctx context.Context, modID string, unapproved bool) *[ var versions []Version - DBCtx(ctx).Select("distinct on (mod_id, stability) *"). + DBCtx(ctx).Preload("Targets").Select("distinct on (mod_id, stability) *"). Where("mod_id = ?", modID). Where("approved = ? AND denied = ?", !unapproved, false). Order("mod_id, stability, created_at desc"). @@ -62,7 +62,7 @@ func GetModsLatestVersions(ctx context.Context, modIds []string, unapproved bool var versions []Version - DBCtx(ctx).Select("distinct on (mod_id, stability) *"). + DBCtx(ctx).Preload("Targets").Select("distinct on (mod_id, stability) *"). Where("mod_id in (?)", modIds). Where("approved = ? AND denied = ?", !unapproved, false). Order("mod_id, stability, created_at desc"). @@ -80,7 +80,25 @@ func GetModVersions(ctx context.Context, modID string, limit int, offset int, or } var versions []Version - DBCtx(ctx).Limit(limit).Offset(offset).Order(orderBy+" "+order).Where("approved = ? AND denied = ?", !unapproved, false).Find(&versions, "mod_id = ?", modID) + DBCtx(ctx).Preload("Targets").Limit(limit).Offset(offset).Order(orderBy+" "+order).Where("approved = ? AND denied = ?", !unapproved, false).Find(&versions, "mod_id = ?", modID) + + dbCache.Set(cacheKey, versions, cache.DefaultExpiration) + + return versions +} + +func GetAllModVersionsWithDependencies(ctx context.Context, modID string) []TinyVersion { + cacheKey := "GetAllModVersionsWithDependencies_" + modID + if versions, ok := dbCache.Get(cacheKey); ok { + return versions.([]TinyVersion) + } + + var versions []TinyVersion + DBCtx(ctx). + Preload("Dependencies"). + Preload("Targets"). + Where("approved = ? AND denied = ?", true, false). + Find(&versions, "mod_id = ?", modID) dbCache.Set(cacheKey, versions, cache.DefaultExpiration) @@ -98,7 +116,7 @@ func GetModVersionsNew(ctx context.Context, modID string, filter *models.Version } var versions []Version - query := DBCtx(ctx) + query := DBCtx(ctx).Preload("Targets") if filter != nil { query = query.Limit(*filter.Limit). @@ -106,7 +124,7 @@ func GetModVersionsNew(ctx context.Context, modID string, filter *models.Version Order(string(*filter.OrderBy) + " " + string(*filter.Order)) } - query.Where("approved = ? AND denied = ?", !unapproved, false).Find(&versions, "mod_id = ?", modID) + query.Preload("Targets").Where("approved = ? AND denied = ?", !unapproved, false).Find(&versions, "mod_id = ?", modID) if cacheKey != "" { dbCache.Set(cacheKey, versions, cache.DefaultExpiration) @@ -122,7 +140,7 @@ func GetModVersion(ctx context.Context, modID string, versionID string) *Version } var version Version - DBCtx(ctx).First(&version, "mod_id = ? AND id = ?", modID, versionID) + DBCtx(ctx).Preload("Targets").First(&version, "mod_id = ? AND id = ?", modID, versionID) if version.ID == "" { return nil @@ -140,7 +158,7 @@ func GetModVersionByName(ctx context.Context, modID string, versionName string) } var version Version - DBCtx(ctx).First(&version, "mod_id = ? AND version = ?", modID, versionName) + DBCtx(ctx).Preload("Targets").First(&version, "mod_id = ? AND version = ?", modID, versionName) if version.ID == "" { return nil @@ -186,7 +204,7 @@ func GetVersion(ctx context.Context, versionID string) *Version { } var version Version - DBCtx(ctx).First(&version, "id = ?", versionID) + DBCtx(ctx).Preload("Targets").First(&version, "id = ?", versionID) if version.ID == "" { return nil @@ -208,7 +226,7 @@ func GetVersionsNew(ctx context.Context, filter *models.VersionFilter, unapprove } var versions []Version - query := DBCtx(ctx).Where("approved = ? AND denied = ?", !unapproved, false) + query := DBCtx(ctx).Preload("Targets").Where("approved = ? AND denied = ?", !unapproved, false) if filter != nil { query = query.Limit(*filter.Limit). @@ -216,7 +234,7 @@ func GetVersionsNew(ctx context.Context, filter *models.VersionFilter, unapprove Order(string(*filter.OrderBy) + " " + string(*filter.Order)) if filter.Search != nil && *filter.Search != "" { - query = query.Where("to_tsvector(version) @@ to_tsquery(?)", strings.Replace(*filter.Search, " ", " & ", -1)) + query = query.Where("to_tsvector(version) @@ to_tsquery(?)", strings.ReplaceAll(*filter.Search, " ", " & ")) } if filter.Fields != nil && len(filter.Fields) > 0 { @@ -224,7 +242,7 @@ func GetVersionsNew(ctx context.Context, filter *models.VersionFilter, unapprove } } - query.Find(&versions) + query.Preload("Targets").Find(&versions) if cacheKey != "" { dbCache.Set(cacheKey, versions, cache.DefaultExpiration) @@ -248,7 +266,7 @@ func GetVersionCountNew(ctx context.Context, filter *models.VersionFilter, unapp if filter != nil { if filter.Search != nil && *filter.Search != "" { - query = query.Where("to_tsvector(version) @@ to_tsquery(?)", strings.Replace(*filter.Search, " ", " & ", -1)) + query = query.Where("to_tsvector(version) @@ to_tsquery(?)", strings.ReplaceAll(*filter.Search, " ", " & ")) } } @@ -261,6 +279,24 @@ func GetVersionCountNew(ctx context.Context, filter *models.VersionFilter, unapp return versionCount } +func GetVersionTarget(ctx context.Context, versionID string, target string) *VersionTarget { + cacheKey := "GetVersionTarget_" + versionID + "_" + target + if versionTarget, ok := dbCache.Get(cacheKey); ok { + return versionTarget.(*VersionTarget) + } + + var versionTarget VersionTarget + DBCtx(ctx).First(&versionTarget, "version_id = ? AND target_name = ?", versionID, target) + + if versionTarget.VersionID == "" { + return nil + } + + dbCache.Set(cacheKey, &versionTarget, cache.DefaultExpiration) + + return &versionTarget +} + func GetVersionDependencies(ctx context.Context, versionID string) []VersionDependency { var versionDependencies []VersionDependency DBCtx(ctx).Where("version_id = ?", versionID).Find(&versionDependencies) @@ -288,7 +324,7 @@ func GetModVersionsConstraint(ctx context.Context, modID string, constraint stri return nil } - query := DBCtx(ctx).Where("mod_id", modID) + query := DBCtx(ctx).Preload("Targets").Where("mod_id", modID) /* <=1.2.3 @@ -357,6 +393,6 @@ func GetModVersionsConstraint(ctx context.Context, modID string, constraint stri } var versions []Version - query.Find(&versions) + query.Preload("Targets").Find(&versions) return versions } diff --git a/db/statistics.go b/db/statistics.go index 6db6114..f772341 100644 --- a/db/statistics.go +++ b/db/statistics.go @@ -5,10 +5,10 @@ import ( "regexp" "time" + "github.com/rs/zerolog/log" + "github.com/satisfactorymodding/smr-api/db/postgres" "github.com/satisfactorymodding/smr-api/redis" - - "github.com/rs/zerolog/log" ) var keyRegex = regexp.MustCompile(`^([^:]+):([^:]+):([^:]+):([^:]+)$`) @@ -34,7 +34,7 @@ func RunAsyncStatisticLoop(ctx context.Context) { resultMap[entityType][action] = make(map[string]uint) } - resultMap[entityType][action][entityID] = resultMap[entityType][action][entityID] + 1 + resultMap[entityType][action][entityID]++ } } @@ -45,27 +45,25 @@ func RunAsyncStatisticLoop(ctx context.Context) { ctxWithTx := postgres.ContextWithDB(ctx, updateTx) switch entityType { case "mod": - switch action { - case "view": + if action == "view" { mod := postgres.GetModByID(ctxWithTx, entityID) if mod != nil { currentHotness := mod.Hotness if currentHotness > 4 { // Preserve some of the hotness - currentHotness = currentHotness / 4 + currentHotness /= 4 } updateTx.Model(&mod).UpdateColumns(postgres.Mod{Hotness: currentHotness + count}) } } case "version": - switch action { - case "download": + if action == "download" { version := postgres.GetVersion(ctxWithTx, entityID) if version != nil { currentHotness := version.Hotness if currentHotness > 4 { // Preserve some of the popularity - currentHotness = currentHotness / 4 + currentHotness /= 4 } updateTx.Model(&version).UpdateColumns(postgres.Version{Hotness: currentHotness + count}) } @@ -94,7 +92,7 @@ func RunAsyncStatisticLoop(ctx context.Context) { currentPopularity := mod.Popularity if currentPopularity > 4 { // Preserve some of the popularity - currentPopularity = currentPopularity / 4 + currentPopularity /= 4 } updateTx.Model(&mod).UpdateColumns(postgres.Mod{ Popularity: currentPopularity + row.Hotness, diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 73212cb..6b30231 100755 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -15,7 +15,7 @@ services: - 5432:5432 minio: - image: minio/minio + image: quay.io/minio/minio ports: - 9000:9000 - 9001:9001 @@ -25,4 +25,9 @@ services: MINIO_ROOT_USER: minio MINIO_ROOT_PASSWORD: minio123 MINIO_ACCESS_KEY: REPLACE_ME_KEY - MINIO_SECRET_KEY: REPLACE_ME_SECRET \ No newline at end of file + MINIO_SECRET_KEY: REPLACE_ME_SECRET + + pak_parser: + image: ghcr.io/vilsol/ficsit-pak-parser:v0.0.3 + ports: + - 50051:50051 \ No newline at end of file diff --git a/generated/custom_models.go b/generated/custom_models.go index a7288de..8498088 100755 --- a/generated/custom_models.go +++ b/generated/custom_models.go @@ -21,7 +21,7 @@ type UpdateMod struct { SourceURL *string `json:"source_url"` ModReference *string `json:"mod_reference"` Hidden *bool `json:"hidden"` + Compatibility *CompatibilityInfoInput `json:"compatibility"` Authors []UpdateUserMod `json:"authors"` TagIDs []string `json:"tagIDs" validate:"dive,min=3,max=24"` - Compatibility *CompatibilityInfoInput `json:"compatibility"` } diff --git a/go.mod b/go.mod index cffed55..43a278e 100755 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( github.com/99designs/gqlgen v0.17.16 + github.com/MarvinJWendt/testza v0.4.3 github.com/Masterminds/semver/v3 v3.1.1 github.com/Vilsol/ue4pak v0.1.5 github.com/VirusTotal/vt-go v0.0.0-20220413144842-e010bf48aaee @@ -21,6 +22,7 @@ require ( github.com/lab259/go-migration v1.3.1 github.com/labstack/echo-contrib v0.13.0 github.com/labstack/echo/v4 v4.7.2 + github.com/machinebox/graphql v0.2.2 github.com/microcosm-cc/bluemonday v1.0.18 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/mitchellh/mapstructure v1.5.0 @@ -47,6 +49,7 @@ require ( golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa golang.org/x/net v0.0.0-20220728030405-41545e8bf201 golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c gopkg.in/go-playground/validator.v9 v9.31.0 gorm.io/driver/postgres v1.3.5 gorm.io/gorm v1.23.5 @@ -61,12 +64,14 @@ require ( github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb // indirect github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect + github.com/atomicgo/cursor v0.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bsm/redislock v0.7.1 // indirect github.com/capnm/sysinfo v0.0.0-20130621111458-5909a53897f3 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.0 // indirect @@ -88,6 +93,7 @@ require ( github.com/golang/protobuf v1.5.2 // indirect github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect github.com/google/uuid v1.3.0 // indirect + github.com/gookit/color v1.5.0 // indirect github.com/gorilla/css v1.0.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect @@ -108,16 +114,22 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.13.6 // indirect + github.com/klauspost/cpuid/v2 v2.1.0 // indirect github.com/labstack/gommon v0.3.1 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/lib/pq v1.10.2 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/matryer/is v1.4.0 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect github.com/pelletier/go-toml v1.9.4 // indirect github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect + github.com/pterm/pterm v0.12.40 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sergi/go-diff v1.2.0 // indirect github.com/sizeofint/webp-animation v0.0.0-20190207194838-b631dc900de9 // indirect github.com/spate/glimage v0.0.0-20200505055513-fbdcc60a65e5 // indirect github.com/spf13/afero v1.8.2 // indirect @@ -135,6 +147,7 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.7.0 // indirect go.opentelemetry.io/proto/otlp v0.16.0 // indirect @@ -142,6 +155,7 @@ require ( golang.org/x/exp v0.0.0-20210220032938-85be41e4509f // indirect golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect golang.org/x/tools v0.1.10 // indirect diff --git a/go.sum b/go.sum index f5182f5..7828d76 100755 --- a/go.sum +++ b/go.sum @@ -89,6 +89,14 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= +github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= +github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= +github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= +github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= +github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= +github.com/MarvinJWendt/testza v0.4.3 h1:u2XaM4IqGp9dsdUmML8/Z791fu4yjQYzOiufOtJwTII= +github.com/MarvinJWendt/testza v0.4.3/go.mod h1:CpXaOfceNEYnLDtNIyTrPPcCpDJYqzZnu2aiA2Wp33U= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= @@ -162,6 +170,8 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/atomicgo/cursor v0.0.1 h1:xdogsqa6YYlLfM+GyClC/Lchf7aiMerFiZQn7soTOoU= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/avast/retry-go/v3 v3.1.1 h1:49Scxf4v8PmiQ/nY0aY3p0hDueqSmc7++cBbtiDGu2g= github.com/avast/retry-go/v3 v3.1.1/go.mod h1:6cXRK369RpzFL3UQGqIUp9Q7GDrams+KsYWrfNA1/nQ= github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= @@ -720,6 +730,9 @@ github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3i github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0 h1:1Opow3+BWDwqor78DcJkJCIwnkviFi+rrOANki9BUFw= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= @@ -910,6 +923,11 @@ github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8 github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0= +github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -949,6 +967,8 @@ github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= +github.com/machinebox/graphql v0.2.2 h1:dWKpJligYKhYKO5A2gvNhkJdQMNZeChZYyBbrZkBZfo= +github.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= @@ -964,6 +984,8 @@ github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsI github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= @@ -984,6 +1006,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= @@ -1177,8 +1201,18 @@ github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= +github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= +github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= +github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= +github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= +github.com/pterm/pterm v0.12.40 h1:LvQE43RYegVH+y5sCDcqjlbsRu0DlAecEn9FDfs9ePs= +github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -1205,8 +1239,9 @@ github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= @@ -1344,6 +1379,8 @@ github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= @@ -1640,6 +1677,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1758,6 +1796,7 @@ golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1772,12 +1811,14 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/gql/directive.go b/gql/directive.go index 86f735c..72657ee 100644 --- a/gql/directive.go +++ b/gql/directive.go @@ -5,13 +5,13 @@ import ( "net/http" "reflect" + "github.com/99designs/gqlgen/graphql" + "github.com/pkg/errors" + "github.com/satisfactorymodding/smr-api/auth" "github.com/satisfactorymodding/smr-api/db/postgres" "github.com/satisfactorymodding/smr-api/generated" "github.com/satisfactorymodding/smr-api/util" - - "github.com/99designs/gqlgen/graphql" - "github.com/pkg/errors" ) func MakeDirective() generated.DirectiveRoot { @@ -37,7 +37,7 @@ type Directive struct { generated.DirectiveRoot } -func canEditMod(ctx context.Context, obj interface{}, next graphql.Resolver, field string) (res interface{}, err error) { +func canEditMod(ctx context.Context, obj interface{}, next graphql.Resolver, field string) (interface{}, error) { user := ctx.Value(postgres.UserKey{}).(*postgres.User) dbMod := postgres.GetModByID(ctx, getArgument(ctx, field).(string)) @@ -57,7 +57,7 @@ func canEditMod(ctx context.Context, obj interface{}, next graphql.Resolver, fie return nil, errors.New("user not authorized to perform this action") } -func canEditModCompatibility(ctx context.Context, obj interface{}, next graphql.Resolver, field *string) (res interface{}, err error) { +func canEditModCompatibility(ctx context.Context, obj interface{}, next graphql.Resolver, field *string) (interface{}, error) { user := ctx.Value(postgres.UserKey{}).(*postgres.User) if user.Has(ctx, auth.RoleEditAnyModCompatibility) || user.Has(ctx, auth.RoleEditAnyContent) { @@ -81,7 +81,7 @@ func canEditModCompatibility(ctx context.Context, obj interface{}, next graphql. return nil, errors.New("user not authorized to perform this action") } -func canEditVersion(ctx context.Context, obj interface{}, next graphql.Resolver, field string) (res interface{}, err error) { +func canEditVersion(ctx context.Context, obj interface{}, next graphql.Resolver, field string) (interface{}, error) { user := ctx.Value(postgres.UserKey{}).(*postgres.User) dbVersion := postgres.GetVersion(ctx, getArgument(ctx, field).(string)) @@ -101,7 +101,7 @@ func canEditVersion(ctx context.Context, obj interface{}, next graphql.Resolver, return nil, errors.New("user not authorized to perform this action") } -func canEditUser(ctx context.Context, obj interface{}, next graphql.Resolver, field string, object bool) (res interface{}, err error) { +func canEditUser(ctx context.Context, obj interface{}, next graphql.Resolver, field string, object bool) (interface{}, error) { user := ctx.Value(postgres.UserKey{}).(*postgres.User) var userID string @@ -128,7 +128,7 @@ func canEditUser(ctx context.Context, obj interface{}, next graphql.Resolver, fi return nil, errors.New("user not authorized to perform this action") } -func canEditGuide(ctx context.Context, obj interface{}, next graphql.Resolver, field string) (res interface{}, err error) { +func canEditGuide(ctx context.Context, obj interface{}, next graphql.Resolver, field string) (interface{}, error) { user := ctx.Value(postgres.UserKey{}).(*postgres.User) dbGuide := postgres.GetGuideByID(ctx, getArgument(ctx, field).(string)) @@ -148,7 +148,7 @@ func canEditGuide(ctx context.Context, obj interface{}, next graphql.Resolver, f return nil, errors.New("user not authorized to perform this action") } -func isLoggedIn(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) { +func isLoggedIn(ctx context.Context, obj interface{}, next graphql.Resolver) (interface{}, error) { header := ctx.Value(util.ContextHeader{}).(http.Header) authorization := header.Get("Authorization") @@ -171,7 +171,7 @@ func isLoggedIn(ctx context.Context, obj interface{}, next graphql.Resolver) (re return next(userCtx) } -func isNotLoggedIn(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) { +func isNotLoggedIn(ctx context.Context, obj interface{}, next graphql.Resolver) (interface{}, error) { header := ctx.Value(util.ContextHeader{}).(http.Header) authorization := header.Get("Authorization") @@ -190,7 +190,7 @@ func getArgument(ctx context.Context, key string) interface{} { return graphql.GetFieldContext(ctx).Args[key] } -func canApproveMods(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) { +func canApproveMods(ctx context.Context, obj interface{}, next graphql.Resolver) (interface{}, error) { user := ctx.Value(postgres.UserKey{}).(*postgres.User) if user.Has(ctx, auth.RoleApproveMods) { @@ -200,7 +200,7 @@ func canApproveMods(ctx context.Context, obj interface{}, next graphql.Resolver) return nil, errors.New("user not authorized to perform this action") } -func canApproveVersions(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) { +func canApproveVersions(ctx context.Context, obj interface{}, next graphql.Resolver) (interface{}, error) { user := ctx.Value(postgres.UserKey{}).(*postgres.User) if user.Has(ctx, auth.RoleApproveVersions) { @@ -210,7 +210,7 @@ func canApproveVersions(ctx context.Context, obj interface{}, next graphql.Resol return nil, errors.New("user not authorized to perform this action") } -func canEditUsers(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) { +func canEditUsers(ctx context.Context, obj interface{}, next graphql.Resolver) (interface{}, error) { user := ctx.Value(postgres.UserKey{}).(*postgres.User) if user.Has(ctx, auth.RoleEditUsers) { @@ -220,7 +220,7 @@ func canEditUsers(ctx context.Context, obj interface{}, next graphql.Resolver) ( return nil, errors.New("user not authorized to perform this action") } -func canEditSMLVersions(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) { +func canEditSMLVersions(ctx context.Context, obj interface{}, next graphql.Resolver) (interface{}, error) { user := ctx.Value(postgres.UserKey{}).(*postgres.User) if user.Has(ctx, auth.RoleEditSMLVersions) { @@ -230,7 +230,7 @@ func canEditSMLVersions(ctx context.Context, obj interface{}, next graphql.Resol return nil, errors.New("user not authorized to perform this action") } -func canEditBootstrapVersions(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) { +func canEditBootstrapVersions(ctx context.Context, obj interface{}, next graphql.Resolver) (interface{}, error) { user := ctx.Value(postgres.UserKey{}).(*postgres.User) if user.Has(ctx, auth.RoleEditBootstrapVersions) { @@ -240,7 +240,7 @@ func canEditBootstrapVersions(ctx context.Context, obj interface{}, next graphql return nil, errors.New("user not authorized to perform this action") } -func canEditAnnouncements(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) { +func canEditAnnouncements(ctx context.Context, obj interface{}, next graphql.Resolver) (interface{}, error) { user := ctx.Value(postgres.UserKey{}).(*postgres.User) if user.Has(ctx, auth.RoleEditAnnouncements) { @@ -250,7 +250,7 @@ func canEditAnnouncements(ctx context.Context, obj interface{}, next graphql.Res return nil, errors.New("user not authorized to perform this action") } -func canManageTags(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) { +func canManageTags(ctx context.Context, obj interface{}, next graphql.Resolver) (interface{}, error) { user := ctx.Value(postgres.UserKey{}).(*postgres.User) if user.Has(ctx, auth.RoleManageTags) { diff --git a/gql/gql_types.go b/gql/gql_types.go index 607d367..43dc719 100644 --- a/gql/gql_types.go +++ b/gql/gql_types.go @@ -12,18 +12,18 @@ func DBUserToGenerated(user *postgres.User) *generated.User { return nil } - Email := (*user).Email - Avatar := (*user).Avatar + Email := user.Email + Avatar := user.Avatar result := &generated.User{ - ID: (*user).ID, - Username: (*user).Username, + ID: user.ID, + Username: user.Username, Email: &Email, Avatar: &Avatar, CreatedAt: user.CreatedAt.Format(time.RFC3339Nano), - GithubID: (*user).GithubID, - GoogleID: (*user).GoogleID, - FacebookID: (*user).FacebookID, + GithubID: user.GithubID, + GoogleID: user.GoogleID, + FacebookID: user.FacebookID, } return result @@ -34,13 +34,13 @@ func DBModToGenerated(mod *postgres.Mod) *generated.Mod { return nil } - Logo := (*mod).Logo - SourceURL := (*mod).SourceURL - FullDescription := (*mod).FullDescription + Logo := mod.Logo + SourceURL := mod.SourceURL + FullDescription := mod.FullDescription var LastVersionDate string - if (*mod).LastVersionDate != nil { - LastVersionDate = (*mod).LastVersionDate.Format(time.RFC3339Nano) + if mod.LastVersionDate != nil { + LastVersionDate = mod.LastVersionDate.Format(time.RFC3339Nano) } return &generated.Mod{ @@ -61,6 +61,7 @@ func DBModToGenerated(mod *postgres.Mod) *generated.Mod { LastVersionDate: &LastVersionDate, ModReference: mod.ModReference, Hidden: mod.Hidden, + Versions: DBVersionsToGeneratedSlice(mod.Versions), Tags: DBTagsToGeneratedSlice(mod.Tags), Compatibility: DBCompInfoToGenCompInfo(mod.Compatibility), } @@ -84,6 +85,7 @@ func DBVersionToGenerated(version *postgres.Version) *generated.Version { Changelog: version.Changelog, Downloads: int(version.Downloads), Stability: generated.VersionStabilities(version.Stability), + Targets: DBVersionTargetsToGeneratedSlice(version.Targets), Approved: version.Approved, UpdatedAt: version.UpdatedAt.Format(time.RFC3339Nano), CreatedAt: version.CreatedAt.Format(time.RFC3339Nano), @@ -94,6 +96,14 @@ func DBVersionToGenerated(version *postgres.Version) *generated.Version { } } +func DBVersionsToGeneratedSlice(versions []postgres.Version) []*generated.Version { + converted := make([]*generated.Version, len(versions)) + for i, version := range versions { + converted[i] = DBVersionToGenerated(&version) + } + return converted +} + func DBGuideToGenerated(guide *postgres.Guide) *generated.Guide { if guide == nil { return nil @@ -124,10 +134,12 @@ func DBSMLVersionToGenerated(smlVersion *postgres.SMLVersion) *generated.SMLVers BootstrapVersion: smlVersion.BootstrapVersion, Stability: generated.VersionStabilities(smlVersion.Stability), Link: smlVersion.Link, + Targets: DBSMLVersionTargetToGeneratedSlice(smlVersion.Targets), Changelog: smlVersion.Changelog, Date: smlVersion.Date.Format(time.RFC3339Nano), UpdatedAt: smlVersion.UpdatedAt.Format(time.RFC3339Nano), CreatedAt: smlVersion.CreatedAt.Format(time.RFC3339Nano), + EngineVersion: smlVersion.EngineVersion, } } @@ -187,8 +199,9 @@ func DBTagToGenerated(tag *postgres.Tag) *generated.Tag { return nil } return &generated.Tag{ - Name: tag.Name, - ID: tag.ID, + Name: tag.Name, + ID: tag.ID, + Description: tag.Description, } } @@ -200,6 +213,50 @@ func DBTagsToGeneratedSlice(tags []postgres.Tag) []*generated.Tag { return converted } +func DBVersionTargetToGenerated(versionTarget *postgres.VersionTarget) *generated.VersionTarget { + if versionTarget == nil { + return nil + } + + hash := versionTarget.Hash + size := int(versionTarget.Size) + + return &generated.VersionTarget{ + VersionID: versionTarget.VersionID, + TargetName: generated.TargetName(versionTarget.TargetName), + Hash: &hash, + Size: &size, + } +} + +func DBVersionTargetsToGeneratedSlice(versionTargets []postgres.VersionTarget) []*generated.VersionTarget { + converted := make([]*generated.VersionTarget, len(versionTargets)) + for i, versionTarget := range versionTargets { + converted[i] = DBVersionTargetToGenerated(&versionTarget) + } + return converted +} + +func DBSMLVersionTargetToGenerated(smlVersionTarget *postgres.SMLVersionTarget) *generated.SMLVersionTarget { + if smlVersionTarget == nil { + return nil + } + + return &generated.SMLVersionTarget{ + VersionID: smlVersionTarget.VersionID, + TargetName: generated.TargetName(smlVersionTarget.TargetName), + Link: smlVersionTarget.Link, + } +} + +func DBSMLVersionTargetToGeneratedSlice(smlVersionTargets []postgres.SMLVersionTarget) []*generated.SMLVersionTarget { + converted := make([]*generated.SMLVersionTarget, len(smlVersionTargets)) + for i, smlVersionTarget := range smlVersionTargets { + converted[i] = DBSMLVersionTargetToGenerated(&smlVersionTarget) + } + return converted +} + func GenCompInfoToDBCompInfo(gen *generated.CompatibilityInfoInput) *postgres.CompatibilityInfo { if gen == nil { return nil diff --git a/gql/gql_utils.go b/gql/gql_utils.go index 37dd59d..5dbe22c 100644 --- a/gql/gql_utils.go +++ b/gql/gql_utils.go @@ -8,14 +8,13 @@ import ( "strings" "time" - "github.com/satisfactorymodding/smr-api/db/postgres" - - "github.com/satisfactorymodding/smr-api/generated" - "github.com/satisfactorymodding/smr-api/util" - "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" + + "github.com/satisfactorymodding/smr-api/db/postgres" + "github.com/satisfactorymodding/smr-api/generated" + "github.com/satisfactorymodding/smr-api/util" ) type TraceWrapper struct { diff --git a/gql/resolver.go b/gql/resolver.go index 07b9e99..4354780 100755 --- a/gql/resolver.go +++ b/gql/resolver.go @@ -9,42 +9,59 @@ type Resolver struct{} func (r *Resolver) Mod() generated.ModResolver { return &modResolver{r} } + func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} } + func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } + func (r *Resolver) User() generated.UserResolver { return &userResolver{r} } + func (r *Resolver) UserMod() generated.UserModResolver { return &userModResolver{r} } + +func (r *Resolver) VersionTarget() generated.VersionTargetResolver { + return &versionTargetResolver{r} +} + func (r *Resolver) Version() generated.VersionResolver { return &versionResolver{r} } + func (r *Resolver) GetMods() generated.GetModsResolver { return &getModsResolver{r} } + func (r *Resolver) GetMyMods() generated.GetMyModsResolver { return &getMyModsResolver{r} } + func (r *Resolver) GetVersions() generated.GetVersionsResolver { return &getVersionsResolver{r} } + func (r *Resolver) GetMyVersions() generated.GetMyVersionsResolver { return &getMyVersionsResolver{r} } + func (r *Resolver) Guide() generated.GuideResolver { return &guideResolver{r} } + func (r *Resolver) GetGuides() generated.GetGuidesResolver { return &getGuidesResolver{r} } + func (r *Resolver) GetSMLVersions() generated.GetSMLVersionsResolver { return &getSMLVersionsResolver{r} } + func (r *Resolver) GetBootstrapVersions() generated.GetBootstrapVersionsResolver { return &getBootstrapVersionsResolver{r} } diff --git a/gql/resolver_announcements.go b/gql/resolver_announcements.go index d1f1557..47b9d16 100644 --- a/gql/resolver_announcements.go +++ b/gql/resolver_announcements.go @@ -4,11 +4,11 @@ import ( "context" "github.com/pkg/errors" - "github.com/satisfactorymodding/smr-api/util" "gopkg.in/go-playground/validator.v9" "github.com/satisfactorymodding/smr-api/db/postgres" "github.com/satisfactorymodding/smr-api/generated" + "github.com/satisfactorymodding/smr-api/util" ) func (r *mutationResolver) CreateAnnouncement(ctx context.Context, announcement generated.NewAnnouncement) (*generated.Announcement, error) { diff --git a/gql/resolver_bootstrap_versions.go b/gql/resolver_bootstrap_versions.go index 33118c9..5a1ecfc 100644 --- a/gql/resolver_bootstrap_versions.go +++ b/gql/resolver_bootstrap_versions.go @@ -4,15 +4,14 @@ import ( "context" "time" + "github.com/99designs/gqlgen/graphql" + "github.com/pkg/errors" + "gopkg.in/go-playground/validator.v9" + "github.com/satisfactorymodding/smr-api/db/postgres" "github.com/satisfactorymodding/smr-api/generated" "github.com/satisfactorymodding/smr-api/models" "github.com/satisfactorymodding/smr-api/util" - - "github.com/pkg/errors" - - "github.com/99designs/gqlgen/graphql" - "gopkg.in/go-playground/validator.v9" ) func (r *mutationResolver) CreateBootstrapVersion(ctx context.Context, bootstrapVersion generated.NewBootstrapVersion) (*generated.BootstrapVersion, error) { @@ -25,7 +24,6 @@ func (r *mutationResolver) CreateBootstrapVersion(ctx context.Context, bootstrap } date, err := time.Parse(time.RFC3339Nano, bootstrapVersion.Date) - if err != nil { return nil, errors.Wrap(err, "failed to parse date") } @@ -40,7 +38,6 @@ func (r *mutationResolver) CreateBootstrapVersion(ctx context.Context, bootstrap } resultBootstrapVersion, err := postgres.CreateBootstrapVersion(newCtx, dbBootstrapVersion) - if err != nil { return nil, errors.Wrap(err, "failed to create bootstrap version") } @@ -111,7 +108,6 @@ func (r *getBootstrapVersionsResolver) BootstrapVersions(ctx context.Context, ob resolverContext := graphql.GetFieldContext(ctx) bootstrapVersionFilter, err := models.ProcessBootstrapVersionFilter(resolverContext.Parent.Args["filter"].(map[string]interface{})) - if err != nil { return nil, err } @@ -142,7 +138,6 @@ func (r *getBootstrapVersionsResolver) Count(ctx context.Context, obj *generated resolverContext := graphql.GetFieldContext(ctx) bootstrapVersionFilter, err := models.ProcessBootstrapVersionFilter(resolverContext.Parent.Args["filter"].(map[string]interface{})) - if err != nil { return 0, err } diff --git a/gql/resolver_guides.go b/gql/resolver_guides.go index 552ba7e..33d5252 100644 --- a/gql/resolver_guides.go +++ b/gql/resolver_guides.go @@ -4,16 +4,15 @@ import ( "context" "time" + "github.com/99designs/gqlgen/graphql" + "github.com/pkg/errors" + "gopkg.in/go-playground/validator.v9" + "github.com/satisfactorymodding/smr-api/db/postgres" "github.com/satisfactorymodding/smr-api/generated" "github.com/satisfactorymodding/smr-api/models" "github.com/satisfactorymodding/smr-api/redis" "github.com/satisfactorymodding/smr-api/util" - - "github.com/pkg/errors" - - "github.com/99designs/gqlgen/graphql" - "gopkg.in/go-playground/validator.v9" ) func (r *mutationResolver) CreateGuide(ctx context.Context, guide generated.NewGuide) (*generated.Guide, error) { @@ -36,7 +35,6 @@ func (r *mutationResolver) CreateGuide(ctx context.Context, guide generated.NewG dbGuide.UserID = user.ID resultGuide, err := postgres.CreateGuide(newCtx, dbGuide) - if err != nil { return nil, err } @@ -124,7 +122,6 @@ func (r *getGuidesResolver) Guides(ctx context.Context, obj *generated.GetGuides resolverContext := graphql.GetFieldContext(ctx) guideFilter, err := models.ProcessGuideFilter(resolverContext.Parent.Args["filter"].(map[string]interface{})) - if err != nil { return nil, err } @@ -155,7 +152,6 @@ func (r *getGuidesResolver) Count(ctx context.Context, obj *generated.GetGuides) resolverContext := graphql.GetFieldContext(ctx) guideFilter, err := models.ProcessGuideFilter(resolverContext.Parent.Args["filter"].(map[string]interface{})) - if err != nil { return 0, err } diff --git a/gql/resolver_mods.go b/gql/resolver_mods.go index f9ececb..58586c7 100644 --- a/gql/resolver_mods.go +++ b/gql/resolver_mods.go @@ -4,8 +4,14 @@ import ( "bytes" "context" "io" + "strings" "time" + "github.com/99designs/gqlgen/graphql" + "github.com/dgraph-io/ristretto" + "github.com/pkg/errors" + "gopkg.in/go-playground/validator.v9" + "github.com/satisfactorymodding/smr-api/dataloader" "github.com/satisfactorymodding/smr-api/db/postgres" "github.com/satisfactorymodding/smr-api/generated" @@ -15,14 +21,17 @@ import ( "github.com/satisfactorymodding/smr-api/storage" "github.com/satisfactorymodding/smr-api/util" "github.com/satisfactorymodding/smr-api/util/converter" - - "github.com/pkg/errors" - - "github.com/99designs/gqlgen/graphql" - "github.com/dgraph-io/ristretto" - "gopkg.in/go-playground/validator.v9" ) +var DisallowedModReferences = map[string]bool{ + "satisfactory": true, + "factorygame": true, + "sml": true, + "satisfactorymodloader": true, + "examplemod": true, + "docmod": true, +} + func (r *mutationResolver) CreateMod(ctx context.Context, mod generated.NewMod) (*generated.Mod, error) { wrapper, newCtx := WrapMutationTrace(ctx, "createMod") defer wrapper.end() @@ -32,6 +41,10 @@ func (r *mutationResolver) CreateMod(ctx context.Context, mod generated.NewMod) return nil, errors.Wrap(err, "validation failed") } + if DisallowedModReferences[strings.ToLower(mod.ModReference)] { + return nil, errors.New("using this mod reference is not allowed") + } + if postgres.GetModByReference(newCtx, mod.ModReference) != nil { return nil, errors.New("mod with this mod reference already exists") } @@ -55,7 +68,6 @@ func (r *mutationResolver) CreateMod(ctx context.Context, mod generated.NewMod) if mod.Logo != nil { file, err := io.ReadAll(mod.Logo.File) - if err != nil { return nil, errors.Wrap(err, "failed to read logo file") } @@ -70,7 +82,6 @@ func (r *mutationResolver) CreateMod(ctx context.Context, mod generated.NewMod) } resultMod, err := postgres.CreateMod(newCtx, dbMod) - if err != nil { return nil, err } @@ -102,10 +113,11 @@ func (r *mutationResolver) UpdateMod(ctx context.Context, modID string, mod gene return nil, errors.Wrap(err, "validation failed") } - err := postgres.ResetModTags(newCtx, modID, mod.TagIDs) - - if err != nil { - return nil, err + if mod.TagIDs != nil { + err := postgres.ResetModTags(newCtx, modID, mod.TagIDs) + if err != nil { + return nil, err + } } dbMod := postgres.GetModByIDNoCache(newCtx, modID) @@ -128,13 +140,11 @@ func (r *mutationResolver) UpdateMod(ctx context.Context, modID string, mod gene if mod.Logo != nil { file, err := io.ReadAll(mod.Logo.File) - if err != nil { return nil, errors.Wrap(err, "failed to read logo file") } logoData, err := converter.ConvertAnyImageToWebp(ctx, file) - if err != nil { return nil, err } @@ -151,7 +161,6 @@ func (r *mutationResolver) UpdateMod(ctx context.Context, modID string, mod gene if mod.Authors != nil { authors, err := dataloader.For(ctx).UserModsByModID.Load(modID) - if err != nil { return nil, err } @@ -330,7 +339,6 @@ func (r *getModsResolver) Mods(ctx context.Context, obj *generated.GetMods) ([]* unapproved := resolverContext.Parent.Field.Field.Name == "getUnapprovedMods" modFilter, err := models.ProcessModFilter(resolverContext.Parent.Args["filter"].(map[string]interface{})) - if err != nil { return nil, err } @@ -361,7 +369,6 @@ func (r *getModsResolver) Count(ctx context.Context, obj *generated.GetMods) (in unapproved := resolverContext.Parent.Field.Field.Name == "getUnapprovedMods" modFilter, err := models.ProcessModFilter(resolverContext.Parent.Args["filter"].(map[string]interface{})) - if err != nil { return 0, err } @@ -383,7 +390,6 @@ func (r *getMyModsResolver) Mods(ctx context.Context, obj *generated.GetMyMods) unapproved := resolverContext.Parent.Field.Field.Name == "getMyUnapprovedMods" modFilter, err := models.ProcessModFilter(resolverContext.Parent.Args["filter"].(map[string]interface{})) - if err != nil { return nil, err } @@ -420,7 +426,6 @@ func (r *getMyModsResolver) Count(ctx context.Context, obj *generated.GetMyMods) unapproved := resolverContext.Parent.Field.Field.Name == "getMyUnapprovedMods" modFilter, err := models.ProcessModFilter(resolverContext.Parent.Args["filter"].(map[string]interface{})) - if err != nil { return 0, err } @@ -439,7 +444,6 @@ func (r *modResolver) Authors(ctx context.Context, obj *generated.Mod) ([]*gener defer wrapper.end() authors, err := dataloader.For(ctx).UserModsByModID.Load(obj.ID) - if err != nil { return nil, err } @@ -479,7 +483,6 @@ func (r *modResolver) Versions(ctx context.Context, obj *generated.Mod, filter m defer wrapper.end() versionFilter, err := models.ProcessVersionFilter(filter) - if err != nil { return nil, err } diff --git a/gql/resolver_oauth.go b/gql/resolver_oauth.go index bca2849..b8189f0 100644 --- a/gql/resolver_oauth.go +++ b/gql/resolver_oauth.go @@ -6,13 +6,13 @@ import ( "net/http" "net/url" + "github.com/pkg/errors" + "github.com/satisfactorymodding/smr-api/db/postgres" "github.com/satisfactorymodding/smr-api/generated" "github.com/satisfactorymodding/smr-api/oauth" "github.com/satisfactorymodding/smr-api/storage" "github.com/satisfactorymodding/smr-api/util" - - "github.com/pkg/errors" ) func (r *queryResolver) GetOAuthOptions(ctx context.Context, callbackURL string) (*generated.OAuthOptions, error) { @@ -20,7 +20,6 @@ func (r *queryResolver) GetOAuthOptions(ctx context.Context, callbackURL string) defer wrapper.end() unescapedURL, err := url.PathUnescape(callbackURL) - if err != nil { return nil, errors.Wrap(err, "unable to unescape callback url") } @@ -43,7 +42,6 @@ func (r *mutationResolver) OAuthGithub(ctx context.Context, code string, state s } user, err := oauth.GithubCallback(code, state) - if err != nil { return nil, err } @@ -52,7 +50,6 @@ func (r *mutationResolver) OAuthGithub(ctx context.Context, code string, state s userAgent := header.Get("User-Agent") token, err := completeOAuthFlow(newCtx, user, userAgent) - if err != nil { return nil, err } @@ -71,7 +68,6 @@ func (r *mutationResolver) OAuthGoogle(ctx context.Context, code string, state s } user, err := oauth.GoogleCallback(code, state) - if err != nil { return nil, err } @@ -80,7 +76,6 @@ func (r *mutationResolver) OAuthGoogle(ctx context.Context, code string, state s userAgent := header.Get("User-Agent") token, err := completeOAuthFlow(newCtx, user, userAgent) - if err != nil { return nil, err } @@ -99,7 +94,6 @@ func (r *mutationResolver) OAuthFacebook(ctx context.Context, code string, state } user, err := oauth.FacebookCallback(code, state) - if err != nil { return nil, err } @@ -108,7 +102,6 @@ func (r *mutationResolver) OAuthFacebook(ctx context.Context, code string, state userAgent := header.Get("User-Agent") token, err := completeOAuthFlow(newCtx, user, userAgent) - if err != nil { return nil, err } @@ -126,7 +119,6 @@ func completeOAuthFlow(ctx context.Context, user *oauth.UserData, userAgent stri if avatarURL != "" && newUser { avatarData, err := util.LinkToWebp(ctx, avatarURL) - if err != nil { return nil, err } diff --git a/gql/resolver_sml_versions.go b/gql/resolver_sml_versions.go index 0bfb287..a5859f5 100644 --- a/gql/resolver_sml_versions.go +++ b/gql/resolver_sml_versions.go @@ -4,15 +4,14 @@ import ( "context" "time" + "github.com/99designs/gqlgen/graphql" + "github.com/pkg/errors" + "gopkg.in/go-playground/validator.v9" + "github.com/satisfactorymodding/smr-api/db/postgres" "github.com/satisfactorymodding/smr-api/generated" "github.com/satisfactorymodding/smr-api/models" "github.com/satisfactorymodding/smr-api/util" - - "github.com/pkg/errors" - - "github.com/99designs/gqlgen/graphql" - "gopkg.in/go-playground/validator.v9" ) func (r *mutationResolver) CreateSMLVersion(ctx context.Context, smlVersion generated.NewSMLVersion) (*generated.SMLVersion, error) { @@ -25,7 +24,6 @@ func (r *mutationResolver) CreateSMLVersion(ctx context.Context, smlVersion gene } date, err := time.Parse(time.RFC3339Nano, smlVersion.Date) - if err != nil { return nil, errors.Wrap(err, "failed to parse date") } @@ -38,10 +36,19 @@ func (r *mutationResolver) CreateSMLVersion(ctx context.Context, smlVersion gene Link: smlVersion.Link, Changelog: smlVersion.Changelog, Date: date, + EngineVersion: smlVersion.EngineVersion, } resultSMLVersion, err := postgres.CreateSMLVersion(newCtx, dbSMLVersion) + for _, smlVersionTarget := range smlVersion.Targets { + postgres.Save(newCtx, &postgres.SMLVersionTarget{ + VersionID: resultSMLVersion.ID, + TargetName: string(smlVersionTarget.TargetName), + Link: smlVersionTarget.Link, + }) + } + if err != nil { return nil, err } @@ -58,6 +65,30 @@ func (r *mutationResolver) UpdateSMLVersion(ctx context.Context, smlVersionID st return nil, errors.Wrap(err, "validation failed") } + dbSMLTargets := postgres.GetSMLVersionTargets(newCtx, smlVersionID) + + for _, dbSMLTarget := range dbSMLTargets { + found := false + + for _, smlTarget := range smlVersion.Targets { + if dbSMLTarget.TargetName == string(smlTarget.TargetName) { + found = true + } + } + + if !found { + postgres.Delete(newCtx, &dbSMLTarget) + } + } + + for _, smlTarget := range smlVersion.Targets { + postgres.Save(newCtx, &postgres.SMLVersionTarget{ + VersionID: smlVersionID, + TargetName: string(smlTarget.TargetName), + Link: smlTarget.Link, + }) + } + dbSMLVersion := postgres.GetSMLVersionByID(newCtx, smlVersionID) if dbSMLVersion == nil { @@ -71,6 +102,7 @@ func (r *mutationResolver) UpdateSMLVersion(ctx context.Context, smlVersionID st SetStringINNOE(smlVersion.Link, &dbSMLVersion.Link) SetStringINNOE(smlVersion.Changelog, &dbSMLVersion.Changelog) SetDateINN(smlVersion.Date, &dbSMLVersion.Date) + SetStringINNOE(smlVersion.EngineVersion, &dbSMLVersion.EngineVersion) postgres.Save(newCtx, &dbSMLVersion) @@ -87,6 +119,12 @@ func (r *mutationResolver) DeleteSMLVersion(ctx context.Context, smlVersionID st return false, errors.New("smlVersion not found") } + dbSMLVersionTargets := postgres.GetSMLVersionTargets(newCtx, smlVersionID) + + for _, dbSMLVersionTarget := range dbSMLVersionTargets { + postgres.Delete(newCtx, &dbSMLVersionTarget) + } + postgres.Delete(newCtx, &dbSMLVersion) return true, nil @@ -113,7 +151,6 @@ func (r *getSMLVersionsResolver) SmlVersions(ctx context.Context, obj *generated resolverContext := graphql.GetFieldContext(ctx) smlVersionFilter, err := models.ProcessSMLVersionFilter(resolverContext.Parent.Args["filter"].(map[string]interface{})) - if err != nil { return nil, err } @@ -144,7 +181,6 @@ func (r *getSMLVersionsResolver) Count(ctx context.Context, obj *generated.GetSM resolverContext := graphql.GetFieldContext(ctx) smlVersionFilter, err := models.ProcessSMLVersionFilter(resolverContext.Parent.Args["filter"].(map[string]interface{})) - if err != nil { return 0, err } diff --git a/gql/resolver_tags.go b/gql/resolver_tags.go index 79d0da5..08d618f 100644 --- a/gql/resolver_tags.go +++ b/gql/resolver_tags.go @@ -2,17 +2,19 @@ package gql import ( "github.com/pkg/errors" + "golang.org/x/net/context" + "github.com/satisfactorymodding/smr-api/db/postgres" "github.com/satisfactorymodding/smr-api/generated" - "golang.org/x/net/context" ) -func (r *mutationResolver) CreateTag(ctx context.Context, tagName string) (*generated.Tag, error) { +func (r *mutationResolver) CreateTag(ctx context.Context, tagName string, description string) (*generated.Tag, error) { wrapper, newCtx := WrapMutationTrace(ctx, "createTag") defer wrapper.end() dbTag := &postgres.Tag{ - Name: tagName, + Name: tagName, + Description: description, } resultTag, err := postgres.CreateTag(newCtx, dbTag, true) @@ -22,15 +24,16 @@ func (r *mutationResolver) CreateTag(ctx context.Context, tagName string) (*gene return DBTagToGenerated(resultTag), nil } -func (r *mutationResolver) CreateMultipleTags(ctx context.Context, tagNames []string) ([]*generated.Tag, error) { +func (r *mutationResolver) CreateMultipleTags(ctx context.Context, tags []*generated.NewTag) ([]*generated.Tag, error) { wrapper, newCtx := WrapMutationTrace(ctx, "createMultipleTags") defer wrapper.end() - resultTags := make([]postgres.Tag, len(tagNames)) + resultTags := make([]postgres.Tag, len(tags)) - for i, tagName := range tagNames { + for i, tag := range tags { dbTag := &postgres.Tag{ - Name: tagName, + Name: tag.Name, + Description: tag.Description, } resultTag, err := postgres.CreateTag(newCtx, dbTag, false) @@ -59,7 +62,7 @@ func (r *mutationResolver) DeleteTag(ctx context.Context, id string) (bool, erro return true, nil } -func (r *mutationResolver) UpdateTag(ctx context.Context, id string, newName string) (*generated.Tag, error) { +func (r *mutationResolver) UpdateTag(ctx context.Context, id string, newName string, description string) (*generated.Tag, error) { wrapper, newCtx := WrapMutationTrace(ctx, "updateTag") defer wrapper.end() @@ -75,6 +78,7 @@ func (r *mutationResolver) UpdateTag(ctx context.Context, id string, newName str } SetStringINNOE(&newName, &dbTag.Name) + SetStringINNOE(&description, &dbTag.Description) postgres.Save(newCtx, &dbTag) diff --git a/gql/resolver_users.go b/gql/resolver_users.go index a69f162..6342880 100644 --- a/gql/resolver_users.go +++ b/gql/resolver_users.go @@ -11,6 +11,9 @@ import ( "net/http" "net/url" + "github.com/pkg/errors" + "github.com/spf13/viper" + "github.com/satisfactorymodding/smr-api/auth" "github.com/satisfactorymodding/smr-api/dataloader" "github.com/satisfactorymodding/smr-api/db/postgres" @@ -18,10 +21,6 @@ import ( "github.com/satisfactorymodding/smr-api/storage" "github.com/satisfactorymodding/smr-api/util" "github.com/satisfactorymodding/smr-api/util/converter" - - "github.com/pkg/errors" - - "github.com/spf13/viper" ) func (r *mutationResolver) UpdateUser(ctx context.Context, userID string, input generated.UpdateUser) (*generated.User, error) { @@ -36,13 +35,11 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, userID string, input if input.Avatar != nil { file, err := io.ReadAll(input.Avatar.File) - if err != nil { return nil, errors.Wrap(err, "failed to read avatar file") } avatarData, err := converter.ConvertAnyImageToWebp(ctx, file) - if err != nil { return nil, err } @@ -227,7 +224,6 @@ func (r *userModResolver) User(ctx context.Context, obj *generated.UserMod) (*ge defer wrapper.end() user, err := dataloader.For(ctx).UserByID.Load(obj.UserID) - if err != nil { return nil, err } @@ -264,7 +260,6 @@ func (r *mutationResolver) DiscourseSso(ctx context.Context, sso string, sig str } nonceString, err := base64.StdEncoding.DecodeString(sso) - if err != nil { return nil, errors.Wrap(err, "failed to decode sso") } diff --git a/gql/resolver_versions.go b/gql/resolver_versions.go index 2d93959..4d2d216 100644 --- a/gql/resolver_versions.go +++ b/gql/resolver_versions.go @@ -7,6 +7,11 @@ import ( "runtime/debug" "time" + "github.com/99designs/gqlgen/graphql" + "github.com/dgraph-io/ristretto" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/satisfactorymodding/smr-api/dataloader" "github.com/satisfactorymodding/smr-api/db/postgres" "github.com/satisfactorymodding/smr-api/generated" @@ -15,12 +20,6 @@ import ( "github.com/satisfactorymodding/smr-api/redis" "github.com/satisfactorymodding/smr-api/storage" "github.com/satisfactorymodding/smr-api/util" - - "github.com/pkg/errors" - - "github.com/99designs/gqlgen/graphql" - "github.com/dgraph-io/ristretto" - "github.com/rs/zerolog/log" ) func (r *mutationResolver) CreateVersion(ctx context.Context, modID string) (string, error) { @@ -72,7 +71,6 @@ func (r *mutationResolver) UploadVersionPart(ctx context.Context, modID string, // TODO Optimize fileData, err := io.ReadAll(file.File) - if err != nil { return false, errors.Wrap(err, "failed to read file") } @@ -272,7 +270,6 @@ func (r *getVersionsResolver) Versions(ctx context.Context, _ *generated.GetVers unapproved := resolverContext.Parent.Field.Field.Name == "getUnapprovedVersions" versionFilter, err := models.ProcessVersionFilter(resolverContext.Parent.Args["filter"].(map[string]interface{})) - if err != nil { return nil, err } @@ -309,7 +306,6 @@ func (r *getVersionsResolver) Count(ctx context.Context, _ *generated.GetVersion unapproved := resolverContext.Parent.Field.Field.Name == "getUnapprovedVersions" versionFilter, err := models.ProcessVersionFilter(resolverContext.Parent.Args["filter"].(map[string]interface{})) - if err != nil { return 0, err } @@ -323,7 +319,27 @@ func (r *getVersionsResolver) Count(ctx context.Context, _ *generated.GetVersion type versionResolver struct{ *Resolver } -func (r *versionResolver) Link(_ context.Context, obj *generated.Version) (string, error) { +func findWindowsTarget(obj *generated.Version) *generated.VersionTarget { + var windowsTarget *generated.VersionTarget + for _, target := range obj.Targets { + if target.TargetName == "Windows" { + windowsTarget = target + break + } + } + return windowsTarget +} + +func (r *versionResolver) Link(ctx context.Context, obj *generated.Version) (string, error) { + wrapper, _ := WrapQueryTrace(ctx, "Version.link") + defer wrapper.end() + + windowsTarget := findWindowsTarget(obj) + if windowsTarget != nil { + link, _ := r.VersionTarget().Link(ctx, windowsTarget) + return link, nil + } + return "/v1/version/" + obj.ID + "/download", nil } @@ -334,6 +350,50 @@ func (r *versionResolver) Mod(ctx context.Context, obj *generated.Version) (*gen return DBModToGenerated(postgres.GetModByID(newCtx, obj.ModID)), nil } +func (r *versionResolver) Hash(ctx context.Context, obj *generated.Version) (*string, error) { + wrapper, _ := WrapQueryTrace(ctx, "Version.hash") + defer wrapper.end() + + hash := "" + + windowsTarget := findWindowsTarget(obj) + if windowsTarget == nil { + if obj.Hash == nil { + return nil, nil + } + hash = *obj.Hash + } else { + if windowsTarget.Hash == nil { + return nil, nil + } + hash = *windowsTarget.Hash + } + + return &hash, nil +} + +func (r *versionResolver) Size(ctx context.Context, obj *generated.Version) (*int, error) { + wrapper, _ := WrapQueryTrace(ctx, "Version.size") + defer wrapper.end() + + size := 0 + + windowsTarget := findWindowsTarget(obj) + if windowsTarget == nil { + if obj.Size == nil { + return nil, nil + } + size = *obj.Size + } else { + if windowsTarget.Size == nil { + return nil, nil + } + size = *windowsTarget.Size + } + + return &size, nil +} + var versionDependencyCache, _ = ristretto.NewCache(&ristretto.Config{ NumCounters: 1e6, // number of keys to track frequency of (1M). MaxCost: 1e6, // maximum cost of cache (1M). @@ -371,6 +431,12 @@ func (r *versionResolver) Dependencies(ctx context.Context, obj *generated.Versi return converted, nil } +type versionTargetResolver struct{ *Resolver } + +func (r *versionTargetResolver) Link(_ context.Context, obj *generated.VersionTarget) (string, error) { + return "/v1/version/" + obj.VersionID + "/" + string(obj.TargetName) + "/download", nil +} + type getMyVersionsResolver struct{ *Resolver } func (r *getMyVersionsResolver) Versions(ctx context.Context, _ *generated.GetMyVersions) ([]*generated.Version, error) { @@ -381,7 +447,6 @@ func (r *getMyVersionsResolver) Versions(ctx context.Context, _ *generated.GetMy unapproved := resolverContext.Parent.Field.Field.Name == "getMyUnapprovedVersions" versionFilter, err := models.ProcessVersionFilter(resolverContext.Parent.Args["filter"].(map[string]interface{})) - if err != nil { return nil, err } @@ -418,7 +483,6 @@ func (r *getMyVersionsResolver) Count(ctx context.Context, _ *generated.GetMyVer unapproved := resolverContext.Parent.Field.Field.Name == "getMyUnapprovedVersions" versionFilter, err := models.ProcessVersionFilter(resolverContext.Parent.Args["filter"].(map[string]interface{})) - if err != nil { return 0, err } diff --git a/gql/versions.go b/gql/versions.go index a4f1dc1..de9a28b 100644 --- a/gql/versions.go +++ b/gql/versions.go @@ -6,6 +6,9 @@ import ( "io" "time" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/satisfactorymodding/smr-api/db/postgres" "github.com/satisfactorymodding/smr-api/generated" "github.com/satisfactorymodding/smr-api/integrations" @@ -13,10 +16,6 @@ import ( "github.com/satisfactorymodding/smr-api/storage" "github.com/satisfactorymodding/smr-api/util" "github.com/satisfactorymodding/smr-api/validation" - - "github.com/pkg/errors" - - "github.com/rs/zerolog/log" ) func FinalizeVersionUploadAsync(ctx context.Context, mod *postgres.Mod, versionID string, version generated.NewVersion) (*generated.CreateVersionResponse, error) { @@ -31,7 +30,6 @@ func FinalizeVersionUploadAsync(ctx context.Context, mod *postgres.Mod, versionI } modFile, err := storage.GetMod(mod.ID, mod.Name, versionID) - if err != nil { storage.DeleteMod(ctx, mod.ID, mod.Name, versionID) return nil, err @@ -39,17 +37,15 @@ func FinalizeVersionUploadAsync(ctx context.Context, mod *postgres.Mod, versionI // TODO Optimize fileData, err := io.ReadAll(modFile) - if err != nil { storage.DeleteMod(ctx, mod.ID, mod.Name, versionID) return nil, errors.Wrap(err, "failed reading mod file") } modInfo, err := validation.ExtractModInfo(ctx, fileData, true, true, mod.ModReference) - if err != nil { storage.DeleteMod(ctx, mod.ID, mod.Name, versionID) - return nil, err + return nil, errors.Wrap(err, "failed extracting mod info") } if modInfo.ModReference != mod.ModReference { @@ -57,6 +53,16 @@ func FinalizeVersionUploadAsync(ctx context.Context, mod *postgres.Mod, versionI return nil, errors.New("data.json mod_reference does not match mod reference") } + if modInfo.Type == validation.DataJSON { + storage.DeleteMod(ctx, mod.ID, mod.Name, versionID) + return nil, errors.New("data.json mods are obsolete and not allowed") + } + + if modInfo.Type == validation.MultiTargetUEPlugin && !util.FlagEnabled(util.FeatureFlagAllowMultiTargetUpload) { + storage.DeleteMod(ctx, mod.ID, mod.Name, versionID) + return nil, errors.New("multi-target mods are not allowed") + } + versionMajor := int(modInfo.Semver.Major()) versionMinor := int(modInfo.Semver.Minor()) versionPatch := int(modInfo.Semver.Patch()) @@ -123,37 +129,64 @@ func FinalizeVersionUploadAsync(ctx context.Context, mod *postgres.Mod, versionI postgres.Save(ctx, &dbVersion) } - // TODO Validate mod files - success, key := storage.RenameVersion(ctx, mod.ID, mod.Name, versionID, modInfo.Version) + if modInfo.Type == validation.MultiTargetUEPlugin { + targets := make([]*postgres.VersionTarget, 0) - if !success { - for modID, condition := range modInfo.Dependencies { - dependency := postgres.VersionDependency{ - VersionID: dbVersion.ID, - ModID: modID, - Condition: condition, - Optional: false, + for _, target := range modInfo.Targets { + dbVersionTarget := &postgres.VersionTarget{ + VersionID: dbVersion.ID, + TargetName: target, } - postgres.DeleteForced(ctx, &dependency) + postgres.Save(ctx, dbVersionTarget) + + targets = append(targets, dbVersionTarget) } - for modID, condition := range modInfo.OptionalDependencies { - dependency := postgres.VersionDependency{ - VersionID: dbVersion.ID, - ModID: modID, - Condition: condition, - Optional: true, + separateSuccess := true + for _, target := range targets { + log.Info().Str("target", target.TargetName).Str("mod", mod.Name).Str("version", dbVersion.Version).Msg("separating mod") + success, key, hash, size := storage.SeparateModTarget(ctx, fileData, mod.ID, mod.Name, dbVersion.Version, target.TargetName) + + if !success { + separateSuccess = false + break } - postgres.DeleteForced(ctx, &dependency) + target.Key = key + target.Hash = hash + target.Size = size + + postgres.Save(ctx, target) } - postgres.DeleteForced(ctx, &dbVersion) - storage.DeleteMod(ctx, mod.ID, mod.Name, versionID) + if !separateSuccess { + removeMod(ctx, modInfo, mod, dbVersion) + + return nil, errors.New("failed to separate mod") + } + } + + success, key := storage.RenameVersion(ctx, mod.ID, mod.Name, versionID, modInfo.Version) + + if !success { + removeMod(ctx, modInfo, mod, dbVersion) + return nil, errors.New("failed to upload mod") } + if modInfo.Type == validation.UEPlugin { + dbVersionTarget := &postgres.VersionTarget{ + VersionID: dbVersion.ID, + TargetName: "Windows", + Key: key, + Hash: *dbVersion.Hash, + Size: *dbVersion.Size, + } + + postgres.Save(ctx, dbVersionTarget) + } + dbVersion.Key = key postgres.Save(ctx, &dbVersion) postgres.Save(ctx, &mod) @@ -175,3 +208,46 @@ func FinalizeVersionUploadAsync(ctx context.Context, mod *postgres.Mod, versionI Version: DBVersionToGenerated(dbVersion), }, nil } + +func removeMod(ctx context.Context, modInfo *validation.ModInfo, mod *postgres.Mod, dbVersion *postgres.Version) { + for modID, condition := range modInfo.Dependencies { + dependency := postgres.VersionDependency{ + VersionID: dbVersion.ID, + ModID: modID, + Condition: condition, + Optional: false, + } + + postgres.DeleteForced(ctx, &dependency) + } + + for modID, condition := range modInfo.OptionalDependencies { + dependency := postgres.VersionDependency{ + VersionID: dbVersion.ID, + ModID: modID, + Condition: condition, + Optional: true, + } + + postgres.DeleteForced(ctx, &dependency) + } + + for _, target := range modInfo.Targets { + dbVersionTarget := postgres.VersionTarget{ + VersionID: dbVersion.ID, + TargetName: target, + } + + postgres.DeleteForced(ctx, &dbVersionTarget) + } + + // For UEPlugin mods, a Windows target is created. + // However, that happens after the last possible call to this function, therefore we can ignore it + + postgres.DeleteForced(ctx, &dbVersion) + + storage.DeleteMod(ctx, mod.ID, mod.Name, dbVersion.ID) + for _, target := range modInfo.Targets { + storage.DeleteModTarget(ctx, mod.ID, mod.Name, dbVersion.ID, target) + } +} diff --git a/gqlgen.yml b/gqlgen.yml index 445684c..7c2148e 100755 --- a/gqlgen.yml +++ b/gqlgen.yml @@ -68,6 +68,15 @@ models: resolver: true dependencies: resolver: true + size: + resolver: true + hash: + resolver: true + + VersionTarget: + fields: + link: + resolver: true GetMods: fields: diff --git a/integrations/discord.go b/integrations/discord.go index f2ea0aa..81347f6 100644 --- a/integrations/discord.go +++ b/integrations/discord.go @@ -10,12 +10,12 @@ import ( "runtime/debug" "strings" - "github.com/satisfactorymodding/smr-api/db/postgres" - "github.com/microcosm-cc/bluemonday" "github.com/rs/zerolog/log" "github.com/russross/blackfriday" "github.com/spf13/viper" + + "github.com/satisfactorymodding/smr-api/db/postgres" ) func NewMod(ctx context.Context, mod *postgres.Mod) { @@ -43,7 +43,7 @@ func NewMod(ctx context.Context, mod *postgres.Mod) { "embeds": []interface{}{ map[string]interface{}{ "title": "**" + mod.Name + "**", - "url": "https://ficsit.app/mod/" + mod.ID, + "url": "https://ficsit.app/mod/" + mod.ModReference, "color": 16750592, "description": mod.ShortDescription, "fields": []interface{}{ @@ -58,7 +58,6 @@ func NewMod(ctx context.Context, mod *postgres.Mod) { } payloadJSON, err := json.Marshal(payload) - if err != nil { log.Err(err).Msg("error marshaling discord webhook") return @@ -117,7 +116,7 @@ func NewVersion(ctx context.Context, version *postgres.Version) { "embeds": []interface{}{ map[string]interface{}{ "title": "**" + mod.Name + " v" + version.Version + "**", - "url": "https://ficsit.app/mod/" + mod.ID + "/version/" + version.ID, + "url": "https://ficsit.app/mod/" + mod.ModReference + "/version/" + version.ID, "color": 16750592, "description": "New Version Available!", "fields": []interface{}{ @@ -143,7 +142,6 @@ func NewVersion(ctx context.Context, version *postgres.Version) { } payloadJSON, err := json.Marshal(payload) - if err != nil { log.Err(err).Msg("error marshaling discord webhook") return diff --git a/migrations/code.go b/migrations/code.go index 556a593..cbb9c98 100644 --- a/migrations/code.go +++ b/migrations/code.go @@ -5,21 +5,21 @@ import ( "os" "strings" + "github.com/lab259/go-migration" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + postgres2 "github.com/satisfactorymodding/smr-api/db/postgres" // Import all migrations _ "github.com/satisfactorymodding/smr-api/migrations/code" - - "github.com/lab259/go-migration" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" ) type codeMigrationLogger struct { log *zerolog.Logger } -func (c codeMigrationLogger) Write(p []byte) (n int, err error) { +func (c codeMigrationLogger) Write(p []byte) (int, error) { message := strings.TrimRight(string(p), "\n") if len(message) > 0 { log.Info().Msg(message) diff --git a/migrations/code/20200426221900_test_new_migration.go b/migrations/code/20200426221900_test_new_migration.go index 158e2d0..936920e 100644 --- a/migrations/code/20200426221900_test_new_migration.go +++ b/migrations/code/20200426221900_test_new_migration.go @@ -3,10 +3,10 @@ package code import ( "context" - "github.com/satisfactorymodding/smr-api/migrations/utils" - "github.com/lab259/go-migration" "github.com/rs/zerolog/log" + + "github.com/satisfactorymodding/smr-api/migrations/utils" ) func init() { diff --git a/migrations/code/20200501224200_parse_paks.go b/migrations/code/20200501224200_parse_paks.go index 158e2d0..936920e 100644 --- a/migrations/code/20200501224200_parse_paks.go +++ b/migrations/code/20200501224200_parse_paks.go @@ -3,10 +3,10 @@ package code import ( "context" - "github.com/satisfactorymodding/smr-api/migrations/utils" - "github.com/lab259/go-migration" "github.com/rs/zerolog/log" + + "github.com/satisfactorymodding/smr-api/migrations/utils" ) func init() { diff --git a/migrations/code/20200524203800_after_reference_fix.go b/migrations/code/20200524203800_after_reference_fix.go index da0efdc..f9dbec1 100644 --- a/migrations/code/20200524203800_after_reference_fix.go +++ b/migrations/code/20200524203800_after_reference_fix.go @@ -3,10 +3,10 @@ package code import ( "context" - "github.com/satisfactorymodding/smr-api/migrations/utils" - "github.com/lab259/go-migration" "github.com/rs/zerolog/log" + + "github.com/satisfactorymodding/smr-api/migrations/utils" ) func init() { diff --git a/migrations/code/20200621195500_after_id_length_fix.go b/migrations/code/20200621195500_after_id_length_fix.go index da0efdc..f9dbec1 100644 --- a/migrations/code/20200621195500_after_id_length_fix.go +++ b/migrations/code/20200621195500_after_id_length_fix.go @@ -3,10 +3,10 @@ package code import ( "context" - "github.com/satisfactorymodding/smr-api/migrations/utils" - "github.com/lab259/go-migration" "github.com/rs/zerolog/log" + + "github.com/satisfactorymodding/smr-api/migrations/utils" ) func init() { diff --git a/migrations/code/20200622003600_after_validation_disable.go b/migrations/code/20200622003600_after_validation_disable.go index 158e2d0..936920e 100644 --- a/migrations/code/20200622003600_after_validation_disable.go +++ b/migrations/code/20200622003600_after_validation_disable.go @@ -3,10 +3,10 @@ package code import ( "context" - "github.com/satisfactorymodding/smr-api/migrations/utils" - "github.com/lab259/go-migration" "github.com/rs/zerolog/log" + + "github.com/satisfactorymodding/smr-api/migrations/utils" ) func init() { diff --git a/migrations/code/20200629093800_copy_to_new_bucket.go b/migrations/code/20200629093800_copy_to_new_bucket.go index 120b9d1..9e6aac6 100644 --- a/migrations/code/20200629093800_copy_to_new_bucket.go +++ b/migrations/code/20200629093800_copy_to_new_bucket.go @@ -3,11 +3,11 @@ package code import ( "context" - "github.com/satisfactorymodding/smr-api/redis/jobs" - "github.com/satisfactorymodding/smr-api/storage" - "github.com/lab259/go-migration" "github.com/rs/zerolog/log" + + "github.com/satisfactorymodding/smr-api/redis/jobs" + "github.com/satisfactorymodding/smr-api/storage" ) func init() { diff --git a/migrations/code/20200707150700_after_sml_version_fix.go b/migrations/code/20200707150700_after_sml_version_fix.go index 2937bc6..3b8e25b 100644 --- a/migrations/code/20200707150700_after_sml_version_fix.go +++ b/migrations/code/20200707150700_after_sml_version_fix.go @@ -3,11 +3,11 @@ package code import ( "context" - "github.com/satisfactorymodding/smr-api/db/postgres" - "github.com/satisfactorymodding/smr-api/migrations/utils" - "github.com/lab259/go-migration" "github.com/rs/zerolog/log" + + "github.com/satisfactorymodding/smr-api/db/postgres" + "github.com/satisfactorymodding/smr-api/migrations/utils" ) func init() { diff --git a/migrations/code/20200829171600_after_db_dirty.go b/migrations/code/20200829171600_after_db_dirty.go index 158e2d0..936920e 100644 --- a/migrations/code/20200829171600_after_db_dirty.go +++ b/migrations/code/20200829171600_after_db_dirty.go @@ -3,10 +3,10 @@ package code import ( "context" - "github.com/satisfactorymodding/smr-api/migrations/utils" - "github.com/lab259/go-migration" "github.com/rs/zerolog/log" + + "github.com/satisfactorymodding/smr-api/migrations/utils" ) func init() { diff --git a/migrations/code/20200829225100_after_broken_datajson.go b/migrations/code/20200829225100_after_broken_datajson.go index 3ecdc05..9dde468 100644 --- a/migrations/code/20200829225100_after_broken_datajson.go +++ b/migrations/code/20200829225100_after_broken_datajson.go @@ -3,11 +3,11 @@ package code import ( "context" - "github.com/satisfactorymodding/smr-api/db/postgres" - "github.com/satisfactorymodding/smr-api/migrations/utils" - "github.com/lab259/go-migration" "github.com/rs/zerolog/log" + + "github.com/satisfactorymodding/smr-api/db/postgres" + "github.com/satisfactorymodding/smr-api/migrations/utils" ) func init() { diff --git a/migrations/code/20200830011200_after_gorm_hotfix.go b/migrations/code/20200830011200_after_gorm_hotfix.go index 158e2d0..936920e 100644 --- a/migrations/code/20200830011200_after_gorm_hotfix.go +++ b/migrations/code/20200830011200_after_gorm_hotfix.go @@ -3,10 +3,10 @@ package code import ( "context" - "github.com/satisfactorymodding/smr-api/migrations/utils" - "github.com/lab259/go-migration" "github.com/rs/zerolog/log" + + "github.com/satisfactorymodding/smr-api/migrations/utils" ) func init() { diff --git a/migrations/code/20201014162200_after_bp_fix.go b/migrations/code/20201014162200_after_bp_fix.go index 158e2d0..936920e 100644 --- a/migrations/code/20201014162200_after_bp_fix.go +++ b/migrations/code/20201014162200_after_bp_fix.go @@ -3,10 +3,10 @@ package code import ( "context" - "github.com/satisfactorymodding/smr-api/migrations/utils" - "github.com/lab259/go-migration" "github.com/rs/zerolog/log" + + "github.com/satisfactorymodding/smr-api/migrations/utils" ) func init() { diff --git a/migrations/code/20201016202600_after_body_enable.go b/migrations/code/20201016202600_after_body_enable.go index 158e2d0..936920e 100644 --- a/migrations/code/20201016202600_after_body_enable.go +++ b/migrations/code/20201016202600_after_body_enable.go @@ -3,10 +3,10 @@ package code import ( "context" - "github.com/satisfactorymodding/smr-api/migrations/utils" - "github.com/lab259/go-migration" "github.com/rs/zerolog/log" + + "github.com/satisfactorymodding/smr-api/migrations/utils" ) func init() { diff --git a/migrations/code/20201019203800_after_remove_filter.go b/migrations/code/20201019203800_after_remove_filter.go index 158e2d0..936920e 100644 --- a/migrations/code/20201019203800_after_remove_filter.go +++ b/migrations/code/20201019203800_after_remove_filter.go @@ -3,10 +3,10 @@ package code import ( "context" - "github.com/satisfactorymodding/smr-api/migrations/utils" - "github.com/lab259/go-migration" "github.com/rs/zerolog/log" + + "github.com/satisfactorymodding/smr-api/migrations/utils" ) func init() { diff --git a/migrations/migrations.go b/migrations/migrations.go index 5bf8cf1..3999c69 100644 --- a/migrations/migrations.go +++ b/migrations/migrations.go @@ -5,14 +5,14 @@ import ( "errors" "strings" - postgres2 "github.com/satisfactorymodding/smr-api/db/postgres" - "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/database/postgres" + "github.com/rs/zerolog/log" + + postgres2 "github.com/satisfactorymodding/smr-api/db/postgres" // Import migrations _ "github.com/golang-migrate/migrate/v4/source/file" - "github.com/rs/zerolog/log" ) func RunMigrations(ctx context.Context) { @@ -21,16 +21,20 @@ func RunMigrations(ctx context.Context) { log.Info().Msg("Migrations Complete") } +var migrationDir = "./migrations" + +func SetMigrationDir(newMigrationDir string) { + migrationDir = newMigrationDir +} + func databaseMigrations(ctx context.Context) { db, _ := postgres2.DBCtx(ctx).DB() driver, err := postgres.WithInstance(db, &postgres.Config{}) - if err != nil { panic(err) } - m, err := migrate.NewWithDatabaseInstance("file://./migrations/sql", "postgres", driver) - + m, err := migrate.NewWithDatabaseInstance("file://"+migrationDir+"/sql", "postgres", driver) if err != nil { panic(err) } diff --git a/migrations/sql/000015_enable_trgm.up.sql b/migrations/sql/000015_enable_trgm.up.sql index d497121..588aec0 100755 --- a/migrations/sql/000015_enable_trgm.up.sql +++ b/migrations/sql/000015_enable_trgm.up.sql @@ -1 +1 @@ -CREATE EXTENSION pg_trgm; +CREATE EXTENSION IF NOT EXISTS pg_trgm; diff --git a/migrations/sql/000020_add_compatibility.up.sql b/migrations/sql/000020_add_compatibility.up.sql old mode 100755 new mode 100644 diff --git a/migrations/sql/000021_add_mod_platform.down.sql b/migrations/sql/000021_add_mod_platform.down.sql new file mode 100644 index 0000000..93b25e0 --- /dev/null +++ b/migrations/sql/000021_add_mod_platform.down.sql @@ -0,0 +1,2 @@ +drop table if exists mod_archs; +drop table if exists sml_archs; \ No newline at end of file diff --git a/migrations/sql/000021_add_mod_platform.up.sql b/migrations/sql/000021_add_mod_platform.up.sql new file mode 100644 index 0000000..7b2aad2 --- /dev/null +++ b/migrations/sql/000021_add_mod_platform.up.sql @@ -0,0 +1,21 @@ +-- Mod Links +create table if not exists mod_archs +( + id varchar(14) not null constraint mod_archs_pkey primary key, + mod_version_arch_id varchar(14), + platform varchar(16), + key text, + hash char(64), + size bigint +); +create index if not exists idx_mod_arch_id on mod_archs (mod_version_arch_id, platform); + +-- SML Links +create table if not exists sml_archs +( + id varchar(14) not null constraint sml_archs_pkey primary key, + sml_version_arch_id varchar(14), + platform varchar(16), + link text +); +create index if not exists idx_sml_archs_id on sml_archs (sml_version_arch_id, platform); \ No newline at end of file diff --git a/migrations/sql/000022_update_mod_targets.down.sql b/migrations/sql/000022_update_mod_targets.down.sql new file mode 100644 index 0000000..6a19693 --- /dev/null +++ b/migrations/sql/000022_update_mod_targets.down.sql @@ -0,0 +1,64 @@ +-- ID generation -- +-- This is not as random as the original ID, but it should be good enough -- +Create or replace function update_mod_platform_down_random_string(length integer) returns text as +$$ +declare + chars text[] := '{0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z}'; + result text := ''; + i integer := 0; +begin + if length < 0 then + raise exception 'Given length cannot be less than 0'; + end if; + for i in 1..length loop + result := result || chars[1+random()*(array_length(chars, 1)-1)]; + end loop; + return result; +end; +$$ language plpgsql; + +-- Mod version targets -- +ALTER TABLE version_targets RENAME TO mod_archs; + +ALTER TABLE mod_archs + RENAME COLUMN version_id TO mod_version_arch_id; +ALTER TABLE mod_archs + RENAME COLUMN target_name TO platform; +ALTER TABLE mod_archs + ADD COLUMN id varchar(14); + +UPDATE mod_archs SET id = update_mod_platform_down_random_string(14) WHERE true; + +ALTER TABLE mod_archs + ALTER COLUMN id SET NOT NULL; + +ALTER TABLE mod_archs + DROP CONSTRAINT version_targets_version_id_fkey, + DROP CONSTRAINT version_targets_pkey, + ADD CONSTRAINT mod_archs_pkey PRIMARY KEY (id); + +CREATE INDEX IF NOT EXISTS idx_mod_arch_id ON mod_archs (mod_version_arch_id, platform); + +-- SML version targets -- +ALTER TABLE sml_version_targets RENAME TO sml_archs; + +ALTER TABLE sml_archs + RENAME COLUMN version_id TO sml_version_arch_id; +ALTER TABLE sml_archs + RENAME COLUMN target_name TO platform; +ALTER TABLE sml_archs + ADD COLUMN id varchar(14); + +UPDATE sml_archs SET id = update_mod_platform_down_random_string(14) WHERE true; + +ALTER TABLE sml_archs + ALTER COLUMN id SET NOT NULL; + +ALTER TABLE sml_archs + DROP CONSTRAINT sml_version_targets_version_id_fkey, + DROP CONSTRAINT sml_version_targets_pkey, + ADD CONSTRAINT sml_archs_pkey PRIMARY KEY (id); + +CREATE INDEX IF NOT EXISTS idx_sml_archs_id ON sml_archs (sml_version_arch_id, platform); + +DROP FUNCTION update_mod_platform_down_random_string(length integer); \ No newline at end of file diff --git a/migrations/sql/000022_update_mod_targets.up.sql b/migrations/sql/000022_update_mod_targets.up.sql new file mode 100644 index 0000000..687af42 --- /dev/null +++ b/migrations/sql/000022_update_mod_targets.up.sql @@ -0,0 +1,31 @@ +-- Mod version targets -- +ALTER TABLE mod_archs RENAME TO version_targets; + +DROP INDEX idx_mod_arch_id; + +ALTER TABLE version_targets + RENAME COLUMN mod_version_arch_id TO version_id; +ALTER TABLE version_targets + RENAME COLUMN platform TO target_name; +ALTER TABLE version_targets + DROP COLUMN id; + +ALTER TABLE version_targets + ADD CONSTRAINT version_targets_version_id_fkey FOREIGN KEY (version_id) REFERENCES versions (id), + ADD CONSTRAINT version_targets_pkey PRIMARY KEY (version_id, target_name); + +ALTER TABLE sml_archs RENAME TO sml_version_targets; + +-- SML version targets -- +DROP INDEX idx_sml_archs_id; + +ALTER TABLE sml_version_targets + RENAME COLUMN sml_version_arch_id TO version_id; +ALTER TABLE sml_version_targets + RENAME COLUMN platform TO target_name; +ALTER TABLE sml_version_targets + DROP COLUMN id; + +ALTER TABLE sml_version_targets + ADD CONSTRAINT sml_version_targets_version_id_fkey FOREIGN KEY (version_id) REFERENCES sml_versions (id), + ADD CONSTRAINT sml_version_targets_pkey PRIMARY KEY (version_id, target_name); \ No newline at end of file diff --git a/migrations/sql/000023_migrate_versions_to_targets.down.sql b/migrations/sql/000023_migrate_versions_to_targets.down.sql new file mode 100644 index 0000000..702a1db --- /dev/null +++ b/migrations/sql/000023_migrate_versions_to_targets.down.sql @@ -0,0 +1,15 @@ +--Mod Targets-- +DELETE FROM version_targets + USING versions + WHERE version_targets.version_id = versions.id AND + version_targets.target_name = 'Windows' AND + version_targets.key = versions.key AND + version_targets.hash = versions.hash AND + version_targets.size = versions.size; + +--SML Targets-- +DELETE FROM sml_version_targets + USING sml_versions + WHERE sml_version_targets.version_id = sml_versions.id AND + sml_version_targets.target_name = 'Windows' AND + sml_version_targets.link = replace(sml_versions.link, '/tag/', '/download/') || '/SML.zip'; diff --git a/migrations/sql/000023_migrate_versions_to_targets.up.sql b/migrations/sql/000023_migrate_versions_to_targets.up.sql new file mode 100644 index 0000000..94e32af --- /dev/null +++ b/migrations/sql/000023_migrate_versions_to_targets.up.sql @@ -0,0 +1,13 @@ +-- Mod Targets -- +INSERT INTO version_targets (version_id, target_name, key, hash, size) +SELECT id, 'Windows', key, hash, size +FROM versions +WHERE NOT EXISTS(SELECT 1 FROM version_targets WHERE version_targets.version_id = versions.id) +ON CONFLICT DO NOTHING; + +-- SML Targets -- +INSERT INTO sml_version_targets (version_id, target_name, link) +SELECT id, 'Windows', replace(link, '/tag/', '/download/') || '/SML.zip' +FROM sml_versions +WHERE version LIKE '3%' +ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/migrations/sql/000024_add_sml_engine_version.down.sql b/migrations/sql/000024_add_sml_engine_version.down.sql new file mode 100644 index 0000000..87f13a5 --- /dev/null +++ b/migrations/sql/000024_add_sml_engine_version.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE sml_versions + DROP COLUMN engine_version; \ No newline at end of file diff --git a/migrations/sql/000024_add_sml_engine_version.up.sql b/migrations/sql/000024_add_sml_engine_version.up.sql new file mode 100644 index 0000000..3a018fe --- /dev/null +++ b/migrations/sql/000024_add_sml_engine_version.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE sml_versions + ADD COLUMN IF NOT EXISTS engine_version varchar(16) default '4.26'; \ No newline at end of file diff --git a/migrations/sql/000025_add_tag_description.down.sql b/migrations/sql/000025_add_tag_description.down.sql new file mode 100644 index 0000000..91bd0f5 --- /dev/null +++ b/migrations/sql/000025_add_tag_description.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE tags + DROP COLUMN description; diff --git a/migrations/sql/000025_add_tag_description.up.sql b/migrations/sql/000025_add_tag_description.up.sql new file mode 100644 index 0000000..5cf24be --- /dev/null +++ b/migrations/sql/000025_add_tag_description.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE tags + ADD COLUMN IF NOT EXISTS description varchar(512); \ No newline at end of file diff --git a/models/filters.go b/models/filters.go index 1023c2b..c7fd5d6 100644 --- a/models/filters.go +++ b/models/filters.go @@ -4,14 +4,13 @@ import ( "reflect" "strconv" - "github.com/satisfactorymodding/smr-api/generated" - - "github.com/pkg/errors" - "github.com/99designs/gqlgen/graphql" "github.com/mitchellh/hashstructure/v2" "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" "gopkg.in/go-playground/validator.v9" + + "github.com/satisfactorymodding/smr-api/generated" ) var dataValidator = validator.New() @@ -282,7 +281,6 @@ func ApplyChanges(changes interface{}, to interface{}) error { return v, nil }, }) - if err != nil { return errors.Wrap(err, "failed to create decoder") } diff --git a/nodes/mod.go b/nodes/mod.go index ae9b605..ee3b53c 100644 --- a/nodes/mod.go +++ b/nodes/mod.go @@ -4,12 +4,12 @@ import ( "strings" "time" + "github.com/labstack/echo/v4" + "github.com/satisfactorymodding/smr-api/db/postgres" "github.com/satisfactorymodding/smr-api/redis" "github.com/satisfactorymodding/smr-api/storage" "github.com/satisfactorymodding/smr-api/util" - - "github.com/labstack/echo/v4" ) // @Summary Retrieve a list of Mods @@ -273,13 +273,13 @@ func downloadModVersion(c echo.Context) error { mod := postgres.GetModByID(c.Request().Context(), modID) if mod == nil { - return c.String(404, "mod not found") + return c.String(404, "mod not found, modID:"+modID) } version := postgres.GetModVersion(c.Request().Context(), mod.ID, versionID) if version == nil { - return c.String(404, "version not found") + return c.String(404, "version not found, modID:"+modID+" versionID:"+versionID) } if redis.CanIncrement(c.RealIP(), "download", "version:"+versionID, time.Hour*4) { @@ -288,3 +288,70 @@ func downloadModVersion(c echo.Context) error { return c.Redirect(302, storage.GenerateDownloadLink(version.Key)) } + +// @Summary Download a Mod Version by TargetName +// @Tags Mod +// @Description Download a mod version by mod ID and version ID and TargetName +// @Accept json +// @Produce json +// @Param modId path string true "Mod ID" +// @Param versionId path string true "Version ID" +// @Param target path string true "TargetName" +// @Success 200 +// @Router /mod/{modId}/versions/{versionId}/{target}/download [get] +func downloadModVersionTarget(c echo.Context) error { + modID := c.Param("modId") + versionID := c.Param("versionId") + target := c.Param("target") + + mod := postgres.GetModByID(c.Request().Context(), modID) + + if mod == nil { + return c.String(404, "mod not found, modID:"+modID) + } + + version := postgres.GetModVersion(c.Request().Context(), mod.ID, versionID) + + if version == nil { + return c.String(404, "version not found, modID:"+modID+" versionID:"+versionID) + } + + versionTarget := postgres.GetVersionTarget(c.Request().Context(), versionID, target) + + if versionTarget == nil { + return c.String(404, "target not found, modID:"+modID+" versionID:"+versionID+" target:"+target) + } + + if redis.CanIncrement(c.RealIP(), "download", "version:"+versionID, time.Hour*4) { + postgres.IncrementVersionDownloads(c.Request().Context(), version) + } + + return c.Redirect(302, storage.GenerateDownloadLink(versionTarget.Key)) +} + +// @Summary Retrieve all Mod Versions +// @Tags Mod +// @Description Retrieve all mod versions by mod ID +// @Accept json +// @Produce json +// @Param modId path string true "Mod ID" +// @Success 200 +// @Router /mod/{modId}/versions/all [get] +func getAllModVersions(c echo.Context) (interface{}, *ErrorResponse) { + modID := c.Param("modId") + + mod := postgres.GetModByIDOrReference(c.Request().Context(), modID) + + if mod == nil { + return nil, &ErrorModNotFound + } + + versions := postgres.GetAllModVersionsWithDependencies(c.Request().Context(), mod.ID) + + converted := make([]*Version, len(versions)) + for k, v := range versions { + converted[k] = TinyVersionToVersion(&v) + } + + return converted, nil +} diff --git a/nodes/mod_types.go b/nodes/mod_types.go index 09df0e8..25cab07 100644 --- a/nodes/mod_types.go +++ b/nodes/mod_types.go @@ -7,20 +7,20 @@ import ( ) type Mod struct { - ID string `json:"id"` - Name string `json:"name"` - ShortDescription string `json:"short_description"` + UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` + CreatorID string `json:"creator_id"` FullDescription string `json:"full_description"` Logo string `json:"logo"` SourceURL string `json:"source_url"` - CreatorID string `json:"creator_id"` - Approved bool `json:"approved"` + ID string `json:"id"` + ShortDescription string `json:"short_description"` + Name string `json:"name"` Views uint `json:"views"` Downloads uint `json:"downloads"` Hotness uint `json:"hotness"` Popularity uint `json:"popularity"` - UpdatedAt time.Time `json:"updated_at"` - CreatedAt time.Time `json:"created_at"` + Approved bool `json:"approved"` } func ModToMod(mod *postgres.Mod, short bool) *Mod { @@ -48,16 +48,60 @@ func ModToMod(mod *postgres.Mod, short bool) *Mod { } type Version struct { - ID string `json:"id"` - Version string `json:"version"` - SMLVersion string `json:"sml_version"` - Changelog string `json:"changelog"` - Downloads uint `json:"downloads"` - Stability string `json:"stability"` - ModID string `json:"mod_id"` - Approved bool `json:"approved"` - UpdatedAt time.Time `json:"updated_at"` - CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + ID string `json:"id,omitempty"` + Version string `json:"version,omitempty"` + SMLVersion string `json:"sml_version,omitempty"` + Changelog string `json:"changelog,omitempty"` + Stability string `json:"stability,omitempty"` + ModID string `json:"mod_id,omitempty"` + Dependencies []VersionDependency `json:"dependencies,omitempty"` + Targets []VersionTarget `json:"targets,omitempty"` + Downloads uint `json:"downloads,omitempty"` + Approved bool `json:"approved,omitempty"` +} + +type VersionDependency struct { + ModID string `json:"mod_id"` + Condition string `json:"condition"` + Optional bool `json:"optional"` +} + +type VersionTarget struct { + VersionID string `json:"version_id"` + TargetName string `json:"target_name"` + Key string `json:"key"` + Hash string `json:"hash"` + Size int64 `json:"size"` +} + +func TinyVersionToVersion(version *postgres.TinyVersion) *Version { + var dependencies []VersionDependency + if version.Dependencies != nil { + dependencies = make([]VersionDependency, len(version.Dependencies)) + for i, v := range version.Dependencies { + dependencies[i] = VersionDependencyToVersionDependency(v) + } + } + + var targets []VersionTarget + if version.Targets != nil { + targets = make([]VersionTarget, len(version.Targets)) + for i, v := range version.Targets { + targets[i] = VersionTargetToVersionTarget(v) + } + } + + return &Version{ + UpdatedAt: version.UpdatedAt, + CreatedAt: version.CreatedAt, + ID: version.ID, + Version: version.Version, + SMLVersion: version.SMLVersion, + Dependencies: dependencies, + Targets: targets, + } } func VersionToVersion(version *postgres.Version) *Version { @@ -75,6 +119,24 @@ func VersionToVersion(version *postgres.Version) *Version { } } +func VersionDependencyToVersionDependency(version postgres.VersionDependency) VersionDependency { + return VersionDependency{ + ModID: version.ModID, + Condition: version.Condition, + Optional: version.Optional, + } +} + +func VersionTargetToVersionTarget(version postgres.VersionTarget) VersionTarget { + return VersionTarget{ + VersionID: version.VersionID, + TargetName: version.TargetName, + Key: version.Key, + Hash: version.Hash, + Size: version.Size, + } +} + type ModUser struct { UserID string `json:"user_id"` Role string `json:"role"` @@ -88,16 +150,16 @@ func ModUserToModUser(userMod *postgres.UserMod) *ModUser { } type SMLVersion struct { + Date time.Time `json:"date"` + UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` + BootstrapVersion *string `json:"bootstrap_version"` ID string `json:"id"` Version string `json:"version"` - SatisfactoryVersion int `json:"satisfactory_version"` - BootstrapVersion *string `json:"bootstrap_version"` Stability string `json:"stability"` - Date time.Time `json:"date"` Link string `json:"link"` Changelog string `json:"changelog"` - UpdatedAt time.Time `json:"updated_at"` - CreatedAt time.Time `json:"created_at"` + SatisfactoryVersion int `json:"satisfactory_version"` } func SMLVersionToSMLVersion(version *postgres.SMLVersion) *SMLVersion { diff --git a/nodes/node_types.go b/nodes/node_types.go index b4380e0..c1a8a5f 100755 --- a/nodes/node_types.go +++ b/nodes/node_types.go @@ -1,14 +1,14 @@ package nodes type GenericResponse struct { - Success bool `json:"success"` Data interface{} `json:"data,omitempty"` Error interface{} `json:"error,omitempty"` + Success bool `json:"success"` } type ErrorResponse struct { - Code int `json:"code"` Message string `json:"message"` + Code int `json:"code"` Status int `json:"-"` } diff --git a/nodes/oauth.go b/nodes/oauth.go index fdae9e8..9b8877d 100644 --- a/nodes/oauth.go +++ b/nodes/oauth.go @@ -4,12 +4,12 @@ import ( "bytes" "net/url" + "github.com/labstack/echo/v4" + "github.com/satisfactorymodding/smr-api/db/postgres" "github.com/satisfactorymodding/smr-api/oauth" "github.com/satisfactorymodding/smr-api/storage" "github.com/satisfactorymodding/smr-api/util" - - "github.com/labstack/echo/v4" ) // @Summary Retrieve a list of OAuth methods @@ -21,7 +21,6 @@ import ( func getOAuth(c echo.Context) (interface{}, *ErrorResponse) { callbackURL := c.Param("url") unescapedURL, err := url.PathUnescape(callbackURL) - if err != nil { return nil, GenericUserError(err) } @@ -51,7 +50,6 @@ func getGithub(c echo.Context) (interface{}, *ErrorResponse) { } user, err := oauth.GithubCallback(code, state) - if err != nil { return nil, GenericUserError(err) } @@ -65,7 +63,6 @@ func getGithub(c echo.Context) (interface{}, *ErrorResponse) { if avatarURL != "" && newUser { avatarData, err := util.LinkToWebp(c.Request().Context(), avatarURL) - if err != nil { return nil, GenericUserError(err) } diff --git a/nodes/routes.go b/nodes/routes.go index dd563e8..b6c6cf6 100755 --- a/nodes/routes.go +++ b/nodes/routes.go @@ -13,8 +13,11 @@ func RegisterModRoutes(router *echo.Group) { router.GET("/:modId/versions", dataWrapper(getModVersions)) router.GET("/:modId/authors", dataWrapper(getModAuthors)) + router.GET("/:modId/versions/all", dataWrapper(getAllModVersions)) + router.GET("/:modId/versions/:versionId", dataWrapper(getModVersion)) router.GET("/:modId/versions/:versionId/download", downloadModVersion) + router.GET("/:modId/versions/:versionId/:target/download", downloadModVersionTarget) } func RegisterModsRoutes(router *echo.Group) { @@ -47,6 +50,7 @@ func RegisterUsersRoutes(router *echo.Group) { func RegisterVersionRoutes(router *echo.Group) { router.GET("/:versionId", dataWrapper(getVersion)) router.GET("/:versionId/download", downloadVersion) + router.GET("/:versionId/:target/download", downloadModTarget) } func RegisterSMLRoutes(router *echo.Group) { diff --git a/nodes/shared.go b/nodes/shared.go index 2627e61..ddcd60f 100644 --- a/nodes/shared.go +++ b/nodes/shared.go @@ -1,9 +1,9 @@ package nodes import ( - "github.com/satisfactorymodding/smr-api/db/postgres" - "github.com/labstack/echo/v4" + + "github.com/satisfactorymodding/smr-api/db/postgres" ) type DataFunction func(c echo.Context) (data interface{}, err *ErrorResponse) @@ -11,7 +11,6 @@ type DataFunction func(c echo.Context) (data interface{}, err *ErrorResponse) func dataWrapper(nested DataFunction) func(c echo.Context) error { return func(c echo.Context) error { data, err := nested(c) - if err != nil { return c.JSON(err.Status, GenericResponse{ Success: false, @@ -29,7 +28,7 @@ func dataWrapper(nested DataFunction) func(c echo.Context) error { type AuthorizedDataFunction func(user *postgres.User, c echo.Context) (data interface{}, err *ErrorResponse) func authorized(nested AuthorizedDataFunction) DataFunction { - return func(c echo.Context) (data interface{}, err *ErrorResponse) { + return func(c echo.Context) (interface{}, *ErrorResponse) { user := userFromContext(c) if user == nil { diff --git a/nodes/sml.go b/nodes/sml.go index 497e83b..cc983dc 100644 --- a/nodes/sml.go +++ b/nodes/sml.go @@ -2,6 +2,7 @@ package nodes import ( "github.com/labstack/echo/v4" + "github.com/satisfactorymodding/smr-api/db/postgres" ) diff --git a/nodes/user.go b/nodes/user.go index c80e6e9..a03ba67 100644 --- a/nodes/user.go +++ b/nodes/user.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/labstack/echo/v4" + "github.com/satisfactorymodding/smr-api/db/postgres" ) diff --git a/nodes/user_types.go b/nodes/user_types.go index 08f526a..f929f53 100644 --- a/nodes/user_types.go +++ b/nodes/user_types.go @@ -7,11 +7,11 @@ import ( ) type User struct { + CreatedAt time.Time `json:"created_at"` ID string `json:"id"` Email string `json:"email"` Username string `json:"username"` Avatar string `json:"avatar"` - CreatedAt time.Time `json:"created_at"` } func UserToPrivateUser(user *postgres.User) *User { @@ -25,10 +25,10 @@ func UserToPrivateUser(user *postgres.User) *User { } type PublicUser struct { + CreatedAt time.Time `json:"created_at"` ID string `json:"id"` Username string `json:"username"` Avatar string `json:"avatar"` - CreatedAt time.Time `json:"created_at"` } func UserToPublicUser(user *postgres.User) *PublicUser { diff --git a/nodes/version.go b/nodes/version.go index c3134c4..1da2eee 100644 --- a/nodes/version.go +++ b/nodes/version.go @@ -4,6 +4,7 @@ import ( "time" "github.com/labstack/echo/v4" + "github.com/satisfactorymodding/smr-api/db/postgres" "github.com/satisfactorymodding/smr-api/redis" "github.com/satisfactorymodding/smr-api/storage" @@ -52,3 +53,36 @@ func downloadVersion(c echo.Context) error { return c.Redirect(302, storage.GenerateDownloadLink(version.Key)) } + +// @Summary Download a TargetName +// @Tags Version +// @Tags TargetName +// @Description Download a mod version by version ID and TargetName +// @Accept json +// @Produce json +// @Param versionId path string true "Version ID" +// @Param target path string true "TargetName" +// @Success 200 +// @Router /versions/{versionId}/{target}/download [get] +func downloadModTarget(c echo.Context) error { + versionID := c.Param("versionId") + target := c.Param("target") + + version := postgres.GetVersion(c.Request().Context(), versionID) + + if version == nil { + return c.String(404, "version not found, versionID:"+versionID) + } + + versionTarget := postgres.GetVersionTarget(c.Request().Context(), versionID, target) + + if versionTarget == nil { + return c.String(404, "target not found, versionID:"+versionID+" target:"+target) + } + + if redis.CanIncrement(c.RealIP(), "download", "version:"+versionID, time.Hour*4) { + postgres.IncrementVersionDownloads(c.Request().Context(), version) + } + + return c.Redirect(302, storage.GenerateDownloadLink(versionTarget.Key)) +} diff --git a/oauth/facebook.go b/oauth/facebook.go index db3cf33..3aa9fb3 100644 --- a/oauth/facebook.go +++ b/oauth/facebook.go @@ -4,16 +4,14 @@ import ( "encoding/json" "io" - "github.com/satisfactorymodding/smr-api/redis" - "github.com/pkg/errors" - "golang.org/x/oauth2" + + "github.com/satisfactorymodding/smr-api/redis" ) func FacebookCallback(code string, state string) (*UserData, error) { redirectURI, err := redis.GetNonce(state) - if err != nil { return nil, errors.New("login expired") } @@ -21,7 +19,6 @@ func FacebookCallback(code string, state string) (*UserData, error) { urlParam := oauth2.SetAuthURLParam("redirect_uri", redirectURI) token, err := facebookAuth.Exchange(ctx, code, urlParam) - if err != nil { return nil, errors.Wrap(err, "failed to exchange code") } @@ -29,13 +26,11 @@ func FacebookCallback(code string, state string) (*UserData, error) { client := facebookAuth.Client(ctx, token) resp, err := client.Get("https://graph.facebook.com/v5.0/me?fields=email,short_name,id,picture{url}") - if err != nil { return nil, errors.Wrap(err, "failed to get user data") } bytes, err := io.ReadAll(resp.Body) - if err != nil { return nil, errors.Wrap(err, "failed to read response body") } diff --git a/oauth/github.go b/oauth/github.go index 581f410..7e479e9 100644 --- a/oauth/github.go +++ b/oauth/github.go @@ -5,20 +5,18 @@ import ( "io" "strconv" - "github.com/satisfactorymodding/smr-api/redis" - "github.com/pkg/errors" + + "github.com/satisfactorymodding/smr-api/redis" ) func GithubCallback(code string, state string) (*UserData, error) { _, err := redis.GetNonce(state) - if err != nil { return nil, errors.New("login expired") } token, err := githubAuth.Exchange(ctx, code) - if err != nil { return nil, errors.Wrap(err, "failed to exchange code") } @@ -26,13 +24,11 @@ func GithubCallback(code string, state string) (*UserData, error) { client := githubAuth.Client(ctx, token) resp, err := client.Get("https://api.github.com/user") - if err != nil { return nil, errors.Wrap(err, "failed to get user data") } bytes, err := io.ReadAll(resp.Body) - if err != nil { return nil, errors.Wrap(err, "failed to read user data") } diff --git a/oauth/google.go b/oauth/google.go index c23494b..beab7e2 100644 --- a/oauth/google.go +++ b/oauth/google.go @@ -4,23 +4,20 @@ import ( "encoding/json" "io" - "github.com/satisfactorymodding/smr-api/redis" - "github.com/pkg/errors" - "github.com/spf13/viper" "golang.org/x/oauth2" + + "github.com/satisfactorymodding/smr-api/redis" ) func GoogleCallback(code string, state string) (*UserData, error) { _, err := redis.GetNonce(state) - if err != nil { return nil, errors.New("login expired") } token, err := googleAuth.Exchange(ctx, code, oauth2.SetAuthURLParam("redirect_uri", viper.GetString("frontend.url"))) - if err != nil { return nil, errors.Wrap(err, "failed to exchange code") } @@ -28,13 +25,11 @@ func GoogleCallback(code string, state string) (*UserData, error) { client := googleAuth.Client(ctx, token) resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo") - if err != nil { return nil, errors.Wrap(err, "failed to get user info") } bytes, err := io.ReadAll(resp.Body) - if err != nil { return nil, errors.Wrap(err, "failed to read user info") } diff --git a/oauth/oauth.go b/oauth/oauth.go index b12be4b..9c9ad39 100644 --- a/oauth/oauth.go +++ b/oauth/oauth.go @@ -3,21 +3,23 @@ package oauth import ( "context" - "github.com/satisfactorymodding/smr-api/redis" - "github.com/satisfactorymodding/smr-api/util" - "github.com/spf13/viper" "golang.org/x/oauth2" "golang.org/x/oauth2/facebook" "golang.org/x/oauth2/github" "golang.org/x/oauth2/google" + + "github.com/satisfactorymodding/smr-api/redis" + "github.com/satisfactorymodding/smr-api/util" ) var ctx = context.Background() -var githubAuth *oauth2.Config -var googleAuth *oauth2.Config -var facebookAuth *oauth2.Config +var ( + githubAuth *oauth2.Config + googleAuth *oauth2.Config + facebookAuth *oauth2.Config +) type Site string diff --git a/proto/parser/.gitignore b/proto/parser/.gitignore new file mode 100644 index 0000000..9b0b440 --- /dev/null +++ b/proto/parser/.gitignore @@ -0,0 +1 @@ +*.pb.go \ No newline at end of file diff --git a/proto/parser/parser.proto b/proto/parser/parser.proto new file mode 100644 index 0000000..913d5f6 --- /dev/null +++ b/proto/parser/parser.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option go_package = "github.com/satisfactorymodding/smr-api/proto/parser"; + +service Parser { + rpc Parse (ParseRequest) returns (stream AssetResponse); +} + +message ParseRequest { + bytes zip_data = 1; + string engine_version = 2; +} + +message AssetResponse { + string path = 1; + bytes data = 2; +} \ No newline at end of file diff --git a/redis/jobs/consumers/consumer_copy_object_from_old_bucket.go b/redis/jobs/consumers/consumer_copy_object_from_old_bucket.go index 2767d12..e86cbb5 100644 --- a/redis/jobs/consumers/consumer_copy_object_from_old_bucket.go +++ b/redis/jobs/consumers/consumer_copy_object_from_old_bucket.go @@ -3,12 +3,11 @@ package consumers import ( "encoding/json" - "github.com/satisfactorymodding/smr-api/redis/jobs/tasks" - "github.com/satisfactorymodding/smr-api/storage" - "github.com/pkg/errors" - "github.com/vmihailenco/taskq/v3" + + "github.com/satisfactorymodding/smr-api/redis/jobs/tasks" + "github.com/satisfactorymodding/smr-api/storage" ) func init() { diff --git a/redis/jobs/consumers/consumer_copy_object_to_old_bucket.go b/redis/jobs/consumers/consumer_copy_object_to_old_bucket.go index 2ce9c05..49b1fdb 100644 --- a/redis/jobs/consumers/consumer_copy_object_to_old_bucket.go +++ b/redis/jobs/consumers/consumer_copy_object_to_old_bucket.go @@ -3,12 +3,11 @@ package consumers import ( "encoding/json" - "github.com/satisfactorymodding/smr-api/redis/jobs/tasks" - "github.com/satisfactorymodding/smr-api/storage" - "github.com/pkg/errors" - "github.com/vmihailenco/taskq/v3" + + "github.com/satisfactorymodding/smr-api/redis/jobs/tasks" + "github.com/satisfactorymodding/smr-api/storage" ) func init() { diff --git a/redis/jobs/consumers/consumer_scan_mod_on_virus_total.go b/redis/jobs/consumers/consumer_scan_mod_on_virus_total.go index f5d2f7e..c906f8c 100644 --- a/redis/jobs/consumers/consumer_scan_mod_on_virus_total.go +++ b/redis/jobs/consumers/consumer_scan_mod_on_virus_total.go @@ -10,17 +10,16 @@ import ( "path" "time" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/vmihailenco/taskq/v3" + "github.com/satisfactorymodding/smr-api/db/postgres" "github.com/satisfactorymodding/smr-api/integrations" "github.com/satisfactorymodding/smr-api/redis/jobs/tasks" "github.com/satisfactorymodding/smr-api/storage" "github.com/satisfactorymodding/smr-api/util" "github.com/satisfactorymodding/smr-api/validation" - - "github.com/pkg/errors" - - "github.com/rs/zerolog/log" - "github.com/vmihailenco/taskq/v3" ) func init() { @@ -50,13 +49,11 @@ func ScanModOnVirusTotalConsumer(ctx context.Context, payload []byte) error { response, _ := http.Get(link) fileData, err := io.ReadAll(response.Body) - if err != nil { return errors.Wrap(err, "failed to read mod file") } archive, err := zip.NewReader(bytes.NewReader(fileData), int64(len(fileData))) - if err != nil { return errors.Wrap(err, "failed to unzip mod file") } @@ -64,9 +61,8 @@ func ScanModOnVirusTotalConsumer(ctx context.Context, payload []byte) error { toScan := make([]io.Reader, 0) names := make([]string, 0) for _, file := range archive.File { - if path.Ext(file.Name) == ".dll" { + if path.Ext(file.Name) == ".dll" || path.Ext(file.Name) == ".so" { open, err := file.Open() - if err != nil { return errors.Wrap(err, "failed to open mod file") } @@ -77,7 +73,6 @@ func ScanModOnVirusTotalConsumer(ctx context.Context, payload []byte) error { } success, err := validation.ScanFiles(ctx, toScan, names) - if err != nil { return err } diff --git a/redis/jobs/consumers/consumer_update_db_from_mod_version_file.go b/redis/jobs/consumers/consumer_update_db_from_mod_version_file.go index 6a72370..b6670cb 100644 --- a/redis/jobs/consumers/consumer_update_db_from_mod_version_file.go +++ b/redis/jobs/consumers/consumer_update_db_from_mod_version_file.go @@ -4,11 +4,10 @@ import ( "context" "encoding/json" - "github.com/satisfactorymodding/smr-api/redis/jobs/tasks" - "github.com/pkg/errors" - "github.com/vmihailenco/taskq/v3" + + "github.com/satisfactorymodding/smr-api/redis/jobs/tasks" ) func init() { diff --git a/redis/jobs/consumers/consumer_update_db_from_mod_version_json_file.go b/redis/jobs/consumers/consumer_update_db_from_mod_version_json_file.go index ad4c859..d392d83 100644 --- a/redis/jobs/consumers/consumer_update_db_from_mod_version_json_file.go +++ b/redis/jobs/consumers/consumer_update_db_from_mod_version_json_file.go @@ -4,11 +4,10 @@ import ( "context" "encoding/json" - "github.com/satisfactorymodding/smr-api/redis/jobs/tasks" - "github.com/pkg/errors" - "github.com/vmihailenco/taskq/v3" + + "github.com/satisfactorymodding/smr-api/redis/jobs/tasks" ) func init() { diff --git a/redis/jobs/consumers/utils.go b/redis/jobs/consumers/utils.go index 042c87f..9a8ebab 100644 --- a/redis/jobs/consumers/utils.go +++ b/redis/jobs/consumers/utils.go @@ -6,13 +6,12 @@ import ( "io" "net/http" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/satisfactorymodding/smr-api/db/postgres" "github.com/satisfactorymodding/smr-api/storage" "github.com/satisfactorymodding/smr-api/validation" - - "github.com/pkg/errors" - - "github.com/rs/zerolog/log" ) func UpdateModDataFromStorage(ctx context.Context, modID string, versionID string, metadata bool) error { @@ -25,7 +24,6 @@ func UpdateModDataFromStorage(ctx context.Context, modID string, versionID strin response, _ := http.Get(link) fileData, err := io.ReadAll(response.Body) - if err != nil { return errors.Wrap(err, "failed to read response body") } @@ -37,7 +35,6 @@ func UpdateModDataFromStorage(ctx context.Context, modID string, versionID strin } info, err := validation.ExtractModInfo(ctx, fileData, metadata, false, mod.ModReference) - if err != nil { log.Warn().Err(err).Msgf("[%s] Failed updating mod, likely outdated", versionID) // Outdated version diff --git a/redis/jobs/jobs.go b/redis/jobs/jobs.go index d0a70e2..0ce35aa 100644 --- a/redis/jobs/jobs.go +++ b/redis/jobs/jobs.go @@ -6,14 +6,14 @@ import ( "fmt" "time" - "github.com/vmihailenco/taskq/extra/taskqotel/v3" - "github.com/go-redis/redis/v8" "github.com/rs/zerolog/log" - "github.com/satisfactorymodding/smr-api/redis/jobs/tasks" "github.com/spf13/viper" + "github.com/vmihailenco/taskq/extra/taskqotel/v3" "github.com/vmihailenco/taskq/v3" "github.com/vmihailenco/taskq/v3/redisq" + + "github.com/satisfactorymodding/smr-api/redis/jobs/tasks" ) var queue taskq.Queue @@ -53,7 +53,6 @@ func SubmitJobUpdateDBFromModVersionFileTask(ctx context.Context, modID string, }) err := queue.Add(tasks.UpdateDBFromModVersionFileTask.WithArgs(ctx, task)) - if err != nil { log.Err(err).Msg("error adding task") } @@ -66,7 +65,6 @@ func SubmitJobUpdateDBFromModVersionJSONFileTask(ctx context.Context, modID stri }) err := queue.Add(tasks.UpdateDBFromModVersionJSONFileTask.WithArgs(ctx, task)) - if err != nil { log.Err(err).Msg("error adding task") } @@ -78,7 +76,6 @@ func SubmitJobCopyObjectFromOldBucketTask(ctx context.Context, key string) { }) err := queue.Add(tasks.CopyObjectFromOldBucketTask.WithArgs(ctx, task)) - if err != nil { log.Err(err).Msg("error adding task") } @@ -90,7 +87,6 @@ func SubmitJobCopyObjectToOldBucketTask(ctx context.Context, key string) { }) err := queue.Add(tasks.CopyObjectToOldBucketTask.WithArgs(ctx, task)) - if err != nil { log.Err(err).Msg("error adding task") } @@ -104,7 +100,6 @@ func SubmitJobScanModOnVirusTotalTask(ctx context.Context, modID string, version }) err := queue.Add(tasks.ScanModOnVirusTotalTask.WithArgs(ctx, task)) - if err != nil { log.Err(err).Msg("error adding task") } diff --git a/redis/jobs/tasks/tasks.go b/redis/jobs/tasks/tasks.go index 34b71c6..77b199c 100755 --- a/redis/jobs/tasks/tasks.go +++ b/redis/jobs/tasks/tasks.go @@ -2,11 +2,13 @@ package tasks import "github.com/vmihailenco/taskq/v3" -var UpdateDBFromModVersionFileTask *taskq.Task -var UpdateDBFromModVersionJSONFileTask *taskq.Task -var CopyObjectFromOldBucketTask *taskq.Task -var CopyObjectToOldBucketTask *taskq.Task -var ScanModOnVirusTotalTask *taskq.Task +var ( + UpdateDBFromModVersionFileTask *taskq.Task + UpdateDBFromModVersionJSONFileTask *taskq.Task + CopyObjectFromOldBucketTask *taskq.Task + CopyObjectToOldBucketTask *taskq.Task + ScanModOnVirusTotalTask *taskq.Task +) type UpdateDBFromModVersionFileData struct { ModID string `json:"mod_id"` diff --git a/redis/redis.go b/redis/redis.go index 4a9e7df..d5b829c 100644 --- a/redis/redis.go +++ b/redis/redis.go @@ -9,14 +9,13 @@ import ( "strconv" "time" - "github.com/satisfactorymodding/smr-api/generated" - - "github.com/pkg/errors" - "github.com/cespare/xxhash" "github.com/go-redis/redis" + "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/spf13/viper" + + "github.com/satisfactorymodding/smr-api/generated" ) var client *redis.Client @@ -137,3 +136,7 @@ func GetVersionUploadState(versionID string) (*generated.CreateVersionResponse, return data.Data, nil } + +func FlushRedis() { + client.FlushDB() +} diff --git a/schemas/sml_version.graphql b/schemas/sml_version.graphql index 9684c5b..4a3b5b3 100755 --- a/schemas/sml_version.graphql +++ b/schemas/sml_version.graphql @@ -8,14 +8,22 @@ type SMLVersion { satisfactory_version: Int! stability: VersionStabilities! link: String! + targets: [SMLVersionTarget]! changelog: String! date: Date! bootstrap_version: String + engine_version: String! updated_at: Date! created_at: Date! } +type SMLVersionTarget { + VersionID: SMLVersionID! + targetName: TargetName! + link: String! +} + type GetSMLVersions { sml_versions: [SMLVersion!]! count: Int! @@ -36,9 +44,11 @@ input NewSMLVersion { satisfactory_version: Int! stability: VersionStabilities! link: String! + targets: [NewSMLVersionTarget!]! changelog: String! date: Date! bootstrap_version: String + engine_version: String! } input UpdateSMLVersion { @@ -46,9 +56,21 @@ input UpdateSMLVersion { satisfactory_version: Int stability: VersionStabilities link: String + targets: [UpdateSMLVersionTarget]! changelog: String date: Date bootstrap_version: String + engine_version: String +} + +input NewSMLVersionTarget { + targetName: TargetName! + link: String! +} + +input UpdateSMLVersionTarget { + targetName: TargetName! + link: String! } input SMLVersionFilter { diff --git a/schemas/tags.graphql b/schemas/tags.graphql index e166662..a2a31f3 100644 --- a/schemas/tags.graphql +++ b/schemas/tags.graphql @@ -4,6 +4,12 @@ scalar TagName type Tag { id: TagID! name: TagName! + description: String! +} + +input NewTag { + name: TagName! + description: String! } input TagFilter { @@ -22,8 +28,8 @@ extend type Query { ### Mutations extend type Mutation { - createTag(tagName: TagName!): Tag @canManageTags @isLoggedIn - createMultipleTags(tagNames: [TagName!]!): [Tag!]! @canManageTags @isLoggedIn - updateTag(tagID: TagID!, NewName: TagName!): Tag! @canManageTags @isLoggedIn + createTag(tagName: TagName!, description: String!): Tag @canManageTags @isLoggedIn + createMultipleTags(tagNames: [NewTag!]!): [Tag!]! @canManageTags @isLoggedIn + updateTag(tagID: TagID!, NewName: TagName!, description: String!): Tag! @canManageTags @isLoggedIn deleteTag(tagID: TagID!): Boolean! @canManageTags @isLoggedIn } \ No newline at end of file diff --git a/schemas/version.graphql b/schemas/version.graphql index 0677402..168d0de 100755 --- a/schemas/version.graphql +++ b/schemas/version.graphql @@ -32,6 +32,7 @@ type Version { updated_at: Date! created_at: Date! link: String! + targets: [VersionTarget]! metadata: String size: Int hash: String @@ -40,6 +41,14 @@ type Version { dependencies: [VersionDependency!]! } +type VersionTarget { + VersionID: VersionID! + targetName: TargetName! + link: String! + size: Int + hash: String +} + type CreateVersionResponse { auto_approved: Boolean! version: Version diff --git a/schemas/version_target.graphql b/schemas/version_target.graphql new file mode 100644 index 0000000..2d77e3d --- /dev/null +++ b/schemas/version_target.graphql @@ -0,0 +1,5 @@ +enum TargetName { + Windows, + WindowsServer, + LinuxServer +} \ No newline at end of file diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..216a4f8 --- /dev/null +++ b/shell.nix @@ -0,0 +1,11 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + nativeBuildInputs = with pkgs.buildPackages; [ + libwebp + go + protobuf + protoc-gen-go-grpc + minio-client + ]; +} diff --git a/static/data-json-schema.json b/static/data-json-schema.json index 1471f2b..c66445a 100755 --- a/static/data-json-schema.json +++ b/static/data-json-schema.json @@ -89,7 +89,7 @@ "examples": [ "SDF.dll" ], - "pattern": "^(.*)\\.(pak|dll)$" + "pattern": "^(.*)\\.(pak|dll|so)$" }, "type": { "type": "string", diff --git a/storage/b2.go b/storage/b2.go index 98d574c..a616608 100644 --- a/storage/b2.go +++ b/storage/b2.go @@ -7,16 +7,15 @@ import ( "strconv" "strings" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/s3/s3manager" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/satisfactorymodding/smr-api/redis" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/s3" ) type B2 struct { @@ -35,7 +34,6 @@ func initializeB2(ctx context.Context, config Config) *B2 { } newSession, err := session.NewSession(s3Config) - if err != nil { log.Err(err).Msg("failed to create S3 session") return nil @@ -58,7 +56,6 @@ func (b2o *B2) Get(key string) (io.ReadCloser, error) { Bucket: aws.String(b2o.Config.Bucket), Key: aws.String(cleanedKey), }) - if err != nil { return nil, errors.Wrap(err, "failed to get object") } @@ -75,7 +72,6 @@ func (b2o *B2) Put(ctx context.Context, key string, body io.ReadSeeker) (string, Bucket: aws.String(b2o.Config.Bucket), Key: aws.String(cleanedKey), }) - if err != nil { return cleanedKey, errors.Wrap(err, "failed to upload file") } @@ -100,7 +96,6 @@ func (b2o *B2) StartMultipartUpload(key string) error { Bucket: aws.String(b2o.Config.Bucket), Key: aws.String(cleanedKey), }) - if err != nil { return errors.Wrap(err, "failed to create multipart upload") } @@ -121,7 +116,6 @@ func (b2o *B2) UploadPart(key string, part int64, data io.ReadSeeker) error { PartNumber: aws.Int64(part), UploadId: aws.String(id), }) - if err != nil { return errors.Wrap(err, "failed to upload part") } @@ -173,7 +167,6 @@ func (b2o *B2) Delete(key string) error { KeyMarker: aws.String(cleanedKey), Prefix: aws.String(cleanedKey), }) - if err != nil { return errors.Wrap(err, "failed to list object versions") } @@ -220,7 +213,6 @@ func (b2o *B2) Meta(key string) (*ObjectMeta, error) { Bucket: aws.String(b2o.Config.Bucket), Key: aws.String(cleanedKey), }) - if err != nil { return nil, errors.Wrap(err, "failed to get object meta") } @@ -230,3 +222,7 @@ func (b2o *B2) Meta(key string) (*ObjectMeta, error) { ContentType: data.ContentType, }, nil } + +func (b2o *B2) List(key string) ([]Object, error) { + return nil, nil // no-op +} diff --git a/storage/s3.go b/storage/s3.go index 4a2b6d3..b9d9752 100644 --- a/storage/s3.go +++ b/storage/s3.go @@ -7,17 +7,16 @@ import ( "strconv" "strings" - "github.com/satisfactorymodding/smr-api/redis" - + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/s3/s3manager" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/spf13/viper" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/s3" + "github.com/satisfactorymodding/smr-api/redis" ) type S3 struct { @@ -36,7 +35,6 @@ func initializeS3(ctx context.Context, config Config) *S3 { } newSession, err := session.NewSession(s3Config) - if err != nil { log.Err(err).Msg("failed to create S3 session") return nil @@ -59,7 +57,6 @@ func (s3o *S3) Get(key string) (io.ReadCloser, error) { Bucket: aws.String(s3o.Config.Bucket), Key: aws.String(cleanedKey), }) - if err != nil { return nil, errors.Wrap(err, "failed to get object") } @@ -77,7 +74,6 @@ func (s3o *S3) Put(ctx context.Context, key string, body io.ReadSeeker) (string, Bucket: aws.String(viper.GetString("storage.bucket")), Key: aws.String(cleanedKey), }) - if err != nil { return cleanedKey, errors.Wrap(err, "failed to upload file") } @@ -88,7 +84,7 @@ func (s3o *S3) Put(ctx context.Context, key string, body io.ReadSeeker) (string, func (s3o *S3) SignGet(key string) (string, error) { // Public Bucket cleanedKey := strings.TrimPrefix(key, "/") - return fmt.Sprintf("%s/file/%s/%s", s3o.BaseURL, viper.GetString("storage.bucket"), cleanedKey), nil + return fmt.Sprintf(viper.GetString("storage.keypath"), s3o.BaseURL, viper.GetString("storage.bucket"), cleanedKey), nil } func (s3o *S3) SignPut(key string) (string, error) { @@ -102,7 +98,6 @@ func (s3o *S3) StartMultipartUpload(key string) error { Bucket: aws.String(viper.GetString("storage.bucket")), Key: aws.String(cleanedKey), }) - if err != nil { return errors.Wrap(err, "failed to create multipart upload") } @@ -123,7 +118,6 @@ func (s3o *S3) UploadPart(key string, part int64, data io.ReadSeeker) error { PartNumber: aws.Int64(part), UploadId: aws.String(id), }) - if err != nil { return errors.Wrap(err, "failed to upload part") } @@ -175,8 +169,20 @@ func (s3o *S3) Delete(key string) error { KeyMarker: aws.String(cleanedKey), Prefix: aws.String(cleanedKey), }) - if err != nil { + if strings.Contains(err.Error(), "NotImplemented") { + _, err = s3o.S3Client.DeleteObject(&s3.DeleteObjectInput{ + Bucket: aws.String(viper.GetString("storage.bucket")), + Key: aws.String(cleanedKey), + }) + + if err != nil { + return errors.Wrap(err, "failed to delete objects") + } + + return nil + } + return errors.Wrap(err, "failed to list object versions") } @@ -197,6 +203,15 @@ func (s3o *S3) Delete(key string) error { } if len(objects) == 0 { + _, err = s3o.S3Client.DeleteObject(&s3.DeleteObjectInput{ + Bucket: aws.String(viper.GetString("storage.bucket")), + Key: aws.String(cleanedKey), + }) + + if err != nil { + return errors.Wrap(err, "failed to delete objects") + } + return nil } @@ -222,7 +237,6 @@ func (s3o *S3) Meta(key string) (*ObjectMeta, error) { Bucket: aws.String(viper.GetString("storage.bucket")), Key: aws.String(cleanedKey), }) - if err != nil { return nil, errors.Wrap(err, "failed to get object meta") } @@ -232,3 +246,23 @@ func (s3o *S3) Meta(key string) (*ObjectMeta, error) { ContentType: data.ContentType, }, nil } + +func (s3o *S3) List(prefix string) ([]Object, error) { + objects, err := s3o.S3Client.ListObjects(&s3.ListObjectsInput{ + Bucket: aws.String(viper.GetString("storage.bucket")), + Prefix: aws.String(prefix), + }) + if err != nil { + return nil, errors.Wrap(err, "failed to list objects") + } + + out := make([]Object, len(objects.Contents)) + for i, obj := range objects.Contents { + out[i] = Object{ + Key: obj.Key, + LastModified: obj.LastModified, + } + } + + return out, nil +} diff --git a/storage/storage.go b/storage/storage.go index c0de920..5597aaa 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -1,15 +1,18 @@ package storage import ( + "archive/zip" + "bytes" "context" + "crypto/sha256" + "encoding/hex" "fmt" "io" "strings" + "time" "github.com/avast/retry-go/v3" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" "github.com/spf13/viper" ) @@ -25,6 +28,7 @@ type Storage interface { Rename(from string, to string) error Delete(key string) error Meta(key string) (*ObjectMeta, error) + List(key string) ([]Object, error) } type ObjectMeta struct { @@ -32,6 +36,11 @@ type ObjectMeta struct { ContentType *string } +type Object struct { + Key *string + LastModified *time.Time +} + type Config struct { Type string `json:"type"` Bucket string `json:"bucket"` @@ -139,7 +148,6 @@ func UploadModLogo(ctx context.Context, modID string, data io.ReadSeeker) (bool, key := fmt.Sprintf("/images/mods/%s/logo.webp", modID) key, err := storage.Put(ctx, key, data) - if err != nil { log.Err(err).Msg("failed to upload mod logo") return false, "" @@ -167,7 +175,6 @@ func UploadUserAvatar(ctx context.Context, userID string, data io.ReadSeeker) (b log.Err(err).Msgf("failed to upload user avatar, retrying [%d]", n) }), ) - if err != nil { log.Err(err).Msg("failed to upload user avatar") return false, "" @@ -182,7 +189,6 @@ func GenerateDownloadLink(key string) string { } url, err := storage.SignGet(key) - if err != nil { return "" } @@ -281,6 +287,7 @@ func DeleteMod(ctx context.Context, modID string, name string, versionID string) key := fmt.Sprintf("/mods/%s/%s.smod", modID, cleanName+"-"+versionID) + log.Info().Str("key", key).Msg("deleting version") if err := storage.Delete(key); err != nil { log.Err(err).Msg("failed to delete version") return false @@ -289,6 +296,23 @@ func DeleteMod(ctx context.Context, modID string, name string, versionID string) return true } +func DeleteModTarget(ctx context.Context, modID string, name string, versionID string, target string) bool { + if storage == nil { + return false + } + + cleanName := cleanModName(name) + key := fmt.Sprintf("/mods/%s/%s.smod", modID, cleanName+"-"+target+"-"+versionID) + + log.Info().Str("key", key).Msg("deleting mod target") + if err := storage.Delete(key); err != nil { + log.Err(err).Msg("failed to delete version target") + return false + } + + return true +} + func ModVersionMeta(ctx context.Context, modID string, name string, versionID string) *ObjectMeta { if storage == nil { return nil @@ -348,3 +372,108 @@ func EncodeName(name string) string { } return result } + +func SeparateModTarget(ctx context.Context, body []byte, modID, name, modVersion, target string) (bool, string, string, int64) { + zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body))) + if err != nil { + return false, "", "", 0 + } + + cleanName := cleanModName(name) + + buf := new(bytes.Buffer) + zipWriter := zip.NewWriter(buf) + + for _, file := range zipReader.File { + if !strings.HasPrefix(file.Name, target+"/") && file.Name != target+"/" { + continue + } + + err = copyModFileToArchZip(file, zipWriter, strings.TrimPrefix(file.Name, target+"/")) + + if err != nil { + log.Err(err).Msg("failed to add file to " + target + " archive") + return false, "", "", 0 + } + } + + zipWriter.Close() + + key := fmt.Sprintf("/mods/%s/%s.smod", modID, cleanName+"-"+target+"-"+modVersion) + + _, err = storage.Put(ctx, key, bytes.NewReader(buf.Bytes())) + if err != nil { + log.Err(err).Msg("failed to save " + target + " archive") + return false, "", "", 0 + } + + hash := sha256.New() + hash.Write(buf.Bytes()) + + return true, key, hex.EncodeToString(hash.Sum(nil)), int64(buf.Len()) +} + +func copyModFileToArchZip(file *zip.File, zipWriter *zip.Writer, newName string) error { + fileHeader := file.FileHeader + fileHeader.Name = newName + + zipFile, err := zipWriter.CreateHeader(&fileHeader) + if err != nil { + return errors.Wrap(err, "failed to create file") + } + + rawFile, err := file.Open() + if err != nil { + return errors.Wrap(err, "failed to open file") + } + defer rawFile.Close() + + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(rawFile) + + if err != nil { + return errors.Wrap(err, "failed to read file") + } + + _, err = zipFile.Write(buf.Bytes()) + + if err != nil { + return errors.Wrap(err, "failed to write file") + } + + return nil +} + +func DeleteOldModAssets(modReference string, before time.Time) { + list, err := storage.List(fmt.Sprintf("/assets/mods/%s", modReference)) + if err != nil { + log.Err(err).Msg("failed to list assets") + return + } + + for _, object := range list { + if object.Key == nil { + continue + } + + if object.LastModified == nil || object.LastModified.Before(before) { + if err := storage.Delete(*object.Key); err != nil { + log.Err(err).Str("key", *object.Key).Msg("failed deleting old asset") + return + } + } + } +} + +func UploadModAsset(ctx context.Context, modReference string, path string, data []byte) { + if storage == nil { + return + } + + key := fmt.Sprintf("/assets/mods/%s/%s", modReference, strings.TrimPrefix(path, "/")) + + _, err := storage.Put(ctx, key, bytes.NewReader(data)) + if err != nil { + log.Err(err).Str("path", path).Msg("failed to upload mod asset") + } +} diff --git a/storage/wasabi.go b/storage/wasabi.go index 3a9e957..c35b864 100644 --- a/storage/wasabi.go +++ b/storage/wasabi.go @@ -5,12 +5,11 @@ import ( "io" "time" - "github.com/pkg/errors" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" + "github.com/pkg/errors" "github.com/rs/zerolog/log" ) @@ -30,7 +29,6 @@ func initializeWasabi(ctx context.Context, config Config) *Wasabi { } newSession, err := session.NewSession(s3Config) - if err != nil { log.Err(err).Msg("failed to create session") return nil @@ -49,7 +47,6 @@ func (wasabi *Wasabi) Get(key string) (io.ReadCloser, error) { Bucket: wasabi.Bucket, Key: aws.String(key), }) - if err != nil { return nil, errors.Wrap(err, "failed to get object") } @@ -63,7 +60,6 @@ func (wasabi *Wasabi) Put(ctx context.Context, key string, body io.ReadSeeker) ( Bucket: wasabi.Bucket, Key: aws.String(key), }) - if err != nil { return key, errors.Wrap(err, "failed to put object") } @@ -78,7 +74,6 @@ func (wasabi *Wasabi) SignGet(key string) (string, error) { }) urlStr, err := req.Presign(15 * time.Minute) - if err != nil { return "", errors.Wrap(err, "failed to sign url") } @@ -93,7 +88,6 @@ func (wasabi *Wasabi) SignPut(key string) (string, error) { }) urlStr, err := req.Presign(15 * time.Minute) - if err != nil { return "", errors.Wrap(err, "failed to sign url") } @@ -124,3 +118,7 @@ func (wasabi *Wasabi) Delete(key string) error { func (wasabi *Wasabi) Meta(key string) (*ObjectMeta, error) { return nil, errors.New("Unsupported") } + +func (wasabi *Wasabi) List(key string) ([]Object, error) { + return nil, errors.New("Unsupported") +} diff --git a/tests/announcements_test.go b/tests/announcements_test.go new file mode 100644 index 0000000..7a7d30e --- /dev/null +++ b/tests/announcements_test.go @@ -0,0 +1,127 @@ +package tests + +import ( + "testing" + + "github.com/MarvinJWendt/testza" + + "github.com/satisfactorymodding/smr-api/config" + "github.com/satisfactorymodding/smr-api/db/postgres" + "github.com/satisfactorymodding/smr-api/generated" + "github.com/satisfactorymodding/smr-api/migrations" +) + +func init() { + migrations.SetMigrationDir("../migrations") + config.SetConfigDir("../") + postgres.EnableDebug() +} + +func TestAnnouncements(t *testing.T) { + ctx, client, stop := setup() + defer stop() + + token, _, err := makeUser(ctx) + testza.AssertNoError(t, err) + + // Run Twice to detect any cache issues + for i := 0; i < 2; i++ { + // Create + createAnnouncement := authRequest(`mutation { + createAnnouncement(announcement: { + importance: Alert, + message: "Hello World" + }) { + id + } + }`, token) + + var createAnnouncementResponse struct { + CreateAnnouncement generated.Announcement + } + testza.AssertNoError(t, client.Run(ctx, createAnnouncement, &createAnnouncementResponse)) + testza.AssertNotEqual(t, "", createAnnouncementResponse.CreateAnnouncement.ID) + + // Query One + queryAnnouncement := authRequest(`query ($id: AnnouncementID!) { + getAnnouncement(announcementId: $id) { + id + message + importance + } + }`, token) + queryAnnouncement.Var("id", createAnnouncementResponse.CreateAnnouncement.ID) + + var queryAnnouncementResponse struct { + GetAnnouncement generated.Announcement + } + testza.AssertNoError(t, client.Run(ctx, queryAnnouncement, &queryAnnouncementResponse)) + testza.AssertEqual(t, createAnnouncementResponse.CreateAnnouncement.ID, queryAnnouncementResponse.GetAnnouncement.ID) + testza.AssertEqual(t, "Hello World", queryAnnouncementResponse.GetAnnouncement.Message) + testza.AssertEqual(t, generated.AnnouncementImportanceAlert, queryAnnouncementResponse.GetAnnouncement.Importance) + + // Update + updateAnnouncement := authRequest(`mutation ($id: AnnouncementID!) { + updateAnnouncement( + announcementId: $id, + announcement: { + importance: Fix, + message: "Foo Bar" + } + ) { + id + } + }`, token) + updateAnnouncement.Var("id", createAnnouncementResponse.CreateAnnouncement.ID) + + var updateAnnouncementResponse struct { + UpdateAnnouncement generated.Announcement + } + testza.AssertNoError(t, client.Run(ctx, updateAnnouncement, &updateAnnouncementResponse)) + + // Query Many + queryAnnouncements := authRequest(`query { + getAnnouncements { + id + message + importance + } + }`, token) + + var queryAnnouncementsResponse struct { + GetAnnouncements []generated.Announcement + } + testza.AssertNoError(t, client.Run(ctx, queryAnnouncements, &queryAnnouncementsResponse)) + testza.AssertEqual(t, 1, len(queryAnnouncementsResponse.GetAnnouncements)) + testza.AssertEqual(t, createAnnouncementResponse.CreateAnnouncement.ID, queryAnnouncementsResponse.GetAnnouncements[0].ID) + testza.AssertEqual(t, "Foo Bar", queryAnnouncementsResponse.GetAnnouncements[0].Message) + testza.AssertEqual(t, generated.AnnouncementImportanceFix, queryAnnouncementsResponse.GetAnnouncements[0].Importance) + + // Query By Importance + getAnnouncementsByImportance := authRequest(`query { + getAnnouncementsByImportance(importance: Info) { + id + message + importance + } + }`, token) + + var getAnnouncementsByImportanceResponse struct { + GetAnnouncements []generated.Announcement + } + testza.AssertNoError(t, client.Run(ctx, getAnnouncementsByImportance, &getAnnouncementsByImportanceResponse)) + testza.AssertEqual(t, 0, len(getAnnouncementsByImportanceResponse.GetAnnouncements)) + + // Delete + deleteAnnouncement := authRequest(`mutation ($id: AnnouncementID!) { + deleteAnnouncement(announcementId: $id) + }`, token) + deleteAnnouncement.Var("id", createAnnouncementResponse.CreateAnnouncement.ID) + + var deleteAnnouncementResponse struct { + DeleteAnnouncement bool + } + testza.AssertNoError(t, client.Run(ctx, deleteAnnouncement, &deleteAnnouncementResponse)) + testza.AssertTrue(t, deleteAnnouncementResponse.DeleteAnnouncement) + } +} diff --git a/tests/bootstrap_versions_test.go b/tests/bootstrap_versions_test.go new file mode 100644 index 0000000..8c56581 --- /dev/null +++ b/tests/bootstrap_versions_test.go @@ -0,0 +1,137 @@ +package tests + +import ( + "strconv" + "testing" + + "github.com/MarvinJWendt/testza" + + "github.com/satisfactorymodding/smr-api/config" + "github.com/satisfactorymodding/smr-api/db/postgres" + "github.com/satisfactorymodding/smr-api/generated" + "github.com/satisfactorymodding/smr-api/migrations" +) + +func init() { + migrations.SetMigrationDir("../migrations") + config.SetConfigDir("../") + postgres.EnableDebug() +} + +func TestBootstrapVersions(t *testing.T) { + ctx, client, stop := setup() + defer stop() + + token, _, err := makeUser(ctx) + testza.AssertNoError(t, err) + + // Run Twice to detect any cache issues + for i := 0; i < 2; i++ { + version := strconv.Itoa(i+1) + ".0.0" + + // Create + createBootstrapVersion := authRequest(`mutation ($version: String!) { + createBootstrapVersion(bootstrapVersion: { + version: $version, + satisfactory_version: 12345, + stability: beta, + link: "example.com", + changelog: "Hello World", + date: "2006-01-02T15:04:05Z" + }) { + id + } + }`, token) + createBootstrapVersion.Var("version", version) + + var createBootstrapVersionResponse struct { + CreateBootstrapVersion generated.BootstrapVersion + } + testza.AssertNoError(t, client.Run(ctx, createBootstrapVersion, &createBootstrapVersionResponse)) + testza.AssertNotEqual(t, "", createBootstrapVersionResponse.CreateBootstrapVersion.ID) + + // Query One + queryBootstrapVersion := authRequest(`query ($id: BootstrapVersionID!) { + getBootstrapVersion(bootstrapVersionID: $id) { + id + version + satisfactory_version + stability + link + changelog + date + } + }`, token) + queryBootstrapVersion.Var("id", createBootstrapVersionResponse.CreateBootstrapVersion.ID) + + var queryBootstrapVersionResponse struct { + GetBootstrapVersion generated.BootstrapVersion + } + testza.AssertNoError(t, client.Run(ctx, queryBootstrapVersion, &queryBootstrapVersionResponse)) + testza.AssertEqual(t, createBootstrapVersionResponse.CreateBootstrapVersion.ID, queryBootstrapVersionResponse.GetBootstrapVersion.ID) + testza.AssertEqual(t, version, queryBootstrapVersionResponse.GetBootstrapVersion.Version) + testza.AssertEqual(t, 12345, queryBootstrapVersionResponse.GetBootstrapVersion.SatisfactoryVersion) + testza.AssertEqual(t, generated.VersionStabilitiesBeta, queryBootstrapVersionResponse.GetBootstrapVersion.Stability) + testza.AssertEqual(t, "example.com", queryBootstrapVersionResponse.GetBootstrapVersion.Link) + testza.AssertEqual(t, "Hello World", queryBootstrapVersionResponse.GetBootstrapVersion.Changelog) + + // Update + updateBootstrapVersion := authRequest(`mutation ($id: BootstrapVersionID!) { + updateBootstrapVersion( + bootstrapVersionId: $id, + bootstrapVersion: { + changelog: "Foo Bar", + } + ) { + id + } + }`, token) + updateBootstrapVersion.Var("id", createBootstrapVersionResponse.CreateBootstrapVersion.ID) + + var updateBootstrapVersionResponse struct { + UpdateBootstrapVersion generated.BootstrapVersion + } + testza.AssertNoError(t, client.Run(ctx, updateBootstrapVersion, &updateBootstrapVersionResponse)) + + // Query Many + queryBootstrapVersions := authRequest(`query { + getBootstrapVersions { + count + bootstrap_versions { + id + version + satisfactory_version + stability + link + changelog + date + } + } + }`, token) + + var queryBootstrapVersionsResponse struct { + GetBootstrapVersions generated.GetBootstrapVersions + } + testza.AssertNoError(t, client.Run(ctx, queryBootstrapVersions, &queryBootstrapVersionsResponse)) + testza.AssertEqual(t, 1, queryBootstrapVersionsResponse.GetBootstrapVersions.Count) + testza.AssertEqual(t, 1, len(queryBootstrapVersionsResponse.GetBootstrapVersions.BootstrapVersions)) + testza.AssertEqual(t, createBootstrapVersionResponse.CreateBootstrapVersion.ID, queryBootstrapVersionsResponse.GetBootstrapVersions.BootstrapVersions[0].ID) + testza.AssertEqual(t, version, queryBootstrapVersionsResponse.GetBootstrapVersions.BootstrapVersions[0].Version) + testza.AssertEqual(t, 12345, queryBootstrapVersionsResponse.GetBootstrapVersions.BootstrapVersions[0].SatisfactoryVersion) + testza.AssertEqual(t, generated.VersionStabilitiesBeta, queryBootstrapVersionsResponse.GetBootstrapVersions.BootstrapVersions[0].Stability) + testza.AssertEqual(t, "example.com", queryBootstrapVersionsResponse.GetBootstrapVersions.BootstrapVersions[0].Link) + testza.AssertEqual(t, "Foo Bar", queryBootstrapVersionsResponse.GetBootstrapVersions.BootstrapVersions[0].Changelog) + + // Delete + deleteBootstrapVersion := authRequest(`mutation ($id: BootstrapVersionID!) { + deleteBootstrapVersion(bootstrapVersionId: $id) + }`, token) + deleteBootstrapVersion.Var("id", createBootstrapVersionResponse.CreateBootstrapVersion.ID) + + var deleteBootstrapVersionResponse struct { + DeleteBootstrapVersion bool + } + testza.AssertNoError(t, client.Run(ctx, deleteBootstrapVersion, &deleteBootstrapVersionResponse)) + testza.AssertTrue(t, deleteBootstrapVersionResponse.DeleteBootstrapVersion) + } +} diff --git a/tests/guides_test.go b/tests/guides_test.go new file mode 100644 index 0000000..5faa88b --- /dev/null +++ b/tests/guides_test.go @@ -0,0 +1,128 @@ +package tests + +import ( + "testing" + + "github.com/MarvinJWendt/testza" + + "github.com/satisfactorymodding/smr-api/config" + "github.com/satisfactorymodding/smr-api/db/postgres" + "github.com/satisfactorymodding/smr-api/generated" + "github.com/satisfactorymodding/smr-api/migrations" +) + +func init() { + migrations.SetMigrationDir("../migrations") + config.SetConfigDir("../") + postgres.EnableDebug() +} + +func TestGuides(t *testing.T) { + ctx, client, stop := setup() + defer stop() + + token, userID, err := makeUser(ctx) + testza.AssertNoError(t, err) + + // Run Twice to detect any cache issues + for i := 0; i < 2; i++ { + // Create + createGuide := authRequest(`mutation { + createGuide(guide: { + name: "Hello World", + short_description: "Short description about the guide", + guide: "The full guide text goes here." + }) { + id + } + }`, token) + + var createGuideResponse struct { + CreateGuide generated.Guide + } + testza.AssertNoError(t, client.Run(ctx, createGuide, &createGuideResponse)) + testza.AssertNotEqual(t, "", createGuideResponse.CreateGuide.ID) + + // Query One + queryGuide := authRequest(`query ($id: GuideID!) { + getGuide(guideId: $id) { + id + name + short_description + guide + user { + id + } + } + }`, token) + queryGuide.Var("id", createGuideResponse.CreateGuide.ID) + + var queryGuideResponse struct { + GetGuide generated.Guide + } + testza.AssertNoError(t, client.Run(ctx, queryGuide, &queryGuideResponse)) + testza.AssertEqual(t, createGuideResponse.CreateGuide.ID, queryGuideResponse.GetGuide.ID) + testza.AssertEqual(t, "Hello World", queryGuideResponse.GetGuide.Name) + testza.AssertEqual(t, "Short description about the guide", queryGuideResponse.GetGuide.ShortDescription) + testza.AssertEqual(t, "The full guide text goes here.", queryGuideResponse.GetGuide.Guide) + testza.AssertEqual(t, userID, queryGuideResponse.GetGuide.User.ID) + + // Update + updateGuide := authRequest(`mutation ($id: GuideID!) { + updateGuide( + guideId: $id, + guide: { + name: "Foo Bar" + } + ) { + id + } + }`, token) + updateGuide.Var("id", createGuideResponse.CreateGuide.ID) + + var updateGuideResponse struct { + UpdateGuide generated.Guide + } + testza.AssertNoError(t, client.Run(ctx, updateGuide, &updateGuideResponse)) + + // Query Many + queryGuides := authRequest(`query { + getGuides { + count + guides { + id + name + short_description + guide + user { + id + } + } + } + }`, token) + + var queryGuidesResponse struct { + GetGuides generated.GetGuides + } + testza.AssertNoError(t, client.Run(ctx, queryGuides, &queryGuidesResponse)) + testza.AssertEqual(t, 1, queryGuidesResponse.GetGuides.Count) + testza.AssertEqual(t, 1, len(queryGuidesResponse.GetGuides.Guides)) + testza.AssertEqual(t, createGuideResponse.CreateGuide.ID, queryGuidesResponse.GetGuides.Guides[0].ID) + testza.AssertEqual(t, "Foo Bar", queryGuidesResponse.GetGuides.Guides[0].Name) + testza.AssertEqual(t, "Short description about the guide", queryGuidesResponse.GetGuides.Guides[0].ShortDescription) + testza.AssertEqual(t, "The full guide text goes here.", queryGuidesResponse.GetGuides.Guides[0].Guide) + testza.AssertEqual(t, userID, queryGuidesResponse.GetGuides.Guides[0].User.ID) + + // Delete + deleteGuide := authRequest(`mutation ($id: GuideID!) { + deleteGuide(guideId: $id) + }`, token) + deleteGuide.Var("id", createGuideResponse.CreateGuide.ID) + + var deleteGuideResponse struct { + DeleteGuide bool + } + testza.AssertNoError(t, client.Run(ctx, deleteGuide, &deleteGuideResponse)) + testza.AssertTrue(t, deleteGuideResponse.DeleteGuide) + } +} diff --git a/tests/utils.go b/tests/utils.go new file mode 100644 index 0000000..6e87a95 --- /dev/null +++ b/tests/utils.go @@ -0,0 +1,112 @@ +package tests + +import ( + "context" + "sync" + + "github.com/machinebox/graphql" + "github.com/rs/zerolog/log" + + "github.com/satisfactorymodding/smr-api" + "github.com/satisfactorymodding/smr-api/auth" + "github.com/satisfactorymodding/smr-api/db/postgres" + "github.com/satisfactorymodding/smr-api/redis" + "github.com/satisfactorymodding/smr-api/util" +) + +func setup() (context.Context, *graphql.Client, func()) { + client := graphql.NewClient("http://localhost:5020/v2/query") + + ctx := smr.Initialize(context.Background()) + + redis.FlushRedis() + + var out []struct { + TableName string + } + + err := postgres.DBCtx(ctx).Raw(`SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'`).Scan(&out).Error + if err != nil { + panic(err) + } + + for _, name := range out { + err := postgres.DBCtx(ctx).Exec(`DROP TABLE IF EXISTS ` + name.TableName + ` CASCADE`).Error + if err != nil { + panic(err) + } + } + + smr.Migrate(ctx) + smr.Setup(ctx) + go smr.Serve() + + stopChannel := make(chan bool) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + + <-stopChannel + if err := smr.Stop(); err != nil { + panic(err) + } + }() + + return context.Background(), client, func() { + stopChannel <- true + wg.Wait() + } +} + +func makeUser(ctx context.Context) (string, string, error) { + user := postgres.User{ + SMRModel: postgres.SMRModel{ + ID: util.GenerateUniqueID(), + }, + Email: "test_user@ficsit.app", + Username: "test_user", + } + + err := postgres.DBCtx(ctx).Create(&user).Error + if err != nil { + return "", "", err + } + + log.Info().Str("id", user.ID).Msg("created fake test_user") + + userGroup := postgres.UserGroup{ + UserID: user.ID, + GroupID: auth.GroupAdmin.ID, + } + + err = postgres.DBCtx(ctx).Create(&userGroup).Error + if err != nil { + return "", "", err + } + + log.Info().Msg("created user admin group") + + session := postgres.UserSession{ + SMRModel: postgres.SMRModel{ + ID: util.GenerateUniqueID(), + }, + User: user, + Token: util.GenerateUserToken(), + } + + err = postgres.DBCtx(ctx).Create(&session).Error + if err != nil { + return "", "", err + } + + log.Info().Str("token", session.Token).Msg("created fake user session") + + return session.Token, user.ID, nil +} + +func authRequest(q string, token string) *graphql.Request { + req := graphql.NewRequest(q) + req.Header.Set("Authorization", token) + return req +} diff --git a/tools.go b/tools.go index 0a80bbc..bf72045 100644 --- a/tools.go +++ b/tools.go @@ -6,5 +6,6 @@ package smr import _ "github.com/99designs/gqlgen" import _ "github.com/swaggo/swag/cmd/swag" +//go:generate protoc -I./proto --go_out=./proto --go_opt=paths=source_relative --go-grpc_out=./proto --go-grpc_opt=paths=source_relative proto/parser/parser.proto //go:generate go run github.com/99designs/gqlgen generate //go:generate go run github.com/swaggo/swag/cmd/swag init --generalInfo cmd/api/serve.go diff --git a/util/analytics.go b/util/analytics.go index 8804533..0b92639 100755 --- a/util/analytics.go +++ b/util/analytics.go @@ -25,13 +25,13 @@ func serveReverseProxy(target string, res http.ResponseWriter, req *http.Request req.URL.RawQuery = query.Encode() - req.URL.Path = strings.Replace(req.URL.Path, "analytics/cozzect", "collect", -1) - req.URL.Path = strings.Replace(req.URL.Path, "analytics/r/cozzect", "r/collect", -1) - req.URL.Path = strings.Replace(req.URL.Path, "analytics/j/cozzect", "j/collect", -1) + req.URL.Path = strings.ReplaceAll(req.URL.Path, "analytics/cozzect", "collect") + req.URL.Path = strings.ReplaceAll(req.URL.Path, "analytics/r/cozzect", "r/collect") + req.URL.Path = strings.ReplaceAll(req.URL.Path, "analytics/j/cozzect", "j/collect") - req.RequestURI = strings.Replace(req.RequestURI, "analytics/cozzect", "collect", -1) - req.RequestURI = strings.Replace(req.RequestURI, "analytics/r/cozzect", "r/collect", -1) - req.RequestURI = strings.Replace(req.RequestURI, "analytics/j/cozzect", "j/collect", -1) + req.RequestURI = strings.ReplaceAll(req.RequestURI, "analytics/cozzect", "collect") + req.RequestURI = strings.ReplaceAll(req.RequestURI, "analytics/r/cozzect", "r/collect") + req.RequestURI = strings.ReplaceAll(req.RequestURI, "analytics/j/cozzect", "j/collect") req.Header.Set("X-Forwarded-Host", req.Header.Get("Host")) req.Host = redirectURL.Host diff --git a/util/converter/converter_linux.go b/util/converter/converter_linux.go index 0687354..c4b48c8 100644 --- a/util/converter/converter_linux.go +++ b/util/converter/converter_linux.go @@ -5,26 +5,23 @@ import ( "context" "image" + "github.com/chai2010/webp" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + giftowebp "github.com/sizeofint/gif-to-webp" + // GIF Support _ "image/gif" - // JPEG Support _ "image/jpeg" - // PNG Support _ "image/png" - - "github.com/chai2010/webp" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" - giftowebp "github.com/sizeofint/gif-to-webp" ) var converter = giftowebp.NewConverter() func ConvertAnyImageToWebp(ctx context.Context, imageAsBytes []byte) ([]byte, error) { imageData, imageType, err := image.Decode(bytes.NewReader(imageAsBytes)) - if err != nil { message := "error converting image to webp" log.Err(err).Msg(message) @@ -35,7 +32,6 @@ func ConvertAnyImageToWebp(ctx context.Context, imageAsBytes []byte) ([]byte, er if imageType == "gif" { webpBin, err := converter.Convert(imageAsBytes) - if err != nil { message := "error converting image to webp" log.Err(err).Msg(message) diff --git a/util/converter/converter_windows.go b/util/converter/converter_windows.go index 02a77a7..9ab1eb7 100755 --- a/util/converter/converter_windows.go +++ b/util/converter/converter_windows.go @@ -2,28 +2,39 @@ package converter import ( "bytes" + "context" + "image" + "github.com/chai2010/webp" "github.com/pkg/errors" "github.com/rs/zerolog/log" - "image" + + // GIF Support _ "image/gif" + // JPEG Support _ "image/jpeg" + // PNG Support _ "image/png" ) -func ConvertAnyImageToWebp(imageAsBytes []byte) ([]byte, error) { - imageData, _, err := image.Decode(bytes.NewReader(imageAsBytes)) - +func ConvertAnyImageToWebp(ctx context.Context, imageAsBytes []byte) ([]byte, error) { + imageData, imageType, err := image.Decode(bytes.NewReader(imageAsBytes)) if err != nil { - err := errors.Wrap(err, "error converting image to webp") - log.Error(err) - return nil, err + message := "error converting image to webp" + log.Err(err).Msg(message) + return nil, errors.Wrap(err, message) } result := bytes.NewBuffer(make([]byte, 0)) + if imageType == "gif" { + message := "converting gif to webp not supported on windows" + log.Err(err).Msg(message) + return nil, errors.Wrap(err, message) + } + if err := webp.Encode(result, imageData, nil); err != nil { - return nil, err + return nil, errors.Wrap(err, "error converting image to webp") } return result.Bytes(), nil diff --git a/util/flags.go b/util/flags.go new file mode 100644 index 0000000..7d6edea --- /dev/null +++ b/util/flags.go @@ -0,0 +1,22 @@ +package util + +import ( + "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +type FeatureFlag string + +const ( + FeatureFlagAllowMultiTargetUpload = "allow_multi_target_upload" +) + +func FlagEnabled(flag FeatureFlag) bool { + return viper.GetBool("feature_flags." + string(flag)) +} + +func PrintFeatureFlags() { + for _, flag := range []FeatureFlag{FeatureFlagAllowMultiTargetUpload} { + log.Info().Str("flag", string(flag)).Bool("enabled", FlagEnabled(flag)).Msg("flag") + } +} diff --git a/util/image.go b/util/image.go index dc355ea..65f8cdb 100644 --- a/util/image.go +++ b/util/image.go @@ -2,18 +2,19 @@ package util import ( "context" + "io" + "net/http" + + "github.com/pkg/errors" + + "github.com/satisfactorymodding/smr-api/util/converter" + // GIF Support _ "image/gif" // JPEG Support _ "image/jpeg" // PNG Support _ "image/png" - "io" - "net/http" - - "github.com/satisfactorymodding/smr-api/util/converter" - - "github.com/pkg/errors" ) func LinkToWebp(ctx context.Context, url string) ([]byte, error) { @@ -28,7 +29,6 @@ func LinkToWebp(ctx context.Context, url string) ([]byte, error) { } imageAsBytes, err := io.ReadAll(resp.Body) - if err != nil { return nil, errors.New("invalid url") } diff --git a/util/random.go b/util/random.go index 0a2ab87..f04696b 100644 --- a/util/random.go +++ b/util/random.go @@ -12,8 +12,10 @@ import ( var seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) -var randBuffer = [4]byte{} -var randMutex = sync.Mutex{} +var ( + randBuffer = [4]byte{} + randMutex = sync.Mutex{} +) const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" diff --git a/util/security.go b/util/security.go index 537d56c..56590c2 100644 --- a/util/security.go +++ b/util/security.go @@ -9,8 +9,10 @@ import ( "golang.org/x/crypto/ed25519" ) -var privateKey ed25519.PrivateKey -var pasetoV2 *paseto.V2 +var ( + privateKey ed25519.PrivateKey + pasetoV2 *paseto.V2 +) func InitializeSecurity() { var err error @@ -29,7 +31,6 @@ func GenerateUserToken() string { } token, err := pasetoV2.Sign(privateKey, jsonToken, nil) - if err != nil { panic(err) } diff --git a/validation/extractor.go b/validation/extractor.go new file mode 100644 index 0000000..9526e7b --- /dev/null +++ b/validation/extractor.go @@ -0,0 +1,92 @@ +package validation + +import ( + "encoding/json" + "fmt" + "regexp" +) + +func ExtractMetadata(raw []byte) (map[string]map[string][]interface{}, error) { + meta := make(map[string][]map[string]interface{}) + + if err := json.Unmarshal(raw, &meta); err != nil { + return nil, fmt.Errorf("failed extracting meta: %w", err) + } + + out := make(map[string]map[string][]interface{}) + + for fileName, data := range meta { + bpTypes := make(map[string]string) + for i, obj := range data { + if i == 0 && obj["Type"] != "BlueprintGeneratedClass" { + break + } + + if obj["Type"] == "BlueprintGeneratedClass" { + superName := obj["SuperStruct"].(map[string]interface{})["ObjectName"].(string) + _, objName := splitName(superName) + bpTypes[obj["Name"].(string)] = objName + continue + } + + if obj["Properties"] != nil { + classType := obj["Type"].(string) + if _, ok := ignoredClasses[classType]; ok { + continue + } + + if _, ok := out[fileName]; !ok { + out[fileName] = make(map[string][]interface{}) + } + + typ := bpTypes[classType] + if typ == "" { + typ = classType + } + + out[fileName][typ] = append(out[fileName][typ], rewriteRecursive(obj["Properties"])) + } + } + } + + return out, nil +} + +var objNameRegex = regexp.MustCompile(`^(.+?)'(.+?)'$`) + +func splitName(n string) (string, string) { + matches := objNameRegex.FindStringSubmatch(n) + return matches[1], matches[2] +} + +func rewriteRecursive(obj interface{}) interface{} { + switch b := obj.(type) { + case map[string]interface{}: + if mapHas("CultureInvariantString", b) { + return b["CultureInvariantString"] + } else if mapHas("ObjectName", b) && mapHas("ObjectPath", b) { + _, val := splitName(b["ObjectName"].(string)) + return val + } else if mapHas("AssetPathName", b) && mapHas("SubPathString", b) { + return b["AssetPathName"] + } else { + newOut := make(map[string]interface{}) + for k, v := range b { + newOut[k] = rewriteRecursive(v) + } + return newOut + } + case []interface{}: + newOut := make([]interface{}, len(b)) + for i, v := range b { + newOut[i] = rewriteRecursive(v) + } + return newOut + } + return obj +} + +func mapHas(key string, mp map[string]interface{}) bool { + _, ok := mp[key] + return ok +} diff --git a/validation/paks.go b/validation/paks.go index 3e758e6..6996eae 100644 --- a/validation/paks.go +++ b/validation/paks.go @@ -7,10 +7,10 @@ import ( "strings" "github.com/Vilsol/ue4pak/parser" + "github.com/rs/zerolog/log" // Import satisfactory-specific types _ "github.com/Vilsol/ue4pak/parser/games/satisfactory" - "github.com/rs/zerolog/log" ) var classInheritance = map[string]string{ @@ -168,14 +168,15 @@ var classInheritance = map[string]string{ "BodySetup": "", } -func AttemptExtractDataFromPak(ctx context.Context, reader parser.PakReader) (data map[string]map[string][]interface{}, err error) { +func AttemptExtractDataFromPak(ctx context.Context, reader parser.PakReader) (map[string]map[string][]interface{}, error) { + var err error defer func() { if r := recover(); r != nil { err = fmt.Errorf("%s\n%s", r, string(debug.Stack())) } }() - return ExtractDataFromPak(ctx, reader), nil + return ExtractDataFromPak(ctx, reader), err } // TODO Extract Images diff --git a/validation/paks_test.go b/validation/paks_test.go index d4167cb..2a8bbc3 100644 --- a/validation/paks_test.go +++ b/validation/paks_test.go @@ -32,7 +32,6 @@ func TestExtractDataFromPak(t *testing.T) { fmt.Println("Parsing file:", f) data, err := os.ReadFile(f) - if err != nil { panic(err) } @@ -49,7 +48,7 @@ func TestExtractDataFromPak(t *testing.T) { } else { marshal, _ := json.MarshalIndent(pakData, "", " ") - if err := os.WriteFile(f+".json", marshal, 0644); err != nil { + if err := os.WriteFile(f+".json", marshal, 0o644); err != nil { t.Error(err) } } diff --git a/validation/validation.go b/validation/validation.go index ab40c11..af9cad4 100644 --- a/validation/validation.go +++ b/validation/validation.go @@ -9,46 +9,68 @@ import ( "encoding/json" "fmt" "io" + "path" "path/filepath" + "sort" "strconv" "strings" + "time" "github.com/Masterminds/semver/v3" - "github.com/Vilsol/ue4pak/parser" "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/spf13/viper" "github.com/xeipuuv/gojsonschema" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/satisfactorymodding/smr-api/db/postgres" + "github.com/satisfactorymodding/smr-api/proto/parser" + "github.com/satisfactorymodding/smr-api/storage" ) +var AllowedTargets = []string{"Windows", "WindowsServer", "LinuxServer"} + type ModObject struct { Path string `json:"path"` Type string `json:"type"` } +type ModType int + +const ( + DataJSON ModType = iota + UEPlugin = 1 + MultiTargetUEPlugin = 2 +) + type ModInfo struct { + Dependencies map[string]string `json:"dependencies"` + OptionalDependencies map[string]string `json:"optional_dependencies"` + Semver *semver.Version `json:"-"` ModReference string `json:"mod_reference"` Version string `json:"version"` + Hash string `json:"-"` + SMLVersion string `json:"sml_version"` Objects []ModObject `json:"objects"` - Dependencies map[string]string `json:"dependencies"` - OptionalDependencies map[string]string `json:"optional_dependencies"` Metadata []map[string]map[string][]interface{} `json:"-"` + Targets []string `json:"-"` Size int64 `json:"-"` - Hash string `json:"-"` - Semver *semver.Version `json:"-"` - SMLVersion string `json:"sml_version"` + Type ModType `json:"-"` } -var dataJSONSchema gojsonschema.JSONLoader -var uPluginJSONSchema gojsonschema.JSONLoader +var ( + dataJSONSchema gojsonschema.JSONLoader + uPluginJSONSchema gojsonschema.JSONLoader +) func InitializeValidator() { absPath, err := filepath.Abs("static/data-json-schema.json") - if err != nil { panic(err) } - dataJSONSchema = gojsonschema.NewReferenceLoader("file://" + strings.Replace(absPath, "\\", "/", -1)) + dataJSONSchema = gojsonschema.NewReferenceLoader("file://" + strings.ReplaceAll(absPath, "\\", "/")) absPath, err = filepath.Abs("static/uplugin-json-schema.json") @@ -56,7 +78,7 @@ func InitializeValidator() { panic(err) } - uPluginJSONSchema = gojsonschema.NewReferenceLoader("file://" + strings.Replace(absPath, "\\", "/", -1)) + uPluginJSONSchema = gojsonschema.NewReferenceLoader("file://" + strings.ReplaceAll(absPath, "\\", "/")) } func ExtractModInfo(ctx context.Context, body []byte, withMetadata bool, withValidation bool, modReference string) (*ModInfo, error) { @@ -65,7 +87,6 @@ func ExtractModInfo(ctx context.Context, body []byte, withMetadata bool, withVal } archive, err := zip.NewReader(bytes.NewReader(body), int64(len(body))) - if err != nil { return nil, errors.New("invalid zip archive") } @@ -100,48 +121,94 @@ func ExtractModInfo(ctx context.Context, body []byte, withMetadata bool, withVal } } + if modInfo == nil { + // Neither data.json nor .uplugin found, try multi-target .uplugin + modInfo, err = validateMultiTargetPlugin(archive, withValidation, modReference) + if err != nil { + return nil, err + } + } + if modInfo == nil { return nil, errors.New("missing " + modReference + ".uplugin or data.json") } if withMetadata { // Extract all possible metadata - modInfo.Metadata = make([]map[string]map[string][]interface{}, 0) - for _, obj := range modInfo.Objects { - if strings.ToLower(obj.Type) == "pak" { - for _, archiveFile := range archive.File { - if obj.Path == archiveFile.Name { - data, err := archiveFile.Open() + conn, err := grpc.Dial(viper.GetString("extractor_host"), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, errors.Wrap(err, "failed to connect to metadata server") + } + defer conn.Close() + + engineVersion := "4.26" - if err != nil { - log.Err(err).Msg("failed opening archive file") - break - } + //nolint + if postgres.DBCtx(nil) != nil { + smlVersions := postgres.GetSMLVersions(ctx, nil) - pakData, err := io.ReadAll(data) + // Sort decrementing by version + sort.Slice(smlVersions, func(a, b int) bool { + return semver.MustParse(smlVersions[a].Version).Compare(semver.MustParse(smlVersions[b].Version)) > 0 + }) - if err != nil { - log.Err(err).Msg("failed reading archive file") - break - } + for _, version := range smlVersions { + constraint, err := semver.NewConstraint(modInfo.SMLVersion) + if err != nil { + return nil, errors.Wrap(err, "failed to create semver constraint") + } + + if constraint.Check(semver.MustParse(version.Version)) { + engineVersion = version.EngineVersion + break + } + } + } - reader := &parser.PakByteReader{ - Bytes: pakData, - } + parserClient := parser.NewParserClient(conn) + stream, err := parserClient.Parse(ctx, &parser.ParseRequest{ + ZipData: body, + EngineVersion: engineVersion, + }, + grpc.MaxCallSendMsgSize(1024*1024*1024), // 1GB + grpc.MaxCallRecvMsgSize(1024*1024*1024), // 1GB + ) + if err != nil { + return nil, errors.Wrap(err, "failed to parse mod") + } - pak, err := AttemptExtractDataFromPak(ctx, reader) + defer func(stream parser.Parser_ParseClient) { + err := stream.CloseSend() + if err != nil { + log.Ctx(ctx).Err(err).Msg("failed closing parser stream") + } + }(stream) + + beforeUpload := time.Now().Add(-time.Minute) + for { + asset, err := stream.Recv() + if err != nil { + //nolint + if errors.Is(err, io.EOF) || err == io.EOF { + break + } + return nil, errors.Wrap(err, "failed reading parser stream") + } - if err != nil { - log.Err(err).Msg("failed parsing archive file") - break - } + log.Ctx(ctx).Info().Str("path", asset.GetPath()).Msg("received asset from parser") - modInfo.Metadata = append(modInfo.Metadata, pak) - break - } + if asset.Path == "metadata.json" { + out, err := ExtractMetadata(asset.Data) + if err != nil { + return nil, err } + modInfo.Metadata = append(modInfo.Metadata, out) } + + storage.UploadModAsset(ctx, modInfo.ModReference, asset.GetPath(), asset.GetData()) } + + storage.DeleteOldModAssets(modInfo.ModReference, beforeUpload) } modInfo.Size = int64(len(body)) @@ -156,7 +223,6 @@ func ExtractModInfo(ctx context.Context, body []byte, withMetadata bool, withVal modInfo.Hash = hex.EncodeToString(hash.Sum(nil)) version, err := semver.StrictNewVersion(modInfo.Version) - if err != nil { log.Err(err).Msg("error parsing semver") return nil, errors.Wrap(err, "error parsing semver") @@ -178,13 +244,11 @@ func validateDataJSON(archive *zip.Reader, dataFile *zip.File, withValidation bo } dataJSON, err := io.ReadAll(rc) - if err != nil { return nil, errors.New("invalid zip archive") } result, err := gojsonschema.Validate(dataJSONSchema, gojsonschema.NewBytesLoader(dataJSON)) - if err != nil { return nil, errors.New("data.json doesn't follow schema. please view the help page. (" + err.Error() + ")") } @@ -221,7 +285,7 @@ func validateDataJSON(archive *zip.Reader, dataFile *zip.File, withValidation bo // Validate that all listed files are accounted for in data.json for _, archiveFile := range archive.File { if archiveFile != nil { - if strings.HasSuffix(archiveFile.Name, ".dll") || strings.HasSuffix(archiveFile.Name, ".pak") { + if strings.HasSuffix(archiveFile.Name, ".dll") || strings.HasSuffix(archiveFile.Name, ".pak") || strings.HasSuffix(archiveFile.Name, ".so") { found := false for _, obj := range modInfo.Objects { if obj.Path == archiveFile.Name { @@ -250,20 +314,22 @@ func validateDataJSON(archive *zip.Reader, dataFile *zip.File, withValidation bo } } + modInfo.Type = DataJSON + return &modInfo, nil } type UPlugin struct { SemVersion *string `json:"SemVersion"` - Version int64 `json:"Version"` Plugins []Plugin `json:"Plugins"` + Version int64 `json:"Version"` } type Plugin struct { - Name string `json:"Name"` - SemVersion string `json:"SemVersion"` BIsBasePlugin *bool `json:"bIsBasePlugin"` BIsOptional *bool `json:"bIsOptional"` + Name string `json:"Name"` + SemVersion string `json:"SemVersion"` } func validateUPluginJSON(archive *zip.Reader, uPluginFile *zip.File, withValidation bool, modReference string) (*ModInfo, error) { @@ -277,13 +343,11 @@ func validateUPluginJSON(archive *zip.Reader, uPluginFile *zip.File, withValidat } uPluginJSON, err := io.ReadAll(rc) - if err != nil { return nil, errors.New("invalid zip archive") } result, err := gojsonschema.Validate(uPluginJSONSchema, gojsonschema.NewBytesLoader(uPluginJSON)) - if err != nil { return nil, errors.New(uPluginFile.Name + " doesn't follow schema. please view the help page. (" + err.Error() + ")") } @@ -340,7 +404,7 @@ func validateUPluginJSON(archive *zip.Reader, uPluginFile *zip.File, withValidat Path: file.Name, Type: "pak", }) - } else if extension == "dll" { + } else if extension == "dll" || extension == "so" { modInfo.Objects = append(modInfo.Objects, ModObject{ Path: file.Name, Type: "sml_mod", @@ -365,5 +429,81 @@ func validateUPluginJSON(archive *zip.Reader, uPluginFile *zip.File, withValidat return nil, errors.New(uPluginFile.Name + " doesn't contain SML as a dependency.") } + modInfo.Type = UEPlugin + return &modInfo, nil } + +func validateMultiTargetPlugin(archive *zip.Reader, withValidation bool, modReference string) (*ModInfo, error) { + var targets []string + var uPluginFiles []*zip.File + for _, file := range archive.File { + if path.Base(file.Name) == modReference+".uplugin" && path.Dir(file.Name) != "." { + targets = append(targets, path.Dir(file.Name)) + uPluginFiles = append(uPluginFiles, file) + } + } + + if withValidation { + for _, target := range targets { + found := false + for _, allowedTarget := range AllowedTargets { + if target == allowedTarget { + found = true + break + } + } + if !found { + return nil, errors.New("multi-target plugin contains invalid target: " + target) + } + } + + for _, file := range archive.File { + found := false + for _, target := range targets { + if strings.HasPrefix(file.Name, target+"/") { + found = true + break + } + } + if !found { + return nil, errors.New("multi-target plugin contains file outside of target directories: " + file.Name) + } + } + } + + if len(uPluginFiles) == 0 { + return nil, errors.New("multi-target plugin doesn't contain any .uplugin files") + } + + if withValidation { + var lastData []byte + for _, uPluginFile := range uPluginFiles { + file, err := uPluginFile.Open() + if err != nil { + return nil, errors.Wrap(err, "failed to open .uplugin file") + } + data, err := io.ReadAll(file) + file.Close() + if err != nil { + return nil, errors.Wrap(err, "failed to read .uplugin file") + } + + if lastData != nil && !bytes.Equal(lastData, data) { + return nil, errors.New("multi-target plugin contains different .uplugin files") + } + lastData = data + } + } + + // All the .uplugin files should be the same at this point (assuming validation is enabled) + modInfo, err := validateUPluginJSON(archive, uPluginFiles[0], withValidation, modReference) + if err != nil { + return nil, errors.Wrap(err, "failed to validate multi-target plugin") + } + + modInfo.Targets = targets + modInfo.Type = MultiTargetUEPlugin + + return modInfo, nil +} diff --git a/validation/virustotal.go b/validation/virustotal.go index 9c74887..bbfe8dc 100644 --- a/validation/virustotal.go +++ b/validation/virustotal.go @@ -5,11 +5,11 @@ import ( "io" "time" - "github.com/pkg/errors" - "github.com/VirusTotal/vt-go" + "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/spf13/viper" + "golang.org/x/sync/errgroup" ) var client *vt.Client @@ -24,54 +24,92 @@ func InitializeVirusTotal() { type AnalysisResults struct { Attributes struct { - Status string `json:"status"` - Stats *struct { + Stats *struct { Suspicious *int `json:"suspicious,omitempty"` Malicious *int `json:"malicious,omitempty"` } `json:"stats,omitempty"` + Status string `json:"status"` } `json:"attributes,omitempty"` } func ScanFiles(ctx context.Context, files []io.Reader, names []string) (bool, error) { - for i, file := range files { - scan, err := client.NewFileScanner().Scan(file, names[i], nil) + errs, gctx := errgroup.WithContext(context.Background()) + fileCount := len(files) - if err != nil { - return false, errors.Wrap(err, "failed to scan file") + c := make(chan bool) + + for i := 0; i < fileCount; i++ { + count := i + errs.Go(func() error { + ok, err := scanFile(gctx, files[count], names[count]) + if err != nil { + return errors.Wrap(err, "failed to scan file") + } + c <- ok + return nil + }) + } + go func() { + _ = errs.Wait() + close(c) + }() + + success := true + for res := range c { + if !res { + success = false + break } + } + + if err := errs.Wait(); err != nil { + return false, errors.Wrap(err, "failed to scan file") + } + + return success, nil +} - analysisID := scan.ID() +func scanFile(ctx context.Context, file io.Reader, name string) (bool, error) { + scan, err := client.NewFileScanner().Scan(file, name, nil) + if err != nil { + return false, errors.Wrap(err, "failed to scan file") + } - log.Info().Msgf("uploaded virus scan for file %s and analysis ID: %s", names[i], analysisID) + analysisID := scan.ID() - for { - time.Sleep(time.Second * 15) + log.Info().Msgf("uploaded virus scan for file %s and analysis ID: %s", name, analysisID) - var target AnalysisResults - _, err = client.GetData(vt.URL("analyses/%s", analysisID), &target) + for { + time.Sleep(time.Second * 15) - if err != nil { - return false, errors.Wrap(err, "failed to get analysis results") - } + var target AnalysisResults + _, err = client.GetData(vt.URL("analyses/%s", analysisID), &target) - if target.Attributes.Status != "completed" { - continue - } + if err != nil { + return false, errors.Wrap(err, "failed to get analysis results") + } - if target.Attributes.Stats == nil { - return false, nil - } + if target.Attributes.Status != "completed" { + continue + } - if target.Attributes.Stats.Malicious == nil || target.Attributes.Stats.Suspicious == nil { - return false, nil - } + if target.Attributes.Stats == nil { + log.Error().Msgf("no stats available. failing file: %s", name) + return false, nil + } - if *target.Attributes.Stats.Malicious > 0 || *target.Attributes.Stats.Suspicious > 0 { - return false, nil - } + if target.Attributes.Stats.Malicious == nil || target.Attributes.Stats.Suspicious == nil { + log.Error().Msgf("unable to determine malicious or suspicious File: %s", name) + return false, nil + } - break + // Why 1? Well because some company made a shitty AI and it flags random mods. + if *target.Attributes.Stats.Malicious > 1 || *target.Attributes.Stats.Suspicious > 1 { + log.Error().Msgf("suspicious or malicious file found: %s", name) + return false, nil } + + break } return true, nil