ficsit-cli-flake/cmd/smr/upload.go

226 lines
6.1 KiB
Go
Raw Normal View History

2022-10-14 16:11:16 +00:00
package smr
import (
"bytes"
"encoding/json"
"errors"
"fmt"
2022-10-14 16:11:16 +00:00
"io"
"log/slog"
2022-10-14 16:11:16 +00:00
"math"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/satisfactorymodding/ficsit-cli/cli"
"github.com/satisfactorymodding/ficsit-cli/ficsit"
)
const uploadVersionPartGQL = `mutation UploadVersionPart($modId: ModID!, $versionId: VersionID!, $part: Int!, $file: Upload!) {
uploadVersionPart(modId: $modId, versionId: $versionId, part: $part, file: $file)
}`
func init() {
Cmd.AddCommand(uploadCmd)
}
var uploadCmd = &cobra.Command{
Use: "upload [flags] <mod-id> <file> <changelog...>",
Short: "Upload a new mod version",
Args: cobra.MinimumNArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
chunkSize := viper.GetInt64("chunk-size")
if chunkSize < 1000000 {
return errors.New("chunk size cannot be smaller than 1MB")
}
var versionStability ficsit.VersionStabilities
switch viper.GetString("stability") {
case "alpha":
versionStability = ficsit.VersionStabilitiesAlpha
case "beta":
versionStability = ficsit.VersionStabilitiesBeta
case "release":
versionStability = ficsit.VersionStabilitiesRelease
default:
return errors.New("invalid version stability: " + viper.GetString("stability"))
}
modID := args[0]
filePath := args[1]
changelog := strings.Join(args[2:], " ")
stat, err := os.Stat(filePath)
if err != nil {
return fmt.Errorf("failed to stat file: %w", err)
2022-10-14 16:11:16 +00:00
}
global, err := cli.InitCLI(true)
if err != nil {
return err
}
if stat.IsDir() {
return errors.New("file cannot be a directory")
}
// TODO Validate .smod file before upload
logBase := slog.With(slog.String("mod-id", modID), slog.String("path", filePath))
logBase.Info("creating a new mod version")
2022-10-14 16:11:16 +00:00
createdVersion, err := ficsit.CreateVersion(cmd.Context(), global.APIClient, modID)
if err != nil {
return err
}
logBase = logBase.With(slog.String("version-id", createdVersion.GetVersionID()))
logBase.Info("received version id")
2022-10-14 16:11:16 +00:00
// TODO Parallelize chunk uploading
chunkCount := int(math.Ceil(float64(stat.Size()) / float64(chunkSize)))
for i := 0; i < chunkCount; i++ {
chunkLog := logBase.With(slog.Int("chunk", i))
chunkLog.Info("uploading chunk")
2022-10-14 16:11:16 +00:00
f, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
2022-10-14 16:11:16 +00:00
}
offset := int64(i) * chunkSize
if _, err := f.Seek(offset, 0); err != nil {
return fmt.Errorf("failed to seek to chunk offset: %w", err)
2022-10-14 16:11:16 +00:00
}
bufferSize := chunkSize
if offset+chunkSize > stat.Size() {
bufferSize = stat.Size() - offset
}
chunk := make([]byte, bufferSize)
if _, err := f.Read(chunk); err != nil {
return fmt.Errorf("failed to read from chunk offset: %w", err)
2022-10-14 16:11:16 +00:00
}
operationBody, err := json.Marshal(map[string]interface{}{
"query": uploadVersionPartGQL,
"variables": map[string]interface{}{
"modId": modID,
"versionId": createdVersion.GetVersionID(),
"part": i + 1,
"file": nil,
},
})
if err != nil {
return fmt.Errorf("failed to serialize operation body: %w", err)
2022-10-14 16:11:16 +00:00
}
mapBody, err := json.Marshal(map[string]interface{}{
"0": []string{"variables.file"},
})
if err != nil {
return fmt.Errorf("failed to serialize map body: %w", err)
2022-10-14 16:11:16 +00:00
}
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
operations, err := writer.CreateFormField("operations")
if err != nil {
return fmt.Errorf("failed to create operations field: %w", err)
2022-10-14 16:11:16 +00:00
}
if _, err := operations.Write(operationBody); err != nil {
return fmt.Errorf("failed to write to operation field: %w", err)
2022-10-14 16:11:16 +00:00
}
mapField, err := writer.CreateFormField("map")
if err != nil {
return fmt.Errorf("failed to create map field: %w", err)
2022-10-14 16:11:16 +00:00
}
if _, err := mapField.Write(mapBody); err != nil {
return fmt.Errorf("failed to write to map field: %w", err)
2022-10-14 16:11:16 +00:00
}
part, err := writer.CreateFormFile("0", filepath.Base(filePath))
if err != nil {
return fmt.Errorf("failed to create file field: %w", err)
2022-10-14 16:11:16 +00:00
}
if _, err := io.Copy(part, bytes.NewReader(chunk)); err != nil {
return fmt.Errorf("failed to write to file field: %w", err)
2022-10-14 16:11:16 +00:00
}
if err := writer.Close(); err != nil {
return fmt.Errorf("failed to close body writer: %w", err)
2022-10-14 16:11:16 +00:00
}
r, _ := http.NewRequest("POST", viper.GetString("api-base")+viper.GetString("graphql-api"), body)
r.Header.Add("Content-Type", writer.FormDataContentType())
r.Header.Add("Authorization", viper.GetString("api-key"))
client := &http.Client{}
if _, err := client.Do(r); err != nil {
return fmt.Errorf("failed to execute request: %w", err)
2022-10-14 16:11:16 +00:00
}
}
logBase.Info("finalizing uploaded version")
2022-10-14 16:11:16 +00:00
finalizeSuccess, err := ficsit.FinalizeCreateVersion(cmd.Context(), global.APIClient, modID, createdVersion.GetVersionID(), ficsit.NewVersion{
Changelog: changelog,
Stability: versionStability,
})
if err != nil {
return err
}
if !finalizeSuccess.GetSuccess() {
logBase.Error("failed to finalize version upload")
2022-10-14 16:11:16 +00:00
}
time.Sleep(time.Second * 1)
for {
logBase.Info("checking version upload state")
2022-10-14 16:11:16 +00:00
state, err := ficsit.CheckVersionUploadState(cmd.Context(), global.APIClient, modID, createdVersion.GetVersionID())
if err != nil {
logBase.Error("failed to upload mod", slog.Any("err", err))
2022-10-14 16:11:16 +00:00
return nil
}
if state == nil || state.GetState().Version.Id == "" {
time.Sleep(time.Second * 10)
continue
}
if state.GetState().Auto_approved {
logBase.Info("version successfully uploaded and auto-approved")
2022-10-14 16:11:16 +00:00
break
}
logBase.Info("version successfully uploaded, but has to be scanned for viruses, which may take up to 15 minutes")
2022-10-14 16:11:16 +00:00
break
}
return nil
},
}
func init() {
uploadCmd.PersistentFlags().Int64("chunk-size", 10000000, "Size of chunks to split uploaded mod in bytes")
uploadCmd.PersistentFlags().String("stability", "release", "Stability of the uploaded mod (alpha, beta, release)")
_ = viper.BindPFlag("chunk-size", uploadCmd.PersistentFlags().Lookup("chunk-size"))
_ = viper.BindPFlag("stability", uploadCmd.PersistentFlags().Lookup("stability"))
}