From 08352f070151739a844f19eaeefb72b7917de2e1 Mon Sep 17 00:00:00 2001 From: sewn Date: Mon, 29 Apr 2024 08:31:05 +0300 Subject: [PATCH] refactor: split wasm into lib and cli, theming --- .gitignore | 5 +- Makefile | 11 +- README.md | 13 ++ cmd/wholesale/main.go | 273 ------------------------------------ go.mod | 10 +- go.sum | 36 +++-- internal/factory/factory.go | 178 +++++++++++++++++++++++ js.go | 116 +++++++++++++++ main.go | 46 ++++++ os.go | 43 ++++++ static/index.html | 24 +++- 11 files changed, 466 insertions(+), 289 deletions(-) create mode 100644 README.md delete mode 100644 cmd/wholesale/main.go create mode 100644 internal/factory/factory.go create mode 100644 js.go create mode 100644 main.go create mode 100644 os.go diff --git a/.gitignore b/.gitignore index ad48772..15a4ef6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -static/main.wasm -server +static/wholesale.wasm +wholesale +*.zip diff --git a/Makefile b/Makefile index c2f8db6..0957527 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,15 @@ +WHOLESALE_WASM = static/wholesale.wasm + wholesale: - GOOS=js GOARCH=wasm go build -o static/main.wasm ./cmd/wholesale + go build -o $@ + +$(WHOLESALE_WASM): + GOOS=js GOARCH=wasm go build -o $@ -server: wholesale +server: $(WHOLESALE_WASM) go build ./cmd/server run: server ./server -.PHONY: wholesale run server +.PHONY: wholesale run server $(WHOLESALE_WASM) diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef73cf2 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# wholesale + +Roblox deployment downloader written in Go using [rbxbin](https://github.com/apprehensions/rbxbin), with a CLI and a WASM frontend. + +### Usage (CLI) +``` +go install github.com/vinegarhq/wholesale@latest +PATH="$PATH:$(go env GOPATH)" +wholesale -guid version-24872f7beace4d0a +``` + +### Usage (Web) +Navigate to the WASM implementation at [wholesale.vinegarhq.org](https://wholesale.vinegarhq.org/), an example usage will be given. diff --git a/cmd/wholesale/main.go b/cmd/wholesale/main.go deleted file mode 100644 index 7fa5015..0000000 --- a/cmd/wholesale/main.go +++ /dev/null @@ -1,273 +0,0 @@ -package main - -import ( - "os" - "archive/zip" - "bytes" - "errors" - "flag" - "fmt" - "io" - "log" - "net/http" - "path" - "sort" - "strings" - "syscall/js" - "golang.org/x/sync/errgroup" - "time" - - "github.com/apprehensions/rbxbin" - cs "github.com/apprehensions/rbxweb/clientsettings" - _ "github.com/klauspost/compress/zip" -) - -var ( - guid string - binType string - channel string -) - -func init() { - -} - -type BinaryAssembler struct { - w *zip.Writer - d rbxbin.Deployment - m *rbxbin.Mirror - c js.Value -} - -type htmlLogWriter struct { - outputHTMLName string -} - -type htmlProgressWriter struct { - bar js.Value - max int64 - current int64 -} - -func newHtmlProgressWriter(max int64, id string) *htmlProgressWriter { - doc := js.Global().Get("document") - bar := doc.Call("createElement", "progress") - bar.Set("id", id) - bar.Set("class", "package_progress") - bar.Set("value", 0) - bar.Set("max", max) - doc.Get("body").Call("appendChild", bar) - - return &htmlProgressWriter{ - bar: bar, - max: max, - current: 0, - } -} - -func (hpw *htmlProgressWriter) Write(p []byte) (int, error) { - n := len(p) - hpw.current += int64(n) - go func() { - if hpw.current == hpw.max { - hpw.bar.Call("remove") - hpw = nil - return - } - hpw.bar.Set("value", hpw.current) - }() - return n, nil -} - -func (h htmlLogWriter) Write(p []byte) (n int, err error) { - doc := js.Global().Get("document") - out := js.Global().Get("document").Call("getElementById", h.outputHTMLName) - node := doc.Call("createTextNode", string(p)) - out.Call("appendChild", node) - return len(p), nil -} - -func main() { - log.SetOutput(&htmlLogWriter{outputHTMLName: "log"}) - log.SetFlags(0) - guid := flag.String("guid", "", "Roblox deployment GUID to retrieve") - channel := flag.String("channel", "", "Roblox deployment channel for the GUID") - bin := flag.String("type", "WindowsPlayer", "Roblox BinaryType for the GUID") - flag.Parse() - - if len(os.Args) < 2 { - log.Fatalf("%s\n%s", "usage: wholesale?guid=guid[&channel=channel][&type=binaryType]", - "example: wholesale?guid=version-1870963560174427&type=WindowsStudio64") - } - - var t cs.BinaryType - switch *bin { - case "WindowsPlayer": - t = cs.WindowsPlayer - case "WindowsStudio64": - t = cs.WindowsStudio64 - default: - log.Fatal("Unsupported binary type", binType, - "must be either WindowsPlayer or WindowsStudio64") - } - - d := rbxbin.Deployment{ - Type: t, - Channel: *channel, - GUID: *guid, - } - name := fmt.Sprintf("%s-%s-%s.zip", d.Channel, d.Type, d.GUID) - - buf := new(bytes.Buffer) - zw := zip.NewWriter(buf) - - cacheFly := rbxbin.Mirrors[1] - - ps, err := cacheFly.GetPackages(d) - if err != nil { - log.Fatal(err) - } - - sort.Slice(ps, func(i, j int) bool { - return ps[i].ZipSize < ps[j].ZipSize - }) - - ba := BinaryAssembler{ - w: zw, - d: d, - m: &cacheFly, - } - - cur := time.Now() - - if err := ba.Assemble(ps, &cacheFly); err != nil { - log.Fatal(err) - } - - if err := zw.Close(); err != nil { - log.Fatal(err) - } - - log.Printf("Took %s!", time.Now().Sub(cur)) - - // what a mess - b := buf.Bytes() - data := js.Global().Get("Uint8Array").New(len(b)) - js.CopyBytesToJS(data, b) - blob := js.Global().Get("Blob").New([]interface{}{data}, map[string]interface{}{"type": "application/zip"}) - url := js.Global().Get("URL").Call("createObjectURL", blob) - link := js.Global().Get("document").Call("createElement", "a") - link.Set("href", url) - link.Set("download", name) - button := js.Global().Get("document").Call("createElement", "button") - button.Set("innerText", "Redownload "+name) - link.Call("appendChild", button) - js.Global().Get("document").Get("body").Call("appendChild", link) -} - -type BinaryAssembleJob struct { - Dir string - Length int64 - Name string - Data []byte -} - -func (ba *BinaryAssembler) Assemble(pkgs []rbxbin.Package, mirror *rbxbin.Mirror) error { - dirs := rbxbin.BinaryDirectories(ba.d.Type) - eg := new(errgroup.Group) - bag := new(errgroup.Group) - baj := make(chan BinaryAssembleJob, len(pkgs)) - - bag.Go(func() error { - for j := range baj { - if err := ba.HandleJob(&j); err != nil { - return err - } - } - return nil - }) - - for _, p := range pkgs { - eg.Go(func() error { - if p.Name == "RobloxPlayerLauncher.exe" { - return nil - } - - dir, ok := dirs[p.Name] - if !ok { - log.Fatalf("unhandled package %s, was the correct binary type set?", p.Name) - } - url := ba.m.PackageURL(ba.d, p.Name) - - resp, err := http.Get(url) - if err != nil { - return err - } - - log.Println("Downloading", p.Name) - - if resp.ContentLength == 0 { - return errors.New("ContentLength is unknown") - } - - data, err := io.ReadAll(resp.Body) - if err != nil { - resp.Body.Close() - return fmt.Errorf("read: %w", err) - } - resp.Body.Close() - - baj <- BinaryAssembleJob{ - Dir: dir, - Name: p.Name, - Length: resp.ContentLength, - Data: data, - } - return nil - }) - } - - if err := eg.Wait(); err != nil { - return err - } - - close(baj) - - if err := bag.Wait(); err != nil { - return err - } - - return nil -} - -func (ba *BinaryAssembler) HandleJob(baj *BinaryAssembleJob) error { - zr, err := zip.NewReader(bytes.NewReader(baj.Data), int64(len(baj.Data))) - if err != nil { - return fmt.Errorf("zip reader: %w", err) - } - - log.Println("Extracting", baj.Name) - - for _, f := range zr.File { - rc, err := f.OpenRaw() - if err != nil { - return fmt.Errorf("open: %w", err) - } - - f.Name = path.Join(baj.Dir, strings.ReplaceAll(f.Name, `\`, "/")) - if f.FileInfo().IsDir() { - f.Name += "/" - } - - r, err := ba.w.CreateRaw(&f.FileHeader) - if err != nil { - return fmt.Errorf("create dest: %w", err) - } - - if _, err := io.Copy(r, rc); err != nil { - return fmt.Errorf("copy dest: %w", err) - } - } - - return nil -} diff --git a/go.mod b/go.mod index 1a948e7..817b7bb 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,19 @@ go 1.22.1 require ( github.com/apprehensions/rbxbin v0.0.0-20240407014006-bb26c002dffb github.com/apprehensions/rbxweb v0.0.0-20240329184049-0bdedc184942 + github.com/cheggaaa/pb/v3 v3.1.5 + github.com/dustin/go-humanize v1.0.1 github.com/klauspost/compress v1.17.8 - golang.org/x/sync v0.7.0 ) require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/robloxapi/rbxdhist v0.6.0 // indirect github.com/robloxapi/rbxver v0.3.0 // indirect + golang.org/x/sys v0.19.0 // indirect ) diff --git a/go.sum b/go.sum index 2338fad..29ebf21 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,38 @@ -github.com/apprehensions/rbxbin v0.0.0-20240402183954-152e1f392b95 h1:B+VNJO+96ukN8OxV9k35bQfi3GCyIuDruN+MPAWJTzg= -github.com/apprehensions/rbxbin v0.0.0-20240402183954-152e1f392b95/go.mod h1:xQ/kfERoO1h8tCGp8z6EafN4TdEERb0sRO5fM/3bhKo= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/apprehensions/rbxbin v0.0.0-20240407014006-bb26c002dffb h1:qwu/qBJJkK1R3z7+wNaWS9CZaXZbu06G7DgODIeeTDQ= github.com/apprehensions/rbxbin v0.0.0-20240407014006-bb26c002dffb/go.mod h1:FRJLfv2+HPYGcR7xP2VLG4O6QjkFCf05rBcdfUq1j3M= github.com/apprehensions/rbxweb v0.0.0-20240329184049-0bdedc184942 h1:pNRoIKlv329La+msdHmJSPYYf1y4hY4s5ou2mEQDHqU= github.com/apprehensions/rbxweb v0.0.0-20240329184049-0bdedc184942/go.mod h1:F7WKRLrQxuRgfXxhwnlFJ059ZBMRxkXxvIhUxP4Qc5g= -github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= -github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/cheggaaa/pb/v3 v3.1.5 h1:QuuUzeM2WsAqG2gMqtzaWithDJv0i+i6UlnwSCI4QLk= +github.com/cheggaaa/pb/v3 v3.1.5/go.mod h1:CrxkeghYTXi1lQBEI7jSn+3svI3cuc19haAj6jM60XI= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robloxapi/rbxdhist v0.6.0 h1:DH3hBwjgnJJyfuRQvJxkaX27V/EQP8/0KihW/9Ajo6Q= github.com/robloxapi/rbxdhist v0.6.0/go.mod h1:Dv8zmWSygz0Qc8SIRxasJ8wlAIchvyifEVHrG3qwNXI= github.com/robloxapi/rbxver v0.3.0 h1:ax3ndKtLiXNeYbGc56UEcPQYYMEr6heOfyRDfn+68lg= github.com/robloxapi/rbxver v0.3.0/go.mod h1:mpM7UdZ2YyLq4gSVtWToeRQLUHNBcOfRGRjINbOi5vM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/internal/factory/factory.go b/internal/factory/factory.go new file mode 100644 index 0000000..96f3624 --- /dev/null +++ b/internal/factory/factory.go @@ -0,0 +1,178 @@ +package factory + +import ( + "bytes" + "errors" + "fmt" + "io" + "log" + "net/http" + "path" + "sort" + "strings" + "sync" + "time" + + "github.com/apprehensions/rbxbin" + "github.com/klauspost/compress/zip" +) + +var mirror = rbxbin.Mirrors[1] // cachefly + +type Progress interface { + NewBar(int, string, io.Writer) io.Writer + Stop() error +} + +type BinaryAssembler struct { + d rbxbin.Deployment + p Progress +} + +type Package struct { + Dir string + Name string + Reader *bytes.Reader +} + +func NewBinaryAssembler(d rbxbin.Deployment, p Progress) *BinaryAssembler { + return &BinaryAssembler{ + d: d, + p: p, + } +} + +func (ba *BinaryAssembler) Run() *bytes.Buffer { + pkgs, err := mirror.GetPackages(ba.d) + if err != nil { + log.Fatal(err) + } + + sort.Slice(pkgs, func(i, j int) bool { + return pkgs[i].ZipSize < pkgs[j].ZipSize + }) + + cur := time.Now() + + buf := ba.Assemble(pkgs) + + log.Printf("Took %s!", time.Now().Sub(cur)) + return buf +} + +func (ba *BinaryAssembler) Assemble(pkgs []rbxbin.Package) *bytes.Buffer { + // Errors in this function are unrecoverable. + var eg, hg sync.WaitGroup + h := make(chan *Package, len(pkgs)) + dirs := rbxbin.BinaryDirectories(ba.d.Type) + + buf := new(bytes.Buffer) + w := zip.NewWriter(buf) + + // ZIP is designed to be written in sequential, so a handler + // goroutine with a channel is made to add to the zip concurrently, + // while downloading is done concurrently. + go func() { + hg.Add(1) + for j := range h { + err := ba.HandleJob(j, w) + if err != nil { + log.Fatal(err) + } + } + hg.Done() + }() + + eg.Add(len(pkgs)) + for _, p := range pkgs { + go func() { + defer eg.Done() + if p.Name == "RobloxPlayerLauncher.exe" { + return + } + + dir, ok := dirs[p.Name] + if !ok { + log.Fatalf("unhandled package %s, was the correct binary type set?", p.Name) + } + + j, err := ba.CreateJob(&p, dir) + if err != nil { + log.Fatal(err) + } + + h <- j + }() + } + + eg.Wait() + close(h) + hg.Wait() + + if err := w.Close(); err != nil { + log.Fatal(err) + } + + if err := ba.p.Stop(); err != nil { + log.Fatal(err) + } + + return buf +} + +func (ba *BinaryAssembler) CreateJob(pkg *rbxbin.Package, dir string) (*Package, error) { + url := mirror.PackageURL(ba.d, pkg.Name) + + resp, err := http.Get(url) + if err != nil { + return nil, err + } + + if resp.ContentLength < 0 { + return nil, errors.New("source ContentLength missing") + } + + b := new(bytes.Buffer) + r := ba.p.NewBar(int(resp.ContentLength), pkg.Name, b) + + _, err = io.Copy(r, resp.Body) + if err != nil { + log.Fatal(err) + } + + return &Package{ + Dir: dir, + Name: pkg.Name, + Reader: bytes.NewReader(b.Bytes()), + }, nil +} + +func (ba *BinaryAssembler) HandleJob(pkg *Package, w *zip.Writer) error { + zr, err := zip.NewReader(pkg.Reader, int64(pkg.Reader.Len())) + if err != nil { + return fmt.Errorf("zip reader: %w", err) + } + + for _, f := range zr.File { + rc, err := f.OpenRaw() + if err != nil { + return fmt.Errorf("open: %w", err) + } + + f.Name = path.Join(pkg.Dir, strings.ReplaceAll(f.Name, `\`, "/")) + if f.FileInfo().IsDir() { + f.Name += "/" + } + + r, err := w.CreateRaw(&f.FileHeader) + if err != nil { + return fmt.Errorf("create dest: %w", err) + } + + if _, err := io.Copy(r, rc); err != nil { + return fmt.Errorf("copy dest: %w", err) + } + } + + return nil +} diff --git a/js.go b/js.go new file mode 100644 index 0000000..8d9a1f9 --- /dev/null +++ b/js.go @@ -0,0 +1,116 @@ +//go:build js && wasm + +package main + +import ( + "bytes" + "io" + "log" + "sync" + "syscall/js" + + "github.com/dustin/go-humanize" +) + +type logWriter struct { + outputHTMLName string +} + +type coordinator struct { + bar js.Value + div js.Value + max int + current int + m sync.Mutex +} + +type humanResources struct{} + +func init() { + log.SetOutput(&logWriter{outputHTMLName: "log"}) +} + +func usage() { + log.Fatalf("%s\n%s", "usage: wholesale?guid=guid[&channel=channel][&type=binaryType]", + "example: wholesale?guid=version-1870963560174427&type=WindowsStudio64") +} + +func (l logWriter) Write(p []byte) (n int, err error) { + doc := js.Global().Get("document") + out := js.Global().Get("document").Call("getElementById", l.outputHTMLName) + node := doc.Call("createTextNode", string(p)) + out.Call("appendChild", node) + return len(p), nil +} + +func (hr *humanResources) NewBar(max int, id string, w io.Writer) io.Writer { + doc := js.Global().Get("document") + + bar := doc.Call("createElement", "progress") + bar.Set("id", id) + bar.Set("value", 0) + bar.Set("max", uint64(max)) + + label := doc.Call("createElement", "label") + label.Call("appendChild", doc.Call("createTextNode", + id+" ("+humanize.Bytes(uint64(max))+")")) + + div := doc.Call("createElement", "div") + div.Set("className", "package") + div.Call("appendChild", label) + div.Call("appendChild", bar) + + js.Global().Get("packages").Call("appendChild", div) + + return io.MultiWriter(w, &coordinator{ + div: div, + bar: bar, + max: max, + current: 0, + }) +} + +func (hr *humanResources) Stop() error { + return nil +} + +func (c *coordinator) Write(p []byte) (int, error) { + n := len(p) + c.current += n + go func() { + c.m.Lock() + defer c.m.Unlock() + // if c.current == c.max { + // c.div.Call("remove") + // c = nil + // return + // } + c.bar.Set("value", c.current) + }() + return n, nil +} + +func link(buf *bytes.Buffer, name string) { + b := buf.Bytes() + + data := js.Global().Get("Uint8Array").New(len(b)) + js.CopyBytesToJS(data, b) + blob := js.Global().Get("Blob").New( + []interface{}{data}, + map[string]interface{}{"type": "application/zip"}, + ) + + doc := js.Global().Get("document") + + button := doc.Call("createElement", "button") + button.Set("innerText", "Redownload "+name) + + url := js.Global().Get("URL").Call("createObjectURL", blob) + link := doc.Call("createElement", "a") + link.Set("href", url) + link.Set("download", name) + link.Call("appendChild", button) + + doc.Get("body").Call("appendChild", doc.Call("createElement", "hr")) + js.Global().Get("document").Get("body").Call("appendChild", link) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..14dc03d --- /dev/null +++ b/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + + "github.com/apprehensions/rbxbin" + cs "github.com/apprehensions/rbxweb/clientsettings" + "github.com/vinegarhq/wholesale/internal/factory" +) + +func main() { + guid := flag.String("guid", "", "Roblox deployment GUID to retrieve") + channel := flag.String("channel", "LIVE", "Roblox deployment channel for the GUID") + bin := flag.String("type", "WindowsPlayer", "Roblox BinaryType for the GUID") + flag.Parse() + + if len(os.Args) < 2 { + usage() + } + + var t cs.BinaryType + switch *bin { + case "WindowsPlayer": + t = cs.WindowsPlayer + case "WindowsStudio64": + t = cs.WindowsStudio64 + default: + log.Fatal("Unsupported binary type", *bin, + "must be either WindowsPlayer or WindowsStudio64") + } + + d := rbxbin.Deployment{ + Type: t, + Channel: *channel, + GUID: *guid, + } + + ba := factory.NewBinaryAssembler(d, &humanResources{}) + buf := ba.Run() + + link(buf, fmt.Sprintf( + "%s-%s-%s.zip", d.Type, d.Channel, d.GUID)) +} diff --git a/os.go b/os.go new file mode 100644 index 0000000..cadd665 --- /dev/null +++ b/os.go @@ -0,0 +1,43 @@ +//go:build !js && !wasm + +package main + +import ( + "bytes" + "io" + "log" + "os" + + "github.com/cheggaaa/pb/v3" +) + +func usage() { + log.Fatal("usage: wholesale -guid=guid [-channel channel] [-type binaryType]") +} + +func link(buf *bytes.Buffer, name string) { + err := os.WriteFile(name, buf.Bytes(), 0o644) + if err != nil { + log.Fatal(err) + } + + log.Printf("Exported at: %s", name) +} + +type humanResources struct { + *pb.Pool +} + +func (hr *humanResources) NewBar(max int, name string, r io.Writer) io.Writer { + bar := pb.Simple.New(max).Set("prefix", name) + if hr.Pool != nil { + hr.Pool.Add(bar) + } else { + pool, err := pb.StartPool(bar) + if err != nil { + log.Fatal(err) + } + hr.Pool = pool + } + return bar.NewProxyWriter(r) +} diff --git a/static/index.html b/static/index.html index 6ece643..1452f5f 100644 --- a/static/index.html +++ b/static/index.html @@ -2,10 +2,29 @@ +