bash script. Right now it removes the file if it doesn't match the checksum, but should probably do something like rename it or just output that it doesn't pass.
370 lines
9.4 KiB
Go
370 lines
9.4 KiB
Go
// Base code taken from: https://gist.github.com/marians/3b55318106df0e4e648158f1ffb43d38
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
// "fmt"
|
|
"log"
|
|
"net/http"
|
|
"time"
|
|
"encoding/json"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/webview/webview"
|
|
"golang.org/x/oauth2"
|
|
"io/ioutil"
|
|
"strconv"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"os"
|
|
)
|
|
|
|
var (
|
|
conf *oauth2.Config
|
|
ctx context.Context
|
|
w webview.WebView
|
|
client *http.Client
|
|
)
|
|
|
|
|
|
func saveToken(tok *oauth2.Token, filename string) {
|
|
j, err := json.MarshalIndent(tok, "", "\t")
|
|
if err != nil {
|
|
log.Fatalf("Failed to marshal token: %v", err)
|
|
}
|
|
err = ioutil.WriteFile(filename, j, 0600)
|
|
if err != nil {
|
|
log.Fatalf("Failed to save token: %v", err)
|
|
}
|
|
}
|
|
|
|
func loadToken(filename string) *oauth2.Token {
|
|
data, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var v = new(oauth2.Token)
|
|
err = json.Unmarshal(data, &v)
|
|
|
|
if err != nil {
|
|
log.Fatalf("Failed to unmarshal data: %v", err)
|
|
}
|
|
|
|
return v
|
|
}
|
|
|
|
func giveCode(code string) {
|
|
log.Printf("Code: %s", code)
|
|
// Exchange will do the handshake to retrieve the initial access token.
|
|
tok, err := conf.Exchange(ctx, code)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
log.Printf("Token: %s", tok)
|
|
saveToken(tok, "./.token")
|
|
// The HTTP Client returned by conf.Client will refresh the token as necessary.
|
|
client = conf.Client(ctx, tok)
|
|
|
|
_, err = client.Get("https://embed.gog.com/user/data/games")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
} else {
|
|
log.Println(color.CyanString("Authentication successful"))
|
|
}
|
|
|
|
w.Navigate(`data:text/html,
|
|
<!doctype html>
|
|
<html>
|
|
<body>
|
|
<p><strong>Success!</strong></p>
|
|
<p>You are authenticated and can now return to the CLI.</p>
|
|
</body>
|
|
</html>
|
|
`)
|
|
|
|
time.Sleep(1 * time.Second)
|
|
w.Terminate()
|
|
}
|
|
|
|
func openBrowser(url string) {
|
|
w = webview.New(true)
|
|
defer w.Destroy()
|
|
|
|
w.SetTitle("Login")
|
|
w.SetSize(600,600, webview.HintNone)
|
|
w.Navigate(url)
|
|
|
|
w.Bind("giveCode", giveCode)
|
|
w.Init(`
|
|
var params = new URLSearchParams(window.location.search);
|
|
if (params.has('code')) {
|
|
giveCode(params.get('code'));
|
|
}
|
|
`)
|
|
w.Run()
|
|
}
|
|
|
|
type GameList struct {
|
|
Owned []int `json:"owned"`
|
|
}
|
|
|
|
func getGameList() *GameList {
|
|
res, err := client.Get("https://embed.gog.com/user/data/games")
|
|
if err != nil {
|
|
log.Fatalf("Failed to get game list: %v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse game body: %v", err)
|
|
}
|
|
|
|
// log.Println(string(body))
|
|
var gl = new(GameList)
|
|
err = json.Unmarshal(body, gl)
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse game list response: %v", err)
|
|
}
|
|
|
|
return gl
|
|
}
|
|
|
|
type Product struct {
|
|
Id int `json:"id"`
|
|
Title string `json:"title"`
|
|
Downloads struct {
|
|
Installers [] struct {
|
|
Name string `json:"name"`
|
|
Os string `json:"os"`
|
|
Language string `json:"language"`
|
|
Files [] struct {
|
|
Id string `json:"id"`
|
|
Size int64 `json:"size"`
|
|
Downlink string `json:"downlink"`
|
|
} `json:"files"`
|
|
} `json:"installers"`
|
|
} `json:"downloads`
|
|
}
|
|
|
|
func getProduct(id int) *Product {
|
|
url := "https://api.gog.com/products/" + strconv.Itoa(id) + "?expand=downloads"
|
|
res, err := client.Get(url)
|
|
if err != nil {
|
|
log.Fatalf("Failed to get product info: %v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse product body: %v", err)
|
|
}
|
|
|
|
//log.Println(string(body))
|
|
var p = new(Product)
|
|
err = json.Unmarshal(body, p)
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse product response: %v", err)
|
|
}
|
|
|
|
return p
|
|
}
|
|
|
|
type DownloadLinks struct {
|
|
Downlink string `json:"downlink"`
|
|
Checksum string `json:"checksum"`
|
|
InstallerName string
|
|
Filename string
|
|
Md5 string
|
|
Os string
|
|
Language string
|
|
}
|
|
|
|
func (dl *DownloadLinks) String() string {
|
|
return fmt.Sprintf("{downlink: %s, checksum: %s}", dl.Downlink, dl.Checksum)
|
|
}
|
|
|
|
func getDownloadLinks(p *Product) []*DownloadLinks {
|
|
var links []*DownloadLinks
|
|
|
|
for _, i := range p.Downloads.Installers {
|
|
for _, f := range i.Files {
|
|
res, err := client.Get(f.Downlink)
|
|
if err != nil {
|
|
log.Fatalf("Failed to get file download info: %v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse file download body: %v", err)
|
|
}
|
|
|
|
//log.Println(string(body))
|
|
var dl = new(DownloadLinks)
|
|
err = json.Unmarshal(body, dl)
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse file download response: %v", err)
|
|
}
|
|
|
|
dl.InstallerName = sanitizeName(i.Name)
|
|
dl.Os = i.Os
|
|
dl.Language = i.Language
|
|
links = append(links, dl)
|
|
}
|
|
}
|
|
|
|
return links
|
|
}
|
|
|
|
func sanitizeName(name string) string {
|
|
// s := strings.Replace(name, " ", "_", -1)
|
|
// s = strings.Replace(s, "'", "", -1)
|
|
// s = strings.Replace(s, "™", "", -1)
|
|
re := regexp.MustCompile("[^0-9a-zA-Z_.-]+")
|
|
s := name
|
|
s = re.ReplaceAllString(s, "")
|
|
return s
|
|
}
|
|
|
|
|
|
func updateBashScriptProductFolder(p *Product, folder string, scriptName string, productNumber int, totalProducts int) {
|
|
|
|
f, err := os.OpenFile("./" + scriptName, os.O_APPEND|os.O_WRONLY, 0664)
|
|
if err != nil {
|
|
log.Fatalf("Failed to open the script file: %v", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
// Get a list of the OS's
|
|
var osList []string
|
|
for _, i := range p.Downloads.Installers {
|
|
exists := false
|
|
for _, o := range osList {
|
|
if o == sanitizeName(i.Os) + "/" + sanitizeName(i.Language) {
|
|
exists = true
|
|
break
|
|
}
|
|
}
|
|
if !exists {
|
|
osList = append(osList, sanitizeName(i.Os) + "/" + sanitizeName(i.Language))
|
|
}
|
|
}
|
|
|
|
// Print out the product
|
|
_, err = f.WriteString("echo \"[" + strconv.Itoa(productNumber) + " of " + strconv.Itoa(totalProducts) + "]Downloading files for: " + sanitizeName(p.Title) + "\"\n")
|
|
if err != nil {
|
|
log.Fatalf("Failed to update the script: %v", err)
|
|
}
|
|
|
|
// Create a folder for each os installers
|
|
for _, o := range osList {
|
|
_, err = f.WriteString("mkdir -p " + folder + "/" + sanitizeName(p.Title) + "/" + o + "\n")
|
|
if err != nil {
|
|
log.Fatalf("Failed to update the script: %v", err)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
func writeBashScript(dl *DownloadLinks, folder string, scriptName string, productNumber int, totalProducts int, fileNumber int, totalFiles int) {
|
|
// Find out file name and size; have to follow redirects
|
|
res, err := client.Head(dl.Downlink)
|
|
if err != nil {
|
|
log.Fatalf("Failed to do a head request for the download file: %v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
// log.Printf("head request url: %+v", res.Request.URL.Path)
|
|
|
|
pp := strings.Split(res.Request.URL.Path, "/")
|
|
dl.Filename = sanitizeName(pp[len(pp) - 1])
|
|
|
|
// Add the lines to the script
|
|
path := folder + "/" + sanitizeName(dl.InstallerName) + "/" + sanitizeName(dl.Os) + "/" + sanitizeName(dl.Language) + "/"
|
|
file := path + dl.Filename
|
|
line := "echo \"[" + strconv.Itoa(productNumber) + " of " + strconv.Itoa(totalProducts) + "]{" + strconv.Itoa(fileNumber) + "/" + strconv.Itoa(totalFiles) + "}Getting installers for (" + dl.InstallerName + ")" + "\"\n"
|
|
line += "wget --no-clobber --continue --quiet --show-progress -O \"" + file + ".xml\" \"" + dl.Checksum + "\"\n"
|
|
line += "wget --no-clobber --continue --quiet --show-progress -O \"" + file + "\" \"" + dl.Downlink + "\"\n"
|
|
line += "[ ! -f \"" + file + ".md5\" ] && cat \"" + file + ".xml\" | tr '\\n' '\\r' | sed -E 's|<file name=\"([^\"]+)\".*md5=\"([^\"]+).*|\\2 " + file + "\\n|' > " + file + ".md5\n"
|
|
line += "! md5sum -c \"" + file + ".md5\" && rm \"" + file + "\" && echo \"Removed unmatched file: " + file + "\"\n"
|
|
|
|
f, err := os.OpenFile("./" + scriptName, os.O_APPEND|os.O_WRONLY, 0664)
|
|
if err != nil {
|
|
log.Fatalf("Failed to open the script file: %v", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
_, err = f.WriteString(line)
|
|
if err != nil {
|
|
log.Fatalf("Failed to update the script: %v", err)
|
|
}
|
|
}
|
|
|
|
func login(url string) {
|
|
log.Println(color.CyanString("You will now be taken to your browser for authentication"))
|
|
time.Sleep(1 * time.Second)
|
|
openBrowser(url)
|
|
}
|
|
|
|
func main() {
|
|
productsFolder := "./products"
|
|
scriptName := "grab_stuff.sh"
|
|
ctx = context.Background()
|
|
conf = &oauth2.Config{
|
|
ClientID: "46899977096215655",
|
|
ClientSecret: "9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9",
|
|
Scopes: []string{},
|
|
Endpoint: oauth2.Endpoint{
|
|
AuthURL: "https://auth.gog.com/auth",
|
|
TokenURL: "https://auth.gog.com/token",
|
|
},
|
|
// my own callback URL
|
|
RedirectURL: "https://embed.gog.com/on_login_success?origin=client",
|
|
}
|
|
|
|
// add transport
|
|
tr := &http.Transport{
|
|
TLSClientConfig: &tls.Config{},
|
|
}
|
|
sslcli := &http.Client{Transport: tr}
|
|
ctx = context.WithValue(ctx, oauth2.HTTPClient, sslcli)
|
|
|
|
// Redirect user to consent page to ask for permission
|
|
// for the scopes specified above.
|
|
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
|
|
|
|
tok := loadToken("./.token")
|
|
if tok != nil {
|
|
client = conf.Client(ctx, tok)
|
|
|
|
_, err := client.Get("https://embed.gog.com/user/data/games")
|
|
if err != nil {
|
|
login(url)
|
|
} else {
|
|
log.Println(color.CyanString("Authentication successful"))
|
|
}
|
|
} else {
|
|
login(url)
|
|
}
|
|
|
|
gl := getGameList()
|
|
log.Println(gl.Owned)
|
|
ioutil.WriteFile("./" + scriptName, []byte("#!/bin/bash\n"), 0775)
|
|
|
|
for index, id := range gl.Owned {
|
|
log.Printf("Inspecting product: %d of %d - %d", index, len(gl.Owned), id)
|
|
p := getProduct(id)
|
|
dl := getDownloadLinks(p)
|
|
updateBashScriptProductFolder(p, productsFolder, scriptName, index+1, len(gl.Owned))
|
|
|
|
for idx, d := range dl {
|
|
//writeChecksumIfNeeded(d, productsFolder + "/" + sanitizeName(p.Title))
|
|
writeBashScript(d, productsFolder, scriptName, index+1, len(gl.Owned), idx+1, len(dl))
|
|
}
|
|
}
|
|
|
|
}
|
|
|