aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMitchell <mitch@riedstra.dev>2021-03-20 01:37:03 -0400
committerMitchell <mitch@riedstra.dev>2021-03-20 01:37:03 -0400
commitd047c36dd09b6169bf27c244196e99bb5df54c3a (patch)
tree5103e1951bd58bbef54c90a13c199e8d1d866492
parent0e62a3b46b25e7c101b14ed44235f3c276982fc0 (diff)
downloadsteam-export-d047c36dd09b6169bf27c244196e99bb5df54c3a.tar.gz
steam-export-d047c36dd09b6169bf27c244196e99bb5df54c3a.tar.xz
Update documentation. Remove all traces of chdir from the steam library. Remove most linter complaints.
-rw-r--r--cmd/web/app.go11
-rw-r--r--cmd/web/handlers.go14
-rw-r--r--cmd/web/install.go5
-rw-r--r--cmd/web/main.go11
-rw-r--r--cmd/web/util.go18
-rw-r--r--conf/steam-export-example.yml13
-rw-r--r--img/download.pngbin59591 -> 74217 bytes
-rw-r--r--img/filter.pngbin0 -> 53504 bytes
-rw-r--r--img/install_window.pngbin0 -> 80377 bytes
-rw-r--r--img/main.pngbin0 -> 72712 bytes
-rw-r--r--img/remote.pngbin59758 -> 0 bytes
-rw-r--r--readme.md11
-rw-r--r--steam/archive.go17
-rw-r--r--steam/filelisting.go26
-rw-r--r--steam/package.go31
-rw-r--r--steam/steam.go52
-rw-r--r--steam/update.go70
17 files changed, 100 insertions, 179 deletions
diff --git a/cmd/web/app.go b/cmd/web/app.go
index b0749b5..1050343 100644
--- a/cmd/web/app.go
+++ b/cmd/web/app.go
@@ -8,11 +8,13 @@ import (
"riedstra.dev/mitch/steam-export/steam"
)
+// steamLib is a steam libary with an embeded mutex to protect it
type steamLib struct {
steam.Library
sync.Mutex
}
+// statusInfo represents the internal status of game installation
type statusInfo struct {
sync.RWMutex
Running bool
@@ -23,6 +25,8 @@ type statusInfo struct {
Start *time.Time
}
+// App binds together the steam library, templates, and channel for install
+// requests as well as most the app specific http handlers.
type App struct {
Library *steamLib
Status *statusInfo
@@ -33,6 +37,8 @@ type App struct {
download chan string
}
+// NewApp sets up the steam library for us as well as parses the embedded
+// template
func NewApp(libPath string) (*App, error) {
lib, err := steam.NewLibrary(libPath)
if err != nil {
@@ -52,6 +58,9 @@ func NewApp(libPath string) (*App, error) {
return a, nil
}
+// LibrarySet takes care of locking the Library and switching over to a new
+// path, unlocking when done.
+// Errors will be logged and no changes will be made if unsuccessful.
func (a *App) LibrarySet(path string) {
Logger.Println("Starting library reload")
a.Library.Lock()
@@ -66,6 +75,8 @@ func (a *App) LibrarySet(path string) {
Logger.Println("Done reloading lib")
}
+// LibraryReload calls LibrarySet but with the current directory, forcing a reload of
+// information off of disk.
func (a *App) LibraryReload() {
cur := a.Library.Folder
a.LibrarySet(cur)
diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go
index b9b863a..661c411 100644
--- a/cmd/web/handlers.go
+++ b/cmd/web/handlers.go
@@ -14,6 +14,8 @@ import (
"riedstra.dev/mitch/steam-export/steam"
)
+// HandleIndex takes care of rendering our embedded template
+// and locks the steam library for each request.
func (a *App) HandleIndex(w http.ResponseWriter, r *http.Request) {
// During rendering of the template I believe it's
// mutating during the sort of keys, so Lib no longer
@@ -36,7 +38,7 @@ func (a *App) HandleIndex(w http.ResponseWriter, r *http.Request) {
&a.Library.Library,
a.Status,
isLocal(r.RemoteAddr),
- getHostIP(),
+ GetHostIP(),
getPort(),
Version,
})
@@ -46,6 +48,8 @@ func (a *App) HandleIndex(w http.ResponseWriter, r *http.Request) {
Logger.Printf("Client %s Index page", r.RemoteAddr)
}
+// HandleInstall takes the HTTP requests for installing a game from a URL
+// or local file path
func (a *App) HandleInstall(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
@@ -78,6 +82,7 @@ func (a *App) HandleInstall(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", 302)
}
+// HandleDownload takes care of exporting our games out to an HTTP request
func (a *App) HandleDownload(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
game := vars["game"]
@@ -129,6 +134,9 @@ func (a *App) HandleDownload(w http.ResponseWriter, r *http.Request) {
Logger.Printf("Client %s finished downloading: %s", r.RemoteAddr, game)
}
+// HandleDelete removes the game in question, though it doesn't
+// spawn any background processes it usually completes fast enough.
+// TODO: Fix if the request taking too long becomes an issue
func (a *App) HandleDelete(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
@@ -166,6 +174,8 @@ func (a *App) HandleDelete(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", 302)
}
+// HandleStats dumps out some internal statistics of installation which
+// is then parsed by some JS for a progress bar and such
func (a *App) HandleStats(w http.ResponseWriter, r *http.Request) {
a.Status.RLock()
defer a.Status.RUnlock()
@@ -181,6 +191,7 @@ func (a *App) HandleStats(w http.ResponseWriter, r *http.Request) {
return
}
+// HandleSetLib sets a new library path
func (a *App) HandleSetLib(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
@@ -194,6 +205,7 @@ func (a *App) HandleSetLib(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", 302)
}
+// HandleQuit just calls os.Exit after finishing the request
func HandleQuit(w http.ResponseWriter, r *http.Request) {
Logger.Println("Quit was called, exiting")
w.Header().Add("Content-type", "text/plain")
diff --git a/cmd/web/install.go b/cmd/web/install.go
index dc85a89..e59a5c5 100644
--- a/cmd/web/install.go
+++ b/cmd/web/install.go
@@ -10,7 +10,7 @@ import (
"time"
)
-func (a *App) installHttp(u string) error {
+func (a *App) installHTTP(u string) error {
Logger.Println("Installer: loading from url")
resp, err := http.Get(u)
if err != nil {
@@ -97,7 +97,7 @@ func (a *App) installer() {
a.Status.Unlock()
if strings.HasPrefix(u, "http") {
- err = a.installHttp(u)
+ err = a.installHTTP(u)
} else {
err = a.installPath(u)
}
@@ -111,4 +111,3 @@ func (a *App) installer() {
a.LibraryReload()
}
}
-
diff --git a/cmd/web/main.go b/cmd/web/main.go
index da0bebd..7475478 100644
--- a/cmd/web/main.go
+++ b/cmd/web/main.go
@@ -14,9 +14,12 @@ import (
)
var (
+ // Version is overridden by the build script
Version = "Development"
- Logger = log.New(os.Stderr, "", log.LstdFlags)
- Listen = ":8899"
+ // Logger default logging to stderr
+ Logger = log.New(os.Stderr, "", log.LstdFlags)
+ // Listen address for the webserver
+ Listen = ":8899"
//go:embed static/*
embeddedStatic embed.FS
@@ -48,7 +51,7 @@ func main() {
r.Handle("/setLib", UnauthorizedIfNotLocal(http.HandlerFunc(a.HandleSetLib)))
r.Handle("/delete", UnauthorizedIfNotLocal(http.HandlerFunc(a.HandleDelete)))
r.Handle("/install", UnauthorizedIfNotLocal(http.HandlerFunc(a.HandleInstall)))
- r.HandleFunc("/steam-export-web.exe", serveSelf)
+ r.HandleFunc("/steam-export-web.exe", ServeSelf)
r.HandleFunc("/download/{game}", a.HandleDownload)
r.Handle("/status", UnauthorizedIfNotLocal(http.HandlerFunc(a.HandleStats)))
r.PathPrefix("/static").Handler(
@@ -72,7 +75,7 @@ func main() {
if err != nil {
panic(err)
}
- port += 1
+ port++
Listen = fmt.Sprintf("%s:%d", parts[0], port)
diff --git a/cmd/web/util.go b/cmd/web/util.go
index 1252c66..2c32922 100644
--- a/cmd/web/util.go
+++ b/cmd/web/util.go
@@ -2,19 +2,21 @@ package main
import (
"io"
- "os"
- "net/http"
"net"
+ "net/http"
+ "os"
"strings"
)
+// UnauthorizedIfNotLocal is a middleware that returns unauthorized if not
+// being accessed from loopback, as a basic form of host authentication.
func UnauthorizedIfNotLocal(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !isLocal(r.RemoteAddr) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
Logger.Printf("Unauthorized request from: %s for %s",
r.RemoteAddr, r.RequestURI)
- return
+ return
}
h.ServeHTTP(w, r)
})
@@ -25,10 +27,10 @@ func isLocal(addr string) bool {
return localNet.Contains(net.ParseIP(strings.Split(addr, ":")[0]))
}
-// getHostIP attempts to guess the IP address of the current machine and
+// GetHostIP attempts to guess the IP address of the current machine and
// returns that. Simply bails at the first non sane looking IP and returns it.
// Not ideal but it should work well enough most of the time
-func getHostIP() string {
+func GetHostIP() string {
iFaces, err := net.Interfaces()
if err != nil {
return "127.0.0.1"
@@ -40,7 +42,7 @@ func getHostIP() string {
for _, iFace := range iFaces {
addrs, err := iFace.Addrs()
if err != nil {
- return "127.0.0.1"
+ continue
}
for _, a := range addrs {
@@ -68,7 +70,9 @@ func getPort() string {
return s[1]
}
-func serveSelf(w http.ResponseWriter, r *http.Request) {
+// ServeSelf tries to locate the currently running executable and serve
+// it down to the client.
+func ServeSelf(w http.ResponseWriter, r *http.Request) {
s, err := os.Executable()
if err != nil {
Logger.Println("While trying to get my executable path: ", err)
diff --git a/conf/steam-export-example.yml b/conf/steam-export-example.yml
deleted file mode 100644
index fd81fb7..0000000
--- a/conf/steam-export-example.yml
+++ /dev/null
@@ -1,13 +0,0 @@
----
-listen: 0.0.0.0:9413
-# Repositories that you want to expose on the web server
-SteamRepositories:
- # We're ideally looking for the full path to the steamapps folder
- # On windows you're going to escape the slashes
- - "C:\\Program Files (x86)\\Steam\\steamapps"
- # - "/usr/mitch/SteamGames/steamapps"
-
-# This defaults to:
-# Only change this if your default Steam library is different than the Steam default
-# DefaultRepository: "/usr/mitch/SteamGames/steamapps"
-# DefaultRepository: "Z:\\SteamGames\\steamapps"
diff --git a/img/download.png b/img/download.png
index dfc6cd4..2516fa8 100644
--- a/img/download.png
+++ b/img/download.png
Binary files differ
diff --git a/img/filter.png b/img/filter.png
new file mode 100644
index 0000000..ac4309e
--- /dev/null
+++ b/img/filter.png
Binary files differ
diff --git a/img/install_window.png b/img/install_window.png
new file mode 100644
index 0000000..be378bf
--- /dev/null
+++ b/img/install_window.png
Binary files differ
diff --git a/img/main.png b/img/main.png
new file mode 100644
index 0000000..8908dc8
--- /dev/null
+++ b/img/main.png
Binary files differ
diff --git a/img/remote.png b/img/remote.png
deleted file mode 100644
index 8613b4a..0000000
--- a/img/remote.png
+++ /dev/null
Binary files differ
diff --git a/readme.md b/readme.md
index 7d972c0..f0c1129 100644
--- a/readme.md
+++ b/readme.md
@@ -25,12 +25,21 @@ https://git.riedstra.dev/mitch/steam-export/plain/bin/?h=binaries)
`steam-export-web-windows-amd64.exe` is probably the one of interest to most
people.
+Windows "smart screen" may complain about the binaries since I have not
+signed them in any way. I don't currently have any plans to remedy this
+since it's a niche utility. I may PGP sign a set of hashes in the future
+if the additional security is warranted.
+
The server started when accessed remotely also allows people to download
a copy of the application to easily install games from.
Screenshots:
-![remote view](../plain/img/remote.png)
+![main view](../plain/img/main.png)
+
+![remote filtered view](../plain/img/filter.png)
+
+![install window](../plain/img/install_window.png)
![download view](../plain/img/download.png)
diff --git a/steam/archive.go b/steam/archive.go
index d997051..296defa 100644
--- a/steam/archive.go
+++ b/steam/archive.go
@@ -2,13 +2,14 @@ package steam
import (
"archive/tar"
+ "fmt"
"io"
"os"
"path/filepath"
"strings"
)
-func TarWalkfn(writer *tar.Writer) filepath.WalkFunc {
+func tarWalkfn(writer *tar.Writer, prefix string) filepath.WalkFunc {
return func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
@@ -24,13 +25,16 @@ func TarWalkfn(writer *tar.Writer) filepath.WalkFunc {
}
defer f.Close()
- // Convert Windows paths to Unix paths
- path = strings.Replace(path, "\\", "/", -1)
+ // Let's strip out all of the leading parts of the path
+ // to create a tarball realative to the root of the steam library
+ tarPth := strings.TrimPrefix(path, prefix)
+ tarPth = strings.ReplaceAll(tarPth, "\\", "/")
+ tarPth = strings.TrimPrefix(tarPth, "/")
// TODO; See if tar.FileInfoheader() could be used instead
// without the pathing issues I encountered
h := &tar.Header{
- Name: path,
+ Name: tarPth,
Size: info.Size(),
// I don't like it... but it helps with platform compatibility
Mode: 0664,
@@ -44,10 +48,7 @@ func TarWalkfn(writer *tar.Writer) filepath.WalkFunc {
_, err = io.Copy(writer, f)
if err != nil {
- // TODO: Figure out how to add more useful information to
- // These errors
- // fmt.Fprintln(os.Stderr, f.Name())
- return err
+ return fmt.Errorf("While copying %s: %w", path, err)
}
return nil
diff --git a/steam/filelisting.go b/steam/filelisting.go
deleted file mode 100644
index f94319e..0000000
--- a/steam/filelisting.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package steam
-
-import (
- "path/filepath"
- "os"
-)
-
-func fileListing(pth string) (map[string]struct{}, error) {
- out := map[string]struct{}{}
- err := filepath.Walk(pth, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return err
- }
-
- if info.Mode().IsRegular() {
- out[path] = struct{}{}
- }
-
- return nil
- })
- if err != nil {
- return nil, err
- }
-
- return out, nil
-}
diff --git a/steam/package.go b/steam/package.go
index 7287e10..38b5819 100644
--- a/steam/package.go
+++ b/steam/package.go
@@ -10,9 +10,6 @@ import (
// Package writes the package, returning bytes written and an error if any
func (g *Game) Package(wr io.WriteCloser) error {
- if err := os.Chdir(g.LibraryPath); err != nil {
- return err
- }
acf, err := FindACF(g.LibraryPath, g.Name)
if err != nil {
return err
@@ -20,8 +17,13 @@ func (g *Game) Package(wr io.WriteCloser) error {
twriter := tar.NewWriter(wr)
- for _, pth := range []string{"common/" + g.Name, acf} {
- if err := filepath.Walk(pth, TarWalkfn(twriter)); err != nil {
+ paths := []string{
+ filepath.Join(g.LibraryPath, "common", g.Name),
+ acf,
+ }
+ for _, pth := range paths {
+ err := filepath.Walk(pth, tarWalkfn(twriter, g.LibraryPath))
+ if err != nil {
return err
}
}
@@ -36,11 +38,9 @@ func (g *Game) Package(wr io.WriteCloser) error {
return wr.Close()
}
+// Extract will read a tarball from the io.Reader and install the game into
+// the current library path
func (l *Library) Extract(r io.Reader) error {
- if err := os.Chdir(l.Folder); err != nil {
- return err
- }
-
treader := tar.NewReader(r)
for {
@@ -56,6 +56,8 @@ func (l *Library) Extract(r io.Reader) error {
// Fix windows slashes...
fileName := strings.Replace(hdr.Name, "\\", "/", -1)
+ fileName = filepath.Join(l.Folder, fileName)
+
info := hdr.FileInfo()
if info.IsDir() {
// I don't like hard-coded permissions but it
@@ -85,11 +87,8 @@ func (l *Library) Extract(r io.Reader) error {
return nil
}
+// Delete removes all of the game files and the ACF
func (g *Game) Delete() error {
- if err := os.Chdir(g.LibraryPath); err != nil {
- return err
- }
-
acf, err := FindACF(g.LibraryPath, g.Name)
if err != nil {
return err
@@ -97,14 +96,16 @@ func (g *Game) Delete() error {
if err := os.Remove(acf); err != nil {
return err
}
- if err := os.RemoveAll("common/" + g.Name); err != nil {
+
+ err = os.RemoveAll(filepath.Join(g.LibraryPath, "common", g.Name))
+ if err != nil {
return err
}
return nil
}
-// Get the Size of a game in a pretty format
+// GetSize returns the size of a game in a pretty format
func (g Game) GetSize() string {
return formatBytes(g.Size)
}
diff --git a/steam/steam.go b/steam/steam.go
index aca20df..ff0ce5a 100644
--- a/steam/steam.go
+++ b/steam/steam.go
@@ -1,10 +1,10 @@
-// This is designed to be a rather simplistic library to help pull information
-// from a local steam library
+// Package steam is designed to be a rather simplistic library to help pull
+// information from a local steam library and run basic operations like
+// archiving, restore and deleting games
package steam
import (
"bufio"
- "errors"
"fmt"
"io/ioutil"
"os"
@@ -13,11 +13,15 @@ import (
"strings"
)
+// Library is used to represent the steam library, the Games map is populated
+// by NewLibrary when called or when ProcessLibrary is called directly
type Library struct {
Folder string
Games map[string]Game
}
+// Game represents an actual game in the steam Library, allowing you to perform
+// a delete, package, and such.
type Game struct {
Name string
LibraryPath string
@@ -26,24 +30,14 @@ type Game struct {
var slugregexp = regexp.MustCompile(`[^-0-9A-Za-z_:.]`)
+// Slug returns a safer version of the name with spaces and other chars
+// transformed into dashes for use in HTML element ids and such.
func (g Game) Slug() string {
- // return strings.ReplaceAll(g.Name, " ", "-")
return slugregexp.ReplaceAllString(g.Name, "-")
}
-func ProcessMultipleLibraries(r []string) ([]*Library, error) {
- var libs []*Library
- for _, i := range r {
- lib := &Library{}
- err := lib.ProcessLibrary(i)
- if err != nil {
- return nil, err
- }
- libs = append(libs, lib)
- }
- return libs, nil
-}
-
+// NewLibrary returns a pointer to a processed library and an error
+// if any
func NewLibrary(path string) (*Library, error) {
l := &Library{}
err := l.ProcessLibrary(path)
@@ -53,6 +47,8 @@ func NewLibrary(path string) (*Library, error) {
return l, err
}
+// NewLibraryMust is the same as NewLibrary but calls panic if there
+// is any error
func NewLibraryMust(path string) *Library {
l, err := NewLibrary(path)
if err != nil {
@@ -61,10 +57,11 @@ func NewLibraryMust(path string) *Library {
return l
}
-// Populate the "Folder" and "Games" fields based on the provided directory
+// ProcessLibrary Populates the "Folder" and "Games" fields based on the
+// provided directory.
func (s *Library) ProcessLibrary(r string) error {
if !hasCommon(r) {
- return errors.New(fmt.Sprintf("No common directory in: %s", r))
+ return fmt.Errorf("No common directory in: %s", r)
}
s.Games = make(map[string]Game)
@@ -89,12 +86,9 @@ func (s *Library) ProcessLibrary(r string) error {
return nil
}
-// Find the ACF files related to this video game
+// FindACF will return the filename of the ACF file for a given `game`
func FindACF(libraryPath, game string) (string, error) {
- if err := os.Chdir(libraryPath); err != nil {
- return "", err
- }
- files, err := filepath.Glob("*.acf")
+ files, err := filepath.Glob(filepath.Join(libraryPath, "*.acf"))
if err != nil {
return "", err
}
@@ -110,26 +104,22 @@ func FindACF(libraryPath, game string) (string, error) {
// Open up the file
f, err := os.Open(fn)
- defer f.Close()
if err != nil {
return "", err
}
+ defer f.Close()
+
scanner := bufio.NewScanner(f)
for scanner.Scan() {
- // Finally check and see if the file has the video game name
if strings.Contains(scanner.Text(), game) {
return fn, nil
- // fmt.Printf("%s/%s:%d: %s\n", root, path, i, scanner.Text())
}
}
}
- str := "Couldn't find ACF file related to Game: %s"
- return "", errors.New(fmt.Sprintf(str, game))
+ return "", fmt.Errorf("Couldn't find ACF file related to Game: %s", game)
}
-// This is automatically called to print out the contents of the struct
-// when things like fmt.Println are used
func (s *Library) String() (str string) {
str = fmt.Sprintf("Library: %s\n", s.Folder)
str = str + "----\n"
diff --git a/steam/update.go b/steam/update.go
deleted file mode 100644
index b3e621e..0000000
--- a/steam/update.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package steam
-
-import (
- "archive/tar"
- "io"
- "os"
- "strings"
- "path/filepath"
-)
-
-func (l *Library) ExtractUpdate(r io.Reader) error {
- if err := os.Chdir(l.Folder); err != nil {
- return err
- }
-
- // withinArchive := map[string]struct{}{}
- // onDisk, err := fileListing(
-
- treader := tar.NewReader(r)
-
- for {
- hdr, err := treader.Next()
- if err == io.EOF {
- // We've reached the end! Whoee
- break
- }
- if err != nil {
- return err
- }
-
- // Fix windows slashes...
- fileName := strings.Replace(hdr.Name, "\\", "/", -1)
-
- info := hdr.FileInfo()
- if info.IsDir() {
- // I don't like hard-coded permissions but it
- // it helps with overall platform compatibility
- if err = os.MkdirAll(fileName, 0775); err != nil {
- return err
- }
- continue
- }
-
- if err = os.MkdirAll(filepath.Dir(fileName), 0775); err != nil {
- return err
- }
-
- fi, err := os.Stat(fileName)
- if !os.IsNotExist(err) {
- // If the file in the archive is not newer, skip
- if !info.ModTime().After(fi.ModTime()) {
- continue
- }
- }
-
- // Create a file handle to work with
- f, err := os.OpenFile(fileName, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0664)
- if err != nil {
- return err
- }
- if _, err := io.Copy(f, treader); err != nil {
- return err
- }
- f.Close()
-
- }
-
- return nil
-}
-